coap 0.0.16 → 0.1.0
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 +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +13 -2
- data/Gemfile +0 -1
- data/LICENSE +2 -2
- data/README.md +37 -33
- data/Rakefile +12 -3
- data/bin/coap +111 -0
- data/coap.gemspec +34 -29
- data/lib/coap.rb +3 -34
- data/lib/core.rb +11 -0
- data/lib/core/coap.rb +42 -0
- data/lib/core/coap/block.rb +98 -0
- data/lib/core/coap/client.rb +314 -0
- data/lib/core/coap/coap.rb +26 -0
- data/lib/core/coap/coding.rb +146 -0
- data/lib/core/coap/fsm.rb +82 -0
- data/lib/core/coap/message.rb +203 -0
- data/lib/core/coap/observer.rb +40 -0
- data/lib/core/coap/options.rb +44 -0
- data/lib/core/coap/registry.rb +32 -0
- data/lib/core/coap/registry/content_formats.yml +7 -0
- data/lib/core/coap/resolver.rb +17 -0
- data/lib/core/coap/transmission.rb +165 -0
- data/lib/core/coap/types.rb +69 -0
- data/lib/core/coap/utility.rb +34 -0
- data/lib/core/coap/version.rb +5 -0
- data/lib/core/core_ext/socket.rb +19 -0
- data/lib/core/hexdump.rb +18 -0
- data/lib/core/link.rb +97 -0
- data/lib/core/os.rb +15 -0
- data/spec/block_spec.rb +160 -0
- data/spec/client_spec.rb +86 -0
- data/spec/fixtures/coap.me.link +1 -0
- data/spec/link_spec.rb +98 -0
- data/spec/registry_spec.rb +39 -0
- data/spec/resolver_spec.rb +19 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/transmission_spec.rb +70 -0
- data/test/helper.rb +15 -0
- data/test/test_client.rb +99 -228
- data/test/test_message.rb +99 -71
- metadata +140 -37
- data/bin/client +0 -42
- data/lib/coap/block.rb +0 -45
- data/lib/coap/client.rb +0 -364
- data/lib/coap/coap.rb +0 -273
- data/lib/coap/message.rb +0 -187
- data/lib/coap/mysocket.rb +0 -81
- data/lib/coap/observer.rb +0 -41
- data/lib/coap/version.rb +0 -3
- data/lib/misc/hexdump.rb +0 -17
- data/test/coap_test_helper.rb +0 -2
- 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,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
         |