Skip to content

Commit

Permalink
implements CoAP
Browse files Browse the repository at this point in the history
  • Loading branch information
RicYaben committed Jul 17, 2024
1 parent 571bf96 commit bfa7b6d
Show file tree
Hide file tree
Showing 9 changed files with 755 additions and 2 deletions.
9 changes: 9 additions & 0 deletions modules/coap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package modules

import (
"github.com/zmap/zgrab2/modules/coap"
)

func init() {
coap.RegisterModule()
}
168 changes: 168 additions & 0 deletions modules/coap/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package coap

import (
"bytes"
"net"
"slices"
"time"

"github.com/zmap/zgrab2"
"github.com/zmap/zgrab2/modules/coap/message"
)

type Result struct {
path string
messages []*message.Message
}

type Probe struct {
header []byte
timeout time.Duration
results []*Result
decoder message.Decoder
conn net.Conn
}

func (p *Probe) Do(path string) *zgrab2.ScanError {
u := p.getUriPath(path)
pkt := slices.Concat(p.header, u)
msgs, err := p.handle(pkt)
if err != nil {
return err
}

res := &Result{
path: path,
messages: msgs,
}
p.results = append(p.results, res)
return nil
}

func (p *Probe) handle(packet []byte) ([]*message.Message, *zgrab2.ScanError) {
msgs := []*message.Message{}
for b := 0; ; b++ {
msg, err := p.handleBlock(packet, b)
if err != nil {
return nil, err
}
msgs = append(msgs, msg)
block := msg.GetBlock()
if block == nil || !block.More {
return msgs, nil
}
}
}

func (p *Probe) handleBlock(packet []byte, block int) (*message.Message, *zgrab2.ScanError) {
if b := p.getBlock(block); len(b) > 0 {
packet = append(packet, b...)
}

if err := p.conn.SetReadDeadline(time.Now().Add(p.timeout)); err != nil {
zgrab2.NewScanError(zgrab2.SCAN_UNKNOWN_ERROR, err)
}

if _, err := p.conn.Write(packet); err != nil {
return nil, zgrab2.DetectScanError(err)
}

buf := make([]byte, 1024)
n, err := p.conn.Read(buf)
if err != nil {
return nil, zgrab2.DetectScanError(err)
}

msg := message.NewMessage()
if err := p.decoder.Decode(buf[:n], msg); err != nil {
return nil, zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, err)
}
return msg, nil
}

func (p *Probe) getUriPath(path string) []byte {
const uriOption int = 11

if path == "" {
panic("empty path not allowed")
}

if path == "/" {
// Option delta and length for an empty path segment
option := (uriOption << 4)
return []byte{byte(option)}
}

// Transform to bytes and separate by the URI separator
var buf bytes.Buffer
paths := bytes.Split([]byte(path), []byte("/"))

// Include option delta number, length and value
option := (uriOption << 4) + len(paths[0])
buf.WriteByte(byte(option))
buf.Write(paths[0])

// Extend the option with length and value
for _, p := range paths[1:] {
buf.WriteByte(byte(len(p)))
buf.Write(p)
}
return buf.Bytes()
}

func (p *Probe) getBlock(n int) []byte {
if n < 1 {
return []byte{}
}

var buf bytes.Buffer
b := (12 << 4) + 1 // 193
c := (n << 4) + 3
buf.WriteByte(byte(b))
buf.WriteByte(byte(c))
return buf.Bytes()
}

type ProbeBuilder struct {
decoder message.Decoder
header []byte
timeout time.Duration
}

func (b *ProbeBuilder) Build(conn net.Conn) *Probe {
p := &Probe{
header: b.header,
decoder: b.decoder,
timeout: b.timeout,
conn: conn,
results: []*Result{},
}
return p
}

func (b *ProbeBuilder) setHeader() *ProbeBuilder {
b.header = []byte{
0x40, // CoAP version and type (version: 1, type: Confirmable)
0x01, // CoAP code (GET)
0x12, 0x34, // Message ID (0x0001)
}
return b
}

func (b *ProbeBuilder) setDecoder() *ProbeBuilder {
b.decoder = message.NewDecoder()
return b
}

func (b *ProbeBuilder) setTimeout(t time.Duration) *ProbeBuilder {
b.timeout = t
return b
}

func newProbeBuilder(timeout time.Duration) *ProbeBuilder {
b := new(ProbeBuilder)
b.setHeader()
b.setDecoder()
b.setTimeout(timeout)
return b
}
68 changes: 68 additions & 0 deletions modules/coap/coap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package coap

import (
"testing"
"time"

"github.com/zmap/zgrab2"
)

