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,82 @@
1
+ module CoRE
2
+ module CoAP
3
+ class FSM
4
+ # CoAP Message Layer FSM
5
+ # https://tools.ietf.org/html/draft-ietf-lwig-coap-01#section-2.5.2
6
+ class Message
7
+ include Celluloid::FSM
8
+
9
+ default_state :closed
10
+
11
+ # Receive CON
12
+ # Send ACK (accept) -> :closed
13
+ state :ack_pending, to: [:closed]
14
+
15
+ # Sending and receiving
16
+ # Send NON (unreliable_send)
17
+ # Receive NON
18
+ # Receive ACK
19
+ # Receive CON -> :ack_pending
20
+ # Send CON (reliable_send) -> :reliable_tx
21
+ state :closed, to: [:reliable_tx, :ack_pending]
22
+
23
+ # Send CON
24
+ # Retransmit until
25
+ # Failure
26
+ # Timeout (fail) -> :closed
27
+ # Receive matching RST (fail) -> :closed
28
+ # Cancel (cancel) -> :closed
29
+ # Success
30
+ # Receive matching ACK, NON (rx) -> :closed
31
+ # Receive matching CON (rx) -> :ack_pending
32
+ state :reliable_tx, to: [:closed, :ack_pending]
33
+ end
34
+
35
+ # CoAP Client Request/Response Layer FSM
36
+ # https://tools.ietf.org/html/draft-ietf-lwig-coap-01#section-2.5.1
37
+ class Client
38
+ include Celluloid::FSM
39
+
40
+ default_state :idle
41
+
42
+ # Idle
43
+ # Outgoing request ((un)reliable_send) -> :waiting
44
+ state :idle, to: [:waiting]
45
+
46
+ # Waiting for response
47
+ # Response received (accept, rx) -> :idle
48
+ # Failure (fail) -> :idle
49
+ # Cancel (cancel) -> :idle
50
+ state :waiting, to: [:idle]
51
+ end
52
+
53
+ # CoAP Server Request/Response Layer FSM
54
+ # https://tools.ietf.org/html/draft-ietf-lwig-coap-01#section-2.5.1
55
+ class Server
56
+ include Celluloid::FSM
57
+
58
+ default_state :idle
59
+
60
+ # Idle
61
+ # On NON (rx) -> :separate
62
+ # On CON (rx) -> :serving
63
+ state :idle, to: [:separate, :serving]
64
+
65
+ # Separate
66
+ # Respond ((un)reliable_send) -> :idle
67
+ state :separate, to: [:idle]
68
+
69
+ # Serving
70
+ # Respond (accept) -> :idle
71
+ # Empty ACK (accept) -> :separate
72
+ state :serving, to: [:idle, :separate]
73
+ end
74
+
75
+ attr_reader :fsm
76
+
77
+ def initialize
78
+ @fsm = FSM::Message.new
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,203 @@
1
+ # coapmessage.rb
2
+ # Copyright (C) 2010..2013 Carsten Bormann <cabo@tzi.org>
3
+
4
+ module CoRE
5
+ module CoAP
6
+ class Message < Struct.new(:ver, :tt, :mcode, :mid, :options, :payload)
7
+ def initialize(*args) # convenience: .new(tt?, mcode?, mid?, payload?, hash?)
8
+ if args.size < 6
9
+ h = {}
10
+ h = args.pop.dup if args.last.is_a? Hash
11
+
12
+ tt = h.delete(:tt) || args.shift
13
+
14
+ mcode = h.delete(:mcode) || args.shift
15
+
16
+ # TODO Allow later setting of mcode by Float.
17
+ case mcode
18
+ when Integer
19
+ mcode = METHODS[mcode] || [mcode >> 5, mcode & 0x1f]
20
+ when Float
21
+ mcode = [mcode.to_i, (mcode * 100 % 100).round] # Accept 2.05 and such
22
+ end
23
+
24
+ mid = h.delete(:mid) || args.shift
25
+
26
+ payload = h.delete(:payload) || args.shift || EMPTY
27
+
28
+ unless args.empty?
29
+ raise 'CoRE::CoAP::Message.new: Either specify Hash or all arguments.'
30
+ end
31
+
32
+ super(1, tt, mcode, mid, h, payload)
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ def klausify(n)
39
+ if n < 13
40
+ [n, '']
41
+ else
42
+ n -= 13
43
+ if n < 256
44
+ [13, [n].pack("C")]
45
+ else
46
+ [14, [n-256].pack("n")]
47
+ end
48
+ end
49
+ end
50
+
51
+ def mcode_readable
52
+ return "#{mcode[0]}.#{"%02d" % mcode[1]}" if mcode.is_a? Array
53
+ mcode.to_s
54
+ end
55
+
56
+ def prepare_options
57
+ prepared_options = {}
58
+ options.each do |k, v|
59
+ if oinfo_i = CoAP::OPTIONS_I[k]
60
+ onum, oname, defv, minmax, rep, _, encoder = *oinfo_i
61
+ prepared_options[onum] = a = encoder.call(v)
62
+ rep or a.size <= 1 or raise "repeated option #{oname} #{a.inspect}"
63
+ a.each do |v1|
64
+ unless minmax === v1.bytesize
65
+ raise ArgumentError, "#{v1.inspect} out of #{minmax} for #{oname}"
66
+ end
67
+ end
68
+ else
69
+ raise ArgumentError, "#{k.inspect}: unknown option" unless Integer === k
70
+ prepared_options[k] = Array(v) # store raw option
71
+ end
72
+ end
73
+ prepared_options
74
+ end
75
+
76
+ def to_s
77
+ path = CoAP.path_encode(self.options[:uri_path])
78
+ query = CoAP.query_encode(self.options[:uri_query])
79
+
80
+ [mcode_readable, path, query].join(' ')
81
+ end
82
+
83
+ def to_wire
84
+ # check and encode option values
85
+ prepared_options = prepare_options
86
+ # puts "prepared_options: #{prepared_options}"
87
+
88
+ token = (prepared_options.delete(CoAP::TOKEN_ON) || [nil])[0] || ''
89
+ puts "TOKEN: #{token.inspect}" unless token
90
+
91
+ b1 = 0x40 | TTYPES_I[tt] << 4 | token.bytesize
92
+ b2 = METHODS_I[mcode] || (mcode[0] << 5) + mcode[1]
93
+ result = [b1, b2, mid].pack("CCn")
94
+ result << token
95
+
96
+ # stuff options in packet
97
+ onumber = 0
98
+ num_encoded_options = 0
99
+ prepared_options.keys.sort.each do |k|
100
+ raise "Bad Option Type #{k.inspect}" unless Integer === k && k >= 0
101
+ a = prepared_options[k]
102
+ a.each do |v|
103
+ # result << frob(k, v)
104
+ odelta = k - onumber
105
+ onumber = k
106
+
107
+ odelta1, odelta2 = klausify(odelta)
108
+ odelta1 <<= 4
109
+
110
+ length1, length2 = klausify(v.bytesize)
111
+ result << [odelta1 | length1].pack("C")
112
+ result << odelta2
113
+ result << length2
114
+ result << v.dup.force_encoding(CoAP::BIN) # value
115
+ end
116
+ end
117
+
118
+ if payload != '' && !payload.nil?
119
+ result << 0xFF
120
+ result << payload.dup.force_encoding(CoAP::BIN)
121
+ end
122
+
123
+ result
124
+ end
125
+
126
+ def self.decode_options_and_put_together(b1, tt, mcode, mid, options, payload)
127
+ # check and decode option values
128
+ decoded_options = CoAP::DEFAULTING_OPTIONS.dup
129
+ options.each_pair do |k, v|
130
+ if oinfo = CoAP::OPTIONS[k]
131
+ oname, _, minmax, repeatable, decoder, _ = *oinfo
132
+ repeatable or v.size <= 1 or
133
+ raise ArgumentError, "repeated unrepeatable option #{oname}"
134
+ v.each do |v1|
135
+ unless minmax === v1.bytesize
136
+ raise ArgumentError, "#{v1.inspect} out of #{minmax} for #{oname}"
137
+ end
138
+ end
139
+ decoded_options[oname] = decoder.call(v)
140
+ else
141
+ decoded_options[k] = v # we don't know what that is -- keep it in raw
142
+ end
143
+ end
144
+
145
+ new(b1, tt, mcode, mid, Hash[decoded_options], payload) # XXX: why Hash[] again?
146
+ end
147
+
148
+ def self.deklausify(d, dpos, len)
149
+ case len
150
+ when 0..12
151
+ [len, dpos]
152
+ when 13
153
+ [d.getbyte(dpos) + 13, dpos += 1]
154
+ when 14
155
+ [d.byteslice(dpos, 2).unpack("n")[0] + 269, dpos += 2]
156
+ else
157
+ raise "[#{d.inspect}] Bad delta/length nibble #{len} at #{dpos}"
158
+ end
159
+ end
160
+
161
+ def self.parse(d)
162
+ # dpos keeps our current position in parsing d
163
+ b1, mcode, mid = d.unpack("CCn"); dpos = 4
164
+ toklen = b1 & 0xf
165
+ token = d.byteslice(dpos, toklen); dpos += toklen
166
+ b1 >>= 4
167
+ tt = TTYPES[b1 & 0x3]
168
+ b1 >>= 2
169
+ raise ArgumentError, "unknown CoAP version #{b1}" unless b1 == 1
170
+ mcode = METHODS[mcode] || [mcode>>5, mcode&0x1F]
171
+
172
+ # collect options
173
+ onumber = 0 # current option number
174
+ options = Hash.new { |h, k| h[k] = [] }
175
+ dlen = d.bytesize
176
+ while dpos < dlen
177
+ tl1 = d.getbyte(dpos); dpos += 1
178
+ raise ArgumentError, "option is not there at #{dpos} with oc #{orig_numopt}" unless tl1 # XXX
179
+
180
+ break if tl1 == 0xff
181
+
182
+ odelta, dpos = deklausify(d, dpos, tl1 >> 4)
183
+ olen, dpos = deklausify(d, dpos, tl1 & 0xF)
184
+
185
+ onumber += odelta
186
+
187
+ if dpos + olen > dlen
188
+ raise ArgumentError, "#{olen}-byte option at #{dpos} -- not enough data in #{dlen} total"
189
+ end
190
+
191
+ oval = d.byteslice(dpos, olen); dpos += olen
192
+ options[onumber] << oval
193
+ end
194
+
195
+ options[CoAP::TOKEN_ON] = [token] if token != ''
196
+
197
+ # d.bytesize = more than all the rest...
198
+ decode_options_and_put_together(b1, tt, mcode, mid, options,
199
+ d.byteslice(dpos, d.bytesize))
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,40 @@
1
+ module CoRE
2
+ module CoAP
3
+ class Observer
4
+ MAX_OBSERVE_OPTION_VALUE = 8_388_608
5
+
6
+ def initialize
7
+ @logger = CoAP.logger
8
+ end
9
+
10
+ def observe(message, callback, socket)
11
+ n = message.options[:observe]
12
+
13
+ callback.call(socket, message)
14
+
15
+ # This does not seem to be able to cope with concurrency.
16
+ loop do
17
+ answer = socket.receive(timeout: 0)
18
+
19
+ next unless answer.options[:observe]
20
+
21
+ if update?(n, answer.options[:observe])
22
+ callback.call(socket, answer)
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def update?(old, new)
30
+ if new > old
31
+ new - old < MAX_OBSERVE_OPTION_VALUE
32
+ elsif new < old
33
+ old - new > MAX_OBSERVE_OPTION_VALUE
34
+ else
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ module CoRE
2
+ module CoAP
3
+ module Options
4
+ extend Types
5
+
6
+ TOKEN_ON = 19
7
+
8
+ # 14 => :user, default, length range, replicable?, decoder, encoder
9
+ OPTIONS = { # name minlength, maxlength, [default] defined where:
10
+ 1 => [:if_match, *o256_many(0, 8)], # core-coap-12
11
+ 3 => [:uri_host, *str_once(1, 255)], # core-coap-12
12
+ 4 => [:etag, *o256_many(1, 8)], # core-coap-12 !! once in rp
13
+ 5 => [:if_none_match, *presence_once], # core-coap-12
14
+ 6 => [:observe, *uint_once(0, 3)], # core-observe-07
15
+ 7 => [:uri_port, *uint_once(0, 2)], # core-coap-12
16
+ 8 => [:location_path, *str_many(0, 255)], # core-coap-12
17
+ 11 => [:uri_path, *str_many(0, 255)], # core-coap-12
18
+ 12 => [:content_format, *uint_once(0, 2)], # core-coap-12
19
+ 14 => [:max_age, *uint_once(0, 4, 60)], # core-coap-12
20
+ 15 => [:uri_query, *str_many(0, 255)], # core-coap-12
21
+ 17 => [:accept, *uint_once(0, 2)], # core-coap-18!
22
+ TOKEN_ON => [:token, *o256_once(1, 8, 0)], # core-coap-12 -> opaq_once(1, 8, EMPTY)
23
+ 20 => [:location_query, *str_many(0, 255)], # core-coap-12
24
+ 23 => [:block2, *uint_once(0, 3)], # core-block-10
25
+ 27 => [:block1, *uint_once(0, 3)], # core-block-10
26
+ 28 => [:size2, *uint_once(0, 4)], # core-block-10
27
+ 35 => [:proxy_uri, *str_once(1, 1034)], # core-coap-12
28
+ 39 => [:proxy_scheme, *str_once(1, 255)], # core-coap-13
29
+ 60 => [:size1, *uint_once(0, 4)], # core-block-10
30
+ }
31
+
32
+ # :user => 14, :user, def, range, rep, deco, enco
33
+ OPTIONS_I =
34
+ Hash[OPTIONS.map { |k, v| [v[0], [k, *v]] }]
35
+
36
+ DEFAULTING_OPTIONS =
37
+ Hash[
38
+ OPTIONS
39
+ .map { |k, v| [v[0].freeze, v[1].freeze] }
40
+ .select { |k, v| v }
41
+ ].freeze
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ module CoRE
2
+ module CoAP
3
+ module Registry
4
+ REGISTRY_PATH = File.join(File.dirname(__FILE__), 'registry').freeze
5
+
6
+ def self.load_yaml(registry)
7
+ registry = "#{registry}.yml"
8
+ YAML.load_file(File.join(REGISTRY_PATH, registry))
9
+ end
10
+
11
+ CONTENT_FORMATS = load_yaml(:content_formats).freeze
12
+ CONTENT_FORMATS_INVERTED = CONTENT_FORMATS.invert.freeze
13
+
14
+ def self.convert_content_format(cf)
15
+ if cf.is_a? String
16
+ CONTENT_FORMATS_INVERTED[cf] || without_charset(cf)
17
+ else
18
+ CONTENT_FORMATS[cf]
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def self.without_charset(cf)
25
+ cf = cf.split(';').first
26
+ CONTENT_FORMATS_INVERTED.select { |k| k =~ /^#{cf}/ }.first[1]
27
+ rescue NoMethodError
28
+ nil
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ 0: 'text/plain; charset=utf-8'
2
+ 40: 'application/link-format'
3
+ 41: 'application/xml'
4
+ 42: 'application/octet-stream'
5
+ 47: 'application/exi'
6
+ 50: 'application/json'
7
+ 60: 'application/cbor'
@@ -0,0 +1,17 @@
1
+ module CoRE
2
+ module CoAP
3
+ class Resolver
4
+ def self.address(host)
5
+ a = if ENV['IPv4'].nil?
6
+ IPv6FavorResolv.getaddress(host).to_s
7
+ else
8
+ Resolv.getaddress(host).to_s
9
+ end
10
+
11
+ raise Resolv::ResolvError if a.empty?
12
+
13
+ a
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,165 @@
1
+ module CoRE
2
+ module CoAP
3
+ # Socket abstraction.
4
+ class Transmission
5
+ DEFAULT_RECV_TIMEOUT = 2
6
+
7
+ attr_accessor :max_retransmit, :recv_timeout
8
+ attr_reader :address_family, :socket
9
+
10
+ def initialize(options = {})
11
+ @max_retransmit = options[:max_retransmit] || 4
12
+ @recv_timeout = options[:recv_timeout] || DEFAULT_RECV_TIMEOUT
13
+ @socket = options[:socket]
14
+
15
+ @retransmit = if options[:retransmit].nil?
16
+ true
17
+ else
18
+ !!options[:retransmit]
19
+ end
20
+
21
+ if @socket
22
+ @socket_class = @socket.class
23
+ @address_family = @socket.addr.first
24
+ else
25
+ @socket_class = options[:socket_class] || Celluloid::IO::UDPSocket
26
+ @address_family = options[:address_family] || Socket::AF_INET6
27
+ @socket = @socket_class.new(@address_family)
28
+ end
29
+
30
+ # http://lists.apple.com/archives/darwin-kernel/2014/Mar/msg00012.html
31
+ if OS.osx? && ipv6?
32
+ ifname = Socket.if_up?('en1') ? 'en1' : 'en0'
33
+ ifindex = Socket.if_nametoindex(ifname)
34
+
35
+ s = @socket.to_io rescue @socket
36
+ s.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_IF, [ifindex].pack('i_'))
37
+ end
38
+
39
+ @socket
40
+ end
41
+
42
+ def ipv6?
43
+ @address_family == Socket::AF_INET6
44
+ end
45
+
46
+ # Receive from socket and return parsed CoAP message. (ACK is sent on CON
47
+ # messages.)
48
+ def receive(options = {})
49
+ retry_count = options[:retry_count] || 0
50
+ timeout = (options[:timeout] || @recv_timeout) ** (retry_count + 1)
51
+
52
+ mid = options[:mid]
53
+ flags = mid.nil? ? 0 : Socket::MSG_PEEK
54
+
55
+ data = Timeout.timeout(timeout) do
56
+ @socket.recvfrom(1152, flags)
57
+ end
58
+
59
+ answer = CoAP.parse(data[0].force_encoding('BINARY'))
60
+
61
+ if mid == answer.mid
62
+ Timeout.timeout(1) { @socket.recvfrom(1152) }
63
+ end
64
+
65
+ return if answer.nil?
66
+
67
+ if answer.tt == :con
68
+ message = Message.new(:ack, 0, answer.mid, nil,
69
+ {token: answer.options[:token]})
70
+
71
+ send(message, data[1][3])
72
+ end
73
+
74
+ answer
75
+ end
76
+
77
+ # Send +message+ (retransmit if necessary) and wait for answer. Returns
78
+ # answer.
79
+ def request(message, host, port = CoAP::PORT)
80
+ retry_count = 0
81
+ retransmit = @retransmit && message.tt == :con
82
+
83
+ begin
84
+ send(message, host, port)
85
+ response = receive(retry_count: retry_count, mid: message.mid)
86
+ rescue Timeout::Error
87
+ raise unless retransmit
88
+
89
+ retry_count += 1
90
+
91
+ if retry_count > @max_retransmit
92
+ raise "Maximum retransmission count of #{@max_retransmit} reached."
93
+ end
94
+
95
+ retry unless message.tt == :non
96
+ end
97
+
98
+ if seperate?(response)
99
+ response = receive(timeout: 10, mid: message.mid)
100
+ end
101
+
102
+ response
103
+ end
104
+
105
+ # Send +message+.
106
+ def send(message, host, port = CoAP::PORT)
107
+ message = message.to_wire if message.respond_to?(:to_wire)
108
+
109
+ # In MRI and Rubinius, the Socket::MSG_DONTWAIT option is 64.
110
+ # It is not defined by JRuby.
111
+ # TODO Is it really necessary?
112
+ @socket.send(message, 64, host, port)
113
+ end
114
+
115
+ private
116
+
117
+ # Check if answer is seperated.
118
+ def seperate?(response)
119
+ r = response
120
+ r.tt == :ack && r.payload.empty? && r.mcode == [0, 0]
121
+ end
122
+
123
+ class << self
124
+ # Return Transmission instance with socket matching address family.
125
+ def from_host(host, options = {})
126
+ if IPAddr.new(host).ipv6?
127
+ new(options)
128
+ else
129
+ new(options.merge(address_family: Socket::AF_INET))
130
+ end
131
+ # MRI throws IPAddr::InvalidAddressError, JRuby an ArgumentError
132
+ rescue ArgumentError
133
+ host = Resolver.address(host)
134
+ retry
135
+ end
136
+
137
+ # Instanciate matching Transmission and send message.
138
+ def send(*args)
139
+ invoke(:send, *args)
140
+ end
141
+
142
+ # Instanciate matching Transmission and perform request.
143
+ def request(*args)
144
+ invoke(:request, *args)
145
+ end
146
+
147
+ private
148
+
149
+ # Instanciate matching Transmission and invoke +method+ on instance.
150
+ def invoke(method, *args)
151
+ options = {}
152
+ options = args.pop if args.last.is_a? Hash
153
+
154
+ if options[:socket]
155
+ transmission = Transmission.new(options)
156
+ else
157
+ transmission = from_host(args[1], options)
158
+ end
159
+
160
+ [transmission, transmission.__send__(method, *args)]
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end