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
@@ -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
|