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
@@ -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
|
@@ -15,12 +15,8 @@ module EventMachine
|
|
15
15
|
@masking_key = nil
|
16
16
|
end
|
17
17
|
|
18
|
-
def slice_mask
|
19
|
-
slice!(0, 4)
|
20
|
-
end
|
21
|
-
|
22
18
|
def getbyte(index)
|
23
|
-
if @masking_key
|
19
|
+
if defined?(@masking_key) && @masking_key
|
24
20
|
masked_char = super
|
25
21
|
masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
|
26
22
|
else
|
@@ -29,7 +25,8 @@ module EventMachine
|
|
29
25
|
end
|
30
26
|
|
31
27
|
def getbytes(start_index, count)
|
32
|
-
data = ''
|
28
|
+
data = ''
|
29
|
+
data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
|
33
30
|
count.times do |i|
|
34
31
|
data << getbyte(start_index + i)
|
35
32
|
end
|
@@ -6,17 +6,20 @@ 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
|
@@ -31,7 +34,7 @@ module EventMachine
|
|
31
34
|
end
|
32
35
|
@connection.trigger_on_message(application_data)
|
33
36
|
when :binary
|
34
|
-
@connection.
|
37
|
+
@connection.trigger_on_binary(application_data)
|
35
38
|
end
|
36
39
|
end
|
37
40
|
|
@@ -19,32 +19,53 @@ module EventMachine
|
|
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
29
|
# We can close connection immediately since no more data may be
|
24
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
|
-
#
|
37
|
-
|
40
|
+
# There are a couple of protections here against malicious/broken WebSocket abusing ping frames.
|
41
|
+
#
|
42
|
+
# 1. Delay 200ms before replying. This reduces the number of pings from WebSocket clients behaving as
|
43
|
+
# `for (;;) { send_ping(conn); rcv_pong(conn); }`. The spec says we "SHOULD respond with Pong frame as soon
|
44
|
+
# as is practical".
|
45
|
+
# 2. Reply at most every 200ms. This reduces the number of pong frames sent to WebSocket clients behaving as
|
46
|
+
# `for (;;) { send_ping(conn); }`. The spec says "If an endpoint receives a Ping frame and has not yet sent
|
47
|
+
# Pong frame(s) in response to previous Ping frame(s), the endpoint MAY elect to send a Pong frame for only
|
48
|
+
# the most recently processed Ping frame."
|
49
|
+
@most_recent_pong_application_data = application_data
|
50
|
+
if @pong_timer == nil then
|
51
|
+
@pong_timer = EventMachine.add_timer(0.2) do
|
52
|
+
@pong_timer = nil
|
53
|
+
send_frame(:pong, @most_recent_pong_application_data)
|
54
|
+
end
|
55
|
+
end
|
38
56
|
@connection.trigger_on_ping(application_data)
|
39
57
|
when :pong
|
40
58
|
@connection.trigger_on_pong(application_data)
|
41
59
|
when :text
|
42
60
|
if application_data.respond_to?(:force_encoding)
|
43
61
|
application_data.force_encoding("UTF-8")
|
62
|
+
unless application_data.valid_encoding?
|
63
|
+
raise InvalidDataError, "Invalid UTF8 data"
|
64
|
+
end
|
44
65
|
end
|
45
66
|
@connection.trigger_on_message(application_data)
|
46
67
|
when :binary
|
47
|
-
@connection.
|
68
|
+
@connection.trigger_on_binary(application_data)
|
48
69
|
end
|
49
70
|
end
|
50
71
|
|
data/lib/em-websocket/version.rb
CHANGED
@@ -1,47 +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
|
+
|
3
11
|
# All errors raised by em-websocket should descend from this class
|
4
|
-
#
|
5
12
|
class WebSocketError < RuntimeError; end
|
6
13
|
|
7
14
|
# Used for errors that occur during WebSocket handshake
|
8
|
-
#
|
9
15
|
class HandshakeError < WebSocketError; end
|
10
16
|
|
11
17
|
# Used for errors which should cause the connection to close.
|
12
18
|
# See RFC6455 §7.4.1 for a full description of meanings
|
13
|
-
#
|
14
19
|
class WSProtocolError < WebSocketError
|
15
20
|
def code; 1002; end
|
16
21
|
end
|
17
22
|
|
23
|
+
class InvalidDataError < WSProtocolError
|
24
|
+
def code; 1007; end
|
25
|
+
end
|
26
|
+
|
18
27
|
# 1009: Message too big to process
|
19
28
|
class WSMessageTooBigError < WSProtocolError
|
20
29
|
def code; 1009; end
|
21
30
|
end
|
22
31
|
|
32
|
+
# Start WebSocket server, including starting eventmachine run loop
|
23
33
|
def self.start(options, &blk)
|
24
34
|
EM.epoll
|
25
|
-
EM.run
|
26
|
-
|
35
|
+
EM.run {
|
27
36
|
trap("TERM") { stop }
|
28
37
|
trap("INT") { stop }
|
29
38
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
39
|
+
run(options, &blk)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
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
|
34
48
|
end
|
35
49
|
end
|
36
50
|
|
37
51
|
def self.stop
|
38
52
|
puts "Terminating WebSocket Server"
|
39
|
-
|
53
|
+
EM.stop
|
40
54
|
end
|
41
|
-
|
42
|
-
class << self
|
43
|
-
attr_accessor :max_frame_size
|
44
|
-
end
|
45
|
-
@max_frame_size = 10 * 1024 * 1024 # 10MB
|
46
55
|
end
|
47
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,75 +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)
|
45
|
+
end
|
46
|
+
|
47
|
+
def send_frame(type, application_data)
|
48
|
+
send_data construct_frame(type, application_data)
|
40
49
|
end
|
41
50
|
|
42
51
|
def unbind
|
43
|
-
@onclose.call if @onclose
|
52
|
+
@onclose.call if defined? @onclose
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def construct_frame(type, data)
|
58
|
+
"\x00#{data}\xff"
|
44
59
|
end
|
45
60
|
end
|
46
61
|
|
47
62
|
class Draft03FakeWebSocketClient < FakeWebSocketClient
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
frame <<
|
63
|
+
private
|
64
|
+
|
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
|
53
71
|
|
54
|
-
|
72
|
+
def encoded_length(length)
|
55
73
|
if length <= 125
|
56
|
-
|
57
|
-
frame << byte2
|
74
|
+
[length].pack('C') # since rsv4 is 0
|
58
75
|
elsif length < 65536 # write 2 byte length
|
59
|
-
|
60
|
-
frame << [length].pack('n')
|
76
|
+
"\126#{[length].pack('n')}"
|
61
77
|
else # write 8 byte length
|
62
|
-
|
63
|
-
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
78
|
+
"\127#{[length >> 32, length & 0xFFFFFFFF].pack("NN")}"
|
64
79
|
end
|
65
|
-
|
66
|
-
frame << application_data
|
67
|
-
|
68
|
-
send_data(frame)
|
69
80
|
end
|
70
81
|
end
|
71
82
|
|
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
|
83
|
+
class Draft05FakeWebSocketClient < Draft03FakeWebSocketClient
|
84
|
+
private
|
78
85
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
frame << 127
|
88
|
-
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
89
|
-
end
|
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
|
92
|
+
end
|
93
|
+
end
|
90
94
|
|
91
|
-
|
95
|
+
class Draft07FakeWebSocketClient < Draft05FakeWebSocketClient
|
96
|
+
private
|
92
97
|
|
93
|
-
|
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
|
94
105
|
end
|
95
106
|
end
|
96
107
|
|
97
|
-
|
98
|
-
# Wrap EM:HttpRequest in a websocket like interface so that it can be used in the specs with the same interface as FakeWebSocketClient
|
108
|
+
# Wrapper around em-websocket-client
|
99
109
|
class Draft75WebSocketClient
|
100
110
|
def onopen(&blk); @onopen = blk; end
|
101
111
|
def onclose(&blk); @onclose = blk; end
|
@@ -103,15 +113,17 @@ class Draft75WebSocketClient
|
|
103
113
|
def onmessage(&blk); @onmessage = blk; end
|
104
114
|
|
105
115
|
def initialize
|
106
|
-
@ws = EventMachine::
|
107
|
-
|
108
|
-
|
109
|
-
@ws.
|
110
|
-
@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 }
|
111
123
|
end
|
112
124
|
|
113
125
|
def send(message)
|
114
|
-
@ws.
|
126
|
+
@ws.send_msg(message)
|
115
127
|
end
|
116
128
|
|
117
129
|
def close_connection
|
@@ -119,6 +131,12 @@ class Draft75WebSocketClient
|
|
119
131
|
end
|
120
132
|
end
|
121
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
|
+
|
122
140
|
def format_request(r)
|
123
141
|
data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n"
|
124
142
|
header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
|
@@ -133,8 +151,23 @@ def format_response(r)
|
|
133
151
|
data
|
134
152
|
end
|
135
153
|
|
136
|
-
RSpec::Matchers.define :
|
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
|
162
|
+
end
|
163
|
+
|
164
|
+
RSpec::Matchers.define :fail_with_error do |error_klass, error_message|
|
137
165
|
match do |actual|
|
138
|
-
|
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
|
139
172
|
end
|
140
173
|
end
|