em-websocket 0.2.1 → 0.3.0
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.
- data/CHANGELOG.rdoc +10 -0
- data/README.md +16 -0
- data/examples/test.html +11 -8
- data/lib/em-websocket.rb +6 -3
- data/lib/em-websocket/close03.rb +11 -0
- data/lib/em-websocket/close05.rb +11 -0
- data/lib/em-websocket/close06.rb +16 -0
- data/lib/em-websocket/close75.rb +10 -0
- data/lib/em-websocket/connection.rb +58 -32
- data/lib/em-websocket/framing03.rb +9 -30
- data/lib/em-websocket/framing04.rb +15 -0
- data/lib/em-websocket/framing05.rb +157 -0
- data/lib/em-websocket/framing76.rb +5 -6
- data/lib/em-websocket/handler.rb +2 -4
- data/lib/em-websocket/handler03.rb +2 -6
- data/lib/em-websocket/handler05.rb +10 -0
- data/lib/em-websocket/handler06.rb +10 -0
- data/lib/em-websocket/handler75.rb +1 -0
- data/lib/em-websocket/handler76.rb +1 -0
- data/lib/em-websocket/handler_factory.rb +41 -22
- data/lib/em-websocket/handshake04.rb +35 -0
- data/lib/em-websocket/handshake75.rb +4 -4
- data/lib/em-websocket/handshake76.rb +8 -8
- data/lib/em-websocket/masking04.rb +27 -0
- data/lib/em-websocket/message_processor_03.rb +33 -0
- data/lib/em-websocket/message_processor_06.rb +46 -0
- data/lib/em-websocket/version.rb +1 -1
- data/spec/helper.rb +54 -2
- data/spec/integration/common_spec.rb +115 -0
- data/spec/integration/draft03_spec.rb +26 -11
- data/spec/integration/draft05_spec.rb +45 -0
- data/spec/integration/draft06_spec.rb +79 -0
- data/spec/integration/draft75_spec.rb +115 -0
- data/spec/integration/draft76_spec.rb +25 -10
- data/spec/integration/shared_examples.rb +62 -0
- data/spec/unit/framing_spec.rb +55 -0
- data/spec/unit/masking_spec.rb +18 -0
- metadata +29 -33
- data/spec/websocket_spec.rb +0 -210
@@ -42,12 +42,11 @@ module EventMachine
|
|
42
42
|
|
43
43
|
# Addition to the spec to protect against malicious requests
|
44
44
|
if length > MAXIMUM_FRAME_LENGTH
|
45
|
-
|
46
|
-
return false
|
45
|
+
raise DataError, "Frame length too long (#{length} bytes)"
|
47
46
|
end
|
48
47
|
|
49
48
|
if @data.getbyte(pointer+length-1) == nil
|
50
|
-
debug [:buffer_incomplete, @data
|
49
|
+
debug [:buffer_incomplete, @data]
|
51
50
|
# Incomplete data - leave @data to accumulate
|
52
51
|
error = true
|
53
52
|
else
|
@@ -70,13 +69,12 @@ module EventMachine
|
|
70
69
|
|
71
70
|
if @data.getbyte(0) != 0x00
|
72
71
|
# Close the connection since this buffer can never match
|
73
|
-
|
72
|
+
raise DataError, "Invalid frame received"
|
74
73
|
end
|
75
74
|
|
76
75
|
# Addition to the spec to protect against malicious requests
|
77
76
|
if @data.size > MAXIMUM_FRAME_LENGTH
|
78
|
-
|
79
|
-
return false
|
77
|
+
raise DataError, "Frame length too long (#{@data.size} bytes)"
|
80
78
|
end
|
81
79
|
|
82
80
|
# Optimization to avoid calling slice! unnecessarily
|
@@ -105,6 +103,7 @@ module EventMachine
|
|
105
103
|
# byte to a value betweent 0x80 and 0xFF, followed by
|
106
104
|
# a leading length indicator
|
107
105
|
def send_text_frame(data)
|
106
|
+
debug [:sending_text_frame, data]
|
108
107
|
ary = ["\x00", data, "\xff"]
|
109
108
|
ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) }
|
110
109
|
@connection.send_data(ary.join)
|
data/lib/em-websocket/handler.rb
CHANGED
@@ -28,10 +28,8 @@ module EventMachine
|
|
28
28
|
process_data(data)
|
29
29
|
end
|
30
30
|
|
31
|
-
def close_websocket
|
32
|
-
#
|
33
|
-
@state = :closed
|
34
|
-
@connection.close_connection_after_writing
|
31
|
+
def close_websocket(code, body)
|
32
|
+
# Implemented in subclass
|
35
33
|
end
|
36
34
|
|
37
35
|
def unbind
|
@@ -3,12 +3,8 @@ module EventMachine
|
|
3
3
|
class Handler03 < Handler
|
4
4
|
include Handshake76
|
5
5
|
include Framing03
|
6
|
-
|
7
|
-
|
8
|
-
# TODO: Should we send data and check the response matches?
|
9
|
-
send_frame(:close, '')
|
10
|
-
@state = :closing
|
11
|
-
end
|
6
|
+
include MessageProcessor03
|
7
|
+
include Close03
|
12
8
|
end
|
13
9
|
end
|
14
10
|
end
|
@@ -18,59 +18,78 @@ module EventMachine
|
|
18
18
|
# extract request path
|
19
19
|
first_line = lines.shift.match(PATH)
|
20
20
|
raise HandshakeError, "Invalid HTTP header" unless first_line
|
21
|
-
request['
|
22
|
-
request['
|
21
|
+
request['method'] = first_line[1].strip
|
22
|
+
request['path'] = first_line[2].strip
|
23
23
|
|
24
|
-
unless request["
|
24
|
+
unless request["method"] == "GET"
|
25
25
|
raise HandshakeError, "Must be GET request"
|
26
26
|
end
|
27
27
|
|
28
28
|
# extract query string values
|
29
|
-
request['
|
29
|
+
request['query'] = Addressable::URI.parse(request['path']).query_values ||= {}
|
30
30
|
# extract remaining headers
|
31
31
|
lines.each do |line|
|
32
32
|
h = HEADER.match(line)
|
33
|
-
request[h[1].strip] = h[2].strip if h
|
33
|
+
request[h[1].strip.downcase] = h[2].strip if h
|
34
34
|
end
|
35
35
|
|
36
|
-
|
36
|
+
build_with_request(connection, request, remains, secure, debug)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.build_with_request(connection, request, remains, secure = false, debug = false)
|
40
|
+
# Determine version heuristically
|
41
|
+
version = if request['sec-websocket-version']
|
42
|
+
# Used from drafts 04 onwards
|
43
|
+
request['sec-websocket-version'].to_i
|
44
|
+
elsif request['sec-websocket-draft']
|
45
|
+
# Used in drafts 01 - 03
|
46
|
+
request['sec-websocket-draft'].to_i
|
47
|
+
elsif request['sec-websocket-key1']
|
48
|
+
76
|
49
|
+
else
|
50
|
+
75
|
51
|
+
end
|
52
|
+
|
53
|
+
# Additional handling of bytes after the header if required
|
37
54
|
case version
|
38
55
|
when 75
|
39
56
|
if !remains.empty?
|
40
57
|
raise HandshakeError, "Extra bytes after header"
|
41
58
|
end
|
42
|
-
when 76
|
59
|
+
when 76, 1..3
|
43
60
|
if remains.length < 8
|
44
61
|
# The whole third-key has not been received yet.
|
45
62
|
return nil
|
46
63
|
elsif remains.length > 8
|
47
64
|
raise HandshakeError, "Extra bytes after third key"
|
48
65
|
end
|
49
|
-
request['
|
50
|
-
else
|
51
|
-
raise WebSocketError, "Must not happen"
|
66
|
+
request['third-key'] = remains
|
52
67
|
end
|
53
68
|
|
54
|
-
|
69
|
+
# Validate that Connection and Upgrade headers
|
70
|
+
unless request['connection'] && request['connection'] =~ /Upgrade/ && request['upgrade'] && request['upgrade'].downcase == 'websocket'
|
55
71
|
raise HandshakeError, "Connection and Upgrade headers required"
|
56
72
|
end
|
57
73
|
|
58
74
|
# transform headers
|
59
75
|
protocol = (secure ? "wss" : "ws")
|
60
|
-
request['
|
76
|
+
request['host'] = Addressable::URI.parse("#{protocol}://"+request['host'])
|
61
77
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
else
|
67
|
-
# According to spec should abort the connection
|
68
|
-
raise WebSocketError, "Unknown draft version: #{version}"
|
69
|
-
end
|
70
|
-
elsif request['Sec-WebSocket-Key1']
|
78
|
+
case version
|
79
|
+
when 75
|
80
|
+
Handler75.new(connection, request, debug)
|
81
|
+
when 76
|
71
82
|
Handler76.new(connection, request, debug)
|
83
|
+
when 1..3
|
84
|
+
# We'll use handler03 - I believe they're all compatible
|
85
|
+
Handler03.new(connection, request, debug)
|
86
|
+
when 5
|
87
|
+
Handler05.new(connection, request, debug)
|
88
|
+
when 6
|
89
|
+
Handler06.new(connection, request, debug)
|
72
90
|
else
|
73
|
-
|
91
|
+
# According to spec should abort the connection
|
92
|
+
raise WebSocketError, "Protocol version #{version} not supported"
|
74
93
|
end
|
75
94
|
end
|
76
95
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
module WebSocket
|
6
|
+
module Handshake04
|
7
|
+
def handshake
|
8
|
+
# Required
|
9
|
+
unless key = request['sec-websocket-key']
|
10
|
+
raise HandshakeError, "Sec-WebSocket-Key header is required"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Optional
|
14
|
+
origin = request['sec-websocket-origin']
|
15
|
+
protocols = request['sec-websocket-protocol']
|
16
|
+
extensions = request['sec-websocket-extensions']
|
17
|
+
|
18
|
+
string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
19
|
+
signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
|
20
|
+
|
21
|
+
upgrade = ["HTTP/1.1 101 Switching Protocols"]
|
22
|
+
upgrade << "Upgrade: websocket"
|
23
|
+
upgrade << "Connection: Upgrade"
|
24
|
+
upgrade << "Sec-WebSocket-Accept: #{signature}"
|
25
|
+
|
26
|
+
# TODO: Support Sec-WebSocket-Protocol
|
27
|
+
# TODO: Sec-WebSocket-Extensions
|
28
|
+
|
29
|
+
debug [:upgrade_headers, upgrade]
|
30
|
+
|
31
|
+
return upgrade.join("\r\n") + "\r\n\r\n"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -2,14 +2,14 @@ module EventMachine
|
|
2
2
|
module WebSocket
|
3
3
|
module Handshake75
|
4
4
|
def handshake
|
5
|
-
location = "#{request['
|
6
|
-
location << ":#{request['
|
7
|
-
location << request['
|
5
|
+
location = "#{request['host'].scheme}://#{request['host'].host}"
|
6
|
+
location << ":#{request['host'].port}" if request['host'].port
|
7
|
+
location << request['path']
|
8
8
|
|
9
9
|
upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
10
10
|
upgrade << "Upgrade: WebSocket\r\n"
|
11
11
|
upgrade << "Connection: Upgrade\r\n"
|
12
|
-
upgrade << "WebSocket-Origin: #{request['
|
12
|
+
upgrade << "WebSocket-Origin: #{request['origin']}\r\n"
|
13
13
|
upgrade << "WebSocket-Location: #{location}\r\n\r\n"
|
14
14
|
|
15
15
|
debug [:upgrade_headers, upgrade]
|
@@ -5,21 +5,21 @@ module EventMachine
|
|
5
5
|
module Handshake76
|
6
6
|
def handshake
|
7
7
|
challenge_response = solve_challenge(
|
8
|
-
request['
|
9
|
-
request['
|
10
|
-
request['
|
8
|
+
request['sec-websocket-key1'],
|
9
|
+
request['sec-websocket-key2'],
|
10
|
+
request['third-key']
|
11
11
|
)
|
12
12
|
|
13
|
-
location = "#{request['
|
14
|
-
location << ":#{request['
|
15
|
-
location << request['
|
13
|
+
location = "#{request['host'].scheme}://#{request['host'].host}"
|
14
|
+
location << ":#{request['host'].port}" if request['host'].port
|
15
|
+
location << request['path']
|
16
16
|
|
17
17
|
upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
|
18
18
|
upgrade << "Upgrade: WebSocket\r\n"
|
19
19
|
upgrade << "Connection: Upgrade\r\n"
|
20
20
|
upgrade << "Sec-WebSocket-Location: #{location}\r\n"
|
21
|
-
upgrade << "Sec-WebSocket-Origin: #{request['
|
22
|
-
if protocol = request['
|
21
|
+
upgrade << "Sec-WebSocket-Origin: #{request['origin']}\r\n"
|
22
|
+
if protocol = request['sec-websocket-protocol']
|
23
23
|
validate_protocol!(protocol)
|
24
24
|
upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
|
25
25
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module WebSocket
|
3
|
+
class MaskedString < String
|
4
|
+
def read_mask
|
5
|
+
raise "Too short" if bytesize < 4 # TODO - change
|
6
|
+
@masking_key = String.new(self[0..3])
|
7
|
+
end
|
8
|
+
|
9
|
+
def slice_mask
|
10
|
+
slice!(0, 4)
|
11
|
+
end
|
12
|
+
|
13
|
+
def getbyte(index)
|
14
|
+
masked_char = super(index + 4)
|
15
|
+
masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def getbytes(start_index, count)
|
19
|
+
data = ''
|
20
|
+
count.times do |i|
|
21
|
+
data << getbyte(start_index + i)
|
22
|
+
end
|
23
|
+
data
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module WebSocket
|
5
|
+
module MessageProcessor03
|
6
|
+
def message(message_type, extension_data, application_data)
|
7
|
+
case message_type
|
8
|
+
when :close
|
9
|
+
if @state == :closing
|
10
|
+
# TODO: Check that message body matches sent data
|
11
|
+
# We can close connection immediately since there is no more data
|
12
|
+
# is allowed to be sent or received on this connection
|
13
|
+
@connection.close_connection
|
14
|
+
@state = :closed
|
15
|
+
else
|
16
|
+
# Acknowlege close
|
17
|
+
# The connection is considered closed
|
18
|
+
send_frame(:close, application_data)
|
19
|
+
@state = :closed
|
20
|
+
@connection.close_connection_after_writing
|
21
|
+
end
|
22
|
+
when :ping
|
23
|
+
# Pong back the same data
|
24
|
+
send_frame(:pong, application_data)
|
25
|
+
when :pong
|
26
|
+
# TODO: Do something. Complete a deferrable established by a ping?
|
27
|
+
when :text, :binary
|
28
|
+
@connection.trigger_on_message(application_data)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module WebSocket
|
3
|
+
module MessageProcessor06
|
4
|
+
def message(message_type, extension_data, application_data)
|
5
|
+
debug [:message_received, message_type, application_data]
|
6
|
+
|
7
|
+
case message_type
|
8
|
+
when :close
|
9
|
+
status_code = case application_data.length
|
10
|
+
when 0
|
11
|
+
# close messages MAY contain a body
|
12
|
+
nil
|
13
|
+
when 1
|
14
|
+
# Illegal close frame
|
15
|
+
raise DataError, "Close frames with a body must contain a 2 byte status code"
|
16
|
+
else
|
17
|
+
application_data.slice!(0, 2).unpack('n').first
|
18
|
+
end
|
19
|
+
|
20
|
+
debug [:close_frame_received, status_code, application_data]
|
21
|
+
|
22
|
+
if @state == :closing
|
23
|
+
# We can close connection immediately since there is no more data
|
24
|
+
# is allowed to be sent or received on this connection
|
25
|
+
@connection.close_connection
|
26
|
+
@state = :closed
|
27
|
+
else
|
28
|
+
# Acknowlege close
|
29
|
+
# The connection is considered closed
|
30
|
+
send_frame(:close, '')
|
31
|
+
@state = :closed
|
32
|
+
@connection.close_connection_after_writing
|
33
|
+
# TODO: Send close status code and body to app code
|
34
|
+
end
|
35
|
+
when :ping
|
36
|
+
# Pong back the same data
|
37
|
+
send_frame(:pong, application_data)
|
38
|
+
when :pong
|
39
|
+
# TODO: Do something. Complete a deferrable established by a ping?
|
40
|
+
when :text, :binary
|
41
|
+
@connection.trigger_on_message(application_data)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/em-websocket/version.rb
CHANGED
data/spec/helper.rb
CHANGED
@@ -10,9 +10,13 @@ Rspec.configure do |c|
|
|
10
10
|
end
|
11
11
|
|
12
12
|
class FakeWebSocketClient < EM::Connection
|
13
|
-
attr_writer :onopen, :onclose, :onmessage
|
14
13
|
attr_reader :handshake_response, :packets
|
15
14
|
|
15
|
+
def onopen(&blk); @onopen = blk; end
|
16
|
+
def onclose(&blk); @onclose = blk; end
|
17
|
+
def onerror(&blk); @onerror = blk; end
|
18
|
+
def onmessage(&blk); @onmessage = blk; end
|
19
|
+
|
16
20
|
def initialize
|
17
21
|
@state = :new
|
18
22
|
@packets = []
|
@@ -39,6 +43,54 @@ class FakeWebSocketClient < EM::Connection
|
|
39
43
|
end
|
40
44
|
end
|
41
45
|
|
46
|
+
class Draft03FakeWebSocketClient < FakeWebSocketClient
|
47
|
+
def send(application_data)
|
48
|
+
frame = ''
|
49
|
+
opcode = 4 # fake only supports text frames
|
50
|
+
byte1 = opcode # since more, rsv1-3 are 0
|
51
|
+
frame << byte1
|
52
|
+
|
53
|
+
length = application_data.size
|
54
|
+
if length <= 125
|
55
|
+
byte2 = length # since rsv4 is 0
|
56
|
+
frame << byte2
|
57
|
+
elsif length < 65536 # write 2 byte length
|
58
|
+
frame << 126
|
59
|
+
frame << [length].pack('n')
|
60
|
+
else # write 8 byte length
|
61
|
+
frame << 127
|
62
|
+
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
63
|
+
end
|
64
|
+
|
65
|
+
frame << application_data
|
66
|
+
|
67
|
+
send_data(frame)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Wrap EM:HttpRequest in a websocket like interface so that it can be used in the specs with the same interface as FakeWebSocketClient
|
72
|
+
class Draft75WebSocketClient
|
73
|
+
def onopen(&blk); @onopen = blk; end
|
74
|
+
def onclose(&blk); @onclose = blk; end
|
75
|
+
def onerror(&blk); @onerror = blk; end
|
76
|
+
def onmessage(&blk); @onmessage = blk; end
|
77
|
+
|
78
|
+
def initialize
|
79
|
+
@ws = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get(:timeout => 0)
|
80
|
+
@ws.errback { @onerror.call if @onerror }
|
81
|
+
@ws.callback { @onopen.call if @onopen }
|
82
|
+
@ws.stream { |msg| @onmessage.call(msg) if @onmessage }
|
83
|
+
end
|
84
|
+
|
85
|
+
def send(message)
|
86
|
+
@ws.send(message)
|
87
|
+
end
|
88
|
+
|
89
|
+
def close_connection
|
90
|
+
@ws.close_connection
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
42
94
|
def failed
|
43
95
|
EventMachine.stop
|
44
96
|
fail
|
@@ -52,7 +104,7 @@ def format_request(r)
|
|
52
104
|
end
|
53
105
|
|
54
106
|
def format_response(r)
|
55
|
-
data = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
|
107
|
+
data = r[:protocol] || "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
|
56
108
|
header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
|
57
109
|
data << [header_lines, '', r[:body]].join("\r\n")
|
58
110
|
data
|