david 0.3.0.pre → 0.3.0

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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +24 -0
  4. data/Gemfile +11 -7
  5. data/Gemfile.lock +155 -19
  6. data/README.md +21 -3
  7. data/Rakefile +5 -0
  8. data/TODO.md +73 -0
  9. data/benchmarks/Gemfile +4 -0
  10. data/benchmarks/Gemfile.lock +31 -0
  11. data/benchmarks/rps.rb +20 -0
  12. data/bin/david +9 -4
  13. data/config.ru +4 -0
  14. data/david.gemspec +10 -4
  15. data/experiments/mcast.rb +37 -0
  16. data/experiments/structs.rb +45 -0
  17. data/{test.rb → experiments/test.rb} +0 -0
  18. data/lib/david.rb +15 -4
  19. data/lib/david/actor.rb +18 -0
  20. data/lib/david/garbage_collector.rb +35 -0
  21. data/lib/david/observe.rb +102 -0
  22. data/lib/david/rails/action_controller/base.rb +11 -0
  23. data/lib/david/railties/config.rb +20 -1
  24. data/lib/david/railties/middleware.rb +18 -6
  25. data/lib/david/request.rb +80 -0
  26. data/lib/david/resource_discovery.rb +92 -0
  27. data/lib/david/resource_discovery_proxy.rb +13 -0
  28. data/lib/david/server.rb +72 -27
  29. data/lib/david/server/constants.rb +48 -0
  30. data/lib/david/server/deduplication.rb +21 -0
  31. data/lib/david/server/mapping.rb +64 -12
  32. data/lib/david/server/multicast.rb +54 -0
  33. data/lib/david/server/options.rb +32 -0
  34. data/lib/david/server/respond.rb +146 -0
  35. data/lib/david/server/utility.rb +1 -6
  36. data/lib/david/show_exceptions.rb +52 -0
  37. data/lib/david/version.rb +2 -1
  38. data/lib/rack/handler/david.rb +16 -6
  39. data/lib/rack/hello_world.rb +23 -0
  40. data/spec/dummy/Rakefile +6 -0
  41. data/spec/dummy/app/assets/images/.keep +0 -0
  42. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  43. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  44. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  45. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  46. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  47. data/spec/dummy/app/mailers/.keep +0 -0
  48. data/spec/dummy/app/models/.keep +0 -0
  49. data/spec/dummy/app/models/concerns/.keep +0 -0
  50. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  51. data/spec/dummy/bin/bundle +3 -0
  52. data/spec/dummy/bin/rails +4 -0
  53. data/spec/dummy/bin/rake +4 -0
  54. data/spec/dummy/config.ru +4 -0
  55. data/spec/dummy/config/application.rb +29 -0
  56. data/spec/dummy/config/boot.rb +5 -0
  57. data/spec/dummy/config/database.yml +25 -0
  58. data/spec/dummy/config/environment.rb +5 -0
  59. data/spec/dummy/config/environments/development.rb +37 -0
  60. data/spec/dummy/config/environments/production.rb +78 -0
  61. data/spec/dummy/config/environments/test.rb +39 -0
  62. data/spec/dummy/config/initializers/assets.rb +8 -0
  63. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  64. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  65. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  66. data/spec/dummy/config/initializers/inflections.rb +16 -0
  67. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  68. data/spec/dummy/config/initializers/session_store.rb +3 -0
  69. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  70. data/spec/dummy/config/locales/en.yml +23 -0
  71. data/spec/dummy/config/routes.rb +58 -0
  72. data/spec/dummy/config/secrets.yml +22 -0
  73. data/spec/dummy/db/test.sqlite3 +0 -0
  74. data/spec/dummy/lib/assets/.keep +0 -0
  75. data/spec/dummy/log/.keep +0 -0
  76. data/spec/dummy/public/404.html +67 -0
  77. data/spec/dummy/public/422.html +67 -0
  78. data/spec/dummy/public/500.html +66 -0
  79. data/spec/dummy/public/favicon.ico +0 -0
  80. data/spec/guerilla_rack_handler_spec.rb +16 -0
  81. data/spec/mapping_spec.rb +56 -0
  82. data/spec/observe_spec.rb +111 -0
  83. data/spec/perf/server_perf_spec.rb +15 -9
  84. data/spec/resource_discovery_spec.rb +65 -0
  85. data/spec/server_spec.rb +306 -0
  86. data/spec/spec_helper.rb +43 -1
  87. data/spec/utility_spec.rb +8 -0
  88. metadata +195 -38
  89. data/lib/david/server/response.rb +0 -124
  90. data/lib/david/well_known.rb +0 -59
