reel 0.4.0 → 0.5.0.pre

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of reel might be problematic. Click here for more details.

Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -2
  3. data/Gemfile +6 -0
  4. data/README.md +26 -16
  5. data/benchmarks/hello_reel.rb +1 -1
  6. data/benchmarks/reel_pool.rb +27 -0
  7. data/examples/hello_world.rb +2 -2
  8. data/examples/{ssl_hello_world.rb → https_hello_world.rb} +1 -1
  9. data/examples/roundtrip.rb +1 -1
  10. data/examples/server_sent_events.rb +22 -18
  11. data/examples/spy_hello_world.rb +22 -0
  12. data/examples/websockets.rb +3 -3
  13. data/lib/reel.rb +6 -9
  14. data/lib/reel/connection.rb +40 -42
  15. data/lib/reel/mixins.rb +3 -1
  16. data/lib/reel/request.rb +40 -9
  17. data/lib/reel/request/body.rb +65 -0
  18. data/lib/reel/request/info.rb +21 -0
  19. data/lib/reel/{request_parser.rb → request/parser.rb} +2 -2
  20. data/lib/reel/request/state_machine.rb +26 -0
  21. data/lib/reel/response.rb +4 -11
  22. data/lib/reel/response/writer.rb +59 -0
  23. data/lib/reel/server.rb +30 -6
  24. data/lib/reel/server/http.rb +20 -0
  25. data/lib/reel/server/https.rb +63 -0
  26. data/lib/reel/spy.rb +71 -0
  27. data/lib/reel/stream.rb +2 -2
  28. data/lib/reel/version.rb +2 -2
  29. data/lib/reel/websocket.rb +1 -1
  30. data/reel.gemspec +3 -3
  31. data/spec/fixtures/ca.crt +27 -0
  32. data/spec/fixtures/ca.key +27 -0
  33. data/spec/fixtures/client.crt +81 -20
  34. data/spec/fixtures/client.unsigned.crt +22 -0
  35. data/spec/fixtures/server.crt +80 -20
  36. data/spec/reel/connection_spec.rb +50 -11
  37. data/spec/reel/{server_spec.rb → http_server_spec.rb} +1 -1
  38. data/spec/reel/https_server_spec.rb +119 -0
  39. data/spec/reel/{response_writer_spec.rb → response/writer_spec.rb} +10 -2
  40. data/spec/reel/websocket_spec.rb +2 -2
  41. data/spec/spec_helper.rb +3 -34
  42. data/spec/support/example_request.rb +34 -0
  43. metadata +56 -43
  44. data/examples/chunked.rb +0 -25
  45. data/lib/reel/request_body.rb +0 -56
  46. data/lib/reel/request_info.rb +0 -19
  47. data/lib/reel/response_writer.rb +0 -76
  48. data/lib/reel/ssl_server.rb +0 -41
  49. data/spec/reel/ssl_server_spec.rb +0 -54
data/lib/reel/mixins.rb CHANGED
@@ -19,6 +19,7 @@ module Reel
19
19
  # NOTE: Celluloid::IO does not yet support non-blocking reverse DNS
20
20
  socket.peeraddr(true)[2]
21
21
  end
22
+
22
23
  end
23
24
 
24
25
  module RequestMixin
@@ -31,7 +32,7 @@ module Reel
31
32
  @request_info.headers
32
33
  end
33
34
 
34
- def [] header
35
+ def [](header)
35
36
  headers[header]
36
37
  end
37
38
 
@@ -60,4 +61,5 @@ module Reel
60
61
  end
61
62
 
62
63
  end
64
+
63
65
  end
data/lib/reel/request.rb CHANGED
@@ -1,11 +1,19 @@
1
1
  require 'forwardable'
2
2
 
3
+ require 'reel/request/body'
4
+ require 'reel/request/info'
5
+ require 'reel/request/parser'
6
+ require 'reel/request/state_machine'
7
+
8
+ require 'reel/response/writer'
9
+
3
10
  module Reel
