david 0.3.0.pre → 0.3.0

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