@@ -0,0 +1,92 @@
1
+ module David
2
+ class ResourceDiscovery
3
+ include Celluloid
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ dup._call(env)
11
+ end
12
+
13
+ def _call(env)
14
+ return @app.call(env) if env['PATH_INFO'] != '/.well-known/core'
15
+ return [405, {}, []] if env['REQUEST_METHOD'] != 'GET'
16
+
17
+ @env = env
18
+
19
+ filtered = routes_hash.select { |link| filter(link) }
20
+ body = filtered.keys.map(&:to_s).join(',')
21
+
22
+ # TODO On multicast, do not respond if result set empty.
23
+
24
+ [
25
+ 200,
26
+ {
27
+ 'Content-Type' => 'application/link-format',
28
+ 'Content-Length' => body.bytesize.to_s
29
+ },
30
+ [body]
31
+ ]
32
+ end
33
+
34
+ def register(controller, options)
35
+ name = controller.controller_name
36
+ default = options.delete(:default)
37
+
38
+ routes_hash.each do |link, route|
39
+ next unless route[:controller] == name
40
+
41
+ link.merge!(default) unless default.nil?
42
+
43
+ attrs = options[route[:action].to_sym]
44
+ link.merge!(attrs) unless attrs.nil?
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def clean_routes
51
+ @clean_routes ||= routes
52
+ .uniq { |r| r[0] }
53
+ .select { |r| r if include_route?(r) }
54
+ .each { |r| delete_format!(r) }
55
+ end
56
+
57
+ def delete_format!(route)
58
+ route[0].gsub!(/\(\.:format\)\z/, '')
59
+ end
60
+
61
+ def filter(link)
62
+ href = @env['QUERY_STRING'].split('href=').last
63
+
64
+ return true if href.blank?
65
+
66
+ # TODO If query end in '*', match on prefix.
67
+ # Otherwise match on whole string.
68
+ # https://tools.ietf.org/html/rfc6690#section-4.1
69
+ link.uri =~ Regexp.new(href)
70
+ end
71
+
72
+ def include_route?(route)
73
+ !(route[0] =~ /\A\/(assets|rails)/)
74
+ end
75
+
76
+ def routes
77
+ Rails.application.routes.routes.map do |route|
78
+ [
79
+ route.path.spec.to_s,
80
+ route.defaults[:controller],
81
+ route.defaults[:action]
82
+ ]
83
+ end
84
+ end
85
+
86
+ def routes_hash
87
+ @routes_hash ||= Hash[clean_routes.collect { |r|
88
+ [CoRE::Link.new(r[0]), { controller: r[1], action: r[2] }]
89
+ }]
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,13 @@
1
+ require 'david/resource_discovery'
2
+
3
+ module David
4
+ class ResourceDiscoveryProxy
5
+ def initialize(app)
6
+ ResourceDiscovery.supervise_as(:discovery, app)
7
+ end
8
+
9
+ def call(env)
10
+ Celluloid::Actor[:discovery].call(env)
11
+ end
12
+ end
13
+ end
@@ -1,35 +1,46 @@
1
+ require 'david/server/deduplication'
2
+ require 'david/server/multicast'
1
3
  require 'david/server/options'
2
- require 'david/server/response'
4
+ require 'david/server/respond'
3
5
 
4
6
  module David
5
7
  class Server
6
8
  include Celluloid::IO
7
- include CoAP::Codification
9
+ include CoAP::Coding
8
10
 
11
+ include Deduplication
12
+ include Multicast
9
13
  include Options
10
- include Response
14
+ include Respond
11
15
 
