reel 0.2.0 → 0.3.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.
- data/.travis.yml +2 -3
- data/CHANGES.md +12 -2
- data/README.md +105 -19
- data/bin/reel +2 -1
- data/examples/chunked.rb +25 -0
- data/examples/hello_world.rb +4 -1
- data/examples/roundtrip.rb +157 -0
- data/examples/server-sent-events.rb +48 -0
- data/examples/stream.rb +26 -0
- data/examples/websocket-wall.rb +53 -0
- data/examples/websocket.ru +92 -0
- data/examples/websocket_rack.sh +1 -0
- data/examples/websockets.rb +1 -3
- data/lib/rack/handler/reel.rb +7 -5
- data/lib/reel.rb +6 -0
- data/lib/reel/connection.rb +43 -34
- data/lib/reel/mixins.rb +57 -0
- data/lib/reel/rack_worker.rb +103 -59
- data/lib/reel/request.rb +19 -34
- data/lib/reel/request_parser.rb +12 -2
- data/lib/reel/response.rb +36 -30
- data/lib/reel/server.rb +11 -7
- data/lib/reel/stream.rb +128 -0
- data/lib/reel/version.rb +1 -1
- data/lib/reel/websocket.rb +44 -8
- data/spec/reel/connection_spec.rb +89 -0
- data/spec/reel/rack_worker_spec.rb +28 -16
- data/spec/reel/response_spec.rb +2 -2
- data/spec/reel/server_spec.rb +2 -2
- data/spec/reel/websocket_spec.rb +2 -17
- data/spec/spec_helper.rb +26 -2
- metadata +74 -22
data/lib/reel/rack_worker.rb
CHANGED
@@ -1,18 +1,57 @@
|
|
1
1
|
module Reel
|
2
2
|
class RackWorker
|
3
3
|
include Celluloid
|
4
|
+
include Celluloid::Logger
|
5
|
+
|
6
|
+
INITIAL_BODY = ''
|
7
|
+
|
8
|
+
# Freeze some HTTP header names & values
|
9
|
+
CONTENT_TYPE_ORIG = 'Content-Type'.freeze
|
10
|
+
CONTENT_LENGTH_ORIG = 'Content-Length'.freeze
|
11
|
+
CONTENT_TYPE = 'CONTENT_TYPE'.freeze
|
12
|
+
CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
|
13
|
+
|
14
|
+
SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
|
15
|
+
SERVER_NAME = 'SERVER_NAME'.freeze
|
16
|
+
SERVER_PORT = 'SERVER_PORT'.freeze
|
17
|
+
SERVER_PROTOCOL = 'SERVER_PROTOCOL'.freeze
|
18
|
+
GATEWAY_INTERFACE = "GATEWAY_INTERFACE".freeze
|
19
|
+
LOCALHOST = 'localhost'.freeze
|
20
|
+
HTTP_VERSION = 'HTTP_VERSION'.freeze
|
21
|
+
CGI_1_1 = 'CGI/1.1'.freeze
|
22
|
+
REMOTE_ADDR = 'REMOTE_ADDR'.freeze
|
23
|
+
CONNECTION = 'HTTP_CONNECTION'.freeze
|
24
|
+
SCRIPT_NAME = 'SCRIPT_NAME'.freeze
|
25
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
26
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
27
|
+
QUERY_STRING = 'QUERY_STRING'.freeze
|
28
|
+
HTTP_1_0 = 'HTTP/1.0'.freeze
|
29
|
+
HTTP_1_1 = 'HTTP/1.1'.freeze
|
30
|
+
HTTP_ = 'HTTP_'.freeze
|
31
|
+
HOST = 'Host'.freeze
|
32
|
+
|
33
|
+
# Freeze some Rack header names
|
34
|
+
RACK_INPUT = 'rack.input'.freeze
|
35
|
+
RACK_LOGGER = 'rack.logger'.freeze
|
36
|
+
RACK_VERSION = 'rack.version'.freeze
|
37
|
+
RACK_ERRORS = 'rack.errors'.freeze
|
38
|
+
RACK_MULTITHREAD = 'rack.multithread'.freeze
|
39
|
+
RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
|
40
|
+
RACK_RUN_ONCE = 'rack.run_once'.freeze
|
41
|
+
RACK_URL_SCHEME = 'rack.url_scheme'.freeze
|
42
|
+
RACK_WEBSOCKET = 'rack.websocket'.freeze
|
4
43
|
|
5
44
|
PROTO_RACK_ENV = {
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
45
|
+
RACK_VERSION => ::Rack::VERSION,
|
46
|
+
RACK_ERRORS => STDERR,
|
47
|
+
RACK_MULTITHREAD => true,
|
48
|
+
RACK_MULTIPROCESS => false,
|
49
|
+
RACK_RUN_ONCE => false,
|
50
|
+
RACK_URL_SCHEME => "http".freeze,
|
51
|
+
SCRIPT_NAME => ENV[SCRIPT_NAME] || "",
|
52
|
+
SERVER_PROTOCOL => HTTP_1_1,
|
53
|
+
SERVER_SOFTWARE => "Reel/#{Reel::VERSION}".freeze,
|
54
|
+
GATEWAY_INTERFACE => CGI_1_1
|
16
55
|
}.freeze
|
17
56
|
|
18
57
|
def initialize(handler)
|
@@ -21,67 +60,72 @@ module Reel
|
|
21
60
|
|
22
61
|
def handle(connection)
|
23
62
|
while request = connection.request
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
File.new(body_parts.to_path)
|
30
|
-
else
|
31
|
-
body_text = ""
|
32
|
-
body_parts.each { |part| body_text += part }
|
33
|
-
body_text
|
34
|
-
end
|
35
|
-
|
36
|
-
connection.respond Response.new(status, headers, body)
|
37
|
-
ensure
|
38
|
-
body.close if body.respond_to?(:close)
|
39
|
-
body_parts.close if body_parts.respond_to?(:close)
|
63
|
+
case request
|
64
|
+
when Request
|
65
|
+
handle_request(request, connection)
|
66
|
+
when WebSocket
|
67
|
+
handle_websocket(request, connection)
|
40
68
|
end
|
41
69
|
end
|
42
70
|
end
|
43
71
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
env["REMOTE_ADDR"] = connection.remote_ip
|
51
|
-
env["REMOTE_HOST"] = connection.remote_host
|
52
|
-
|
53
|
-
env["PATH_INFO"] = request.path
|
54
|
-
env["REQUEST_METHOD"] = request.method.to_s.upcase
|
55
|
-
|
56
|
-
body = request.body || ""
|
57
|
-
|
58
|
-
rack_input = StringIO.new(body)
|
59
|
-
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
|
72
|
+
def handle_request(request, connection)
|
73
|
+
status, headers, body_parts = @app.call(request_env(request, connection))
|
74
|
+
body, is_stream = response_body(body_parts)
|
75
|
+
connection.respond (is_stream ? StreamResponse : Response).new(status, headers, body)
|
76
|
+
end
|
60
77
|
|
61
|
-
|
62
|
-
|
78
|
+
def handle_websocket(request, connection)
|
79
|
+
status, *rest = @app.call(websocket_env(request))
|
80
|
+
request.close unless status < 300
|
81
|
+
end
|
63
82
|
|
64
|
-
|
65
|
-
env
|
83
|
+
def request_env request, connection
|
84
|
+
env = env(request)
|
85
|
+
env[REMOTE_ADDR] = connection.remote_ip
|
86
|
+
env
|
87
|
+
end
|
66
88
|
|
67
|
-
|
68
|
-
|
89
|
+
def websocket_env request
|
90
|
+
env = env(request)
|
91
|
+
env[REMOTE_ADDR] = request.remote_ip
|
92
|
+
env[RACK_WEBSOCKET] = request
|
93
|
+
env
|
94
|
+
end
|
69
95
|
|
70
|
-
|
96
|
+
def response_body(body_parts)
|
97
|
+
if body_parts.respond_to?(:to_path)
|
98
|
+
::File.new(body_parts.to_path)
|
99
|
+
else
|
100
|
+
body = ''
|
101
|
+
body_parts.each do |c|
|
102
|
+
return [c, true] if c.is_a?(Reel::Stream)
|
103
|
+
body << c
|
104
|
+
end
|
105
|
+
body_parts.close if body_parts.respond_to?(:close)
|
106
|
+
body
|
107
|
+
end
|
108
|
+
end
|
71
109
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
name = "HTTP_" + key
|
76
|
-
name.gsub!(/-/o, "_")
|
77
|
-
name.upcase!
|
78
|
-
env[name] = val
|
79
|
-
}
|
110
|
+
private
|
111
|
+
def env request
|
112
|
+
env = Hash[PROTO_RACK_ENV]
|
80
113
|
|
81
|
-
|
114
|
+
env[RACK_INPUT] = StringIO.new(request.body || INITIAL_BODY)
|
115
|
+
env[RACK_INPUT].set_encoding(Encoding::BINARY) if env[RACK_INPUT].respond_to?(:set_encoding)
|
116
|
+
env[SERVER_NAME], env[SERVER_PORT] = (request[HOST]||'').split(':', 2)
|
117
|
+
env[SERVER_PORT] ||= @handler[:port].to_s
|
118
|
+
env[HTTP_VERSION] = request.version || env[SERVER_PROTOCOL]
|
119
|
+
env[REQUEST_METHOD] = request.method
|
120
|
+
env[PATH_INFO] = request.path
|
121
|
+
env[QUERY_STRING] = request.query_string || ''
|
82
122
|
|
83
|
-
env[
|
123
|
+
(_ = request.headers.delete CONTENT_TYPE_ORIG) && (env[CONTENT_TYPE] = _)
|
124
|
+
(_ = request.headers.delete CONTENT_LENGTH_ORIG) && (env[CONTENT_LENGTH] = _)
|
84
125
|
|
126
|
+
request.headers.each_pair do |key, val|
|
127
|
+
env[HTTP_ + key.gsub('-', '_').upcase] = val
|
128
|
+
end
|
85
129
|
env
|
86
130
|
end
|
87
131
|
end
|
data/lib/reel/request.rb
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
-
require '
|
1
|
+
require 'forwardable'
|
2
2
|
|
3
3
|
module Reel
|
4
4
|
class Request
|
5
|
-
|
5
|
+
extend Forwardable
|
6
|
+
include RequestMixin
|
7
|
+
|
8
|
+
UPGRADE = 'Upgrade'.freeze
|
9
|
+
WEBSOCKET = 'websocket'.freeze
|
10
|
+
|
11
|
+
# Array#include? seems slow compared to Hash lookup
|
12
|
+
request_methods = Http::METHODS.map { |m| m.to_s.upcase }
|
13
|
+
REQUEST_METHODS = Hash[request_methods.zip(request_methods)].freeze
|
6
14
|
|
7
15
|
def self.read(connection)
|
8
16
|
parser = connection.parser
|
@@ -12,44 +20,21 @@ module Reel
|
|
12
20
|
parser << data
|
13
21
|
end until parser.headers
|
14
22
|
|
15
|
-
|
16
|
-
|
17
|
-
headers[Http.canonicalize_header(field)] = value
|
18
|
-
end
|
23
|
+
REQUEST_METHODS[parser.http_method] ||
|
24
|
+
raise(ArgumentError, "Unknown Request Method: %s" % parser.http_method)
|
19
25
|
|
20
|
-
upgrade = headers[
|
21
|
-
if upgrade && upgrade.downcase ==
|
22
|
-
WebSocket.new(connection.socket
|
26
|
+
upgrade = parser.headers[UPGRADE]
|
27
|
+
if upgrade && upgrade.downcase == WEBSOCKET
|
28
|
+
WebSocket.new(parser, connection.socket)
|
23
29
|
else
|
24
|
-
Request.new(parser
|
30
|
+
Request.new(parser, connection)
|
25
31
|
end
|
26
32
|
end
|
27
33
|
|
28
|
-
|
29
|
-
@method = method.to_s.downcase.to_sym
|
30
|
-
raise UnsupportedArgumentError, "unknown method: #{method}" unless Http::METHODS.include? @method
|
31
|
-
|
32
|
-
@url, @version, @headers, @connection = url, version, headers, connection
|
33
|
-
end
|
34
|
-
|
35
|
-
def [](header)
|
36
|
-
@headers[header]
|
37
|
-
end
|
38
|
-
|
39
|
-
def uri
|
40
|
-
@uri ||= URI(url)
|
41
|
-
end
|
42
|
-
|
43
|
-
def path
|
44
|
-
uri.path
|
45
|
-
end
|
46
|
-
|
47
|
-
def query_string
|
48
|
-
uri.query
|
49
|
-
end
|
34
|
+
def_delegators :@connection, :respond, :finish_response, :close, :read
|
50
35
|
|
51
|
-
def
|
52
|
-
|
36
|
+
def initialize(http_parser, connection = nil)
|
37
|
+
@http_parser, @connection = http_parser, connection
|
53
38
|
end
|
54
39
|
|
55
40
|
def body
|
data/lib/reel/request_parser.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
module Reel
|
2
2
|
class Request
|
3
3
|
class Parser
|
4
|
+
include HTTPVersionsMixin
|
4
5
|
attr_reader :headers
|
5
6
|
|
6
7
|
def initialize
|
@@ -8,6 +9,12 @@ module Reel
|
|
8
9
|
reset
|
9
10
|
end
|
10
11
|
|
12
|
+
[:request_path, :query_string].each do |m|
|
13
|
+
define_method m do
|
14
|
+
@parser.send m
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
11
18
|
def add(data)
|
12
19
|
@parser << data
|
13
20
|
end
|
@@ -18,17 +25,20 @@ module Reel
|
|
18
25
|
end
|
19
26
|
|
20
27
|
def http_method
|
21
|
-
@parser.http_method
|
28
|
+
@parser.http_method
|
22
29
|
end
|
23
30
|
|
24
31
|
def http_version
|
25
|
-
|
32
|
+
# TODO: add extra HTTP_VERSION handler when HTTP/1.2 released
|
33
|
+
@parser.http_version[1] == 1 ? HTTP_VERSION_1_1 : HTTP_VERSION_1_0
|
26
34
|
end
|
27
35
|
|
28
36
|
def url
|
29
37
|
@parser.request_url
|
30
38
|
end
|
31
39
|
|
40
|
+
def finished?; @finished; end
|
41
|
+
|
32
42
|
#
|
33
43
|
# Http::Parser callbacks
|
34
44
|
#
|
data/lib/reel/response.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
module Reel
|
2
2
|
class Response
|
3
|
+
|
4
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
5
|
+
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
6
|
+
CHUNKED = 'chunked'.freeze
|
7
|
+
|
3
8
|
# Use status code tables from the Http gem
|
4
9
|
STATUS_CODES = Http::Response::STATUS_CODES
|
5
10
|
SYMBOL_TO_STATUS_CODE = Http::Response::SYMBOL_TO_STATUS_CODE
|
@@ -20,30 +25,19 @@ module Reel
|
|
20
25
|
@body = body_or_headers
|
21
26
|
end
|
22
27
|
|
23
|
-
@headers = {}
|
24
|
-
headers.each do |name, value|
|
25
|
-
name = name.to_s
|
26
|
-
key = name[Http::CANONICAL_HEADER]
|
27
|
-
key ||= canonicalize_header(name)
|
28
|
-
@headers[key] = value.to_s
|
29
|
-
end
|
30
|
-
|
31
28
|
case @body
|
32
29
|
when String
|
33
|
-
|
30
|
+
headers[CONTENT_LENGTH] ||= @body.bytesize
|
34
31
|
when IO
|
35
|
-
|
32
|
+
headers[CONTENT_LENGTH] ||= @body.stat.size
|
36
33
|
when Enumerable
|
37
|
-
|
34
|
+
headers[TRANSFER_ENCODING] ||= CHUNKED
|
38
35
|
when NilClass
|
39
36
|
else raise TypeError, "can't render #{@body.class} as a response body"
|
40
37
|
end
|
41
38
|
|
42
|
-
|
43
|
-
@
|
44
|
-
|
45
|
-
# FIXME: real HTTP versioning
|
46
|
-
@version = "HTTP/1.1"
|
39
|
+
@headers = canonicalize_headers(headers)
|
40
|
+
@version = http_version
|
47
41
|
end
|
48
42
|
|
49
43
|
# Set the status
|
@@ -71,22 +65,26 @@ module Reel
|
|
71
65
|
when String
|
72
66
|
socket << @body
|
73
67
|
when IO
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
68
|
+
begin
|
69
|
+
if !defined?(JRUBY_VERSION)
|
70
|
+
IO.copy_stream(@body, socket)
|
71
|
+
else
|
72
|
+
# JRuby 1.6.7 doesn't support IO.copy_stream :(
|
73
|
+
while data = @body.read(4096)
|
74
|
+
socket << data
|
75
|
+
end
|
80
76
|
end
|
77
|
+
ensure
|
78
|
+
@body.close
|
81
79
|
end
|
82
80
|
when Enumerable
|
83
81
|
@body.each do |chunk|
|
84
|
-
chunk_header = chunk.bytesize.to_s(16)
|
85
|
-
socket << chunk_header
|
86
|
-
socket << chunk
|
82
|
+
chunk_header = chunk.bytesize.to_s(16)
|
83
|
+
socket << chunk_header + CRLF
|
84
|
+
socket << chunk + CRLF
|
87
85
|
end
|
88
86
|
|
89
|
-
socket << "0
|
87
|
+
socket << "0#{CRLF * 2}"
|
90
88
|
end
|
91
89
|
end
|
92
90
|
|
@@ -105,10 +103,18 @@ module Reel
|
|
105
103
|
end
|
106
104
|
private :render_header
|
107
105
|
|
108
|
-
|
109
|
-
|
110
|
-
|
106
|
+
def canonicalize_headers headers
|
107
|
+
headers.inject({}) do |headers, (header, value)|
|
108
|
+
headers.merge Http.canonicalize_header(header) => value.to_s
|
109
|
+
end.freeze
|
111
110
|
end
|
112
|
-
private :
|
111
|
+
private :canonicalize_headers
|
112
|
+
|
113
|
+
def http_version
|
114
|
+
# FIXME: real HTTP versioning
|
115
|
+
"HTTP/1.1".freeze
|
116
|
+
end
|
117
|
+
private :http_version
|
118
|
+
|
113
119
|
end
|
114
120
|
end
|
data/lib/reel/server.rb
CHANGED
@@ -1,22 +1,26 @@
|
|
1
1
|
module Reel
|
2
2
|
class Server
|
3
3
|
include Celluloid::IO
|
4
|
-
|
4
|
+
|
5
|
+
# FIXME: remove respond_to? check after Celluloid 1.0
|
6
|
+
finalizer :finalize if respond_to?(:finalizer)
|
7
|
+
|
5
8
|
def initialize(host, port, &callback)
|
6
9
|
# This is actually an evented Celluloid::IO::TCPServer
|
7
10
|
@server = TCPServer.new(host, port)
|
11
|
+
@server.listen(1024)
|
8
12
|
@callback = callback
|
9
|
-
run
|
13
|
+
async.run
|
10
14
|
end
|
11
|
-
|
15
|
+
|
12
16
|
def finalize
|
13
17
|
@server.close
|
14
18
|
end
|
15
|
-
|
19
|
+
|
16
20
|
def run
|
17
|
-
loop { handle_connection
|
21
|
+
loop { async.handle_connection @server.accept }
|
18
22
|
end
|
19
|
-
|
23
|
+
|
20
24
|
def handle_connection(socket)
|
21
25
|
connection = Connection.new(socket)
|
22
26
|
begin
|
@@ -31,4 +35,4 @@ module Reel
|
|
31
35
|
# TODO: log this?
|
32
36
|
end
|
33
37
|
end
|
34
|
-
end
|
38
|
+
end
|