em-websocket 0.3.7 → 0.5.2
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.
- 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
@@ -6,9 +6,10 @@ module EventMachine
|
|
6
6
|
def initialize_framing
|
7
7
|
@data = ''
|
8
8
|
@application_data_buffer = '' # Used for MORE frames
|
9
|
+
@frame_type = nil
|
9
10
|
end
|
10
11
|
|
11
|
-
def process_data
|
12
|
+
def process_data
|
12
13
|
error = false
|
13
14
|
|
14
15
|
while !error && @data.size > 1
|
@@ -150,7 +151,7 @@ module EventMachine
|
|
150
151
|
end
|
151
152
|
|
152
153
|
def opcode_to_type(opcode)
|
153
|
-
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode")
|
154
|
+
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
|
154
155
|
end
|
155
156
|
|
156
157
|
def data_frame?(type)
|
@@ -6,9 +6,10 @@ module EventMachine
|
|
6
6
|
def initialize_framing
|
7
7
|
@data = MaskedString.new
|
8
8
|
@application_data_buffer = '' # Used for MORE frames
|
9
|
+
@frame_type = nil
|
9
10
|
end
|
10
11
|
|
11
|
-
def process_data
|
12
|
+
def process_data
|
12
13
|
error = false
|
13
14
|
|
14
15
|
while !error && @data.size > 5 # mask plus first byte present
|
@@ -151,7 +152,7 @@ module EventMachine
|
|
151
152
|
end
|
152
153
|
|
153
154
|
def opcode_to_type(opcode)
|
154
|
-
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode")
|
155
|
+
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
|
155
156
|
end
|
156
157
|
|
157
158
|
def data_frame?(type)
|
@@ -7,9 +7,10 @@ module EventMachine
|
|
7
7
|
def initialize_framing
|
8
8
|
@data = MaskedString.new
|
9
9
|
@application_data_buffer = '' # Used for MORE frames
|
10
|
+
@frame_type = nil
|
10
11
|
end
|
11
12
|
|
12
|
-
def process_data
|
13
|
+
def process_data
|
13
14
|
error = false
|
14
15
|
|
15
16
|
while !error && @data.size >= 2
|
@@ -86,8 +87,19 @@ module EventMachine
|
|
86
87
|
|
87
88
|
frame_type = opcode_to_type(opcode)
|
88
89
|
|
89
|
-
if frame_type == :continuation
|
90
|
-
|
90
|
+
if frame_type == :continuation
|
91
|
+
if !@frame_type
|
92
|
+
raise WSProtocolError, 'Continuation frame not expected'
|
93
|
+
end
|
94
|
+
else # Not a continuation frame
|
95
|
+
if @frame_type && data_frame?(frame_type)
|
96
|
+
raise WSProtocolError, "Continuation frame expected"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Validate that control frames are not fragmented
|
101
|
+
if !fin && !data_frame?(frame_type)
|
102
|
+
raise WSProtocolError, 'Control frames must not be fragmented'
|
91
103
|
end
|
92
104
|
|
93
105
|
if !fin
|
@@ -162,7 +174,7 @@ module EventMachine
|
|
162
174
|
end
|
163
175
|
|
164
176
|
def opcode_to_type(opcode)
|
165
|
-
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode")
|
177
|
+
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
|
166
178
|
end
|
167
179
|
|
168
180
|
def data_frame?(type)
|
@@ -7,7 +7,7 @@ module EventMachine
|
|
7
7
|
@data = ''
|
8
8
|
end
|
9
9
|
|
10
|
-
def process_data
|
10
|
+
def process_data
|
11
11
|
debug [:message, @data]
|
12
12
|
|
13
13
|
# This algorithm comes straight from the spec
|
@@ -71,9 +71,6 @@ module EventMachine
|
|
71
71
|
raise WSMessageTooBigError, "Frame length too long (#{@data.size} bytes)"
|
72
72
|
end
|
73
73
|
|
74
|
-
# Optimization to avoid calling slice! unnecessarily
|
75
|
-
error = true and next unless newdata =~ /\xff/
|
76
|
-
|
77
74
|
msg = @data.slice!(/\A\x00[^\xff]*\xff/)
|
78
75
|
if msg
|
79
76
|
msg.gsub!(/\A\x00|\xff\z/, '')
|
data/lib/em-websocket/handler.rb
CHANGED
@@ -1,40 +1,86 @@
|
|
1
1
|
module EventMachine
|
2
2
|
module WebSocket
|
3
3
|
class Handler
|
4
|
+
def self.klass_factory(version)
|
5
|
+
case version
|
6
|
+
when 75
|
7
|
+
Handler75
|
8
|
+
when 76
|
9
|
+
Handler76
|
10
|
+
when 1..3
|
11
|
+
# We'll use handler03 - I believe they're all compatible
|
12
|
+
Handler03
|
13
|
+
when 5
|
14
|
+
Handler05
|
15
|
+
when 6
|
16
|
+
Handler06
|
17
|
+
when 7
|
18
|
+
Handler07
|
19
|
+
when 8
|
20
|
+
# drafts 9, 10, 11 and 12 should never change the version
|
21
|
+
# number as they are all the same as version 08.
|
22
|
+
Handler08
|
23
|
+
when 13
|
24
|
+
# drafts 13 to 17 all identify as version 13 as they are
|
25
|
+
# only minor changes or text changes.
|
26
|
+
Handler13
|
27
|
+
else
|
28
|
+
# According to spec should abort the connection
|
29
|
+
raise HandshakeError, "Protocol version #{version} not supported"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
4
33
|
include Debugger
|
5
34
|
|
6
35
|
attr_reader :request, :state
|
7
36
|
|
8
|
-
def initialize(connection,
|
9
|
-
@connection
|
37
|
+
def initialize(connection, debug = false)
|
38
|
+
@connection = connection
|
10
39
|
@debug = debug
|
11
|
-
@state = :
|
40
|
+
@state = :connected
|
41
|
+
@close_timer = nil
|
12
42
|
initialize_framing
|
13
43
|
end
|
14
44
|
|
15
|
-
def
|
16
|
-
@
|
17
|
-
|
18
|
-
|
45
|
+
def receive_data(data)
|
46
|
+
@data << data
|
47
|
+
process_data
|
48
|
+
rescue WSProtocolError => e
|
49
|
+
fail_websocket(e)
|
19
50
|
end
|
20
51
|
|
21
|
-
|
22
|
-
def handshake
|
52
|
+
def close_websocket(code, body)
|
23
53
|
# Implemented in subclass
|
24
54
|
end
|
25
55
|
|
26
|
-
|
27
|
-
|
28
|
-
|
56
|
+
# Used to avoid un-acked and unclosed remaining open indefinitely
|
57
|
+
def start_close_timeout
|
58
|
+
@close_timer = EM::Timer.new(@connection.close_timeout) {
|
59
|
+
@connection.close_connection
|
60
|
+
e = WSProtocolError.new("Close handshake un-acked after #{@connection.close_timeout}s, closing tcp connection")
|
61
|
+
@connection.trigger_on_error(e)
|
62
|
+
}
|
29
63
|
end
|
30
64
|
|
31
|
-
|
32
|
-
|
65
|
+
# This corresponds to "Fail the WebSocket Connection" in the spec.
|
66
|
+
def fail_websocket(e)
|
67
|
+
debug [:error, e]
|
68
|
+
close_websocket(e.code, e.message)
|
69
|
+
@connection.close_connection_after_writing
|
70
|
+
@connection.trigger_on_error(e)
|
33
71
|
end
|
34
72
|
|
35
73
|
def unbind
|
36
74
|
@state = :closed
|
37
|
-
|
75
|
+
|
76
|
+
@close_timer.cancel if @close_timer
|
77
|
+
|
78
|
+
@close_info = defined?(@close_info) ? @close_info : {
|
79
|
+
:code => 1006,
|
80
|
+
:was_clean => false,
|
81
|
+
}
|
82
|
+
|
83
|
+
@connection.trigger_on_close(@close_info)
|
38
84
|
end
|
39
85
|
|
40
86
|
def ping
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require "http/parser"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
module WebSocket
|
6
|
+
|
7
|
+
# Resposible for creating the server handshake response
|
8
|
+
class Handshake
|
9
|
+
include EM::Deferrable
|
10
|
+
|
11
|
+
attr_reader :parser, :protocol_version
|
12
|
+
|
13
|
+
# Unfortunately drafts 75 & 76 require knowledge of whether the
|
14
|
+
# connection is being terminated as ws/wss in order to generate the
|
15
|
+
# correct handshake response
|
16
|
+
def initialize(secure)
|
17
|
+
@parser = Http::Parser.new
|
18
|
+
@secure = secure
|
19
|
+
|
20
|
+
@parser.on_headers_complete = proc { |headers|
|
21
|
+
@headers = Hash[headers.map { |k,v| [k.downcase, v] }]
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def receive_data(data)
|
26
|
+
@parser << data
|
27
|
+
|
28
|
+
if defined? @headers
|
29
|
+
process(@headers, @parser.upgrade_data)
|
30
|
+
end
|
31
|
+
rescue HTTP::Parser::Error => e
|
32
|
+
fail(HandshakeError.new("Invalid HTTP header: #{e.message}"))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the WebSocket upgrade headers as a hash.
|
36
|
+
#
|
37
|
+
# Keys are strings, unmodified from the request.
|
38
|
+
#
|
39
|
+
def headers
|
40
|
+
@parser.headers
|
41
|
+
end
|
42
|
+
|
43
|
+
# The same as headers, except that the hash keys are downcased
|
44
|
+
#
|
45
|
+
def headers_downcased
|
46
|
+
@headers
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the request path (excluding any query params)
|
50
|
+
#
|
51
|
+
def path
|
52
|
+
@path
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the query params as a string foo=bar&baz=...
|
56
|
+
def query_string
|
57
|
+
@query_string
|
58
|
+
end
|
59
|
+
|
60
|
+
def query
|
61
|
+
Hash[query_string.split('&').map { |c| c.split('=', 2) }]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the WebSocket origin header if provided
|
65
|
+
#
|
66
|
+
def origin
|
67
|
+
@headers["origin"] || @headers["sec-websocket-origin"] || nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def secure?
|
71
|
+
@secure
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def process(headers, remains)
|
77
|
+
unless @parser.http_method == "GET"
|
78
|
+
raise HandshakeError, "Must be GET request"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Validate request path
|
82
|
+
#
|
83
|
+
# According to http://tools.ietf.org/search/rfc2616#section-5.1.2, an
|
84
|
+
# invalid Request-URI should result in a 400 status code, but
|
85
|
+
# HandshakeError's currently result in a WebSocket abort. It's not
|
86
|
+
# clear which should take precedence, but an abort will do just fine.
|
87
|
+
begin
|
88
|
+
uri = URI.parse(@parser.request_url)
|
89
|
+
@path = uri.path
|
90
|
+
@query_string = uri.query || ""
|
91
|
+
rescue URI::InvalidURIError
|
92
|
+
raise HandshakeError, "Invalid request URI: #{@parser.request_url}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Validate Upgrade
|
96
|
+
unless @parser.upgrade?
|
97
|
+
raise HandshakeError, "Not an upgrade request"
|
98
|
+
end
|
99
|
+
upgrade = @headers['upgrade']
|
100
|
+
unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket'
|
101
|
+
raise HandshakeError, "Invalid upgrade header: #{upgrade.inspect}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Determine version heuristically
|
105
|
+
version = if @headers['sec-websocket-version']
|
106
|
+
# Used from drafts 04 onwards
|
107
|
+
@headers['sec-websocket-version'].to_i
|
108
|
+
elsif @headers['sec-websocket-draft']
|
109
|
+
# Used in drafts 01 - 03
|
110
|
+
@headers['sec-websocket-draft'].to_i
|
111
|
+
elsif @headers['sec-websocket-key1']
|
112
|
+
76
|
113
|
+
else
|
114
|
+
75
|
115
|
+
end
|
116
|
+
|
117
|
+
# Additional handling of bytes after the header if required
|
118
|
+
case version
|
119
|
+
when 75
|
120
|
+
if !remains.empty?
|
121
|
+
raise HandshakeError, "Extra bytes after header"
|
122
|
+
end
|
123
|
+
when 76, 1..3
|
124
|
+
if remains.length < 8
|
125
|
+
# The whole third-key has not been received yet.
|
126
|
+
return nil
|
127
|
+
elsif remains.length > 8
|
128
|
+
raise HandshakeError, "Extra bytes after third key"
|
129
|
+
end
|
130
|
+
@headers['third-key'] = remains
|
131
|
+
end
|
132
|
+
|
133
|
+
handshake_klass = case version
|
134
|
+
when 75
|
135
|
+
Handshake75
|
136
|
+
when 76, 1..3
|
137
|
+
Handshake76
|
138
|
+
when 5, 6, 7, 8, 13
|
139
|
+
Handshake04
|
140
|
+
else
|
141
|
+
# According to spec should abort the connection
|
142
|
+
raise HandshakeError, "Protocol version #{version} not supported"
|
143
|
+
end
|
144
|
+
|
145
|
+
upgrade_response = handshake_klass.handshake(@headers, @parser.request_url, @secure)
|
146
|
+
|
147
|
+
handler_klass = Handler.klass_factory(version)
|
148
|
+
|
149
|
+
@protocol_version = version
|
150
|
+
succeed(upgrade_response, handler_klass)
|
151
|
+
rescue HandshakeError => e
|
152
|
+
fail(e)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -4,32 +4,34 @@ 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
|
-
|
13
|
-
# Optional
|
14
|
-
origin = request['sec-websocket-origin']
|
15
|
-
protocols = request['sec-websocket-protocol']
|
16
|
-
extensions = request['sec-websocket-extensions']
|
17
|
-
|
12
|
+
|
18
13
|
string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
19
14
|
signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
|
20
|
-
|
15
|
+
|
21
16
|
upgrade = ["HTTP/1.1 101 Switching Protocols"]
|
22
17
|
upgrade << "Upgrade: websocket"
|
23
18
|
upgrade << "Connection: Upgrade"
|
24
19
|
upgrade << "Sec-WebSocket-Accept: #{signature}"
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
20
|
+
if protocol = headers['sec-websocket-protocol']
|
21
|
+
validate_protocol!(protocol)
|
22
|
+
upgrade << "Sec-WebSocket-Protocol: #{protocol}"
|
23
|
+
end
|
24
|
+
|
25
|
+
# TODO: Support sec-websocket-protocol selection
|
26
|
+
# TODO: sec-websocket-extensions
|
27
|
+
|
31
28
|
return upgrade.join("\r\n") + "\r\n\r\n"
|
32
29
|
end
|
30
|
+
|
31
|
+
def self.validate_protocol!(protocol)
|
32
|
+
raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
|
33
|
+
# TODO: Validate characters
|
34
|
+
end
|
33
35
|
end
|
34
36
|
end
|
35
37
|
end
|