12
- attr_reader :logger
16
+ attr_reader :logger, :socket
13
17
 
14
18
  finalizer :shutdown
15
19
 
16
20
  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
+ @block = choose(:block, options[:Block])
22
+ @cbor = choose(:cbor, options[:CBOR])
23
+ @host = choose(:host, options[:Host])
24
+ @logger = choose(:logger, options[:Log])
25
+ @mcast = choose(:mcast, options[:Multicast])
26
+ @observe = choose(:observe, options[:Observe])
27
+ @port = options[:Port].to_i
21
28
 
22
- @app = app.respond_to?(:new) ? app.new : app
29
+ @app = app.respond_to?(:new) ? app.new : app
30
+
31
+ @default_format = choose(:default_format, options[:DefaultFormat])
32
+
33
+ @dedup_cache = {}
23
34
 
24
35
  logger.info "David #{David::VERSION} on #{RUBY_DESCRIPTION}"
25
36
  logger.info "Starting on [#{@host}]:#{@port}"
26
37
 
27
- ipv6 = IPAddr.new(@host).ipv6?
28
- af = ipv6 ? ::Socket::AF_INET6 : ::Socket::AF_INET
38
+ @ipv6 = IPAddr.new(@host).ipv6?
39
+ af = @ipv6 ? ::Socket::AF_INET6 : ::Socket::AF_INET
29
40
 
30
- # Actually Celluloid::IO::UDPServer.
31
- # (Use celluloid-io from git, 0.15.0 does not support AF_INET6).
41
+ # Actually Celluloid::IO::UDPSocket.
32
42
  @socket = UDPSocket.new(af)
43
+ multicast_initialize if @mcast
33
44
  @socket.bind(@host, @port)
34
45
 
35
46
  async.run
@@ -37,26 +48,60 @@ module David
37
48
 
38
49
  private
39
50
 
40
- def shutdown
41
- @socket.close unless @socket.nil?
42
- end
51
+ def handle_input(*args)
52
+ data, sender, _, anc = args
43
53
 
44
- def run
45
- loop { async.handle_input(*@socket.recvfrom(1024)) }
46
- end
54
+ if defined?(JRuby)
55
+ port, _, host = sender[1..3]
56
+ else
57
+ host, port = sender.ip_address, sender.ip_port
58
+ end
59
+
60
+ message = CoAP::Message.parse(data)
61
+ request = Request.new(host, port, message, anc)
47
62
 
48
- def handle_input(data, sender)
49
- _, port, host = sender
50
- request = CoAP::Message.parse(data)
63
+ return unless request.con? || request.non?
64
+ return unless request.valid_method?
65
+ return if !request.non? && request.multicast?
51
66
 
52
- logger.info "[#{host}]:#{port}: #{request}"
53
- logger.debug request.inspect
67
+ logger.info "[#{host}]:#{port}: #{message} (block #{request.block.num})"
68
+ logger.debug message.inspect
54
69
 
55
- response = respond(host, port, request)
70
+ if request.con? && duplicate?(request) #&& !request.idempotent?
71
+ response, options = cached_response(request)
72
+ logger.debug "(mid:#{request.mid} duplicate, response cached)"
73
+ else
74
+ response, options = respond(request)
75
+ end
56
76
 
57
- logger.debug response.inspect
77
+ unless response.nil?
78
+ logger.debug response.inspect
58
79
 
59
- CoAP::Ether.send(response, host, port, socket: @socket)
80
+ CoAP::Transmission.send(response, host, port,
81
+ options.merge(socket: @socket))
82
+
83
+ request.options = options
84
+ cache_response(request, response)
85
+ end
86
+ end
87
+
88
+ def run
89
+ loop do
90
+ if defined?(JRuby)
91
+ async.handle_input(*@socket.recvfrom(1152))
92
+ else
93
+ begin
94
+ async.handle_input(*@socket.to_io.recvmsg_nonblock)
95
+ rescue ::IO::WaitReadable
96
+ Celluloid::IO.wait_readable(@socket)
97
+ retry
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def shutdown
104
+ @socket.close unless @socket.nil?
60
105
  end
