From ecd108c8c7913dd1ed6228802c8a03f948a48f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Tempel?= Date: Fri, 13 Jul 2018 16:06:53 +0200 Subject: [PATCH 1/5] Add initial support for DTLS using the tinydtls ruby gem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently it is not possible to enable both CoAP und CoAPs at the same time. Doing so would require starting multiple instance of all other celluloid actors (observe, garbage collector, …). Alternatively we could extend this actors in a way that they perform all operations on both servers. The support is implemented by implementing two different server classes which extend the David::Server class: 1. David::Server::CoAP for doing CoAP over UDP 2. David::Server::CoAPs for doing CoAP over DTLS This required some namespacing changes since the CoAP gem can now no longer be addressed as CoAP::<...>. This seems to be the preferred way of implementing multi protocol servers with celluloid. Since this approach was also used for the reel web server: https://github.com/celluloid/reel/wiki/Multi-Protocol-Server --- Gemfile | 4 ++++ lib/david.rb | 6 ++++++ lib/david/app_config.rb | 5 +++++ lib/david/server.rb | 15 +++++++++------ lib/david/server/coap.rb | 17 +++++++++++++++++ lib/david/server/coaps.rb | 20 ++++++++++++++++++++ lib/david/server/mapping.rb | 2 +- lib/david/server/respond.rb | 4 ++-- lib/david/server/utility.rb | 5 +++++ lib/rack/handler/david.rb | 11 +++++++++-- 10 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 lib/david/server/coap.rb create mode 100644 lib/david/server/coaps.rb 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/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..f2450e6 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, @@ -32,6 +33,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? diff --git a/lib/david/server.rb b/lib/david/server.rb index 046d1ed..c1ddc04 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 @@ -24,15 +26,14 @@ def initialize(app, options) @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 +42,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,13 +71,13 @@ 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) + message = ::CoAP::Message.parse(data) exchange = Exchange.new(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/rack/handler/david.rb b/lib/rack/handler/david.rb index 592b1a2..5715484 100644 --- a/lib/rack/handler/david.rb +++ b/lib/rack/handler/david.rb @@ -4,9 +4,15 @@ 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) + if options[:DTLS] == 'true' + g.supervise(as: :server, type: ::David::Server::CoAPs, + args: [app, options.merge!(Port: "5684")]) + else + g.supervise(as: :server, type: ::David::Server::CoAP, + args: [app, options]) + end + g.supervise(as: :gc, type: ::David::GarbageCollector) if options[:Observe] != 'false' g.supervise(as: :observe, type: ::David::Observe) end @@ -27,6 +33,7 @@ def self.valid_options { '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)', From 7e9cb4ff78cb749f02fe9b27a283ea8d6dfdf78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Tempel?= Date: Fri, 13 Jul 2018 17:45:40 +0200 Subject: [PATCH 2/5] Add an option for specifying a DTLS port --- lib/david/app_config.rb | 7 ++++++- lib/rack/handler/david.rb | 19 ++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/david/app_config.rb b/lib/david/app_config.rb index f2450e6..b773cc9 100644 --- a/lib/david/app_config.rb +++ b/lib/david/app_config.rb @@ -11,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 = {}) @@ -87,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/rack/handler/david.rb b/lib/rack/handler/david.rb index 5715484..c440ba9 100644 --- a/lib/rack/handler/david.rb +++ b/lib/rack/handler/david.rb @@ -4,12 +4,9 @@ class David def self.run(app, options={}) g = Celluloid::Supervision::Container.run! + g.supervise(as: :server_udp, type: ::David::Server::CoAP, args: [app, options]) if options[:DTLS] == 'true' - g.supervise(as: :server, type: ::David::Server::CoAPs, - args: [app, options.merge!(Port: "5684")]) - else - g.supervise(as: :server, type: ::David::Server::CoAP, - args: [app, options]) + g.supervise(as: :server_dtls, type: ::David::Server::CoAPs, args: [app, options]) end g.supervise(as: :gc, type: ::David::GarbageCollector) @@ -18,7 +15,10 @@ def self.run(app, options={}) end begin - Celluloid::Actor[:server].run + Celluloid::Actor[:server_udp].run + if options[:DTLS] == 'true' + Celluloid::Actor[:server_dtls].run + end rescue Interrupt Celluloid.logger.info 'Terminated' Celluloid.logger = nil @@ -27,8 +27,8 @@ 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)', @@ -40,7 +40,8 @@ def self.valid_options '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 From a9b788596fc696a682923bc3e382e3caa9bb4e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Tempel?= Date: Fri, 13 Jul 2018 18:11:14 +0200 Subject: [PATCH 3/5] Support multiple servers in all actors --- lib/david/exchange.rb | 2 +- lib/david/garbage_collector.rb | 8 +++++--- lib/david/observe.rb | 4 ++-- lib/david/registry.rb | 17 ++++++++++++++--- lib/david/server.rb | 5 ++--- lib/david/transmitter.rb | 2 +- lib/rack/handler/david.rb | 5 +++-- spec/observe_spec.rb | 18 +++++------------- spec/server_spec.rb | 2 +- spec/spec_helper.rb | 4 ++-- 10 files changed, 36 insertions(+), 31 deletions(-) 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 c1ddc04..ba01be6 100644 --- a/lib/david/server.rb +++ b/lib/david/server.rb @@ -16,7 +16,7 @@ class Server include Respond include Utility - attr_reader :log, :socket + attr_reader :socket finalizer :shutdown @@ -24,7 +24,6 @@ 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_key) @@ -78,7 +77,7 @@ def dispatch(*args) end message = ::CoAP::Message.parse(data) - exchange = Exchange.new(host, port, message, anc) + exchange = Exchange.new(self, host, port, message, anc) return if !exchange.non? && exchange.multicast? 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 c440ba9..51f68cb 100644 --- a/lib/rack/handler/david.rb +++ b/lib/rack/handler/david.rb @@ -15,10 +15,11 @@ def self.run(app, options={}) end begin - Celluloid::Actor[:server_udp].run if options[:DTLS] == 'true' - Celluloid::Actor[:server_dtls].run + Celluloid::Actor[:server_dtls].async.run end + + Celluloid::Actor[:server_udp].run rescue Interrupt Celluloid.logger.info 'Terminated' Celluloid.logger = nil 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 From efa3bbe9a3176e811db387dc861569c5d675e8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Tempel?= Date: Sat, 14 Jul 2018 15:04:48 +0200 Subject: [PATCH 4/5] Update dependencies --- Gemfile.lock | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 2ea5611..775be19 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.1.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 From 62dfe6d627476d33e3c4577f1f85d2b8ea73ccdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Tempel?= Date: Thu, 2 Aug 2018 17:26:26 +0200 Subject: [PATCH 5/5] fixup! Update dependencies --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 775be19..ca8a2b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -221,7 +221,7 @@ GEM timers (4.1.2) hitimes tins (1.16.3) - tinydtls (0.1.0) + tinydtls (0.2.0) ffi (~> 1.9) tzinfo (1.2.5) thread_safe (~> 0.1)