4
11
  class Request
5
12
  extend Forwardable
6
13
  include RequestMixin
7
14
 
8
- def_delegators :@connection, :<<, :write, :remote_addr, :respond, :finish_response
15
+ def_delegators :@connection, :remote_addr, :respond
16
+ def_delegator :@response_writer, :handle_response
9
17
  attr_reader :body
10
18
 
11
19
  # request_info is a RequestInfo object including the headers and
@@ -13,13 +21,14 @@ module Reel
13
21
  #
14
22
  # Access it through the RequestMixin methods.
15
23
  def initialize(request_info, connection = nil)
16
- @request_info = request_info
17
- @connection = connection
18
- @finished = false
19
- @buffer = ""
20
- @body = RequestBody.new(self)
21
- @finished_read = false
22
- @websocket = nil
24
+ @request_info = request_info
25
+ @connection = connection
26
+ @finished = false
27
+ @buffer = ""
28
+ @finished_read = false
29
+ @websocket = nil
30
+ @body = Request::Body.new(self)
31
+ @response_writer = Response::Writer.new(connection.socket)
23
32
  end
24
33
 
25
34
  # Returns true if request fully finished reading
@@ -65,7 +74,7 @@ module Reel
65
74
  @buffer = ""
66
75
  else
67
76
  unless finished_reading? || (length && length <= @buffer.length)
68
- @connection.readpartial(length ? length - @buffer.length : Connection::BUFFER_SIZE)
77
+ @connection.readpartial(length ? length - @buffer.length : @connection.buffer_size)
69
78
  end
70
79
 
71
80
  if length
@@ -79,6 +88,23 @@ module Reel
79
88
  slice && slice.length == 0 ? nil : slice
80
89
  end
81
90
 
91
+ # Write body chunks directly to the connection
92
+ def write(chunk)
93
+ unless @connection.response_state == :chunked_body
94
+ raise StateError, "not in chunked body mode"
95
+ end
96
+
97
+ @response_writer.write(chunk)
98
+ end
99
+ alias_method :<<, :write
100
+
101
+ # Finish the response and reset the response state to header
102
+ def finish_response
103
+ raise StateError, "not in body state" if @connection.response_state != :chunked_body
104
+ @response_writer.finish_response
105
+ @connection.response_state = :headers
106
+ end
107
+
82
108
  # Can the current request be upgraded to a WebSocket?
83
109
  def websocket?; @request_info.websocket_request?; end
84
110
 
@@ -90,5 +116,10 @@ module Reel
90
116
  WebSocket.new(@request_info, @connection.hijack_socket)
91
117
  end
92
118
  end
119
+
120
+ # Friendlier inspect
121
+ def inspect
122
+ "#<#{self.class} #{method} #{url} HTTP/#{version} @headers=#{headers.inspect}>"
123
+ end
93
124
  end
94
125
  end
