david 0.3.0.pre

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.
@@ -0,0 +1,5 @@
1
+ David
2
+ =====
3
+
4
+ David is a CoAP server with Rack interface to bring the illustrious family of
5
+ Rack compatible web frameworks into the Internet of Things.
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/david'
4
+ require_relative '../lib/rack/hello_world'
5
+
6
+ debug = true if ARGV.include?('-d')
7
+
8
+ klass = Rack::HelloWorld.new
9
+ klass = Rack::Lint.new(klass) if ARGV.include?('-l')
10
+
11
+ Rack::Handler::David.run(klass, :Debug => debug)
@@ -0,0 +1,4 @@
1
+ require_relative 'lib/david'
2
+ require_relative 'lib/rack/hello_world'
3
+
4
+ run Rack::HelloWorld.new
@@ -0,0 +1,26 @@
1
+ require_relative 'lib/david/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'david'
5
+ s.version = David::VERSION
6
+
7
+ s.summary = 'CoAP server with Rack interface.'
8
+ s.description = "David is a CoAP server with Rack interface to bring the
9
+ illustrious family of Rack compatible web frameworks into the Internet of
10
+ Things."
11
+
12
+ s.homepage = 'https://github.com/nning/david'
13
+ s.license = 'GPL-3.0'
14
+ s.author = 'henning mueller'
15
+ s.email = 'henning@orgizm.net'
16
+
17
+ s.files = `git ls-files`.split($/)
18
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
20
+ s.require_paths = ['lib']
21
+
22
+ s.add_dependency 'cbor', '~> 0.5'
23
+ s.add_dependency 'celluloid-io', '~> 0.16'
24
+ s.add_dependency 'coap', '~> 0'
25
+ s.add_dependency 'rack', '~> 1.5'
26
+ end
@@ -0,0 +1,33 @@
1
+ module David
2
+ end
3
+
4
+ require 'bundler/setup'
5
+ Bundler.require
6
+
7
+ unless defined? JRuby
8
+ require 'cbor'
9
+ end
10
+
11
+ require 'celluloid'
12
+ require 'celluloid/io'
13
+ require 'coap'
14
+ require 'ipaddr'
15
+ require 'rack'
16
+
17
+ include CoRE
18
+
19
+ $: << File.dirname(__FILE__)
20
+
21
+ require 'rack/hello_world'
22
+ require 'rack/handler/david'
23
+ require 'rack/handler/coap'
24
+
25
+ require 'david/guerilla/rack/handler'
26
+
27
+ require 'david/version'
28
+ require 'david/server'
29
+
30
+ if defined? Rails
31
+ require 'david/railties/config'
32
+ require 'david/railties/middleware'
33
+ end
@@ -0,0 +1,21 @@
1
+ # Monkey-patch Rack to first try David.
2
+ # https://github.com/rack/rack/blob/master/lib/rack/handler.rb#L46-L61
3
+ module Rack
4
+ module Handler
5
+ def self.default(options = {})
6
+ # Guess.
7
+ if ENV.include?("PHP_FCGI_CHILDREN")
8
+ # We already speak FastCGI
9
+ options.delete :File
10
+ options.delete :Port
11
+ Rack::Handler::FastCGI
12
+ elsif ENV.include?("REQUEST_METHOD")
13
+ Rack::Handler::CGI
14
+ elsif ENV.include?("RACK_HANDLER")
15
+ self.get(ENV["RACK_HANDLER"])
16
+ else
17
+ pick ['david', 'thin', 'puma', 'webrick']
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module David
2
+ module Railties
3
+ class Config < Rails::Railtie
4
+ config.coap = ActiveSupport::OrderedOptions.new
5
+ config.coap.cbor = true
6
+ config.coap.only = true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ require 'david/well_known'
2
+
3
+ module David
4
+ module Railties
5
+ class Middleware < Rails::Railtie
6
+ UNWANTED = [
7
+ ActionDispatch::Cookies,
8
+ ActionDispatch::DebugExceptions,
9
+ ActionDispatch::Flash,
10
+ ActionDispatch::RemoteIp,
11
+ ActionDispatch::RequestId,
12
+ ActionDispatch::Session::CookieStore,
13
+ # ActionDispatch::ShowExceptions,
14
+ Rack::ConditionalGet,
15
+ # Rack::ETag,
16
+ Rack::Head,
17
+ Rack::Lock,
18
+ Rack::MethodOverride,
19
+ Rack::Runtime,
20
+ ]
21
+
22
+ initializer 'david.clear_out_middleware' do |app|
23
+ if config.coap.only
24
+ UNWANTED.each { |klass| app.middleware.delete klass }
25
+ end
26
+
27
+ app.middleware.insert_after(Rails::Rack::Logger, David::WellKnown)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,62 @@
1
+ require 'david/server/options'
2
+ require 'david/server/response'
3
+
4
+ module David
5
+ class Server
6
+ include Celluloid::IO
7
+ include CoAP::Codification
8
+
9
+ include Options
10
+ include Response
11
+
12
+ attr_reader :logger
13
+
14
+ finalizer :shutdown
15
+
16
+ def initialize(app, options)
17
+ @cbor = choose(:cbor, options[:CBOR])
18
+ @host = choose(:host, options[:Host])
19
+ @logger = choose(:logger, options[:Log])
20
+ @port = options[:Port].to_i
21
+
22
+ @app = app.respond_to?(:new) ? app.new : app
23
+
24
+ logger.info "David #{David::VERSION} on #{RUBY_DESCRIPTION}"
25
+ logger.info "Starting on [#{@host}]:#{@port}"
26
+
27
+ ipv6 = IPAddr.new(@host).ipv6?
28
+ af = ipv6 ? ::Socket::AF_INET6 : ::Socket::AF_INET
29
+
30
+ # Actually Celluloid::IO::UDPServer.
31
+ # (Use celluloid-io from git, 0.15.0 does not support AF_INET6).
32
+ @socket = UDPSocket.new(af)
33
+ @socket.bind(@host, @port)
34
+
35
+ async.run
36
+ end
37
+
38
+ private
39
+
40
+ def shutdown
41
+ @socket.close unless @socket.nil?
42
+ end
43
+
44
+ def run
45
+ loop { async.handle_input(*@socket.recvfrom(1024)) }
46
+ end
47
+
48
+ def handle_input(data, sender)
49
+ _, port, host = sender
50
+ request = CoAP::Message.parse(data)
51
+
52
+ logger.info "[#{host}]:#{port}: #{request}"
53
+ logger.debug request.inspect
54
+
55
+ response = respond(host, port, request)
56
+
57
+ logger.debug response.inspect
58
+
59
+ CoAP::Ether.send(response, host, port, socket: @socket)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,32 @@
1
+ module David
2
+ class Server
3
+ module Mapping
4
+ protected
5
+
6
+ def body_to_cbor(body)
7
+ JSON.parse(body).to_cbor
8
+ end
9
+
10
+ def coap_to_http_method(method)
11
+ method.to_s.upcase
12
+ end
13
+
14
+ def etag(options, bytes = 8)
15
+ etag = options['ETag']
16
+ etag.delete('"').bytes.first(bytes * 2).pack('C*').hex if etag
17
+ end
18
+
19
+ def http_to_coap_code(code)
20
+ code = code.to_i
21
+
22
+ h = {200 => 205}
23
+ code = h[code] if h[code]
24
+
25
+ a = code / 100
26
+ b = code - (a * 100)
27
+
28
+ [a, b]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ module David
2
+ class Server
3
+ module Options
4
+ protected
5
+
6
+ def choose(name, value)
7
+ send(('choose_' + name.to_s).to_sym, value)
8
+ end
9
+
10
+ def choose_cbor(value)
11
+ if value.nil? && defined? Rails
12
+ value = Rails.application.config.coap.cbor
13
+ end
14
+
15
+ !!value
16
+ end
17
+
18
+ # Rails starts on 'localhost' since 4.2.0.beta1
19
+ # (Resolv class seems not to consider /etc/hosts)
20
+ def choose_host(value)
21
+ Socket::getaddrinfo(value, nil, nil, Socket::SOCK_STREAM)[0][3]
22
+ end
23
+
24
+ def choose_logger(log)
25
+ fd = $stderr
26
+ level = ::Logger::INFO
27
+
28
+ case log
29
+ when 'debug'
30
+ level = ::Logger::DEBUG
31
+ when 'none'
32
+ fd = File.open('/dev/null', 'w')
33
+ end
34
+
35
+ logger = ::Logger.new(fd)
36
+ logger.level = level
37
+ logger.formatter = proc do |sev, time, prog, msg|
38
+ "#{time.strftime('[%Y-%m-%d %H:%M:%S]')} #{sev} #{msg}\n"
39
+ end
40
+
41
+ Celluloid.logger = logger
42
+
43
+ logger
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,124 @@
1
+ require 'david/server/mapping'
2
+ require 'david/server/utility'
3
+
4
+ module David
5
+ class Server
6
+ module Response
7
+ include Mapping
8
+ include Utility
9
+
10
+ # Freeze some WSGI env keys.
11
+ REMOTE_ADDR = 'REMOTE_ADDR'.freeze
12
+ REMOTE_PORT = 'REMOTE_PORT'.freeze
13
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
14
+ SCRIPT_NAME = 'SCRIPT_NAME'.freeze
15
+ PATH_INFO = 'PATH_INFO'.freeze
16
+ QUERY_STRING = 'QUERY_STRING'.freeze
17
+ SERVER_NAME = 'SERVER_NAME'.freeze
18
+ SERVER_PORT = 'SERVER_PORT'.freeze
19
+ CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
20
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
21
+ HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
22
+
23
+ # Freeze some Rack env keys.
24
+ RACK_VERSION = 'rack.version'.freeze
25
+ RACK_URL_SCHEME = 'rack.url_scheme'.freeze
26
+ RACK_INPUT = 'rack.input'.freeze
27
+ RACK_ERRORS = 'rack.errors'.freeze
28
+ RACK_MULTITHREAD = 'rack.multithread'.freeze
29
+ RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
30
+ RACK_RUN_ONCE = 'rack.run_once'.freeze
31
+ RACK_LOGGER = 'rack.logger'.freeze
32
+
33
+ # Freeze some Rack env values.
34
+ EMPTY_STRING = ''.freeze
35
+ CONTENT_TYPE_JSON = 'application/json'.freeze
36
+ CONTENT_TYPE_CBOR = 'application/cbor'.freeze
37
+ RACK_URL_SCHEME_HTTP = 'http'.freeze
38
+
39
+ protected
40
+
41
+ def respond(host, port, request)
42
+ block = CoAP::Block.new(request.options[:block2]).decode
43
+ block.size = 1024 if request.options[:block2] == 0
44
+
45
+ # Fail if m set.
46
+ if block.more
47
+ response = initialize_response(request)
48
+ response.mcode = 4.05
49
+ return response
50
+ end
51
+
52
+ env = basic_env(host, port, request)
53
+ logger.debug env
54
+
55
+ code, options, body = @app.call(env)
56
+
57
+ ct = content_type(options)
58
+ body = body_to_string(body)
59
+
60
+ body.close if body.respond_to?(:close)
61
+
62
+ if @cbor
63
+ body = body_to_cbor(body)
64
+ ct = CONTENT_TYPE_CBOR
65
+ end
66
+
67
+ mcode = http_to_coap_code(code)
68
+ etag = etag(options, 4)
69
+ cf = CoAP::Registry.convert_content_format(ct)
70
+
71
+ response = initialize_response(request)
72
+
73
+ response.mcode = mcode
74
+ response.payload = block.chunk(body)
75
+
76
+ response.options[:etag] = etag
77
+ response.options[:content_format] = cf
78
+
79
+ block.more = !block.last?(body)
80
+
81
+ response.options[:block2] = block.encode
82
+
83
+ logger.debug block.inspect
84
+
85
+ response
86
+ end
87
+
88
+ def basic_env(host, port, request)
89
+ {
90
+ REMOTE_ADDR => host,
91
+ REMOTE_PORT => port.to_s,
92
+ REQUEST_METHOD => coap_to_http_method(request.mcode),
93
+ SCRIPT_NAME => EMPTY_STRING,
94
+ PATH_INFO => path_encode(request.options[:uri_path]),
95
+ QUERY_STRING => query_encode(request.options[:uri_query])
96
+ .gsub(/^\?/, ''),
97
+ SERVER_NAME => @host,
98
+ SERVER_PORT => @port.to_s,
99
+ CONTENT_LENGTH => request.payload.size.to_s,
100
+ CONTENT_TYPE => CONTENT_TYPE_JSON,
101
+ HTTP_ACCEPT => CONTENT_TYPE_JSON,
102
+ RACK_VERSION => [1, 2],
103
+ RACK_URL_SCHEME => RACK_URL_SCHEME_HTTP,
104
+ RACK_INPUT => StringIO.new(request.payload),
105
+ RACK_ERRORS => $stderr,
106
+ RACK_MULTITHREAD => true,
107
+ RACK_MULTIPROCESS => true,
108
+ RACK_RUN_ONCE => false,
109
+ RACK_LOGGER => @logger,
110
+ }
111
+ end
112
+
113
+ def initialize_response(request)
114
+ type = request.tt == :con ? :ack : :non
115
+
116
+ CoAP::Message.new \
117
+ tt: type,
118
+ mcode: 2.00,
119
+ mid: request.mid,
120
+ token: request.options[:token]
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,19 @@
1
+ module David
2
+ class Server
3
+ module Utility
4
+ protected
5
+
6
+ # This can only use each on body and currently does not support streaming.
7
+ def body_to_string(body)
8
+ s = ''
9
+ body.each { |line| s += line + "\r\n" }
10
+ s.chomp
11
+ end
12
+
13
+ def content_type(options)
14
+ ct = options['Content-Type']
15
+ ct.split(';').first unless ct.nil?
16
+ end
17
+ end
18
+ end
19
+ end