coap 0.0.16 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +13 -2
  4. data/Gemfile +0 -1
  5. data/LICENSE +2 -2
  6. data/README.md +37 -33
  7. data/Rakefile +12 -3
  8. data/bin/coap +111 -0
  9. data/coap.gemspec +34 -29
  10. data/lib/coap.rb +3 -34
  11. data/lib/core.rb +11 -0
  12. data/lib/core/coap.rb +42 -0
  13. data/lib/core/coap/block.rb +98 -0
  14. data/lib/core/coap/client.rb +314 -0
  15. data/lib/core/coap/coap.rb +26 -0
  16. data/lib/core/coap/coding.rb +146 -0
  17. data/lib/core/coap/fsm.rb +82 -0
  18. data/lib/core/coap/message.rb +203 -0
  19. data/lib/core/coap/observer.rb +40 -0
  20. data/lib/core/coap/options.rb +44 -0
  21. data/lib/core/coap/registry.rb +32 -0
  22. data/lib/core/coap/registry/content_formats.yml +7 -0
  23. data/lib/core/coap/resolver.rb +17 -0
  24. data/lib/core/coap/transmission.rb +165 -0
  25. data/lib/core/coap/types.rb +69 -0
  26. data/lib/core/coap/utility.rb +34 -0
  27. data/lib/core/coap/version.rb +5 -0
  28. data/lib/core/core_ext/socket.rb +19 -0
  29. data/lib/core/hexdump.rb +18 -0
  30. data/lib/core/link.rb +97 -0
  31. data/lib/core/os.rb +15 -0
  32. data/spec/block_spec.rb +160 -0
  33. data/spec/client_spec.rb +86 -0
  34. data/spec/fixtures/coap.me.link +1 -0
  35. data/spec/link_spec.rb +98 -0
  36. data/spec/registry_spec.rb +39 -0
  37. data/spec/resolver_spec.rb +19 -0
  38. data/spec/spec_helper.rb +17 -0
  39. data/spec/transmission_spec.rb +70 -0
  40. data/test/helper.rb +15 -0
  41. data/test/test_client.rb +99 -228
  42. data/test/test_message.rb +99 -71
  43. metadata +140 -37
  44. data/bin/client +0 -42
  45. data/lib/coap/block.rb +0 -45
  46. data/lib/coap/client.rb +0 -364
  47. data/lib/coap/coap.rb +0 -273
  48. data/lib/coap/message.rb +0 -187
  49. data/lib/coap/mysocket.rb +0 -81
  50. data/lib/coap/observer.rb +0 -41
  51. data/lib/coap/version.rb +0 -3
  52. data/lib/misc/hexdump.rb +0 -17
  53. data/test/coap_test_helper.rb +0 -2
  54. data/test/disabled_econotag_blck.rb +0 -33
