em-websocket 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +13 -0
- data/README.md +62 -35
- data/examples/echo.rb +22 -6
- data/examples/test.html +2 -5
- data/lib/em-websocket.rb +2 -1
- data/lib/em-websocket/connection.rb +38 -25
- data/lib/em-websocket/handler.rb +32 -14
- 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/handshake.rb +137 -0
- data/lib/em-websocket/handshake04.rb +13 -15
- data/lib/em-websocket/handshake75.rb +4 -7
- data/lib/em-websocket/handshake76.rb +11 -14
- data/lib/em-websocket/version.rb +1 -1
- data/lib/em-websocket/websocket.rb +17 -15
- data/spec/helper.rb +21 -3
- data/spec/integration/common_spec.rb +57 -54
- data/spec/integration/draft03_spec.rb +2 -0
- data/spec/integration/draft13_spec.rb +2 -0
- data/spec/integration/draft75_spec.rb +3 -1
- data/spec/integration/draft76_spec.rb +3 -1
- data/spec/integration/shared_examples.rb +26 -0
- data/spec/unit/{handler_spec.rb → handshake_spec.rb} +89 -55
- metadata +112 -127
- 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
@@ -0,0 +1,137 @@
|
|
1
|
+
require "http/parser"
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module WebSocket
|
5
|
+
|
6
|
+
# Resposible for creating the server handshake response
|
7
|
+
class Handshake
|
8
|
+
include EM::Deferrable
|
9
|
+
|
10
|
+
attr_reader :parser, :protocol_version
|
11
|
+
|
12
|
+
# Unfortunately drafts 75 & 76 require knowledge of whether the
|
13
|
+
# connection is being terminated as ws/wss in order to generate the
|
14
|
+
# correct handshake response
|
15
|
+
def initialize(secure)
|
16
|
+
@parser = Http::Parser.new
|
17
|
+
@secure = secure
|
18
|
+
|
19
|
+
@parser.on_headers_complete = proc { |headers|
|
20
|
+
@headers = Hash[headers.map { |k,v| [k.downcase, v] }]
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def receive_data(data)
|
25
|
+
@parser << data
|
26
|
+
|
27
|
+
if @headers
|
28
|
+
process(@headers, @parser.upgrade_data)
|
29
|
+
end
|
30
|
+
rescue HTTP::Parser::Error => e
|
31
|
+
fail(HandshakeError.new("Invalid HTTP header: #{e.message}"))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the WebSocket upgrade headers as a hash.
|
35
|
+
#
|
36
|
+
# Keys are strings, unmodified from the request.
|
37
|
+
#
|
38
|
+
def headers
|
39
|
+
@parser.headers
|
40
|
+
end
|
41
|
+
|
42
|
+
# The same as headers, except that the hash keys are downcased
|
43
|
+
#
|
44
|
+
def headers_downcased
|
45
|
+
@headers
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the request path (excluding any query params)
|
49
|
+
#
|
50
|
+
def path
|
51
|
+
@parser.request_path
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the query params as a string foo=bar&baz=...
|
55
|
+
def query_string
|
56
|
+
@parser.query_string
|
57
|
+
end
|
58
|
+
|
59
|
+
def query
|
60
|
+
Hash[query_string.split('&').map { |c| c.split('=', 2) }]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the WebSocket origin header if provided
|
64
|
+
#
|
65
|
+
def origin
|
66
|
+
@headers["origin"] || @headers["sec-websocket-origin"] || nil
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def process(headers, remains)
|
72
|
+
unless @parser.http_method == "GET"
|
73
|
+
raise HandshakeError, "Must be GET request"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Validate Upgrade
|
77
|
+
unless @parser.upgrade?
|
78
|
+
raise HandshakeError, "Not an upgrade request"
|
79
|
+
end
|
80
|
+
upgrade = @headers['upgrade']
|
81
|
+
unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket'
|
82
|
+
raise HandshakeError, "Invalid upgrade header: #{upgrade}"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Determine version heuristically
|
86
|
+
version = if @headers['sec-websocket-version']
|
87
|
+
# Used from drafts 04 onwards
|
88
|
+
@headers['sec-websocket-version'].to_i
|
89
|
+
elsif @headers['sec-websocket-draft']
|
90
|
+
# Used in drafts 01 - 03
|
91
|
+
@headers['sec-websocket-draft'].to_i
|
92
|
+
elsif @headers['sec-websocket-key1']
|
93
|
+
76
|
94
|
+
else
|
95
|
+
75
|
96
|
+
end
|
97
|
+
|
98
|
+
# Additional handling of bytes after the header if required
|
99
|
+
case version
|
100
|
+
when 75
|
101
|
+
if !remains.empty?
|
102
|
+
raise HandshakeError, "Extra bytes after header"
|
103
|
+
end
|
104
|
+
when 76, 1..3
|
105
|
+
if remains.length < 8
|
106
|
+
# The whole third-key has not been received yet.
|
107
|
+
return nil
|
108
|
+
elsif remains.length > 8
|
109
|
+
raise HandshakeError, "Extra bytes after third key"
|
110
|
+
end
|
111
|
+
@headers['third-key'] = remains
|
112
|
+
end
|
113
|
+
|
114
|
+
handshake_klass = case version
|
115
|
+
when 75
|
116
|
+
Handshake75
|
117
|
+
when 76, 1..3
|
118
|
+
Handshake76
|
119
|
+
when 5, 6, 7, 8, 13
|
120
|
+
Handshake04
|
121
|
+
else
|
122
|
+
# According to spec should abort the connection
|
123
|
+
raise HandshakeError, "Protocol version #{version} not supported"
|
124
|
+
end
|
125
|
+
|
126
|
+
upgrade_response = handshake_klass.handshake(@headers, @parser.request_url, @secure)
|
127
|
+
|
128
|
+
handler_klass = Handler.klass_factory(version)
|
129
|
+
|
130
|
+
@protocol_version = version
|
131
|
+
succeed(upgrade_response, handler_klass)
|
132
|
+
rescue HandshakeError => e
|
133
|
+
fail(e)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -4,30 +4,28 @@ require 'base64'
|
|
4
4
|
module EventMachine
|
5
5
|
module WebSocket
|
6
6
|
module Handshake04
|
7
|
-
def handshake
|
7
|
+
def self.handshake(headers, _, __)
|
8
8
|
# Required
|
9
|
-
unless key =
|
10
|
-
raise HandshakeError, "
|
9
|
+
unless key = headers['sec-websocket-key']
|
10
|
+
raise HandshakeError, "sec-websocket-key header is required"
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
# Optional
|
14
|
-
origin =
|
15
|
-
protocols =
|
16
|
-
extensions =
|
17
|
-
|
14
|
+
origin = headers['sec-websocket-origin']
|
15
|
+
protocols = headers['sec-websocket-protocol']
|
16
|
+
extensions = headers['sec-websocket-extensions']
|
17
|
+
|
18
18
|
string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
19
19
|
signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
|
20
|
-
|
20
|
+
|
21
21
|
upgrade = ["HTTP/1.1 101 Switching Protocols"]
|
22
22
|
upgrade << "Upgrade: websocket"
|
23
23
|
upgrade << "Connection: Upgrade"
|
24
24
|
upgrade << "Sec-WebSocket-Accept: #{signature}"
|
25
|
-
|
26
|
-
# TODO: Support
|
27
|
-
# TODO:
|
28
|
-
|
29
|
-
debug [:upgrade_headers, upgrade]
|
30
|
-
|
25
|
+
|
26
|
+
# TODO: Support sec-websocket-protocol
|
27
|
+
# TODO: sec-websocket-extensions
|
28
|
+
|
31
29
|
return upgrade.join("\r\n") + "\r\n\r\n"
|
32
30
|
end
|
33
31
|
end
|
@@ -1,19 +1,16 @@
|
|
1
1
|
module EventMachine
|
2
2
|
module WebSocket
|
3
3
|
module Handshake75
|
4
|
-
def handshake
|
5
|
-
|
6
|
-
location
|
7
|
-
location << request['path']
|
4
|
+
def self.handshake(headers, path, secure)
|
5
|
+
scheme = (secure ? "wss" : "ws")
|
6
|
+
location = "#{scheme}://#{headers['host']}#{path}"
|
8
7
|
|
9
8
|
upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
10
9
|
upgrade << "Upgrade: WebSocket\r\n"
|
11
10
|
upgrade << "Connection: Upgrade\r\n"
|
12
|
-
upgrade << "WebSocket-Origin: #{
|
11
|
+
upgrade << "WebSocket-Origin: #{headers['origin']}\r\n"
|
13
12
|
upgrade << "WebSocket-Location: #{location}\r\n\r\n"
|
14
13
|
|
15
|
-
debug [:upgrade_headers, upgrade]
|
16
|
-
|
17
14
|
return upgrade
|
18
15
|
end
|
19
16
|
end
|
@@ -1,33 +1,30 @@
|
|
1
1
|
require 'digest/md5'
|
2
2
|
|
3
|
-
module EventMachine
|
4
|
-
module
|
5
|
-
|
6
|
-
def handshake
|
3
|
+
module EventMachine::WebSocket
|
4
|
+
module Handshake76
|
5
|
+
class << self
|
6
|
+
def handshake(headers, path, secure)
|
7
7
|
challenge_response = solve_challenge(
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
headers['sec-websocket-key1'],
|
9
|
+
headers['sec-websocket-key2'],
|
10
|
+
headers['third-key']
|
11
11
|
)
|
12
12
|
|
13
|
-
|
14
|
-
location
|
15
|
-
location << request['path']
|
13
|
+
scheme = (secure ? "wss" : "ws")
|
14
|
+
location = "#{scheme}://#{headers['host']}#{path}"
|
16
15
|
|
17
16
|
upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
|
18
17
|
upgrade << "Upgrade: WebSocket\r\n"
|
19
18
|
upgrade << "Connection: Upgrade\r\n"
|
20
19
|
upgrade << "Sec-WebSocket-Location: #{location}\r\n"
|
21
|
-
upgrade << "Sec-WebSocket-Origin: #{
|
22
|
-
if protocol =
|
20
|
+
upgrade << "Sec-WebSocket-Origin: #{headers['origin']}\r\n"
|
21
|
+
if protocol = headers['sec-websocket-protocol']
|
23
22
|
validate_protocol!(protocol)
|
24
23
|
upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
|
25
24
|
end
|
26
25
|
upgrade << "\r\n"
|
27
26
|
upgrade << challenge_response
|
28
27
|
|
29
|
-
debug [:upgrade_headers, upgrade]
|
30
|
-
|
31
28
|
return upgrade
|
32
29
|
end
|
33
30
|
|
data/lib/em-websocket/version.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
module EventMachine
|
2
2
|
module WebSocket
|
3
|
+
class << self
|
4
|
+
attr_accessor :max_frame_size
|
5
|
+
end
|
6
|
+
@max_frame_size = 10 * 1024 * 1024 # 10MB
|
7
|
+
|
3
8
|
# All errors raised by em-websocket should descend from this class
|
4
|
-
#
|
5
9
|
class WebSocketError < RuntimeError; end
|
6
10
|
|
7
11
|
# Used for errors that occur during WebSocket handshake
|
8
|
-
#
|
9
12
|
class HandshakeError < WebSocketError; end
|
10
13
|
|
11
14
|
# Used for errors which should cause the connection to close.
|
12
15
|
# See RFC6455 §7.4.1 for a full description of meanings
|
13
|
-
#
|
14
16
|
class WSProtocolError < WebSocketError
|
15
17
|
def code; 1002; end
|
16
18
|
end
|
@@ -20,28 +22,28 @@ module EventMachine
|
|
20
22
|
def code; 1009; end
|
21
23
|
end
|
22
24
|
|
25
|
+
# Start WebSocket server, including starting eventmachine run loop
|
23
26
|
def self.start(options, &blk)
|
24
27
|
EM.epoll
|
25
|
-
EM.run
|
26
|
-
|
28
|
+
EM.run {
|
27
29
|
trap("TERM") { stop }
|
28
30
|
trap("INT") { stop }
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
32
|
+
run(options, &blk)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Start WebSocket server inside eventmachine run loop
|
37
|
+
def self.run(options)
|
38
|
+
host, port = options.values_at(:host, :port)
|
39
|
+
EM.start_server(host, port, Connection, options) do |c|
|
40
|
+
yield c
|
34
41
|
end
|
35
42
|
end
|
36
43
|
|
37
44
|
def self.stop
|
38
45
|
puts "Terminating WebSocket Server"
|
39
|
-
|
40
|
-
end
|
41
|
-
|
42
|
-
class << self
|
43
|
-
attr_accessor :max_frame_size
|
46
|
+
EM.stop
|
44
47
|
end
|
45
|
-
@max_frame_size = 10 * 1024 * 1024 # 10MB
|
46
48
|
end
|
47
49
|
end
|
data/spec/helper.rb
CHANGED
@@ -103,7 +103,10 @@ class Draft75WebSocketClient
|
|
103
103
|
def onmessage(&blk); @onmessage = blk; end
|
104
104
|
|
105
105
|
def initialize
|
106
|
-
@ws = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get(
|
106
|
+
@ws = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get({
|
107
|
+
:timeout => 0,
|
108
|
+
:origin => 'http://example.com',
|
109
|
+
})
|
107
110
|
@ws.errback { @onerror.call if @onerror }
|
108
111
|
@ws.callback { @onopen.call if @onopen }
|
109
112
|
@ws.stream { |msg| @onmessage.call(msg) if @onmessage }
|
@@ -133,8 +136,23 @@ def format_response(r)
|
|
133
136
|
data
|
134
137
|
end
|
135
138
|
|
136
|
-
RSpec::Matchers.define :
|
139
|
+
RSpec::Matchers.define :succeed_with_upgrade do |response|
|
137
140
|
match do |actual|
|
138
|
-
|
141
|
+
success = nil
|
142
|
+
actual.callback { |upgrade_response, handler_klass|
|
143
|
+
success = (upgrade_response.lines.sort == format_response(response).lines.sort)
|
144
|
+
}
|
145
|
+
success
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
RSpec::Matchers.define :fail_with_error do |error_klass, error_message|
|
150
|
+
match do |actual|
|
151
|
+
success = nil
|
152
|
+
actual.errback { |e|
|
153
|
+
success = (e.class == error_klass)
|
154
|
+
success &= (e.message == error_message) if error_message
|
155
|
+
}
|
156
|
+
success
|
139
157
|
end
|
140
158
|
end
|
@@ -1,27 +1,27 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
# These tests are not specific to any particular draft of the specification
|
4
|
-
#
|
4
|
+
#
|
5
5
|
describe "WebSocket server" do
|
6
6
|
include EM::SpecHelper
|
7
7
|
default_timeout 1
|
8
8
|
|
9
9
|
it "should fail on non WebSocket requests" do
|
10
10
|
em {
|
11
|
-
|
12
|
-
http =
|
11
|
+
EM.add_timer(0.1) do
|
12
|
+
http = EM::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0
|
13
13
|
http.errback { done }
|
14
14
|
http.callback { fail }
|
15
15
|
end
|
16
16
|
|
17
|
-
|
17
|
+
EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) {}
|
18
18
|
}
|
19
19
|
end
|
20
|
-
|
21
|
-
it "should
|
20
|
+
|
21
|
+
it "should expose the WebSocket request headers, path and query params" do
|
22
22
|
em {
|
23
|
-
|
24
|
-
http =
|
23
|
+
EM.add_timer(0.1) do
|
24
|
+
http = EM::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
|
25
25
|
http.errback { fail }
|
26
26
|
http.callback {
|
27
27
|
http.response_header.status.should == 101
|
@@ -30,27 +30,32 @@ describe "WebSocket server" do
|
|
30
30
|
http.stream { |msg| }
|
31
31
|
end
|
32
32
|
|
33
|
-
|
34
|
-
ws.onopen {
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
33
|
+
EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
|
34
|
+
ws.onopen { |handshake|
|
35
|
+
headers = handshake.headers
|
36
|
+
headers["User-Agent"].should == "EventMachine HttpClient"
|
37
|
+
headers["Connection"].should == "Upgrade"
|
38
|
+
headers["Upgrade"].should == "WebSocket"
|
39
|
+
headers["Host"].to_s.should == "127.0.0.1:12345"
|
40
|
+
handshake.path.should == "/"
|
41
|
+
handshake.query.should == {}
|
42
|
+
handshake.origin.should == "127.0.0.1"
|
41
43
|
}
|
42
44
|
ws.onclose {
|
43
45
|
ws.state.should == :closed
|
44
|
-
|
46
|
+
done
|
45
47
|
}
|
46
48
|
end
|
47
49
|
}
|
48
50
|
end
|
49
|
-
|
50
|
-
it "should
|
51
|
+
|
52
|
+
it "should expose the WebSocket path and query params when nonempty" do
|
51
53
|
em {
|
52
|
-
|
53
|
-
http =
|
54
|
+
EM.add_timer(0.1) do
|
55
|
+
http = EM::HttpRequest.new('ws://127.0.0.1:12345/hello').get({
|
56
|
+
:query => {'foo' => 'bar', 'baz' => 'qux'},
|
57
|
+
:timeout => 0
|
58
|
+
})
|
54
59
|
http.errback { fail }
|
55
60
|
http.callback {
|
56
61
|
http.response_header.status.should == 101
|
@@ -59,26 +64,41 @@ describe "WebSocket server" do
|
|
59
64
|
http.stream { |msg| }
|
60
65
|
end
|
61
66
|
|
62
|
-
|
63
|
-
ws.onopen {
|
64
|
-
path
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
ws.request["query"]["baz"].should == "qux"
|
67
|
+
EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
|
68
|
+
ws.onopen { |handshake|
|
69
|
+
handshake.path.should == '/hello'
|
70
|
+
handshake.query_string.split('&').sort.
|
71
|
+
should == ["baz=qux", "foo=bar"]
|
72
|
+
handshake.query.should == {"foo"=>"bar", "baz"=>"qux"}
|
69
73
|
}
|
70
74
|
ws.onclose {
|
71
75
|
ws.state.should == :closed
|
72
|
-
|
76
|
+
done
|
73
77
|
}
|
74
78
|
end
|
75
79
|
}
|
76
80
|
end
|
77
|
-
|
78
|
-
it "should
|
81
|
+
|
82
|
+
it "should raise an exception if frame sent before handshake complete" do
|
79
83
|
em {
|
80
|
-
|
81
|
-
|
84
|
+
# 1. Start WebSocket server
|
85
|
+
EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) { |ws|
|
86
|
+
# 3. Try to send a message to the socket
|
87
|
+
lambda {
|
88
|
+
ws.send('early message')
|
89
|
+
}.should raise_error('Cannot send data before onopen callback')
|
90
|
+
done
|
91
|
+
}
|
92
|
+
|
93
|
+
# 2. Connect a dumb TCP connection (will not send handshake)
|
94
|
+
EM.connect('0.0.0.0', 12345, EM::Connection)
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should allow the server to be started inside an existing EM" do
|
99
|
+
em {
|
100
|
+
EM.add_timer(0.1) do
|
101
|
+
http = EM::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
|
82
102
|
http.errback { fail }
|
83
103
|
http.callback {
|
84
104
|
http.response_header.status.should == 101
|
@@ -87,32 +107,15 @@ describe "WebSocket server" do
|
|
87
107
|
http.stream { |msg| }
|
88
108
|
end
|
89
109
|
|
90
|
-
|
91
|
-
ws.onopen {
|
92
|
-
|
93
|
-
ws.request["query"].should == {}
|
110
|
+
EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) do |ws|
|
111
|
+
ws.onopen { |handshake|
|
112
|
+
handshake.headers["User-Agent"].should == "EventMachine HttpClient"
|
94
113
|
}
|
95
114
|
ws.onclose {
|
96
115
|
ws.state.should == :closed
|
97
|
-
EventMachine.stop
|
98
|
-
}
|
99
|
-
end
|
100
|
-
}
|
101
|
-
end
|
102
|
-
|
103
|
-
it "should raise an exception if frame sent before handshake complete" do
|
104
|
-
em {
|
105
|
-
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) { |c|
|
106
|
-
# We're not using a real client so the handshake will not be sent
|
107
|
-
EM.add_timer(0.1) {
|
108
|
-
lambda {
|
109
|
-
c.send('early message')
|
110
|
-
}.should raise_error('Cannot send data before onopen callback')
|
111
116
|
done
|
112
117
|
}
|
113
|
-
|
114
|
-
|
115
|
-
client = EM.connect('0.0.0.0', 12345, EM::Connection)
|
118
|
+
end
|
116
119
|
}
|
117
120
|
end
|
118
121
|
end
|