coap 0.0.13

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.
data/lib/coap/coap.rb ADDED
@@ -0,0 +1,273 @@
1
+ # coapmessage.rb
2
+ # Copyright (C) 2010..2013 Carsten Bormann <cabo@tzi.org>
3
+
4
+ module CoAP
5
+ BIN = Encoding::BINARY
6
+ UTF8 = Encoding::UTF_8
7
+
8
+ class << self
9
+ def empty_buffer
10
+ ''.encode(BIN)
11
+ end
12
+
13
+ def invert_into_hash(a)
14
+ a.each_with_index.each_with_object({}) { |k, h| h[k[0]] = k[1] if k[0] }
15
+ end
16
+
17
+ # Arrays that describe a specific option type:
18
+ # [default value, length range, repeatable?, decoder, encoder]
19
+
20
+ # we only care about presence or absence, always empty
21
+ def presence_once
22
+ [false, (0..0), false,
23
+ ->(a) { true },
24
+ ->(v) { v ? [''] : [] }
25
+ ]
26
+ end
27
+
28
+ # token-style, goedelized in o256 here
29
+ def o256_once(min, max, default = nil)
30
+ [default, (min..max), false,
31
+ ->(a) { o256_decode(a[0]) },
32
+ ->(v) { v == default ? [] : [o256_encode(v)] }
33
+ ]
34
+ end
35
+
36
+ def o256_many(min, max) # unused
37
+ [nil, (min..max), true,
38
+ ->(a) { a.map { |x| o256_decode(x) } },
39
+ ->(v) { Array(v).map { |x| o256_encode(x) } }
40
+ ]
41
+ end
42
+
43
+ # vlb as in core-coap Annex A
44
+ def uint_once(min, max, default = nil)
45
+ [default, (min..max), false,
46
+ ->(a) { vlb_decode(a[0]) },
47
+ ->(v) { v == default ? [] : [vlb_encode(v)] }
48
+ ]
49
+ end
50
+
51
+ def uint_many(min, max)
52
+ [nil, (min..max), true,
53
+ ->(a) { a.map { |x| vlb_decode(x) } },
54
+ ->(v) { Array(v).map { |x| vlb_encode(x) } }
55
+ ]
56
+ end
57
+
58
+ # Any other opaque byte sequence
59
+ def opaq_once(min, max, default = nil)
60
+ [default, (min..max), false,
61
+ ->(a) { a[0] },
62
+ ->(v) { v == default ? [] : Array(v) }
63
+ ]
64
+ end
65
+
66
+ def opaq_many(min, max)
67
+ [nil, (min..max), true,
68
+ ->(a) { a },
69
+ ->(v) { Array(v) }
70
+ ]
71
+ end
72
+
73
+ # same, but interpreted as UTF-8
74
+ def str_once(min, max, default = nil)
75
+ [default, (min..max), false,
76
+ ->(a) { a[0].force_encoding('utf-8') }, # XXX needed?
77
+ ->(v) { v == default ? [] : Array(v) }
78
+ ]
79
+ end
80
+
81
+ def str_many(min, max)
82
+ [nil, (min..max), true,
83
+ ->(a) { a.map { |s| s.force_encoding('utf-8') } }, # XXX needed?
84
+ ->(v) { Array(v) }
85
+ ]
86
+ end
87
+ end
88
+
89
+ EMPTY = empty_buffer.freeze
90
+
91
+ TTYPES = [:con, :non, :ack, :rst]
92
+ TTYPES_I = invert_into_hash(TTYPES)
93
+ METHODS = [nil, :get, :post, :put, :delete]
94
+ METHODS_I = invert_into_hash(METHODS)
95
+
96
+ # for now, keep 19
97
+ # TOKEN_ON = -1 # handled specially
98
+ TOKEN_ON = 19
99
+
100
+ # 14 => :user, default, length range, replicable?, decoder, encoder
101
+ OPTIONS = { # name minlength, maxlength, [default] defined where:
102
+ 1 => [:if_match, *o256_many(0, 8)], # core-coap-12
103
+ 3 => [:uri_host, *str_once(1, 255)], # core-coap-12
104
+ 4 => [:etag, *o256_many(1, 8)], # core-coap-12 !! once in rp
105
+ 5 => [:if_none_match, *presence_once], # core-coap-12
106
+ 6 => [:observe, *uint_once(0, 3)], # core-observe-07
107
+ 7 => [:uri_port, *uint_once(0, 2)], # core-coap-12
108
+ 8 => [:location_path, *str_many(0, 255)], # core-coap-12
109
+ 11 => [:uri_path, *str_many(0, 255)], # core-coap-12
110
+ 12 => [:content_format, *uint_once(0, 2)], # core-coap-12
111
+ 14 => [:max_age, *uint_once(0, 4, 60)], # core-coap-12
112
+ 15 => [:uri_query, *str_many(0, 255)], # core-coap-12
113
+ 17 => [:accept, *uint_once(0, 2)], # core-coap-18!
114
+ TOKEN_ON => [:token, *o256_once(1, 8, 0)], # core-coap-12 -> opaq_once(1, 8, EMPTY)
115
+ 20 => [:location_query, *str_many(0, 255)], # core-coap-12
116
+ 23 => [:block2, *uint_once(0, 3)], # core-block-10
117
+ 27 => [:block1, *uint_once(0, 3)], # core-block-10
118
+ 28 => [:size2, *uint_once(0, 4)], # core-block-10
119
+ 35 => [:proxy_uri, *str_once(1, 1034)], # core-coap-12
120
+ 39 => [:proxy_scheme, *str_once(1, 255)], # core-coap-13
121
+ 60 => [:size1, *uint_once(0, 4)], # core-block-10
122
+ }
123
+ # :user => 14, :user, def, range, rep, deco, enco
124
+ OPTIONS_I = Hash[OPTIONS.map { |k, v| [v[0], [k, *v]] }]
125
+ DEFAULTING_OPTIONS = Hash[OPTIONS.map { |k, v| [v[0].freeze, v[1].freeze] }
126
+ .select { |k, v| v }].freeze
127
+
128
+ class << self
129
+ def critical?(option)
130
+ oi = OPTIONS_I[option] # this is really an option name symbol
131
+ if oi
132
+ option = oi[0] # -> to option number
133
+ end
134
+ option.odd?
135
+ end
136
+
137
+ def unsafe?(option)
138
+ oi = OPTIONS_I[option] # this is really an option name symbol
139
+ if oi
140
+ option = oi[0] # -> to option number
141
+ end
142
+ option & 2 == 2
143
+ end
144
+
145
+ def no_cache_key?(option)
146
+ oi = OPTIONS_I[option] # this is really an option name symbol
147
+ if oi
148
+ option = oi[0] # -> to option number
149
+ end
150
+ option & 0x1e == 0x1c
151
+ end
152
+
153
+ # The variable-length binary (vlb) numbers defined in CoRE-CoAP Appendix A.
154
+ def vlb_encode(n)
155
+ # on = n
156
+ n = Integer(n)
157
+ fail ArgumentError, "Can't encode negative number #{n}" if n < 0
158
+ v = empty_buffer
159
+ while n > 0
160
+ v << (n & 0xFF)
161
+ n >>= 8
162
+ end
163
+ v.reverse!
164
+ # warn "Encoded #{on} as #{v.inspect}"
165
+ v
166
+ end
167
+
168
+ def vlb_decode(s)
169
+ n = 0
170
+ s.each_byte do |b|
171
+ n <<= 8
172
+ n += b
173
+ end
174
+ n
175
+ end
176
+
177
+ # byte strings lexicographically goedelized as numbers (one+256 coding)
178
+ def o256_encode(num)
179
+ str = empty_buffer
180
+ while num > 0
181
+ num -= 1
182
+ str << (num & 0xFF)
183
+ num >>= 8
184
+ end
185
+ str.reverse
186
+ end
187
+
188
+ def o256_decode(str)
189
+ num = 0
190
+ str.each_byte do |b|
191
+ num <<= 8
192
+ num += b + 1
193
+ end
194
+ num
195
+ end
196
+
197
+ # n must be 2**k
198
+ # returns k
199
+ def number_of_bits_up_to(n)
200
+ Math.frexp(n - 1)[1]
201
+ end
202
+
203
+ def scheme_and_authority_encode(host, port)
204
+ unless host =~ /\[.*\]/
205
+ host = "[#{host}]" if host =~ /:/
206
+ end
207
+ scheme_and_authority = "coap://#{host}"
208
+ port = Integer(port)
209
+ scheme_and_authority << ":#{port}" unless port == 5683
210
+ scheme_and_authority
211
+ end
212
+
213
+ def scheme_and_authority_decode(s)
214
+ if s =~ %r{\A(?:coap://)((?:\[|%5B)([^\]]*)(?:\]|%5D)|([^:/]*))(:(\d+))?(/.*)?\z}i
215
+ host = Regexp.last_match[2] || Regexp.last_match[3] # should check syntax...
216
+ port = Regexp.last_match[5] || 5683
217
+ [host, port.to_i, Regexp.last_match[6]]
218
+ end
219
+ end
220
+
221
+ UNRESERVED = 'A-Za-z0-9\\-\\._~' # ALPHA / DIGIT / "-" / "." / "_" / "~"
222
+ SUB_DELIM = "!$&'()*+,;=" # "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
223
+ PATH_UNENCODED = "#{UNRESERVED}#{SUB_DELIM}:@"
224
+ PATH_ENCODED_RE = /[^#{PATH_UNENCODED}]/mn
225
+ def path_encode(uri_elements)
226
+ '/' << uri_elements.map do |el|
227
+ el.dup.force_encoding(BIN).gsub(PATH_ENCODED_RE) { |x| '%%%02X' % x.ord }
228
+ end.join('/')
229
+ end
230
+
231
+ SUB_DELIM_NO_AMP = SUB_DELIM.gsub('&', '')
232
+ QUERY_UNENCODED = "#{UNRESERVED}#{SUB_DELIM_NO_AMP}:@/?"
233
+ QUERY_ENCODED_RE = /[^#{QUERY_UNENCODED}]/mn
234
+ def query_encode(query_elements)
235
+ if query_elements.empty?
236
+ ''
237
+ else
238
+ '?' << query_elements.map do |el|
239
+ el.dup.force_encoding(BIN).gsub(QUERY_ENCODED_RE) { |x| '%%%02X' % x.ord }
240
+ end.join('&')
241
+ end
242
+ end
243
+
244
+ def percent_decode(el)
245
+ el.gsub(/%(..)/) { Regexp.last_match[1].to_i(16).chr(BIN) }.force_encoding(UTF8)
246
+ end
247
+
248
+ def path_decode(path)
249
+ a = path.split('/', -1) # needs -1 to avoid eating trailing slashes!
250
+ return a if a.empty?
251
+ fail ArgumentError, 'path #{path.inspect} did not start with /' unless a[0] == ''
252
+ return [] if a[1] == '' # special case for "/"
253
+ a[1..-1].map do |el|
254
+ percent_decode(el)
255
+ end
256
+ end
257
+
258
+ def query_decode(query)
259
+ return [] if query.empty?
260
+ fail ArgumentError, 'query #{query.inspect} did not start with ?' unless query[0] == '?'
261
+ a = query.split('&', -1).map do |el|
262
+ el.gsub(/%(..)/) { Regexp.last_match[1].to_i(16).chr(BIN) }.force_encoding(UTF8)
263
+ end
264
+ a[0] = a[0][1..-1] # remove "?"
265
+ a
266
+ end
267
+
268
+ # Shortcut: CoRE::CoAP::parse == CoRE::CoAP::Message.parse
269
+ def parse(*args)
270
+ Message.parse(*args)
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,187 @@
1
+ # coapmessage.rb
2
+ # Copyright (C) 2010..2013 Carsten Bormann <cabo@tzi.org>
3
+
4
+ module CoAP
5
+ class Message < Struct.new(:ver, :tt, :mcode, :mid, :options, :payload)
6
+ def initialize(*args) # convenience: .new(tt?, mcode?, mid?, payload?, hash)
7
+ if args.size < 6
8
+ h = args.pop.dup
9
+ tt = h.delete(:tt) || args.shift
10
+ mcode = h.delete(:mcode) || args.shift
11
+ case mcode
12
+ when Integer then mcode = METHODS[mcode] || [mcode >> 5, mcode & 0x1f]
13
+ when Float then mcode = [mcode.to_i, (mcode * 100 % 100).round] # accept 2.05 and such
14
+ end
15
+ mid = h.delete(:mid) || args.shift
16
+ payload = h.delete(:payload) || args.shift || EMPTY # no payload = empty payload
17
+ fail 'CoRE::CoAPMessage.new: hash or all args' unless args.empty?
18
+ super(1, tt, mcode, mid, h, payload)
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def mcode_readable
25
+ case mcode
26
+ when Array
27
+ "#{mcode[0]}.#{"%02d" % mcode[1]}"
28
+ else
29
+ mcode.to_s
30
+ end
31
+ end
32
+
33
+ def self.deklausify(d, dpos, len)
34
+ case len
35
+ when 0..12
36
+ [len, dpos]
37
+ when 13
38
+ [d.getbyte(dpos) + 13, dpos += 1]
39
+ when 14
40
+ [d.byteslice(dpos, 2).unpack('n')[0] + 269, dpos += 2]
41
+ else
42
+ fail "[#{d.inspect}] Bad delta/length nibble #{len} at #{dpos}"
43
+ end
44
+ end
45
+
46
+ def self.parse(d)
47
+ # dpos keeps our current position in parsing d
48
+ b1, mcode, mid = d.unpack('CCn'); dpos = 4
49
+ toklen = b1 & 0xf
50
+ token = d.byteslice(dpos, toklen); dpos += toklen
51
+ b1 >>= 4
52
+ tt = TTYPES[b1 & 0x3]
53
+ b1 >>= 2
54
+ fail ArgumentError, "unknown CoAP version #{b1}" unless b1 == 1
55
+ mcode = METHODS[mcode] || [mcode >> 5, mcode & 0x1F]
56
+
57
+ # collect options
58
+ onumber = 0 # current option number
59
+ options = Hash.new { |h, k| h[k] = [] }
60
+ dlen = d.bytesize
61
+ while dpos < dlen
62
+ tl1 = d.getbyte(dpos); dpos += 1
63
+ fail ArgumentError, "option is not there at #{dpos} with oc #{orig_numopt}" unless tl1 # XXX
64
+
65
+ break if tl1 == 0xff
66
+
67
+ odelta, dpos = deklausify(d, dpos, tl1 >> 4)
68
+ olen, dpos = deklausify(d, dpos, tl1 & 0xF)
69
+
70
+ onumber += odelta
71
+
72
+ if dpos + olen > dlen
73
+ fail ArgumentError, "#{olen}-byte option at #{dpos} -- not enough data in #{dlen} total"
74
+ end
75
+
76
+ oval = d.byteslice(dpos, olen); dpos += olen
77
+ options[onumber] << oval
78
+ end
79
+
80
+ options[TOKEN_ON] = [token] if token != ''
81
+
82
+ # d.bytesize = more than all the rest...
83
+ decode_options_and_put_together(b1, tt, mcode, mid, options,
84
+ d.byteslice(dpos, d.bytesize))
85
+ end
86
+
87
+ def self.decode_options_and_put_together(b1, tt, mcode, mid, options, payload)
88
+ # check and decode option values
89
+ decoded_options = DEFAULTING_OPTIONS.dup
90
+ options.each_pair do |k, v|
91
+ if oinfo = OPTIONS[k]
92
+ oname, _, minmax, repeatable, decoder, _ = *oinfo
93
+ repeatable or v.size <= 1 or
94
+ fail ArgumentError, "repeated unrepeatable option #{oname}"
95
+ v.each do |v1|
96
+ unless minmax === v1.bytesize
97
+ fail ArgumentError, "#{v1.inspect} out of #{minmax} for #{oname}"
98
+ end
99
+ end
100
+ decoded_options[oname] = decoder.call(v)
101
+ else
102
+ decoded_options[k] = v # we don't know what that is -- keep it in raw
103
+ end
104
+ end
105
+
106
+ new(b1, tt, mcode, mid, Hash[decoded_options], payload) # XXX: why Hash[] again?
107
+ end
108
+
109
+ def prepare_options
110
+ prepared_options = {}
111
+ options.each do |k, v|
112
+ # puts "k = #{k.inspect}, oinfo_i = #{OPTIONS_I[k].inspect}"
113
+ if oinfo_i = OPTIONS_I[k]
114
+ onum, oname, defv, minmax, rep, _, encoder = *oinfo_i
115
+ prepared_options[onum] = a = encoder.call(v)
116
+ rep or a.size <= 1 or fail "repeated option #{oname} #{a.inspect}"
117
+ a.each do |v1|
118
+ unless minmax === v1.bytesize
119
+ fail ArgumentError, "#{v1.inspect} out of #{minmax} for #{oname}"
120
+ end
121
+ end
122
+ else
123
+ fail ArgumentError, "#{k.inspect}: unknown option" unless Integer === k
124
+ prepared_options[k] = Array(v) # store raw option
125
+ end
126
+ end
127
+ prepared_options
128
+ end
129
+
130
+ def klausify(n)
131
+ if n < 13
132
+ [n, '']
133
+ else
134
+ n -= 13
135
+ if n < 256
136
+ [13, [n].pack('C')]
137
+ else
138
+ [14, [n - 256].pack('n')]
139
+ end
140
+ end
141
+ end
142
+
143
+ def to_wire
144
+ # check and encode option values
145
+ prepared_options = prepare_options
146
+ # puts "prepared_options: #{prepared_options}"
147
+
148
+ token = (prepared_options.delete(TOKEN_ON) || [nil])[0] || ''
149
+ # puts "TOKEN: #{token.inspect}" unless token
150
+
151
+ b1 = 0x40 | TTYPES_I[tt] << 4 | token.bytesize
152
+ b2 = METHODS_I[mcode] || (mcode[0] << 5) + mcode[1]
153
+ result = [b1, b2, mid].pack('CCn')
154
+ result << token
155
+
156
+ # stuff options in packet
157
+ onumber = 0
158
+ num_encoded_options = 0
159
+
160
+ prepared_options.keys.sort.each do |k|
161
+ fail "Bad Option Type #{k.inspect}" unless Integer === k && k >= 0
162
+ a = prepared_options[k]
163
+ a.each do |v|
164
+ # result << frob(k, v)
165
+ odelta = k - onumber
166
+ onumber = k
167
+
168
+ odelta1, odelta2 = klausify(odelta)
169
+ odelta1 <<= 4
170
+
171
+ length1, length2 = klausify(v.bytesize)
172
+ result << [odelta1 | length1].pack('C')
173
+ result << odelta2
174
+ result << length2
175
+ result << v.dup.force_encoding(BIN) # value
176
+ end
177
+
178
+ end
179
+
180
+ if payload != ''
181
+ result << 0xFF
182
+ result << payload.dup.force_encoding(BIN)
183
+ end
184
+ result
185
+ end
186
+ end
187
+ end