@@ -0,0 +1,98 @@
1
+ module CoRE
2
+ module CoAP
3
+ class Block
4
+ VALID_SIZE = [16, 32, 64, 128, 256, 512, 1024].freeze
5
+ MAX_NUM = (1048576 - 1).freeze
6
+
7
+ attr_reader :num, :more, :size
8
+
9
+ def initialize(*args)
10
+ if args.size == 1
11
+ @encoded = args.first.to_i
12
+ else
13
+ @decoded = []
14
+ self.num, self.more, self.size = args
15
+ @decoded = [self.num, self.more, self.size]
16
+ end
17
+
18
+ self
19
+ end
20
+
21
+ def chunk(data)
22
+ data[@size * @num, @size]
23
+ end
24
+
25
+ def chunk_count(data)
26
+ return 0 if data.nil? || data.empty?
27
+ i = data.size % self.size == 0 ? 0 : 1
28
+ data.size / self.size + i
29
+ end
30
+
31
+ def chunkify(data)
32
+ Block.chunkify(data, self.size)
33
+ end
34
+
35
+ def decode
36
+ if @encoded == 0
37
+ @decoded = [0, false, 16]
38
+ else
39
+ @decoded = [@encoded >> 4, (@encoded & 8) == 8, 16 << (@encoded & 7)]
40
+ end
41
+
42
+ self.num, self.more, self.size = @decoded
43
+
44
+ self
45
+ end
46
+
47
+ def encode
48
+ @encoded = @num << 4 | (@more ? 1 : 0) << 3 | CoAP.number_of_bits_up_to(@size) - 4
49
+ end
50
+
51
+ def included_by?(body)
52
+ return true if self.num == 0 && (body.nil? || body.empty?)
53
+ self.num < chunk_count(body)
54
+ end
55
+
56
+ def last?(data)
57
+ return true if data.nil? || data.empty?
58
+ self.num == chunk_count(data) - 1
59
+ end
60
+
61
+ def more=(v)
62
+ if @num > MAX_NUM
63
+ raise ArgumentError, 'num MUST be < 1048576'
64
+ end
65
+
66
+ @more = !!v
67
+ @decoded[1] = @more
68
+ end
69
+
70
+ def more?(data)
71
+ return false if data.nil? || data.empty?
72
+ data.bytesize > (self.num + 1) * self.size
73
+ end
74
+
75
+ def num=(v)
76
+ @num = v.to_i
77
+ @decoded[0] = @num
78
+ end
79
+
80
+ def set_more!(body)
81
+ self.more = self.more?(body)
82
+ end
83
+
84
+ def size=(v)
85
+ unless VALID_SIZE.include?(v.to_i)
86
+ raise ArgumentError, 'size MUST be power of 2 between 16 and 1024.'
87
+ end
88
+
89
+ @size = v.to_i
90
+ @decoded[2] = @size
91
+ end
92
+
93
+ def self.chunkify(data, size)
94
+ data.bytes.each_slice(size).map { |c| c.pack('C*') }
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,314 @@
1
+ # encoding: utf-8
2
+
3
+ module CoRE
4
+ module CoAP
5
+ # CoAP client library
6
+ class Client
7
+ attr_accessor :max_payload, :host, :port
8
+
9
+ # @param options Valid options are (all optional): max_payload
10
+ # (maximum payload size, default 256), max_retransmit
11
+ # (maximum retransmission count, default 4),
12
+ # recv_timeout (timeout for ACK responses, default: 2),
13
+ # host (destination host), post (destination port,
14
+ # default 5683).
15
+ def initialize(options = {})
16
+ @max_payload = options[:max_payload] || 256
17
+
18
+ @host = options[:host]
19
+ @port = options[:port] || CoAP::PORT
20
+
21
+ @options = options
22
+
23
+ @logger = CoAP.logger
24
+ end
25
+
26
+ # Enable DTLS socket.
27
+ def use_dtls
28
+ require 'CoDTLS'
29
+ @options[:socket] = CoDTLS::SecureSocket
30
+ self
31
+ end
32
+
33
+ # GET
34
+ #
35
+ # @param path Path
36
+ # @param host Destination host
37
+ # @param port Destination port
38
+ # @param payload Payload
39
+ # @param options Options
40
+ #
41
+ # @return CoAP::Message
42
+ def get(*args)
43
+ client(:get, *args)
44
+ end
45
+
46
+ # GET by URI
47
+ #
48
+ # @param uri URI
49
+ # @param payload Payload
50
+ # @param options Options
51
+ #
52
+ # @return CoAP::Message
53
+ def get_by_uri(uri, *args)
54
+ get(*decode_uri(uri), *args)
55
+ end
56
+
57
+ # POST
58
+ #
59
+ # @param host Destination host
60
+ # @param port Destination port
61
+ # @param path Path
62
+ # @param payload Payload
63
+ # @param options Options
64
+ #
65
+ # @return CoAP::Message
66
+ def post(*args)
67
+ client(:post, *args)
68
+ end
69
+
70
+ # POST by URI
71
+ #
72
+ # @param uri URI
73
+ # @param payload Payload
74
+ # @param options Options
75
+ #
76
+ # @return CoAP::Message
77
+ def post_by_uri(uri, *args)
78
+ post(*decode_uri(uri), *args)
79
+ end
80
+
81
+ # PUT
82
+ #
83
+ # @param host Destination host
84
+ # @param port Destination port
85
+ # @param path Path
86
+ # @param payload Payload
87
+ # @param options Options
88
+ #
89
+ # @return CoAP::Message
90
+ def put(*args)
91
+ client(:put, *args)
92
+ end
93
+
94
+ # PUT by URI
95
+ #
96
+ # @param uri URI
97
+ # @param payload Payload
98
+ # @param options Options
99
+ #
100
+ # @return CoAP::Message
101
+ def put_by_uri(uri, *args)
102
+ put(*decode_uri(uri), *args)
103
+ end
104
+
105
+ # DELETE
106
+ #
107
+ # @param host Destination host
108
+ # @param port Destination port
109
+ # @param path Path
110
+ # @param payload Payload
111
+ # @param options Options
112
+ #
113
+ # @return CoAP::Message
114
+ def delete(*args)
115
+ client(:delete, *args)
116
+ end
117
+
118
+ # DELETE by URI
119
+ #
120
+ # @param uri URI
121
+ # @param payload Payload
122
+ # @param options Options
123
+ #
124
+ # @return CoAP::Message
125
+ def delete_by_uri(uri, *args)
126
+ delete(*decode_uri(uri), *args)
127
+ end
128
+
129
+ # OBSERVE
130
+ #
131
+ # @param host Destination host
132
+ # @param port Destination port
133
+ # @param path Path
134
+ # @param callback Method to call with the observe data. Must provide
135
+ # arguments payload and socket.
136
+ # @param payload Payload
137
+ # @param options Options
138
+ #
139
+ # @return CoAP::Message
140
+ def observe(path, host, port, callback, payload = nil, options = {})
141
+ options[:observe] = 0
142
+ client(:get, path, host, port, payload, options, callback)
143
+ end
144
+
145
+ # OBSERVE by URI
146
+ #
147
+ # @param uri URI
148
+ # @param callback Method to call with the observe data. Must provide
149
+ # arguments payload and socket.
150
+ # @param payload Payload
151
+ # @param options Options
152
+ #
153
+ # @return CoAP::Message
154
+ def observe_by_uri(uri, *args)
155
+ observe(*decode_uri(uri), *args)
156
+ end
157
+
158
+ private
159
+
160
+ def client(method, path, host = nil, port = nil, payload = nil, options = {}, observe_callback = nil)
161
+ # Set host and port only one time on multiple requests
162
+ host.nil? ? (host = @host unless @host.nil?) : @host = host
163
+ port.nil? ? (port = @port unless @port.nil?) : @port = port
164
+
165
+ path, query = path.split('?')
166
+
167
+ validate_arguments!(host, port, path, payload)
168
+
169
+ szx = 2 ** CoAP.number_of_bits_up_to(@max_payload)
170
+
171
+ # Initialize block2 with payload size.
172
+ block2 = Block.new(0, false, szx)
173
+
174
+ # Initialize block1.
175
+ block1 = if options[:block1].nil?
176
+ Block.new(0, false, szx)
177
+ else
178
+ Block.new(options[:block1]).decode
179
+ end
180
+
181
+ # Initialize chunks if payload size > max_payload.
182
+ if !payload.nil? && payload.bytesize > @max_payload
183
+ chunks = Block.chunkify(payload, @max_payload)
184
+ else
185
+ chunks = [payload]
186
+ end
187
+
188
+ # Create CoAP message struct.
189
+ message = initialize_message(method, path, query, payload)
190
+ message.mid = options.delete(:mid) if options[:mid]
191
+
192
+ # Set message type to non if chosen in global or local options.
193
+ if options.delete(:tt) == :non || @options.delete(:tt) == :non
194
+ message.tt = :non
195
+ end
196
+
197
+ # If more than 1 chunk, we need to use block1.
198
+ if !payload.nil? && chunks.size > 1
199
+ # Increase block number.
200
+ block1.num += 1 unless options[:block1].nil?
201
+
202
+ # More chunks?
203
+ if chunks.size > block1.num + 1
204
+ block1.more = true
205
+ message.options.delete(:block2)
206
+ else
207
+ block1.more = false
208
+ end
209
+
210
+ # Set final payload.
211
+ message.payload = chunks[block1.num]
212
+
213
+ # Set block1 message option.
214
+ message.options[:block1] = block1.encode
215
+ end
216
+
217
+ # Preserve user options.
218
+ message.options[:block2] = options[:block2] unless options[:block2] == nil
219
+ message.options[:observe] = options[:observe] unless options[:observe] == nil
220
+ message.options.merge!(options)
221
+
222
+ log_message(:sending_message, message)
223
+
224
+ # Wait for answer and retry sending message if timeout reached.
225
+ @transmission, recv_parsed = Transmission.request(message, host, port, @options)
226
+
227
+ log_message(:received_message, recv_parsed)
228
+
229
+ # Payload is not fully transmitted.
230
+ # TODO Get rid of nasty recursion.
231
+ if block1.more
232
+ return client(method, path, host, port, payload, message.options)
233
+ end
234
+
235
+ # Test for more block2 payload.
236
+ block2 = Block.new(recv_parsed.options[:block2]).decode
237
+
238
+ if block2.more
239
+ block2.num += 1
240
+
241
+ options.delete(:block1) # end block1
242
+ options[:block2] = block2.encode
243
+
244
+ local_recv_parsed = client(method, path, host, port, nil, options)
245
+
246
+ unless local_recv_parsed.nil?
247
+ recv_parsed.payload << local_recv_parsed.payload
248
+ end
249
+ end
250
+
251
+ # Do we need to observe?
252
+ if recv_parsed.options[:observe]
253
+ CoAP::Observer.new.observe(recv_parsed, observe_callback, @transmission)
254
+ end
255
+
256
+ recv_parsed
257
+ end
258
+
259
+ private
260
+
261
+ # Decode CoAP URIs.
262
+ def decode_uri(uri)
263
+ uri = CoAP.scheme_and_authority_decode(uri.to_s)
264
+
265
+ @logger.debug 'URI decoded: ' + uri.inspect
266
+ fail ArgumentError, 'Invalid URI' if uri.nil?
267
+
268
+ uri
269
+ end
270
+
271
+ def initialize_message(method, path, query = nil, payload = nil)
272
+ mid = SecureRandom.random_number(0xffff)
273
+ token = SecureRandom.random_number(0xff)
274
+
275
+ options = {
276
+ uri_path: CoAP.path_decode(path),
277
+ token: token
278
+ }
279
+
280
+ unless query.nil?
281
+ options[:uri_query] = CoAP.query_decode(query)
282
+ end
283
+
284
+ Message.new(:con, method, mid, payload, options)
285
+ end
286
+
287
+ # Log message to debug log.
288
+ def log_message(text, message)
289
+ @logger.debug '### ' + text.to_s.upcase.gsub('_', ' ')
290
+ @logger.debug message.inspect
291
+ @logger.debug message.to_s.hexdump if $DEBUG
292
+ end
293
+
294
+ # Raise ArgumentError exceptions on wrong client method arguments.
295
+ def validate_arguments!(host, port, path, payload)
296
+ if host.nil? || host.empty?
297
+ fail ArgumentError, 'Argument «host» missing.'
298
+ end
299
+
300
+ if port.nil? || !port.is_a?(Integer)
301
+ fail ArgumentError, 'Argument «port» missing or not an Integer.'
302
+ end
303
+
304
+ if path.nil? || path.empty?
305
+ fail ArgumentError, 'Argument «path» missing.'
306
+ end
307
+
308
+ if !payload.nil? && (payload.empty? || !payload.is_a?(String))
309
+ fail ArgumentError, 'Argument «payload» must be a non-emtpy String'
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,26 @@
1
+ # coapmessage.rb
2
+ # Copyright (C) 2010..2013 Carsten Bormann <cabo@tzi.org>
3
+
4
+ module CoRE
5
+ module CoAP
6
+ extend Utility
7
+
8
+ include Coding
9
+ include Options
10
+
11
+ EMPTY = empty_buffer.freeze
12
+
13
+ TTYPES = [:con, :non, :ack, :rst]
14
+ TTYPES_I = invert_into_hash(TTYPES)
15
+
16
+ METHODS = [nil, :get, :post, :put, :delete]
17
+ METHODS_I = invert_into_hash(METHODS)
18
+
19
+ PORT = 5683
20
+
21
+ # Shortcut: CoRE::CoAP::parse == CoRE::CoAP::Message.parse
22
+ def self.parse(*args)
23
+ Message.parse(*args)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,146 @@
1
+ # coapmessage.rb
2
+ # Copyright (C) 2010..2013 Carsten Bormann <cabo@tzi.org>
3
+
4
+ module CoRE
5
+ module CoAP
6
+ module Coding
7
+ BIN = Encoding::BINARY
8
+ UTF8 = Encoding::UTF_8
9
+
10
+ UNRESERVED = '\w\-\.~' # Alphanumerics, '_', '-', '.', '~'
11
+ SUB_DELIM = "!$&'()*+,;="
12
+ SUB_DELIM_NO_AMP = SUB_DELIM.delete('&')
13
+
14
+ PATH_UNENCODED = "#{UNRESERVED}#{SUB_DELIM}:@"
15
+ PATH_ENCODED_RE = /[^#{PATH_UNENCODED}]/mn
16
+
17
+ QUERY_UNENCODED = "#{UNRESERVED}#{SUB_DELIM_NO_AMP}:@/?"
18
+ QUERY_ENCODED_RE = /[^#{QUERY_UNENCODED}]/mn
19
+
20
+ # Also extend on include.
21
+ def self.included(base)
22
+ base.send :extend, Coding
23
+ end
24
+
25
+ # The variable-length binary (vlb) numbers defined in CoRE-CoAP Appendix A.
26
+ def vlb_encode(n)
27
+ n = Integer(n)
28
+ v = empty_buffer
29
+
30
+ if n < 0
31
+ raise ArgumentError, "Can't encode negative number #{n}."
32
+ end
33
+
34
+ while n > 0
35
+ v << (n & 0xFF)
36
+ n >>= 8
37
+ end
38
+
39
+ v.reverse!
40
+ end
41
+
42
+ def vlb_decode(s)
43
+ n = 0
44
+ s.each_byte { |b| n <<= 8; n += b }
45
+ n
46
+ end
47
+
48
+ # Byte strings lexicographically goedelized as numbers (one+256 coding)
49
+ def o256_encode(num)
50
+ str = empty_buffer
51
+
52
+ while num > 0
53
+ num -= 1
54
+ str << (num & 0xFF)
55
+ num >>= 8
56
+ end
57
+
58
+ str.reverse
59
+ end
60
+
61
+ def o256_decode(str)
62
+ num = 0
63
+
64
+ str.each_byte do |b|
65
+ num <<= 8
66
+ num += b + 1
67
+ end
68
+
69
+ num
70
+ end
71
+
72
+ # n must be 2**k
73
+ # Returns k
74
+ def number_of_bits_up_to(n)
75
+ # Math.frexp(n-1)[1]
76
+ Math.log2(n).floor
77
+ end
78
+
79
+ def scheme_and_authority_encode(host, port)
80
+ unless host =~ /\[.*\]/
81
+ host = "[#{host}]" if host =~ /:/
82
+ end
83
+
84
+ port = Integer(port)
85
+
86
+ scheme_and_authority = "coap://#{host}"
87
+ scheme_and_authority << ":#{port}" unless port == CoAP::PORT
88
+ scheme_and_authority
89
+ end
90
+
91
+ def scheme_and_authority_decode(s)
92
+ if s =~ %r{\A(?:coap://)((?:\[|%5B)([^\]]*)(?:\]|%5D)|([^:/]*))(:(\d+))?(/.*)?\z}i
93
+ host = $2 || $3 # Should check syntax...
94
+ port = $5 || CoAP::PORT
95
+ [$6, host, port.to_i]
96
+ end
97
+ end
98
+
99
+ def path_encode(uri_elements)
100
+ return '/' if uri_elements.nil? || uri_elements.empty?
101
+ '/' + uri_elements.map { |el| uri_encode_element(el, PATH_ENCODED_RE) }.join('/')
102
+ end
103
+
104
+ def query_encode(query_elements)
105
+ return '' if query_elements.nil? || query_elements.empty?
106
+ '?' + query_elements.map { |el| uri_encode_element(el, QUERY_ENCODED_RE) }.join('&')
107
+ end
108
+
109
+ def uri_encode_element(el, re)
110
+ el.dup.force_encoding(BIN).gsub(re) { |x| "%%%02X" % x.ord }
111
+ end
112
+
113
+ def percent_decode(el)
114
+ el.gsub(/%(..)/) { $1.to_i(16).chr(BIN) }.force_encoding(UTF8)
115
+ end
116
+
117
+ def path_decode(path)
118
+ # Needs -1 to avoid eating trailing slashes!
119
+ a = path.split('/', -1)
120
+
121
+ return a if a.empty?
122
+
123
+ if a[0] != ''
124
+ raise ArgumentError, "Path #{path.inspect} not starting with /"
125
+ end
126
+
127
+ # Special case for '/'
128
+ return [] if a[1] == ''
129
+
130
+ a[1..-1].map { |el| percent_decode(el) }
131
+ end
132
+
133
+ def query_decode(query)
134
+ return [] if query.empty?
135
+
136
+ query = query[1..-1] if query[0] == '?'
137
+
138
+ a = query.split('&', -1).map do |el|
139
+ el.gsub(/%(..)/) { $1.to_i(16).chr(BIN) }.force_encoding(UTF8)
140
+ end
141
+
142
+ a
143
+ end
144
+ end
145
+ end
146
+ end