61
106
  end
62
107
  end
@@ -0,0 +1,48 @@
1
+ module David
2
+ class Server
3
+ module Constants
4
+ # Freeze some WSGI env keys.
5
+ REMOTE_ADDR = 'REMOTE_ADDR'.freeze
6
+ REMOTE_PORT = 'REMOTE_PORT'.freeze
7
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
8
+ SCRIPT_NAME = 'SCRIPT_NAME'.freeze
9
+ PATH_INFO = 'PATH_INFO'.freeze
10
+ QUERY_STRING = 'QUERY_STRING'.freeze
11
+ SERVER_NAME = 'SERVER_NAME'.freeze
12
+ SERVER_PORT = 'SERVER_PORT'.freeze
13
+ CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
14
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
15
+ HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
16
+
17
+ # Freeze some Rack env keys.
18
+ RACK_VERSION = 'rack.version'.freeze
19
+ RACK_URL_SCHEME = 'rack.url_scheme'.freeze
20
+ RACK_INPUT = 'rack.input'.freeze
21
+ RACK_ERRORS = 'rack.errors'.freeze
22
+ RACK_MULTITHREAD = 'rack.multithread'.freeze
23
+ RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
24
+ RACK_RUN_ONCE = 'rack.run_once'.freeze
25
+ RACK_LOGGER = 'rack.logger'.freeze
26
+
27
+ # Freeze CoAP specific env keys.
28
+ COAP_VERSION = 'coap.version'.freeze
29
+ COAP_MULTICAST = 'coap.multicast'.freeze
30
+ COAP_DTLS = 'coap.dtls'.freeze
31
+ COAP_DTLS_ID = 'coap.dtls.id'.freeze
32
+ COAP_DTLS_NOSEC = 'NoSec'.freeze
33
+
34
+ # Freeze some Rack env values.
35
+ EMPTY_STRING = ''.freeze
36
+ CONTENT_TYPE_JSON = 'application/json'.freeze
37
+ CONTENT_TYPE_CBOR = 'application/cbor'.freeze
38
+ RACK_URL_SCHEME_HTTP = 'http'.freeze
39
+
40
+ # Freeze HTTP header strings.
41
+ HTTP_CACHE_CONTROL = 'Cache-Control'.freeze
42
+ HTTP_CONTENT_LENGTH = 'Content-Length'.freeze
43
+ HTTP_CONTENT_TYPE = 'Content-Type'.freeze
44
+ HTTP_ETAG = 'ETag'.freeze
45
+ HTTP_LOCATION = 'Location'.freeze
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ module David
2
+ module Deduplication
3
+ def self.included(base)
4
+ attr_reader :dedup_cache
5
+ end
6
+
7
+ def cache_response(request, response)
8
+ return if duplicate?(request)
9
+ @dedup_cache[[request.host, request.mid]] = [response, Time.now.to_i]
10
+ end
11
+
12
+ def cached_response(request)
13
+ response = @dedup_cache[[request.host, request.mid]]
14
+ [response[0], response[0].options] if response
15
+ end
16
+
17
+ def duplicate?(request)
18
+ return !!cached_response(request)
19
+ end
20
+ end
21
+ end
@@ -1,32 +1,84 @@
1
1
  module David
2
2
  class Server
3
3
  module Mapping
4
+ include Constants
5
+
6
+ HTTP_TO_COAP_CODES = {
7
+ 200 => 205,
8
+ 202 => 201,
9
+ 203 => 205,
10
+ 204 => 205,
11
+ 304 => 203,
12
+ 407 => 401,
13
+ 408 => 400,
14
+ 409 => 412,
15
+ 410 => 404,
16
+ 411 => 402,
17
+ 414 => 402,
18
+ 505 => 500,
19
+ 506 => 500,
20
+ 511 => 500,
21
+ }.freeze
22
+
4
23
  protected
24
+
25
+ def accept_to_http(request)
26
+ if request.accept.nil?
27
+ @default_format
28
+ else
29
+ CoAP::Registry.convert_content_format(request.accept)
30
+ end
31
+ end
5
32
 
