reel 0.1.0 → 0.2.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 -5
- data/CHANGES.md +6 -2
- data/README.md +19 -7
- data/bin/reel +49 -5
- data/examples/hello_world.rb +9 -3
- data/examples/websockets.rb +124 -0
- data/lib/rack/handler/reel.rb +84 -0
- data/lib/reel.rb +15 -0
- data/lib/reel/connection.rb +47 -32
- data/lib/reel/rack_worker.rb +90 -0
- data/lib/reel/request.rb +46 -8
- data/lib/reel/response.rb +8 -4
- data/lib/reel/server.rb +7 -4
- data/lib/reel/version.rb +1 -1
- data/lib/reel/websocket.rb +64 -0
- data/reel.gemspec +7 -5
- data/spec/reel/connection_spec.rb +14 -17
- data/spec/reel/rack_worker_spec.rb +67 -0
- data/spec/reel/response_spec.rb +3 -19
- data/spec/reel/server_spec.rb +28 -22
- data/spec/reel/websocket_spec.rb +92 -0
- data/spec/spec_helper.rb +24 -4
- metadata +46 -15
@@ -0,0 +1,90 @@
|
|
1
|
+
module Reel
|
2
|
+
class RackWorker
|
3
|
+
include Celluloid
|
4
|
+
|
5
|
+
PROTO_RACK_ENV = {
|
6
|
+
"rack.version".freeze => Rack::VERSION,
|
7
|
+
"rack.errors".freeze => STDERR,
|
8
|
+
"rack.multithread".freeze => true,
|
9
|
+
"rack.multiprocess".freeze => false,
|
10
|
+
"rack.run_once".freeze => false,
|
11
|
+
"rack.url_scheme".freeze => "http",
|
12
|
+
"SCRIPT_NAME".freeze => ENV['SCRIPT_NAME'] || "",
|
13
|
+
"SERVER_PROTOCOL".freeze => "HTTP/1.1",
|
14
|
+
"SERVER_SOFTWARE".freeze => "Reel/#{Reel::VERSION}",
|
15
|
+
"GATEWAY_INTERFACE".freeze => "CGI/1.1"
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def initialize(handler)
|
19
|
+
@handler, @app = handler, handler.rack_app
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle(connection)
|
23
|
+
while request = connection.request
|
24
|
+
begin
|
25
|
+
env = rack_env(request, connection)
|
26
|
+
status, headers, body_parts = @handler.rack_app.call(env)
|
27
|
+
|
28
|
+
body = if body_parts.respond_to?(:to_path)
|
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)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def rack_env(request, connection)
|
45
|
+
env = PROTO_RACK_ENV.dup
|
46
|
+
|
47
|
+
env["SERVER_NAME"] = @handler[:host]
|
48
|
+
env["SERVER_PORT"] = @handler[:port]
|
49
|
+
|
50
|
+
peer_address = connection.peer_address
|
51
|
+
|
52
|
+
env["REMOTE_ADDR"] = peer_address[3]
|
53
|
+
env["REMOTE_HOST"] = peer_address[2]
|
54
|
+
|
55
|
+
env["PATH_INFO"] = request.path
|
56
|
+
env["REQUEST_METHOD"] = request.method.to_s.upcase
|
57
|
+
|
58
|
+
body = request.body || ""
|
59
|
+
|
60
|
+
rack_input = StringIO.new(body)
|
61
|
+
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
|
62
|
+
|
63
|
+
env["rack.input"] = rack_input
|
64
|
+
env["rack.logger"] = @app if Rack::CommonLogger === @app
|
65
|
+
|
66
|
+
env["REQUEST_PATH"] = request.path
|
67
|
+
env["ORIGINAL_FULLPATH"] = request.path
|
68
|
+
|
69
|
+
query_string = request.query_string || ""
|
70
|
+
query_string += "##{request.fragment}" if request.fragment
|
71
|
+
|
72
|
+
env["QUERY_STRING"] = query_string
|
73
|
+
|
74
|
+
request.headers.each{|key, val|
|
75
|
+
next if /^content-type$/i =~ key
|
76
|
+
next if /^content-length$/i =~ key
|
77
|
+
name = "HTTP_" + key
|
78
|
+
name.gsub!(/-/o, "_")
|
79
|
+
name.upcase!
|
80
|
+
env[name] = val
|
81
|
+
}
|
82
|
+
|
83
|
+
host = env['HTTP_HOST'] || env["SERVER_NAME"]
|
84
|
+
|
85
|
+
env["REQUEST_URI"] = "#{env['rack.url_scheme']}://#{host}#{request.path}"
|
86
|
+
|
87
|
+
env
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/reel/request.rb
CHANGED
@@ -1,23 +1,61 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
1
3
|
module Reel
|
2
4
|
class Request
|
3
5
|
attr_accessor :method, :version, :url, :headers
|
4
|
-
|
5
|
-
|
6
|
+
|
7
|
+
def self.read(connection)
|
8
|
+
parser = connection.parser
|
9
|
+
|
10
|
+
begin
|
11
|
+
data = connection.socket.readpartial(Connection::BUFFER_SIZE)
|
12
|
+
parser << data
|
13
|
+
end until parser.headers
|
14
|
+
|
15
|
+
headers = {}
|
16
|
+
parser.headers.each do |field, value|
|
17
|
+
headers[Http.canonicalize_header(field)] = value
|
18
|
+
end
|
19
|
+
|
20
|
+
upgrade = headers['Upgrade']
|
21
|
+
if upgrade && upgrade.downcase == 'websocket'
|
22
|
+
WebSocket.new(connection.socket, parser.url, headers)
|
23
|
+
else
|
24
|
+
Request.new(parser.http_method, parser.url, parser.http_version, headers, connection)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
6
28
|
def initialize(method, url, version = "1.1", headers = {}, connection = nil)
|
7
29
|
@method = method.to_s.downcase.to_sym
|
8
|
-
raise UnsupportedArgumentError, "unknown method: #{method}" unless METHODS.include? @method
|
9
|
-
|
30
|
+
raise UnsupportedArgumentError, "unknown method: #{method}" unless Http::METHODS.include? @method
|
31
|
+
|
10
32
|
@url, @version, @headers, @connection = url, version, headers, connection
|
11
33
|
end
|
12
|
-
|
34
|
+
|
13
35
|
def [](header)
|
14
36
|
@headers[header]
|
15
37
|
end
|
16
|
-
|
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
|
50
|
+
|
51
|
+
def fragment
|
52
|
+
uri.fragment
|
53
|
+
end
|
54
|
+
|
17
55
|
def body
|
18
56
|
@body ||= begin
|
19
57
|
raise "no connection given" unless @connection
|
20
|
-
|
58
|
+
|
21
59
|
body = "" unless block_given?
|
22
60
|
while (chunk = @connection.readpartial)
|
23
61
|
if block_given?
|
@@ -30,4 +68,4 @@ module Reel
|
|
30
68
|
end
|
31
69
|
end
|
32
70
|
end
|
33
|
-
end
|
71
|
+
end
|
data/lib/reel/response.rb
CHANGED
@@ -36,7 +36,7 @@ module Reel
|
|
36
36
|
when Enumerable
|
37
37
|
@headers['Transfer-Encoding'] ||= 'chunked'
|
38
38
|
when NilClass
|
39
|
-
else raise
|
39
|
+
else raise TypeError, "can't render #{@body.class} as a response body"
|
40
40
|
end
|
41
41
|
|
42
42
|
# Prevent modification through the accessor
|
@@ -71,9 +71,13 @@ module Reel
|
|
71
71
|
when String
|
72
72
|
socket << @body
|
73
73
|
when IO
|
74
|
-
|
75
|
-
|
76
|
-
|
74
|
+
if !defined?(JRUBY_VERSION)
|
75
|
+
IO.copy_stream(@body, socket)
|
76
|
+
else
|
77
|
+
# JRuby 1.6.7 doesn't support IO.copy_stream :(
|
78
|
+
while data = @body.read(4096)
|
79
|
+
socket << data
|
80
|
+
end
|
77
81
|
end
|
78
82
|
when Enumerable
|
79
83
|
@body.each do |chunk|
|
data/lib/reel/server.rb
CHANGED
@@ -20,12 +20,15 @@ module Reel
|
|
20
20
|
def handle_connection(socket)
|
21
21
|
connection = Connection.new(socket)
|
22
22
|
begin
|
23
|
-
connection.read_request
|
24
23
|
@callback[connection]
|
25
|
-
|
26
|
-
|
24
|
+
ensure
|
25
|
+
if connection.attached?
|
26
|
+
connection.close rescue nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
rescue RequestError, EOFError
|
27
30
|
# Client disconnected prematurely
|
28
|
-
#
|
31
|
+
# TODO: log this?
|
29
32
|
end
|
30
33
|
end
|
31
34
|
end
|
data/lib/reel/version.rb
CHANGED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'websocket_parser'
|
2
|
+
|
3
|
+
module Reel
|
4
|
+
class WebSocket
|
5
|
+
attr_reader :url, :headers
|
6
|
+
|
7
|
+
def initialize(socket, url, headers)
|
8
|
+
@socket, @url, @headers = socket, url, headers
|
9
|
+
|
10
|
+
handshake = ::WebSocket::ClientHandshake.new(:get, url, headers)
|
11
|
+
|
12
|
+
if handshake.valid?
|
13
|
+
response = handshake.accept_response
|
14
|
+
response.render(socket)
|
15
|
+
else
|
16
|
+
error = handshake.errors.first
|
17
|
+
|
18
|
+
response = Response.new(400)
|
19
|
+
response.reason = handshake.errors.first
|
20
|
+
response.render(@socket)
|
21
|
+
|
22
|
+
raise HandshakeError, "error during handshake: #{error}"
|
23
|
+
end
|
24
|
+
|
25
|
+
@parser = ::WebSocket::Parser.new
|
26
|
+
|
27
|
+
@parser.on_close do |status, reason|
|
28
|
+
# According to the spec the server must respond with another
|
29
|
+
# close message before closing the connection
|
30
|
+
@socket << ::WebSocket::Message.close.to_data
|
31
|
+
close
|
32
|
+
end
|
33
|
+
|
34
|
+
@parser.on_ping do
|
35
|
+
@socket << ::WebSocket::Message.pong.to_data
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](header)
|
40
|
+
@headers[header]
|
41
|
+
end
|
42
|
+
|
43
|
+
def read
|
44
|
+
@parser.append @socket.readpartial(Connection::BUFFER_SIZE) until msg = @parser.next_message
|
45
|
+
msg
|
46
|
+
end
|
47
|
+
|
48
|
+
def write(msg)
|
49
|
+
@socket << ::WebSocket::Message.new(msg).to_data
|
50
|
+
msg
|
51
|
+
rescue Errno::EPIPE
|
52
|
+
raise SocketError, "error writing to socket"
|
53
|
+
end
|
54
|
+
alias_method :<<, :write
|
55
|
+
|
56
|
+
def closed?
|
57
|
+
@socket.closed?
|
58
|
+
end
|
59
|
+
|
60
|
+
def close
|
61
|
+
@socket.close
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/reel.gemspec
CHANGED
@@ -14,11 +14,13 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.name = "reel"
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Reel::VERSION
|
17
|
-
|
18
|
-
gem.
|
19
|
-
gem.
|
20
|
-
gem.
|
21
|
-
|
17
|
+
|
18
|
+
gem.add_runtime_dependency 'celluloid-io', '>= 0.8.0'
|
19
|
+
gem.add_runtime_dependency 'http', '>= 0.2.0'
|
20
|
+
gem.add_runtime_dependency 'http_parser.rb', '>= 0.5.3'
|
21
|
+
gem.add_runtime_dependency 'websocket_parser', '>= 0.1.0'
|
22
|
+
gem.add_runtime_dependency 'rack', '>= 1.4.0'
|
23
|
+
|
22
24
|
gem.add_development_dependency 'rake'
|
23
25
|
gem.add_development_dependency 'rspec'
|
24
26
|
end
|
@@ -6,7 +6,7 @@ describe Reel::Connection do
|
|
6
6
|
it "reads requests without bodies" do
|
7
7
|
with_socket_pair do |client, connection|
|
8
8
|
client << ExampleRequest.new.to_s
|
9
|
-
request = connection.
|
9
|
+
request = connection.request
|
10
10
|
|
11
11
|
request.url.should eq "/"
|
12
12
|
request.version.should eq "1.1"
|
@@ -28,7 +28,7 @@ describe Reel::Connection do
|
|
28
28
|
example_request.body = body
|
29
29
|
|
30
30
|
client << example_request.to_s
|
31
|
-
request = connection.
|
31
|
+
request = connection.request
|
32
32
|
|
33
33
|
request.url.should eq "/"
|
34
34
|
request.version.should eq "1.1"
|
@@ -40,14 +40,15 @@ describe Reel::Connection do
|
|
40
40
|
it "serves static files" do
|
41
41
|
with_socket_pair do |client, connection|
|
42
42
|
client << ExampleRequest.new.to_s
|
43
|
-
request = connection.
|
43
|
+
request = connection.request
|
44
44
|
|
45
45
|
fixture_text = File.read(fixture_path)
|
46
46
|
File.open(fixture_path) do |file|
|
47
47
|
connection.respond :ok, file
|
48
|
+
connection.close
|
48
49
|
end
|
49
50
|
|
50
|
-
response = client.
|
51
|
+
response = client.read(4096)
|
51
52
|
response[(response.length - fixture_text.length)..-1].should eq fixture_text
|
52
53
|
end
|
53
54
|
end
|
@@ -55,7 +56,7 @@ describe Reel::Connection do
|
|
55
56
|
it "streams responses when transfer-encoding is chunked" do
|
56
57
|
with_socket_pair do |client, connection|
|
57
58
|
client << ExampleRequest.new.to_s
|
58
|
-
request = connection.
|
59
|
+
request = connection.request
|
59
60
|
|
60
61
|
# Sending transfer_encoding chunked without a body enables streaming mode
|
61
62
|
connection.respond :ok, :transfer_encoding => :chunked
|
@@ -81,20 +82,16 @@ describe Reel::Connection do
|
|
81
82
|
end
|
82
83
|
end
|
83
84
|
|
84
|
-
|
85
|
-
|
86
|
-
|
85
|
+
it "reset the request after a response is sent" do
|
86
|
+
with_socket_pair do |client, connection|
|
87
|
+
example_request = ExampleRequest.new(:get, "/", "1.1", {'Connection' => 'close'})
|
88
|
+
client << example_request
|
89
|
+
|
90
|
+
connection.request.should_not be_false
|
87
91
|
|
88
|
-
|
89
|
-
client = TCPSocket.new(host, port)
|
90
|
-
peer = server.accept
|
92
|
+
connection.respond :ok, "Response sent"
|
91
93
|
|
92
|
-
|
93
|
-
yield client, Reel::Connection.new(peer)
|
94
|
-
ensure
|
95
|
-
server.close rescue nil
|
96
|
-
client.close rescue nil
|
97
|
-
peer.close rescue nil
|
94
|
+
connection.request.should be_false
|
98
95
|
end
|
99
96
|
end
|
100
97
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Reel::RackWorker do
|
4
|
+
|
5
|
+
let(:endpoint) { URI("http://#{example_addr}:#{example_port}#{example_url}") }
|
6
|
+
|
7
|
+
let(:worker) do
|
8
|
+
app = Proc.new do |env|
|
9
|
+
[200, {'Content-Type' => 'text/plain'}, ['Hello world!']]
|
10
|
+
[200, {'Content-Type' => 'text/plain'}, ['Hello rack world!']]
|
11
|
+
end
|
12
|
+
|
13
|
+
handler = Rack::Handler::Reel.new
|
14
|
+
handler.options[:app] = app
|
15
|
+
|
16
|
+
Reel::RackWorker.new(handler)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "creates a rack env from a request" do
|
20
|
+
with_socket_pair do |client, connection|
|
21
|
+
client << ExampleRequest.new(:get, '/test?hello=true').to_s
|
22
|
+
request = connection.request
|
23
|
+
env = worker.rack_env(request, connection)
|
24
|
+
|
25
|
+
Reel::RackWorker::PROTO_RACK_ENV.each do |k, v|
|
26
|
+
env[k].should == v
|
27
|
+
end
|
28
|
+
|
29
|
+
env["SERVER_NAME"].should == '0.0.0.0'
|
30
|
+
env["SERVER_PORT"].should == 3000
|
31
|
+
env["REMOTE_ADDR"].should == "127.0.0.1"
|
32
|
+
env["REMOTE_HOST"].should == "127.0.0.1"
|
33
|
+
env["PATH_INFO"].should == "/test"
|
34
|
+
env["REQUEST_METHOD"].should == "GET"
|
35
|
+
env["REQUEST_PATH"].should == "/test"
|
36
|
+
env["ORIGINAL_FULLPATH"].should == "/test"
|
37
|
+
env["QUERY_STRING"].should == "hello=true"
|
38
|
+
env["HTTP_HOST"].should == 'www.example.com'
|
39
|
+
env["HTTP_ACCEPT_LANGUAGE"].should == "en-US,en;q=0.8"
|
40
|
+
env["REQUEST_URI"].should == 'http://www.example.com/test'
|
41
|
+
|
42
|
+
env["rack.input"].should be_kind_of(StringIO)
|
43
|
+
env["rack.input"].string.should == ''
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "delegates web requests to the rack app" do
|
48
|
+
ex = nil
|
49
|
+
|
50
|
+
handler = proc do |connection|
|
51
|
+
begin
|
52
|
+
worker.handle!(connection.detach)
|
53
|
+
rescue => ex
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
with_reel(handler) do
|
58
|
+
http = Net::HTTP.new(endpoint.host, endpoint.port)
|
59
|
+
request = Net::HTTP::Get.new(endpoint.request_uri)
|
60
|
+
response = http.request(request)
|
61
|
+
response.should be_a Net::HTTPOK
|
62
|
+
response.body.should == 'Hello rack world!'
|
63
|
+
end
|
64
|
+
|
65
|
+
raise ex if ex
|
66
|
+
end
|
67
|
+
end
|
data/spec/reel/response_spec.rb
CHANGED
@@ -4,31 +4,15 @@ describe Reel::Response do
|
|
4
4
|
it "streams enumerables" do
|
5
5
|
with_socket_pair do |client, connection|
|
6
6
|
client << ExampleRequest.new.to_s
|
7
|
-
request = connection.
|
7
|
+
request = connection.request
|
8
8
|
|
9
9
|
connection.respond Reel::Response.new(:ok, ["Hello", "World"])
|
10
|
+
connection.close
|
10
11
|
|
11
|
-
response = client.
|
12
|
+
response = client.read(4096)
|
12
13
|
crlf = "\r\n"
|
13
14
|
fixture = "5#{crlf}Hello5#{crlf}World0#{crlf*2}"
|
14
15
|
response[(response.length - fixture.length)..-1].should eq fixture
|
15
16
|
end
|
16
17
|
end
|
17
|
-
|
18
|
-
def with_socket_pair
|
19
|
-
host = '127.0.0.1'
|
20
|
-
port = 10103
|
21
|
-
|
22
|
-
server = TCPServer.new(host, port)
|
23
|
-
client = TCPSocket.new(host, port)
|
24
|
-
peer = server.accept
|
25
|
-
|
26
|
-
begin
|
27
|
-
yield client, Reel::Connection.new(peer)
|
28
|
-
ensure
|
29
|
-
server.close
|
30
|
-
client.close
|
31
|
-
peer.close
|
32
|
-
end
|
33
|
-
end
|
34
18
|
end
|