em-websocket 0.3.7 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.rdoc +52 -0
- data/Gemfile +6 -0
- data/LICENCE +7 -0
- data/README.md +105 -40
- data/examples/echo.rb +22 -6
- data/examples/test.html +5 -6
- data/lib/em-websocket.rb +2 -1
- data/lib/em-websocket/close03.rb +3 -0
- data/lib/em-websocket/close05.rb +3 -0
- data/lib/em-websocket/close06.rb +3 -0
- data/lib/em-websocket/close75.rb +2 -1
- data/lib/em-websocket/connection.rb +154 -48
- data/lib/em-websocket/framing03.rb +3 -2
- data/lib/em-websocket/framing05.rb +3 -2
- data/lib/em-websocket/framing07.rb +16 -4
- data/lib/em-websocket/framing76.rb +1 -4
- data/lib/em-websocket/handler.rb +61 -15
- data/lib/em-websocket/handler03.rb +0 -1
- data/lib/em-websocket/handler05.rb +0 -1
- data/lib/em-websocket/handler06.rb +0 -1
- data/lib/em-websocket/handler07.rb +0 -1
- data/lib/em-websocket/handler08.rb +0 -1
- data/lib/em-websocket/handler13.rb +0 -1
- data/lib/em-websocket/handler76.rb +2 -0
- data/lib/em-websocket/handshake.rb +156 -0
- data/lib/em-websocket/handshake04.rb +18 -16
- data/lib/em-websocket/handshake75.rb +15 -8
- data/lib/em-websocket/handshake76.rb +15 -14
- data/lib/em-websocket/masking04.rb +3 -6
- data/lib/em-websocket/message_processor_03.rb +6 -3
- data/lib/em-websocket/message_processor_06.rb +30 -9
- data/lib/em-websocket/version.rb +1 -1
- data/lib/em-websocket/websocket.rb +24 -15
- data/spec/helper.rb +84 -51
- data/spec/integration/common_spec.rb +89 -69
- data/spec/integration/draft03_spec.rb +84 -56
- data/spec/integration/draft05_spec.rb +14 -12
- data/spec/integration/draft06_spec.rb +67 -7
- data/spec/integration/draft13_spec.rb +30 -19
- data/spec/integration/draft75_spec.rb +46 -40
- data/spec/integration/draft76_spec.rb +59 -45
- data/spec/integration/gte_03_examples.rb +42 -0
- data/spec/integration/shared_examples.rb +119 -0
- data/spec/unit/framing_spec.rb +24 -4
- data/spec/unit/handshake_spec.rb +216 -0
- data/spec/unit/masking_spec.rb +2 -0
- metadata +32 -86
- data/examples/flash_policy_file_server.rb +0 -21
- data/examples/js/FABridge.js +0 -604
- data/examples/js/WebSocketMain.swf +0 -0
- data/examples/js/swfobject.js +0 -4
- data/examples/js/web_socket.js +0 -312
- data/lib/em-websocket/handler_factory.rb +0 -109
- data/spec/unit/handler_spec.rb +0 -159
@@ -0,0 +1,42 @@
|
|
1
|
+
shared_examples_for "a WebSocket server drafts 3 and above" do
|
2
|
+
it "should force close connections after a timeout if close handshake is not sent by the client" do
|
3
|
+
em {
|
4
|
+
server_onerror_fired = false
|
5
|
+
server_onclose_fired = false
|
6
|
+
client_got_close_handshake = false
|
7
|
+
|
8
|
+
start_server(:close_timeout => 0.1) { |ws|
|
9
|
+
ws.onopen {
|
10
|
+
# 1: Send close handshake to client
|
11
|
+
EM.next_tick { ws.close(4999, "Close message") }
|
12
|
+
}
|
13
|
+
|
14
|
+
ws.onerror { |e|
|
15
|
+
# 3: Client should receive onerror
|
16
|
+
e.class.should == EM::WebSocket::WSProtocolError
|
17
|
+
e.message.should == "Close handshake un-acked after 0.1s, closing tcp connection"
|
18
|
+
server_onerror_fired = true
|
19
|
+
}
|
20
|
+
|
21
|
+
ws.onclose {
|
22
|
+
server_onclose_fired = true
|
23
|
+
}
|
24
|
+
}
|
25
|
+
start_client { |client|
|
26
|
+
client.onmessage { |msg|
|
27
|
+
# 2: Client does not respond to close handshake (the fake client
|
28
|
+
# doesn't understand them at all hence this is in onmessage)
|
29
|
+
msg.should =~ /Close message/ if version >= 6
|
30
|
+
client_got_close_handshake = true
|
31
|
+
}
|
32
|
+
|
33
|
+
client.onclose {
|
34
|
+
server_onerror_fired.should == true
|
35
|
+
server_onclose_fired.should == true
|
36
|
+
client_got_close_handshake.should == true
|
37
|
+
done
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
@@ -3,6 +3,125 @@
|
|
3
3
|
# These tests are run against all draft versions
|
4
4
|
#
|
5
5
|
shared_examples_for "a websocket server" do
|
6
|
+
it "should expose the protocol version" do
|
7
|
+
em {
|
8
|
+
start_server { |ws|
|
9
|
+
ws.onopen { |handshake|
|
10
|
+
handshake.protocol_version.should == version
|
11
|
+
done
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
start_client
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should expose the origin header" do
|
20
|
+
em {
|
21
|
+
start_server { |ws|
|
22
|
+
ws.onopen { |handshake|
|
23
|
+
handshake.origin.should == 'http://example.com'
|
24
|
+
done
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
start_client
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should expose the remote IP address" do
|
33
|
+
em {
|
34
|
+
start_server { |ws|
|
35
|
+
ws.onopen {
|
36
|
+
ws.remote_ip.should == "127.0.0.1"
|
37
|
+
done
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
start_client
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should send messages successfully" do
|
46
|
+
em {
|
47
|
+
start_server { |ws|
|
48
|
+
ws.onmessage { |message|
|
49
|
+
message.should == "hello server"
|
50
|
+
done
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
start_client { |client|
|
55
|
+
client.onopen {
|
56
|
+
client.send("hello server")
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should allow connection to be closed with valid close code" do
|
63
|
+
em {
|
64
|
+
start_server { |ws|
|
65
|
+
ws.onopen {
|
66
|
+
ws.close(4004, "Bye bye")
|
67
|
+
done
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
start_client
|
72
|
+
# TODO: Use a real client which understands how to respond to closing
|
73
|
+
# handshakes, sending the handshake currently untested
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should raise error if if invalid close code is used" do
|
78
|
+
em {
|
79
|
+
start_server { |ws|
|
80
|
+
ws.onopen {
|
81
|
+
lambda {
|
82
|
+
ws.close(2000)
|
83
|
+
}.should raise_error("Application code may only use codes from 1000, 3000-4999")
|
84
|
+
done
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
start_client
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should call onclose with was_clean set to false if connection closed without closing handshake by server" do
|
93
|
+
em {
|
94
|
+
start_server { |ws|
|
95
|
+
ws.onopen {
|
96
|
+
# Close tcp connection (no close handshake)
|
97
|
+
ws.close_connection
|
98
|
+
}
|
99
|
+
ws.onclose { |event|
|
100
|
+
event.should == {:code => 1006, :was_clean => false}
|
101
|
+
done
|
102
|
+
}
|
103
|
+
}
|
104
|
+
start_client
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should call onclose with was_clean set to false if connection closed without closing handshake by client" do
|
109
|
+
em {
|
110
|
+
start_server { |ws|
|
111
|
+
ws.onclose { |event|
|
112
|
+
event.should == {:code => 1006, :was_clean => false}
|
113
|
+
done
|
114
|
+
}
|
115
|
+
}
|
116
|
+
start_client { |client|
|
117
|
+
client.onopen {
|
118
|
+
# Close tcp connection (no close handshake)
|
119
|
+
client.close_connection
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
6
125
|
it "should call onerror if an application error raised in onopen" do
|
7
126
|
em {
|
8
127
|
start_server { |ws|
|
data/spec/unit/framing_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
|
1
3
|
require 'helper'
|
2
4
|
|
3
5
|
describe EM::WebSocket::Framing03 do
|
@@ -13,7 +15,7 @@ describe EM::WebSocket::Framing03 do
|
|
13
15
|
|
14
16
|
def <<(data)
|
15
17
|
@data << data
|
16
|
-
process_data
|
18
|
+
process_data
|
17
19
|
end
|
18
20
|
|
19
21
|
def debug(*args); end
|
@@ -138,7 +140,7 @@ describe EM::WebSocket::Framing04 do
|
|
138
140
|
|
139
141
|
def <<(data)
|
140
142
|
@data << data
|
141
|
-
process_data
|
143
|
+
process_data
|
142
144
|
end
|
143
145
|
|
144
146
|
def debug(*args); end
|
@@ -207,7 +209,7 @@ describe EM::WebSocket::Framing07 do
|
|
207
209
|
|
208
210
|
def <<(data)
|
209
211
|
@data << data
|
210
|
-
process_data
|
212
|
+
process_data
|
211
213
|
end
|
212
214
|
|
213
215
|
def debug(*args); end
|
@@ -265,7 +267,7 @@ describe EM::WebSocket::Framing07 do
|
|
265
267
|
lambda {
|
266
268
|
# Opcode 3 is not supported by this draft
|
267
269
|
@f << "\x83\x05Hello"
|
268
|
-
}.should raise_error(EventMachine::WebSocket::WSProtocolError, "Unknown opcode")
|
270
|
+
}.should raise_error(EventMachine::WebSocket::WSProtocolError, "Unknown opcode 3")
|
269
271
|
end
|
270
272
|
|
271
273
|
it "should accept a fragmented unmasked text message in 3 frames" do
|
@@ -274,5 +276,23 @@ describe EM::WebSocket::Framing07 do
|
|
274
276
|
@f << "\x00\x02lo"
|
275
277
|
@f << "\x80\x06 world"
|
276
278
|
end
|
279
|
+
|
280
|
+
it "should raise if non-fin frame is followed by a non-continuation data frame (continuation frame would be expected)" do
|
281
|
+
lambda {
|
282
|
+
@f << 0b00000001 # Not fin, text
|
283
|
+
@f << 0b00000001 # Length 1
|
284
|
+
@f << 'f'
|
285
|
+
@f << 0b10000001 # fin, text (continutation expected)
|
286
|
+
@f << 0b00000001 # Length 1
|
287
|
+
@f << 'b'
|
288
|
+
}.should raise_error(EM::WebSocket::WebSocketError, 'Continuation frame expected')
|
289
|
+
end
|
290
|
+
|
291
|
+
it "should raise on non-fin control frames (control frames must not be fragmented)" do
|
292
|
+
lambda {
|
293
|
+
@f << 0b00001010 # Not fin, pong (opcode 10)
|
294
|
+
@f << 0b00000000 # Length 1
|
295
|
+
}.should raise_error(EM::WebSocket::WebSocketError, 'Control frames must not be fragmented')
|
296
|
+
end
|
277
297
|
end
|
278
298
|
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe EM::WebSocket::Handshake do
|
4
|
+
def handshake(request, secure = false)
|
5
|
+
handshake = EM::WebSocket::Handshake.new(secure)
|
6
|
+
handshake.receive_data(format_request(request))
|
7
|
+
handshake
|
8
|
+
end
|
9
|
+
|
10
|
+
before :each do
|
11
|
+
@request = {
|
12
|
+
:port => 80,
|
13
|
+
:method => "GET",
|
14
|
+
:path => "/demo",
|
15
|
+
:headers => {
|
16
|
+
'Host' => 'example.com',
|
17
|
+
'Connection' => 'Upgrade',
|
18
|
+
'Sec-WebSocket-Key2' => '12998 5 Y3 1 .P00',
|
19
|
+
'Sec-WebSocket-Protocol' => 'sample',
|
20
|
+
'Upgrade' => 'WebSocket',
|
21
|
+
'Sec-WebSocket-Key1' => '4 @1 46546xW%0l 1 5',
|
22
|
+
'Origin' => 'http://example.com'
|
23
|
+
},
|
24
|
+
:body => '^n:ds[4U'
|
25
|
+
}
|
26
|
+
@secure_request = @request.merge(:port => 443)
|
27
|
+
|
28
|
+
@response = {
|
29
|
+
:headers => {
|
30
|
+
"Upgrade" => "WebSocket",
|
31
|
+
"Connection" => "Upgrade",
|
32
|
+
"Sec-WebSocket-Location" => "ws://example.com/demo",
|
33
|
+
"Sec-WebSocket-Origin" => "http://example.com",
|
34
|
+
"Sec-WebSocket-Protocol" => "sample"
|
35
|
+
},
|
36
|
+
:body => "8jKS\'y:G*Co,Wxa-"
|
37
|
+
}
|
38
|
+
@secure_response = @response.merge(:headers => @response[:headers].merge('Sec-WebSocket-Location' => "wss://example.com/demo"))
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should handle good request" do
|
42
|
+
handshake(@request).should succeed_with_upgrade(@response)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should handle good request to secure default port if secure mode is enabled" do
|
46
|
+
handshake(@secure_request, true).
|
47
|
+
should succeed_with_upgrade(@secure_response)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should not handle good request to secure default port if secure mode is disabled" do
|
51
|
+
handshake(@secure_request, false).
|
52
|
+
should_not succeed_with_upgrade(@secure_response)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should handle good request on nondefault port" do
|
56
|
+
@request[:port] = 8081
|
57
|
+
@request[:headers]['Host'] = 'example.com:8081'
|
58
|
+
@response[:headers]['Sec-WebSocket-Location'] =
|
59
|
+
'ws://example.com:8081/demo'
|
60
|
+
|
61
|
+
handshake(@request).should succeed_with_upgrade(@response)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should handle good request to secure nondefault port" do
|
65
|
+
@secure_request[:port] = 8081
|
66
|
+
@secure_request[:headers]['Host'] = 'example.com:8081'
|
67
|
+
@secure_response[:headers]['Sec-WebSocket-Location'] = 'wss://example.com:8081/demo'
|
68
|
+
|
69
|
+
handshake(@secure_request, true).
|
70
|
+
should succeed_with_upgrade(@secure_response)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should handle good request with no protocol" do
|
74
|
+
@request[:headers].delete('Sec-WebSocket-Protocol')
|
75
|
+
@response[:headers].delete("Sec-WebSocket-Protocol")
|
76
|
+
|
77
|
+
handshake(@request).should succeed_with_upgrade(@response)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should handle extra headers by simply ignoring them" do
|
81
|
+
@request[:headers]['EmptyValue'] = ""
|
82
|
+
@request[:headers]['AKey'] = "AValue"
|
83
|
+
|
84
|
+
handshake(@request).should succeed_with_upgrade(@response)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should raise error on HTTP request" do
|
88
|
+
@request[:headers] = {
|
89
|
+
'Host' => 'www.google.com',
|
90
|
+
'User-Agent' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 GTB6 GTBA',
|
91
|
+
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
92
|
+
'Accept-Language' => 'en-us,en;q=0.5',
|
93
|
+
'Accept-Encoding' => 'gzip,deflate',
|
94
|
+
'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
95
|
+
'Keep-Alive' => '300',
|
96
|
+
'Connection' => 'keep-alive',
|
97
|
+
}
|
98
|
+
|
99
|
+
handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should raise error on wrong method" do
|
103
|
+
@request[:method] = 'POST'
|
104
|
+
|
105
|
+
handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should raise error if upgrade header incorrect" do
|
109
|
+
@request[:headers]['Upgrade'] = 'NonWebSocket'
|
110
|
+
|
111
|
+
handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should raise error if Sec-WebSocket-Protocol is empty" do
|
115
|
+
@request[:headers]['Sec-WebSocket-Protocol'] = ''
|
116
|
+
|
117
|
+
handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError)
|
118
|
+
end
|
119
|
+
|
120
|
+
%w[Sec-WebSocket-Key1 Sec-WebSocket-Key2].each do |header|
|
121
|
+
it "should raise error if #{header} has zero spaces" do
|
122
|
+
@request[:headers][header] = 'nospaces'
|
123
|
+
|
124
|
+
handshake(@request).
|
125
|
+
should fail_with_error(EM::WebSocket::HandshakeError, 'Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should raise error if Sec-WebSocket-Key1 is missing" do
|
130
|
+
@request[:headers].delete("Sec-WebSocket-Key1")
|
131
|
+
|
132
|
+
# The error message isn't correct since key1 is used to heuristically
|
133
|
+
# determine the protocol version in use, however this test at least checks
|
134
|
+
# that the handshake does correctly fail
|
135
|
+
handshake(@request).
|
136
|
+
should fail_with_error(EM::WebSocket::HandshakeError, 'Extra bytes after header')
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should raise error if Sec-WebSocket-Key2 is missing" do
|
140
|
+
@request[:headers].delete("Sec-WebSocket-Key2")
|
141
|
+
|
142
|
+
handshake(@request).
|
143
|
+
should fail_with_error(EM::WebSocket::HandshakeError, 'WebSocket key1 or key2 is missing')
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should raise error if spaces do not divide numbers in Sec-WebSocket-Key* " do
|
147
|
+
@request[:headers]['Sec-WebSocket-Key2'] = '12998 5 Y3 1.P00'
|
148
|
+
|
149
|
+
handshake(@request).
|
150
|
+
should fail_with_error(EM::WebSocket::HandshakeError, 'Invalid Key "12998 5 Y3 1.P00"')
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should raise error if the HTTP header is empty" do
|
154
|
+
handshake = EM::WebSocket::Handshake.new(false)
|
155
|
+
handshake.receive_data("\r\n\r\nfoobar")
|
156
|
+
|
157
|
+
handshake.
|
158
|
+
should fail_with_error(EM::WebSocket::HandshakeError, 'Invalid HTTP header: Could not parse data entirely (4 != 10)')
|
159
|
+
end
|
160
|
+
|
161
|
+
# This might seems crazy, but very occasionally we saw multiple "Upgrade:
|
162
|
+
# WebSocket" headers in the wild. RFC 4.2.1 isn't particularly clear on this
|
163
|
+
# point, so for now I have decided not to accept --@mloughran
|
164
|
+
it "should raise error on multiple upgrade headers" do
|
165
|
+
handshake = EM::WebSocket::Handshake.new(false)
|
166
|
+
|
167
|
+
# Add a duplicate upgrade header
|
168
|
+
headers = format_request(@request)
|
169
|
+
upgrade_header = "Upgrade: WebSocket\r\n"
|
170
|
+
headers.gsub!(upgrade_header, "#{upgrade_header}#{upgrade_header}")
|
171
|
+
|
172
|
+
handshake.receive_data(headers)
|
173
|
+
|
174
|
+
handshake.errback { |e|
|
175
|
+
e.class.should == EM::WebSocket::HandshakeError
|
176
|
+
e.message.should == 'Invalid upgrade header: ["WebSocket", "WebSocket"]'
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
it "should cope with requests where the header is split" do
|
181
|
+
request = format_request(@request)
|
182
|
+
incomplete_request = request[0...(request.length / 2)]
|
183
|
+
rest = request[(request.length / 2)..-1]
|
184
|
+
handshake = EM::WebSocket::Handshake.new(false)
|
185
|
+
handshake.receive_data(incomplete_request)
|
186
|
+
|
187
|
+
handshake.instance_variable_get(:@deferred_status).should == nil
|
188
|
+
|
189
|
+
# Send the remaining header
|
190
|
+
handshake.receive_data(rest)
|
191
|
+
|
192
|
+
handshake(@request).should succeed_with_upgrade(@response)
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should cope with requests where the third key is split" do
|
196
|
+
request = format_request(@request)
|
197
|
+
# Removes last two bytes of the third key
|
198
|
+
incomplete_request = request[0..-3]
|
199
|
+
rest = request[-2..-1]
|
200
|
+
handshake = EM::WebSocket::Handshake.new(false)
|
201
|
+
handshake.receive_data(incomplete_request)
|
202
|
+
|
203
|
+
handshake.instance_variable_get(:@deferred_status).should == nil
|
204
|
+
|
205
|
+
# Send the remaining third key
|
206
|
+
handshake.receive_data(rest)
|
207
|
+
|
208
|
+
handshake(@request).should succeed_with_upgrade(@response)
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should fail if the request URI is invalid" do
|
212
|
+
@request[:path] = "/%"
|
213
|
+
handshake(@request).should \
|
214
|
+
fail_with_error(EM::WebSocket::HandshakeError, 'Invalid request URI: /%')
|
215
|
+
end
|
216
|
+
end
|