david 0.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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