@@ -0,0 +1,65 @@
1
+ module Reel
2
+ class Request
3
+ # Represents the bodies of Requests
4
+ class Body
5
+ include Enumerable
6
+
7
+ def initialize(request)
8
+ @request = request
9
+ @streaming = nil
10
+ @contents = nil
11
+ end
12
+
13
+ # Read exactly the given amount of data
14
+ def read(length)
15
+ stream!
16
+ @request.read(length)
17
+ end
18
+
19
+ # Read up to length bytes, but return any data that's available
20
+ def readpartial(length = nil)
21
+ stream!
22
+ @request.readpartial(length)
23
+ end
24
+
25
+ # Iterate over the body, allowing it to be enumerable
26
+ def each
27
+ while chunk = readpartial
28
+ yield chunk
29
+ end
30
+ end
31
+
32
+ # Eagerly consume the entire body as a string
33
+ def to_str
34
+ return @contents if @contents
35
+ raise StateError, "body is being streamed" unless @streaming.nil?
36
+
37
+ begin
38
+ @streaming = false
39
+ @contents = ""
40
+ while chunk = @request.readpartial
41
+ @contents << chunk
42
+ end
43
+ rescue
44
+ @contents = nil
45
+ raise
46
+ end
47
+
48
+ @contents
49
+ end
50
+ alias_method :to_s, :to_str
51
+
52
+ # Easier to interpret string inspect
53
+ def inspect
54
+ "#<#{self.class}:#{object_id.to_s(16)} @streaming=#{!!@streaming}>"
55
+ end
56
+
57
+ # Assert that the body is actively being streamed
58
+ def stream!
59
+ raise StateError, "body has already been consumed" if @streaming == false
60
+ @streaming = true
61
+ end
62
+ private :stream!
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ module Reel
2
+ class Request
3
+ class Info
4
+ attr_reader :http_method, :url, :http_version, :headers
5
+
6
+ def initialize(http_method, url, http_version, headers)
7
+ @http_method = http_method
8
+ @url = url
9
+ @http_version = http_version
10
+ @headers = headers
11
+ end
12
+
13
+ UPGRADE = 'Upgrade'.freeze
14
+ WEBSOCKET = 'websocket'.freeze
15
+
16
+ def websocket_request?
17
+ headers[UPGRADE] && headers[UPGRADE].downcase == WEBSOCKET
18
+ end
19
+ end
20
+ end
21
+ end
@@ -5,7 +5,7 @@ module Reel
5
5
  attr_reader :socket, :connection
6
6
 
7
7
  def initialize(connection)
8
- @parser = Http::Parser.new(self)
8
+ @parser = HTTP::Parser.new(self)
9
9
  @connection = connection
10
10
  @socket = connection.socket
11
11
  @buffer_size = connection.buffer_size
@@ -50,7 +50,7 @@ module Reel
50
50
  # HTTP::Parser callbacks
51
51
  #
52
52
  def on_headers_complete(headers)
53
- info = RequestInfo.new(http_method, url, http_version, headers)
53
+ info = Info.new(http_method, url, http_version, headers)
54
54
  req = Request.new(info, connection)
55
55
 
56
56
  if @currently_reading
@@ -0,0 +1,26 @@
1
+ module Reel
2
+ class Request
3
+ # Tracks the state of Reel requests
4
+ class StateMachine
5
+ include Celluloid::FSM
6
+
7
+ def initialize(socket)
8
+ @socket = socket
9
+ @hijacked = false
10
+ end
11
+
12
+ default_state :headers
13
+
14
+ state :headers, :to => [:body, :hijacked, :closed]
15
+ state :body, :to => [:headers, :closed]
16
+
17
+ state :hijacked do
18
+ @hijacked = true
19
+ end
20
+
21
+ state :closed do
22
+ @socket.close unless @hijacked || @socket.closed?
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/reel/response.rb CHANGED
@@ -1,7 +1,7 @@
1
+ require 'http/headers'
2
+
1
3
  module Reel
2
4
  class Response
3
- include HTTP::Header
4
-
5
5
  CONTENT_LENGTH = 'Content-Length'.freeze
6
6
  TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
7
7
  CHUNKED = 'chunked'.freeze
@@ -36,12 +36,12 @@ module Reel
36
36
  else raise TypeError, "can't render #{@body.class} as a response body"
37
37
  end
38
38
 
39
- @headers = canonicalize_headers(headers)
39
+ @headers = HTTP::Headers.coerce(headers)
40
40
  @version = http_version
41
41
  end
42
42
 
43
43
  def chunked?
44
- headers[TRANSFER_ENCODING] == CHUNKED
44
+ headers[TRANSFER_ENCODING].to_s == CHUNKED
45
45
  end
46
46
 
47
47
  # Set the status
@@ -61,13 +61,6 @@ module Reel
61
61
  end
