1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-29 13:49:28 +08:00
Dušan Borovčanin d6755e4a72 MF-374 - Bring back CoAP adapter (#413)
* Bring old CoAP code back

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix channel ID formatting due to type change

Uncomment error handling for authorization.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update CoAP adapter docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add copyright headers

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove redundant type declaration

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add CoAP adapter to the list of services

Add CoAp adapter in Makefile services list and fix corresponding documentation.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor CoAP code

Merge multipe `const` block int single and declare consts before vars.
Un-export notFound handler since there is no need to export it.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update http version endpoint

This separates CoAP and HTTP APIs.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor CoAP POST method handling

This PR is a part of CoAP adapter refactoring that will simplify adapter implementation.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor CoAP adapter

Change CoAP message handling to simplify adapter implementation.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add backoff timeout for server ping to client

Update CoAP adapter to provide subset of necessary features from
protocol specification.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix leaking locked goroutine

In case of the stopped ticker, its channel is NOT closed, so pinging might be left stuck waiting for the stopped ticker to send a notification.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Format code

Use more meaningful name for Handlers map.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use and stop ticker from the same goroutine

Stop handler Ticker from ping goroutine rather than the cancel goroutine.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Check if subscription already exists in put method

Fix potential leak of handlers providing check inside of put method.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use MessageID as Observe option

Since MessageID satisfies observe option behaviour, use Message ID
instead of local timestamp. Remove Thicker from handler and use it on
transport layer.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use name Observer insted of Handler

Name `Observer` is used in protocol specification, so this naming makes
code more self-documenting.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add CoAP adapter to docker-compose.yml

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add copyright headers

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove unused constants

Fix service name in startup log message.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add metrics endpoint

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor code

Config fields from main.go should not be exported; minor style changes.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update authorization URI-Query option

Use `authorization` value in URI-Query option instead of `key`. This
mimics Authorization header in some other protocols (e.g. HTTP). Please
note that this value can be replaced with simple `auth` to save space,
due to constrained URI-Query option size.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2018-10-31 18:53:25 +01:00

642 lines
16 KiB
Go

package coap
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"reflect"
"sort"
"strings"
)
// COAPType represents the message type.
type COAPType uint8
const (
// Confirmable messages require acknowledgements.
Confirmable COAPType = 0
// NonConfirmable messages do not require acknowledgements.
NonConfirmable COAPType = 1
// Acknowledgement is a message indicating a response to confirmable message.
Acknowledgement COAPType = 2
// Reset indicates a permanent negative acknowledgement.
Reset COAPType = 3
)
var typeNames = [256]string{
Confirmable: "Confirmable",
NonConfirmable: "NonConfirmable",
Acknowledgement: "Acknowledgement",
Reset: "Reset",
}
func init() {
for i := range typeNames {
if typeNames[i] == "" {
typeNames[i] = fmt.Sprintf("Unknown (0x%x)", i)
}
}
}
func (t COAPType) String() string {
return typeNames[t]
}
// COAPCode is the type used for both request and response codes.
type COAPCode uint8
// Request Codes
const (
GET COAPCode = 1
POST COAPCode = 2
PUT COAPCode = 3
DELETE COAPCode = 4
)
// Response Codes
const (
Created COAPCode = 65
Deleted COAPCode = 66
Valid COAPCode = 67
Changed COAPCode = 68
Content COAPCode = 69
BadRequest COAPCode = 128
Unauthorized COAPCode = 129
BadOption COAPCode = 130
Forbidden COAPCode = 131
NotFound COAPCode = 132
MethodNotAllowed COAPCode = 133
NotAcceptable COAPCode = 134
PreconditionFailed COAPCode = 140
RequestEntityTooLarge COAPCode = 141
UnsupportedMediaType COAPCode = 143
InternalServerError COAPCode = 160
NotImplemented COAPCode = 161
BadGateway COAPCode = 162
ServiceUnavailable COAPCode = 163
GatewayTimeout COAPCode = 164
ProxyingNotSupported COAPCode = 165
)
var codeNames = [256]string{
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE",
Created: "Created",
Deleted: "Deleted",
Valid: "Valid",
Changed: "Changed",
Content: "Content",
BadRequest: "BadRequest",
Unauthorized: "Unauthorized",
BadOption: "BadOption",
Forbidden: "Forbidden",
NotFound: "NotFound",
MethodNotAllowed: "MethodNotAllowed",
NotAcceptable: "NotAcceptable",
PreconditionFailed: "PreconditionFailed",
RequestEntityTooLarge: "RequestEntityTooLarge",
UnsupportedMediaType: "UnsupportedMediaType",
InternalServerError: "InternalServerError",
NotImplemented: "NotImplemented",
BadGateway: "BadGateway",
ServiceUnavailable: "ServiceUnavailable",
GatewayTimeout: "GatewayTimeout",
ProxyingNotSupported: "ProxyingNotSupported",
}
func init() {
for i := range codeNames {
if codeNames[i] == "" {
codeNames[i] = fmt.Sprintf("Unknown (0x%x)", i)
}
}
}
func (c COAPCode) String() string {
return codeNames[c]
}
// Message encoding errors.
var (
ErrInvalidTokenLen = errors.New("invalid token length")
ErrOptionTooLong = errors.New("option is too long")
ErrOptionGapTooLarge = errors.New("option gap too large")
)
// OptionID identifies an option in a message.
type OptionID uint8
/*
+-----+----+---+---+---+----------------+--------+--------+---------+
| No. | C | U | N | R | Name | Format | Length | Default |
+-----+----+---+---+---+----------------+--------+--------+---------+
| 1 | x | | | x | If-Match | opaque | 0-8 | (none) |
| 3 | x | x | - | | Uri-Host | string | 1-255 | (see |
| | | | | | | | | below) |
| 4 | | | | x | ETag | opaque | 1-8 | (none) |
| 5 | x | | | | If-None-Match | empty | 0 | (none) |
| 7 | x | x | - | | Uri-Port | uint | 0-2 | (see |
| | | | | | | | | below) |
| 8 | | | | x | Location-Path | string | 0-255 | (none) |
| 11 | x | x | - | x | Uri-Path | string | 0-255 | (none) |
| 12 | | | | | Content-Format | uint | 0-2 | (none) |
| 14 | | x | - | | Max-Age | uint | 0-4 | 60 |
| 15 | x | x | - | x | Uri-Query | string | 0-255 | (none) |
| 17 | x | | | | Accept | uint | 0-2 | (none) |
| 20 | | | | x | Location-Query | string | 0-255 | (none) |
| 35 | x | x | - | | Proxy-Uri | string | 1-1034 | (none) |
| 39 | x | x | - | | Proxy-Scheme | string | 1-255 | (none) |
| 60 | | | x | | Size1 | uint | 0-4 | (none) |
+-----+----+---+---+---+----------------+--------+--------+---------+
*/
// Option IDs.
const (
IfMatch OptionID = 1
URIHost OptionID = 3
ETag OptionID = 4
IfNoneMatch OptionID = 5
Observe OptionID = 6
URIPort OptionID = 7
LocationPath OptionID = 8
URIPath OptionID = 11
ContentFormat OptionID = 12
MaxAge OptionID = 14
URIQuery OptionID = 15
Accept OptionID = 17
LocationQuery OptionID = 20
ProxyURI OptionID = 35
ProxyScheme OptionID = 39
Size1 OptionID = 60
)
// Option value format (RFC7252 section 3.2)
type valueFormat uint8
const (
valueUnknown valueFormat = iota
valueEmpty
valueOpaque
valueUint
valueString
)
type optionDef struct {
valueFormat valueFormat
minLen int
maxLen int
}
var optionDefs = [256]optionDef{
IfMatch: optionDef{valueFormat: valueOpaque, minLen: 0, maxLen: 8},
URIHost: optionDef{valueFormat: valueString, minLen: 1, maxLen: 255},
ETag: optionDef{valueFormat: valueOpaque, minLen: 1, maxLen: 8},
IfNoneMatch: optionDef{valueFormat: valueEmpty, minLen: 0, maxLen: 0},
Observe: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 3},
URIPort: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 2},
LocationPath: optionDef{valueFormat: valueString, minLen: 0, maxLen: 255},
URIPath: optionDef{valueFormat: valueString, minLen: 0, maxLen: 255},
ContentFormat: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 2},
MaxAge: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 4},
URIQuery: optionDef{valueFormat: valueString, minLen: 0, maxLen: 255},
Accept: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 2},
LocationQuery: optionDef{valueFormat: valueString, minLen: 0, maxLen: 255},
ProxyURI: optionDef{valueFormat: valueString, minLen: 1, maxLen: 1034},
ProxyScheme: optionDef{valueFormat: valueString, minLen: 1, maxLen: 255},
Size1: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 4},
}
// MediaType specifies the content type of a message.
type MediaType byte
// Content types.
const (
TextPlain MediaType = 0 // text/plain;charset=utf-8
AppLinkFormat MediaType = 40 // application/link-format
AppXML MediaType = 41 // application/xml
AppOctets MediaType = 42 // application/octet-stream
AppExi MediaType = 47 // application/exi
AppJSON MediaType = 50 // application/json
)
type option struct {
ID OptionID
Value interface{}
}
func encodeInt(v uint32) []byte {
switch {
case v == 0:
return nil
case v < 256:
return []byte{byte(v)}
case v < 65536:
rv := []byte{0, 0}
binary.BigEndian.PutUint16(rv, uint16(v))
return rv
case v < 16777216:
rv := []byte{0, 0, 0, 0}
binary.BigEndian.PutUint32(rv, uint32(v))
return rv[1:]
default:
rv := []byte{0, 0, 0, 0}
binary.BigEndian.PutUint32(rv, uint32(v))
return rv
}
}
func decodeInt(b []byte) uint32 {
tmp := []byte{0, 0, 0, 0}
copy(tmp[4-len(b):], b)
return binary.BigEndian.Uint32(tmp)
}
func (o option) toBytes() []byte {
var v uint32
switch i := o.Value.(type) {
case string:
return []byte(i)
case []byte:
return i
case MediaType:
v = uint32(i)
case int:
v = uint32(i)
case int32:
v = uint32(i)
case uint:
v = uint32(i)
case uint32:
v = i
default:
panic(fmt.Errorf("invalid type for option %x: %T (%v)",
o.ID, o.Value, o.Value))
}
return encodeInt(v)
}
func parseOptionValue(optionID OptionID, valueBuf []byte) interface{} {
def := optionDefs[optionID]
if def.valueFormat == valueUnknown {
// Skip unrecognized options (RFC7252 section 5.4.1)
return nil
}
if len(valueBuf) < def.minLen || len(valueBuf) > def.maxLen {
// Skip options with illegal value length (RFC7252 section 5.4.3)
return nil
}
switch def.valueFormat {
case valueUint:
intValue := decodeInt(valueBuf)
if optionID == ContentFormat || optionID == Accept {
return MediaType(intValue)
} else {
return intValue
}
case valueString:
return string(valueBuf)
case valueOpaque, valueEmpty:
return valueBuf
}
// Skip unrecognized options (should never be reached)
return nil
}
type options []option
func (o options) Len() int {
return len(o)
}
func (o options) Less(i, j int) bool {
if o[i].ID == o[j].ID {
return i < j
}
return o[i].ID < o[j].ID
}
func (o options) Swap(i, j int) {
o[i], o[j] = o[j], o[i]
}
func (o options) Minus(oid OptionID) options {
rv := options{}
for _, opt := range o {
if opt.ID != oid {
rv = append(rv, opt)
}
}
return rv
}
// Message is a CoAP message.
type Message struct {
Type COAPType
Code COAPCode
MessageID uint16
Token, Payload []byte
opts options
}
// IsConfirmable returns true if this message is confirmable.
func (m Message) IsConfirmable() bool {
return m.Type == Confirmable
}
// Options gets all the values for the given option.
func (m Message) Options(o OptionID) []interface{} {
var rv []interface{}
for _, v := range m.opts {
if o == v.ID {
rv = append(rv, v.Value)
}
}
return rv
}
// Option gets the first value for the given option ID.
func (m Message) Option(o OptionID) interface{} {
for _, v := range m.opts {
if o == v.ID {
return v.Value
}
}
return nil
}
func (m Message) optionStrings(o OptionID) []string {
var rv []string
for _, o := range m.Options(o) {
rv = append(rv, o.(string))
}
return rv
}
// Path gets the Path set on this message if any.
func (m Message) Path() []string {
return m.optionStrings(URIPath)
}
// PathString gets a path as a / separated string.
func (m Message) PathString() string {
return strings.Join(m.Path(), "/")
}
// SetPathString sets a path by a / separated string.
func (m *Message) SetPathString(s string) {
for s[0] == '/' {
s = s[1:]
}
m.SetPath(strings.Split(s, "/"))
}
// SetPath updates or adds a URIPath attribute on this message.
func (m *Message) SetPath(s []string) {
m.SetOption(URIPath, s)
}
// RemoveOption removes all references to an option
func (m *Message) RemoveOption(opID OptionID) {
m.opts = m.opts.Minus(opID)
}
// AddOption adds an option.
func (m *Message) AddOption(opID OptionID, val interface{}) {
iv := reflect.ValueOf(val)
if (iv.Kind() == reflect.Slice || iv.Kind() == reflect.Array) &&
iv.Type().Elem().Kind() == reflect.String {
for i := 0; i < iv.Len(); i++ {
m.opts = append(m.opts, option{opID, iv.Index(i).Interface()})
}
return
}
m.opts = append(m.opts, option{opID, val})
}
// SetOption sets an option, discarding any previous value
func (m *Message) SetOption(opID OptionID, val interface{}) {
m.RemoveOption(opID)
m.AddOption(opID, val)
}
const (
extoptByteCode = 13
extoptByteAddend = 13
extoptWordCode = 14
extoptWordAddend = 269
extoptError = 15
)
// MarshalBinary produces the binary form of this Message.
func (m *Message) MarshalBinary() ([]byte, error) {
tmpbuf := []byte{0, 0}
binary.BigEndian.PutUint16(tmpbuf, m.MessageID)
/*
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Ver| T | TKL | Code | Message ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Token (if any, TKL bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 1 1 1 1 1 1 1| Payload (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
buf := bytes.Buffer{}
buf.Write([]byte{
(1 << 6) | (uint8(m.Type) << 4) | uint8(0xf&len(m.Token)),
byte(m.Code),
tmpbuf[0], tmpbuf[1],
})
buf.Write(m.Token)
/*
0 1 2 3 4 5 6 7
+---------------+---------------+
| | |
| Option Delta | Option Length | 1 byte
| | |
+---------------+---------------+
\ \
/ Option Delta / 0-2 bytes
\ (extended) \
+-------------------------------+
\ \
/ Option Length / 0-2 bytes
\ (extended) \
+-------------------------------+
\ \
/ /
\ \
/ Option Value / 0 or more bytes
\ \
/ /
\ \
+-------------------------------+
See parseExtOption(), extendOption()
and writeOptionHeader() below for implementation details
*/
extendOpt := func(opt int) (int, int) {
ext := 0
if opt >= extoptByteAddend {
if opt >= extoptWordAddend {
ext = opt - extoptWordAddend
opt = extoptWordCode
} else {
ext = opt - extoptByteAddend
opt = extoptByteCode
}
}
return opt, ext
}
writeOptHeader := func(delta, length int) {
d, dx := extendOpt(delta)
l, lx := extendOpt(length)
buf.WriteByte(byte(d<<4) | byte(l))
tmp := []byte{0, 0}
writeExt := func(opt, ext int) {
switch opt {
case extoptByteCode:
buf.WriteByte(byte(ext))
case extoptWordCode:
binary.BigEndian.PutUint16(tmp, uint16(ext))
buf.Write(tmp)
}
}
writeExt(d, dx)
writeExt(l, lx)
}
sort.Stable(&m.opts)
prev := 0
for _, o := range m.opts {
b := o.toBytes()
writeOptHeader(int(o.ID)-prev, len(b))
buf.Write(b)
prev = int(o.ID)
}
if len(m.Payload) > 0 {
buf.Write([]byte{0xff})
}
buf.Write(m.Payload)
return buf.Bytes(), nil
}
// ParseMessage extracts the Message from the given input.
func ParseMessage(data []byte) (Message, error) {
rv := Message{}
return rv, rv.UnmarshalBinary(data)
}
// UnmarshalBinary parses the given binary slice as a Message.
func (m *Message) UnmarshalBinary(data []byte) error {
if len(data) < 4 {
return errors.New("short packet")
}
if data[0]>>6 != 1 {
return errors.New("invalid version")
}
m.Type = COAPType((data[0] >> 4) & 0x3)
tokenLen := int(data[0] & 0xf)
if tokenLen > 8 {
return ErrInvalidTokenLen
}
m.Code = COAPCode(data[1])
m.MessageID = binary.BigEndian.Uint16(data[2:4])
if tokenLen > 0 {
m.Token = make([]byte, tokenLen)
}
if len(data) < 4+tokenLen {
return errors.New("truncated")
}
copy(m.Token, data[4:4+tokenLen])
b := data[4+tokenLen:]
prev := 0
parseExtOpt := func(opt int) (int, error) {
switch opt {
case extoptByteCode:
if len(b) < 1 {
return -1, errors.New("truncated")
}
opt = int(b[0]) + extoptByteAddend
b = b[1:]
case extoptWordCode:
if len(b) < 2 {
return -1, errors.New("truncated")
}
opt = int(binary.BigEndian.Uint16(b[:2])) + extoptWordAddend
b = b[2:]
}
return opt, nil
}
for len(b) > 0 {
if b[0] == 0xff {
b = b[1:]
break
}
delta := int(b[0] >> 4)
length := int(b[0] & 0x0f)
if delta == extoptError || length == extoptError {
return errors.New("unexpected extended option marker")
}
b = b[1:]
delta, err := parseExtOpt(delta)
if err != nil {
return err
}
length, err = parseExtOpt(length)
if err != nil {
return err
}
if len(b) < length {
return errors.New("truncated")
}
oid := OptionID(prev + delta)
opval := parseOptionValue(oid, b[:length])
b = b[length:]
prev = int(oid)
if opval != nil {
m.opts = append(m.opts, option{ID: oid, Value: opval})
}
}
m.Payload = b
return nil
}