reel 0.4.0.pre → 0.4.0.pre2

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.

@@ -10,25 +10,25 @@ module Reel
10
10
 
11
11
  # Obtain the IP address of the remote connection
12
12
  def remote_ip
13
- @socket.peeraddr(false)[3]
13
+ socket.peeraddr(false)[3]
14
14
  end
15
- alias_method :remote_addr, :remote_ip
15
+ alias remote_addr remote_ip
16
16
 
17
17
  # Obtain the hostname of the remote connection
18
18
  def remote_host
19
19
  # NOTE: Celluloid::IO does not yet support non-blocking reverse DNS
20
- @socket.peeraddr(true)[2]
20
+ socket.peeraddr(true)[2]
21
21
  end
22
22
  end
23
23
 
24
24
  module RequestMixin
25
25
 
26
26
  def method
27
- @http_parser.http_method
27
+ @request_info.http_method
28
28
  end
29
29
 
30
30
  def headers
31
- @http_parser.headers
31
+ @request_info.headers
32
32
  end
33
33
 
34
34
  def [] header
@@ -36,11 +36,11 @@ module Reel
36
36
  end
37
37
 
38
38
  def version
39
- @http_parser.http_version || HTTPVersionsMixin::DEFAULT_HTTP_VERSION
39
+ @request_info.http_version || HTTPVersionsMixin::DEFAULT_HTTP_VERSION
40
40
  end
41
41
 
42
42
  def url
43
- @http_parser.url
43
+ @request_info.url
44
44
  end
45
45
 
46
46
  def uri
@@ -48,11 +48,15 @@ module Reel
48
48
  end
49
49
 
50
50
  def path
51
- @http_parser.request_path
51
+ uri.path
52
52
  end
53
53
 
54
54
  def query_string
55
- @http_parser.query_string
55
+ uri.query
56
+ end
57
+
58
+ def fragment
59
+ uri.fragment
56
60
  end
57
61
 
58
62
  end
@@ -88,8 +88,8 @@ module Reel
88
88
 
89
89
  def websocket_env request
90
90
  env = env(request)
91
- env[REMOTE_ADDR] = request.remote_ip
92
- env[RACK_WEBSOCKET] = request
91
+ env[RACK_WEBSOCKET] = request.websocket
92
+ env[REMOTE_ADDR] = request.websocket.remote_ip
93
93
  env
94
94
  end
95
95
 
@@ -111,7 +111,7 @@ module Reel
111
111
  def env request
112
112
  env = Hash[PROTO_RACK_ENV]
113
113
 
114
- env[RACK_INPUT] = StringIO.new(request.body || INITIAL_BODY)
114
+ env[RACK_INPUT] = StringIO.new(request.body.to_s || INITIAL_BODY)
115
115
  env[RACK_INPUT].set_encoding(Encoding::BINARY) if env[RACK_INPUT].respond_to?(:set_encoding)
116
116
  env[SERVER_NAME], env[SERVER_PORT] = (request[HOST]||'').split(':', 2)
117
117
  env[SERVER_PORT] ||= @handler[:port].to_s
@@ -5,51 +5,89 @@ module Reel
5
5
  extend Forwardable
6
6
  include RequestMixin
7
7
 
8
- UPGRADE = 'Upgrade'.freeze
9
- WEBSOCKET = 'websocket'.freeze
8
+ def_delegators :@connection, :<<, :write, :respond, :finish_response
9
+ attr_reader :body
10
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
11
+ # request_info is a RequestInfo object including the headers and
12
+ # the url, method and http version.
13
+ #
14
+ # Access it through the RequestMixin methods.
15
+ def initialize(request_info, connection = nil)
16
+ @request_info = request_info
17
+ @connection = connection
18
+ @finished = false
19
+ @buffer = ""
20
+ @body = RequestBody.new(self)
21
+ @finished_read = false
22
+ @websocket = nil
23
+ end
14
24
 
