sonixlabs-em-websocket 0.3.8 → 0.5.1.1
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 +69 -0
- data/Gemfile +6 -0
- data/LICENCE +7 -0
- data/README.md +100 -56
- data/README.md.BACKUP.14928.md +195 -0
- data/README.md.BASE.14928.md +77 -0
- data/README.md.LOCAL.14928.md +98 -0
- data/README.md.REMOTE.14928.md +142 -0
- data/examples/echo.rb +23 -7
- data/examples/ping.rb +24 -0
- data/examples/test.html +5 -6
- data/lib/em-websocket.rb +4 -2
- 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 +219 -73
- data/lib/em-websocket/framing03.rb +6 -11
- data/lib/em-websocket/framing05.rb +6 -11
- data/lib/em-websocket/framing07.rb +25 -20
- data/lib/em-websocket/framing76.rb +6 -15
- data/lib/em-websocket/handler.rb +69 -28
- 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 -56
- data/lib/em-websocket/handshake75.rb +15 -8
- data/lib/em-websocket/handshake76.rb +15 -14
- data/lib/em-websocket/masking04.rb +4 -30
- data/lib/em-websocket/message_processor_03.rb +13 -4
- data/lib/em-websocket/message_processor_06.rb +25 -13
- data/lib/em-websocket/version.rb +1 -1
- data/lib/em-websocket/websocket.rb +35 -24
- data/spec/helper.rb +82 -55
- data/spec/integration/common_spec.rb +90 -70
- data/spec/integration/draft03_spec.rb +84 -56
- data/spec/integration/draft05_spec.rb +14 -12
- data/spec/integration/draft06_spec.rb +66 -9
- data/spec/integration/draft13_spec.rb +59 -29
- data/spec/integration/draft75_spec.rb +46 -40
- data/spec/integration/draft76_spec.rb +113 -109
- data/spec/integration/gte_03_examples.rb +42 -0
- data/spec/integration/shared_examples.rb +174 -0
- data/spec/unit/framing_spec.rb +83 -110
- data/spec/unit/handshake_spec.rb +216 -0
- data/spec/unit/masking_spec.rb +2 -0
- metadata +31 -71
- 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 -107
- data/spec/unit/handler_spec.rb +0 -147
@@ -3,17 +3,13 @@
|
|
3
3
|
module EventMachine
|
4
4
|
module WebSocket
|
5
5
|
module Framing05
|
6
|
-
|
7
|
-
# Set the max frame lenth to very high value (10MB) until there is a
|
8
|
-
# limit specified in the spec to protect against malicious attacks
|
9
|
-
MAXIMUM_FRAME_LENGTH = 10 * 1024 * 1024
|
10
|
-
|
11
6
|
def initialize_framing
|
12
7
|
@data = MaskedString.new
|
13
8
|
@application_data_buffer = '' # Used for MORE frames
|
9
|
+
@frame_type = nil
|
14
10
|
end
|
15
11
|
|
16
|
-
def process_data
|
12
|
+
def process_data
|
17
13
|
error = false
|
18
14
|
|
19
15
|
while !error && @data.size > 5 # mask plus first byte present
|
@@ -60,9 +56,8 @@ module EventMachine
|
|
60
56
|
length
|
61
57
|
end
|
62
58
|
|
63
|
-
|
64
|
-
|
65
|
-
raise DataError, "Frame length too long (#{payload_length} bytes)"
|
59
|
+
if payload_length > @connection.max_frame_size
|
60
|
+
raise WSMessageTooBigError, "Frame length too long (#{payload_length} bytes)"
|
66
61
|
end
|
67
62
|
|
68
63
|
# Check buffer size
|
@@ -83,7 +78,7 @@ module EventMachine
|
|
83
78
|
frame_type = opcode_to_type(opcode)
|
84
79
|
|
85
80
|
if frame_type == :continuation && !@frame_type
|
86
|
-
raise
|
81
|
+
raise WSProtocolError, 'Continuation frame not expected'
|
87
82
|
end
|
88
83
|
|
89
84
|
if !fin
|
@@ -157,7 +152,7 @@ module EventMachine
|
|
157
152
|
end
|
158
153
|
|
159
154
|
def opcode_to_type(opcode)
|
160
|
-
FRAME_TYPES_INVERSE[opcode] || raise(
|
155
|
+
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
|
161
156
|
end
|
162
157
|
|
163
158
|
def data_frame?(type)
|
@@ -4,16 +4,13 @@ module EventMachine
|
|
4
4
|
module WebSocket
|
5
5
|
module Framing07
|
6
6
|
|
7
|
-
attr_accessor :mask_outbound_messages, :require_masked_inbound_messages
|
8
|
-
|
9
7
|
def initialize_framing
|
10
8
|
@data = MaskedString.new
|
11
9
|
@application_data_buffer = '' # Used for MORE frames
|
12
|
-
@
|
13
|
-
@require_masked_inbound_messages = true
|
10
|
+
@frame_type = nil
|
14
11
|
end
|
15
12
|
|
16
|
-
def process_data
|
13
|
+
def process_data
|
17
14
|
error = false
|
18
15
|
|
19
16
|
while !error && @data.size >= 2
|
@@ -28,9 +25,7 @@ module EventMachine
|
|
28
25
|
length = @data.getbyte(pointer) & 0b01111111
|
29
26
|
pointer += 1
|
30
27
|
|
31
|
-
|
32
|
-
raise WebSocketError, 'Data from client must be masked' unless mask
|
33
|
-
end
|
28
|
+
# raise WebSocketError, 'Data from client must be masked' unless mask
|
34
29
|
|
35
30
|
payload_length = case length
|
36
31
|
when 127 # Length defined by 8 bytes
|
@@ -65,6 +60,10 @@ module EventMachine
|
|
65
60
|
frame_length = pointer + payload_length
|
66
61
|
frame_length += 4 if mask
|
67
62
|
|
63
|
+
if frame_length > @connection.max_frame_size
|
64
|
+
raise WSMessageTooBigError, "Frame length too long (#{frame_length} bytes)"
|
65
|
+
end
|
66
|
+
|
68
67
|
# Check buffer size
|
69
68
|
if @data.getbyte(frame_length - 1) == nil
|
70
69
|
debug [:buffer_incomplete, @data]
|
@@ -88,8 +87,19 @@ module EventMachine
|
|
88
87
|
|
89
88
|
frame_type = opcode_to_type(opcode)
|
90
89
|
|
91
|
-
if frame_type == :continuation
|
92
|
-
|
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'
|
93
103
|
end
|
94
104
|
|
95
105
|
if !fin
|
@@ -124,24 +134,19 @@ module EventMachine
|
|
124
134
|
byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0
|
125
135
|
frame << byte1
|
126
136
|
|
127
|
-
mask = mask_outbound_messages ? 0b10000000 : 0b00000000 # must be masked if from client
|
128
137
|
length = application_data.size
|
129
138
|
if length <= 125
|
130
139
|
byte2 = length # since rsv4 is 0
|
131
|
-
frame <<
|
140
|
+
frame << byte2
|
132
141
|
elsif length < 65536 # write 2 byte length
|
133
|
-
frame <<
|
142
|
+
frame << 126
|
134
143
|
frame << [length].pack('n')
|
135
144
|
else # write 8 byte length
|
136
|
-
frame <<
|
145
|
+
frame << 127
|
137
146
|
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
138
147
|
end
|
139
148
|
|
140
|
-
|
141
|
-
frame << MaskedString.create_masked_string(application_data)
|
142
|
-
else
|
143
|
-
frame << application_data
|
144
|
-
end
|
149
|
+
frame << application_data
|
145
150
|
|
146
151
|
@connection.send_data(frame)
|
147
152
|
end
|
@@ -169,7 +174,7 @@ module EventMachine
|
|
169
174
|
end
|
170
175
|
|
171
176
|
def opcode_to_type(opcode)
|
172
|
-
FRAME_TYPES_INVERSE[opcode] || raise(
|
177
|
+
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
|
173
178
|
end
|
174
179
|
|
175
180
|
def data_frame?(type)
|
@@ -3,16 +3,11 @@
|
|
3
3
|
module EventMachine
|
4
4
|
module WebSocket
|
5
5
|
module Framing76
|
6
|
-
|
7
|
-
# Set the max frame lenth to very high value (10MB) until there is a
|
8
|
-
# limit specified in the spec to protect against malicious attacks
|
9
|
-
MAXIMUM_FRAME_LENGTH = 10 * 1024 * 1024
|
10
|
-
|
11
6
|
def initialize_framing
|
12
7
|
@data = ''
|
13
8
|
end
|
14
9
|
|
15
|
-
def process_data
|
10
|
+
def process_data
|
16
11
|
debug [:message, @data]
|
17
12
|
|
18
13
|
# This algorithm comes straight from the spec
|
@@ -40,9 +35,8 @@ module EventMachine
|
|
40
35
|
break unless (b & 0x80) == 0x80
|
41
36
|
end
|
42
37
|
|
43
|
-
|
44
|
-
|
45
|
-
raise DataError, "Frame length too long (#{length} bytes)"
|
38
|
+
if length > @connection.max_frame_size
|
39
|
+
raise WSMessageTooBigError, "Frame length too long (#{length} bytes)"
|
46
40
|
end
|
47
41
|
|
48
42
|
if @data.getbyte(pointer+length-1) == nil
|
@@ -69,17 +63,14 @@ module EventMachine
|
|
69
63
|
|
70
64
|
if @data.getbyte(0) != 0x00
|
71
65
|
# Close the connection since this buffer can never match
|
72
|
-
raise
|
66
|
+
raise WSProtocolError, "Invalid frame received"
|
73
67
|
end
|
74
68
|
|
75
69
|
# Addition to the spec to protect against malicious requests
|
76
|
-
if @data.size >
|
77
|
-
raise
|
70
|
+
if @data.size > @connection.max_frame_size
|
71
|
+
raise WSMessageTooBigError, "Frame length too long (#{@data.size} bytes)"
|
78
72
|
end
|
79
73
|
|
80
|
-
# Optimization to avoid calling slice! unnecessarily
|
81
|
-
error = true and next unless newdata =~ /\xff/
|
82
|
-
|
83
74
|
msg = @data.slice!(/\A\x00[^\xff]*\xff/)
|
84
75
|
if msg
|
85
76
|
msg.gsub!(/\A\x00|\xff\z/, '')
|
data/lib/em-websocket/handler.rb
CHANGED
@@ -1,55 +1,96 @@
|
|
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 = :handshake
|
12
|
-
initialize_framing
|
13
|
-
end
|
14
|
-
|
15
|
-
def run_server
|
16
|
-
@connection.send_data handshake_server
|
17
40
|
@state = :connected
|
18
|
-
@
|
41
|
+
@close_timer = nil
|
42
|
+
initialize_framing
|
19
43
|
end
|
20
44
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
24
|
-
|
45
|
+
def receive_data(data)
|
46
|
+
@data << data
|
47
|
+
process_data
|
48
|
+
rescue WSProtocolError => e
|
49
|
+
fail_websocket(e)
|
25
50
|
end
|
26
51
|
|
27
|
-
|
28
|
-
def handshake
|
52
|
+
def close_websocket(code, body)
|
29
53
|
# Implemented in subclass
|
30
54
|
end
|
31
55
|
|
32
|
-
|
33
|
-
|
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
|
+
}
|
34
63
|
end
|
35
64
|
|
36
|
-
#
|
37
|
-
def
|
38
|
-
|
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)
|
39
71
|
end
|
40
72
|
|
41
|
-
def
|
42
|
-
@
|
43
|
-
|
73
|
+
def unbind
|
74
|
+
@state = :closed
|
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)
|
44
84
|
end
|
45
85
|
|
46
|
-
def
|
47
|
-
#
|
86
|
+
def ping
|
87
|
+
# Overridden in subclass
|
88
|
+
false
|
48
89
|
end
|
49
90
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
91
|
+
def pingable?
|
92
|
+
# Also Overridden
|
93
|
+
false
|
53
94
|
end
|
54
95
|
end
|
55
96
|
end
|
@@ -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
|