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.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +19 -0
- data/LICENSE +21 -0
- data/README.md +64 -0
- data/Rakefile +9 -0
- data/bin/client +42 -0
- data/coap.gemspec +25 -0
- data/lib/coap.rb +34 -0
- data/lib/coap/block.rb +45 -0
- data/lib/coap/client.rb +364 -0
- data/lib/coap/coap.rb +273 -0
- data/lib/coap/message.rb +187 -0
- data/lib/coap/mysocket.rb +81 -0
- data/lib/coap/observer.rb +41 -0
- data/lib/coap/version.rb +3 -0
- data/lib/misc/hexdump.rb +17 -0
- data/test/coap_test_helper.rb +2 -0
- data/test/disabled_econotag_blck.rb +33 -0
- data/test/test_client.rb +311 -0
- data/test/test_message.rb +127 -0
- metadata +113 -0
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
|
data/lib/coap/message.rb
ADDED
@@ -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
|