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.
- checksums.yaml +4 -4
- data/CHANGES.md +11 -3
- data/Gemfile +4 -0
- data/Guardfile +24 -0
- data/README.md +26 -16
- data/examples/ssl_hello_world.rb +27 -0
- data/examples/websocket_rack.sh +1 -1
- data/examples/websockets.rb +15 -5
- data/lib/rack/handler/reel.rb +1 -0
- data/lib/reel.rb +12 -1
- data/lib/reel/app.rb +1 -1
- data/lib/reel/connection.rb +59 -56
- data/lib/reel/mixins.rb +13 -9
- data/lib/reel/rack_worker.rb +3 -3
- data/lib/reel/request.rb +72 -34
- data/lib/reel/request_body.rb +56 -0
- data/lib/reel/request_info.rb +27 -0
- data/lib/reel/request_parser.rb +42 -30
- data/lib/reel/response.rb +5 -49
- data/lib/reel/response_writer.rb +69 -0
- data/lib/reel/server.rb +6 -6
- data/lib/reel/ssl_server.rb +39 -0
- data/lib/reel/stream.rb +5 -6
- data/lib/reel/version.rb +1 -1
- data/lib/reel/websocket.rb +6 -4
- data/reel.gemspec +4 -3
- data/spec/fixtures/client.crt +22 -0
- data/spec/fixtures/client.key +27 -0
- data/spec/fixtures/server.crt +22 -0
- data/spec/fixtures/server.key +27 -0
- data/spec/reel/connection_spec.rb +233 -7
- data/spec/reel/response_spec.rb +1 -1
- data/spec/reel/server_spec.rb +1 -1
- data/spec/reel/ssl_server_spec.rb +54 -0
- data/spec/reel/websocket_spec.rb +34 -2
- data/spec/spec_helper.rb +11 -5
- metadata +38 -8
data/lib/reel/mixins.rb
CHANGED
@@ -10,25 +10,25 @@ module Reel
|
|
10
10
|
|
11
11
|
# Obtain the IP address of the remote connection
|
12
12
|
def remote_ip
|
13
|
-
|
13
|
+
socket.peeraddr(false)[3]
|
14
14
|
end
|
15
|
-
|
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
|
-
|
20
|
+
socket.peeraddr(true)[2]
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
24
|
module RequestMixin
|
25
25
|
|
26
26
|
def method
|
27
|
-
@
|
27
|
+
@request_info.http_method
|
28
28
|
end
|
29
29
|
|
30
30
|
def headers
|
31
|
-
@
|
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
|
-
@
|
39
|
+
@request_info.http_version || HTTPVersionsMixin::DEFAULT_HTTP_VERSION
|
40
40
|
end
|
41
41
|
|
42
42
|
def url
|
43
|
-
@
|
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
|
-
|
51
|
+
uri.path
|
52
52
|
end
|
53
53
|
|
54
54
|
def query_string
|
55
|
-
|
55
|
+
uri.query
|
56
|
+
end
|
57
|
+
|
58
|
+
def fragment
|
59
|
+
uri.fragment
|
56
60
|
end
|
57
61
|
|
58
62
|
end
|
data/lib/reel/rack_worker.rb
CHANGED
@@ -88,8 +88,8 @@ module Reel
|
|
88
88
|
|
89
89
|
def websocket_env request
|
90
90
|
env = env(request)
|
91
|
-
env[
|
92
|
-
env[
|
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
|
data/lib/reel/request.rb
CHANGED
@@ -5,51 +5,89 @@ module Reel
|
|
5
5
|
extend Forwardable
|
6
6
|
include RequestMixin
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
def_delegators :@connection, :<<, :write, :respond, :finish_response
|
9
|
+
attr_reader :body
|
10
10
|
|
11
|
-
#
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
-
|
25
|
+
# Returns true if request fully finished reading
|
26
|
+
def finished_reading?; @finished_read; end
|
17
27
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
37
|
-
@http_parser, @connection = http_parser, connection
|
79
|
+
slice && slice.length == 0 ? nil : slice
|
38
80
|
end
|
39
81
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
data/lib/reel/request_parser.rb
CHANGED
@@ -2,17 +2,17 @@ module Reel
|
|
2
2
|
class Request
|
3
3
|
class Parser
|
4
4
|
include HTTPVersionsMixin
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :socket, :connection
|
6
6
|
|
7
|
-
def initialize
|
7
|
+
def initialize(sock, conn)
|
8
8
|
@parser = Http::Parser.new(self)
|
9
|
-
|
10
|
-
|
9
|
+
@socket = sock
|
10
|
+
@connection = conn
|
11
|
+
@currently_reading = @currently_responding = nil
|
12
|
+
@pending_reads = []
|
13
|
+
@pending_responses = []
|
11
14
|
|
12
|
-
|
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
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
@
|
57
|
+
@pending_reads << req
|
55
58
|
end
|
56
59
|
end
|
57
60
|
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
@
|
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
|
-
|
71
|
-
|
72
|
-
|
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
|
data/lib/reel/response.rb
CHANGED
@@ -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
|