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.
- checksums.yaml +7 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +51 -0
- data/LICENSE +674 -0
- data/README.md +5 -0
- data/Rakefile +6 -0
- data/bin/david +11 -0
- data/config.ru +4 -0
- data/david.gemspec +26 -0
- data/lib/david.rb +33 -0
- data/lib/david/guerilla/rack/handler.rb +21 -0
- data/lib/david/railties/config.rb +9 -0
- data/lib/david/railties/middleware.rb +31 -0
- data/lib/david/server.rb +62 -0
- data/lib/david/server/mapping.rb +32 -0
- data/lib/david/server/options.rb +47 -0
- data/lib/david/server/response.rb +124 -0
- data/lib/david/server/utility.rb +19 -0
- data/lib/david/version.rb +7 -0
- data/lib/david/well_known.rb +59 -0
- data/lib/rack/handler/coap.rb +6 -0
- data/lib/rack/handler/david.rb +35 -0
- data/lib/rack/hello_world.rb +45 -0
- data/spec/perf/server_perf_spec.rb +19 -0
- data/spec/spec_helper.rb +2 -0
- data/test.rb +58 -0
- metadata +131 -0
data/README.md
ADDED
data/Rakefile
ADDED
data/bin/david
ADDED
@@ -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)
|
data/config.ru
ADDED
data/david.gemspec
ADDED
@@ -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
|
data/lib/david.rb
ADDED
@@ -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,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
|
data/lib/david/server.rb
ADDED
@@ -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
|