sonixlabs-em-websocket 0.3.8 → 0.5.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|