diff --git a/Gemfile b/Gemfile index 1470d37..66cc431 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,10 @@ end gemspec +group :dtls do + gem 'tinydtls', platforms: :ruby +end + group :cbor do gem 'cbor', platforms: :ruby end diff --git a/Gemfile.lock b/Gemfile.lock index 2ea5611..ca8a2b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -107,6 +107,7 @@ GEM docile (1.1.5) equalizer (0.0.11) erubi (1.7.1) + ffi (1.9.25) globalid (0.4.1) activesupport (>= 4.2.0) grape (1.0.3) @@ -220,6 +221,8 @@ GEM timers (4.1.2) hitimes tins (1.16.3) + tinydtls (0.2.0) + ffi (~> 1.9) tzinfo (1.2.5) thread_safe (~> 0.1) virtus (1.0.5) @@ -252,6 +255,7 @@ DEPENDENCIES rspec-rails (~> 3.5.0) ruby-prof sinatra! + tinydtls BUNDLED WITH 1.16.2 diff --git a/lib/david.rb b/lib/david.rb index 4a18869..6927c71 100644 --- a/lib/david.rb +++ b/lib/david.rb @@ -11,6 +11,12 @@ module David $stderr << "`gem install cbor` for transparent JSON/CBOR conversion " $stderr << "support.\n" end + + begin + require 'tinydtls' + rescue LoadError + $stderr << "`gem install tinydtls` for DTLs support.\n" + end end require 'celluloid/current' diff --git a/lib/david/app_config.rb b/lib/david/app_config.rb index 18b8120..b773cc9 100644 --- a/lib/david/app_config.rb +++ b/lib/david/app_config.rb @@ -3,6 +3,7 @@ class AppConfig < Hash DEFAULT_OPTIONS = { :Block => true, :CBOR => false, + :DTLS => false, :DefaultFormat => 'application/json', :Host => ENV['RACK_ENV'] == 'development' ? '::1' : '::', :Log => nil, @@ -10,7 +11,8 @@ class AppConfig < Hash :Multicast => true, :MulticastGroups => ['ff02::fd', 'ff05::fd'], :Observe => true, - :Port => ::CoAP::PORT + :Port => ::CoAP::PORT, + :PortDTLS => 5684 # TODO } def initialize(hash = {}) @@ -32,6 +34,10 @@ def choose_cbor(value) default_to_false(:cbor, value) end + def choose_dtls(value) + default_to_false(:dtls, value) + end + def choose_defaultformat(value) value = from_rails(:default_format) return nil if value.nil? @@ -82,6 +88,10 @@ def choose_port(value) value.nil? ? nil : value.to_i end + def choose_portdtls(value) + value.nil? ? nil: value.to_i + end + def default_to_false(key, value) return true if value.to_s == 'true' diff --git a/lib/david/exchange.rb b/lib/david/exchange.rb index 01df46f..1f759b8 100644 --- a/lib/david/exchange.rb +++ b/lib/david/exchange.rb @@ -1,5 +1,5 @@ module David - class Exchange < Struct.new(:host, :port, :message, :ancillary, :options) + class Exchange < Struct.new(:server, :host, :port, :message, :ancillary, :options) include Registry def accept diff --git a/lib/david/garbage_collector.rb b/lib/david/garbage_collector.rb index 00ea95f..bdde064 100644 --- a/lib/david/garbage_collector.rb +++ b/lib/david/garbage_collector.rb @@ -18,9 +18,11 @@ def run end def tick - unless server.cache.empty? - log.debug('GarbageCollector tick') - server.cache_clean!(@timeout) + servers.each do |server| + unless server.cache.empty? + log.debug("GarbageCollector tick for #{server.class.name}") + server.cache_clean!(@timeout) + end end end end diff --git a/lib/david/observe.rb b/lib/david/observe.rb index feb93cd..03080e6 100644 --- a/lib/david/observe.rb +++ b/lib/david/observe.rb @@ -52,7 +52,7 @@ def handle_update(key) n, exchange, env, etag = @store[key] n += 1 - response, options = server.respond(exchange, env) + response, options = exchange.server.respond(exchange, env) return if response.nil? @@ -82,7 +82,7 @@ def handle_update(key) def transmit(exchange, message, options) begin - server.socket.send(message.to_wire, 0, exchange.host, exchange.port) + exchange.server.socket.send(message.to_wire, 0, exchange.host, exchange.port) log.debug(message.inspect) rescue Timeout::Error, RuntimeError, Errno::ENETUNREACH end diff --git a/lib/david/registry.rb b/lib/david/registry.rb index f138e9e..cf5bfc6 100644 --- a/lib/david/registry.rb +++ b/lib/david/registry.rb @@ -5,7 +5,7 @@ module Registry protected def log - @log ||= server.log + @log ||= Celluloid.logger # In some tests no server actor is present rescue NoMethodError @log ||= FakeLogger.new @@ -21,8 +21,19 @@ def observe Celluloid::Actor[:observe] end - def server - Celluloid::Actor[:server] + def servers + server_udp = Celluloid::Actor[:server_udp] + server_dtls = Celluloid::Actor[:server_dtls] + + servers = [] + [:server_udp, :server_dtls].each do |key| + server = Celluloid::Actor[key] + unless server.nil? + servers << server + end + end + + servers end end end diff --git a/lib/david/server.rb b/lib/david/server.rb index 046d1ed..ba01be6 100644 --- a/lib/david/server.rb +++ b/lib/david/server.rb @@ -4,6 +4,8 @@ require 'david/server/multicast' require 'david/server/respond' require 'david/server/utility' +require 'david/server/coap' +require 'david/server/coaps' module David class Server @@ -14,7 +16,7 @@ class Server include Respond include Utility - attr_reader :log, :socket + attr_reader :socket finalizer :shutdown @@ -22,17 +24,15 @@ def initialize(app, options) @app = app.respond_to?(:new) ? app.new : app @mid_cache = {} @options = AppConfig.new(options) - @log = @options[:Log] - host, port = @options.values_at(:Host, :Port) + host, port = @options.values_at(:Host, port_key) log.info "David #{David::VERSION} on #{RUBY_DESCRIPTION}" - log.info "Starting on coap://[#{host}]:#{port}" + log.info "Starting on #{protocol_scheme}://[#{host}]:#{port}" af = ipv6? ? ::Socket::AF_INET6 : ::Socket::AF_INET - # Actually Celluloid::IO::UDPSocket. - @socket = UDPSocket.new(af) + @socket = create_socket(af) multicast_initialize! if @options[:Multicast] @socket.bind(host, port) end @@ -41,6 +41,8 @@ def run loop do if jruby_or_rbx? dispatch(*@socket.recvfrom(1152)) + elsif dtls? + defer { dispatch(*@socket.recvfrom) } else begin dispatch(*@socket.to_io.recvmsg_nonblock) @@ -68,14 +70,14 @@ def answer(exchange, key = nil) def dispatch(*args) data, sender, _, anc = args - if jruby_or_rbx? + if jruby_or_rbx? or dtls? port, _, host = sender[1..3] else host, port = sender.ip_address, sender.ip_port end - message = CoAP::Message.parse(data) - exchange = Exchange.new(host, port, message, anc) + message = ::CoAP::Message.parse(data) + exchange = Exchange.new(self, host, port, message, anc) return if !exchange.non? && exchange.multicast? diff --git a/lib/david/server/coap.rb b/lib/david/server/coap.rb new file mode 100644 index 0000000..0982bd5 --- /dev/null +++ b/lib/david/server/coap.rb @@ -0,0 +1,17 @@ +module David + class Server + class CoAP < Server + def create_socket(af) + Celluloid::IO::UDPSocket.new(af) + end + + def port_key + :Port + end + + def protocol_scheme + "coap" + end + end + end +end diff --git a/lib/david/server/coaps.rb b/lib/david/server/coaps.rb new file mode 100644 index 0000000..f47d54d --- /dev/null +++ b/lib/david/server/coaps.rb @@ -0,0 +1,20 @@ +module David + class Server + class CoAPs < Server + def create_socket(af) + socket = TinyDTLS::UDPSocket.new(af) + socket.add_client("foobar", "foobar") + + return socket + end + + def port_key + :PortDTLS + end + + def protocol_scheme + "coaps" + end + end + end +end diff --git a/lib/david/server/mapping.rb b/lib/david/server/mapping.rb index 0b539e0..c945aa8 100644 --- a/lib/david/server/mapping.rb +++ b/lib/david/server/mapping.rb @@ -30,7 +30,7 @@ def accept_to_http(request) if request.accept.nil? @options[:DefaultFormat] else - CoAP::Registry.convert_content_format(request.accept) + ::CoAP::Registry.convert_content_format(request.accept) end end diff --git a/lib/david/server/respond.rb b/lib/david/server/respond.rb index a52ac41..11654dc 100644 --- a/lib/david/server/respond.rb +++ b/lib/david/server/respond.rb @@ -60,7 +60,7 @@ def respond(exchange, env = nil) # No response on exchange for non-existent block. return if block_enabled && !exchange.block.included_by?(body) - cf = CoAP::Registry.convert_content_format(ct) + cf = ::CoAP::Registry.convert_content_format(ct) etag = etag_to_coap(headers, 4) loc = location_to_coap(headers) ma = max_age_to_coap(headers) @@ -145,7 +145,7 @@ def handle_observe(exchange, env, etag) def initialize_response(exchange, mcode = 2.05) type = exchange.con? ? :ack : :non - CoAP::Message.new \ + ::CoAP::Message.new \ tt: type, mcode: mcode, mid: exchange.message.mid || SecureRandom.random_number(0xffff), diff --git a/lib/david/server/utility.rb b/lib/david/server/utility.rb index fe302f5..98d1f21 100644 --- a/lib/david/server/utility.rb +++ b/lib/david/server/utility.rb @@ -16,6 +16,11 @@ def ipv6? @ipv6 ||= IPAddr.new(@options[:Host]).ipv6? end + def dtls? + @dtls ||= defined? TinyDTLS::UDPSocket and + @socket.is_a? TinyDTLS::UDPSocket + end + def jruby_or_rbx? @jruby_or_rbx ||= !!(defined?(JRuby) || defined?(Rubinius)) end diff --git a/lib/david/transmitter.rb b/lib/david/transmitter.rb index 8450c2e..a8e7ab6 100644 --- a/lib/david/transmitter.rb +++ b/lib/david/transmitter.rb @@ -6,7 +6,7 @@ class Transmitter def initialize(socket) @log = Celluloid.logger - @socket = socket || server.socket + @socket = socket end # TODO Retransmissions diff --git a/lib/rack/handler/david.rb b/lib/rack/handler/david.rb index 592b1a2..51f68cb 100644 --- a/lib/rack/handler/david.rb +++ b/lib/rack/handler/david.rb @@ -4,15 +4,22 @@ class David def self.run(app, options={}) g = Celluloid::Supervision::Container.run! - g.supervise(as: :server, type: ::David::Server, args: [app, options]) - g.supervise(as: :gc, type: ::David::GarbageCollector) + g.supervise(as: :server_udp, type: ::David::Server::CoAP, args: [app, options]) + if options[:DTLS] == 'true' + g.supervise(as: :server_dtls, type: ::David::Server::CoAPs, args: [app, options]) + end + g.supervise(as: :gc, type: ::David::GarbageCollector) if options[:Observe] != 'false' g.supervise(as: :observe, type: ::David::Observe) end begin - Celluloid::Actor[:server].run + if options[:DTLS] == 'true' + Celluloid::Actor[:server_dtls].async.run + end + + Celluloid::Actor[:server_udp].run rescue Interrupt Celluloid.logger.info 'Terminated' Celluloid.logger = nil @@ -21,19 +28,21 @@ def self.run(app, options={}) end def self.valid_options - host, port, maddrs = - AppConfig::DEFAULT_OPTIONS.values_at(:Host, :Port, :MulticastGroups) + host, port, dport, maddrs = + AppConfig::DEFAULT_OPTIONS.values_at(:Host, :Port, :PortDTLS, :MulticastGroups) { 'Block=BOOLEAN' => 'Support for blockwise transfer (default: true)', 'CBOR=BOOLEAN' => 'Transparent JSON/CBOR conversion (default: false)', + 'DTLS=BOOLEAN' => 'DTLS support (default: false)', 'DefaultFormat=F' => 'Content-Type if CoAP accept option on request is undefined', 'Host=HOST' => "Hostname to listen on (default: #{host})", 'Log=LOG' => 'Change logging (debug|none)', 'Multicast=BOOLEAN' => 'Multicast support (default: true)', 'MulticastGroups=ARRAY' => "Multicast groups (default: #{maddrs.join(', ')})", 'Observe=BOOLEAN' => 'Observe support (default: true)', - 'Port=PORT' => "Port to listen on (default: #{port})" + 'Port=PORT' => "UDP port to listen on (default: #{port})", + 'PortDTLS=PORT' => "DTLS port to listen on (default: #{dport})" } end end diff --git a/spec/observe_spec.rb b/spec/observe_spec.rb index 3102dfe..433ab9d 100644 --- a/spec/observe_spec.rb +++ b/spec/observe_spec.rb @@ -5,6 +5,9 @@ describe Observe do let!(:observe) { Observe.supervise(as: :observe) } + let(:port) { random_port } + let!(:server) { supervised_server(:Host => '0.0.0.0', :Port => port) } + # TODO Replace this with factory. before do [:@exchange1, :@exchange2].each do |var| @@ -13,7 +16,7 @@ options = { uri_path: [], token: token } message = CoAP::Message.new(:con, :get, mid, '', options) - exchange = Exchange.new('127.0.0.1', CoAP::PORT, message) + exchange = Exchange.new(Celluloid::Actor[:server_udp], '127.0.0.1', CoAP::PORT, message) instance_variable_set(var, exchange) end @@ -138,10 +141,6 @@ end describe '#handle_update' do - let(:port) { random_port } - - let!(:server) { supervised_server(:Host => '0.0.0.0', :Port => port) } - context 'error (4.04)' do let!(:key) { [dummy1[0].host, dummy1[0].token] } @@ -174,10 +173,6 @@ end describe '#tick' do - let(:port) { random_port } - - let!(:server) { supervised_server(:Host => '0.0.0.0', :Port => port) } - context 'update (2.05)' do let!(:key) { [dummy2[0].host, dummy2[0].token] } @@ -196,9 +191,6 @@ end describe 'integration' do - let(:port) { random_port } - - let!(:server) { supervised_server(:Port => port) } let!(:client) do CoAP::Client.new(port: port, retransmit: false, recv_timeout: 0.1) end @@ -208,7 +200,7 @@ @t1 = Thread.start do client.observe \ - '/value', localhost, nil, + '/value', '0.0.0.0', nil, ->(s, m) { @answers << m } end diff --git a/spec/server_spec.rb b/spec/server_spec.rb index f49824a..5f3bfc9 100644 --- a/spec/server_spec.rb +++ b/spec/server_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'benchmark' -describe Server do +describe Server::CoAP do let(:port) { random_port } let(:client) do CoAP::Client.new(port: port, retransmit: false, recv_timeout: 0.1) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c796034..43d49cf 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -63,7 +63,7 @@ def supervised_server(options) g = Celluloid::Supervision::Container.run! - g.supervise(as: :server, type: ::David::Server, + g.supervise(as: :server_udp, type: ::David::Server::CoAP, args: [app, defaults.merge(options)]) g.supervise(as: :gc, type: ::David::GarbageCollector) @@ -72,7 +72,7 @@ def supervised_server(options) g.supervise(as: :observe, type: ::David::Observe) end - Celluloid::Actor[:server].async.run + Celluloid::Actor[:server_udp].async.run g end