62
62
  end
63
63
 
64
- def canonicalize_headers(headers)
65
- headers.inject({}) do |headers, (header, value)|
66
- headers.merge canonicalize_header(header) => value.to_s
67
- end.freeze
68
- end
69
- private :canonicalize_headers
70
-
71
64
  def http_version
72
65
  # FIXME: real HTTP versioning
73
66
  "HTTP/1.1".freeze
@@ -0,0 +1,59 @@
1
+ module Reel
2
+ class Response
3
+ class Writer
4
+ CRLF = "\r\n"
5
+
6
+ def initialize(socket)
7
+ @socket = socket
8
+ end
9
+
10
+ # Write body chunks directly to the connection
11
+ def write(chunk)
12
+ chunk_header = chunk.bytesize.to_s(16)
13
+ @socket << chunk_header + CRLF
14
+ @socket << chunk + CRLF
15
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET => ex
16
+ raise Reel::SocketError, ex.to_s
17
+ end
18
+
19
+ # Finish the response and reset the response state to header
20
+ def finish_response
21
+ @socket << "0#{CRLF * 2}"
22
+ end
23
+
24
+ # Render a given response object to the network socket
25
+ def handle_response(response)
26
+ @socket << render_header(response)
27
+ return response.render(@socket) if response.respond_to?(:render)
28
+
29
+ case response.body
30
+ when String
31
+ @socket << response.body
32
+ when IO
33
+ Celluloid::IO.copy_stream(response.body, @socket)
34
+ when Enumerable
35
+ response.body.each { |chunk| write(chunk) }
36
+ finish_response
37
+ when NilClass
38
+ # Used for streaming Transfer-Encoding chunked responses
39
+ return
40
+ else
41
+ raise TypeError, "don't know how to render a #{response.body.class}"
42
+ end
43
+ response.body.close if response.body.respond_to?(:close)
44
+ end
45
+
46
+ # Convert headers into a string
47
+ def render_header(response)
48
+ response_header = "#{response.version} #{response.status} #{response.reason}#{CRLF}"
49
+ unless response.headers.empty?
50
+ response_header << response.headers.map do |header, value|
51
+ "#{header}: #{value}"
52
+ end.join(CRLF) << CRLF
53
+ end
54
+ response_header << CRLF
55
+ end
56
+ private :render_header
57
+ end
58
+ end
59
+ end
data/lib/reel/server.rb CHANGED
@@ -1,19 +1,32 @@
1
1
  module Reel
2
+ # Base class for Reel servers.
3
+ #
4
+ # This class is a Celluloid::IO actor which provides a barebones server
5
+ # which does not open a socket itself, it just begin handling connections once
6
+ # initialized with a specific kind of protocol-based server.
7
+
8
+ # For specific protocol support, use:
9
+
10
+ # Reel::Server::HTTP
11
+ # Reel::Server::HTTPS
12
+ # Coming soon: Reel::Server::UNIX
13
+
2
14
  class Server
3
15
  include Celluloid::IO
4
-
5
16
  # How many connections to backlog in the TCP accept queue
6
17
  DEFAULT_BACKLOG = 100
7
18
 
8
19
  execute_block_on_receiver :initialize
9
20
  finalizer :shutdown
10
21
 
11
- def initialize(host, port, backlog = DEFAULT_BACKLOG, &callback)
12
- # This is actually an evented Celluloid::IO::TCPServer
13
- @server = TCPServer.new(host, port)
14
- @server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
15
- @server.listen(backlog)
22
+ def initialize(server, options={}, &callback)
23
+ @spy = STDOUT if options[:spy]
24
+ @options = options
16
25
  @callback = callback
26
+ @server = server
27
+
28
+ @server.listen(options.fetch(:backlog, DEFAULT_BACKLOG))
29
+
17
30
  async.run
18
31
  end
19
32
 
