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.

@@ -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
- "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"
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
- 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)
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 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
- 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
- env["rack.input"] = rack_input
62
- env["rack.logger"] = @app if Rack::CommonLogger === @app
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
- env["REQUEST_PATH"] = request.path
65
- env["ORIGINAL_FULLPATH"] = request.path
83
+ def request_env request, connection
84
+ env = env(request)
85
+ env[REMOTE_ADDR] = connection.remote_ip
86
+ env
87
+ end
66
88
 
67
- query_string = request.query_string || ""
68
- query_string += "##{request.fragment}" if request.fragment
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
- env["QUERY_STRING"] = query_string
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
- request.headers.each{|key, val|
73
- next if /^content-type$/i =~ key
74
- next if /^content-length$/i =~ key
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
- host = env['HTTP_HOST'] || env["SERVER_NAME"]
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["REQUEST_URI"] = "#{env['rack.url_scheme']}://#{host}#{request.path}"
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 'uri'
1
+ require 'forwardable'
2
2
 
3
3
  module Reel
4
4
  class Request
5
- attr_accessor :method, :version, :url, :headers
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
- headers = {}
16
- parser.headers.each do |field, value|
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['Upgrade']
21
- if upgrade && upgrade.downcase == 'websocket'
22
- WebSocket.new(connection.socket, parser.url, headers)
26
+ upgrade = parser.headers[UPGRADE]
27
+ if upgrade && upgrade.downcase == WEBSOCKET
28
+ WebSocket.new(parser, connection.socket)
23
29
  else
24
- Request.new(parser.http_method, parser.url, parser.http_version, headers, connection)
30
+ Request.new(parser, connection)
25
31
  end
26
32
  end
27
33
 
28
- def initialize(method, url, version = "1.1", headers = {}, connection = nil)
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 fragment
52
- uri.fragment
36
+ def initialize(http_parser, connection = nil)
37
+ @http_parser, @connection = http_parser, connection
53
38
  end
54
39
 
55
40
  def body
@@ -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.downcase.to_sym
28
+ @parser.http_method
22
29
  end
23
30
 
24
31
  def http_version
25
- @parser.http_version.join(".")
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
- @headers['Content-Length'] ||= @body.bytesize
30
+ headers[CONTENT_LENGTH] ||= @body.bytesize
34
31
  when IO
35
- @headers['Content-Length'] ||= @body.stat.size
32
+ headers[CONTENT_LENGTH] ||= @body.stat.size
36
33
  when Enumerable
37
- @headers['Transfer-Encoding'] ||= 'chunked'
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
- # Prevent modification through the accessor
43
- @headers.freeze
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
- 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
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) + CRLF
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" << CRLF * 2
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
- # Transform to canonical HTTP header capitalization
109
- def canonicalize_header(header)
110
- header.to_s.split(/[\-_]/).map(&:capitalize).join('-')
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 :canonicalize_header
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! @server.accept }
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