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.
- checksums.yaml +4 -4
- data/.travis.yml +5 -2
- data/Gemfile +6 -0
- data/README.md +26 -16
- data/benchmarks/hello_reel.rb +1 -1
- data/benchmarks/reel_pool.rb +27 -0
- data/examples/hello_world.rb +2 -2
- data/examples/{ssl_hello_world.rb → https_hello_world.rb} +1 -1
- data/examples/roundtrip.rb +1 -1
- data/examples/server_sent_events.rb +22 -18
- data/examples/spy_hello_world.rb +22 -0
- data/examples/websockets.rb +3 -3
- data/lib/reel.rb +6 -9
- data/lib/reel/connection.rb +40 -42
- data/lib/reel/mixins.rb +3 -1
- data/lib/reel/request.rb +40 -9
- data/lib/reel/request/body.rb +65 -0
- data/lib/reel/request/info.rb +21 -0
- data/lib/reel/{request_parser.rb → request/parser.rb} +2 -2
- data/lib/reel/request/state_machine.rb +26 -0
- data/lib/reel/response.rb +4 -11
- data/lib/reel/response/writer.rb +59 -0
- data/lib/reel/server.rb +30 -6
- data/lib/reel/server/http.rb +20 -0
- data/lib/reel/server/https.rb +63 -0
- data/lib/reel/spy.rb +71 -0
- data/lib/reel/stream.rb +2 -2
- data/lib/reel/version.rb +2 -2
- data/lib/reel/websocket.rb +1 -1
- data/reel.gemspec +3 -3
- data/spec/fixtures/ca.crt +27 -0
- data/spec/fixtures/ca.key +27 -0
- data/spec/fixtures/client.crt +81 -20
- data/spec/fixtures/client.unsigned.crt +22 -0
- data/spec/fixtures/server.crt +80 -20
- data/spec/reel/connection_spec.rb +50 -11
- data/spec/reel/{server_spec.rb → http_server_spec.rb} +1 -1
- data/spec/reel/https_server_spec.rb +119 -0
- data/spec/reel/{response_writer_spec.rb → response/writer_spec.rb} +10 -2
- data/spec/reel/websocket_spec.rb +2 -2
- data/spec/spec_helper.rb +3 -34
- data/spec/support/example_request.rb +34 -0
- metadata +56 -43
- data/examples/chunked.rb +0 -25
- data/lib/reel/request_body.rb +0 -56
- data/lib/reel/request_info.rb +0 -19
- data/lib/reel/response_writer.rb +0 -76
- data/lib/reel/ssl_server.rb +0 -41
- 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 []
|
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,
|
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
|
17
|
-
@connection
|
18
|
-
@finished
|
19
|
-
@buffer
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@
|
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 :
|
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 =
|
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 =
|
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 =
|
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(
|
12
|
-
|
13
|
-
@
|
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
|