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.

@@ -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
- METHODS = [:get, :head, :post, :put, :delete, :trace, :options, :connect, :patch]
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 ArgumentError, "can't render #{@body.class} as a response body"
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
- # TODO: IO.copy_stream when it works cross-platform
75
- while data = @body.read(4096)
76
- socket << data
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
- end while connection.alive?
26
- rescue EOFError
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
- # FIXME: should probably do something here
31
+ # TODO: log this?
29
32
  end
30
33
  end
31
34
  end
data/lib/reel/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Reel
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0.pre"
3
3
  end
@@ -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.add_dependency 'celluloid-io', '>= 0.8.0'
19
- gem.add_dependency 'http', '>= 0.2.0'
20
- gem.add_dependency 'http_parser.rb', '>= 0.5.3'
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.read_request
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.read_request
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.read_request
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.readpartial(4096)
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.read_request
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
- def with_socket_pair
85
- host = '127.0.0.1'
86
- port = 10103
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
- server = TCPServer.new(host, port)
89
- client = TCPSocket.new(host, port)
90
- peer = server.accept
92
+ connection.respond :ok, "Response sent"
91
93
 
92
- begin
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
@@ -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.read_request
7
+ request = connection.request
8
8
 
9
9
  connection.respond Reel::Response.new(:ok, ["Hello", "World"])
10
+ connection.close
10
11
 
11
- response = client.readpartial(4096)
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