6
33
  def body_to_cbor(body)
7
34
  JSON.parse(body).to_cbor
8
35
  end
9
36
 
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
37
+ def code_to_coap(code)
38
+ if code.is_a?(Float)
39
+ return [code.to_i, (code * 100 % 100).round]
40
+ end
18
41
 
19
- def http_to_coap_code(code)
20
42
  code = code.to_i
21
-
22
- h = {200 => 205}
23
- code = h[code] if h[code]
43
+ code = HTTP_TO_COAP_CODES[code] if HTTP_TO_COAP_CODES[code]
24
44
 
25
45
  a = code / 100
26
46
  b = code - (a * 100)
27
47
 
28
48
  [a, b]
29
49
  end
50
+
51
+ def etag_to_coap(headers, bytes = 8)
52
+ etag = headers[HTTP_ETAG]
53
+
54
+ if etag
55
+ etag = etag.split('"')
56
+ etag = etag[1] || etag[0]
57
+
58
+ etag
59
+ .bytes
60
+ .first(bytes * 2)
61
+ .pack('C*')
62
+ .hex
63
+ end
64
+ end
65
+
66
+ def location_to_coap(headers)
67
+ l = headers[HTTP_LOCATION].split('/').reject(&:empty?)
68
+ return l.empty? ? nil : l
69
+ rescue NoMethodError
70
+ nil
71
+ end
72
+
73
+ def max_age_to_coap(headers)
74
+ headers[HTTP_CACHE_CONTROL][/max-age=([0-9]*)/, 1]
75
+ rescue NoMethodError
76
+ nil
77
+ end
78
+
79
+ def method_to_http(method)
80
+ method.to_s.upcase
81
+ end
30
82
  end
31
83
  end
32
84
  end
@@ -0,0 +1,54 @@
1
+ module David
2
+ class Server
3
+ # See https://tools.ietf.org/html/rfc7252#section-12.8
4
+ module Multicast
5
+ def multicast_initialize
6
+ @socket.to_io.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, 1)
7
+
8
+ if @ipv6
9
+ maddrs = ['ff02::fd', 'ff05::fd']
10
+ maddrs << 'ff02::1' if OS.osx? # OSX needs ff02::1 explicitly joined.
11
+ maddrs.each { |maddr| multicast_listen_ipv6(maddr) }
12
+
13
+ setsockopts_ipv6
14
+ else
15
+ maddrs = ['224.0.1.187']
16
+ multicast_listen_ipv4(maddrs.first)
17
+
18
+ setsockopts_ipv4
19
+ end
20
+
21
+ logger.debug "Joined multicast groups: #{maddrs.join(', ')}"
22
+ rescue Errno::ENODEV, Errno::EADDRNOTAVAIL
23
+ logger.warn 'Multicast initialization failure: Device not found.'
24
+ @mcast = false
25
+ end
26
+
27
+ def multicast_listen_ipv4(address)
28
+ mreq = IPAddr.new(address).hton + IPAddr.new('0.0.0.0').hton
29
+ @socket.to_io.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, mreq)
30
+ end
31
+
32
+ def multicast_listen_ipv6(address)
33
+ ifindex = 0
34
+
35
+ # http://lists.apple.com/archives/darwin-kernel/2014/Mar/msg00012.html
36
+ if OS.osx?
37
+ ifname = Socket.if_up?('en1') ? 'en1' : 'en0'
38
+ ifindex = Socket.if_nametoindex(ifname)
39
+ end
40
+
41
+ mreq = IPAddr.new(address).hton + [ifindex].pack('i_')
42
+ @socket.to_io.setsockopt(:IPPROTO_IPV6, :IPV6_JOIN_GROUP, mreq)
43
+ end
44
+
45
+ def setsockopts_ipv4
46
+ @socket.to_io.setsockopt(:IPPROTO_IP, :IP_PKTINFO, 1)
47
+ end
48
+
49
+ def setsockopts_ipv6
50
+ @socket.to_io.setsockopt(:IPPROTO_IPV6, :IPV6_RECVPKTINFO, 1)
51
+ end
52
+ end
53
+ end
54
+ end