coap 0.0.16 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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