15
- def self.read(connection)
16
- parser = connection.parser
25
+ # Returns true if request fully finished reading
26
+ def finished_reading?; @finished_read; end
17
27
 
18
- begin
19
- data = connection.socket.readpartial(Connection::BUFFER_SIZE)
20
- parser << data
21
- end until parser.headers
28
+ # When HTTP Parser marks the message parsing as complete, this will be set.
29
+ def finish_reading!
30
+ raise StateError, "already finished" if @finished_read
31
+ @finished_read = true
32
+ end
33
+
34
+ # Fill the request buffer with data as it becomes available
35
+ def fill_buffer(chunk)
36
+ @buffer << chunk
37
+ end
22
38
 
23
- REQUEST_METHODS[parser.http_method] ||
24
- raise(ArgumentError, "Unknown Request Method: %s" % parser.http_method)
39
+ # Read a number of bytes, looping until they are available or until
40
+ # readpartial returns nil, indicating there are no more bytes to read
41
+ def read(length = nil, buffer = nil)
42
+ raise ArgumentError, "negative length #{length} given" if length && length < 0
25
43
 
26
- upgrade = parser.headers[UPGRADE]
27
- if upgrade && upgrade.downcase == WEBSOCKET
28
- WebSocket.new(parser, connection.socket)
29
- else
30
- Request.new(parser, connection)
44
+ return '' if length == 0
45
+ res = buffer.nil? ? '' : buffer.clear
46
+
47
+ chunk_size = length.nil? ? @connection.buffer_size : length
48
+ begin
49
+ while chunk_size > 0
50
+ chunk = readpartial(chunk_size)
51
+ break unless chunk
52
+ res << chunk
53
+ chunk_size = length - res.length unless length.nil?
54
+ end
55
+ rescue EOFError
31
56
  end
57
+ return length && res.length == 0 ? nil : res
32
58
  end
33
59
 
34
- def_delegators :@connection, :respond, :finish_response, :close, :read
60
+ # Read a string up to the given number of bytes, blocking until some
61
+ # data is available but returning immediately if some data is available
62
+ def readpartial(length = nil)
63
+ if length.nil? && @buffer.length > 0
64
+ slice = @buffer
65
+ @buffer = ""
66
+ else
67
+ unless finished_reading? || (length && length <= @buffer.length)
68
+ @connection.readpartial(length ? length - @buffer.length : Connection::BUFFER_SIZE)
69
+ end
70
+
71
+ if length
72
+ slice = @buffer.slice!(0, length)
73
+ else
74
+ slice = @buffer
75
+ @buffer = ""
76
+ end
77
+ end
35
78
 
36
- def initialize(http_parser, connection = nil)
37
- @http_parser, @connection = http_parser, connection
79
+ slice && slice.length == 0 ? nil : slice
38
80
  end
39
81
 
40
- def body
41
- @body ||= begin
42
- raise "no connection given" unless @connection
43
-
44
- body = "" unless block_given?
45
- while (chunk = @connection.readpartial)
46
- if block_given?
47
- yield chunk
48
- else
49
- body << chunk
50
- end
51
- end
52
- body unless block_given?
82
+ # Can the current request be upgraded to a WebSocket?
83
+ def websocket?; @request_info.websocket_request?; end
84
+
85
+ # Return a Reel::WebSocket for this request, hijacking the socket from
86
+ # the underlying connection
87
+ def websocket
88
+ @websocket ||= begin
89
+ raise StateError, "can't upgrade this request to a websocket" unless websocket?
90
+ WebSocket.new(@request_info, @connection.hijack_socket)
53
91
  end
54
92
  end
55
93
  end