@@ -25,7 +38,18 @@ module Reel
25
38
  loop { async.handle_connection @server.accept }
26
39
  end
27
40
 
41
+ def optimize(socket)
42
+ if socket.is_a? TCPSocket
43
+ socket.setsockopt(Socket::IPPROTO_TCP, :TCP_NODELAY, 1)
44
+ end
45
+ end
46
+
28
47
  def handle_connection(socket)
48
+ if @spy
49
+ require 'reel/spy'
50
+ socket = Reel::Spy.new(socket, @spy)
51
+ end
52
+
29
53
  connection = Connection.new(socket)
30
54
 
31
55
  begin
@@ -0,0 +1,20 @@
1
+ module Reel
2
+ class Server
3
+ class HTTP < Server
4
+
5
+ # Create a new Reel HTTP server
6
+ #
7
+ # @param [String] host address to bind to
8
+ # @param [Fixnum] port to bind to
9
+ # @option options [Fixnum] backlog of requests to accept
10
+ #
11
+ # @return [Reel::Server::HTTP] Reel HTTP server actor
12
+ def initialize(host, port, options={}, &callback)
13
+ optimize server = Celluloid::IO::TCPServer.new(host, port)
14
+ options.merge!(host: host, port: port)
15
+ super(server, options, &callback)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,63 @@
1
+ module Reel
2
+ class Server
3
+ class HTTPS < Server
4
+
5
+ # Create a new Reel HTTPS server
6
+ #
7
+ # @param [String] host address to bind to
8
+ # @param [Fixnum] port to bind to
9
+ # @option options [Fixnum] backlog of requests to accept
10
+ # @option options [String] :cert the server's TLS certificate
11
+ # @option options [String] :key the server's TLS key
12
+ # @option options [Array] :extra_cert_chain TLS certificate chain
13
+ #
14
+ # @return [Reel::Server::HTTPS] Reel HTTPS server actor
15
+ def initialize(host, port, options={}, &callback)
16
+
17
+ # Ideally we can encapsulate this rather than making Ruby OpenSSL a
18
+ # mandatory part of the Reel API. It would be nice to support
19
+ # alternatives (e.g. Puma's MiniSSL)
20
+ ssl_context = OpenSSL::SSL::SSLContext.new
21
+ ssl_context.cert = OpenSSL::X509::Certificate.new options.fetch(:cert)
22
+ ssl_context.key = OpenSSL::PKey::RSA.new options.fetch(:key)
23
+
24
+ ssl_context.ca_file = options[:ca_file]
25
+ ssl_context.ca_path = options[:ca_path]
26
+ ssl_context.extra_chain_cert = options[:extra_chain_cert]
27
+
28
+ # if verify_mode isn't explicitly set, verify peers if we've
29
+ # been provided CA information that would enable us to do so
30
+ ssl_context.verify_mode = case
31
+ when options.include?(:verify_mode)
32
+ options[:verify_mode]
33
+ when options.include?(:ca_file)
34
+ OpenSSL::SSL::VERIFY_PEER
35
+ when options.include?(:ca_path)
36
+ OpenSSL::SSL::VERIFY_PEER
37
+ else
38
+ OpenSSL::SSL::VERIFY_NONE
39
+ end
40
+
41
+ optimize @tcpserver = Celluloid::IO::TCPServer.new(host, port)
42
+
43
+ server = Celluloid::IO::SSLServer.new(@tcpserver, ssl_context)
44
+ options.merge!(host: host, port: port)
45
+
46
+ super(server, options, &callback)
47
+ end
48
+
49
+ def run
50
+ loop do
51
+ begin
52
+ socket = @server.accept
53
+ rescue OpenSSL::SSL::SSLError => ex
54
+ Logger.warn "Error accepting SSLSocket: #{ex.class}: #{ex.to_s}"
55
+ retry
56
+ end
57
+
58
+ async.handle_connection socket
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end