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