@@ -0,0 +1,56 @@
1
+ module Reel
2
+ # Represents the bodies of Requests
3
+ class RequestBody
4
+ include Enumerable
5
+
6
+ def initialize(request)
7
+ @request = request
8
+ @streaming = nil
9
+ @contents = nil
10
+ end
11
+
12
+ # Read exactly the given amount of data
13
+ def read(length)
14
+ stream!
15
+ @request.read(length)
16
+ end
17
+
18
+ # Read up to length bytes, but return any data that's available
19
+ def readpartial(length = nil)
20
+ stream!
21
+ @request.readpartial(length)
22
+ end
23
+
24
+ # Iterate over the body, allowing it to be enumerable
25
+ def each
26
+ while chunk = readpartial
27
+ yield chunk
28
+ end
29
+ end
30
+
31
+ # Eagerly consume the entire body as a string
32
+ def to_s
33
+ return @contents if @contents
34
+ raise StateError, "body is being streamed" unless @streaming.nil?
35
+
36
+ begin
37
+ @streaming = false
38
+ @contents = ""
39
+ while chunk = @request.readpartial
40
+ @contents << chunk
41
+ end
42
+ rescue
43
+ @contents = nil
44
+ raise
45
+ end
46
+
47
+ @contents
48
+ end
49
+
50
+ # Assert that the body is actively being streamed
51
+ def stream!
52
+ raise StateError, "body has already been consumed" if @streaming == false
53
+ @streaming = true
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,27 @@
1
+ module Reel
2
+ class RequestInfo
3
+ attr_reader :http_method, :url, :http_version, :headers
4
+
5
+ def initialize(http_method, url, http_version, headers)
6
+ @http_method = http_method
7
+ @url = url
8
+ @http_version = http_version
9
+ @headers = headers
10
+ end
11
+
12
+ UPGRADE = 'Upgrade'.freeze
13
+ WEBSOCKET = 'websocket'.freeze
14
+
15
+ # Array#include? seems slow compared to Hash lookup
16
+ request_methods = Http::METHODS.map { |m| m.to_s.upcase }
17
+ REQUEST_METHODS = Hash[request_methods.zip(request_methods)].freeze
18
+
19
+ def method
20
+ REQUEST_METHODS[http_method]
21
+ end
22
+
23
+ def websocket_request?
24
+ headers[UPGRADE] && headers[UPGRADE].downcase == WEBSOCKET
25
+ end
26
+ end
27
+ end
@@ -2,17 +2,17 @@ module Reel
2
2
  class Request
3
3
  class Parser
4
4
  include HTTPVersionsMixin
5
- attr_reader :headers
5
+ attr_reader :socket, :connection
6
6
 
7
- def initialize
7
+ def initialize(sock, conn)
8
8
  @parser = Http::Parser.new(self)
9
- reset
10
- end
9
+ @socket = sock
10
+ @connection = conn
11
+ @currently_reading = @currently_responding = nil
12
+ @pending_reads = []
13
+ @pending_responses = []
11
14
 
12
- [:request_path, :query_string].each do |m|
13
- define_method m do
14
- @parser.send m
15
- end
15
+ reset
16
16
  end
17
17
 
18
18
  def add(data)
@@ -20,10 +20,6 @@ module Reel
20
20
  end
21
21
  alias_method :<<, :add
22
22
 
23
- def headers?
24
- !!@headers
25
- end
26
-
27
23
  def http_method
28
24
  @parser.http_method
29
25
  end
@@ -37,39 +33,55 @@ module Reel
37
33
  @parser.request_url
38
34
  end
39
35
 
40
- def finished?; @finished; end
36
+ def current_request
37
+ until @currently_responding || @currently_reading
38
+ readpartial
39
+ end
40
+ @currently_responding || @currently_reading
41
+ end
42
+
43
+ def readpartial(size = @connection.buffer_size)
44
+ bytes = @socket.readpartial(size)
45
+ @parser << bytes
46
+ end
41
47
 
42
48
  #
43
49
  # Http::Parser callbacks
44
50
  #
45
-
46
51
  def on_headers_complete(headers)
