coap 0.0.13

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