├── .gitignore ├── .gitmodules ├── Godeps └── Godeps.json ├── LICENSE ├── README.md ├── bytecontent.go ├── canopus.go ├── client.go ├── conn.go ├── corelink.go ├── coreresource.go ├── coreresource_test.go ├── dtls.go ├── empty.go ├── event.go ├── event_test.go ├── examples ├── block1 │ ├── client.go │ ├── ietf-block.htm │ └── server.go ├── discovery │ └── main.go ├── dtls │ └── simple-psk │ │ ├── client.go │ │ └── server.go ├── observe │ ├── client.go │ └── server.go ├── proxy │ ├── coap │ │ ├── client.go │ │ ├── proxy.go │ │ └── server.go │ └── http │ │ ├── client.go │ │ └── server.go └── simple │ ├── client.go │ └── server.go ├── init.go ├── json.go ├── message.go ├── message_test.go ├── options.go ├── plaintext.go ├── proxy.go ├── request.go ├── request_test.go ├── response.go ├── response_test.go ├── routes.go ├── routes_test.go ├── server.go ├── server_test.go ├── serverconn.go ├── session.go ├── test-coverage.sh ├── types.go ├── utilconn_test.go ├── utildebug.go ├── utils.go ├── utils_test.go └── xml.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | Godeps/_workspace 26 | Readme 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "openssl"] 2 | path = openssl 3 | url = https://github.com/openssl/openssl.git 4 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/zubairhamed/canopus", 3 | "GoVersion": "go1.7", 4 | "Deps": [ 5 | { 6 | "ImportPath": "github.com/davecgh/go-spew/spew", 7 | "Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d" 8 | }, 9 | { 10 | "ImportPath": "github.com/pmezard/go-difflib/difflib", 11 | "Rev": "d8ed2627bdf02c080bf22230dbb337003b7aba2d" 12 | }, 13 | { 14 | "ImportPath": "github.com/stretchr/testify/assert", 15 | "Comment": "v1.0-80-g67106a5", 16 | "Rev": "67106a5111a06241c8d84952c33214675f51a34a" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015, Zubair Hamed 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Canopus 2 | 3 | [![GoDoc](https://godoc.org/github.com/zubairhamed/canopus?status.svg)](https://godoc.org/github.com/zubairhamed/canopus) 4 | [![Build Status](https://drone.io/github.com/zubairhamed/canopus/status.png?)](https://drone.io/github.com/zubairhamed/canopus/latest) 5 | [![Coverage Status](https://coveralls.io/repos/zubairhamed/canopus/badge.svg?branch=master)](https://coveralls.io/r/zubairhamed/canopus?branch=master) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/zubairhamed/canopus)](https://goreportcard.com/report/github.com/zubairhamed/canopus) 7 | 8 | #### Canopus is a client/server implementation of the [Constrained Application Protocol (CoAP)][RFC7252] 9 | [RFC7252]: http://tools.ietf.org/html/rfc7252 10 | 11 | ## Updates 12 | #### 25.11.2016 13 | I've added basic dTLS Support based on [Julien Vermillard's][JVERMILLARD] [implementation][NATIVEDTLS]. Thanks Julien! It should now support PSK-based authentication. 14 | I've also gone ahead and refactored the APIs to make it that bit more Go idiomatic. 15 | [JVERMILLARD]: https://github.com/jvermillard 16 | [NATIVEDTLS]: https://github.com/jvermillard/nativedtls 17 | 18 | ## Building and running 19 | 1. git submodule update --init --recursive 20 | 2. cd openssl 21 | 3. ./config && make 22 | 4. You should then be able to run the examples in the /examples folder 23 | 24 | #### Simple Example 25 | ```go 26 | // Server 27 | // See /examples/simple/server/main.go 28 | server := canopus.NewServer() 29 | 30 | server.Get("/hello", func(req canopus.Request) canopus.Response { 31 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 32 | msg.SetStringPayload("Acknowledged: " + req.GetMessage().GetPayload().String()) 33 | 34 | res := canopus.NewResponse(msg, nil) 35 | return res 36 | }) 37 | 38 | server.ListenAndServe(":5683") 39 | 40 | // Client 41 | // See /examples/simple/client/main.go 42 | conn, err := canopus.Dial("localhost:5683") 43 | if err != nil { 44 | panic(err.Error()) 45 | } 46 | 47 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Get, canopus.GenerateMessageID()).(*canopus.CoapRequest) 48 | req.SetStringPayload("Hello, canopus") 49 | req.SetRequestURI("/hello") 50 | 51 | resp, err := conn.Send(req) 52 | if err != nil { 53 | panic(err.Error()) 54 | } 55 | 56 | fmt.Println("Got Response:" + resp.GetMessage().GetPayload().String()) 57 | ``` 58 | 59 | #### Observe / Notify 60 | ```go 61 | // Server 62 | // See /examples/observe/server/main.go 63 | server := canopus.NewServer() 64 | server.Get("/watch/this", func(req canopus.Request) canopus.Response { 65 | msg := canopus.NewMessageOfType(canopus.MessageAcknowledgment, req.GetMessage().GetMessageId(), canopus.NewPlainTextPayload("Acknowledged")) 66 | res := canopus.NewResponse(msg, nil) 67 | 68 | return res 69 | }) 70 | 71 | ticker := time.NewTicker(3 * time.Second) 72 | go func() { 73 | for { 74 | select { 75 | case <-ticker.C: 76 | changeVal := strconv.Itoa(rand.Int()) 77 | fmt.Println("[SERVER << ] Change of value -->", changeVal) 78 | 79 | server.NotifyChange("/watch/this", changeVal, false) 80 | } 81 | } 82 | }() 83 | 84 | server.OnObserve(func(resource string, msg canopus.Message) { 85 | fmt.Println("[SERVER << ] Observe Requested for " + resource) 86 | }) 87 | 88 | server.ListenAndServe(":5683") 89 | 90 | // Client 91 | // See /examples/observe/client/main.go 92 | conn, err := canopus.Dial("localhost:5683") 93 | 94 | tok, err := conn.ObserveResource("/watch/this") 95 | if err != nil { 96 | panic(err.Error()) 97 | } 98 | 99 | obsChannel := make(chan canopus.ObserveMessage) 100 | done := make(chan bool) 101 | go conn.Observe(obsChannel) 102 | 103 | notifyCount := 0 104 | for { 105 | select { 106 | case obsMsg, _ := <-obsChannel: 107 | if notifyCount == 5 { 108 | fmt.Println("[CLIENT >> ] Canceling observe after 5 notifications..") 109 | go conn.CancelObserveResource("watch/this", tok) 110 | go conn.StopObserve(obsChannel) 111 | return 112 | } else { 113 | notifyCount++ 114 | // msg := obsMsg.Msg\ 115 | resource := obsMsg.GetResource() 116 | val := obsMsg.GetValue() 117 | 118 | fmt.Println("[CLIENT >> ] Got Change Notification for resource and value: ", notifyCount, resource, val) 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | ### dTLS with PSK 125 | ```go 126 | // Server 127 | // See /examples/dtls/simple-psk/server/main.go 128 | server := canopus.NewServer() 129 | 130 | server.Get("/hello", func(req canopus.Request) canopus.Response { 131 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 132 | msg.SetStringPayload("Acknowledged: " + req.GetMessage().GetPayload().String()) 133 | res := canopus.NewResponse(msg, nil) 134 | 135 | return res 136 | }) 137 | 138 | server.HandlePSK(func(id string) []byte { 139 | return []byte("secretPSK") 140 | }) 141 | 142 | server.ListenAndServeDTLS(":5684") 143 | 144 | // Client 145 | // See /examples/dtls/simple-psk/client/main.go 146 | conn, err := canopus.DialDTLS("localhost:5684", "canopus", "secretPSK") 147 | if err != nil { 148 | panic(err.Error()) 149 | } 150 | 151 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Get, canopus.GenerateMessageID()) 152 | req.SetStringPayload("Hello, canopus") 153 | req.SetRequestURI("/hello") 154 | 155 | resp, err := conn.Send(req) 156 | if err != nil { 157 | panic(err.Error()) 158 | } 159 | 160 | fmt.Println("Got Response:" + resp.GetMessage().GetPayload().String()) 161 | ``` 162 | 163 | #### CoAP-CoAP Proxy 164 | ```go 165 | // Server 166 | // See /examples/proxy/coap/server/main.go 167 | server := canopus.NewServer() 168 | 169 | server.Get("/proxycall", func(req canopus.Request) canopus.Response { 170 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 171 | msg.SetStringPayload("Data from :5685 -- " + req.GetMessage().GetPayload().String()) 172 | res := canopus.NewResponse(msg, nil) 173 | 174 | return res 175 | }) 176 | server.ListenAndServe(":5685") 177 | 178 | // Proxy Server 179 | // See /examples/proxy/coap/proxy/main.go 180 | server := canopus.NewServer() 181 | server.ProxyOverCoap(true) 182 | 183 | server.Get("/proxycall", func(req canopus.Request) canopus.Response { 184 | canopus.PrintMessage(req.GetMessage()) 185 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 186 | msg.SetStringPayload("Acknowledged: " + req.GetMessage().GetPayload().String()) 187 | res := canopus.NewResponse(msg, nil) 188 | 189 | return res 190 | }) 191 | server.ListenAndServe(":5683") 192 | 193 | // Client 194 | // See /examples/proxy/coap/client/main.go 195 | conn, err := canopus.Dial("localhost:5683") 196 | if err != nil { 197 | panic(err.Error()) 198 | } 199 | 200 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Get, canopus.GenerateMessageID()) 201 | req.SetProxyURI("coap://localhost:5685/proxycall") 202 | 203 | resp, err := conn.Send(req) 204 | if err != nil { 205 | println("err", err) 206 | } 207 | canopus.PrintMessage(resp.GetMessage()) 208 | ``` 209 | 210 | #### CoAP-HTTP Proxy 211 | ```go 212 | // Server 213 | // See /examples/proxy/http/server/main.go 214 | server := canopus.NewServer() 215 | server.ProxyOverHttp(true) 216 | 217 | server.ListenAndServe(":5683") 218 | 219 | // Client 220 | // See /examples/proxy/http/client/main.go 221 | conn, err := canopus.Dial("localhost:5683") 222 | if err != nil { 223 | panic(err.Error()) 224 | } 225 | 226 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Get, canopus.GenerateMessageID()) 227 | req.SetProxyURI("https://httpbin.org/get") 228 | 229 | resp, err := conn.Send(req) 230 | if err != nil { 231 | println("err", err) 232 | } 233 | canopus.PrintMessage(resp.GetMessage()) 234 | ``` 235 | -------------------------------------------------------------------------------- /bytecontent.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | // Represents a message payload containing an array of bytes 4 | func NewBytesPayload(v []byte) MessagePayload { 5 | return &BytesPayload{ 6 | content: v, 7 | } 8 | } 9 | 10 | type BytesPayload struct { 11 | content []byte 12 | } 13 | 14 | func (p *BytesPayload) GetBytes() []byte { 15 | return p.content 16 | } 17 | 18 | func (p *BytesPayload) Length() int { 19 | return len(p.content) 20 | } 21 | 22 | func (p *BytesPayload) String() string { 23 | return string(p.content) 24 | } 25 | -------------------------------------------------------------------------------- /canopus.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "errors" 5 | "math/rand" 6 | "net" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // CurrentMessageID stores the current message id used/generated for messages 12 | var CurrentMessageID = 0 13 | var MESSAGEID_MUTEX *sync.Mutex 14 | 15 | func init() { 16 | rand.Seed(time.Now().UTC().UnixNano()) 17 | 18 | CurrentMessageID = rand.Intn(65535) 19 | 20 | MESSAGEID_MUTEX = &sync.Mutex{} 21 | } 22 | 23 | var GENERATE_ID uint16 = 0 24 | 25 | const UDP = "udp" 26 | 27 | // Types of Messages 28 | const ( 29 | MessageConfirmable = 0 30 | MessageNonConfirmable = 1 31 | MessageAcknowledgment = 2 32 | MessageReset = 3 33 | ) 34 | 35 | // Fragments/parts of a CoAP Message packet 36 | const ( 37 | DataHeader = 0 38 | DataCode = 1 39 | DataMsgIDStart = 2 40 | DataMsgIDEnd = 4 41 | DataTokenStart = 4 42 | ) 43 | 44 | // OptionCode type represents a valid CoAP Option Code 45 | type OptionCode int 46 | 47 | const ( 48 | // OptionIfMatch request-header field is used with a method to make it conditional. 49 | // A client that has one or more entities previously obtained from the resource can verify 50 | // that one of those entities is current by including a list of their associated entity tags 51 | // in the If-Match header field. 52 | OptionIfMatch OptionCode = 1 53 | 54 | OptionURIHost OptionCode = 3 55 | OptionEtag OptionCode = 4 56 | OptionIfNoneMatch OptionCode = 5 57 | OptionObserve OptionCode = 6 58 | OptionURIPort OptionCode = 7 59 | OptionLocationPath OptionCode = 8 60 | OptionURIPath OptionCode = 11 61 | OptionContentFormat OptionCode = 12 62 | OptionMaxAge OptionCode = 14 63 | OptionURIQuery OptionCode = 15 64 | OptionAccept OptionCode = 17 65 | OptionLocationQuery OptionCode = 20 66 | OptionBlock2 OptionCode = 23 67 | OptionBlock1 OptionCode = 27 68 | OptionSize2 OptionCode = 28 69 | OptionProxyURI OptionCode = 35 70 | OptionProxyScheme OptionCode = 39 71 | OptionSize1 OptionCode = 60 72 | ) 73 | 74 | // CoapCode defines a valid CoAP Code Type 75 | type CoapCode uint8 76 | 77 | const ( 78 | Get CoapCode = 1 79 | Post CoapCode = 2 80 | Put CoapCode = 3 81 | Delete CoapCode = 4 82 | 83 | // 2.x 84 | CoapCodeEmpty CoapCode = 0 85 | CoapCodeCreated CoapCode = 65 // 2.01 86 | CoapCodeDeleted CoapCode = 66 // 2.02 87 | CoapCodeValid CoapCode = 67 // 2.03 88 | CoapCodeChanged CoapCode = 68 // 2.04 89 | CoapCodeContent CoapCode = 69 // 2.05 90 | CoapCodeContinue CoapCode = 95 // 2.31 91 | 92 | // 4.x 93 | CoapCodeBadRequest CoapCode = 128 // 4.00 94 | CoapCodeUnauthorized CoapCode = 129 // 4.01 95 | CoapCodeBadOption CoapCode = 130 // 4.02 96 | CoapCodeForbidden CoapCode = 131 // 4.03 97 | CoapCodeNotFound CoapCode = 132 // 4.04 98 | CoapCodeMethodNotAllowed CoapCode = 133 // 4.05 99 | CoapCodeNotAcceptable CoapCode = 134 // 4.06 100 | CoapCodeRequestEntityIncomplete CoapCode = 136 // 4.08 101 | CoapCodeConflict CoapCode = 137 // 4.09 102 | CoapCodePreconditionFailed CoapCode = 140 // 4.12 103 | CoapCodeRequestEntityTooLarge CoapCode = 141 // 4.13 104 | CoapCodeUnsupportedContentFormat CoapCode = 143 // 4.15 105 | 106 | // 5.x 107 | CoapCodeInternalServerError CoapCode = 160 // 5.00 108 | CoapCodeNotImplemented CoapCode = 161 // 5.01 109 | CoapCodeBadGateway CoapCode = 162 // 5.02 110 | CoapCodeServiceUnavailable CoapCode = 163 // 5.03 111 | CoapCodeGatewayTimeout CoapCode = 164 // 5.04 112 | CoapCodeProxyingNotSupported CoapCode = 165 // 5.05 113 | ) 114 | 115 | const DefaultAckTimeout = 2 116 | const DefaultAckRandomFactor = 1.5 117 | const DefaultMaxRetransmit = 4 118 | const DefaultNStart = 1 119 | const DefaultLeisure = 5 120 | const DefaultProbingRate = 1 121 | 122 | const CoapDefaultHost = "" 123 | const CoapDefaultPort = 5683 124 | const CoapsDefaultPort = 5684 125 | 126 | const PayloadMarker = 0xff 127 | const MaxPacketSize = 1500 128 | 129 | // MessageIDPurgeDuration defines the number of seconds before a MessageID Purge is initiated 130 | const MessageIDPurgeDuration = 60 131 | 132 | type RouteHandler func(Request) Response 133 | 134 | // Proxy Filter 135 | type ProxyFilter func(Message, net.Addr) bool 136 | type ProxyHandler func(c CoapServer, msg Message, session Session) 137 | 138 | type MediaType int 139 | 140 | const ( 141 | MediaTypeTextPlain MediaType = 0 142 | MediaTypeTextXML MediaType = 1 143 | MediaTypeTextCsv MediaType = 2 144 | MediaTypeTextHTML MediaType = 3 145 | MediaTypeImageGif MediaType = 21 146 | MediaTypeImageJpeg MediaType = 22 147 | MediaTypeImagePng MediaType = 23 148 | MediaTypeImageTiff MediaType = 24 149 | MediaTypeAudioRaw MediaType = 25 150 | MediaTypeVideoRaw MediaType = 26 151 | MediaTypeApplicationLinkFormat MediaType = 40 152 | MediaTypeApplicationXML MediaType = 41 153 | MediaTypeApplicationOctetStream MediaType = 42 154 | MediaTypeApplicationRdfXML MediaType = 43 155 | MediaTypeApplicationSoapXML MediaType = 44 156 | MediaTypeApplicationAtomXML MediaType = 45 157 | MediaTypeApplicationXmppXML MediaType = 46 158 | MediaTypeApplicationExi MediaType = 47 159 | MediaTypeApplicationFastInfoSet MediaType = 48 160 | MediaTypeApplicationSoapFastInfoSet MediaType = 49 161 | MediaTypeApplicationJSON MediaType = 50 162 | MediaTypeApplicationXObitBinary MediaType = 51 163 | MediaTypeTextPlainVndOmaLwm2m MediaType = 1541 164 | MediaTypeTlvVndOmaLwm2m MediaType = 1542 165 | MediaTypeJSONVndOmaLwm2m MediaType = 1543 166 | MediaTypeOpaqueVndOmaLwm2m MediaType = 1544 167 | ) 168 | 169 | const ( 170 | MethodGet = "GET" 171 | MethodPut = "PUT" 172 | MethodPost = "POST" 173 | MethodDelete = "DELETE" 174 | MethodOptions = "OPTIONS" 175 | MethodPatch = "PATCH" 176 | ) 177 | 178 | type BlockSizeType byte 179 | 180 | const ( 181 | BlockSize16 BlockSizeType = 0 182 | BlockSize32 BlockSizeType = 1 183 | BlockSize64 BlockSizeType = 2 184 | BlockSize128 BlockSizeType = 3 185 | BlockSize256 BlockSizeType = 4 186 | BlockSize512 BlockSizeType = 5 187 | BlockSize1024 BlockSizeType = 6 188 | ) 189 | 190 | // Errors 191 | var ErrPacketLengthLessThan4 = errors.New("Packet length less than 4 bytes") 192 | var ErrInvalidCoapVersion = errors.New("Invalid CoAP version. Should be 1.") 193 | var ErrOptionLengthUsesValue15 = errors.New(("Message format error. Option length has reserved value of 15")) 194 | var ErrOptionDeltaUsesValue15 = errors.New(("Message format error. Option delta has reserved value of 15")) 195 | var ErrUnknownMessageType = errors.New("Unknown message type") 196 | var ErrInvalidTokenLength = errors.New("Invalid Token Length ( > 8)") 197 | var ErrUnknownCriticalOption = errors.New("Unknown critical option encountered") 198 | var ErrUnsupportedMethod = errors.New("Unsupported Method") 199 | var ErrNoMatchingRoute = errors.New("No matching route found") 200 | var ErrUnsupportedContentFormat = errors.New("Unsupported Content-Format") 201 | var ErrNoMatchingMethod = errors.New("No matching method") 202 | var ErrNilMessage = errors.New("Message is nil") 203 | var ErrNilConn = errors.New("Connection object is nil") 204 | var ErrNilAddr = errors.New("Address cannot be nil") 205 | var ErrMessageSizeTooLongBlockOptionValNotSet = errors.New("Message is too long, block option or value not set") 206 | 207 | // Security Options 208 | const ( 209 | SecNoSec = "NoSec" 210 | SecPreSharedKey = "PreSharedKey" 211 | SecRawPublicKey = "RawPublicKey" 212 | SecCertificate = "Certificate" 213 | ) 214 | 215 | // Interfaces 216 | type CoapServer interface { 217 | ListenAndServe(addr string) 218 | ListenAndServeDTLS(addr string) 219 | Stop() 220 | 221 | Get(path string, fn RouteHandler) Route 222 | Delete(path string, fn RouteHandler) Route 223 | Put(path string, fn RouteHandler) Route 224 | Post(path string, fn RouteHandler) Route 225 | Options(path string, fn RouteHandler) Route 226 | Patch(path string, fn RouteHandler) Route 227 | 228 | NewRoute(path string, method CoapCode, fn RouteHandler) Route 229 | NotifyChange(resource, value string, confirm bool) 230 | 231 | OnNotify(fn FnEventNotify) 232 | OnStart(fn FnEventStart) 233 | OnClose(fn FnEventClose) 234 | OnDiscover(fn FnEventDiscover) 235 | OnError(fn FnEventError) 236 | OnObserve(fn FnEventObserve) 237 | OnObserveCancel(fn FnEventObserveCancel) 238 | OnMessage(fn FnEventMessage) 239 | OnBlockMessage(fn FnEventBlockMessage) 240 | 241 | ProxyOverHttp(enabled bool) 242 | ProxyOverCoap(enabled bool) 243 | 244 | GetEvents() Events 245 | 246 | AllowProxyForwarding(Message, net.Addr) bool 247 | GetRoutes() []Route 248 | ForwardCoap(msg Message, session Session) 249 | ForwardHTTP(msg Message, session Session) 250 | 251 | AddObservation(resource, token string, session Session) 252 | HasObservation(resource string, addr net.Addr) bool 253 | RemoveObservation(resource string, addr net.Addr) 254 | 255 | HandlePSK(func(id string) []byte) 256 | 257 | GetSession(addr string) Session 258 | DeleteSession(ssn Session) 259 | 260 | GetCookieSecret() []byte 261 | } 262 | 263 | type ServerConnection interface { 264 | ReadFrom(b []byte) (n int, addr net.Addr, err error) 265 | WriteTo(b []byte, addr net.Addr) (n int, err error) 266 | Close() error 267 | LocalAddr() net.Addr 268 | SetDeadline(t time.Time) error 269 | SetReadDeadline(t time.Time) error 270 | SetWriteDeadline(t time.Time) error 271 | } 272 | 273 | type Option interface { 274 | Name() string 275 | IsElective() bool 276 | IsCritical() bool 277 | StringValue() string 278 | IntValue() int 279 | GetCode() OptionCode 280 | GetValue() interface{} 281 | } 282 | 283 | type Session interface { 284 | GetConnection() ServerConnection 285 | GetAddress() net.Addr 286 | Write([]byte) (int, error) 287 | Read([]byte) (n int, err error) 288 | GetServer() CoapServer 289 | WriteBuffer([]byte) int 290 | } 291 | 292 | type Request interface { 293 | GetAttributes() map[string]string 294 | GetAttribute(o string) string 295 | GetAttributeAsInt(o string) int 296 | GetMessage() Message 297 | GetURIQuery(q string) string 298 | 299 | SetProxyURI(uri string) 300 | SetMediaType(mt MediaType) 301 | SetPayload([]byte) 302 | SetStringPayload(s string) 303 | SetRequestURI(uri string) 304 | SetConfirmable(con bool) 305 | SetToken(t string) 306 | SetURIQuery(k string, v string) 307 | } 308 | 309 | type Response interface { 310 | GetMessage() Message 311 | GetError() error 312 | GetPayload() []byte 313 | GetURIQuery(q string) string 314 | } 315 | 316 | type Connection interface { 317 | ObserveResource(resource string) (tok string, err error) 318 | CancelObserveResource(resource string, token string) (err error) 319 | StopObserve(ch chan ObserveMessage) 320 | Observe(ch chan ObserveMessage) 321 | Send(req Request) (resp Response, err error) 322 | 323 | Write(b []byte) (n int, err error) 324 | Read(b []byte) (n int, err error) 325 | Close() error 326 | } 327 | 328 | // Represents the payload/content of a CoAP Message 329 | type MessagePayload interface { 330 | GetBytes() []byte 331 | Length() int 332 | String() string 333 | } 334 | 335 | type Message interface { 336 | GetToken() []byte 337 | GetMessageId() uint16 338 | GetMessageType() uint8 339 | GetAcceptedContent() MediaType 340 | GetCodeString() string 341 | GetCode() CoapCode 342 | GetMethod() uint8 343 | GetTokenLength() uint8 344 | GetTokenString() string 345 | GetOptions(id OptionCode) []Option 346 | GetOption(id OptionCode) Option 347 | GetAllOptions() []Option 348 | GetOptionsAsString(id OptionCode) []string 349 | GetLocationPath() string 350 | GetURIPath() string 351 | GetPayload() MessagePayload 352 | 353 | SetToken([]byte) 354 | SetMessageId(uint16) 355 | SetMessageType(uint8) 356 | SetBlock1Option(opt Option) 357 | SetStringPayload(s string) 358 | SetPayload(MessagePayload) 359 | 360 | AddOption(code OptionCode, value interface{}) 361 | AddOptions(opts []Option) 362 | CloneOptions(cm Message, opts ...OptionCode) 363 | ReplaceOptions(code OptionCode, opts []Option) 364 | RemoveOptions(id OptionCode) 365 | } 366 | 367 | type Route interface { 368 | GetMethod() string 369 | GetMediaTypes() []MediaType 370 | GetConfiguredPath() string 371 | 372 | Matches(path string) (bool, map[string]string) 373 | AutoAcknowledge() bool 374 | Handle(req Request) Response 375 | } 376 | 377 | type FnEventNotify func(string, interface{}, Message) 378 | type FnEventStart func(CoapServer) 379 | type FnEventClose func(CoapServer) 380 | type FnEventDiscover func() 381 | type FnEventError func(error) 382 | type FnEventObserve func(string, Message) 383 | type FnEventObserveCancel func(string, Message) 384 | type FnEventMessage func(Message, bool) 385 | type FnEventBlockMessage func(Message, bool) 386 | 387 | type EventCode int 388 | 389 | const ( 390 | EventStart EventCode = 0 391 | EventClose EventCode = 1 392 | EventDiscover EventCode = 2 393 | EventMessage EventCode = 3 394 | EventError EventCode = 4 395 | EventObserve EventCode = 5 396 | EventObserveCancel EventCode = 6 397 | EventNotify EventCode = 7 398 | ) 399 | 400 | type ObserveMessage interface { 401 | GetResource() string 402 | GetValue() interface{} 403 | GetMessage() Message 404 | } 405 | 406 | type Events interface { 407 | OnNotify(fn FnEventNotify) 408 | OnStart(fn FnEventStart) 409 | OnClose(fn FnEventClose) 410 | OnDiscover(fn FnEventDiscover) 411 | OnError(fn FnEventError) 412 | OnObserve(fn FnEventObserve) 413 | OnObserveCancel(fn FnEventObserveCancel) 414 | OnMessage(fn FnEventMessage) 415 | OnBlockMessage(fn FnEventBlockMessage) 416 | 417 | Notify(resource string, value interface{}, msg Message) 418 | Started(server CoapServer) 419 | Closed(server CoapServer) 420 | Discover() 421 | Error(err error) 422 | Observe(resource string, msg Message) 423 | ObserveCancelled(resource string, msg Message) 424 | Message(msg Message, inbound bool) 425 | BlockMessage(msg Message, inbound bool) 426 | } 427 | 428 | type BlockMessage interface { 429 | } 430 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import "net" 4 | 5 | func Dial(address string) (conn Connection, err error) { 6 | udpConn, err := net.Dial("udp", address) 7 | if err != nil { 8 | return 9 | } 10 | 11 | conn = &UDPConnection{ 12 | conn: udpConn, 13 | } 14 | 15 | return 16 | } 17 | 18 | func DialDTLS(address, identity, psk string) (conn Connection, err error) { 19 | udpConn, err := net.Dial("udp", address) 20 | if err != nil { 21 | return 22 | } 23 | 24 | conn, err = NewDTLSConnection(udpConn, identity, psk) 25 | if err != nil { 26 | return 27 | } 28 | 29 | return 30 | } 31 | 32 | func NewObserveMessage(r string, val interface{}, msg Message) ObserveMessage { 33 | return &CoapObserveMessage{ 34 | Resource: r, 35 | Value: val, 36 | Msg: msg, 37 | } 38 | } 39 | 40 | type CoapObserveMessage struct { 41 | CoapMessage 42 | Resource string 43 | Value interface{} 44 | Msg Message 45 | } 46 | 47 | func (m *CoapObserveMessage) GetResource() string { 48 | return m.Resource 49 | } 50 | 51 | func (m *CoapObserveMessage) GetValue() interface{} { 52 | return m.Value 53 | } 54 | 55 | func (m *CoapObserveMessage) GetMessage() Message { 56 | return m.GetMessage() 57 | } 58 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "sync" 7 | ) 8 | 9 | func MessageSizeAllowed(req Request) bool { 10 | msg := req.GetMessage() 11 | b, _ := MessageToBytes(msg) 12 | 13 | if len(b) > 65536 { 14 | return false 15 | } 16 | 17 | return true 18 | } 19 | 20 | type UDPConnection struct { 21 | conn net.Conn 22 | } 23 | 24 | func (c *UDPConnection) ObserveResource(resource string) (tok string, err error) { 25 | req := NewRequest(MessageConfirmable, Get) 26 | req.SetRequestURI(resource) 27 | req.GetMessage().AddOption(OptionObserve, 0) 28 | 29 | resp, err := c.Send(req) 30 | tok = string(resp.GetMessage().GetToken()) 31 | 32 | return 33 | } 34 | 35 | func (c *UDPConnection) CancelObserveResource(resource string, token string) (err error) { 36 | req := NewRequest(MessageConfirmable, Get) 37 | req.SetRequestURI(resource) 38 | req.GetMessage().AddOption(OptionObserve, 1) 39 | 40 | _, err = c.Send(req) 41 | return 42 | } 43 | 44 | func (c *UDPConnection) StopObserve(ch chan ObserveMessage) { 45 | close(ch) 46 | } 47 | 48 | func (c *UDPConnection) Close() error { 49 | return c.conn.Close() 50 | } 51 | 52 | func (c *UDPConnection) Observe(ch chan ObserveMessage) { 53 | 54 | readBuf := make([]byte, MaxPacketSize) 55 | for { 56 | len, err := c.Read(readBuf) 57 | if err == nil { 58 | msgBuf := make([]byte, len) 59 | copy(msgBuf, readBuf) 60 | 61 | msg, err := BytesToMessage(msgBuf) 62 | if msg.GetOption(OptionObserve) != nil { 63 | ch <- NewObserveMessage(msg.GetURIPath(), msg.GetPayload(), msg) 64 | } 65 | if err != nil { 66 | log.Println("Error occured reading UDP", err) 67 | close(ch) 68 | } 69 | } else { 70 | log.Println("Error occured reading UDP", err) 71 | close(ch) 72 | } 73 | } 74 | } 75 | 76 | func (c *UDPConnection) Send(req Request) (resp Response, err error) { 77 | msg := req.GetMessage() 78 | opt := msg.GetOption(OptionBlock1) 79 | 80 | if opt == nil { // Block1 was not set 81 | if MessageSizeAllowed(req) != true { 82 | return nil, ErrMessageSizeTooLongBlockOptionValNotSet 83 | } 84 | } else { // Block1 was set 85 | // log.Println("Block 1 was set") 86 | } 87 | 88 | if opt != nil { 89 | blockOpt := Block1OptionFromOption(opt) 90 | if blockOpt.Value == nil { 91 | if MessageSizeAllowed(req) != true { 92 | err = ErrMessageSizeTooLongBlockOptionValNotSet 93 | return 94 | } else { 95 | // - Block # = one and only block (sz = unspecified), whereas 0 = 16bits 96 | // - MOre bit = 0 97 | } 98 | } else { // BLock transfer request 99 | payload := msg.GetPayload().GetBytes() 100 | payloadLen := uint32(len(payload)) 101 | blockSize := blockOpt.BlockSizeLength() 102 | currSeq := uint32(0) 103 | totalBlocks := uint32(payloadLen / blockSize) 104 | completed := false 105 | 106 | var wg sync.WaitGroup 107 | wg.Add(1) 108 | 109 | for completed == false { 110 | if currSeq <= totalBlocks { 111 | 112 | var blockPayloadStart uint32 113 | var blockPayloadEnd uint32 114 | var blockPayload []byte 115 | 116 | blockPayloadStart = currSeq * uint32(blockSize) 117 | 118 | more := true 119 | if currSeq == totalBlocks { 120 | more = false 121 | blockPayloadEnd = payloadLen 122 | } else { 123 | blockPayloadEnd = blockPayloadStart + uint32(blockSize) 124 | } 125 | 126 | blockPayload = payload[blockPayloadStart : blockPayloadEnd+1] 127 | 128 | blockOpt = NewBlock1Option(blockOpt.Size(), more, currSeq) 129 | msg.ReplaceOptions(blockOpt.Code, []Option{blockOpt}) 130 | modifiedMsg := msg.(*CoapMessage) 131 | modifiedMsg.SetMessageId(GenerateMessageID()) 132 | modifiedMsg.SetPayload(NewBytesPayload(blockPayload)) 133 | 134 | // send message 135 | _, err2 := c.SendMessage(msg) 136 | if err2 != nil { 137 | wg.Done() 138 | return 139 | } 140 | currSeq = currSeq + 1 141 | 142 | } else { 143 | completed = true 144 | wg.Done() 145 | } 146 | } 147 | } 148 | } 149 | resp, err = c.SendMessage(msg) 150 | return 151 | } 152 | 153 | func (c *UDPConnection) SendMessage(msg Message) (resp Response, err error) { 154 | if msg == nil { 155 | return nil, ErrNilMessage 156 | } 157 | 158 | b, err := MessageToBytes(msg) 159 | if err != nil { 160 | return 161 | } 162 | 163 | if msg.GetMessageType() == MessageNonConfirmable { 164 | go c.Write(b) 165 | resp = NewResponse(NewEmptyMessage(msg.GetMessageId()), nil) 166 | return 167 | } 168 | 169 | _, err = c.Write(b) 170 | if err != nil { 171 | return 172 | } 173 | 174 | msgBuf := make([]byte, 1500) 175 | if msg.GetMessageType() == MessageAcknowledgment { 176 | resp = NewResponse(NewEmptyMessage(msg.GetMessageId()), nil) 177 | return 178 | } 179 | 180 | n, err := c.Read(msgBuf) 181 | if err != nil { 182 | return 183 | } 184 | 185 | respMsg, err := BytesToMessage(msgBuf[:n]) 186 | if err != nil { 187 | return 188 | } 189 | resp = NewResponse(respMsg, nil) 190 | 191 | if msg.GetMessageType() == MessageConfirmable { 192 | // TODO: Send out message and wait for a confirm. If confirmation not retrieved, 193 | // resend (taking into account timeouts and back-off transmissions 194 | 195 | // c.Send(NewRequestFromMessage(msg)) 196 | } 197 | return 198 | } 199 | 200 | func (c *UDPConnection) Write(b []byte) (int, error) { 201 | return c.conn.Write(b) 202 | } 203 | 204 | func (c *UDPConnection) Read(b []byte) (int, error) { 205 | return c.conn.Read(b) 206 | } 207 | -------------------------------------------------------------------------------- /corelink.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | // Represents a message payload containing core-link format values 4 | type CoreLinkFormatPayload struct { 5 | } 6 | 7 | func (p *CoreLinkFormatPayload) GetBytes() []byte { 8 | return make([]byte, 0) 9 | } 10 | 11 | func (p *CoreLinkFormatPayload) Length() int { 12 | return 0 13 | } 14 | 15 | func (p *CoreLinkFormatPayload) String() string { 16 | return "" 17 | } 18 | -------------------------------------------------------------------------------- /coreresource.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | // Instantiates a new core-attribute with a given key/value 4 | func NewCoreAttribute(key string, value interface{}) *CoreAttribute { 5 | return &CoreAttribute{ 6 | Key: key, 7 | Value: value, 8 | } 9 | } 10 | 11 | // Instantiates a new Core Resource Object 12 | func NewCoreResource() *CoreResource { 13 | c := &CoreResource{} 14 | 15 | return c 16 | } 17 | -------------------------------------------------------------------------------- /coreresource_test.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCoreResourceParsing(t *testing.T) { 10 | 11 | cases1 := []struct { 12 | in string 13 | elemCount int 14 | targets []string 15 | attrCount []int 16 | attributes []map[string]string 17 | }{ 18 | { 19 | ",,,,,,,", 20 | 8, 21 | []string{"/1", "/2", "/3", "/4", "/5/0", "/5/1", "/5/2", "/5/3"}, 22 | []int{0, 0, 0, 0, 0, 0, 0, 0, 0}, 23 | nil, 24 | }, 25 | { 26 | ";ct=40;title=\"Sensor Index\",;rt=\"temperature-c\";if=\"sensor\",;rt=\"light-lux\";if=\"sensor\",;anchor=\"/sensors/temp\";rel=\"describedby\",;anchor=\"/sensors/temp\";rel=\"alternate\"", 27 | 5, 28 | []string{"/sensors", "/sensors/temp", "/sensors/light", "http://www.example.com/sensors/t123", "/t"}, 29 | []int{1, 2, 2, 2, 2}, 30 | []map[string]string{ 31 | map[string]string{ 32 | "ct": "40", 33 | "title": "Sensor Index", 34 | }, 35 | map[string]string{ 36 | "rt": "temperature-c", 37 | "if": "sensor", 38 | }, 39 | map[string]string{ 40 | "rt": "light-lux", 41 | "if": "sensor", 42 | }, 43 | map[string]string{ 44 | "anchor": "/sensors/temp", 45 | "rel": "describedby", 46 | }, 47 | map[string]string{ 48 | "anchor": "/sensors/temp", 49 | "rel": "alternate", 50 | }, 51 | }, 52 | }, 53 | } 54 | 55 | for _, c := range cases1 { 56 | resources := CoreResourcesFromString(c.in) 57 | assert.Equal(t, len(resources), c.elemCount) 58 | 59 | for i, o := range resources { 60 | assert.Equal(t, o.Target, c.targets[i]) 61 | 62 | for _, a := range o.Attributes { 63 | key := a.Key 64 | val := a.Value 65 | 66 | assert.Equal(t, c.attributes[i][key], val) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /dtls.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | /* 4 | #cgo LDFLAGS: -L${SRCDIR}/openssl -lssl -lcrypto -ldl 5 | #cgo CFLAGS: -g -Wno-deprecated -Wno-error -I${SRCDIR}/openssl/include -Wno-incompatible-pointer-types -Wno-return-type 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | extern int go_session_bio_write(BIO* bio, char* buf, int num); 15 | extern int go_session_bio_read(BIO* bio, char* buf, int num); 16 | extern int go_session_bio_free(BIO* bio); 17 | extern unsigned int go_server_psk_callback(SSL *ssl, char *identity, char *psk, unsigned int max_psk_len); 18 | extern int generate_cookie_callback(SSL* ssl, unsigned char* cookie, unsigned int *cookie_len); 19 | extern int verify_cookie_callback(SSL* ssl, unsigned char* cookie, unsigned int cookie_len); 20 | 21 | static long go_session_bio_ctrl(BIO *bp,int cmd,long larg,void *parg) { 22 | return 1; 23 | } 24 | 25 | static int write_wrapper(BIO* bio, char* data, int n) { 26 | return go_session_bio_write(bio,data,n); 27 | } 28 | 29 | static int go_session_bio_create( BIO *b ) { 30 | BIO_set_init(b,1); 31 | BIO_set_flags(b, BIO_FLAGS_READ | BIO_FLAGS_WRITE); 32 | return 1; 33 | } 34 | 35 | // a BIO for a client conencted to our server 36 | static BIO_METHOD* go_session_bio_method; 37 | 38 | static BIO_METHOD* BIO_go_session() { 39 | return go_session_bio_method; 40 | } 41 | 42 | static void set_errno(int e) { 43 | errno = e; 44 | } 45 | 46 | static char *getGoData(BIO* bio) { 47 | return BIO_get_data(bio); 48 | } 49 | 50 | static unsigned int server_psk_callback(SSL *ssl, char *identity, unsigned char *psk, unsigned int max_psk_len) { 51 | return go_server_psk_callback(ssl,identity,(char*)psk,max_psk_len); 52 | } 53 | 54 | static void init_lib() { 55 | setvbuf(stdout, NULL, _IOLBF, 0); 56 | SSL_library_init(); 57 | ERR_load_BIO_strings(); 58 | SSL_load_error_strings(); 59 | } 60 | 61 | static int init_session_bio_method() { 62 | go_session_bio_method = BIO_meth_new(BIO_TYPE_SOURCE_SINK,"go session dtls"); 63 | BIO_meth_set_write(go_session_bio_method,write_wrapper); 64 | BIO_meth_set_read(go_session_bio_method,go_session_bio_read); 65 | BIO_meth_set_ctrl(go_session_bio_method,go_session_bio_ctrl); 66 | BIO_meth_set_create(go_session_bio_method,go_session_bio_create); 67 | BIO_meth_set_destroy(go_session_bio_method,go_session_bio_free); 68 | } 69 | 70 | static void init_server_ctx(SSL_CTX *ctx) { 71 | SSL_CTX_set_min_proto_version(ctx, 0xFEFD); // 1.2 72 | SSL_CTX_set_max_proto_version(ctx, 0xFEFD); // 1.2 73 | SSL_CTX_set_read_ahead(ctx, 1); 74 | SSL_CTX_set_cookie_generate_cb(ctx, &generate_cookie_callback); 75 | SSL_CTX_set_cookie_verify_cb(ctx, &verify_cookie_callback); 76 | } 77 | 78 | static int get_errno(void) { 79 | return errno; 80 | } 81 | 82 | static void setGoData(BIO* bio, char *data) { 83 | BIO_set_data(bio, data); 84 | } 85 | 86 | static void set_cookie_option(SSL *ssl) { 87 | SSL_set_options(ssl, SSL_OP_COOKIE_EXCHANGE); 88 | } 89 | 90 | static void set_psk_callback(SSL *ssl) { 91 | SSL_set_psk_server_callback(ssl, &server_psk_callback); 92 | } 93 | 94 | static void setGoSessionId(BIO* bio, unsigned int clientId) { 95 | unsigned int * pId = malloc(sizeof(unsigned int)); 96 | *pId = clientId; 97 | BIO_set_data(bio,pId); 98 | } 99 | 100 | // Client 101 | extern int go_conn_bio_write(BIO* bio, char* buf, int num); 102 | extern int go_conn_bio_read(BIO* bio, char* buf, int num); 103 | extern int go_conn_bio_free(BIO* bio); 104 | extern unsigned int go_psk_callback(SSL *ssl, char *hint, char *identity, unsigned int max_identity_len, char *psk, unsigned int max_psk_len); 105 | 106 | static long go_bio_ctrl(BIO *bp,int cmd,long larg,void *parg) { 107 | //always return operation not supported 108 | //http://www.openssl.org/docs/crypto/BIO_ctrl.html 109 | //printf("go_bio_ctrl %d\n", cmd); 110 | return 1; 111 | } 112 | 113 | static int go_bio_create( BIO *b ) { 114 | BIO_set_init(b,1); 115 | //BIO_set_num(b,-1); 116 | //BIO_set_ptr(b,NULL); 117 | BIO_set_flags(b, BIO_FLAGS_READ | BIO_FLAGS_WRITE); 118 | return 1; 119 | } 120 | 121 | static BIO_METHOD go_bio_method = { 122 | BIO_TYPE_SOURCE_SINK, 123 | "go dtls", 124 | (int (*)(BIO *, const char *, int))go_conn_bio_write, 125 | go_conn_bio_read, 126 | NULL, 127 | NULL, 128 | go_bio_ctrl, // ctrl 129 | go_bio_create, // new 130 | go_conn_bio_free // delete 131 | }; 132 | 133 | static BIO_METHOD* BIO_go() { 134 | return &go_bio_method; 135 | } 136 | 137 | static void set_proto_1_2(SSL_CTX *ctx) { 138 | SSL_CTX_set_min_proto_version(ctx, 0xFEFD); // 1.2 139 | SSL_CTX_set_max_proto_version(ctx, 0xFEFD); // 1.2 140 | } 141 | 142 | static unsigned int psk_callback(SSL *ssl, const char *hint, 143 | char *identity, unsigned int max_identity_len, 144 | unsigned char *psk, unsigned int max_psk_len) { 145 | return go_psk_callback(ssl,hint,identity,max_identity_len,(char*)psk,max_psk_len); 146 | } 147 | 148 | static void init_ctx(SSL_CTX *ctx) { 149 | SSL_CTX_set_read_ahead(ctx, 1); 150 | SSL_CTX_set_psk_client_callback(ctx,&psk_callback); 151 | } 152 | 153 | static void setGoClientId(BIO* bio, unsigned int clientId) { 154 | unsigned int * pId = malloc(sizeof(unsigned int)); 155 | *pId = clientId; 156 | BIO_set_data(bio,pId); 157 | } 158 | 159 | */ 160 | import "C" 161 | import ( 162 | "bytes" 163 | "crypto/hmac" 164 | "crypto/sha256" 165 | "errors" 166 | "fmt" 167 | "io" 168 | "net" 169 | "reflect" 170 | "sync" 171 | "sync/atomic" 172 | "syscall" 173 | "unsafe" 174 | ) 175 | 176 | func init() { 177 | // low level init of OpenSSL 178 | C.init_lib() 179 | 180 | // init server BIO 181 | server_bio_method_init() 182 | } 183 | 184 | func server_bio_method_init() { 185 | C.init_session_bio_method() 186 | } 187 | 188 | func NewServerDtlsContext() (ctx *ServerDtlsContext, err error) { 189 | sslCtx := C.SSL_CTX_new(C.DTLSv1_2_server_method()) 190 | 191 | if sslCtx == nil { 192 | err = errors.New("error creating SSL context") 193 | return 194 | } 195 | 196 | C.init_server_ctx(sslCtx) 197 | 198 | ret := int(C.SSL_CTX_set_cipher_list(sslCtx, C.CString("PSK-AES256-CCM8:PSK-AES128-CCM8"))) 199 | if ret != 1 { 200 | err = errors.New("impossible to set cipherlist") 201 | } 202 | 203 | ctx = &ServerDtlsContext{ 204 | sslCtx: sslCtx, 205 | } 206 | 207 | return 208 | } 209 | 210 | type ServerDtlsContext struct { 211 | sslCtx *C.SSL_CTX 212 | } 213 | 214 | //export go_session_bio_read 215 | func go_session_bio_read(bio *C.BIO, buf *C.char, num C.int) C.int { 216 | session := DTLS_SERVER_SESSIONS[*(*int32)(C.BIO_get_data(bio))] 217 | socketData := <-session.rcvd 218 | 219 | data := goSliceFromCString(buf, int(num)) 220 | if data == nil { 221 | return 0 222 | } 223 | 224 | wrote := copy(data, socketData) 225 | return C.int(wrote) 226 | } 227 | 228 | //export go_session_bio_write 229 | func go_session_bio_write(bio *C.BIO, buf *C.char, num C.int) C.int { 230 | session := DTLS_SERVER_SESSIONS[*(*int32)(C.BIO_get_data(bio))] 231 | data := goSliceFromCString(buf, int(num)) 232 | 233 | n, err := session.GetConnection().WriteTo(data, session.GetAddress()) 234 | if err != nil && err != io.EOF { 235 | //We expect either a syscall error 236 | //or a netOp error wrapping a syscall error 237 | TESTERR: 238 | switch err.(type) { 239 | case syscall.Errno: 240 | C.set_errno(C.int(err.(syscall.Errno))) 241 | case *net.OpError: 242 | err = err.(*net.OpError).Err 243 | break TESTERR 244 | } 245 | return C.int(-1) 246 | } 247 | return C.int(n) 248 | } 249 | 250 | //export go_session_bio_free 251 | func go_session_bio_free(bio *C.BIO) C.int { 252 | // TODO 253 | 254 | // some flags magic 255 | if C.int(C.BIO_get_shutdown(bio)) != 0 { 256 | C.BIO_set_data(bio, nil) 257 | C.BIO_set_flags(bio, 0) 258 | C.BIO_set_init(bio, 0) 259 | } 260 | return C.int(1) 261 | } 262 | 263 | //export go_server_psk_callback 264 | func go_server_psk_callback(ssl *C.SSL, identity *C.char, psk *C.char, max_psk_len C.uint) C.uint { 265 | bio := C.SSL_get_rbio(ssl) 266 | session := DTLS_SERVER_SESSIONS[*(*int32)(C.BIO_get_data(bio))] 267 | server := session.GetServer().(*DefaultCoapServer) 268 | 269 | goPskID := C.GoString(identity) 270 | 271 | serverPsk := server.fnPskHandler(goPskID) 272 | 273 | if serverPsk == nil { 274 | return 0 275 | } 276 | 277 | if len(serverPsk) >= int(max_psk_len) { 278 | return 0 279 | } 280 | 281 | targetPsk := goSliceFromCString(psk, int(max_psk_len)) 282 | return C.uint(copy(targetPsk, serverPsk)) 283 | } 284 | 285 | //export generate_cookie_callback 286 | func generate_cookie_callback(ssl *C.SSL, cookie *C.uchar, cookie_len *C.uint) C.int { 287 | bio := C.SSL_get_rbio(ssl) 288 | session := DTLS_SERVER_SESSIONS[*(*int32)(C.BIO_get_data(bio))] 289 | 290 | mac := hmac.New(sha256.New, session.GetServer().GetCookieSecret()) 291 | mac.Write([]byte(session.GetAddress().String())) 292 | cookieValue := mac.Sum(nil) 293 | 294 | if len(cookieValue) >= int(*cookie_len) { 295 | logMsg("Not enough cookie space (should not happen..)") 296 | return 0 297 | } 298 | 299 | data := goSliceFromUCString(cookie, int(*cookie_len)) 300 | 301 | *cookie_len = C.uint(copy(data, cookieValue)) 302 | return 1 303 | 304 | } 305 | 306 | //export verify_cookie_callback 307 | func verify_cookie_callback(ssl *C.SSL, cookie *C.uchar, cookie_len C.uint) C.int { 308 | bio := C.SSL_get_rbio(ssl) 309 | session := DTLS_SERVER_SESSIONS[*(*int32)(C.BIO_get_data(bio))] 310 | 311 | mac := hmac.New(sha256.New, session.GetServer().GetCookieSecret()) 312 | mac.Write([]byte(session.GetAddress().String())) 313 | cookieValue := mac.Sum(nil) 314 | 315 | if len(cookieValue) != int(cookie_len) { 316 | return 0 317 | } 318 | 319 | data := goSliceFromUCString(cookie, int(cookie_len)) 320 | 321 | if bytes.Equal(data, cookieValue) { 322 | return 1 323 | } else { 324 | return 0 325 | } 326 | } 327 | 328 | // Provides a zero copy interface for returning a go slice backed by a c array. 329 | func goSliceFromCString(cArray *C.char, size int) (cslice []byte) { 330 | //See http://code.google.com/p/go-wiki/wiki/cgo 331 | //It turns out it's really easy to 332 | //make a string from a *C.char and vise versa. 333 | //not so easy to write to a c array. 334 | sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&cslice))) 335 | sliceHeader.Cap = size 336 | sliceHeader.Len = size 337 | sliceHeader.Data = uintptr(unsafe.Pointer(cArray)) 338 | return 339 | } 340 | 341 | // Provides a zero copy interface for returning a go slice backed by a c array. 342 | func goSliceFromUCString(cArray *C.uchar, size int) (cslice []byte) { 343 | //See http://code.google.com/p/go-wiki/wiki/cgo 344 | //It turns out it's really easy to 345 | //make a string from a *C.char and vise versa. 346 | //not so easy to write to a c array. 347 | sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&cslice))) 348 | sliceHeader.Cap = size 349 | sliceHeader.Len = size 350 | sliceHeader.Data = uintptr(unsafe.Pointer(cArray)) 351 | return 352 | } 353 | 354 | func getErrorString(code C.ulong) string { 355 | if code == 0 { 356 | return "" 357 | } 358 | msg := fmt.Sprintf("%s:%s:%s\n", 359 | C.GoString(C.ERR_lib_error_string(code)), 360 | C.GoString(C.ERR_func_error_string(code)), 361 | C.GoString(C.ERR_reason_error_string(code))) 362 | if len(msg) == 4 { //being lazy here, all the strings were empty 363 | return "" 364 | } 365 | //Check for extra line data 366 | var file *C.char 367 | var line C.int 368 | var data *C.char 369 | var flags C.int 370 | if int(C.ERR_get_error_line_data(&file, &line, &data, &flags)) != 0 { 371 | msg += fmt.Sprintf("%s:%s", C.GoString(file), int(line)) 372 | if flags&C.ERR_TXT_STRING != 0 { 373 | msg += ":" + C.GoString(data) 374 | } 375 | if flags&C.ERR_TXT_MALLOCED != 0 { 376 | C.CRYPTO_free(unsafe.Pointer(data), C.CString(""), 0) 377 | } 378 | } 379 | return msg 380 | } 381 | 382 | func newSslSession(session *DTLSServerSession, ctx *ServerDtlsContext, pskCallback func(id string) []byte) (err error) { 383 | ssl := C.SSL_new(ctx.sslCtx) 384 | 385 | id := atomic.AddInt32(&NEXT_SESSION_ID, 1) 386 | 387 | if pskCallback != nil { 388 | C.set_psk_callback(ssl) 389 | } 390 | 391 | bio := C.BIO_new(C.BIO_go_session()) 392 | 393 | if bio == nil { 394 | err = errors.New("Error creating session: Bio is nil") 395 | return 396 | } 397 | C.SSL_set_bio(ssl, bio, bio) 398 | 399 | session.ssl = ssl 400 | session.bio = bio 401 | 402 | DTLS_SERVER_SESSIONS[id] = session 403 | 404 | C.setGoSessionId(bio, C.uint(id)) 405 | 406 | C.set_cookie_option(ssl) 407 | C.SSL_set_accept_state(ssl) 408 | C.DTLSv1_listen 409 | 410 | return 411 | } 412 | 413 | type DTLSServerSession struct { 414 | UDPServerSession 415 | ssl *C.SSL 416 | bio *C.BIO 417 | } 418 | 419 | func (s *DTLSServerSession) GetConnection() ServerConnection { 420 | return s.conn 421 | } 422 | 423 | func (s *DTLSServerSession) Write(b []byte) (int, error) { 424 | // TODO test is connected ? 425 | length := len(b) 426 | ret := C.SSL_write(s.ssl, unsafe.Pointer(&b[0]), C.int(length)) 427 | if err := s.getError(ret); err != nil { 428 | return 0, err 429 | } 430 | return int(ret), nil 431 | } 432 | 433 | func (s *DTLSServerSession) Read(b []byte) (n int, err error) { 434 | // TODO test if closed? 435 | length := len(b) 436 | // s.rcvd <- s.buf 437 | 438 | ret := C.SSL_read(s.ssl, unsafe.Pointer(&b[0]), C.int(length)) 439 | if err = s.getError(ret); err != nil { 440 | n = 0 441 | return 442 | } 443 | // if there's no error, but a return value of 0 444 | // let's say it's an EOF 445 | if ret == 0 { 446 | n = 0 447 | err = io.EOF 448 | return 449 | } 450 | n = int(ret) 451 | return 452 | } 453 | 454 | func (s *DTLSServerSession) getError(ret C.int) error { 455 | err := C.SSL_get_error(s.ssl, ret) 456 | switch err { 457 | case C.SSL_ERROR_NONE: 458 | return nil 459 | case C.SSL_ERROR_ZERO_RETURN: 460 | return io.EOF 461 | case C.SSL_ERROR_SYSCALL: 462 | if int(C.ERR_peek_error()) != 0 { 463 | return syscall.Errno(C.get_errno()) 464 | } 465 | 466 | default: 467 | msg := "" 468 | for { 469 | errCode := C.ERR_get_error() 470 | if errCode == 0 { 471 | break 472 | } 473 | msg += getErrorString(errCode) 474 | } 475 | C.ERR_clear_error() 476 | return errors.New(msg) 477 | } 478 | return nil 479 | } 480 | 481 | // Client DTLS 482 | func NewDTLSConnection(c net.Conn, identity, psk string) (conn Connection, err error) { 483 | sslCtx := C.SSL_CTX_new(C.DTLSv1_2_client_method()) 484 | 485 | C.set_proto_1_2(sslCtx) 486 | C.init_ctx(sslCtx) 487 | 488 | ret := int(C.SSL_CTX_set_cipher_list(sslCtx, C.CString("PSK-AES256-CCM8:PSK-AES128-CCM8"))) 489 | if ret != 1 { 490 | err = errors.New("impossible to set cipherlist") 491 | return 492 | } 493 | 494 | ssl := C.SSL_new(sslCtx) 495 | 496 | // self := DTLSClient{false, 0, C.BIO_new(C.BIO_go()), dtlsCtx.ctx, ssl, conn, nil, nil} 497 | bio := C.BIO_new(C.BIO_go()) 498 | 499 | conn = &DTLSConnection{ 500 | UDPConnection: UDPConnection{ 501 | conn: c, 502 | }, 503 | sslCtx: sslCtx, 504 | ssl: ssl, 505 | bio: bio, 506 | psk: []byte(psk), 507 | pskId: &identity, 508 | } 509 | 510 | C.SSL_set_bio(ssl, bio, bio) 511 | 512 | id := atomic.AddInt32(&NEXT_SESSION_ID, 1) 513 | C.setGoClientId(bio, C.uint(id)) 514 | DTLS_CLIENT_CONNECTIONS[id] = conn.(*DTLSConnection) 515 | 516 | return 517 | } 518 | 519 | type DTLSConnection struct { 520 | UDPConnection 521 | closed bool 522 | connected int32 // connection handshake was done, atomic (0 false, 1 true) 523 | sslCtx *C.SSL_CTX 524 | bio *C.BIO 525 | ssl *C.SSL 526 | pskId *string 527 | psk []byte 528 | } 529 | 530 | func (c *DTLSConnection) ObserveResource(resource string) (tok string, err error) { 531 | req := NewRequest(MessageConfirmable, Get) 532 | req.SetRequestURI(resource) 533 | req.GetMessage().AddOption(OptionObserve, 0) 534 | 535 | resp, err := c.Send(req) 536 | tok = string(resp.GetMessage().GetToken()) 537 | 538 | return 539 | } 540 | 541 | func (c *DTLSConnection) CancelObserveResource(resource string, token string) (err error) { 542 | req := NewRequest(MessageConfirmable, Get) 543 | req.SetRequestURI(resource) 544 | req.GetMessage().AddOption(OptionObserve, 1) 545 | 546 | _, err = c.Send(req) 547 | return 548 | } 549 | 550 | func (c *DTLSConnection) StopObserve(ch chan ObserveMessage) { 551 | close(ch) 552 | } 553 | 554 | func (c *DTLSConnection) Observe(ch chan ObserveMessage) { 555 | 556 | readBuf := make([]byte, MaxPacketSize) 557 | for { 558 | len, err := c.Read(readBuf) 559 | if err == nil { 560 | msgBuf := make([]byte, len) 561 | copy(msgBuf, readBuf) 562 | 563 | msg, err := BytesToMessage(msgBuf) 564 | if msg.GetOption(OptionObserve) != nil { 565 | ch <- NewObserveMessage(msg.GetURIPath(), msg.GetPayload(), msg) 566 | } 567 | if err != nil { 568 | logMsg("Error occured reading UDP", err) 569 | close(ch) 570 | } 571 | } else { 572 | logMsg("Error occured reading UDP", err) 573 | close(ch) 574 | } 575 | } 576 | } 577 | 578 | func (c *DTLSConnection) Send(req Request) (resp Response, err error) { 579 | msg := req.GetMessage() 580 | opt := msg.GetOption(OptionBlock1) 581 | 582 | if opt == nil { // Block1 was not set 583 | if MessageSizeAllowed(req) != true { 584 | return nil, ErrMessageSizeTooLongBlockOptionValNotSet 585 | } 586 | 587 | } else { // Block1 was set 588 | // fmt.Println("Block 1 was set") 589 | } 590 | 591 | if opt != nil { 592 | blockOpt := Block1OptionFromOption(opt) 593 | 594 | if blockOpt.Value == nil { 595 | if MessageSizeAllowed(req) != true { 596 | err = ErrMessageSizeTooLongBlockOptionValNotSet 597 | return 598 | } else { 599 | // - Block # = one and only block (sz = unspecified), whereas 0 = 16bits 600 | // - MOre bit = 0 601 | } 602 | } else { // BLock transfer request 603 | payload := msg.GetPayload().GetBytes() 604 | payloadLen := uint32(len(payload)) 605 | blockSize := blockOpt.BlockSizeLength() 606 | currSeq := uint32(0) 607 | totalBlocks := uint32(payloadLen / blockSize) 608 | completed := false 609 | 610 | var wg sync.WaitGroup 611 | wg.Add(1) 612 | 613 | for completed == false { 614 | if currSeq <= totalBlocks { 615 | 616 | var blockPayloadStart uint32 617 | var blockPayloadEnd uint32 618 | var blockPayload []byte 619 | 620 | blockPayloadStart = currSeq*uint32(blockSize) + (currSeq * 1) 621 | 622 | more := true 623 | if currSeq == totalBlocks { 624 | more = false 625 | blockPayloadEnd = payloadLen 626 | } else { 627 | blockPayloadEnd = blockPayloadStart + uint32(blockSize) 628 | } 629 | 630 | blockPayload = payload[blockPayloadStart:blockPayloadEnd] 631 | 632 | blockOpt = NewBlock1Option(blockOpt.Size(), more, currSeq) 633 | msg.ReplaceOptions(blockOpt.Code, []Option{blockOpt}) 634 | modifiedMsg := msg.(*CoapMessage) 635 | modifiedMsg.SetMessageId(GenerateMessageID()) 636 | modifiedMsg.SetPayload(NewBytesPayload(blockPayload)) 637 | 638 | // send message 639 | _, err2 := c.sendMessage(msg) 640 | if err2 != nil { 641 | wg.Done() 642 | return 643 | } 644 | currSeq = currSeq + 1 645 | 646 | } else { 647 | completed = true 648 | wg.Done() 649 | } 650 | } 651 | } 652 | } 653 | resp, err = c.sendMessage(msg) 654 | return 655 | } 656 | 657 | func (c *DTLSConnection) sendMessage(msg Message) (resp Response, err error) { 658 | 659 | if msg == nil { 660 | return nil, ErrNilMessage 661 | } 662 | 663 | b, err := MessageToBytes(msg) 664 | if err != nil { 665 | return 666 | } 667 | 668 | if msg.GetMessageType() == MessageNonConfirmable { 669 | go c.Write(b) 670 | resp = NewResponse(NewEmptyMessage(msg.GetMessageId()), nil) 671 | return 672 | } 673 | 674 | _, err = c.Write(b) 675 | if err != nil { 676 | return 677 | } 678 | 679 | msgBuf := make([]byte, 1500) 680 | if msg.GetMessageType() == MessageAcknowledgment { 681 | resp = NewResponse(NewEmptyMessage(msg.GetMessageId()), nil) 682 | return 683 | } 684 | 685 | n, err := c.Read(msgBuf) 686 | if err != nil { 687 | return 688 | } 689 | 690 | respMsg, err := BytesToMessage(msgBuf[:n]) 691 | if err != nil { 692 | return 693 | } 694 | 695 | resp = NewResponse(respMsg, nil) 696 | 697 | if msg.GetMessageType() == MessageConfirmable { 698 | // TODO: Send out message and wait for a confirm. If confirmation not retrieved, 699 | // resend (taking into account timeouts and back-off transmissions 700 | 701 | // c.Send(NewRequestFromMessage(msg)) 702 | } 703 | return 704 | } 705 | 706 | func (c *DTLSConnection) Write(b []byte) (int, error) { 707 | if atomic.CompareAndSwapInt32(&c.connected, 0, 1) { 708 | if err := c.connect(); err != nil { 709 | return 0, err 710 | } 711 | } 712 | length := len(b) 713 | ret := C.SSL_write(c.ssl, unsafe.Pointer(&b[0]), C.int(length)) 714 | if err := c.getError(ret); err != nil { 715 | return 0, err 716 | } 717 | 718 | return int(ret), nil 719 | } 720 | 721 | func (c *DTLSConnection) Read(b []byte) (int, error) { 722 | if atomic.CompareAndSwapInt32(&c.connected, 0, 1) { 723 | if err := c.connect(); err != nil { 724 | return 0, err 725 | } 726 | } 727 | 728 | length := len(b) 729 | ret := C.SSL_read(c.ssl, unsafe.Pointer(&b[0]), C.int(length)) 730 | if err := c.getError(ret); err != nil { 731 | return 0, err 732 | } 733 | 734 | // if there's no error, but a return value of 0 735 | // let's say it's an EOF 736 | if ret == 0 { 737 | return 0, io.EOF 738 | } 739 | 740 | return int(ret), nil 741 | } 742 | 743 | func (c *DTLSConnection) Close() error { 744 | if c.closed { 745 | return nil 746 | } 747 | c.closed = true 748 | defer func() { 749 | C.SSL_free(c.ssl) 750 | }() 751 | 752 | ret := C.SSL_shutdown(c.ssl) 753 | if int(ret) == 0 { 754 | ret = C.SSL_shutdown(c.ssl) 755 | if int(ret) != 1 { 756 | return c.getError(ret) 757 | } 758 | } 759 | return nil 760 | } 761 | 762 | func (c *DTLSConnection) connect() error { 763 | ret := C.SSL_connect(c.ssl) 764 | if err := c.getError(ret); err != nil { 765 | return err 766 | } 767 | return nil 768 | } 769 | 770 | func (c *DTLSConnection) getError(ret C.int) error { 771 | err := C.SSL_get_error(c.ssl, ret) 772 | switch err { 773 | case C.SSL_ERROR_NONE: 774 | return nil 775 | case C.SSL_ERROR_ZERO_RETURN: 776 | return io.EOF 777 | case C.SSL_ERROR_SYSCALL: 778 | if int(C.ERR_peek_error()) != 0 { 779 | return syscall.Errno(C.get_errno()) 780 | } 781 | 782 | default: 783 | msg := "" 784 | for { 785 | errCode := C.ERR_get_error() 786 | if errCode == 0 { 787 | break 788 | } 789 | msg += getErrorString(errCode) 790 | } 791 | C.ERR_clear_error() 792 | return errors.New(msg) 793 | } 794 | return nil 795 | } 796 | 797 | //export go_conn_bio_write 798 | func go_conn_bio_write(bio *C.BIO, buf *C.char, num C.int) C.int { 799 | client := DTLS_CLIENT_CONNECTIONS[*(*int32)(C.BIO_get_data(bio))] 800 | data := goSliceFromCString(buf, int(num)) 801 | n, err := client.conn.Write(data) 802 | if err != nil && err != io.EOF { 803 | //We expect either a syscall error 804 | //or a netOp error wrapping a syscall error 805 | TESTERR: 806 | switch err.(type) { 807 | case syscall.Errno: 808 | C.set_errno(C.int(err.(syscall.Errno))) 809 | case *net.OpError: 810 | err = err.(*net.OpError).Err 811 | break TESTERR 812 | } 813 | return C.int(-1) 814 | } 815 | return C.int(n) 816 | } 817 | 818 | //export go_conn_bio_read 819 | func go_conn_bio_read(bio *C.BIO, buf *C.char, num C.int) C.int { 820 | client := DTLS_CLIENT_CONNECTIONS[*(*int32)(C.BIO_get_data(bio))] 821 | data := goSliceFromCString(buf, int(num)) 822 | n, err := client.conn.Read(data) 823 | if err == nil { 824 | return C.int(n) 825 | } 826 | 827 | if err == io.EOF || err == io.ErrUnexpectedEOF { 828 | return 0 829 | } 830 | //We expect either a syscall error 831 | //or a netOp error wrapping a syscall error 832 | 833 | TESTERR: 834 | switch err.(type) { 835 | case syscall.Errno: 836 | C.set_errno(C.int(err.(syscall.Errno))) 837 | case *net.OpError: 838 | err = err.(*net.OpError).Err 839 | break TESTERR 840 | } 841 | return C.int(-1) 842 | } 843 | 844 | //export go_conn_bio_free 845 | func go_conn_bio_free(bio *C.BIO) C.int { 846 | client := DTLS_CLIENT_CONNECTIONS[*(*int32)(C.BIO_get_data(bio))] 847 | client.Close() 848 | if C.int(C.BIO_get_shutdown(bio)) != 0 { 849 | C.BIO_set_data(bio, nil) 850 | C.BIO_set_flags(bio, 0) 851 | C.BIO_set_init(bio, 0) 852 | } 853 | return C.int(1) 854 | } 855 | 856 | //export go_psk_callback 857 | func go_psk_callback(ssl *C.SSL, hint *C.char, identity *C.char, max_identity_len C.uint, psk *C.char, max_psk_len C.uint) C.uint { 858 | bio := C.SSL_get_rbio(ssl) 859 | client := DTLS_CLIENT_CONNECTIONS[*(*int32)(C.BIO_get_data(bio))] 860 | 861 | if client.pskId == nil || client.psk == nil { 862 | return 0 863 | } 864 | 865 | if len(*client.pskId) >= int(max_identity_len) || len(client.psk) >= int(max_psk_len) { 866 | logMsg("PSKID or PSK too large") 867 | return 0 868 | } 869 | targetId := goSliceFromCString(identity, int(max_identity_len)) 870 | copy(targetId, *client.pskId) 871 | targetPsk := goSliceFromCString(psk, int(max_psk_len)) 872 | return C.uint(copy(targetPsk, client.psk)) 873 | } 874 | -------------------------------------------------------------------------------- /empty.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | func NewEmptyPayload() MessagePayload { 4 | return &EmptyPayload{} 5 | } 6 | 7 | // Represents an empty message payload 8 | type EmptyPayload struct { 9 | } 10 | 11 | func (p *EmptyPayload) GetBytes() []byte { 12 | return []byte{} 13 | } 14 | 15 | func (p *EmptyPayload) Length() int { 16 | return 0 17 | } 18 | 19 | func (p *EmptyPayload) String() string { 20 | return "" 21 | } 22 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | func NewEvents() *ServerEvents { 4 | return &ServerEvents{ 5 | evtFnNotify: []FnEventNotify{}, 6 | evtFnStart: []FnEventStart{}, 7 | evtFnClose: []FnEventClose{}, 8 | evtFnDiscover: []FnEventDiscover{}, 9 | evtFnError: []FnEventError{}, 10 | evtFnObserve: []FnEventObserve{}, 11 | evtFnObserveCancel: []FnEventObserveCancel{}, 12 | evtFnMessage: []FnEventMessage{}, 13 | evtFnBlockMessage: []FnEventBlockMessage{}, 14 | } 15 | } 16 | 17 | // This holds the various events which are triggered throughout 18 | // an application's lifetime 19 | type ServerEvents struct { 20 | evtFnNotify []FnEventNotify 21 | evtFnStart []FnEventStart 22 | evtFnClose []FnEventClose 23 | evtFnDiscover []FnEventDiscover 24 | evtFnError []FnEventError 25 | evtFnObserve []FnEventObserve 26 | evtFnObserveCancel []FnEventObserveCancel 27 | evtFnMessage []FnEventMessage 28 | evtFnBlockMessage []FnEventBlockMessage 29 | } 30 | 31 | // OnNotify is Fired when an observeed resource is notified 32 | func (ce *ServerEvents) OnNotify(fn FnEventNotify) { 33 | ce.evtFnNotify = append(ce.evtFnNotify, fn) 34 | } 35 | 36 | // Fired when the server/client starts up 37 | func (ce *ServerEvents) OnStart(fn FnEventStart) { 38 | ce.evtFnStart = append(ce.evtFnStart, fn) 39 | } 40 | 41 | // Fired when the server/client closes 42 | func (ce *ServerEvents) OnClose(fn FnEventClose) { 43 | ce.evtFnClose = append(ce.evtFnClose, fn) 44 | } 45 | 46 | // Fired when a discovery request is triggered 47 | func (ce *ServerEvents) OnDiscover(fn FnEventDiscover) { 48 | ce.evtFnDiscover = append(ce.evtFnDiscover, fn) 49 | } 50 | 51 | // Catch-all event which is fired when an error occurs 52 | func (ce *ServerEvents) OnError(fn FnEventError) { 53 | ce.evtFnError = append(ce.evtFnError, fn) 54 | } 55 | 56 | // Fired when an observe request is triggered for a resource 57 | func (ce *ServerEvents) OnObserve(fn FnEventObserve) { 58 | ce.evtFnObserve = append(ce.evtFnObserve, fn) 59 | } 60 | 61 | // Fired when an observe-cancel request is triggered foa r esource 62 | func (ce *ServerEvents) OnObserveCancel(fn FnEventObserveCancel) { 63 | ce.evtFnObserveCancel = append(ce.evtFnObserveCancel, fn) 64 | } 65 | 66 | // Fired when a message is received 67 | func (ce *ServerEvents) OnMessage(fn FnEventMessage) { 68 | ce.evtFnMessage = append(ce.evtFnMessage, fn) 69 | } 70 | 71 | // Fired when a block messageis received 72 | func (ce *ServerEvents) OnBlockMessage(fn FnEventBlockMessage) { 73 | ce.evtFnBlockMessage = append(ce.evtFnBlockMessage, fn) 74 | } 75 | 76 | // Fires the "OnNotify" event 77 | func (ce *ServerEvents) Notify(resource string, value interface{}, msg Message) { 78 | for _, fn := range ce.evtFnNotify { 79 | fn(resource, value, msg) 80 | } 81 | } 82 | 83 | // Fires the "OnStarted" event 84 | func (ce *ServerEvents) Started(server CoapServer) { 85 | for _, fn := range ce.evtFnStart { 86 | fn(server) 87 | } 88 | } 89 | 90 | // Fires the "OnClosed" event 91 | func (ce *ServerEvents) Closed(server CoapServer) { 92 | for _, fn := range ce.evtFnClose { 93 | fn(server) 94 | } 95 | } 96 | 97 | // Fires the "OnDiscover" event 98 | func (ce *ServerEvents) Discover() { 99 | for _, fn := range ce.evtFnDiscover { 100 | fn() 101 | } 102 | } 103 | 104 | // Fires the "OnError" event given an error object 105 | func (ce *ServerEvents) Error(err error) { 106 | for _, fn := range ce.evtFnError { 107 | fn(err) 108 | } 109 | } 110 | 111 | // Fires the "OnObserve" event for a given resource 112 | func (ce *ServerEvents) Observe(resource string, msg Message) { 113 | for _, fn := range ce.evtFnObserve { 114 | fn(resource, msg) 115 | } 116 | } 117 | 118 | // Fires the "OnObserveCancelled" event for a given resource 119 | func (ce *ServerEvents) ObserveCancelled(resource string, msg Message) { 120 | for _, fn := range ce.evtFnObserveCancel { 121 | fn(resource, msg) 122 | } 123 | } 124 | 125 | // Fires the "OnMessage" event. The 'inbound' variables determines if the 126 | // message is inbound or outgoing 127 | func (ce *ServerEvents) Message(msg Message, inbound bool) { 128 | for _, fn := range ce.evtFnMessage { 129 | fn(msg, inbound) 130 | } 131 | } 132 | 133 | // Fires the "OnBlockMessage" event. The 'inbound' variables determines if the 134 | // message is inbound or outgoing 135 | func (ce *ServerEvents) BlockMessage(msg Message, inbound bool) { 136 | for _, fn := range ce.evtFnBlockMessage { 137 | fn(msg, inbound) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestEvents(t *testing.T) { 12 | events := NewEvents() 13 | 14 | assert.NotNil(t, events) 15 | 16 | // OnNotify 17 | evtOnNotifyCalled := false 18 | events.OnNotify(func(string, interface{}, Message) { 19 | evtOnNotifyCalled = true 20 | }) 21 | events.Notify("/test", "", nil) 22 | 23 | // OnStarted 24 | evtOnStartedCalled := false 25 | events.OnStart(func(CoapServer) { 26 | evtOnStartedCalled = true 27 | }) 28 | events.Started(nil) 29 | 30 | // OnClosed 31 | evtOnClosedCalled := false 32 | events.OnClose(func(CoapServer) { 33 | evtOnClosedCalled = true 34 | }) 35 | events.Closed(nil) 36 | 37 | // OnDiscover 38 | evtOnDiscoverCalled := false 39 | events.OnDiscover(func() { 40 | evtOnDiscoverCalled = true 41 | }) 42 | events.Discover() 43 | 44 | // OnError 45 | evtOnErrorCalled := false 46 | events.OnError(func(error) { 47 | evtOnErrorCalled = true 48 | }) 49 | events.Error(errors.New("An error occured")) 50 | 51 | // OnObserve 52 | evtOnObserveCalled := false 53 | events.OnObserve(func(string, Message) { 54 | evtOnObserveCalled = true 55 | }) 56 | events.Observe("/test", nil) 57 | 58 | // OnObserveCancelled 59 | evtOnObserveCancelledCalled := false 60 | events.OnObserveCancel(func(string, Message) { 61 | evtOnObserveCancelledCalled = true 62 | }) 63 | events.ObserveCancelled("/test", nil) 64 | 65 | // OnMessage 66 | evtOnMessageCalled := false 67 | events.OnMessage(func(Message, bool) { 68 | evtOnMessageCalled = true 69 | }) 70 | events.Message(nil, true) 71 | 72 | time.Sleep(3000) 73 | 74 | assert.True(t, evtOnNotifyCalled) 75 | assert.True(t, evtOnStartedCalled) 76 | assert.True(t, evtOnClosedCalled) 77 | assert.True(t, evtOnDiscoverCalled) 78 | assert.True(t, evtOnErrorCalled) 79 | assert.True(t, evtOnObserveCalled) 80 | assert.True(t, evtOnObserveCancelledCalled) 81 | assert.True(t, evtOnMessageCalled) 82 | } 83 | -------------------------------------------------------------------------------- /examples/block1/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | 7 | "github.com/zubairhamed/canopus" 8 | ) 9 | 10 | func main() { 11 | conn, err := canopus.Dial("localhost:5683") 12 | 13 | file, err := ioutil.ReadFile("./ietf-block.htm") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | 18 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Post) 19 | blockOpt := canopus.NewBlock1Option(canopus.BlockSize16, true, 0) 20 | 21 | req.GetMessage().SetBlock1Option(blockOpt) 22 | req.SetRequestURI("/blockupload") 23 | req.SetPayload(file) 24 | 25 | resp, err := conn.Send(req) 26 | if err != nil { 27 | log.Println(err) 28 | } else { 29 | log.Println("Got Response:") 30 | log.Println(resp.GetMessage().GetPayload().String()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/block1/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | 7 | "github.com/zubairhamed/canopus" 8 | ) 9 | 10 | func main() { 11 | server := canopus.NewServer() 12 | 13 | server.Get("/blockinfo", func(req canopus.Request) canopus.Response { 14 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 15 | msg.SetStringPayload("Acknowledged: " + req.GetMessage().GetPayload().String()) 16 | 17 | res := canopus.NewResponse(msg, nil) 18 | 19 | return res 20 | }) 21 | 22 | server.Post("/blockupload", func(req canopus.Request) canopus.Response { 23 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 24 | msg.SetStringPayload("Acknowledged: " + req.GetMessage().GetPayload().String()) 25 | res := canopus.NewResponse(msg, nil) 26 | 27 | // Save to file 28 | payload := req.GetMessage().GetPayload().GetBytes() 29 | log.Println("len", len(payload)) 30 | err := ioutil.WriteFile("output.html", payload, 0644) 31 | if err != nil { 32 | log.Println(err) 33 | } 34 | 35 | return res 36 | }) 37 | 38 | server.OnBlockMessage(func(msg canopus.Message, inbound bool) { 39 | // log.Println("Incoming Block Message:") 40 | // canopus.PrintMessage(msg) 41 | }) 42 | 43 | server.ListenAndServe(":5683") 44 | <-make(chan struct{}) 45 | } 46 | -------------------------------------------------------------------------------- /examples/discovery/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "crypto/rand" 7 | 8 | "github.com/zubairhamed/canopus" 9 | ) 10 | 11 | func main() { 12 | fmt.Println("Starting up") 13 | server := canopus.NewServer() 14 | 15 | server.ListenAndServeDTLS(":5682") 16 | 17 | fmt.Println("New Request..") 18 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Post, canopus.GenerateMessageID()) 19 | // req.SetStringPayload(BuildModelResourceStringPayload(c.enabledObjects)) 20 | req.SetRequestURI("/rd") 21 | req.SetURIQuery("ep", "name") 22 | 23 | // session := canopus.NewUDPServerSession("localhost:5684", server.GetConnection(), server) 24 | 25 | // DTLS Version 26 | fmt.Println("Initializing SSL") 27 | secret := make([]byte, 32) 28 | if n, err := rand.Read(secret); n != 32 || err != nil { 29 | panic(err) 30 | } 31 | server.(*canopus.DefaultCoapServer).SetCookieSecret(secret) 32 | server.HandlePSK(func(id string) []byte { 33 | return []byte("secretPSK") 34 | }) 35 | 36 | ctx, err := canopus.NewServerDtlsContext() 37 | if err != nil { 38 | panic(err.Error()) 39 | } 40 | session := canopus.NewDTLSServerSession("localhost:5684", server.GetConnection(), server, ctx) 41 | 42 | fmt.Println("Sending Message") 43 | resp, err := canopus.SendMessage(req.GetMessage(), session) 44 | fmt.Println("Response", resp, err) 45 | 46 | canopus.PrintMessage(resp.GetMessage()) 47 | 48 | <-make(chan struct{}) 49 | } 50 | -------------------------------------------------------------------------------- /examples/dtls/simple-psk/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zubairhamed/canopus" 7 | ) 8 | 9 | func main() { 10 | conn, err := canopus.DialDTLS("localhost:5684", "canopus", "secretPSK") 11 | if err != nil { 12 | panic(err.Error()) 13 | } 14 | 15 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Get) 16 | req.SetStringPayload("Hello, canopus") 17 | req.SetRequestURI("/hello") 18 | 19 | resp, err := conn.Send(req) 20 | if err != nil { 21 | panic(err.Error()) 22 | } 23 | 24 | fmt.Println("Got Response:" + resp.GetMessage().GetPayload().String()) 25 | } 26 | -------------------------------------------------------------------------------- /examples/dtls/simple-psk/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zubairhamed/canopus" 7 | ) 8 | 9 | func main() { 10 | server := canopus.NewServer() 11 | 12 | server.Get("/hello", func(req canopus.Request) canopus.Response { 13 | fmt.Println("Hello Called") 14 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 15 | msg.SetStringPayload("Acknowledged: " + req.GetMessage().GetPayload().String()) 16 | res := canopus.NewResponse(msg, nil) 17 | 18 | return res 19 | }) 20 | 21 | server.HandlePSK(func(id string) []byte { 22 | return []byte("secretPSK") 23 | }) 24 | 25 | server.ListenAndServeDTLS(":5684") 26 | 27 | <-make(chan struct{}) 28 | } 29 | -------------------------------------------------------------------------------- /examples/observe/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zubairhamed/canopus" 7 | ) 8 | 9 | func main() { 10 | conn, err := canopus.Dial("localhost:5683") 11 | 12 | tok, err := conn.ObserveResource("/watch/this") 13 | if err != nil { 14 | panic(err.Error()) 15 | } 16 | 17 | obsChannel := make(chan canopus.ObserveMessage) 18 | done := make(chan bool) 19 | go conn.Observe(obsChannel) 20 | 21 | notifyCount := 0 22 | go func() { 23 | for { 24 | select { 25 | case obsMsg, open := <-obsChannel: 26 | if open { 27 | if notifyCount == 5 { 28 | fmt.Println("[CLIENT >> ] Canceling observe after 5 notifications..") 29 | go conn.CancelObserveResource("watch/this", tok) 30 | go conn.StopObserve(obsChannel) 31 | done <- true 32 | return 33 | } else { 34 | notifyCount++ 35 | // msg := obsMsg.Msg\ 36 | resource := obsMsg.GetResource() 37 | val := obsMsg.GetValue() 38 | 39 | fmt.Println("[CLIENT >> ] Got Change Notification for resource and value: ", notifyCount, resource, val) 40 | } 41 | } else { 42 | done <- true 43 | return 44 | } 45 | } 46 | } 47 | }() 48 | <-done 49 | fmt.Println("Done") 50 | } 51 | -------------------------------------------------------------------------------- /examples/observe/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/zubairhamed/canopus" 10 | ) 11 | 12 | func main() { 13 | server := canopus.NewServer() 14 | server.Get("/watch/this", func(req canopus.Request) canopus.Response { 15 | msg := canopus.NewMessageOfType(canopus.MessageAcknowledgment, req.GetMessage().GetMessageId(), canopus.NewPlainTextPayload("Acknowledged")) 16 | res := canopus.NewResponse(msg, nil) 17 | 18 | return res 19 | }) 20 | 21 | ticker := time.NewTicker(3 * time.Second) 22 | go func() { 23 | for { 24 | select { 25 | case <-ticker.C: 26 | changeVal := strconv.Itoa(rand.Int()) 27 | fmt.Println("[SERVER << ] Change of value -->", changeVal) 28 | 29 | server.NotifyChange("/watch/this", changeVal, false) 30 | } 31 | } 32 | }() 33 | 34 | server.OnMessage(func(msg canopus.Message, inbound bool) { 35 | canopus.PrintMessage(msg) 36 | }) 37 | 38 | server.OnObserve(func(resource string, msg canopus.Message) { 39 | fmt.Println("[SERVER << ] Observe Requested for " + resource) 40 | }) 41 | 42 | server.ListenAndServe(":5683") 43 | <-make(chan struct{}) 44 | } 45 | -------------------------------------------------------------------------------- /examples/proxy/coap/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/zubairhamed/canopus" 4 | 5 | func main() { 6 | conn, err := canopus.Dial("localhost:5683") 7 | if err != nil { 8 | panic(err.Error()) 9 | } 10 | 11 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Get, canopus.GenerateMessageID()) 12 | req.SetProxyURI("coap://localhost:5685/proxycall") 13 | 14 | canopus.PrintMessage(req.GetMessage()) 15 | resp, err := conn.Send(req) 16 | if err != nil { 17 | println("err", err) 18 | } 19 | canopus.PrintMessage(resp.GetMessage()) 20 | } 21 | -------------------------------------------------------------------------------- /examples/proxy/coap/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/zubairhamed/canopus" 4 | 5 | func main() { 6 | server := canopus.NewServer() 7 | server.ProxyOverCoap(true) 8 | 9 | server.Get("/proxycall", func(req canopus.Request) canopus.Response { 10 | canopus.PrintMessage(req.GetMessage()) 11 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 12 | msg.SetStringPayload("Acknowledged: " + req.GetMessage().GetPayload().String()) 13 | res := canopus.NewResponse(msg, nil) 14 | 15 | return res 16 | }) 17 | server.ListenAndServe(":5683") 18 | <-make(chan struct{}) 19 | } 20 | -------------------------------------------------------------------------------- /examples/proxy/coap/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/zubairhamed/canopus" 4 | 5 | func main() { 6 | server := canopus.NewServer() 7 | 8 | server.Get("/proxycall", func(req canopus.Request) canopus.Response { 9 | canopus.PrintMessage(req.GetMessage()) 10 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 11 | msg.SetStringPayload("Data from :5685 -- " + req.GetMessage().GetPayload().String()) 12 | res := canopus.NewResponse(msg, nil) 13 | 14 | return res 15 | }) 16 | server.ListenAndServe(":5685") 17 | <-make(chan struct{}) 18 | } 19 | -------------------------------------------------------------------------------- /examples/proxy/http/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/zubairhamed/canopus" 4 | 5 | func main() { 6 | conn, err := canopus.Dial("localhost:5683") 7 | if err != nil { 8 | panic(err.Error()) 9 | } 10 | 11 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Get, canopus.GenerateMessageID()) 12 | req.SetProxyURI("https://httpbin.org/get") 13 | 14 | canopus.PrintMessage(req.GetMessage()) 15 | resp, err := conn.Send(req) 16 | if err != nil { 17 | println("err", err) 18 | } 19 | canopus.PrintMessage(resp.GetMessage()) 20 | } 21 | -------------------------------------------------------------------------------- /examples/proxy/http/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/zubairhamed/canopus" 4 | 5 | func main() { 6 | server := canopus.NewServer() 7 | server.ProxyOverHttp(true) 8 | 9 | server.ListenAndServe(":5683") 10 | <-make(chan struct{}) 11 | 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zubairhamed/canopus" 7 | ) 8 | 9 | func main() { 10 | fmt.Println("Connecting to CoAP Server") 11 | conn, err := canopus.Dial("localhost:5683") 12 | if err != nil { 13 | panic(err.Error()) 14 | } 15 | 16 | req := canopus.NewRequest(canopus.MessageConfirmable, canopus.Get) 17 | req.SetStringPayload("Hello, canopus") 18 | req.SetRequestURI("/hello") 19 | 20 | fmt.Println("Sending request..") 21 | resp, err := conn.Send(req) 22 | if err != nil { 23 | panic(err.Error()) 24 | } 25 | 26 | fmt.Println("Got Response:" + resp.GetMessage().GetPayload().String()) 27 | } 28 | -------------------------------------------------------------------------------- /examples/simple/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/zubairhamed/canopus" 7 | ) 8 | 9 | func main() { 10 | server := canopus.NewServer() 11 | 12 | server.Get("/hello", func(req canopus.Request) canopus.Response { 13 | log.Println("Hello Called") 14 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 15 | msg.SetStringPayload("Acknowledged with response : " + req.GetMessage().GetPayload().String()) 16 | 17 | res := canopus.NewResponse(msg, nil) 18 | return res 19 | }) 20 | 21 | server.Post("/hello", func(req canopus.Request) canopus.Response { 22 | log.Println("Hello Called via POST") 23 | 24 | msg := canopus.ContentMessage(req.GetMessage().GetMessageId(), canopus.MessageAcknowledgment) 25 | msg.SetStringPayload("Acknowledged: " + req.GetMessage().GetPayload().String()) 26 | res := canopus.NewResponse(msg, nil) 27 | return res 28 | }) 29 | 30 | server.Get("/basic", func(req canopus.Request) canopus.Response { 31 | msg := canopus.NewMessageOfType(canopus.MessageAcknowledgment, req.GetMessage().GetMessageId(), canopus.NewPlainTextPayload("Acknowledged")) 32 | res := canopus.NewResponse(msg, nil) 33 | return res 34 | }) 35 | 36 | server.Get("/basic/json", func(req canopus.Request) canopus.Response { 37 | msg := canopus.NewMessageOfType(canopus.MessageAcknowledgment, req.GetMessage().GetMessageId(), nil) 38 | 39 | res := canopus.NewResponse(msg, nil) 40 | 41 | return res 42 | }) 43 | 44 | server.Get("/basic/xml", func(req canopus.Request) canopus.Response { 45 | msg := canopus.NewMessageOfType(canopus.MessageAcknowledgment, req.GetMessage().GetMessageId(), nil) 46 | res := canopus.NewResponse(msg, nil) 47 | 48 | return res 49 | }) 50 | 51 | server.OnMessage(func(msg canopus.Message, inbound bool) { 52 | canopus.PrintMessage(msg) 53 | }) 54 | 55 | server.ListenAndServe(":5683") 56 | <-make(chan struct{}) 57 | } 58 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | func init() { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | ) 7 | 8 | func NewJSONPayload(obj interface{}) MessagePayload { 9 | return &JSONPayload{ 10 | obj: obj, 11 | } 12 | } 13 | 14 | // Represents a message payload containing JSON String 15 | type JSONPayload struct { 16 | obj interface{} 17 | } 18 | 19 | func (p *JSONPayload) GetBytes() []byte { 20 | o, err := json.MarshalIndent(p.obj, "", " ") 21 | 22 | if err != nil { 23 | log.Println(err) 24 | 25 | return []byte{} 26 | } 27 | 28 | return []byte(string(o)) 29 | } 30 | 31 | func (p *JSONPayload) Length() int { 32 | return 0 33 | } 34 | 35 | func (p *JSONPayload) String() string { 36 | o, _ := json.Marshal(p.obj) 37 | 38 | return string(o) 39 | } 40 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "log" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | // Instantiates a new message object 13 | // messageType (e.g. Confirm/Non-Confirm) 14 | // CoAP code 404 - Not found etc 15 | // Message ID uint16 unique id 16 | func NewMessage(messageType uint8, code CoapCode, messageID uint16) Message { 17 | return &CoapMessage{ 18 | MessageType: messageType, 19 | MessageID: messageID, 20 | Code: code, 21 | Token: []byte(GenerateToken(8)), 22 | } 23 | } 24 | 25 | // Instantiates an empty message with a given message id 26 | func NewEmptyMessage(id uint16) Message { 27 | msg := NewMessageOfType(MessageAcknowledgment, id, nil) 28 | 29 | return msg 30 | } 31 | 32 | // Instantiates an empty message of a specific type and message id 33 | func NewMessageOfType(t uint8, id uint16, payload MessagePayload) Message { 34 | return &CoapMessage{ 35 | MessageType: t, 36 | MessageID: id, 37 | Payload: payload, 38 | } 39 | } 40 | 41 | /* 42 | 0 1 2 3 43 | 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 44 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 45 | |Ver| T | TKL | Code | Message ID | 46 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 47 | | Token (if any, TKL bytes) ... 48 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 49 | | Options (if any) ... 50 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 51 | |1 1 1 1 1 1 1 1| Payload (if any) ... 52 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 53 | */ 54 | 55 | // Converts an array of bytes to a Mesasge object. 56 | // An error is returned if a parsing error occurs 57 | func BytesToMessage(data []byte) (Message, error) { 58 | msg := &CoapMessage{} 59 | 60 | dataLen := len(data) 61 | if dataLen < 4 { 62 | return msg, ErrPacketLengthLessThan4 63 | } 64 | 65 | ver := data[DataHeader] >> 6 66 | if ver != 1 { 67 | return nil, ErrInvalidCoapVersion 68 | } 69 | 70 | msg.MessageType = data[DataHeader] >> 4 & 0x03 71 | tokenLength := data[DataHeader] & 0x0f 72 | msg.Code = CoapCode(data[DataCode]) 73 | 74 | msg.MessageID = binary.BigEndian.Uint16(data[DataMsgIDStart:DataMsgIDEnd]) 75 | 76 | // Token 77 | if tokenLength > 0 { 78 | msg.Token = make([]byte, tokenLength) 79 | token := data[DataTokenStart : DataTokenStart+tokenLength] 80 | copy(msg.Token, token) 81 | } 82 | 83 | /* 84 | 0 1 2 3 4 5 6 7 85 | +---------------+---------------+ 86 | | | | 87 | | Option Delta | Option Length | 1 byte 88 | | | | 89 | +---------------+---------------+ 90 | \ \ 91 | / Option Delta / 0-2 bytes 92 | \ (extended) \ 93 | +-------------------------------+ 94 | \ \ 95 | / Option Length / 0-2 bytes 96 | \ (extended) \ 97 | +-------------------------------+ 98 | \ \ 99 | / / 100 | \ \ 101 | / Option Value / 0 or more bytes 102 | \ \ 103 | / / 104 | \ \ 105 | +-------------------------------+ 106 | */ 107 | 108 | tmp := data[DataTokenStart+msg.GetTokenLength():] 109 | 110 | lastOptionID := uint(0) 111 | for len(tmp) > 0 { 112 | if tmp[0] == PayloadMarker { 113 | tmp = tmp[1:] 114 | break 115 | } 116 | 117 | optionDelta := uint(tmp[0] >> 4) 118 | optionLength := uint(tmp[0] & 0x0f) 119 | 120 | tmp = tmp[1:] 121 | switch optionDelta { 122 | case 13: 123 | optionDeltaExtended := uint(tmp[0]) 124 | optionDelta += optionDeltaExtended 125 | tmp = tmp[1:] 126 | break 127 | 128 | case 14: 129 | optionDeltaExtended := uint(decodeInt(tmp[:2])) 130 | optionDelta = uint(optionDeltaExtended + 269) 131 | tmp = tmp[2:] 132 | break 133 | 134 | case 15: 135 | return msg, ErrOptionDeltaUsesValue15 136 | } 137 | lastOptionID += optionDelta 138 | 139 | switch optionLength { 140 | case 13: 141 | optionLengthExtended := uint(tmp[0]) 142 | optionLength += optionLengthExtended 143 | tmp = tmp[1:] 144 | break 145 | 146 | case 14: 147 | optionLengthExtended := uint(decodeInt(tmp[:1])) 148 | optionLength += uint(optionLengthExtended - uint(269)) 149 | tmp = tmp[2:] 150 | break 151 | 152 | case 15: 153 | return msg, ErrOptionLengthUsesValue15 154 | } 155 | 156 | optCode := OptionCode(lastOptionID) 157 | if optionLength > 0 { 158 | optionValue := tmp[:optionLength] 159 | 160 | switch optCode { 161 | case OptionURIPort, OptionContentFormat, OptionMaxAge, OptionAccept, OptionSize1, 162 | OptionSize2, OptionBlock1, OptionBlock2: 163 | msg.Options = append(msg.Options, NewOption(optCode, decodeInt(optionValue))) 164 | break 165 | 166 | case OptionURIHost, OptionEtag, OptionLocationPath, OptionURIPath, OptionURIQuery, 167 | OptionLocationQuery, OptionProxyURI, OptionProxyScheme, OptionObserve: 168 | msg.Options = append(msg.Options, NewOption(optCode, string(optionValue))) 169 | break 170 | 171 | default: 172 | if lastOptionID&0x01 == 1 { 173 | log.Println("Unknown Critical Option id ", lastOptionID) 174 | return msg, ErrUnknownCriticalOption 175 | } 176 | log.Println("Warning: Unknown Option id ", optionDelta) 177 | msg.Options = append(msg.Options, NewOption(optCode, optionValue)) 178 | break 179 | } 180 | tmp = tmp[optionLength:] 181 | } else { 182 | msg.Options = append(msg.Options, NewOption(optCode, nil)) 183 | } 184 | } 185 | msg.Payload = NewBytesPayload(tmp) 186 | err := ValidateMessage(msg) 187 | 188 | return msg, err 189 | } 190 | 191 | // type to sort the coap options list (which is mandatory) prior to transmission 192 | type SortOptions []Option 193 | 194 | func (opts SortOptions) Len() int { 195 | return len(opts) 196 | } 197 | 198 | func (opts SortOptions) Swap(i, j int) { 199 | opts[i], opts[j] = opts[j], opts[i] 200 | } 201 | 202 | func (opts SortOptions) Less(i, j int) bool { 203 | return opts[i].GetCode() < opts[j].GetCode() 204 | } 205 | 206 | // Converts a message object to a byte array. Typically done prior to transmission 207 | func MessageToBytes(msg Message) ([]byte, error) { 208 | messageID := []byte{0, 0} 209 | binary.BigEndian.PutUint16(messageID, msg.GetMessageId()) 210 | 211 | buf := bytes.Buffer{} 212 | buf.Write([]byte{(1 << 6) | (msg.GetMessageType() << 4) | 0x0f&msg.GetTokenLength()}) 213 | buf.Write([]byte{byte(msg.GetCode())}) 214 | buf.Write([]byte{messageID[0]}) 215 | buf.Write([]byte{messageID[1]}) 216 | buf.Write(msg.GetToken()) 217 | 218 | // Sort Options 219 | sort.Sort(SortOptions(msg.GetAllOptions())) 220 | 221 | lastOptionCode := 0 222 | for _, opt := range msg.GetAllOptions() { 223 | optCode := int(opt.GetCode()) 224 | optDelta := optCode - lastOptionCode 225 | optDeltaValue, _ := getOptionHeaderValue(optDelta) 226 | 227 | byteValue := valueToBytes(opt.GetValue()) 228 | valueLength := len(byteValue) 229 | optLength := valueLength 230 | optLengthValue, _ := getOptionHeaderValue(optLength) 231 | 232 | buf.Write([]byte{byte(optDeltaValue<<4 | optLengthValue)}) 233 | 234 | if optDeltaValue == 13 { 235 | buf.Write([]byte{byte(optDelta - 13)}) 236 | } else if optDeltaValue == 14 { 237 | tmpBuf := new(bytes.Buffer) 238 | binary.Write(tmpBuf, binary.BigEndian, uint16(optDelta-269)) 239 | buf.Write(tmpBuf.Bytes()) 240 | } 241 | 242 | // TODO: If optDeltaValue == 15, throw error 243 | 244 | if optLengthValue == 13 { 245 | buf.Write([]byte{byte(optLength - 13)}) 246 | } else if optLengthValue == 14 { 247 | tmpBuf := new(bytes.Buffer) 248 | binary.Write(tmpBuf, binary.BigEndian, uint16(optLength-269)) 249 | buf.Write(tmpBuf.Bytes()) 250 | } 251 | 252 | buf.Write(byteValue) 253 | lastOptionCode = int(optCode) 254 | } 255 | 256 | if msg.GetPayload() != nil { 257 | if msg.GetPayload().Length() > 0 { 258 | buf.Write([]byte{PayloadMarker}) 259 | } 260 | buf.Write(msg.GetPayload().GetBytes()) 261 | } 262 | return buf.Bytes(), nil 263 | } 264 | 265 | func getOptionHeaderValue(optValue int) (int, error) { 266 | switch true { 267 | case optValue <= 12: 268 | return optValue, nil 269 | 270 | case optValue <= 268: 271 | return 13, nil 272 | 273 | case optValue <= 65804: 274 | return 14, nil 275 | } 276 | return 0, errors.New("Invalid Option Delta") 277 | } 278 | 279 | // Validates a message object and returns any error upon validation failure 280 | func ValidateMessage(msg Message) error { 281 | if msg.GetMessageType() > 3 { 282 | return ErrUnknownMessageType 283 | } 284 | 285 | if msg.GetTokenLength() > 8 { 286 | return ErrInvalidTokenLength 287 | } 288 | 289 | // Repeated Unrecognized Options 290 | for _, opt := range msg.GetAllOptions() { 291 | opts := msg.GetOptions(opt.GetCode()) 292 | 293 | if len(opts) > 1 { 294 | if !IsRepeatableOption(opts[0]) { 295 | if opts[0].GetCode()&0x01 == 1 { 296 | return ErrUnknownCriticalOption 297 | } 298 | } 299 | } 300 | } 301 | 302 | return nil 303 | } 304 | 305 | func NewBlockMessage() BlockMessage { 306 | return &CoapBlockMessage{ 307 | Sequence: 0, 308 | } 309 | } 310 | 311 | type CoapBlockMessage struct { 312 | CoapMessage 313 | MessageBuf []byte 314 | Sequence uint32 315 | } 316 | 317 | type BySequence []*CoapBlockMessage 318 | 319 | func (o BySequence) Len() int { 320 | return len(o) 321 | } 322 | 323 | func (o BySequence) Swap(i, j int) { 324 | o[i], o[j] = o[j], o[i] 325 | } 326 | 327 | func (o BySequence) Less(i, j int) bool { 328 | return o[i].Sequence < o[j].Sequence 329 | } 330 | 331 | // A Message object represents a CoAP payload 332 | type CoapMessage struct { 333 | MessageType uint8 334 | Code CoapCode 335 | MessageID uint16 336 | Payload MessagePayload 337 | Token []byte 338 | Options []Option 339 | } 340 | 341 | func (m *CoapMessage) SetMessageType(t uint8) { 342 | m.MessageType = t 343 | } 344 | 345 | func (m *CoapMessage) SetToken(t []byte) { 346 | m.Token = t 347 | } 348 | 349 | func (m *CoapMessage) SetPayload(p MessagePayload) { 350 | m.Payload = p 351 | } 352 | 353 | func (m *CoapMessage) SetMessageId(id uint16) { 354 | m.MessageID = id 355 | } 356 | 357 | func (m *CoapMessage) GetToken() []byte { 358 | return m.Token 359 | } 360 | 361 | func (m *CoapMessage) GetPayload() MessagePayload { 362 | return m.Payload 363 | } 364 | 365 | func (m *CoapMessage) GetMessageType() uint8 { 366 | return m.MessageType 367 | } 368 | 369 | func (m *CoapMessage) GetMessageId() uint16 { 370 | return m.MessageID 371 | } 372 | 373 | func (m *CoapMessage) GetCode() CoapCode { 374 | return m.Code 375 | } 376 | 377 | func (m *CoapMessage) GetAllOptions() []Option { 378 | return m.Options 379 | } 380 | 381 | func (m *CoapMessage) GetAcceptedContent() MediaType { 382 | mediaTypeCode := m.GetOption(OptionAccept).IntValue() 383 | 384 | return MediaType(mediaTypeCode) 385 | } 386 | 387 | func (m *CoapMessage) GetCodeString() string { 388 | codeClass := string(m.Code >> 5) 389 | codeDetail := string(m.Code & 0x1f) 390 | 391 | return codeClass + "." + codeDetail 392 | } 393 | 394 | func (m *CoapMessage) GetMethod() uint8 { 395 | return (byte(m.Code) & 0x1f) 396 | } 397 | 398 | func (m *CoapMessage) GetTokenLength() uint8 { 399 | return uint8(len(m.Token)) 400 | } 401 | 402 | func (m *CoapMessage) GetTokenString() string { 403 | return string(m.Token[:len(m.Token)]) 404 | } 405 | 406 | // Returns an array of options given an option code 407 | func (m CoapMessage) GetOptions(id OptionCode) []Option { 408 | var opts []Option 409 | for _, val := range m.Options { 410 | if val.GetCode() == id { 411 | opts = append(opts, val) 412 | } 413 | } 414 | return opts 415 | } 416 | 417 | // Returns the first option found for a given option code 418 | func (m CoapMessage) GetOption(id OptionCode) Option { 419 | for _, val := range m.Options { 420 | if val.GetCode() == id { 421 | return val 422 | } 423 | } 424 | return nil 425 | } 426 | 427 | // Attempts to return the string value of an Option 428 | func (m CoapMessage) GetOptionsAsString(id OptionCode) []string { 429 | opts := m.GetOptions(id) 430 | 431 | var str []string 432 | for _, o := range opts { 433 | if o.GetValue() != nil { 434 | str = append(str, o.GetValue().(string)) 435 | } 436 | } 437 | return str 438 | } 439 | 440 | // Returns the string value of the Location Path Options by joining and defining a / separator 441 | func (m *CoapMessage) GetLocationPath() string { 442 | opts := m.GetOptionsAsString(OptionLocationPath) 443 | 444 | return strings.Join(opts, "/") 445 | } 446 | 447 | // Returns the string value of the Uri Path Options by joining and defining a / separator 448 | func (m CoapMessage) GetURIPath() string { 449 | opts := m.GetOptionsAsString(OptionURIPath) 450 | 451 | return "/" + strings.Join(opts, "/") 452 | } 453 | 454 | // Add an Option to the message. If an option is not repeatable, it will replace 455 | // any existing defined Option of the same type 456 | func (m *CoapMessage) AddOption(code OptionCode, value interface{}) { 457 | opt := NewOption(code, value) 458 | if IsRepeatableOption(opt) { 459 | m.Options = append(m.Options, opt) 460 | } else { 461 | m.RemoveOptions(code) 462 | m.Options = append(m.Options, opt) 463 | } 464 | } 465 | 466 | // Add an array of Options to the message. If an option is not repeatable, it will replace 467 | // any existing defined Option of the same type 468 | func (m *CoapMessage) AddOptions(opts []Option) { 469 | for _, opt := range opts { 470 | if IsRepeatableOption(opt) { 471 | m.Options = append(m.Options, opt) 472 | } else { 473 | m.RemoveOptions(opt.GetCode()) 474 | m.Options = append(m.Options, opt) 475 | } 476 | } 477 | } 478 | 479 | func (c *CoapMessage) SetBlock1Option(opt Option) { 480 | c.AddOption(OptionBlock1, opt.GetValue()) 481 | } 482 | 483 | // Copies the given list of options from another message to this one 484 | func (m *CoapMessage) CloneOptions(cm Message, opts ...OptionCode) { 485 | for _, opt := range opts { 486 | m.AddOptions(cm.GetOptions(opt)) 487 | } 488 | } 489 | 490 | // Replace an Option 491 | func (m *CoapMessage) ReplaceOptions(code OptionCode, opts []Option) { 492 | m.RemoveOptions(code) 493 | 494 | m.AddOptions(opts) 495 | } 496 | 497 | // Removes an Option 498 | func (m *CoapMessage) RemoveOptions(id OptionCode) { 499 | var opts []Option 500 | for _, opt := range m.Options { 501 | if opt.GetCode() != id { 502 | opts = append(opts, opt) 503 | } 504 | } 505 | m.Options = opts 506 | } 507 | 508 | // Adds a string payload 509 | func (m *CoapMessage) SetStringPayload(s string) { 510 | m.Payload = NewPlainTextPayload(s) 511 | } 512 | 513 | // Determines if a message contains options for proxying (i.e. Proxy-Scheme or Proxy-Uri) 514 | func IsProxyRequest(msg Message) bool { 515 | if msg.GetOption(OptionProxyScheme) != nil || msg.GetOption(OptionProxyURI) != nil { 516 | return true 517 | } 518 | return false 519 | } 520 | 521 | func valueToBytes(value interface{}) []byte { 522 | var v uint32 523 | 524 | switch i := value.(type) { 525 | case string: 526 | return []byte(i) 527 | case []byte: 528 | return i 529 | case MediaType: 530 | v = uint32(i) 531 | case byte: 532 | v = uint32(i) 533 | case int: 534 | v = uint32(i) 535 | case int32: 536 | v = uint32(i) 537 | case uint: 538 | v = uint32(i) 539 | case uint32: 540 | v = i 541 | default: 542 | break 543 | } 544 | 545 | return encodeInt(v) 546 | } 547 | 548 | func decodeInt(b []byte) uint32 { 549 | tmp := []byte{0, 0, 0, 0} 550 | copy(tmp[4-len(b):], b) 551 | 552 | return binary.BigEndian.Uint32(tmp) 553 | } 554 | 555 | func encodeInt(v uint32) []byte { 556 | switch { 557 | case v == 0: 558 | return nil 559 | 560 | case v < 256: 561 | return []byte{byte(v)} 562 | 563 | case v < 65536: 564 | rv := []byte{0, 0} 565 | binary.BigEndian.PutUint16(rv, uint16(v)) 566 | return rv 567 | 568 | case v < 16777216: 569 | rv := []byte{0, 0, 0, 0} 570 | binary.BigEndian.PutUint32(rv, uint32(v)) 571 | return rv[1:] 572 | 573 | default: 574 | rv := []byte{0, 0, 0, 0} 575 | binary.BigEndian.PutUint32(rv, uint32(v)) 576 | return rv 577 | } 578 | } 579 | 580 | // Determines if a message contains URI targeting a CoAP resource 581 | func IsCoapURI(uri string) bool { 582 | if strings.HasPrefix(uri, "coap") || strings.HasPrefix(uri, "coaps") { 583 | return true 584 | } 585 | return false 586 | } 587 | 588 | // Determines if a message contains URI targeting an HTTP resource 589 | func IsHTTPURI(uri string) bool { 590 | if strings.HasPrefix(uri, "http") || strings.HasPrefix(uri, "https") { 591 | return true 592 | } 593 | return false 594 | 595 | } 596 | 597 | // Gets the string representation of a CoAP Method code (e.g. GET, PUT, DELETE etc) 598 | func MethodString(c CoapCode) string { 599 | switch c { 600 | case Get: 601 | return "GET" 602 | 603 | case Delete: 604 | return "DELETE" 605 | 606 | case Post: 607 | return "POST" 608 | 609 | case Put: 610 | return "PUT" 611 | } 612 | return "" 613 | } 614 | 615 | // Response Code Messages 616 | // Creates a Non-Confirmable Empty Message 617 | func EmptyMessage(messageID uint16, messageType uint8) Message { 618 | return NewMessage(messageType, CoapCodeEmpty, messageID) 619 | } 620 | 621 | // Creates a Non-Confirmable with CoAP Code 201 - Created 622 | func CreatedMessage(messageID uint16, messageType uint8) Message { 623 | return NewMessage(messageType, CoapCodeCreated, messageID) 624 | } 625 | 626 | // // Creates a Non-Confirmable with CoAP Code 202 - Deleted 627 | func DeletedMessage(messageID uint16, messageType uint8) Message { 628 | return NewMessage(messageType, CoapCodeDeleted, messageID) 629 | } 630 | 631 | // Creates a Non-Confirmable with CoAP Code 203 - Valid 632 | func ValidMessage(messageID uint16, messageType uint8) Message { 633 | return NewMessage(messageType, CoapCodeValid, messageID) 634 | } 635 | 636 | // Creates a Non-Confirmable with CoAP Code 204 - Changed 637 | func ChangedMessage(messageID uint16, messageType uint8) Message { 638 | return NewMessage(messageType, CoapCodeChanged, messageID) 639 | } 640 | 641 | // Creates a Non-Confirmable with CoAP Code 205 - Content 642 | func ContentMessage(messageID uint16, messageType uint8) Message { 643 | return NewMessage(messageType, CoapCodeContent, messageID) 644 | } 645 | 646 | // Creates a Non-Confirmable with CoAP Code 400 - Bad Request 647 | func BadRequestMessage(messageID uint16, messageType uint8) Message { 648 | return NewMessage(messageType, CoapCodeBadRequest, messageID) 649 | } 650 | 651 | func ContinueMessage(messageID uint16, messageType uint8) Message { 652 | return NewMessage(messageType, CoapCodeContinue, messageID) 653 | } 654 | 655 | // Creates a Non-Confirmable with CoAP Code 401 - Unauthorized 656 | func UnauthorizedMessage(messageID uint16, messageType uint8) Message { 657 | return NewMessage(messageType, CoapCodeUnauthorized, messageID) 658 | } 659 | 660 | // Creates a Non-Confirmable with CoAP Code 402 - Bad Option 661 | func BadOptionMessage(messageID uint16, messageType uint8) Message { 662 | return NewMessage(messageType, CoapCodeBadOption, messageID) 663 | } 664 | 665 | // Creates a Non-Confirmable with CoAP Code 403 - Forbidden 666 | func ForbiddenMessage(messageID uint16, messageType uint8) Message { 667 | return NewMessage(messageType, CoapCodeForbidden, messageID) 668 | } 669 | 670 | // Creates a Non-Confirmable with CoAP Code 404 - Not Found 671 | func NotFoundMessage(messageID uint16, messageType uint8, token []byte) (m Message) { 672 | m = NewMessage(messageType, CoapCodeNotFound, messageID) 673 | m.SetToken(token) 674 | 675 | return 676 | } 677 | 678 | // Creates a Non-Confirmable with CoAP Code 405 - Method Not Allowed 679 | func MethodNotAllowedMessage(messageID uint16, messageType uint8) Message { 680 | return NewMessage(messageType, CoapCodeMethodNotAllowed, messageID) 681 | } 682 | 683 | // Creates a Non-Confirmable with CoAP Code 406 - Not Acceptable 684 | func NotAcceptableMessage(messageID uint16, messageType uint8) Message { 685 | return NewMessage(messageType, CoapCodeNotAcceptable, messageID) 686 | } 687 | 688 | // Creates a Non-Confirmable with CoAP Code 409 - Conflict 689 | func ConflictMessage(messageID uint16, messageType uint8) Message { 690 | return NewMessage(messageType, CoapCodeConflict, messageID) 691 | } 692 | 693 | // Creates a Non-Confirmable with CoAP Code 412 - Precondition Failed 694 | func PreconditionFailedMessage(messageID uint16, messageType uint8) Message { 695 | return NewMessage(messageType, CoapCodePreconditionFailed, messageID) 696 | } 697 | 698 | // Creates a Non-Confirmable with CoAP Code 413 - Request Entity Too Large 699 | func RequestEntityTooLargeMessage(messageID uint16, messageType uint8) Message { 700 | return NewMessage(messageType, CoapCodeRequestEntityTooLarge, messageID) 701 | } 702 | 703 | // Creates a Non-Confirmable with CoAP Code 415 - Unsupported Content Format 704 | func UnsupportedContentFormatMessage(messageID uint16, messageType uint8) Message { 705 | return NewMessage(messageType, CoapCodeUnsupportedContentFormat, messageID) 706 | } 707 | 708 | // Creates a Non-Confirmable with CoAP Code 500 - Internal Server Error 709 | func InternalServerErrorMessage(messageID uint16, messageType uint8) Message { 710 | return NewMessage(messageType, CoapCodeInternalServerError, messageID) 711 | } 712 | 713 | // Creates a Non-Confirmable with CoAP Code 501 - Not Implemented 714 | func NotImplementedMessage(messageID uint16, messageType uint8) Message { 715 | return NewMessage(messageType, CoapCodeNotImplemented, messageID) 716 | } 717 | 718 | // Creates a Non-Confirmable with CoAP Code 502 - Bad Gateway 719 | func BadGatewayMessage(messageID uint16, messageType uint8) Message { 720 | return NewMessage(messageType, CoapCodeBadGateway, messageID) 721 | } 722 | 723 | // Creates a Non-Confirmable with CoAP Code 503 - Service Unavailable 724 | func ServiceUnavailableMessage(messageID uint16, messageType uint8) Message { 725 | return NewMessage(messageType, CoapCodeServiceUnavailable, messageID) 726 | } 727 | 728 | // Creates a Non-Confirmable with CoAP Code 504 - Gateway Timeout 729 | func GatewayTimeoutMessage(messageID uint16, messageType uint8) Message { 730 | return NewMessage(messageType, CoapCodeGatewayTimeout, messageID) 731 | } 732 | 733 | // Creates a Non-Confirmable with CoAP Code 505 - Proxying Not Supported 734 | func ProxyingNotSupportedMessage(messageID uint16, messageType uint8) Message { 735 | return NewMessage(messageType, CoapCodeProxyingNotSupported, messageID) 736 | } 737 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMessage(t *testing.T) { 11 | assert.NotNil(t, NewMessage(MessageConfirmable, Get, 12345)) 12 | assert.NotNil(t, NewEmptyMessage(12345)) 13 | } 14 | 15 | func TestInvalidMessage(t *testing.T) { 16 | _, err := BytesToMessage(make([]byte, 0)) 17 | 18 | assert.NotNil(t, err, "Message should be invalid") 19 | 20 | _, err = BytesToMessage(make([]byte, 4)) 21 | assert.NotNil(t, err, "Message should be invalid") 22 | } 23 | 24 | func TestMessagValidation(t *testing.T) { 25 | // ValidateMessage() 26 | } 27 | 28 | func TestMessageConversion(t *testing.T) { 29 | 30 | msg := NewBasicConfirmableMessage() 31 | msgId := msg.GetMessageId() 32 | 33 | // Byte 1 34 | msg.AddOption(OptionContentFormat, MediaTypeApplicationLinkFormat) 35 | 36 | // Convert Message to BYte 37 | b, err := MessageToBytes(msg) 38 | 39 | // Reconvert Back Bytes to Message 40 | newMsg, err := BytesToMessage(b) 41 | assert.Nil(t, err, "An error occured converting bytes to message") 42 | 43 | assert.Equal(t, 0, int(newMsg.GetMessageType())) // 0x0: Type Confirmable 44 | assert.Equal(t, "abcd1234", bytes.NewBuffer(newMsg.GetToken()).String(), "Token String not the same after reconversion") 45 | assert.Equal(t, int(msgId), int(newMsg.GetMessageId()), "Message ID not the same after reconversion") 46 | assert.NotEqual(t, 0, len(newMsg.GetAllOptions()), "Options should not be 0") 47 | } 48 | 49 | func TestMessageBadOptions(t *testing.T) { 50 | // testMsg := NewBasicConfirmableMessage() 51 | 52 | // Unknown Critical Option 53 | // unk := OptionCode(99) 54 | // testMsg.AddOption(unk, 0) 55 | // testMsg.AddOption(OPTION_CONTENT_FORMAT, MEDIATYPE_APPLICATION_LINK_FORMAT) 56 | // _, err := MessageToBytes(testMsg) 57 | // assert.NotNil(t, err, "Should throw ERR_UNKNOWN_CRITICAL_OPTION") 58 | } 59 | 60 | func TestMessageObject(t *testing.T) { 61 | msg := &CoapMessage{} 62 | 63 | assert.Equal(t, 0, len(msg.Options)) 64 | msg.AddOptions(NewPathOptions("/example")) 65 | msg.AddOption(OptionAccept, MediaTypeApplicationXML) 66 | msg.AddOption(OptionContentFormat, MediaTypeApplicationJSON) 67 | assert.Equal(t, 3, len(msg.Options)) 68 | 69 | opt := msg.GetOption(OptionAccept) 70 | assert.NotNil(t, opt) 71 | 72 | msg.RemoveOptions(OptionURIPath) 73 | assert.Equal(t, 2, len(msg.Options)) 74 | } 75 | 76 | func TestOptionConversion(t *testing.T) { 77 | preMsg := NewBasicConfirmableMessage() 78 | 79 | preMsg.AddOption(OptionIfMatch, "") 80 | preMsg.AddOptions(NewPathOptions("/test")) 81 | preMsg.AddOption(OptionEtag, "1234567890") 82 | preMsg.AddOption(OptionIfNoneMatch, nil) 83 | preMsg.AddOption(OptionObserve, 0) 84 | preMsg.AddOption(OptionURIPort, 1234) 85 | preMsg.AddOption(OptionLocationPath, "/aaa") 86 | preMsg.AddOption(OptionContentFormat, 1) 87 | preMsg.AddOption(OptionMaxAge, 1) 88 | preMsg.AddOption(OptionProxyURI, "http://www.google.com") 89 | preMsg.AddOption(OptionProxyScheme, "http://proxy.scheme") 90 | 91 | converted, _ := MessageToBytes(preMsg) 92 | 93 | postMsg, _ := BytesToMessage(converted) 94 | 95 | PrintMessage(postMsg) 96 | } 97 | 98 | func TestNewMessageHelpers(t *testing.T) { 99 | var messageID uint16 = 12345 100 | 101 | testData := []struct { 102 | msg Message 103 | code CoapCode 104 | }{ 105 | {EmptyMessage(messageID, MessageAcknowledgment), CoapCodeEmpty}, 106 | {CreatedMessage(messageID, MessageAcknowledgment), CoapCodeCreated}, 107 | {DeletedMessage(messageID, MessageAcknowledgment), CoapCodeDeleted}, 108 | {ValidMessage(messageID, MessageAcknowledgment), CoapCodeValid}, 109 | {ChangedMessage(messageID, MessageAcknowledgment), CoapCodeChanged}, 110 | {ContentMessage(messageID, MessageAcknowledgment), CoapCodeContent}, 111 | {BadRequestMessage(messageID, MessageAcknowledgment), CoapCodeBadRequest}, 112 | {UnauthorizedMessage(messageID, MessageAcknowledgment), CoapCodeUnauthorized}, 113 | {BadOptionMessage(messageID, MessageAcknowledgment), CoapCodeBadOption}, 114 | {ForbiddenMessage(messageID, MessageAcknowledgment), CoapCodeForbidden}, 115 | {NotFoundMessage(messageID, MessageAcknowledgment, nil), CoapCodeNotFound}, 116 | {MethodNotAllowedMessage(messageID, MessageAcknowledgment), CoapCodeMethodNotAllowed}, 117 | {NotAcceptableMessage(messageID, MessageAcknowledgment), CoapCodeNotAcceptable}, 118 | {ConflictMessage(messageID, MessageAcknowledgment), CoapCodeConflict}, 119 | {PreconditionFailedMessage(messageID, MessageAcknowledgment), CoapCodePreconditionFailed}, 120 | {RequestEntityTooLargeMessage(messageID, MessageAcknowledgment), CoapCodeRequestEntityTooLarge}, 121 | {UnsupportedContentFormatMessage(messageID, MessageAcknowledgment), CoapCodeUnsupportedContentFormat}, 122 | {InternalServerErrorMessage(messageID, MessageAcknowledgment), CoapCodeInternalServerError}, 123 | {NotImplementedMessage(messageID, MessageAcknowledgment), CoapCodeNotImplemented}, 124 | {BadGatewayMessage(messageID, MessageAcknowledgment), CoapCodeBadGateway}, 125 | {ServiceUnavailableMessage(messageID, MessageAcknowledgment), CoapCodeServiceUnavailable}, 126 | {GatewayTimeoutMessage(messageID, MessageAcknowledgment), CoapCodeGatewayTimeout}, 127 | {ProxyingNotSupportedMessage(messageID, MessageAcknowledgment), CoapCodeProxyingNotSupported}, 128 | } 129 | 130 | for _, td := range testData { 131 | assert.NotNil(t, td.msg) 132 | assert.Equal(t, td.code, td.msg.GetCode()) 133 | } 134 | } 135 | 136 | func NewBasicConfirmableMessage() *CoapMessage { 137 | msg := NewMessageOfType(MessageConfirmable, GenerateMessageID(), nil).(*CoapMessage) 138 | msg.Code = Get 139 | msg.Token = []byte("abcd1234") 140 | msg.SetStringPayload("xxxxx") 141 | 142 | return msg 143 | } 144 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | ) 7 | 8 | // Represents an Option for a CoAP Message 9 | type CoapOption struct { 10 | Code OptionCode 11 | Value interface{} 12 | } 13 | 14 | func (o *CoapOption) GetValue() interface{} { 15 | return o.Value 16 | } 17 | 18 | func (o *CoapOption) GetCode() OptionCode { 19 | return o.Code 20 | } 21 | 22 | func (o *CoapOption) Name() string { 23 | return "Name of option" 24 | } 25 | 26 | // Determines if an option is elective 27 | func (o *CoapOption) IsElective() bool { 28 | if (int(o.Code) % 2) != 0 { 29 | return false 30 | } 31 | return true 32 | } 33 | 34 | // Determines if an option is critical 35 | func (o *CoapOption) IsCritical() bool { 36 | if (int(o.Code) % 2) != 0 { 37 | return true 38 | } 39 | return false 40 | } 41 | 42 | // Returns the string value of an option 43 | func (o *CoapOption) StringValue() string { 44 | return o.Value.(string) 45 | } 46 | 47 | func (o *CoapOption) IntValue() int { 48 | return o.Value.(int) 49 | } 50 | 51 | // Instantiates a New Option 52 | func NewOption(optionNumber OptionCode, optionValue interface{}) *CoapOption { 53 | return &CoapOption{ 54 | Code: optionNumber, 55 | Value: optionValue, 56 | } 57 | } 58 | 59 | // Creates an array of options decomposed from a given path 60 | func NewPathOptions(path string) []Option { 61 | opts := []Option{} 62 | ps := strings.Split(path, "/") 63 | 64 | for _, p := range ps { 65 | if p != "" { 66 | opt := NewOption(OptionURIPath, p) 67 | opts = append(opts, opt) 68 | } 69 | } 70 | return opts 71 | } 72 | 73 | // Checks if an option is repeatable 74 | func IsRepeatableOption(opt Option) bool { 75 | 76 | switch opt.GetCode() { 77 | 78 | case OptionIfMatch, OptionEtag, OptionURIPort, OptionLocationPath, OptionURIPath, OptionURIQuery, OptionLocationQuery, 79 | OptionBlock2, OptionBlock1: 80 | return true 81 | 82 | default: 83 | return false 84 | } 85 | } 86 | 87 | // Checks if an option/option code is recognizable/valid 88 | func IsValidOption(opt Option) bool { 89 | switch opt.GetCode() { 90 | 91 | case OptionIfNoneMatch, OptionURIHost, 92 | OptionEtag, OptionIfMatch, OptionObserve, OptionURIPort, OptionLocationPath, 93 | OptionURIPath, OptionContentFormat, OptionMaxAge, OptionURIQuery, OptionAccept, 94 | OptionLocationQuery, OptionBlock2, OptionBlock1, OptionProxyURI, OptionProxyScheme, OptionSize1: 95 | return true 96 | 97 | default: 98 | return false 99 | } 100 | } 101 | 102 | // Determines if an option is elective 103 | func IsElectiveOption(opt Option) bool { 104 | i := int(opt.GetCode()) 105 | 106 | if (i & 1) == 1 { 107 | return false 108 | } 109 | return true 110 | } 111 | 112 | // Determines if an option is critical 113 | func IsCriticalOption(opt Option) bool { 114 | return !IsElectiveOption(opt) 115 | } 116 | 117 | func NewBlock1Option(bs BlockSizeType, more bool, seq uint32) *Block1Option { 118 | opt := &Block1Option{} 119 | opt.Code = OptionBlock1 120 | 121 | val := seq 122 | 123 | val = val << 4 124 | if more { 125 | val |= (1 << 3) 126 | } 127 | 128 | val |= (uint32(bs) << 0) 129 | 130 | opt.Value = val 131 | 132 | /* 133 | BLockSize 134 | val := o.Value.(uint) 135 | exp := val & 0x07 136 | 137 | return math.Exp2(float64(exp + 4)) 138 | 139 | More 140 | val := o.Value.(uint) 141 | 142 | return ((val >> 3) & 0x01) == 1 143 | 144 | */ 145 | 146 | return opt 147 | } 148 | 149 | func Block1OptionFromOption(opt Option) *Block1Option { 150 | blockOpt := &Block1Option{} 151 | 152 | blockOpt.Value = opt.GetValue() 153 | blockOpt.Code = opt.GetCode() 154 | 155 | return blockOpt 156 | } 157 | 158 | type Block1Option struct { 159 | CoapOption 160 | } 161 | 162 | func (o *Block1Option) Sequence() uint32 { 163 | val := o.GetValue().(uint32) 164 | 165 | return val >> 4 166 | } 167 | 168 | func (o *Block1Option) Exponent() uint32 { 169 | val := o.GetValue().(uint32) 170 | 171 | return val & 0x07 172 | } 173 | 174 | func (o *Block1Option) BlockSizeLength() uint32 { 175 | sz := uint32(o.Size()) + 4 176 | 177 | return sz * sz 178 | } 179 | 180 | func (o *Block1Option) Size() BlockSizeType { 181 | val := o.GetValue().(uint32) 182 | exp := val & 0x07 183 | 184 | return BlockSizeType(byte(math.Exp2(float64(exp + 4)))) 185 | } 186 | 187 | func (o *Block1Option) HasMore() bool { 188 | val := o.Value.(uint32) 189 | 190 | return ((val >> 3) & 0x01) == 1 191 | } 192 | -------------------------------------------------------------------------------- /plaintext.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import "bytes" 4 | 5 | // Instantiates a new message payload of type string 6 | func NewPlainTextPayload(s string) MessagePayload { 7 | return &PlainTextPayload{ 8 | content: s, 9 | } 10 | } 11 | 12 | // Represents a message payload containing string value 13 | type PlainTextPayload struct { 14 | content string 15 | } 16 | 17 | func (p *PlainTextPayload) GetBytes() []byte { 18 | return bytes.NewBufferString(p.content).Bytes() 19 | } 20 | 21 | func (p *PlainTextPayload) Length() int { 22 | return len(p.content) 23 | } 24 | 25 | func (p *PlainTextPayload) String() string { 26 | return p.content 27 | } 28 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | func NullProxyFilter(Message, net.Addr) bool { 12 | return true 13 | } 14 | 15 | // The default handler when proxying is disabled 16 | func NullProxyHandler(c CoapServer, msg Message, session Session) { 17 | SendMessage(ProxyingNotSupportedMessage(msg.GetMessageId(), MessageAcknowledgment), session) 18 | } 19 | 20 | func COAPProxyHandler(c CoapServer, msg Message, session Session) { 21 | proxyURI := msg.GetOption(OptionProxyURI).StringValue() 22 | 23 | parsedURL, err := url.Parse(proxyURI) 24 | if err != nil { 25 | log.Println("Error parsing proxy URI") 26 | SendMessage(BadGatewayMessage(msg.GetMessageId(), MessageAcknowledgment), session) 27 | return 28 | } 29 | clientConn, err := Dial(parsedURL.Host) 30 | 31 | msg.RemoveOptions(OptionProxyURI) 32 | req := NewRequestFromMessage(msg).(*CoapRequest) 33 | req.SetRequestURI(parsedURL.RequestURI()) 34 | 35 | response, err := clientConn.Send(req) 36 | if err != nil { 37 | SendMessage(BadGatewayMessage(msg.GetMessageId(), MessageAcknowledgment), session) 38 | clientConn.Close() 39 | return 40 | } 41 | 42 | _, err = SendMessage(response.GetMessage(), session) 43 | if err != nil { 44 | log.Println("Error occured responding to proxy request") 45 | clientConn.Close() 46 | return 47 | } 48 | clientConn.Close() 49 | } 50 | 51 | // Handles requests for proxying from CoAP to HTTP 52 | func HTTPProxyHandler(c CoapServer, msg Message, session Session) { 53 | proxyURI := msg.GetOption(OptionProxyURI).StringValue() 54 | requestMethod := msg.GetCode() 55 | 56 | client := &http.Client{} 57 | req, err := http.NewRequest(MethodString(CoapCode(msg.GetMethod())), proxyURI, nil) 58 | if err != nil { 59 | SendMessage(BadGatewayMessage(msg.GetMessageId(), MessageAcknowledgment), session) 60 | return 61 | } 62 | 63 | etag := msg.GetOption(OptionEtag) 64 | if etag != nil { 65 | req.Header.Add("ETag", etag.StringValue()) 66 | } 67 | 68 | // TODO: Set timeout handler, and on timeout return 5.04 69 | resp, err := client.Do(req) 70 | if err != nil { 71 | SendMessage(BadGatewayMessage(msg.GetMessageId(), MessageAcknowledgment), session) 72 | return 73 | } 74 | 75 | defer resp.Body.Close() 76 | 77 | contents, _ := ioutil.ReadAll(resp.Body) 78 | modifiedMsg := msg.(*CoapMessage) 79 | modifiedMsg.SetPayload(NewBytesPayload(contents)) 80 | respMsg := NewRequestFromMessage(modifiedMsg) 81 | 82 | if requestMethod == Get { 83 | etag := resp.Header.Get("ETag") 84 | if etag != "" { 85 | msg.AddOption(OptionEtag, etag) 86 | } 87 | } 88 | 89 | // TODO: Check payload length against Size1 options 90 | if len(respMsg.GetMessage().GetPayload().String()) > MaxPacketSize { 91 | SendMessage(BadGatewayMessage(msg.GetMessageId(), MessageAcknowledgment), session) 92 | return 93 | } 94 | 95 | _, err = SendMessage(respMsg.GetMessage(), session) 96 | if err != nil { 97 | println(err.Error()) 98 | } 99 | } 100 | 101 | // Handles requests for proxying from HTTP to CoAP 102 | func HTTPCOAPProxyHandler(msg *Message, conn *net.UDPConn, addr net.Addr) { 103 | log.Println("HttpCoapProxyHandler Proxy Handler") 104 | } 105 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // Creates a New Request Instance 9 | func NewRequest(messageType uint8, messageMethod CoapCode) Request { 10 | return NewRequestWithMessageId(messageType, messageMethod, GenerateMessageID()) 11 | } 12 | 13 | func NewRequestWithMessageId(messageType uint8, messageMethod CoapCode, messageID uint16) Request { 14 | msg := NewMessage(messageType, messageMethod, messageID) 15 | return &CoapRequest{ 16 | msg: msg, 17 | } 18 | } 19 | 20 | func NewConfirmableGetRequest() Request { 21 | return &CoapRequest{ 22 | msg: NewMessage(MessageConfirmable, Get, GenerateMessageID()), 23 | } 24 | } 25 | 26 | func NewConfirmablePostRequest() Request { 27 | return &CoapRequest{ 28 | msg: NewMessage(MessageConfirmable, Post, GenerateMessageID()), 29 | } 30 | } 31 | 32 | func NewConfirmablePutRequest() Request { 33 | return &CoapRequest{ 34 | msg: NewMessage(MessageConfirmable, Put, GenerateMessageID()), 35 | } 36 | } 37 | 38 | func NewConfirmableDeleteRequest() Request { 39 | return &CoapRequest{ 40 | msg: NewMessage(MessageConfirmable, Delete, GenerateMessageID()), 41 | } 42 | } 43 | 44 | // Creates a new request messages from a CoAP Message 45 | func NewRequestFromMessage(msg Message) Request { 46 | return &CoapRequest{ 47 | msg: msg, 48 | } 49 | } 50 | 51 | func NewClientRequestFromMessage(msg Message, attrs map[string]string, session Session) Request { 52 | return &CoapRequest{ 53 | msg: msg, 54 | attrs: attrs, 55 | session: session, 56 | } 57 | } 58 | 59 | // Wraps a CoAP Message as a Request 60 | // Provides various methods which proxies the Message object methods 61 | type CoapRequest struct { 62 | msg Message 63 | attrs map[string]string 64 | session Session 65 | server *CoapServer 66 | } 67 | 68 | func (c *CoapRequest) SetProxyURI(uri string) { 69 | c.msg.AddOption(OptionProxyURI, uri) 70 | } 71 | 72 | func (c *CoapRequest) SetMediaType(mt MediaType) { 73 | c.msg.AddOption(OptionContentFormat, mt) 74 | } 75 | 76 | func (c *CoapRequest) GetSession() Session { 77 | return c.session 78 | } 79 | 80 | func (c *CoapRequest) GetAttributes() map[string]string { 81 | return c.attrs 82 | } 83 | 84 | func (c *CoapRequest) GetAttribute(o string) string { 85 | return c.attrs[o] 86 | } 87 | 88 | func (c *CoapRequest) GetAttributeAsInt(o string) int { 89 | attr := c.GetAttribute(o) 90 | i, _ := strconv.Atoi(attr) 91 | 92 | return i 93 | } 94 | 95 | func (c *CoapRequest) GetMessage() Message { 96 | return c.msg 97 | } 98 | 99 | func (c *CoapRequest) SetStringPayload(s string) { 100 | c.msg.(*CoapMessage).SetPayload(NewPlainTextPayload(s)) 101 | } 102 | 103 | func (c *CoapRequest) SetPayload(b []byte) { 104 | c.msg.(*CoapMessage).SetPayload(NewBytesPayload(b)) 105 | } 106 | 107 | func (c *CoapRequest) SetRequestURI(uri string) { 108 | c.msg.AddOptions(NewPathOptions(uri)) 109 | } 110 | 111 | func (c *CoapRequest) SetConfirmable(con bool) { 112 | if con { 113 | c.msg.(*CoapMessage).SetMessageType(MessageConfirmable) 114 | } else { 115 | c.msg.(*CoapMessage).SetMessageType(MessageNonConfirmable) 116 | } 117 | } 118 | 119 | func (c *CoapRequest) SetToken(t string) { 120 | c.msg.(*CoapMessage).SetToken([]byte(t)) 121 | } 122 | 123 | func (c *CoapRequest) GetURIQuery(q string) string { 124 | qs := c.GetMessage().GetOptionsAsString(OptionURIQuery) 125 | 126 | for _, o := range qs { 127 | ps := strings.Split(o, "=") 128 | if len(ps) == 2 { 129 | if ps[0] == q { 130 | return ps[1] 131 | } 132 | } 133 | } 134 | return "" 135 | } 136 | 137 | func (c *CoapRequest) SetURIQuery(k string, v string) { 138 | c.GetMessage().AddOption(OptionURIQuery, k+"="+v) 139 | } 140 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRequest(t *testing.T) { 10 | var req Request 11 | assert.NotNil(t, NewRequest(MessageConfirmable, Get, 12345)) 12 | 13 | msg := NewMessage(MessageConfirmable, Get, 12345) 14 | 15 | req = NewRequestFromMessage(msg) 16 | assert.NotNil(t, req) 17 | 18 | assert.Equal(t, Get, req.GetMessage().GetCode()) 19 | // &net.UDPConn{}, &net.UDPAddr{} 20 | assert.NotNil(t, NewClientRequestFromMessage(msg, make(map[string]string), nil)) 21 | 22 | req = NewRequest(MessageConfirmable, Get, 12345) 23 | assert.Equal(t, uint8(0), req.GetMessage().GetMessageType()) 24 | 25 | req.SetConfirmable(false) 26 | assert.Equal(t, uint8(1), req.GetMessage().GetMessageType()) 27 | } 28 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func NoResponse() Response { 8 | return NilResponse{} 9 | } 10 | 11 | type NilResponse struct { 12 | } 13 | 14 | func (c NilResponse) GetMessage() Message { 15 | return nil 16 | } 17 | 18 | func (c NilResponse) GetError() error { 19 | return nil 20 | } 21 | 22 | func (c NilResponse) GetPayload() []byte { 23 | return nil 24 | } 25 | 26 | func (c NilResponse) GetURIQuery(q string) string { 27 | return "" 28 | } 29 | 30 | // Creates a new Response object with a Message object and any error messages 31 | func NewResponse(msg Message, err error) Response { 32 | resp := &DefaultResponse{ 33 | msg: msg, 34 | err: err, 35 | } 36 | 37 | return resp 38 | } 39 | 40 | // Creates a new response object with a Message object 41 | func NewResponseWithMessage(msg Message) Response { 42 | resp := &DefaultResponse{ 43 | msg: msg, 44 | } 45 | 46 | return resp 47 | } 48 | 49 | type DefaultResponse struct { 50 | msg Message 51 | err error 52 | } 53 | 54 | func (c *DefaultResponse) GetMessage() Message { 55 | return c.msg 56 | } 57 | 58 | func (c *DefaultResponse) GetError() error { 59 | return c.err 60 | } 61 | 62 | func (c *DefaultResponse) GetPayload() []byte { 63 | return c.GetMessage().GetPayload().GetBytes() 64 | } 65 | 66 | func (c *DefaultResponse) GetURIQuery(q string) string { 67 | qs := c.GetMessage().GetOptionsAsString(OptionURIQuery) 68 | 69 | for _, o := range qs { 70 | ps := strings.Split(o, "=") 71 | if len(ps) == 2 { 72 | if ps[0] == q { 73 | return ps[1] 74 | } 75 | } 76 | } 77 | return "" 78 | } 79 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestResponse(t *testing.T) { 10 | msg := NewEmptyMessage(12345) 11 | msg.SetStringPayload("hello canopus") 12 | assert.NotNil(t, NewResponseWithMessage(msg)) 13 | 14 | response := NewResponse(msg, ErrUnknownCriticalOption) 15 | assert.NotNil(t, response) 16 | assert.Equal(t, uint16(12345), response.GetMessage().GetMessageId()) 17 | assert.Equal(t, ErrUnknownCriticalOption, response.GetError()) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | // CreateNewRoute creates a new Route object 9 | func CreateNewRegExRoute(path string, method string, fn RouteHandler) Route { 10 | var re *regexp.Regexp 11 | regexpString := path 12 | 13 | // Dots 14 | re = regexp.MustCompile(`([^\\])\.`) 15 | regexpString = re.ReplaceAllStringFunc(regexpString, func(m string) string { 16 | return fmt.Sprintf(`%s\.`, string(m[0])) 17 | }) 18 | 19 | // Wildcard names 20 | re = regexp.MustCompile(`:[^/#?()\.\\]+\*`) 21 | regexpString = re.ReplaceAllStringFunc(regexpString, func(m string) string { 22 | return fmt.Sprintf("(?P<%s>.+)", m[1:len(m)-1]) 23 | }) 24 | 25 | re = regexp.MustCompile(`:[^/#?()\.\\]+`) 26 | regexpString = re.ReplaceAllStringFunc(regexpString, func(m string) string { 27 | return fmt.Sprintf(`(?P<%s>[^/#?]+)`, m[1:len(m)]) 28 | }) 29 | 30 | s := fmt.Sprintf(`\A%s\z`, regexpString) 31 | 32 | return &RegExRoute{ 33 | AutoAck: false, 34 | Path: path, 35 | Method: method, 36 | Handler: fn, 37 | RegEx: regexp.MustCompile(s), 38 | } 39 | } 40 | 41 | // Route represents a CoAP Route/Resource 42 | type RegExRoute struct { 43 | Path string 44 | Method string 45 | Handler RouteHandler 46 | RegEx *regexp.Regexp 47 | AutoAck bool 48 | MediaTypes []MediaType 49 | } 50 | 51 | func (r *RegExRoute) Matches(path string) (bool, map[string]string) { 52 | re := r.RegEx 53 | matches := re.FindAllStringSubmatch(path, -1) 54 | attrs := make(map[string]string) 55 | if len(matches) > 0 { 56 | subExp := re.SubexpNames() 57 | for idx, exp := range subExp { 58 | attrs[exp] = matches[0][idx] 59 | } 60 | return true, attrs 61 | } 62 | return false, attrs 63 | } 64 | 65 | func (r *RegExRoute) GetMethod() string { 66 | return r.Method 67 | } 68 | 69 | func (r *RegExRoute) GetMediaTypes() []MediaType { 70 | return r.MediaTypes 71 | } 72 | 73 | func (r *RegExRoute) GetConfiguredPath() string { 74 | return r.Path 75 | } 76 | 77 | func (r *RegExRoute) AutoAcknowledge() bool { 78 | return r.AutoAck 79 | } 80 | 81 | func (r *RegExRoute) Handle(req Request) Response { 82 | return r.Handler(req) 83 | } 84 | 85 | // MatchingRoute checks if a given path matches any defined routes/resources 86 | func MatchingRoute(path string, method string, cf interface{}, routes []Route) (Route, map[string]string, error) { 87 | for _, route := range routes { 88 | if method == route.GetMethod() { 89 | match, attrs := route.Matches(path) 90 | 91 | if match { 92 | if len(route.GetMediaTypes()) > 0 { 93 | if cf == nil { 94 | return route, attrs, ErrUnsupportedContentFormat 95 | } 96 | 97 | foundMediaType := false 98 | for _, o := range route.GetMediaTypes() { 99 | if uint32(o) == cf { 100 | foundMediaType = true 101 | break 102 | } 103 | } 104 | 105 | if !foundMediaType { 106 | return route, attrs, ErrUnsupportedContentFormat 107 | } 108 | } 109 | return route, attrs, nil 110 | } 111 | } 112 | } 113 | return nil, nil, ErrNoMatchingRoute 114 | } 115 | -------------------------------------------------------------------------------- /routes_test.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRoutes(t *testing.T) { 10 | var route Route 11 | var matches bool 12 | 13 | route = CreateNewRegExRoute("/", "GET", nil) 14 | matches, _ = route.Matches("/") 15 | assert.True(t, matches) 16 | 17 | route = CreateNewRegExRoute("/test", "GET", nil) 18 | matches, _ = route.Matches("/") 19 | assert.False(t, matches) 20 | matches, _ = route.Matches("/test") 21 | assert.True(t, matches) 22 | 23 | route = CreateNewRegExRoute("/test/:var", "GET", nil) 24 | matches, _ = route.Matches("/test/abc") 25 | assert.True(t, matches) 26 | matches, _ = route.Matches("/test/abc/def") 27 | assert.False(t, matches) 28 | 29 | route = CreateNewRegExRoute("/test/:var/foo", "GET", nil) 30 | matches, _ = route.Matches("/test/abc/foo") 31 | assert.True(t, matches) 32 | matches, _ = route.Matches("/test/abc") 33 | assert.False(t, matches) 34 | matches, _ = route.Matches("/test/abc/def") 35 | assert.False(t, matches) 36 | matches, _ = route.Matches("/test//foo") 37 | assert.False(t, matches) 38 | matches, _ = route.Matches("/test/foo") 39 | assert.False(t, matches) 40 | 41 | route = CreateNewRegExRoute("/test.abc/:var", "GET", nil) 42 | matches, _ = route.Matches("/test.abc/abc") 43 | assert.True(t, matches) 44 | matches, _ = route.Matches("/test.abc/abc/def") 45 | assert.False(t, matches) 46 | } 47 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "log" 7 | "net" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var DTLS_SERVER_SESSIONS = make(map[int32]*DTLSServerSession) 14 | var NEXT_SESSION_ID int32 = 0 15 | var DTLS_CLIENT_CONNECTIONS = make(map[int32]*DTLSConnection) 16 | 17 | type ServerConfiguration struct { 18 | EnableResourceDiscovery bool 19 | } 20 | 21 | func NewServer() CoapServer { 22 | return createServer() 23 | } 24 | 25 | func createServer() CoapServer { 26 | return &DefaultCoapServer{ 27 | events: NewEvents(), 28 | observations: make(map[string][]*Observation), 29 | fnHandleCOAPProxy: NullProxyHandler, 30 | fnHandleHTTPProxy: NullProxyHandler, 31 | fnProxyFilter: NullProxyFilter, 32 | stopChannel: make(chan int), 33 | coapResponseChannelsMap: make(map[uint16]chan *CoapResponseChannel), 34 | messageIds: make(map[uint16]time.Time), 35 | incomingBlockMessages: make(map[string]Message), 36 | outgoingBlockMessages: make(map[string]Message), 37 | sessions: make(map[string]Session), 38 | createdSession: make(chan Session), 39 | } 40 | } 41 | 42 | type DefaultCoapServer struct { 43 | messageIds map[uint16]time.Time 44 | incomingBlockMessages map[string]Message 45 | outgoingBlockMessages map[string]Message 46 | 47 | routes []Route 48 | events Events 49 | observations map[string][]*Observation 50 | 51 | fnHandleHTTPProxy ProxyHandler 52 | fnHandleCOAPProxy ProxyHandler 53 | fnProxyFilter ProxyFilter 54 | 55 | stopChannel chan int 56 | 57 | coapResponseChannelsMap map[uint16]chan *CoapResponseChannel 58 | 59 | sessions map[string]Session 60 | createdSession chan Session 61 | serverConfig *ServerConfiguration 62 | 63 | cookieSecret []byte 64 | 65 | fnPskHandler func(id string) []byte 66 | } 67 | 68 | func (s *DefaultCoapServer) DeleteSession(ssn Session) { 69 | s.closeSession(ssn) 70 | } 71 | 72 | func (s *DefaultCoapServer) HandlePSK(fn func(id string) []byte) { 73 | s.fnPskHandler = fn 74 | } 75 | 76 | func (s *DefaultCoapServer) handleRequest(msg Message, session Session) { 77 | if msg.GetMessageType() != MessageReset { 78 | // Unsupported Method 79 | if msg.GetCode() != Get && msg.GetCode() != Post && msg.GetCode() != Put && msg.GetCode() != Delete { 80 | s.handleReqUnsupportedMethodRequest(msg, session) 81 | return 82 | } 83 | 84 | // Proxy 85 | if IsProxyRequest(msg) { 86 | s.handleReqProxyRequest(msg, session) 87 | } else { 88 | route, attrs, err := MatchingRoute(msg.GetURIPath(), MethodString(msg.GetCode()), msg.GetOptions(OptionContentFormat), s.GetRoutes()) 89 | if err != nil { 90 | s.GetEvents().Error(err) 91 | if err == ErrNoMatchingRoute { 92 | s.handleReqNoMatchingRoute(msg, session) 93 | return 94 | } 95 | 96 | if err == ErrNoMatchingMethod { 97 | s.handleReqNoMatchingMethod(msg, session) 98 | return 99 | } 100 | 101 | if err == ErrUnsupportedContentFormat { 102 | s.handleReqUnsupportedContentFormat(msg, session) 103 | return 104 | } 105 | 106 | log.Println("Error occured parsing inbound message") 107 | return 108 | } 109 | 110 | // Duplicate Message ID Check 111 | if s.isDuplicateMessage(msg) { 112 | PrintMessage(msg) 113 | if msg.GetMessageType() == MessageConfirmable { 114 | log.Println("Duplicate Message ID ", msg.GetMessageId()) 115 | s.handleReqDuplicateMessageID(msg, session) 116 | } 117 | return 118 | } 119 | 120 | s.updateMessageTS(msg) 121 | 122 | // Auto acknowledge 123 | // TODO: Necessary? 124 | if msg.GetMessageType() == MessageConfirmable && route.AutoAcknowledge() { 125 | s.handleRequestAcknowledge(msg, session) 126 | } 127 | req := NewClientRequestFromMessage(msg, attrs, session) 128 | 129 | if msg.GetMessageType() == MessageConfirmable { 130 | // Observation Request 131 | obsOpt := msg.GetOption(OptionObserve) 132 | if obsOpt != nil { 133 | s.handleReqObserve(req, msg, session) 134 | } 135 | } 136 | opt := req.GetMessage().GetOption(OptionBlock1) 137 | if opt != nil { 138 | blockOpt := Block1OptionFromOption(opt) 139 | 140 | // 0000 1 010 141 | /* 142 | [NUM][M][SZX] 143 | 2 ^ (2 + 4) 144 | 2 ^ 6 = 32 145 | Size = 2 ^ (SZX + 4) 146 | 147 | The value 7 for SZX (which would 148 | indicate a block size of 2048) is reserved, i.e. MUST NOT be sent 149 | and MUST lead to a 4.00 Bad Request response code upon reception 150 | in a request. 151 | */ 152 | 153 | if blockOpt.Value != nil { 154 | if blockOpt.Code == OptionBlock1 { 155 | exp := blockOpt.Exponent() 156 | 157 | if exp == 7 { 158 | s.handleReqBadRequest(msg, session) 159 | return 160 | } 161 | 162 | // szx := blockOpt.Size() 163 | hasMore := blockOpt.HasMore() 164 | seqNum := blockOpt.Sequence() 165 | // fmt.Println("Out Values == ", blockOpt.Value, exp, szx, 2, hasMore, seqNum) 166 | 167 | s.GetEvents().BlockMessage(msg, true) 168 | 169 | s.updateBlockMessageFragment(session.GetAddress().String(), msg, seqNum) 170 | 171 | if hasMore { 172 | s.handleReqContinue(msg, session) 173 | 174 | // Auto Respond to client 175 | 176 | } else { 177 | // TODO: Check if message is too large 178 | msg = NewMessage(msg.GetMessageType(), msg.GetCode(), msg.GetMessageId()) 179 | msg.SetPayload(s.flushBlockMessagePayload(session.GetAddress().String())) 180 | req = NewClientRequestFromMessage(msg, attrs, session) 181 | } 182 | } else if blockOpt.Code == OptionBlock2 { 183 | 184 | } else { 185 | // TOOO: Invalid Block option Code 186 | } 187 | } 188 | } 189 | 190 | resp := route.Handle(req) 191 | _, nilresponse := resp.(NilResponse) 192 | if !nilresponse { 193 | respMsg := resp.GetMessage().(*CoapMessage) 194 | respMsg.SetToken(req.GetMessage().GetToken()) 195 | 196 | // TODO: Validate Message before sending (e.g missing messageId) 197 | err := ValidateMessage(respMsg) 198 | if err == nil { 199 | s.GetEvents().Message(respMsg, false) 200 | SendMessage(respMsg, session) 201 | } 202 | } 203 | } 204 | } 205 | } 206 | 207 | func (s *DefaultCoapServer) handleReqObserve(req Request, msg Message, session Session) { 208 | // TODO: if server doesn't allow observing, return error 209 | addr := session.GetAddress() 210 | 211 | // TODO: Check if observation has been registered, if yes, remove it (observation == cancel) 212 | resource := msg.GetURIPath() 213 | 214 | if s.HasObservation(resource, addr) { 215 | // Remove observation of client 216 | s.RemoveObservation(resource, addr) 217 | 218 | // Observe Cancel Request & Fire OnObserveCancel Event 219 | s.GetEvents().ObserveCancelled(resource, msg) 220 | } else { 221 | // Register observation of client 222 | s.AddObservation(msg.GetURIPath(), string(msg.GetToken()), session) 223 | 224 | // Observe Request & Fire OnObserve Event 225 | s.GetEvents().Observe(resource, msg) 226 | } 227 | 228 | req.GetMessage().AddOption(OptionObserve, 1) 229 | } 230 | 231 | func (s *DefaultCoapServer) handleResponse(msg Message, session Session) { 232 | defer s.closeSession(session) 233 | if msg.GetOption(OptionObserve) != nil { 234 | s.handleAcknowledgeObserveRequest(msg) 235 | return 236 | } 237 | 238 | ch := GetResponseChannel(s, msg.GetMessageId()) 239 | if ch != nil { 240 | resp := &CoapResponseChannel{ 241 | Response: NewResponse(msg, nil), 242 | } 243 | ch <- resp 244 | DeleteResponseChannel(s, msg.GetMessageId()) 245 | } 246 | } 247 | 248 | func (s *DefaultCoapServer) GetEvents() Events { 249 | return s.events 250 | } 251 | 252 | func (s *DefaultCoapServer) addDiscoveryRoute() { 253 | var discoveryRoute RouteHandler = func(req Request) Response { 254 | msg := req.GetMessage() 255 | 256 | var buf bytes.Buffer 257 | for _, r := range s.routes { 258 | if r.GetConfiguredPath() != ".well-known/core" { 259 | buf.WriteString("") 262 | 263 | // Media Types 264 | lenMt := len(r.GetMediaTypes()) 265 | if lenMt > 0 { 266 | buf.WriteString(";ct=") 267 | for idx, mt := range r.GetMediaTypes() { 268 | 269 | buf.WriteString(strconv.Itoa(int(mt))) 270 | if idx+1 < lenMt { 271 | buf.WriteString(" ") 272 | } 273 | } 274 | } 275 | 276 | buf.WriteString(",") 277 | } 278 | } 279 | 280 | ack := ContentMessage(msg.GetMessageId(), MessageAcknowledgment) 281 | ack.SetToken(msg.GetToken()) 282 | ack.SetPayload(NewPlainTextPayload(buf.String())) 283 | ack.AddOption(OptionContentFormat, MediaTypeApplicationLinkFormat) 284 | resp := NewResponseWithMessage(ack) 285 | 286 | return resp 287 | } 288 | s.NewRoute("/.well-known/core", Get, discoveryRoute) 289 | } 290 | 291 | func (s *DefaultCoapServer) ListenAndServeDTLS(addr string) { 292 | s.addDiscoveryRoute() 293 | 294 | conn := s.createConn(addr) 295 | 296 | ctx, err := NewServerDtlsContext() 297 | if err != nil { 298 | panic("Unable to create SSL Context:" + err.Error()) 299 | } 300 | 301 | if conn == nil { 302 | log.Fatal("An error occured starting up CoAPS Server") 303 | } else { 304 | secret := make([]byte, 32) 305 | if n, err := rand.Read(secret); n != 32 || err != nil { 306 | panic(err) 307 | } 308 | 309 | s.cookieSecret = secret 310 | log.Println("Started CoAPS Server ", conn.LocalAddr()) 311 | go s.handleIncomingDTLSData(conn, ctx) 312 | go s.events.Started(s) 313 | go s.handleMessageIDPurge() 314 | } 315 | } 316 | 317 | func (s *DefaultCoapServer) ListenAndServe(addr string) { 318 | s.addDiscoveryRoute() 319 | 320 | conn := s.createConn(addr) 321 | 322 | if conn == nil { 323 | log.Fatal("An error occured starting up CoAP Server") 324 | } else { 325 | log.Println("Started CoAP Server ", conn.LocalAddr()) 326 | go s.handleIncomingData(conn) 327 | go s.events.Started(s) 328 | go s.handleMessageIDPurge() 329 | } 330 | } 331 | 332 | func (s *DefaultCoapServer) createConn(addr string) ServerConnection { 333 | localHost := addr 334 | if !strings.Contains(localHost, ":") { 335 | localHost = ":" + localHost 336 | } 337 | localAddr, err := net.ResolveUDPAddr("udp6", localHost) 338 | if err != nil { 339 | panic(err.Error()) 340 | } 341 | 342 | conn, err := net.ListenUDP(UDP, localAddr) 343 | if err != nil { 344 | panic(err.Error()) 345 | } 346 | 347 | return &UDPServerConnection{ 348 | conn: conn, 349 | } 350 | } 351 | 352 | func (s *DefaultCoapServer) handleIncomingDTLSData(conn ServerConnection, ctx *ServerDtlsContext) { 353 | readBuf := make([]byte, MaxPacketSize) 354 | go func() { 355 | for { 356 | select { 357 | case <-s.stopChannel: 358 | return 359 | 360 | default: 361 | // continue 362 | } 363 | 364 | len, addr, err := conn.ReadFrom(readBuf) 365 | if err == nil { 366 | msgBuf := make([]byte, len) 367 | copy(msgBuf, readBuf[:len]) 368 | ssn := s.sessions[addr.String()] 369 | if ssn == nil { 370 | ssn = &DTLSServerSession{ 371 | UDPServerSession: UDPServerSession{ 372 | addr: addr, 373 | conn: conn, 374 | server: s, 375 | buf: []byte{}, 376 | rcvd: make(chan []byte, 1), 377 | }, 378 | } 379 | err := newSslSession(ssn.(*DTLSServerSession), ctx, s.fnPskHandler) 380 | if err != nil { 381 | panic(err.Error()) 382 | } 383 | s.sessions[addr.String()] = ssn 384 | s.createdSession <- ssn 385 | } 386 | 387 | ssn.(*DTLSServerSession).rcvd <- msgBuf 388 | } else { 389 | logMsg("Error occured reading UDP", err) 390 | } 391 | } 392 | }() 393 | 394 | go func() { 395 | for { 396 | ssn := <-s.createdSession 397 | go s.handleSession(ssn) 398 | } 399 | }() 400 | 401 | } 402 | 403 | func (s *DefaultCoapServer) handleIncomingData(conn ServerConnection) { 404 | readBuf := make([]byte, MaxPacketSize) 405 | go func() { 406 | for { 407 | select { 408 | case <-s.stopChannel: 409 | return 410 | 411 | default: 412 | // continue 413 | } 414 | 415 | len, addr, err := conn.ReadFrom(readBuf) 416 | if err == nil { 417 | msgBuf := make([]byte, len) 418 | copy(msgBuf, readBuf[:len]) 419 | ssn := s.sessions[addr.String()] 420 | if ssn == nil { 421 | ssn = &UDPServerSession{ 422 | addr: addr, 423 | conn: conn, 424 | server: s, 425 | rcvd: make(chan []byte), 426 | } 427 | if err != nil { 428 | panic(err.Error()) 429 | } 430 | s.sessions[addr.String()] = ssn 431 | } 432 | go func() { 433 | ssn.(*UDPServerSession).rcvd <- msgBuf 434 | }() 435 | go s.handleSession(ssn) 436 | } else { 437 | logMsg("Error occured reading UDP", err) 438 | } 439 | } 440 | }() 441 | 442 | } 443 | 444 | func (s *DefaultCoapServer) GetSession(addr string) Session { 445 | return s.sessions[addr] 446 | } 447 | 448 | func (s *DefaultCoapServer) Stop() { 449 | close(s.stopChannel) 450 | } 451 | 452 | func (s *DefaultCoapServer) updateBlockMessageFragment(client string, msg Message, seq uint32) { 453 | msgs := s.incomingBlockMessages[client] 454 | 455 | if msgs == nil { 456 | msgs = &CoapBlockMessage{ 457 | Sequence: 0, 458 | MessageBuf: []byte{}, 459 | } 460 | } 461 | 462 | blockMsgs := msgs.(*CoapBlockMessage) 463 | blockMsgs.Sequence = seq 464 | blockMsgs.MessageBuf = append(blockMsgs.MessageBuf, msg.GetPayload().GetBytes()...) 465 | 466 | s.incomingBlockMessages[client] = msgs 467 | } 468 | 469 | func (s *DefaultCoapServer) flushBlockMessagePayload(origin string) MessagePayload { 470 | msgs := s.incomingBlockMessages[origin] 471 | 472 | blockMsg := msgs.(*CoapBlockMessage) 473 | payload := blockMsg.MessageBuf 474 | 475 | return NewBytesPayload(payload) 476 | } 477 | 478 | func (s *DefaultCoapServer) handleMessageIDPurge() { 479 | // Routine for clearing up message IDs which has expired 480 | ticker := time.NewTicker(MessageIDPurgeDuration * time.Second) 481 | go func() { 482 | for { 483 | select { 484 | case <-ticker.C: 485 | for k, v := range s.messageIds { 486 | elapsed := time.Since(v) 487 | if elapsed > MessageIDPurgeDuration { 488 | delete(s.messageIds, k) 489 | } 490 | } 491 | } 492 | } 493 | }() 494 | } 495 | 496 | func (s *DefaultCoapServer) SetProxyFilter(fn ProxyFilter) { 497 | s.fnProxyFilter = fn 498 | } 499 | 500 | func (s *DefaultCoapServer) GetCookieSecret() []byte { 501 | return s.cookieSecret 502 | } 503 | 504 | func (s *DefaultCoapServer) handleSession(session Session) { 505 | msgBuf := make([]byte, 1500) 506 | n, _ := session.Read(msgBuf) 507 | 508 | msg, err := BytesToMessage(msgBuf[:n]) 509 | if err != nil { 510 | logMsg(err.Error()) 511 | s.handleReqBadRequest(msg, session) 512 | } 513 | 514 | if msg.GetMessageType() == MessageAcknowledgment { 515 | s.handleResponse(msg, session) 516 | } else { 517 | s.handleRequest(msg, session) 518 | } 519 | } 520 | 521 | func (s *DefaultCoapServer) closeSession(ssn Session) { 522 | delete(s.sessions, ssn.GetAddress().String()) 523 | } 524 | 525 | func (s *DefaultCoapServer) Get(path string, fn RouteHandler) Route { 526 | return s.add(MethodGet, path, fn) 527 | } 528 | 529 | func (s *DefaultCoapServer) Delete(path string, fn RouteHandler) Route { 530 | return s.add(MethodDelete, path, fn) 531 | } 532 | 533 | func (s *DefaultCoapServer) Put(path string, fn RouteHandler) Route { 534 | return s.add(MethodPut, path, fn) 535 | } 536 | 537 | func (s *DefaultCoapServer) Post(path string, fn RouteHandler) Route { 538 | return s.add(MethodPost, path, fn) 539 | } 540 | 541 | func (s *DefaultCoapServer) Options(path string, fn RouteHandler) Route { 542 | return s.add(MethodOptions, path, fn) 543 | } 544 | 545 | func (s *DefaultCoapServer) Patch(path string, fn RouteHandler) Route { 546 | return s.add(MethodPatch, path, fn) 547 | } 548 | 549 | func (s *DefaultCoapServer) add(method string, path string, fn RouteHandler) Route { 550 | route := CreateNewRegExRoute(path, method, fn) 551 | s.routes = append(s.routes, route) 552 | 553 | return route 554 | } 555 | 556 | func (s *DefaultCoapServer) NewRoute(path string, method CoapCode, fn RouteHandler) Route { 557 | route := CreateNewRegExRoute(path, MethodString(method), fn) 558 | s.routes = append(s.routes, route) 559 | 560 | return route 561 | } 562 | 563 | func (s *DefaultCoapServer) storeNewOutgoingBlockMessage(client string, payload []byte) { 564 | bm := NewBlockMessage().(*CoapBlockMessage) 565 | bm.MessageBuf = payload 566 | s.outgoingBlockMessages[client] = bm 567 | } 568 | 569 | func (s *DefaultCoapServer) NotifyChange(resource, value string, confirm bool) { 570 | t := s.observations[resource] 571 | 572 | if t != nil { 573 | var req Request 574 | 575 | if confirm { 576 | req = NewRequest(MessageConfirmable, CoapCodeContent) 577 | } else { 578 | req = NewRequest(MessageAcknowledgment, CoapCodeContent) 579 | } 580 | 581 | for _, r := range t { 582 | req.SetToken(r.Token) 583 | req.SetStringPayload(value) 584 | req.SetRequestURI(r.Resource) 585 | r.NotifyCount++ 586 | req.GetMessage().AddOption(OptionObserve, r.NotifyCount) 587 | 588 | go SendMessage(req.GetMessage(), r.Session) 589 | } 590 | } 591 | } 592 | 593 | func (s *DefaultCoapServer) AddObservation(resource, token string, session Session) { 594 | s.observations[resource] = append(s.observations[resource], NewObservation(session, token, resource)) 595 | } 596 | 597 | func (s *DefaultCoapServer) HasObservation(resource string, addr net.Addr) bool { 598 | obs := s.observations[resource] 599 | if obs == nil { 600 | return false 601 | } 602 | 603 | for _, o := range obs { 604 | if o.Session.GetAddress().String() == addr.String() { 605 | return true 606 | } 607 | } 608 | return false 609 | } 610 | 611 | func (s *DefaultCoapServer) RemoveObservation(resource string, addr net.Addr) { 612 | obs := s.observations[resource] 613 | if obs == nil { 614 | return 615 | } 616 | 617 | for idx, o := range obs { 618 | if o.Session.GetAddress().String() == addr.String() { 619 | s.observations[resource] = append(obs[:idx], obs[idx+1:]...) 620 | return 621 | } 622 | } 623 | } 624 | 625 | func (s *DefaultCoapServer) OnNotify(fn FnEventNotify) { 626 | s.events.OnNotify(fn) 627 | } 628 | 629 | func (s *DefaultCoapServer) OnStart(fn FnEventStart) { 630 | s.events.OnStart(fn) 631 | } 632 | 633 | func (s *DefaultCoapServer) OnClose(fn FnEventClose) { 634 | s.events.OnClose(fn) 635 | } 636 | 637 | func (s *DefaultCoapServer) OnDiscover(fn FnEventDiscover) { 638 | s.events.OnDiscover(fn) 639 | } 640 | 641 | func (s *DefaultCoapServer) OnError(fn FnEventError) { 642 | s.events.OnError(fn) 643 | } 644 | 645 | func (s *DefaultCoapServer) OnObserve(fn FnEventObserve) { 646 | s.events.OnObserve(fn) 647 | } 648 | 649 | func (s *DefaultCoapServer) OnObserveCancel(fn FnEventObserveCancel) { 650 | s.events.OnObserveCancel(fn) 651 | } 652 | 653 | func (s *DefaultCoapServer) OnMessage(fn FnEventMessage) { 654 | s.events.OnMessage(fn) 655 | } 656 | 657 | func (s *DefaultCoapServer) OnBlockMessage(fn FnEventBlockMessage) { 658 | s.events.OnBlockMessage(fn) 659 | } 660 | 661 | func (s *DefaultCoapServer) ProxyOverHttp(enabled bool) { 662 | if enabled { 663 | s.fnHandleHTTPProxy = HTTPProxyHandler 664 | } else { 665 | s.fnHandleHTTPProxy = NullProxyHandler 666 | } 667 | } 668 | 669 | func (s *DefaultCoapServer) ProxyOverCoap(enabled bool) { 670 | if enabled { 671 | s.fnHandleCOAPProxy = COAPProxyHandler 672 | } else { 673 | s.fnHandleCOAPProxy = NullProxyHandler 674 | } 675 | } 676 | 677 | func (s *DefaultCoapServer) AllowProxyForwarding(msg Message, addr net.Addr) bool { 678 | return s.fnProxyFilter(msg, addr) 679 | } 680 | 681 | func (s *DefaultCoapServer) ForwardCoap(msg Message, session Session) { 682 | s.fnHandleCOAPProxy(s, msg, session) 683 | } 684 | 685 | func (s *DefaultCoapServer) ForwardHTTP(msg Message, session Session) { 686 | s.fnHandleHTTPProxy(s, msg, session) 687 | } 688 | 689 | func (s *DefaultCoapServer) GetRoutes() []Route { 690 | return s.routes 691 | } 692 | 693 | func (s *DefaultCoapServer) isDuplicateMessage(msg Message) bool { 694 | _, ok := s.messageIds[msg.GetMessageId()] 695 | 696 | return ok 697 | } 698 | 699 | func (s *DefaultCoapServer) updateMessageTS(msg Message) { 700 | s.messageIds[msg.GetMessageId()] = time.Now() 701 | } 702 | 703 | func (s *DefaultCoapServer) handleReqUnknownCriticalOption(msg Message, session Session) { 704 | if msg.GetMessageType() == MessageConfirmable { 705 | SendMessage(BadOptionMessage(msg.GetMessageId(), MessageAcknowledgment), session) 706 | } 707 | return 708 | } 709 | 710 | func (s *DefaultCoapServer) handleReqBadRequest(msg Message, session Session) { 711 | if msg.GetMessageType() == MessageConfirmable { 712 | SendMessage(BadRequestMessage(msg.GetMessageId(), msg.GetMessageType()), session) 713 | } 714 | return 715 | } 716 | 717 | func (s *DefaultCoapServer) handleReqContinue(msg Message, session Session) { 718 | if msg.GetMessageType() == MessageConfirmable { 719 | SendMessage(ContinueMessage(msg.GetMessageId(), msg.GetMessageType()), session) 720 | } 721 | return 722 | } 723 | 724 | func (s *DefaultCoapServer) handleReqUnsupportedMethodRequest(msg Message, session Session) { 725 | ret := NotImplementedMessage(msg.GetMessageId(), MessageAcknowledgment) 726 | ret.CloneOptions(msg, OptionURIPath, OptionContentFormat) 727 | 728 | // c.GetEvents().Message(ret, false) 729 | SendMessage(ret, session) 730 | } 731 | 732 | func (s *DefaultCoapServer) handleReqProxyRequest(msg Message, session Session) { 733 | if !s.AllowProxyForwarding(msg, session.GetAddress()) { 734 | SendMessage(ForbiddenMessage(msg.GetMessageId(), MessageAcknowledgment), session) 735 | } 736 | 737 | proxyURI := msg.GetOption(OptionProxyURI).StringValue() 738 | if IsCoapURI(proxyURI) { 739 | s.ForwardCoap(msg, session) 740 | } else if IsHTTPURI(proxyURI) { 741 | s.ForwardHTTP(msg, session) 742 | } else { 743 | // 744 | } 745 | } 746 | 747 | func (s *DefaultCoapServer) handleReqNoMatchingRoute(msg Message, session Session) { 748 | ret := NotFoundMessage(msg.GetMessageId(), MessageAcknowledgment, msg.GetToken()) 749 | ret.CloneOptions(msg, OptionURIPath, OptionContentFormat) 750 | 751 | SendMessage(ret, session) 752 | } 753 | 754 | func (s *DefaultCoapServer) handleReqNoMatchingMethod(msg Message, session Session) { 755 | ret := MethodNotAllowedMessage(msg.GetMessageId(), MessageAcknowledgment) 756 | ret.CloneOptions(msg, OptionURIPath, OptionContentFormat) 757 | 758 | SendMessage(ret, session) 759 | } 760 | 761 | func (s *DefaultCoapServer) handleReqUnsupportedContentFormat(msg Message, session Session) { 762 | ret := UnsupportedContentFormatMessage(msg.GetMessageId(), MessageAcknowledgment) 763 | ret.CloneOptions(msg, OptionURIPath, OptionContentFormat) 764 | 765 | // s.GetEvents().Message(ret, false) 766 | SendMessage(ret, session) 767 | } 768 | 769 | func (s *DefaultCoapServer) handleReqDuplicateMessageID(msg Message, session Session) { 770 | ret := EmptyMessage(msg.GetMessageId(), MessageReset) 771 | ret.CloneOptions(msg, OptionURIPath, OptionContentFormat) 772 | 773 | SendMessage(ret, session) 774 | } 775 | 776 | func (s *DefaultCoapServer) handleRequestAcknowledge(msg Message, session Session) { 777 | ack := NewMessageOfType(MessageAcknowledgment, msg.GetMessageId(), nil) 778 | 779 | SendMessage(ack, session) 780 | } 781 | 782 | func (s *DefaultCoapServer) handleAcknowledgeObserveRequest(msg Message) { 783 | s.GetEvents().Notify(msg.GetURIPath(), msg.GetPayload(), msg) 784 | } 785 | 786 | func (s *DefaultCoapServer) handleAcknowledgeObserveRequestGetSession(addr string) Session { 787 | return s.sessions[addr] 788 | } 789 | 790 | func NewResponseChannel() (ch chan *CoapResponseChannel) { 791 | ch = make(chan *CoapResponseChannel) 792 | 793 | return 794 | } 795 | 796 | func AddResponseChannel(c CoapServer, msgId uint16, ch chan *CoapResponseChannel) { 797 | s := c.(*DefaultCoapServer) 798 | s.coapResponseChannelsMap[msgId] = ch 799 | } 800 | 801 | func DeleteResponseChannel(c CoapServer, msgId uint16) { 802 | s := c.(*DefaultCoapServer) 803 | delete(s.coapResponseChannelsMap, msgId) 804 | } 805 | 806 | func GetResponseChannel(c CoapServer, msgId uint16) (ch chan *CoapResponseChannel) { 807 | s := c.(*DefaultCoapServer) 808 | ch = s.coapResponseChannelsMap[msgId] 809 | 810 | return 811 | } 812 | 813 | func NewObservation(session Session, token string, resource string) *Observation { 814 | return &Observation{ 815 | Session: session, 816 | Token: token, 817 | Resource: resource, 818 | NotifyCount: 0, 819 | } 820 | } 821 | 822 | type Observation struct { 823 | Session Session 824 | Token string 825 | Resource string 826 | NotifyCount int 827 | } 828 | 829 | func _doSendMessage(msg Message, session Session, ch chan *CoapResponseChannel) { 830 | resp := &CoapResponseChannel{} 831 | 832 | b, err := MessageToBytes(msg) 833 | if err != nil { 834 | resp.Error = err 835 | ch <- resp 836 | } 837 | 838 | _, err = session.Write(b) 839 | if err != nil { 840 | resp.Error = err 841 | ch <- resp 842 | } 843 | 844 | if msg.GetMessageType() == MessageNonConfirmable { 845 | resp.Response = NewResponse(NewEmptyMessage(msg.GetMessageId()), nil) 846 | ch <- resp 847 | } 848 | AddResponseChannel(session.GetServer(), msg.GetMessageId(), ch) 849 | } 850 | 851 | func SendMessage(msg Message, session Session) (Response, error) { 852 | if session.GetConnection() == nil { 853 | return nil, ErrNilConn 854 | } 855 | 856 | if msg == nil { 857 | return nil, ErrNilMessage 858 | } 859 | 860 | if session.GetAddress() == nil { 861 | return nil, ErrNilAddr 862 | } 863 | 864 | ch := NewResponseChannel() 865 | go _doSendMessage(msg, session, ch) 866 | 867 | session.GetServer().DeleteSession(session) 868 | respCh := <-ch 869 | return respCh.Response, respCh.Error 870 | } 871 | 872 | type CoapResponseChannel struct { 873 | Response Response 874 | Error error 875 | } 876 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import "testing" 4 | 5 | // TODO Redo this entire test suite 6 | func TestServerInstantiate(t *testing.T) { 7 | //var s CoapServer 8 | //s = NewServer() 9 | 10 | //assert.NotNil(t, s) 11 | //assert.Equal(t, 1000, s.GetLocalAddress().Port) 12 | //assert.Equal(t, "udp", s.GetLocalAddress().Network()) 13 | // 14 | //s = NewLocalServer("TestServer") 15 | //assert.NotNil(t, s) 16 | //assert.Equal(t, 5683, s.GetLocalAddress().Port) 17 | //assert.Equal(t, "udp", s.GetLocalAddress().Network()) 18 | } 19 | 20 | //func TestDiscoveryService(t *testing.T) { 21 | // server := NewCoapServer(":5684") 22 | // assert.NotNil(t, server) 23 | // assert.Equal(t, 5684, server.localAddr.Port) 24 | // assert.Equal(t, "udp", server.localAddr.Network()) 25 | // 26 | // go server.Start() 27 | // client := NewCoapClient() 28 | // client.OnStart(func(server *CoapServer) { 29 | // tok := "abc123" 30 | // client.Dial("localhost:5684") 31 | // 32 | // req := NewRequest(TYPE_CONFIRMABLE, GET, GenerateMessageId()) 33 | // req.SetToken(tok) 34 | // req.SetRequestURI(".well-known/core") 35 | // resp, err := client.Send(req) 36 | // assert.Nil(t, err) 37 | // 38 | // assert.Equal(t, tok, resp.GetMessage().GetTokenString()) 39 | // client.Stop() 40 | // }) 41 | // client.Start() 42 | //} 43 | 44 | //func TestClientServerRequestResponse(t *testing.T) { 45 | // server := NewLocalServer() 46 | // 47 | // server.Get("/ep", func (req CoapRequest) CoapResponse { 48 | // msg := ContentMessage(req.GetMessage().MessageId, TYPE_ACKNOWLEDGEMENT) 49 | // msg.SetStringPayload("ACK GET") 50 | // res := NewResponse(msg, nil) 51 | // 52 | // return res 53 | // }) 54 | // 55 | // server.Post("/ep", func (req CoapRequest) CoapResponse { 56 | // msg := ContentMessage(req.GetMessage().MessageId, TYPE_ACKNOWLEDGEMENT) 57 | // msg.SetStringPayload("ACK POST") 58 | // res := NewResponse(msg, nil) 59 | // 60 | // return res 61 | // }) 62 | // 63 | // server.Put("/ep", func (req CoapRequest) CoapResponse { 64 | // msg := ContentMessage(req.GetMessage().MessageId, TYPE_ACKNOWLEDGEMENT) 65 | // msg.SetStringPayload("ACK PUT") 66 | // res := NewResponse(msg, nil) 67 | // 68 | // return res 69 | // }) 70 | // 71 | // server.Delete("/ep", func (req CoapRequest) CoapResponse { 72 | // msg := ContentMessage(req.GetMessage().MessageId, TYPE_ACKNOWLEDGEMENT) 73 | // msg.SetStringPayload("ACK DELETE") 74 | // res := NewResponse(msg, nil) 75 | // 76 | // return res 77 | // }) 78 | // 79 | // go server.Start() 80 | // 81 | // client := NewCoapClient() 82 | // 83 | // client.OnStart(func(server *CoapServer) { 84 | // client.Dial("localhost:5683") 85 | // token := "tok1234" 86 | // 87 | // var req CoapRequest 88 | // var resp CoapResponse 89 | // var err error 90 | // 91 | // // 404 Test 92 | // req = NewConfirmableGetRequest() 93 | // req.SetToken(token) 94 | // req.SetRequestURI("ep-404") 95 | // resp, err = client.Send(req) 96 | // assert.Equal(t, COAPCODE_404_NOT_FOUND, resp.GetMessage().Code) 97 | // 98 | // // GET 99 | // req = NewConfirmableGetRequest() 100 | // req.SetToken(token) 101 | // req.SetRequestURI("ep") 102 | // resp, err = client.Send(req) 103 | // 104 | // assert.Nil(t, err) 105 | // assert.Equal(t, "ACK GET", resp.GetMessage().Payload.String()) 106 | // assert.Equal(t, token, resp.GetMessage().GetTokenString()) 107 | // 108 | // // POST 109 | // req = NewConfirmablePostRequest() 110 | // req.SetToken(token) 111 | // req.SetRequestURI("ep") 112 | // resp, err = client.Send(req) 113 | // 114 | // assert.Nil(t, err) 115 | // assert.Equal(t, "ACK POST", resp.GetMessage().Payload.String()) 116 | // assert.Equal(t, token, resp.GetMessage().GetTokenString()) 117 | // 118 | // // PUT 119 | // req = NewConfirmablePutRequest() 120 | // req.SetToken(token) 121 | // req.SetRequestURI("ep") 122 | // resp, err = client.Send(req) 123 | // 124 | // assert.Nil(t, err) 125 | // assert.Equal(t, "ACK PUT", resp.GetMessage().Payload.String()) 126 | // assert.Equal(t, token, resp.GetMessage().GetTokenString()) 127 | // 128 | // // DELETE 129 | // req = NewConfirmableDeleteRequest() 130 | // req.SetToken(token) 131 | // req.SetRequestURI("ep") 132 | // resp, err = client.Send(req) 133 | // 134 | // assert.Nil(t, err) 135 | // assert.Equal(t, "ACK DELETE", resp.GetMessage().Payload.String()) 136 | // assert.Equal(t, token, resp.GetMessage().GetTokenString()) 137 | // 138 | // // Test default token set 139 | // req = NewConfirmableGetRequest() 140 | // req.SetRequestURI("ep") 141 | // resp, err = client.Send(req) 142 | // 143 | // assert.Nil(t, err) 144 | // assert.Equal(t, "ACK GET", resp.GetMessage().Payload.String()) 145 | // assert.NotEmpty(t, resp.GetMessage().GetTokenString()) 146 | // 147 | // client.Stop() 148 | // }) 149 | // client.Start() 150 | //} 151 | -------------------------------------------------------------------------------- /serverconn.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | type UDPServerConnection struct { 9 | conn net.PacketConn 10 | } 11 | 12 | func (uc *UDPServerConnection) ReadFrom(b []byte) (n int, addr net.Addr, err error) { 13 | return uc.conn.ReadFrom(b) 14 | } 15 | 16 | func (uc *UDPServerConnection) WriteTo(b []byte, addr net.Addr) (n int, err error) { 17 | return uc.conn.WriteTo(b, addr) 18 | } 19 | 20 | func (uc *UDPServerConnection) Close() error { 21 | return uc.conn.Close() 22 | } 23 | 24 | func (uc *UDPServerConnection) LocalAddr() net.Addr { 25 | return uc.conn.LocalAddr() 26 | } 27 | 28 | func (uc *UDPServerConnection) SetDeadline(t time.Time) error { 29 | return uc.conn.SetDeadline(t) 30 | } 31 | 32 | func (uc *UDPServerConnection) SetReadDeadline(t time.Time) error { 33 | return uc.conn.SetReadDeadline(t) 34 | } 35 | 36 | func (uc *UDPServerConnection) SetWriteDeadline(t time.Time) error { 37 | return uc.conn.SetWriteDeadline(t) 38 | } 39 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import "net" 4 | 5 | type UDPServerSession struct { 6 | addr net.Addr 7 | conn ServerConnection 8 | server CoapServer 9 | rcvd chan []byte 10 | buf []byte 11 | } 12 | 13 | func (s *UDPServerSession) GetConnection() ServerConnection { 14 | return s.conn 15 | } 16 | 17 | func (s *UDPServerSession) GetAddress() net.Addr { 18 | return s.addr 19 | } 20 | 21 | func (s *UDPServerSession) WriteBuffer(b []byte) (n int) { 22 | l := len(b) 23 | s.buf = append(s.buf, b...) 24 | return l 25 | } 26 | 27 | func (s *UDPServerSession) Write(b []byte) (n int, err error) { 28 | n, err = s.conn.WriteTo(b, s.GetAddress()) 29 | 30 | return 31 | } 32 | 33 | func (s *UDPServerSession) Read(b []byte) (n int, err error) { 34 | data := <-s.rcvd 35 | copy(b, data) 36 | return len(data), nil 37 | } 38 | 39 | func (s *UDPServerSession) GetServer() CoapServer { 40 | return s.server 41 | } 42 | -------------------------------------------------------------------------------- /test-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "mode: set" > acc.out 4 | fail=0 5 | 6 | # Standard go tooling behavior is to ignore dirs with leading underscors 7 | for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -type d); 8 | do 9 | if ls $dir/*.go &> /dev/null; then 10 | go test -coverprofile=profile.out $dir || fail=1 11 | if [ -f profile.out ] 12 | then 13 | cat profile.out | grep -v "mode: set" >> acc.out 14 | rm profile.out 15 | fi 16 | fi 17 | done 18 | 19 | # Failures have incomplete results, so don't send 20 | if [ -n "$COVERALLS" ] && [ "$fail" -eq 0 ] 21 | then 22 | goveralls -v -coverprofile=acc.out $COVERALLS 23 | fi 24 | 25 | rm -f acc.out 26 | 27 | exit $fail -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | type CoreAttributes []*CoreAttribute 4 | 5 | type CoreResource struct { 6 | Target string 7 | Attributes CoreAttributes 8 | } 9 | 10 | type CoreAttribute struct { 11 | Key string 12 | Value interface{} 13 | } 14 | 15 | // Adds an attribute (key/value) for a given core resource 16 | func (c *CoreResource) AddAttribute(key string, value interface{}) { 17 | c.Attributes = append(c.Attributes, NewCoreAttribute(key, value)) 18 | } 19 | 20 | // Gets an attribute for a core resource 21 | func (c *CoreResource) GetAttribute(key string) *CoreAttribute { 22 | for _, attr := range c.Attributes { 23 | if attr.Key == key { 24 | return attr 25 | } 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /utilconn_test.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import "testing" 4 | 5 | func TestSendMessages(t *testing.T) { 6 | //var conn Connection 7 | //var s CoapServer 8 | //_, err := SendMessageTo(s, nil, conn, nil) 9 | //assert.NotNil(t, err) 10 | //assert.Equal(t, ErrNilConn, err) 11 | // 12 | //conn = NewUDPConnection(nil) 13 | //SendMessageTo(s, nil, conn, nil) 14 | //_, err = SendMessageTo(s, nil, conn, nil) 15 | //assert.NotNil(t, err) 16 | //assert.Equal(t, ErrNilMessage, err) 17 | // 18 | //_, err = SendMessageTo(s, NewEmptyMessage(12345), conn, nil) 19 | //assert.NotNil(t, err) 20 | //assert.Equal(t, ErrNilAddr, err) 21 | // 22 | //addr := &net.UDPAddr{} 23 | //conn = NewMockCanopusUDPConnection(CoapCodeCreated, false, false) 24 | //msg := NewBasicConfirmableMessage() 25 | //_, err = SendMessageTo(s, msg, conn, addr) 26 | //assert.Nil(t, err) 27 | // 28 | //msg.MessageType = MessageNonConfirmable 29 | //_, err = SendMessageTo(s, msg, conn, addr) 30 | //assert.Nil(t, err) 31 | // 32 | //conn = NewMockCanopusUDPConnection(CoapCodeCreated, false, true) 33 | //msg.MessageType = MessageConfirmable 34 | //_, err = SendMessageTo(s, msg, conn, addr) 35 | //assert.NotNil(t, err) 36 | } 37 | -------------------------------------------------------------------------------- /utildebug.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | // PrintOptions pretty prints out a given Message's options 4 | func PrintOptions(msg Message) { 5 | opts := msg.GetAllOptions() 6 | logMsg(" - - - OPTIONS - - - ") 7 | if len(opts) > 0 { 8 | for _, opts := range msg.GetAllOptions() { 9 | logMsg("Code/Number: ", opts.GetCode(), ", Name: ", OptionNumberToString(opts.GetCode()), ", Value: ", opts.GetValue()) 10 | } 11 | } else { 12 | logMsg("None") 13 | } 14 | } 15 | 16 | // PrintMessage pretty prints out a given Message 17 | func PrintMessage(msg Message) { 18 | logMsg("= = = = = = = = = = = = = = = = ") 19 | logMsg("Code: ", msg.GetCode()) 20 | logMsg("Code String: ", CoapCodeToString(msg.GetCode())) 21 | logMsg("MessageId: ", msg.GetMessageId()) 22 | logMsg("MessageType: ", msg.GetMessageType()) 23 | logMsg("Token: ", string(msg.GetToken())) 24 | logMsg("Token Length: ", msg.GetTokenLength()) 25 | logMsg("Payload: ", PayloadAsString(msg.GetPayload())) 26 | PrintOptions(msg) 27 | logMsg("= = = = = = = = = = = = = = = = ") 28 | } 29 | 30 | // OptionNumberToString returns the string representation of a given Option Code 31 | func OptionNumberToString(o OptionCode) string { 32 | switch o { 33 | case OptionIfMatch: 34 | return "If-Match" 35 | 36 | case OptionURIHost: 37 | return "Uri-Host" 38 | 39 | case OptionEtag: 40 | return "ETag" 41 | 42 | case OptionIfNoneMatch: 43 | return "If-None-Match" 44 | 45 | case OptionURIPort: 46 | return "Uri-Port" 47 | 48 | case OptionLocationPath: 49 | return "Location-Path" 50 | 51 | case OptionURIPath: 52 | return "Uri-Path" 53 | 54 | case OptionContentFormat: 55 | return "Content-Format" 56 | 57 | case OptionMaxAge: 58 | return "Max-Age" 59 | 60 | case OptionURIQuery: 61 | return "Uri-Query" 62 | 63 | case OptionAccept: 64 | return "Accept" 65 | 66 | case OptionLocationQuery: 67 | return "Location-Query" 68 | 69 | case OptionBlock2: 70 | return "Block2" 71 | 72 | case OptionBlock1: 73 | return "Block1" 74 | 75 | case OptionProxyURI: 76 | return "Proxy-Uri" 77 | 78 | case OptionProxyScheme: 79 | return "Proxy-Scheme" 80 | 81 | case OptionSize1: 82 | return "Size1" 83 | 84 | case OptionSize2: 85 | return "Size2" 86 | 87 | default: 88 | return "" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "regexp" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Returns the string value for a Message Payload 12 | func PayloadAsString(p MessagePayload) string { 13 | if p == nil { 14 | return "" 15 | } 16 | return p.String() 17 | } 18 | 19 | // GenerateMessageId generate a uint16 Message ID 20 | func GenerateMessageID() uint16 { 21 | MESSAGEID_MUTEX.Lock() 22 | if CurrentMessageID != 65535 { 23 | CurrentMessageID++ 24 | } else { 25 | CurrentMessageID = 1 26 | } 27 | MESSAGEID_MUTEX.Unlock() 28 | 29 | return uint16(CurrentMessageID) 30 | } 31 | 32 | var genChars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") 33 | 34 | // GenerateToken generates a random token by a given length 35 | func GenerateToken(l int) string { 36 | rand.Seed(time.Now().UTC().UnixNano()) 37 | token := make([]rune, l) 38 | for i := range token { 39 | token[i] = genChars[rand.Intn(len(genChars))] 40 | } 41 | return string(token) 42 | } 43 | 44 | // CoreResourcesFromString Converts to CoRE Resources Object from a CoRE String 45 | func CoreResourcesFromString(str string) []*CoreResource { 46 | var re = regexp.MustCompile(`(<[^>]+>\s*(;\s*\w+\s*(=\s*(\w+|"([^"\\]*(\\.[^"\\]*)*)")\s*)?)*)`) 47 | var elemRe = regexp.MustCompile(`<[^>]*>`) 48 | 49 | var resources []*CoreResource 50 | m := re.FindAllString(str, -1) 51 | 52 | for _, match := range m { 53 | elemMatch := elemRe.FindString(match) 54 | target := elemMatch[1 : len(elemMatch)-1] 55 | 56 | resource := NewCoreResource() 57 | resource.Target = target 58 | 59 | if len(match) > len(elemMatch) { 60 | attrs := strings.Split(match[len(elemMatch)+1:], ";") 61 | 62 | for _, attr := range attrs { 63 | pair := strings.Split(attr, "=") 64 | 65 | resource.AddAttribute(pair[0], strings.Replace(pair[1], "\"", "", -1)) 66 | } 67 | } 68 | resources = append(resources, resource) 69 | } 70 | return resources 71 | } 72 | 73 | // CoapCodeToString returns the string representation of a CoapCode 74 | func CoapCodeToString(code CoapCode) string { 75 | switch code { 76 | case Get: 77 | return "GET" 78 | 79 | case Post: 80 | return "POST" 81 | 82 | case Put: 83 | return "PUT" 84 | 85 | case Delete: 86 | return "DELETE" 87 | 88 | case CoapCodeEmpty: 89 | return "0 Empty" 90 | 91 | case CoapCodeCreated: 92 | return "201 Created" 93 | 94 | case CoapCodeDeleted: 95 | return "202 Deleted" 96 | 97 | case CoapCodeValid: 98 | return "203 Valid" 99 | 100 | case CoapCodeChanged: 101 | return "204 Changed" 102 | 103 | case CoapCodeContent: 104 | return "205 Content" 105 | 106 | case CoapCodeBadRequest: 107 | return "400 Bad Request" 108 | 109 | case CoapCodeUnauthorized: 110 | return "401 Unauthorized" 111 | 112 | case CoapCodeBadOption: 113 | return "402 Bad Option" 114 | 115 | case CoapCodeForbidden: 116 | return "403 Forbidden" 117 | 118 | case CoapCodeNotFound: 119 | return "404 Not Found" 120 | 121 | case CoapCodeMethodNotAllowed: 122 | return "405 Method Not Allowed" 123 | 124 | case CoapCodeNotAcceptable: 125 | return "406 Not Acceptable" 126 | 127 | case CoapCodePreconditionFailed: 128 | return "412 Precondition Failed" 129 | 130 | case CoapCodeRequestEntityTooLarge: 131 | return "413 Request Entity Too Large" 132 | 133 | case CoapCodeUnsupportedContentFormat: 134 | return "415 Unsupported Content Format" 135 | 136 | case CoapCodeInternalServerError: 137 | return "500 Internal Server Error" 138 | 139 | case CoapCodeNotImplemented: 140 | return "501 Not Implemented" 141 | 142 | case CoapCodeBadGateway: 143 | return "502 Bad Gateway" 144 | 145 | case CoapCodeServiceUnavailable: 146 | return "503 Service Unavailable" 147 | 148 | case CoapCodeGatewayTimeout: 149 | return "504 Gateway Timeout" 150 | 151 | case CoapCodeProxyingNotSupported: 152 | return "505 Proxying Not Supported" 153 | 154 | default: 155 | return "Unknown" 156 | } 157 | } 158 | 159 | // ValidCoapMediaTypeCode Checks if a MediaType is of a valid code 160 | func ValidCoapMediaTypeCode(mt MediaType) bool { 161 | switch mt { 162 | case MediaTypeTextPlain, MediaTypeTextXML, MediaTypeTextCsv, MediaTypeTextHTML, MediaTypeImageGif, 163 | MediaTypeImageJpeg, MediaTypeImagePng, MediaTypeImageTiff, MediaTypeAudioRaw, MediaTypeVideoRaw, 164 | MediaTypeApplicationLinkFormat, MediaTypeApplicationXML, MediaTypeApplicationOctetStream, MediaTypeApplicationRdfXML, 165 | MediaTypeApplicationSoapXML, MediaTypeApplicationAtomXML, MediaTypeApplicationXmppXML, MediaTypeApplicationExi, 166 | MediaTypeApplicationFastInfoSet, MediaTypeApplicationSoapFastInfoSet, MediaTypeApplicationJSON, 167 | MediaTypeApplicationXObitBinary, MediaTypeTextPlainVndOmaLwm2m, MediaTypeTlvVndOmaLwm2m, 168 | MediaTypeJSONVndOmaLwm2m, MediaTypeOpaqueVndOmaLwm2m: 169 | return true 170 | } 171 | 172 | return false 173 | } 174 | 175 | func logMsg(a ...interface{}) (n int, err error) { 176 | return fmt.Println(a) 177 | } 178 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGenerateMessageId(t *testing.T) { 10 | 11 | var id, id2 uint16 12 | id = GenerateMessageID() 13 | for i := 0; i < 100; i++ { 14 | id2 = id + 1 15 | id = GenerateMessageID() 16 | assert.NotEqual(t, 65535, id) 17 | assert.Equal(t, id2, id) 18 | } 19 | 20 | CurrentMessageID = 65535 21 | id = GenerateMessageID() 22 | assert.Equal(t, uint16(1), id) 23 | } 24 | 25 | func TestGenerateToken(t *testing.T) { 26 | assert.Equal(t, "", GenerateToken(0)) 27 | 28 | for i := 1; i < 10; i++ { 29 | tok := GenerateToken(i) 30 | assert.NotEqual(t, "", tok) 31 | assert.Equal(t, i, len(tok)) 32 | } 33 | } 34 | 35 | func TestCoreResourceUtil(t *testing.T) { 36 | var resources []*CoreResource 37 | 38 | resources = CoreResourcesFromString("") 39 | 40 | assert.Equal(t, 0, len(resources)) 41 | 42 | resources = CoreResourcesFromString(";ct=41;rt=\"temperature-c\";if=\"sensor\", ;ct=41;rt=\"light-lux\";if=\"sensor\"") 43 | 44 | assert.Equal(t, 2, len(resources)) 45 | resource1 := resources[0] 46 | 47 | assert.Equal(t, "/sensors/temp", resource1.Target) 48 | assert.Equal(t, 3, len(resource1.Attributes)) 49 | 50 | assert.Nil(t, resource1.GetAttribute("invalid_attr")) 51 | 52 | assert.NotNil(t, resource1.GetAttribute("ct")) 53 | assert.Equal(t, "ct", resource1.GetAttribute("ct").Key) 54 | assert.Equal(t, "41", resource1.GetAttribute("ct").Value) 55 | 56 | assert.NotNil(t, resource1.GetAttribute("rt")) 57 | assert.Equal(t, "rt", resource1.GetAttribute("rt").Key) 58 | assert.Equal(t, "temperature-c", resource1.GetAttribute("rt").Value) 59 | 60 | assert.NotNil(t, resource1.GetAttribute("if")) 61 | assert.Equal(t, "if", resource1.GetAttribute("if").Key) 62 | assert.Equal(t, "sensor", resource1.GetAttribute("if").Value) 63 | 64 | resource2 := resources[1] 65 | assert.Equal(t, "/sensors/light", resource2.Target) 66 | assert.Equal(t, 3, len(resource2.Attributes)) 67 | 68 | assert.NotNil(t, resource2.GetAttribute("ct")) 69 | assert.Equal(t, "ct", resource2.GetAttribute("ct").Key) 70 | assert.Equal(t, "41", resource2.GetAttribute("ct").Value) 71 | 72 | assert.NotNil(t, resource2.GetAttribute("rt")) 73 | assert.Equal(t, "rt", resource2.GetAttribute("rt").Key) 74 | assert.Equal(t, "light-lux", resource2.GetAttribute("rt").Value) 75 | 76 | assert.NotNil(t, resource2.GetAttribute("if")) 77 | assert.Equal(t, "if", resource2.GetAttribute("if").Key) 78 | assert.Equal(t, "sensor", resource2.GetAttribute("if").Value) 79 | } 80 | 81 | func TestCoapCodeToString(t *testing.T) { 82 | testData := []struct { 83 | coapCode CoapCode 84 | codeString string 85 | }{ 86 | {Get, "GET"}, 87 | {Post, "POST"}, 88 | {Put, "PUT"}, 89 | {Delete, "DELETE"}, 90 | {CoapCodeEmpty, "0 Empty"}, 91 | {CoapCodeCreated, "201 Created"}, 92 | {CoapCodeDeleted, "202 Deleted"}, 93 | {CoapCodeValid, "203 Valid"}, 94 | {CoapCodeChanged, "204 Changed"}, 95 | {CoapCodeContent, "205 Content"}, 96 | {CoapCodeBadRequest, "400 Bad Request"}, 97 | {CoapCodeUnauthorized, "401 Unauthorized"}, 98 | {CoapCodeBadOption, "402 Bad Option"}, 99 | {CoapCodeForbidden, "403 Forbidden"}, 100 | {CoapCodeNotFound, "404 Not Found"}, 101 | {CoapCodeMethodNotAllowed, "405 Method Not Allowed"}, 102 | {CoapCodeNotAcceptable, "406 Not Acceptable"}, 103 | {CoapCodePreconditionFailed, "412 Precondition Failed"}, 104 | {CoapCodeRequestEntityTooLarge, "413 Request Entity Too Large"}, 105 | {CoapCodeUnsupportedContentFormat, "415 Unsupported Content Format"}, 106 | {CoapCodeInternalServerError, "500 Internal Server Error"}, 107 | {CoapCodeNotImplemented, "501 Not Implemented"}, 108 | {CoapCodeBadGateway, "502 Bad Gateway"}, 109 | {CoapCodeServiceUnavailable, "503 Service Unavailable"}, 110 | {CoapCodeGatewayTimeout, "504 Gateway Timeout"}, 111 | {CoapCodeProxyingNotSupported, "505 Proxying Not Supported"}, 112 | {CoapCode(255), "Unknown"}, 113 | } 114 | 115 | for _, td := range testData { 116 | assert.Equal(t, td.codeString, CoapCodeToString(td.coapCode)) 117 | } 118 | } 119 | 120 | func TestRouteMatching(t *testing.T) { 121 | 122 | } 123 | 124 | func TestMediaTypeUtils(t *testing.T) { 125 | assert.True(t, ValidCoapMediaTypeCode(MediaTypeTextPlain)) 126 | assert.True(t, ValidCoapMediaTypeCode(MediaTypeOpaqueVndOmaLwm2m)) 127 | 128 | assert.False(t, ValidCoapMediaTypeCode(MediaType(9999))) 129 | } 130 | -------------------------------------------------------------------------------- /xml.go: -------------------------------------------------------------------------------- 1 | package canopus 2 | 3 | // Represents a message payload containing XML String 4 | type XMLPayload struct { 5 | } 6 | --------------------------------------------------------------------------------