47
- @headers = headers
48
- end
49
-
50
- def on_body(chunk)
51
- if @chunk
52
- @chunk << chunk
52
+ info = RequestInfo.new(http_method, url, http_version, headers)
53
+ req = Request.new(info, connection)
54
+ if @currently_reading.nil?
55
+ @currently_reading = req
53
56
  else
54
- @chunk = chunk
57
+ @pending_reads << req
55
58
  end
56
59
  end
57
60
 
58
- def chunk
59
- if (chunk = @chunk)
60
- @chunk = nil
61
- chunk
62
- end
61
+ # Send body directly to Reel::Response to be buffered.
62
+ def on_body(chunk)
63
+ @currently_reading.fill_buffer(chunk)
63
64
  end
64
65
 
66
+ # Mark current request as complete, set this as ready to respond.
65
67
  def on_message_complete
66
- @finished = true
68
+ @currently_reading.finish_reading! if @currently_reading.is_a?(Request)
69
+ if @currently_responding.nil?
70
+ @currently_responding = @currently_reading
71
+ else
72
+ @pending_responses << @currently_reading
73
+ end
74
+ @currently_reading = @pending_reads.shift
67
75
  end
68
76
 
69
77
  def reset
70
- @finished = false
71
- @headers = nil
72
- @chunk = nil
78
+ popped = @currently_responding
79
+ if req = @pending_responses.shift
80
+ @currently_responding = req
81
+ elsif @currently_responding
82
+ @currently_responding = nil
83
+ end
84
+ popped
73
85
  end
74
86
  end
75
87
  end
@@ -8,11 +8,10 @@ module Reel
8
8
  # Use status code tables from the Http gem
9
9
  STATUS_CODES = Http::Response::STATUS_CODES
10
10
  SYMBOL_TO_STATUS_CODE = Http::Response::SYMBOL_TO_STATUS_CODE
11
- CRLF = "\r\n"
12
11
 
13
12
  attr_reader :status # Status has a special setter to coerce symbol names
14
13
  attr_accessor :reason # Reason can be set explicitly if desired
15
- attr_reader :headers, :body
14
+ attr_reader :headers, :body, :version
16
15
 
17
16
  def initialize(status, body_or_headers = nil, body = nil)
18
17
  self.status = status
@@ -40,6 +39,10 @@ module Reel
40
39
  @version = http_version
41
40
  end
42
41
 
42
+ def chunked?
43
+ headers[TRANSFER_ENCODING] == CHUNKED
44
+ end
45
+
43
46
  # Set the status
44
47
  def status=(status, reason=nil)
45
48
  case status
@@ -57,52 +60,6 @@ module Reel
57
60
  end
58
61
  end
59
62
 
60
- # Write the response out to the wire
61
- def render(socket)
62
- socket << render_header
63
-
64
- case @body
65
- when String
66
- socket << @body
67
- when IO
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
76
- end
77
- ensure
78
- @body.close
79
- end
80
- when Enumerable
81
- @body.each do |chunk|
82
- chunk_header = chunk.bytesize.to_s(16)
83
- socket << chunk_header + CRLF
84
- socket << chunk + CRLF
85
- end
86
-
87
- socket << "0#{CRLF * 2}"
88
- end
89
- end
90
-
91
- # Convert headers into a string
92
- # FIXME: this should probably be factored elsewhere, SRP and all
93
- def render_header
94
- response_header = "#{@version} #{@status} #{@reason}#{CRLF}"
95
-
96
- unless @headers.empty?
97
- response_header << @headers.map do |header, value|
98
- "#{header}: #{value}"
99
- end.join(CRLF) << CRLF
100
- end
101
-
102
- response_header << CRLF
103
- end
104
- private :render_header
105
-
106
63
  def canonicalize_headers(headers)
107
64
  headers.inject({}) do |headers, (header, value)|
108
65
  headers.merge Http.canonicalize_header(header) => value.to_s
@@ -115,6 +72,5 @@ module Reel
115
72
  "HTTP/1.1".freeze
116
73
  end
117
74
  private :http_version
118
-
119
75
  end
120
76
  end