type coapTester struct {
port int
expectedStatus zgrab2.ScanStatus
}

func (t *coapTester) getScanner() (*Scanner, error) {
var module Module
flags := module.NewFlags().(*Flags)
flags.Port = uint(t.port)

flags.Paths = "\".well-known/core\",\"/\""
flags.PathsDelimiter = ","
flags.Timeout = 10 * time.Second
flags.Port = uint(t.port)

scanner := module.NewScanner()
if err := scanner.Init(flags); err != nil {
return nil, err
}

return scanner.(*Scanner), nil
}

func (t *coapTester) runTest(test *testing.T, name string) {
scanner, err := t.getScanner()
if err != nil {
test.Fatalf("[%s] Unexpected error: %v", name, err)
}

target := zgrab2.ScanTarget{
Domain: "coap.me",
}

status, ret, err := scanner.Scan(target)
if status != t.expectedStatus {
test.Errorf("[%s] Wrong status: expected %s, got %s", name, t.expectedStatus, status)
}

if err != nil {
test.Errorf("[%s] Unexpected error: %v", name, err)
}

if ret == nil {
test.Errorf("[%s] Got empty response", name)
}
}

var tests = map[string]*coapTester{
"success": {
port: 5683,
expectedStatus: zgrab2.SCAN_SUCCESS,
},
}

func TestCoAP(t *testing.T) {
for tname, cfg := range tests {
cfg.runTest(t, tname)
}
}
69 changes: 69 additions & 0 deletions modules/coap/message/decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package message

import (
"bytes"
"errors"
)

const (
OptionBlock1 = 27 // CoAP Block1 Option Number
OptionBlock2 = 23 // CoAP Block2 Option Number
OptionContentFormat = 12 // CoAP Content-Format Option Number
)

var (
ErrMalformedMessage = errors.New("malformed message")
ErrInvalidValue = errors.New("invalid value")
ErrSmallBuffer = errors.New("buffer too small")
)

type Decoder interface {
Decode([]byte, *Message) error
}

type decoder struct{}

func NewDecoder() Decoder {
return &decoder{}
}

func (d *decoder) Decode(data []byte, msg *Message) error {
var buf = new(bytes.Buffer)
buf.Write(data)

var header = new(Header)
if err := header.Unmarshal(buf); err != nil {
return err
}
msg.Header = header

var opts = make(Options)
if err := opts.Unmarshal(buf); err != nil {
return err
}
msg.Options = opts

var payload = new(Payload)
cType := uint16(opts[OptionContentFormat].Value[0])
if err := payload.Unmarshal(buf, cType); err != nil {
return err
}
msg.Payload = payload

for _, oID := range []uint16{OptionBlock1, OptionBlock2} {
if opt, ok := opts[oID]; ok {
msg.block = d.makeBlock(opt)
break
}
}

return nil
}

func (d *decoder) makeBlock(opt *Option) *Block {
v := int(opt.Value[0])
return &Block{
Number: v >> 4,
More: v&0x08 > 0,
}
}
69 changes: 69 additions & 0 deletions modules/coap/message/message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package message

import (
"bytes"
"fmt"
)

type Block struct {
Number int
More bool
}

type Header struct {
MessageID int `json:"message-id"`
Version int `json:"version"`
Type int `json:"type"`
TokenLen int `json:"token-len"`
Token []byte `json:"token"`
Code string `json:"code"`
}

func NewHeader() *Header {
return &Header{
MessageID: -1,
Type: -1,
}
}

func (h *Header) Unmarshal(buf *bytes.Buffer) error {
d := make([]byte, 4)
if _, err := buf.Read(d); err != nil {
return err
}

h.Version = int(d[0] >> 6)
h.Type = int((d[0] >> 4) & 0x03)
h.TokenLen = int(d[0] & 0x0F)
h.Code = h.getCode(d[1])
h.MessageID = int(d[2])<<8 | int(d[3])
h.Token = make([]byte, h.TokenLen)
_, err := buf.Read(h.Token)
return err
}

func (h *Header) getCode(v byte) string {
// Extract class and detail from the hex value
class := int(v >> 5) // upper 3 bits
detail := int(v & 0x1F) // lower 5 bits
return fmt.Sprintf("%d.%02d", class, detail)
}

type Message struct {
Header *Header `json:"header"`
Payload *Payload `json:"payload"`
Options Options `json:"options"`

block *Block
}

func NewMessage() *Message {
return &Message{
Header: NewHeader(),
Options: make(Options),
}
}

func (m *Message) GetBlock() *Block {
return m.block
}
Loading

0 comments on commit bfa7b6d

Please sign in to comment.