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
@@ -4,71 +4,33 @@ require 'base64'
|
|
4
4
|
module EventMachine
|
5
5
|
module WebSocket
|
6
6
|
module Handshake04
|
7
|
+
def self.handshake(headers, _, __)
|
8
|
+
# Required
|
9
|
+
unless key = headers['sec-websocket-key']
|
10
|
+
raise HandshakeError, "sec-websocket-key header is required"
|
11
|
+
end
|
7
12
|
|
8
|
-
def handshake_key_response(key)
|
9
13
|
string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
10
|
-
Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
|
11
|
-
end
|
14
|
+
signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
|
12
15
|
|
13
|
-
def handshake_server
|
14
|
-
# Required
|
15
|
-
unless key = request['sec-websocket-key']
|
16
|
-
raise HandshakeError, "Sec-WebSocket-Key header is required"
|
17
|
-
end
|
18
|
-
|
19
|
-
# Optional
|
20
|
-
origin = request['sec-websocket-origin']
|
21
|
-
protocols = request['sec-websocket-protocol']
|
22
|
-
extensions = request['sec-websocket-extensions']
|
23
|
-
|
24
16
|
upgrade = ["HTTP/1.1 101 Switching Protocols"]
|
25
17
|
upgrade << "Upgrade: websocket"
|
26
18
|
upgrade << "Connection: Upgrade"
|
27
|
-
upgrade << "Sec-WebSocket-Accept: #{
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
19
|
+
upgrade << "Sec-WebSocket-Accept: #{signature}"
|
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
|
36
27
|
|
37
|
-
|
38
|
-
request = ["GET /websocket HTTP/1.1"]
|
39
|
-
request << "Host: #{@request[:host]}:#{@request[:port]}"
|
40
|
-
request << "Connection: keep-alive, Upgrade"
|
41
|
-
request << "Sec-WebSocket-Version: 8" # TODO: supply version somehow
|
42
|
-
request << "Sec-WebSocket-Origin: null"
|
43
|
-
random16 = (0...16).map{rand(255).chr}.join
|
44
|
-
random16_base64 = Base64.encode64(random16).chomp
|
45
|
-
@correct_response = handshake_key_response random16_base64
|
46
|
-
request << "Sec-WebSocket-Key: #{random16_base64}"
|
47
|
-
request << "Upgrade: websocket"
|
48
|
-
# TODO: anything else needed? nothing else parsed anyway
|
49
|
-
return request.join("\r\n") + "\r\n\r\n"
|
28
|
+
return upgrade.join("\r\n") + "\r\n\r\n"
|
50
29
|
end
|
51
30
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
accept = false
|
56
|
-
lines.each do |line|
|
57
|
-
h = /^([^:]+):\s*(.+)$/.match(line)
|
58
|
-
if !h.nil? and h[1].strip.downcase == "sec-websocket-accept"
|
59
|
-
accept = (h[2] == @correct_response)
|
60
|
-
break
|
61
|
-
end
|
62
|
-
end
|
63
|
-
if accept
|
64
|
-
@state = :connected #TODO - some actual logic would be nice
|
65
|
-
@connection.trigger_on_open
|
66
|
-
if msg # handle message bundled in with handshake response
|
67
|
-
receive_data(msg)
|
68
|
-
end
|
69
|
-
else
|
70
|
-
close_websocket(1002,nil)
|
71
|
-
end
|
31
|
+
def self.validate_protocol!(protocol)
|
32
|
+
raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
|
33
|
+
# TODO: Validate characters
|
72
34
|
end
|
73
35
|
end
|
74
36
|
end
|
@@ -1,21 +1,28 @@
|
|
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: #{
|
13
|
-
upgrade << "WebSocket-Location: #{location}\r\n
|
14
|
-
|
15
|
-
|
11
|
+
upgrade << "WebSocket-Origin: #{headers['origin']}\r\n"
|
12
|
+
upgrade << "WebSocket-Location: #{location}\r\n"
|
13
|
+
if protocol = headers['sec-websocket-protocol']
|
14
|
+
validate_protocol!(protocol)
|
15
|
+
upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
|
16
|
+
end
|
17
|
+
upgrade << "\r\n"
|
16
18
|
|
17
19
|
return upgrade
|
18
20
|
end
|
21
|
+
|
22
|
+
def self.validate_protocol!(protocol)
|
23
|
+
raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
|
24
|
+
# TODO: Validate characters
|
25
|
+
end
|
19
26
|
end
|
20
27
|
end
|
21
28
|
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
|
|
@@ -42,6 +39,10 @@ module EventMachine
|
|
42
39
|
end
|
43
40
|
|
44
41
|
def numbers_over_spaces(string)
|
42
|
+
unless string
|
43
|
+
raise HandshakeError, "WebSocket key1 or key2 is missing"
|
44
|
+
end
|
45
|
+
|
45
46
|
numbers = string.scan(/[0-9]/).join.to_i
|
46
47
|
|
47
48
|
spaces = string.scan(/ /).size
|
@@ -10,36 +10,13 @@ module EventMachine
|
|
10
10
|
@masking_key = String.new(self[0..3])
|
11
11
|
end
|
12
12
|
|
13
|
-
def self.create_mask
|
14
|
-
MaskedString.new "rAnD" #TODO make random 4 character string
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.create_masked_string(original)
|
18
|
-
masked_string = MaskedString.new
|
19
|
-
masking_key = self.create_mask
|
20
|
-
masked_string << masking_key
|
21
|
-
original.size.times do |i|
|
22
|
-
char = original.getbyte(i)
|
23
|
-
masked_string << (char ^ masking_key.getbyte(i%4))
|
24
|
-
end
|
25
|
-
if masked_string.respond_to?(:force_encoding)
|
26
|
-
masked_string.force_encoding("ASCII-8BIT")
|
27
|
-
end
|
28
|
-
masked_string.read_mask # get input string
|
29
|
-
return masked_string
|
30
|
-
end
|
31
|
-
|
32
13
|
# Removes the mask, behaves like a normal string again
|
33
14
|
def unset_mask
|
34
15
|
@masking_key = nil
|
35
16
|
end
|
36
17
|
|
37
|
-
def slice_mask
|
38
|
-
slice!(0, 4)
|
39
|
-
end
|
40
|
-
|
41
18
|
def getbyte(index)
|
42
|
-
if @masking_key
|
19
|
+
if defined?(@masking_key) && @masking_key
|
43
20
|
masked_char = super
|
44
21
|
masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
|
45
22
|
else
|
@@ -49,12 +26,9 @@ module EventMachine
|
|
49
26
|
|
50
27
|
def getbytes(start_index, count)
|
51
28
|
data = ''
|
52
|
-
if
|
53
|
-
|
54
|
-
|
55
|
-
end
|
56
|
-
else
|
57
|
-
data = String.new(self[start_index..start_index+count])
|
29
|
+
data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
|
30
|
+
count.times do |i|
|
31
|
+
data << getbyte(start_index + i)
|
58
32
|
end
|
59
33
|
data
|
60
34
|
end
|
@@ -6,33 +6,42 @@ module EventMachine
|
|
6
6
|
def message(message_type, extension_data, application_data)
|
7
7
|
case message_type
|
8
8
|
when :close
|
9
|
+
@close_info = {
|
10
|
+
:code => 1005,
|
11
|
+
:reason => "",
|
12
|
+
:was_clean => true,
|
13
|
+
}
|
9
14
|
if @state == :closing
|
10
15
|
# TODO: Check that message body matches sent data
|
11
16
|
# We can close connection immediately since there is no more data
|
12
17
|
# is allowed to be sent or received on this connection
|
13
18
|
@connection.close_connection
|
14
|
-
@state = :closed
|
15
19
|
else
|
16
20
|
# Acknowlege close
|
17
21
|
# The connection is considered closed
|
18
22
|
send_frame(:close, application_data)
|
19
|
-
@state = :closed
|
20
23
|
@connection.close_connection_after_writing
|
21
24
|
end
|
22
25
|
when :ping
|
23
26
|
# Pong back the same data
|
24
27
|
send_frame(:pong, application_data)
|
28
|
+
@connection.trigger_on_ping(application_data)
|
25
29
|
when :pong
|
26
|
-
|
30
|
+
@connection.trigger_on_pong(application_data)
|
27
31
|
when :text
|
28
32
|
if application_data.respond_to?(:force_encoding)
|
29
33
|
application_data.force_encoding("UTF-8")
|
30
34
|
end
|
31
35
|
@connection.trigger_on_message(application_data)
|
32
36
|
when :binary
|
33
|
-
@connection.
|
37
|
+
@connection.trigger_on_binary(application_data)
|
34
38
|
end
|
35
39
|
end
|
40
|
+
|
41
|
+
# Ping & Pong supported
|
42
|
+
def pingable?
|
43
|
+
true
|
44
|
+
end
|
36
45
|
end
|
37
46
|
end
|
38
47
|
end
|
@@ -12,41 +12,53 @@ module EventMachine
|
|
12
12
|
nil
|
13
13
|
when 1
|
14
14
|
# Illegal close frame
|
15
|
-
raise
|
15
|
+
raise WSProtocolError, "Close frames with a body must contain a 2 byte status code"
|
16
16
|
else
|
17
17
|
application_data.slice!(0, 2).unpack('n').first
|
18
18
|
end
|
19
19
|
|
20
20
|
debug [:close_frame_received, status_code, application_data]
|
21
21
|
|
22
|
+
@close_info = {
|
23
|
+
:code => status_code || 1005,
|
24
|
+
:reason => application_data,
|
25
|
+
:was_clean => true,
|
26
|
+
}
|
27
|
+
|
22
28
|
if @state == :closing
|
23
|
-
# We can close connection immediately since
|
24
|
-
#
|
29
|
+
# We can close connection immediately since no more data may be
|
30
|
+
# sent or received on this connection
|
25
31
|
@connection.close_connection
|
26
|
-
|
27
|
-
|
28
|
-
# Acknowlege close
|
32
|
+
elsif @state == :connected
|
33
|
+
# Acknowlege close & echo status back to client
|
29
34
|
# The connection is considered closed
|
30
|
-
|
31
|
-
|
35
|
+
close_data = [status_code || 1000].pack('n')
|
36
|
+
send_frame(:close, close_data)
|
32
37
|
@connection.close_connection_after_writing
|
33
|
-
# TODO: Send close status code and body to app code
|
34
38
|
end
|
35
39
|
when :ping
|
36
40
|
# Pong back the same data
|
37
41
|
send_frame(:pong, application_data)
|
42
|
+
@connection.trigger_on_ping(application_data)
|
38
43
|
when :pong
|
39
|
-
|
40
|
-
@connection.trigger_on_message(application_data, :pong)
|
44
|
+
@connection.trigger_on_pong(application_data)
|
41
45
|
when :text
|
42
46
|
if application_data.respond_to?(:force_encoding)
|
43
47
|
application_data.force_encoding("UTF-8")
|
48
|
+
unless application_data.valid_encoding?
|
49
|
+
raise InvalidDataError, "Invalid UTF8 data"
|
50
|
+
end
|
44
51
|
end
|
45
|
-
@connection.trigger_on_message(application_data
|
52
|
+
@connection.trigger_on_message(application_data)
|
46
53
|
when :binary
|
47
|
-
@connection.
|
54
|
+
@connection.trigger_on_binary(application_data)
|
48
55
|
end
|
49
56
|
end
|
57
|
+
|
58
|
+
# Ping & Pong supported
|
59
|
+
def pingable?
|
60
|
+
true
|
61
|
+
end
|
50
62
|
end
|
51
63
|
end
|
52
64
|
end
|
data/lib/em-websocket/version.rb
CHANGED
@@ -1,45 +1,56 @@
|
|
1
1
|
module EventMachine
|
2
2
|
module WebSocket
|
3
|
+
class << self
|
4
|
+
attr_accessor :max_frame_size
|
5
|
+
attr_accessor :close_timeout
|
6
|
+
end
|
7
|
+
@max_frame_size = 10 * 1024 * 1024 # 10MB
|
8
|
+
# Connections are given 60s to close after being sent a close handshake
|
9
|
+
@close_timeout = 60
|
10
|
+
|
11
|
+
# All errors raised by em-websocket should descend from this class
|
3
12
|
class WebSocketError < RuntimeError; end
|
13
|
+
|
14
|
+
# Used for errors that occur during WebSocket handshake
|
4
15
|
class HandshakeError < WebSocketError; end
|
5
|
-
class DataError < WebSocketError; end
|
6
16
|
|
7
|
-
#
|
8
|
-
|
9
|
-
|
17
|
+
# Used for errors which should cause the connection to close.
|
18
|
+
# See RFC6455 §7.4.1 for a full description of meanings
|
19
|
+
class WSProtocolError < WebSocketError
|
20
|
+
def code; 1002; end
|
10
21
|
end
|
11
22
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
#trap("TERM") { stop; }
|
17
|
-
#trap("INT") { stop; }
|
23
|
+
class InvalidDataError < WSProtocolError
|
24
|
+
def code; 1007; end
|
25
|
+
end
|
18
26
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
end
|
27
|
+
# 1009: Message too big to process
|
28
|
+
class WSMessageTooBigError < WSProtocolError
|
29
|
+
def code; 1009; end
|
24
30
|
end
|
25
31
|
|
26
|
-
|
32
|
+
# Start WebSocket server, including starting eventmachine run loop
|
33
|
+
def self.start(options, &blk)
|
27
34
|
EM.epoll
|
28
|
-
EM.run
|
35
|
+
EM.run {
|
36
|
+
trap("TERM") { stop }
|
37
|
+
trap("INT") { stop }
|
29
38
|
|
30
|
-
|
31
|
-
|
39
|
+
run(options, &blk)
|
40
|
+
}
|
41
|
+
end
|
32
42
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
43
|
+
# Start WebSocket server inside eventmachine run loop
|
44
|
+
def self.run(options)
|
45
|
+
host, port = options.values_at(:host, :port)
|
46
|
+
EM.start_server(host, port, Connection, options) do |c|
|
47
|
+
yield c
|
37
48
|
end
|
38
49
|
end
|
39
50
|
|
40
51
|
def self.stop
|
41
52
|
puts "Terminating WebSocket Server"
|
42
|
-
|
53
|
+
EM.stop
|
43
54
|
end
|
44
55
|
end
|
45
56
|
end
|
data/spec/helper.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
|
1
3
|
require 'rubygems'
|
2
4
|
require 'rspec'
|
3
5
|
require 'em-spec/rspec'
|
4
|
-
require 'pp'
|
5
6
|
require 'em-http'
|
6
7
|
|
7
8
|
require 'em-websocket'
|
9
|
+
require 'em-websocket-client'
|
10
|
+
|
11
|
+
require 'integration/shared_examples'
|
12
|
+
require 'integration/gte_03_examples'
|
8
13
|
|
9
14
|
RSpec.configure do |c|
|
10
15
|
c.mock_with :rspec
|
@@ -27,77 +32,80 @@ class FakeWebSocketClient < EM::Connection
|
|
27
32
|
# puts "RECEIVE DATA #{data}"
|
28
33
|
if @state == :new
|
29
34
|
@handshake_response = data
|
30
|
-
@onopen.call if @onopen
|
35
|
+
@onopen.call if defined? @onopen
|
31
36
|
@state = :open
|
32
37
|
else
|
33
|
-
@onmessage.call(data) if @onmessage
|
38
|
+
@onmessage.call(data) if defined? @onmessage
|
34
39
|
@packets << data
|
35
40
|
end
|
36
41
|
end
|
37
42
|
|
38
|
-
def send(
|
39
|
-
|
43
|
+
def send(application_data)
|
44
|
+
send_frame(:text, application_data)
|
40
45
|
end
|
41
46
|
|
42
|
-
def
|
43
|
-
|
47
|
+
def send_frame(type, application_data)
|
48
|
+
send_data construct_frame(type, application_data)
|
44
49
|
end
|
45
|
-
end
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
opcode = 4 # fake only supports text frames
|
51
|
-
byte1 = opcode # since more, rsv1-3 are 0
|
52
|
-
frame << byte1
|
53
|
-
|
54
|
-
length = application_data.size
|
55
|
-
if length <= 125
|
56
|
-
byte2 = length # since rsv4 is 0
|
57
|
-
frame << byte2
|
58
|
-
elsif length < 65536 # write 2 byte length
|
59
|
-
frame << 126
|
60
|
-
frame << [length].pack('n')
|
61
|
-
else # write 8 byte length
|
62
|
-
frame << 127
|
63
|
-
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
64
|
-
end
|
51
|
+
def unbind
|
52
|
+
@onclose.call if defined? @onclose
|
53
|
+
end
|
65
54
|
|
66
|
-
|
55
|
+
private
|
67
56
|
|
68
|
-
|
57
|
+
def construct_frame(type, data)
|
58
|
+
"\x00#{data}\xff"
|
69
59
|
end
|
70
60
|
end
|
71
61
|
|
72
|
-
class
|
73
|
-
|
74
|
-
frame = ''
|
75
|
-
opcode = 1 # fake only supports text frames
|
76
|
-
byte1 = opcode | 0b10000000 # since more, rsv1-3 are 0
|
77
|
-
frame << byte1
|
62
|
+
class Draft03FakeWebSocketClient < FakeWebSocketClient
|
63
|
+
private
|
78
64
|
|
79
|
-
|
65
|
+
def construct_frame(type, data)
|
66
|
+
frame = ""
|
67
|
+
frame << EM::WebSocket::Framing03::FRAME_TYPES[type]
|
68
|
+
frame << encoded_length(data.size)
|
69
|
+
frame << data
|
70
|
+
end
|
80
71
|
|
81
|
-
|
72
|
+
def encoded_length(length)
|
82
73
|
if length <= 125
|
83
|
-
|
84
|
-
frame << (mask | byte2)
|
74
|
+
[length].pack('C') # since rsv4 is 0
|
85
75
|
elsif length < 65536 # write 2 byte length
|
86
|
-
|
87
|
-
frame << [length].pack('n')
|
76
|
+
"\126#{[length].pack('n')}"
|
88
77
|
else # write 8 byte length
|
89
|
-
|
90
|
-
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
78
|
+
"\127#{[length >> 32, length & 0xFFFFFFFF].pack("NN")}"
|
91
79
|
end
|
80
|
+
end
|
81
|
+
end
|
92
82
|
|
93
|
-
|
83
|
+
class Draft05FakeWebSocketClient < Draft03FakeWebSocketClient
|
84
|
+
private
|
94
85
|
|
95
|
-
|
86
|
+
def construct_frame(type, data)
|
87
|
+
frame = ""
|
88
|
+
frame << "\x00\x00\x00\x00" # Mask with nothing for simplicity
|
89
|
+
frame << (EM::WebSocket::Framing05::FRAME_TYPES[type] | 0b10000000)
|
90
|
+
frame << encoded_length(data.size)
|
91
|
+
frame << data
|
96
92
|
end
|
97
93
|
end
|
98
94
|
|
95
|
+
class Draft07FakeWebSocketClient < Draft05FakeWebSocketClient
|
96
|
+
private
|
99
97
|
|
100
|
-
|
98
|
+
def construct_frame(type, data)
|
99
|
+
frame = ""
|
100
|
+
frame << (EM::WebSocket::Framing07::FRAME_TYPES[type] | 0b10000000)
|
101
|
+
# Should probably mask the data, but I get away without bothering since
|
102
|
+
# the server doesn't enforce that incoming frames are masked
|
103
|
+
frame << encoded_length(data.size)
|
104
|
+
frame << data
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Wrapper around em-websocket-client
|
101
109
|
class Draft75WebSocketClient
|
102
110
|
def onopen(&blk); @onopen = blk; end
|
103
111
|
def onclose(&blk); @onclose = blk; end
|
@@ -105,14 +113,17 @@ class Draft75WebSocketClient
|
|
105
113
|
def onmessage(&blk); @onmessage = blk; end
|
106
114
|
|
107
115
|
def initialize
|
108
|
-
@ws = EventMachine::
|
109
|
-
|
110
|
-
|
111
|
-
@ws.
|
116
|
+
@ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/',
|
117
|
+
:version => 75,
|
118
|
+
:origin => 'http://example.com')
|
119
|
+
@ws.errback { |err| @onerror.call if defined? @onerror }
|
120
|
+
@ws.callback { @onopen.call if defined? @onopen }
|
121
|
+
@ws.stream { |msg| @onmessage.call(msg) if defined? @onmessage }
|
122
|
+
@ws.disconnect { @onclose.call if defined? @onclose }
|
112
123
|
end
|
113
124
|
|
114
125
|
def send(message)
|
115
|
-
@ws.
|
126
|
+
@ws.send_msg(message)
|
116
127
|
end
|
117
128
|
|
118
129
|
def close_connection
|
@@ -120,6 +131,12 @@ class Draft75WebSocketClient
|
|
120
131
|
end
|
121
132
|
end
|
122
133
|
|
134
|
+
def start_server(opts = {})
|
135
|
+
EM::WebSocket.run({:host => "0.0.0.0", :port => 12345}.merge(opts)) { |ws|
|
136
|
+
yield ws if block_given?
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
123
140
|
def format_request(r)
|
124
141
|
data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n"
|
125
142
|
header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
|
@@ -134,13 +151,23 @@ def format_response(r)
|
|
134
151
|
data
|
135
152
|
end
|
136
153
|
|
137
|
-
|
138
|
-
|
139
|
-
|
154
|
+
RSpec::Matchers.define :succeed_with_upgrade do |response|
|
155
|
+
match do |actual|
|
156
|
+
success = nil
|
157
|
+
actual.callback { |upgrade_response, handler_klass|
|
158
|
+
success = (upgrade_response.lines.sort == format_response(response).lines.sort)
|
159
|
+
}
|
160
|
+
success
|
161
|
+
end
|
140
162
|
end
|
141
163
|
|
142
|
-
RSpec::Matchers.define :
|
164
|
+
RSpec::Matchers.define :fail_with_error do |error_klass, error_message|
|
143
165
|
match do |actual|
|
144
|
-
|
166
|
+
success = nil
|
167
|
+
actual.errback { |e|
|
168
|
+
success = (e.class == error_klass)
|
169
|
+
success &= (e.message == error_message) if error_message
|
170
|
+
}
|
171
|
+
success
|
145
172
|
end
|
146
173
|
end
|