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.
- data/.travis.yml +2 -3
- data/CHANGES.md +12 -2
- data/README.md +105 -19
- data/bin/reel +2 -1
- data/examples/chunked.rb +25 -0
- data/examples/hello_world.rb +4 -1
- data/examples/roundtrip.rb +157 -0
- data/examples/server-sent-events.rb +48 -0
- data/examples/stream.rb +26 -0
- data/examples/websocket-wall.rb +53 -0
- data/examples/websocket.ru +92 -0
- data/examples/websocket_rack.sh +1 -0
- data/examples/websockets.rb +1 -3
- data/lib/rack/handler/reel.rb +7 -5
- data/lib/reel.rb +6 -0
- data/lib/reel/connection.rb +43 -34
- data/lib/reel/mixins.rb +57 -0
- data/lib/reel/rack_worker.rb +103 -59
- data/lib/reel/request.rb +19 -34
- data/lib/reel/request_parser.rb +12 -2
- data/lib/reel/response.rb +36 -30
- data/lib/reel/server.rb +11 -7
- data/lib/reel/stream.rb +128 -0
- data/lib/reel/version.rb +1 -1
- data/lib/reel/websocket.rb +44 -8
- data/spec/reel/connection_spec.rb +89 -0
- data/spec/reel/rack_worker_spec.rb +28 -16
- data/spec/reel/response_spec.rb +2 -2
- data/spec/reel/server_spec.rb +2 -2
- data/spec/reel/websocket_spec.rb +2 -17
- data/spec/spec_helper.rb +26 -2
- metadata +74 -22
data/lib/reel/stream.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
module Reel
|
2
|
+
class Stream
|
3
|
+
|
4
|
+
def initialize &proc
|
5
|
+
@proc = proc
|
6
|
+
end
|
7
|
+
|
8
|
+
def call socket
|
9
|
+
@socket = socket
|
10
|
+
@proc.call self
|
11
|
+
end
|
12
|
+
|
13
|
+
def write data
|
14
|
+
write! data
|
15
|
+
self
|
16
|
+
end
|
17
|
+
alias :<< :write
|
18
|
+
|
19
|
+
# behaves like a true Rack::Response/BodyProxy object
|
20
|
+
def each(*)
|
21
|
+
yield self
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_error &proc
|
25
|
+
@on_error = proc
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def close
|
30
|
+
@socket.close unless closed?
|
31
|
+
end
|
32
|
+
alias finish close
|
33
|
+
|
34
|
+
def closed?
|
35
|
+
@socket.closed?
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def write! string
|
40
|
+
@socket << string
|
41
|
+
rescue => e
|
42
|
+
@on_error ? @on_error.call(e) : raise(e)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
class EventStream < Stream
|
48
|
+
|
49
|
+
# EventSource-related helpers
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# Reel::EventStream.new do |socket|
|
53
|
+
# socket.event 'some event'
|
54
|
+
# socket.retry 10
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# @note
|
58
|
+
# though retry is a reserved word, it is ok to use it as `object#retry`
|
59
|
+
#
|
60
|
+
%w[event id retry].each do |meth|
|
61
|
+
define_method meth do |data|
|
62
|
+
# unlike on #data, these messages expects a single \n at the end.
|
63
|
+
write! "%s: %s\n" % [meth, data]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def data data
|
68
|
+
# - any single message should not contain \n except at the end.
|
69
|
+
# - EventSource expects \n\n at the end of each single message.
|
70
|
+
write! "data: %s\n\n" % data.gsub(/\n|\r/, '')
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
class ChunkStream < Stream
|
77
|
+
|
78
|
+
def write chunk
|
79
|
+
chunk_header = chunk.bytesize.to_s(16)
|
80
|
+
write! chunk_header + Response::CRLF
|
81
|
+
write! chunk + Response::CRLF
|
82
|
+
self
|
83
|
+
end
|
84
|
+
alias :<< :write
|
85
|
+
|
86
|
+
# finish does not actually close the socket,
|
87
|
+
# it only inform the browser there are no more messages
|
88
|
+
def finish
|
89
|
+
write! "0#{Response::CRLF * 2}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def close
|
93
|
+
finish
|
94
|
+
super
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
class StreamResponse < Response
|
100
|
+
|
101
|
+
IDENTITY = 'identity'.freeze
|
102
|
+
|
103
|
+
def initialize status, headers, body
|
104
|
+
self.status = status
|
105
|
+
@body = body
|
106
|
+
|
107
|
+
case @body
|
108
|
+
when EventStream
|
109
|
+
# EventSource behaves extremely bad on chunked Transfer-Encoding
|
110
|
+
headers[TRANSFER_ENCODING] = IDENTITY
|
111
|
+
when ChunkStream
|
112
|
+
headers[TRANSFER_ENCODING] = CHUNKED
|
113
|
+
when Stream
|
114
|
+
else
|
115
|
+
raise TypeError, "can't render #{@body.class} as a response body"
|
116
|
+
end
|
117
|
+
|
118
|
+
@headers = canonicalize_headers(headers)
|
119
|
+
@version = http_version
|
120
|
+
end
|
121
|
+
|
122
|
+
def render socket
|
123
|
+
socket << render_header
|
124
|
+
@body.call socket
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
data/lib/reel/version.rb
CHANGED
data/lib/reel/websocket.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
|
+
require 'forwardable'
|
1
2
|
require 'websocket_parser'
|
2
3
|
|
3
4
|
module Reel
|
4
5
|
class WebSocket
|
5
|
-
|
6
|
+
extend Forwardable
|
7
|
+
include ConnectionMixin
|
8
|
+
include RequestMixin
|
6
9
|
|
7
|
-
|
8
|
-
|
10
|
+
def_delegators :@socket, :addr, :peeraddr
|
11
|
+
|
12
|
+
def initialize(http_parser, socket)
|
13
|
+
@http_parser, @socket = http_parser, socket
|
9
14
|
|
10
15
|
handshake = ::WebSocket::ClientHandshake.new(:get, url, headers)
|
11
16
|
|
@@ -36,20 +41,45 @@ module Reel
|
|
36
41
|
end
|
37
42
|
end
|
38
43
|
|
39
|
-
|
40
|
-
|
44
|
+
[:next_message, :next_messages, :on_message, :on_error, :on_close, :on_ping, :on_pong].each do |meth|
|
45
|
+
define_method meth do |&proc|
|
46
|
+
@parser.send __method__, &proc
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def read_every n, unit = :s
|
51
|
+
cancel_timer! # only one timer allowed per stream
|
52
|
+
seconds = case unit.to_s
|
53
|
+
when /\Am/
|
54
|
+
n * 60
|
55
|
+
when /\Ah/
|
56
|
+
n * 3600
|
57
|
+
else
|
58
|
+
n
|
59
|
+
end
|
60
|
+
@timer = Celluloid.every(seconds) { read }
|
41
61
|
end
|
62
|
+
alias read_interval read_every
|
63
|
+
alias read_frequency read_every
|
42
64
|
|
43
65
|
def read
|
44
66
|
@parser.append @socket.readpartial(Connection::BUFFER_SIZE) until msg = @parser.next_message
|
45
67
|
msg
|
68
|
+
rescue => e
|
69
|
+
cancel_timer!
|
70
|
+
@on_error ? @on_error.call(e) : raise(e)
|
71
|
+
end
|
72
|
+
|
73
|
+
def body
|
74
|
+
nil
|
46
75
|
end
|
47
76
|
|
48
77
|
def write(msg)
|
49
78
|
@socket << ::WebSocket::Message.new(msg).to_data
|
50
79
|
msg
|
51
|
-
rescue
|
52
|
-
|
80
|
+
rescue => e
|
81
|
+
cancel_timer!
|
82
|
+
@on_error ? @on_error.call(e) : raise(e)
|
53
83
|
end
|
54
84
|
alias_method :<<, :write
|
55
85
|
|
@@ -58,7 +88,13 @@ module Reel
|
|
58
88
|
end
|
59
89
|
|
60
90
|
def close
|
61
|
-
|
91
|
+
cancel_timer!
|
92
|
+
@socket.close unless closed?
|
62
93
|
end
|
94
|
+
|
95
|
+
def cancel_timer!
|
96
|
+
@timer && @timer.cancel
|
97
|
+
end
|
98
|
+
|
63
99
|
end
|
64
100
|
end
|
@@ -94,4 +94,93 @@ describe Reel::Connection do
|
|
94
94
|
connection.request.should be_false
|
95
95
|
end
|
96
96
|
end
|
97
|
+
|
98
|
+
describe "Connection#read behaving like IO#read" do
|
99
|
+
it "raises an exception if length is a negative value" do
|
100
|
+
with_socket_pair do |client, connection|
|
101
|
+
example_request = ExampleRequest.new
|
102
|
+
|
103
|
+
client << example_request.to_s
|
104
|
+
request = connection.request
|
105
|
+
|
106
|
+
lambda { request.read(-1) }.should raise_error(ArgumentError)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it "returns an empty string if the length is zero" do
|
111
|
+
with_socket_pair do |client, connection|
|
112
|
+
example_request = ExampleRequest.new
|
113
|
+
|
114
|
+
client << example_request.to_s
|
115
|
+
request = connection.request
|
116
|
+
|
117
|
+
request.read(0).should be_empty
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it "reads to EOF if length is nil" do
|
122
|
+
with_socket_pair do |client, connection|
|
123
|
+
body = "Hello, world!"
|
124
|
+
example_request = ExampleRequest.new
|
125
|
+
example_request.body = body
|
126
|
+
|
127
|
+
client << example_request.to_s
|
128
|
+
request = connection.request
|
129
|
+
|
130
|
+
request.read.should eq "Hello, world!"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
it "uses the optional buffer to recieve data" do
|
135
|
+
with_socket_pair do |client, connection|
|
136
|
+
body = "Hello, world!"
|
137
|
+
example_request = ExampleRequest.new
|
138
|
+
example_request.body = body
|
139
|
+
|
140
|
+
client << example_request.to_s
|
141
|
+
request = connection.request
|
142
|
+
|
143
|
+
buffer = ''
|
144
|
+
request.read(nil, buffer).should eq "Hello, world!"
|
145
|
+
buffer.should eq "Hello, world!"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
it "returns with the content it could read when the length longer than EOF" do
|
150
|
+
with_socket_pair do |client, connection|
|
151
|
+
body = "Hello, world!"
|
152
|
+
example_request = ExampleRequest.new
|
153
|
+
example_request.body = body
|
154
|
+
|
155
|
+
client << example_request.to_s
|
156
|
+
request = connection.request
|
157
|
+
|
158
|
+
request.read(1024).should eq "Hello, world!"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
it "returns nil at EOF if a length is passed" do
|
163
|
+
with_socket_pair do |client, connection|
|
164
|
+
example_request = ExampleRequest.new
|
165
|
+
|
166
|
+
client << example_request.to_s
|
167
|
+
request = connection.request
|
168
|
+
|
169
|
+
request.read(1024).should be_nil
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
it "returns an empty string at EOF if length is nil" do
|
174
|
+
with_socket_pair do |client, connection|
|
175
|
+
example_request = ExampleRequest.new
|
176
|
+
|
177
|
+
client << example_request.to_s
|
178
|
+
request = connection.request
|
179
|
+
|
180
|
+
request.read.should be_empty
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
97
186
|
end
|
@@ -1,17 +1,15 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Reel::RackWorker do
|
4
|
-
|
5
4
|
let(:endpoint) { URI(example_url) }
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
[200, {'Content-Type' => 'text/plain'}, ['Hello rack world!']]
|
11
|
-
end
|
6
|
+
RackApp = Proc.new do |env|
|
7
|
+
[200, {'Content-Type' => 'text/plain'}, ['Hello rack world!']]
|
8
|
+
end
|
12
9
|
|
10
|
+
let(:worker) do
|
13
11
|
handler = Rack::Handler::Reel.new
|
14
|
-
handler.options[:app] =
|
12
|
+
handler.options[:app] = RackApp
|
15
13
|
|
16
14
|
Reel::RackWorker.new(handler)
|
17
15
|
end
|
@@ -20,37 +18,51 @@ describe Reel::RackWorker do
|
|
20
18
|
with_socket_pair do |client, connection|
|
21
19
|
client << ExampleRequest.new(:get, '/test?hello=true').to_s
|
22
20
|
request = connection.request
|
23
|
-
env = worker.
|
21
|
+
env = worker.request_env(request, connection)
|
24
22
|
|
25
23
|
Reel::RackWorker::PROTO_RACK_ENV.each do |k, v|
|
26
24
|
env[k].should == v
|
27
25
|
end
|
28
26
|
|
29
|
-
env["SERVER_NAME"].should == '
|
30
|
-
env["SERVER_PORT"].should == 3000
|
27
|
+
env["SERVER_NAME"].should == 'www.example.com'
|
28
|
+
env["SERVER_PORT"].should == "3000"
|
31
29
|
env["REMOTE_ADDR"].should == "127.0.0.1"
|
32
30
|
env["PATH_INFO"].should == "/test"
|
33
31
|
env["REQUEST_METHOD"].should == "GET"
|
34
|
-
env["REQUEST_PATH"].should == "/test"
|
35
|
-
env["ORIGINAL_FULLPATH"].should == "/test"
|
36
32
|
env["QUERY_STRING"].should == "hello=true"
|
37
33
|
env["HTTP_HOST"].should == 'www.example.com'
|
38
34
|
env["HTTP_ACCEPT_LANGUAGE"].should == "en-US,en;q=0.8"
|
39
|
-
env["REQUEST_URI"].should == 'http://www.example.com/test'
|
40
|
-
|
41
|
-
%w(localhost 127.0.0.1).should include env["REMOTE_HOST"]
|
42
35
|
|
43
36
|
env["rack.input"].should be_kind_of(StringIO)
|
44
37
|
env["rack.input"].string.should == ''
|
38
|
+
|
39
|
+
validator = ::Rack::Lint.new(RackApp)
|
40
|
+
status, *rest = validator.call(env)
|
41
|
+
status.should == 200
|
45
42
|
end
|
46
43
|
end
|
47
44
|
|
45
|
+
context "WebSocket" do
|
46
|
+
include WebSocketHelpers
|
47
|
+
|
48
|
+
it "places websocket into rack env" do
|
49
|
+
with_socket_pair do |client, connection|
|
50
|
+
client << handshake.to_data
|
51
|
+
request = connection.request
|
52
|
+
env = worker.websocket_env(request)
|
53
|
+
|
54
|
+
env["REMOTE_ADDR"].should == "127.0.0.1"
|
55
|
+
env["rack.websocket"].should be_a Reel::WebSocket
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
48
60
|
it "delegates web requests to the rack app" do
|
49
61
|
ex = nil
|
50
62
|
|
51
63
|
handler = proc do |connection|
|
52
64
|
begin
|
53
|
-
worker.handle
|
65
|
+
worker.async.handle(connection.detach)
|
54
66
|
rescue => ex
|
55
67
|
end
|
56
68
|
end
|
data/spec/reel/response_spec.rb
CHANGED
@@ -10,8 +10,8 @@ describe Reel::Response do
|
|
10
10
|
connection.close
|
11
11
|
|
12
12
|
response = client.read(4096)
|
13
|
-
crlf =
|
14
|
-
fixture = "5#{crlf}
|
13
|
+
crlf = Reel::Response::CRLF
|
14
|
+
fixture = "5#{crlf}Hello#{crlf}5#{crlf}World#{crlf}0#{crlf*2}"
|
15
15
|
response[(response.length - fixture.length)..-1].should eq fixture
|
16
16
|
end
|
17
17
|
end
|
data/spec/reel/server_spec.rb
CHANGED
@@ -11,7 +11,7 @@ describe Reel::Server do
|
|
11
11
|
handler = proc do |connection|
|
12
12
|
begin
|
13
13
|
request = connection.request
|
14
|
-
request.method.should eq
|
14
|
+
request.method.should eq 'GET'
|
15
15
|
request.version.should eq "1.1"
|
16
16
|
request.url.should eq example_path
|
17
17
|
|
@@ -34,7 +34,7 @@ describe Reel::Server do
|
|
34
34
|
handler = proc do |connection|
|
35
35
|
begin
|
36
36
|
request = connection.request
|
37
|
-
request.method.should eq
|
37
|
+
request.method.should eq 'POST'
|
38
38
|
connection.respond :ok, request.body
|
39
39
|
rescue => ex
|
40
40
|
end
|
data/spec/reel/websocket_spec.rb
CHANGED
@@ -1,26 +1,11 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Reel::WebSocket do
|
4
|
-
|
5
|
-
|
6
|
-
let(:example_url) { "ws://#{example_host}#{example_path}" }
|
4
|
+
include WebSocketHelpers
|
5
|
+
|
7
6
|
let(:example_message) { "Hello, World!" }
|
8
7
|
let(:another_message) { "What's going on?" }
|
9
8
|
|
10
|
-
let :handshake_headers do
|
11
|
-
{
|
12
|
-
"Host" => example_host,
|
13
|
-
"Upgrade" => "websocket",
|
14
|
-
"Connection" => "Upgrade",
|
15
|
-
"Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==",
|
16
|
-
"Origin" => "http://example.com",
|
17
|
-
"Sec-WebSocket-Protocol" => "chat, superchat",
|
18
|
-
"Sec-WebSocket-Version" => "13"
|
19
|
-
}
|
20
|
-
end
|
21
|
-
|
22
|
-
let(:handshake) { WebSocket::ClientHandshake.new(:get, example_url, handshake_headers) }
|
23
|
-
|
24
9
|
it "performs websocket handshakes" do
|
25
10
|
with_socket_pair do |client, connection|
|
26
11
|
client << handshake.to_data
|