em-websocket 0.0.6 → 0.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.
- data/VERSION +1 -1
- data/examples/multicast.rb +11 -9
- data/lib/em-websocket.rb +1 -1
- data/lib/em-websocket/connection.rb +78 -85
- data/lib/em-websocket/debugger.rb +17 -0
- data/lib/em-websocket/handler.rb +21 -0
- data/lib/em-websocket/handler75.rb +21 -0
- data/lib/em-websocket/handler76.rb +61 -0
- data/lib/em-websocket/handler_factory.rb +52 -0
- data/lib/em-websocket/websocket.rb +1 -1
- data/spec/helper.rb +60 -1
- data/spec/integration/integration_spec.rb +102 -0
- data/spec/unit/handler_spec.rb +117 -0
- data/spec/websocket_spec.rb +0 -5
- metadata +14 -4
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.1.1
|
data/examples/multicast.rb
CHANGED
@@ -25,19 +25,21 @@ EventMachine.run {
|
|
25
25
|
|
26
26
|
|
27
27
|
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080, :debug => true) do |ws|
|
28
|
+
|
28
29
|
ws.onopen {
|
29
|
-
|
30
|
-
@channel.push "#{
|
31
|
-
}
|
30
|
+
sid = @channel.subscribe { |msg| ws.send msg }
|
31
|
+
@channel.push "#{sid} connected!"
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
ws.onmessage { |msg|
|
34
|
+
@channel.push "<#{sid}>: #{msg}"
|
35
|
+
}
|
36
|
+
|
37
|
+
ws.onclose {
|
38
|
+
@channel.unsubscribe(sid)
|
39
|
+
}
|
36
40
|
|
37
|
-
ws.onclose {
|
38
|
-
@channel.unsubscribe(@sid)
|
39
41
|
}
|
40
42
|
end
|
41
43
|
|
42
44
|
puts "Server started"
|
43
|
-
}
|
45
|
+
}
|
data/lib/em-websocket.rb
CHANGED
@@ -3,6 +3,6 @@ $:.unshift(File.dirname(__FILE__) + '/../lib')
|
|
3
3
|
#require "rubygems"
|
4
4
|
require "eventmachine"
|
5
5
|
|
6
|
-
%w[ websocket connection ].each do |file|
|
6
|
+
%w[ debugger websocket connection handler_factory handler handler75 handler76 ].each do |file|
|
7
7
|
require "em-websocket/#{file}"
|
8
8
|
end
|
@@ -3,9 +3,7 @@ require 'addressable/uri'
|
|
3
3
|
module EventMachine
|
4
4
|
module WebSocket
|
5
5
|
class Connection < EventMachine::Connection
|
6
|
-
|
7
|
-
PATH = /^GET (\/[^\s]*) HTTP\/1\.1$/
|
8
|
-
HEADER = /^([^:]+):\s*([^$]+)/
|
6
|
+
include Debugger
|
9
7
|
|
10
8
|
attr_reader :state, :request
|
11
9
|
|
@@ -17,14 +15,18 @@ module EventMachine
|
|
17
15
|
def initialize(options)
|
18
16
|
@options = options
|
19
17
|
@debug = options[:debug] || false
|
18
|
+
@secure = options[:secure] || false
|
20
19
|
@state = :handshake
|
21
20
|
@request = {}
|
22
21
|
@data = ''
|
23
|
-
@skip_onclose = false
|
24
22
|
|
25
23
|
debug [:initialize]
|
26
24
|
end
|
27
25
|
|
26
|
+
def post_init
|
27
|
+
start_tls if @secure
|
28
|
+
end
|
29
|
+
|
28
30
|
def receive_data(data)
|
29
31
|
debug [:receive_data, data]
|
30
32
|
|
@@ -40,58 +42,36 @@ module EventMachine
|
|
40
42
|
end
|
41
43
|
|
42
44
|
def dispatch
|
43
|
-
|
45
|
+
case @state
|
44
46
|
when :handshake
|
45
|
-
|
46
|
-
when :upgrade
|
47
|
-
send_upgrade
|
47
|
+
handshake
|
48
48
|
when :connected
|
49
49
|
process_message
|
50
50
|
else raise RuntimeError, "invalid state: #{@state}"
|
51
|
-
end
|
52
51
|
end
|
53
52
|
end
|
54
53
|
|
55
|
-
def
|
56
|
-
if @data.match(
|
54
|
+
def handshake
|
55
|
+
if @data.match(/<policy-file-request\s*\/>/)
|
56
|
+
send_flash_cross_domain_file
|
57
|
+
return false
|
58
|
+
else
|
57
59
|
debug [:inbound_headers, @data]
|
58
|
-
lines = @data.split("\r\n")
|
59
|
-
|
60
60
|
begin
|
61
|
-
|
62
|
-
@
|
63
|
-
|
64
|
-
|
65
|
-
@request
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
h = HEADER.match(line)
|
70
|
-
@request[h[1].strip] = h[2].strip
|
71
|
-
end
|
72
|
-
|
73
|
-
# transform headers
|
74
|
-
@request['Host'] = Addressable::URI.parse("ws://"+@request['Host'])
|
75
|
-
|
76
|
-
if not websocket_connection?
|
77
|
-
process_bad_request
|
78
|
-
return false
|
79
|
-
else
|
80
|
-
@data = ''
|
81
|
-
@state = :upgrade
|
82
|
-
return true
|
83
|
-
end
|
61
|
+
@handler = HandlerFactory.build(@data, @secure, @debug)
|
62
|
+
@data = ''
|
63
|
+
send_data @handler.handshake
|
64
|
+
|
65
|
+
@request = @handler.request
|
66
|
+
@state = :connected
|
67
|
+
@onopen.call if @onopen
|
68
|
+
return true
|
84
69
|
rescue => e
|
85
70
|
debug [:error, e]
|
86
71
|
process_bad_request
|
87
72
|
return false
|
88
73
|
end
|
89
|
-
elsif @data.match(/<policy-file-request\s*\/>/)
|
90
|
-
send_flash_cross_domain_file
|
91
|
-
return false
|
92
74
|
end
|
93
|
-
|
94
|
-
false
|
95
75
|
end
|
96
76
|
|
97
77
|
def process_bad_request
|
@@ -99,53 +79,76 @@ module EventMachine
|
|
99
79
|
close_connection_after_writing
|
100
80
|
end
|
101
81
|
|
102
|
-
def websocket_connection?
|
103
|
-
@request['Connection'] == 'Upgrade' and @request['Upgrade'] == 'WebSocket'
|
104
|
-
end
|
105
|
-
|
106
|
-
def send_upgrade
|
107
|
-
location = "ws://#{@request['Host'].host}"
|
108
|
-
location << ":#{@request['Host'].port}" if @request['Host'].port
|
109
|
-
location << @request['Path']
|
110
|
-
|
111
|
-
upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
112
|
-
upgrade << "Upgrade: WebSocket\r\n"
|
113
|
-
upgrade << "Connection: Upgrade\r\n"
|
114
|
-
upgrade << "WebSocket-Origin: #{@request['Origin']}\r\n"
|
115
|
-
upgrade << "WebSocket-Location: #{location}\r\n\r\n"
|
116
|
-
|
117
|
-
# upgrade connection and notify client callback
|
118
|
-
# about completed handshake
|
119
|
-
debug [:upgrade_headers, upgrade]
|
120
|
-
send_data upgrade
|
121
|
-
|
122
|
-
@state = :connected
|
123
|
-
@onopen.call if @onopen
|
124
|
-
|
125
|
-
# stop dispatch, wait for messages
|
126
|
-
false
|
127
|
-
end
|
128
|
-
|
129
82
|
def send_flash_cross_domain_file
|
130
83
|
file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
|
131
84
|
debug [:cross_domain, file]
|
132
85
|
send_data file
|
133
86
|
|
134
87
|
# handle the cross-domain request transparently
|
135
|
-
# no need to
|
88
|
+
# no need to notify the user about this connection
|
136
89
|
@onclose = nil
|
137
90
|
close_connection_after_writing
|
138
91
|
end
|
139
92
|
|
140
93
|
def process_message
|
141
|
-
return if not @onmessage
|
142
94
|
debug [:message, @data]
|
143
95
|
|
144
|
-
#
|
145
|
-
#
|
146
|
-
|
147
|
-
|
148
|
-
|
96
|
+
# This algorithm comes straight from the spec
|
97
|
+
# http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76#section-4.2
|
98
|
+
|
99
|
+
error = false
|
100
|
+
|
101
|
+
while !error
|
102
|
+
pointer = 0
|
103
|
+
frame_type = @data[pointer].to_i
|
104
|
+
pointer += 1
|
105
|
+
|
106
|
+
if (frame_type & 0x80) == 0x80
|
107
|
+
# If the high-order bit of the /frame type/ byte is set
|
108
|
+
length = 0
|
109
|
+
|
110
|
+
loop do
|
111
|
+
b = @data[pointer].to_i
|
112
|
+
return false unless b
|
113
|
+
pointer += 1
|
114
|
+
b_v = b & 0x7F
|
115
|
+
length = length * 128 + b_v
|
116
|
+
break unless (b & 0x80) == 0x80
|
117
|
+
end
|
118
|
+
|
119
|
+
if @data[pointer+length-1] == nil
|
120
|
+
debug [:buffer_incomplete, @data.inspect]
|
121
|
+
# Incomplete data - leave @data to accumulate
|
122
|
+
error = true
|
123
|
+
else
|
124
|
+
# Straight from spec - I'm sure this isn't crazy...
|
125
|
+
# 6. Read /length/ bytes.
|
126
|
+
# 7. Discard the read bytes.
|
127
|
+
@data = @data[(pointer+length)..-1]
|
128
|
+
|
129
|
+
# If the /frame type/ is 0xFF and the /length/ was 0, then close
|
130
|
+
if length == 0
|
131
|
+
send_data("\xff\x00")
|
132
|
+
@state = :closing
|
133
|
+
close_connection_after_writing
|
134
|
+
else
|
135
|
+
error = true
|
136
|
+
end
|
137
|
+
end
|
138
|
+
else
|
139
|
+
# If the high-order bit of the /frame type/ byte is _not_ set
|
140
|
+
msg = @data.slice!(/^\x00([^\xff]*)\xff/)
|
141
|
+
if msg
|
142
|
+
msg.gsub!(/^\x00|\xff$/, '')
|
143
|
+
if @state == :closing
|
144
|
+
debug [:ignored_message, msg]
|
145
|
+
else
|
146
|
+
@onmessage.call(msg) if @onmessage
|
147
|
+
end
|
148
|
+
else
|
149
|
+
error = true
|
150
|
+
end
|
151
|
+
end
|
149
152
|
end
|
150
153
|
|
151
154
|
false
|
@@ -162,16 +165,6 @@ module EventMachine
|
|
162
165
|
debug [:send, data]
|
163
166
|
send_data("\x00#{data}\xff")
|
164
167
|
end
|
165
|
-
|
166
|
-
private
|
167
|
-
|
168
|
-
def debug(*data)
|
169
|
-
if @debug
|
170
|
-
require 'pp'
|
171
|
-
pp data
|
172
|
-
puts
|
173
|
-
end
|
174
|
-
end
|
175
168
|
end
|
176
169
|
end
|
177
170
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module WebSocket
|
3
|
+
class HandshakeError < RuntimeError; end
|
4
|
+
|
5
|
+
class Handler
|
6
|
+
include Debugger
|
7
|
+
|
8
|
+
attr_reader :request
|
9
|
+
|
10
|
+
def initialize(request, response, debug = false)
|
11
|
+
@request = request
|
12
|
+
@response = response
|
13
|
+
@debug = debug
|
14
|
+
end
|
15
|
+
|
16
|
+
def handshake
|
17
|
+
# Implemented in subclass
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module WebSocket
|
3
|
+
class Handler75 < Handler
|
4
|
+
def handshake
|
5
|
+
location = "#{@request['Host'].scheme}://#{@request['Host'].host}"
|
6
|
+
location << ":#{@request['Host'].port}" if @request['Host'].port
|
7
|
+
location << @request['Path']
|
8
|
+
|
9
|
+
upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
10
|
+
upgrade << "Upgrade: WebSocket\r\n"
|
11
|
+
upgrade << "Connection: Upgrade\r\n"
|
12
|
+
upgrade << "WebSocket-Origin: #{@request['Origin']}\r\n"
|
13
|
+
upgrade << "WebSocket-Location: #{location}\r\n\r\n"
|
14
|
+
|
15
|
+
debug [:upgrade_headers, upgrade]
|
16
|
+
|
17
|
+
return upgrade
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module WebSocket
|
5
|
+
class Handler76 < Handler
|
6
|
+
# "\377\000" is octet version and "\xff\x00" is hex version
|
7
|
+
TERMINATE_STRING = "\xff\x00"
|
8
|
+
|
9
|
+
def handshake
|
10
|
+
challenge_response = solve_challange(
|
11
|
+
@request['Sec-WebSocket-Key1'],
|
12
|
+
@request['Sec-WebSocket-Key2'],
|
13
|
+
@request['Third-Key']
|
14
|
+
)
|
15
|
+
|
16
|
+
location = "#{@request['Host'].scheme}://#{@request['Host'].host}"
|
17
|
+
location << ":#{@request['Host'].port}" if @request['Host'].port
|
18
|
+
location << @request['Path']
|
19
|
+
|
20
|
+
upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
|
21
|
+
upgrade << "Upgrade: WebSocket\r\n"
|
22
|
+
upgrade << "Connection: Upgrade\r\n"
|
23
|
+
upgrade << "Sec-WebSocket-Location: #{location}\r\n"
|
24
|
+
upgrade << "Sec-WebSocket-Origin: #{@request['Origin']}\r\n"
|
25
|
+
if protocol = @request['Sec-WebSocket-Protocol']
|
26
|
+
validate_protocol!(protocol)
|
27
|
+
upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
|
28
|
+
end
|
29
|
+
upgrade << "\r\n"
|
30
|
+
upgrade << challenge_response
|
31
|
+
|
32
|
+
debug [:upgrade_headers, upgrade]
|
33
|
+
|
34
|
+
return upgrade
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def solve_challange(first, second, third)
|
40
|
+
# Refer to 5.2 4-9 of the draft 76
|
41
|
+
sum = [(extract_nums(first) / count_spaces(first))].pack("N*") +
|
42
|
+
[(extract_nums(second) / count_spaces(second))].pack("N*") +
|
43
|
+
third
|
44
|
+
Digest::MD5.digest(sum)
|
45
|
+
end
|
46
|
+
|
47
|
+
def extract_nums(string)
|
48
|
+
string.scan(/[0-9]/).join.to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
def count_spaces(string)
|
52
|
+
string.scan(/ /).size
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_protocol!(protocol)
|
56
|
+
raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
|
57
|
+
# TODO: Validate characters
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module WebSocket
|
3
|
+
class HandlerFactory
|
4
|
+
PATH = /^(\w+) (\/[^\s]*) HTTP\/1\.1$/
|
5
|
+
HEADER = /^([^:]+):\s*([^$]+)/
|
6
|
+
|
7
|
+
def self.build(data, secure = false, debug = false)
|
8
|
+
request = {}
|
9
|
+
response = nil
|
10
|
+
|
11
|
+
lines = data.split("\r\n")
|
12
|
+
|
13
|
+
# extract request path
|
14
|
+
first_line = lines.shift.match(PATH)
|
15
|
+
request['Method'] = first_line[1].strip
|
16
|
+
request['Path'] = first_line[2].strip
|
17
|
+
|
18
|
+
unless request["Method"] == "GET"
|
19
|
+
raise HandshakeError, "Must be GET request"
|
20
|
+
end
|
21
|
+
|
22
|
+
# extract query string values
|
23
|
+
request['Query'] = Addressable::URI.parse(request['Path']).query_values ||= {}
|
24
|
+
# extract remaining headers
|
25
|
+
lines.each do |line|
|
26
|
+
h = HEADER.match(line)
|
27
|
+
request[h[1].strip] = h[2].strip if h
|
28
|
+
end
|
29
|
+
request['Third-Key'] = lines.last
|
30
|
+
|
31
|
+
unless request['Connection'] == 'Upgrade' and request['Upgrade'] == 'WebSocket'
|
32
|
+
raise HandshakeError, "Connection and Upgrade headers required"
|
33
|
+
end
|
34
|
+
|
35
|
+
# transform headers
|
36
|
+
protocol = (secure ? "wss" : "ws")
|
37
|
+
request['Host'] = Addressable::URI.parse("#{protocol}://"+request['Host'])
|
38
|
+
|
39
|
+
version = request['Sec-WebSocket-Key1'] ? 76 : 75
|
40
|
+
|
41
|
+
case version
|
42
|
+
when 75
|
43
|
+
Handler75.new(request, response, debug)
|
44
|
+
when 76
|
45
|
+
Handler76.new(request, response, debug)
|
46
|
+
else
|
47
|
+
raise "Must not happen"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/spec/helper.rb
CHANGED
@@ -3,4 +3,63 @@ require 'spec'
|
|
3
3
|
require 'pp'
|
4
4
|
require 'em-http'
|
5
5
|
|
6
|
-
require 'lib/em-websocket'
|
6
|
+
require 'lib/em-websocket'
|
7
|
+
|
8
|
+
class FakeWebSocketClient < EM::Connection
|
9
|
+
attr_writer :onopen, :onclose, :onmessage
|
10
|
+
attr_reader :handshake_response, :packets
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@state = :new
|
14
|
+
@packets = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def receive_data(data)
|
18
|
+
# puts "RECEIVE DATA #{data}"
|
19
|
+
if @state == :new
|
20
|
+
@handshake_response = data
|
21
|
+
@onopen.call if @onopen
|
22
|
+
@state = :open
|
23
|
+
else
|
24
|
+
@onmessage.call if @onmessage
|
25
|
+
@packets << data
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def send(data)
|
30
|
+
send_data("\x00#{data}\xff")
|
31
|
+
end
|
32
|
+
|
33
|
+
def unbind
|
34
|
+
@onclose.call if @onclose
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def failed
|
39
|
+
EventMachine.stop
|
40
|
+
fail
|
41
|
+
end
|
42
|
+
|
43
|
+
def format_request(r)
|
44
|
+
data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n"
|
45
|
+
header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
|
46
|
+
data << [header_lines, '', r[:body]].join("\r\n")
|
47
|
+
data
|
48
|
+
end
|
49
|
+
|
50
|
+
def format_response(r)
|
51
|
+
data = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
|
52
|
+
header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
|
53
|
+
data << [header_lines, '', r[:body]].join("\r\n")
|
54
|
+
data
|
55
|
+
end
|
56
|
+
|
57
|
+
def handler(request, secure = false)
|
58
|
+
EM::WebSocket::HandlerFactory.build(format_request(request), secure)
|
59
|
+
end
|
60
|
+
|
61
|
+
def send_handshake(response)
|
62
|
+
simple_matcher do |given|
|
63
|
+
given.handshake.lines.sort == format_response(response).lines.sort
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spec/helper'
|
2
|
+
|
3
|
+
describe "WebSocket server draft76" do
|
4
|
+
before :each do
|
5
|
+
@request = {
|
6
|
+
:port => 80,
|
7
|
+
:method => "GET",
|
8
|
+
:path => "/demo",
|
9
|
+
:headers => {
|
10
|
+
'Host' => 'example.com',
|
11
|
+
'Connection' => 'Upgrade',
|
12
|
+
'Sec-WebSocket-Key2' => '12998 5 Y3 1 .P00',
|
13
|
+
'Sec-WebSocket-Protocol' => 'sample',
|
14
|
+
'Upgrade' => 'WebSocket',
|
15
|
+
'Sec-WebSocket-Key1' => '4 @1 46546xW%0l 1 5',
|
16
|
+
'Origin' => 'http://example.com'
|
17
|
+
},
|
18
|
+
:body => '^n:ds[4U'
|
19
|
+
}
|
20
|
+
|
21
|
+
@response = {
|
22
|
+
:headers => {
|
23
|
+
"Upgrade" => "WebSocket",
|
24
|
+
"Connection" => "Upgrade",
|
25
|
+
"Sec-WebSocket-Location" => "ws://example.com/demo",
|
26
|
+
"Sec-WebSocket-Origin" => "http://example.com",
|
27
|
+
"Sec-WebSocket-Protocol" => "sample"
|
28
|
+
},
|
29
|
+
:body => "8jKS\'y:G*Co,Wxa-"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should send back the correct handshake response" do
|
34
|
+
EM.run do
|
35
|
+
EM.add_timer(0.1) do
|
36
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) { }
|
37
|
+
|
38
|
+
# Create a fake client which sends draft 76 handshake
|
39
|
+
connection = EM.connect('0.0.0.0', 12345, FakeWebSocketClient)
|
40
|
+
connection.send_data(format_request(@request))
|
41
|
+
|
42
|
+
connection.onopen = lambda {
|
43
|
+
connection.handshake_response.lines.sort.
|
44
|
+
should == format_response(@response).lines.sort
|
45
|
+
EM.stop
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should send closing frame back and close the connection after recieving closing frame" do
|
52
|
+
EM.run do
|
53
|
+
EM.add_timer(0.1) do
|
54
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) { }
|
55
|
+
|
56
|
+
# Create a fake client which sends draft 76 handshake
|
57
|
+
connection = EM.connect('0.0.0.0', 12345, FakeWebSocketClient)
|
58
|
+
connection.send_data(format_request(@request))
|
59
|
+
|
60
|
+
# Send closing frame after handshake complete
|
61
|
+
connection.onopen = lambda {
|
62
|
+
connection.send_data(EM::WebSocket::Handler76::TERMINATE_STRING)
|
63
|
+
}
|
64
|
+
|
65
|
+
# Check that this causes a termination string to be returned and the
|
66
|
+
# connection close
|
67
|
+
connection.onclose = lambda {
|
68
|
+
connection.packets[0].should ==
|
69
|
+
EM::WebSocket::Handler76::TERMINATE_STRING
|
70
|
+
EM.stop
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should ignore any data received after the closing frame" do
|
77
|
+
EM.run do
|
78
|
+
EM.add_timer(0.1) do
|
79
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) { |ws|
|
80
|
+
# Fail if foobar message is received
|
81
|
+
ws.onmessage { |msg|
|
82
|
+
failed
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
# Create a fake client which sends draft 76 handshake
|
87
|
+
connection = EM.connect('0.0.0.0', 12345, FakeWebSocketClient)
|
88
|
+
connection.send_data(format_request(@request))
|
89
|
+
|
90
|
+
# Send closing frame after handshake complete, followed by another msg
|
91
|
+
connection.onopen = lambda {
|
92
|
+
connection.send_data(EM::WebSocket::Handler76::TERMINATE_STRING)
|
93
|
+
connection.send('foobar')
|
94
|
+
}
|
95
|
+
|
96
|
+
connection.onclose = lambda {
|
97
|
+
EM.stop
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'spec/helper'
|
2
|
+
|
3
|
+
describe "EventMachine::WebSocket::Handler" do
|
4
|
+
before :each do
|
5
|
+
@request = {
|
6
|
+
:port => 80,
|
7
|
+
:method => "GET",
|
8
|
+
:path => "/demo",
|
9
|
+
:headers => {
|
10
|
+
'Host' => 'example.com',
|
11
|
+
'Connection' => 'Upgrade',
|
12
|
+
'Sec-WebSocket-Key2' => '12998 5 Y3 1 .P00',
|
13
|
+
'Sec-WebSocket-Protocol' => 'sample',
|
14
|
+
'Upgrade' => 'WebSocket',
|
15
|
+
'Sec-WebSocket-Key1' => '4 @1 46546xW%0l 1 5',
|
16
|
+
'Origin' => 'http://example.com'
|
17
|
+
},
|
18
|
+
:body => '^n:ds[4U'
|
19
|
+
}
|
20
|
+
@secure_request = @request.merge(:port => 443)
|
21
|
+
|
22
|
+
@response = {
|
23
|
+
:headers => {
|
24
|
+
"Upgrade" => "WebSocket",
|
25
|
+
"Connection" => "Upgrade",
|
26
|
+
"Sec-WebSocket-Location" => "ws://example.com/demo",
|
27
|
+
"Sec-WebSocket-Origin" => "http://example.com",
|
28
|
+
"Sec-WebSocket-Protocol" => "sample"
|
29
|
+
},
|
30
|
+
:body => "8jKS\'y:G*Co,Wxa-"
|
31
|
+
}
|
32
|
+
@secure_response = @response.merge(:headers => @response[:headers].merge('Sec-WebSocket-Location' => "wss://example.com/demo"))
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should handle good request" do
|
36
|
+
handler(@request).should send_handshake(@response)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should handle good request to secure default port if secure mode is enabled" do
|
40
|
+
handler(@secure_request, true).should send_handshake(@secure_response)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should not handle good request to secure default port if secure mode is disabled" do
|
44
|
+
handler(@secure_request, false).should_not send_handshake(@secure_response)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should handle good request on nondefault port" do
|
48
|
+
@request[:port] = 8081
|
49
|
+
@request[:headers]['Host'] = 'example.com:8081'
|
50
|
+
@response[:headers]['Sec-WebSocket-Location'] =
|
51
|
+
'ws://example.com:8081/demo'
|
52
|
+
|
53
|
+
handler(@request).should send_handshake(@response)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should handle good request to secure nondefault port" do
|
57
|
+
@secure_request[:port] = 8081
|
58
|
+
@secure_request[:headers]['Host'] = 'example.com:8081'
|
59
|
+
@secure_response[:headers]['Sec-WebSocket-Location'] = 'wss://example.com:8081/demo'
|
60
|
+
handler(@secure_request, true).should send_handshake(@secure_response)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should handle good request with no protocol" do
|
64
|
+
@request[:headers].delete('Sec-WebSocket-Protocol')
|
65
|
+
@response[:headers].delete("Sec-WebSocket-Protocol")
|
66
|
+
|
67
|
+
handler(@request).should send_handshake(@response)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should handle extra headers by simply ignoring them" do
|
71
|
+
@request[:headers]['EmptyValue'] = ""
|
72
|
+
@request[:headers]['AKey'] = "AValue"
|
73
|
+
|
74
|
+
handler(@request).should send_handshake(@response)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should raise error on HTTP request" do
|
78
|
+
@request[:headers] = {
|
79
|
+
'Host' => 'www.google.com',
|
80
|
+
'User-Agent' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 GTB6 GTBA',
|
81
|
+
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
82
|
+
'Accept-Language' => 'en-us,en;q=0.5',
|
83
|
+
'Accept-Encoding' => 'gzip,deflate',
|
84
|
+
'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
85
|
+
'Keep-Alive' => '300',
|
86
|
+
'Connection' => 'keep-alive',
|
87
|
+
}
|
88
|
+
|
89
|
+
lambda {
|
90
|
+
handler(@request).handshake
|
91
|
+
}.should raise_error(EM::WebSocket::HandshakeError)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should raise error on wrong method" do
|
95
|
+
@request[:method] = 'POST'
|
96
|
+
|
97
|
+
lambda {
|
98
|
+
handler(@request).handshake
|
99
|
+
}.should raise_error(EM::WebSocket::HandshakeError)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should raise error if upgrade header incorrect" do
|
103
|
+
@request[:headers]['Upgrade'] = 'NonWebSocket'
|
104
|
+
|
105
|
+
lambda {
|
106
|
+
handler(@request).handshake
|
107
|
+
}.should raise_error(EM::WebSocket::HandshakeError)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should raise error if Sec-WebSocket-Protocol is empty" do
|
111
|
+
@request[:headers]['Sec-WebSocket-Protocol'] = ''
|
112
|
+
|
113
|
+
lambda {
|
114
|
+
handler(@request).handshake
|
115
|
+
}.should raise_error(EM::WebSocket::HandshakeError)
|
116
|
+
end
|
117
|
+
end
|
data/spec/websocket_spec.rb
CHANGED
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Ilya Grigorik
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-06-13 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -72,6 +72,7 @@ files:
|
|
72
72
|
- README.rdoc
|
73
73
|
- Rakefile
|
74
74
|
- VERSION
|
75
|
+
- em-websocket.gemspec
|
75
76
|
- examples/echo.rb
|
76
77
|
- examples/js/FABridge.js
|
77
78
|
- examples/js/WebSocketMain.swf
|
@@ -81,8 +82,15 @@ files:
|
|
81
82
|
- examples/test.html
|
82
83
|
- lib/em-websocket.rb
|
83
84
|
- lib/em-websocket/connection.rb
|
85
|
+
- lib/em-websocket/debugger.rb
|
86
|
+
- lib/em-websocket/handler.rb
|
87
|
+
- lib/em-websocket/handler75.rb
|
88
|
+
- lib/em-websocket/handler76.rb
|
89
|
+
- lib/em-websocket/handler_factory.rb
|
84
90
|
- lib/em-websocket/websocket.rb
|
85
91
|
- spec/helper.rb
|
92
|
+
- spec/integration/integration_spec.rb
|
93
|
+
- spec/unit/handler_spec.rb
|
86
94
|
- spec/websocket_spec.rb
|
87
95
|
has_rdoc: true
|
88
96
|
homepage: http://github.com/igrigorik/em-websocket
|
@@ -116,6 +124,8 @@ specification_version: 3
|
|
116
124
|
summary: EventMachine based WebSocket server
|
117
125
|
test_files:
|
118
126
|
- spec/helper.rb
|
127
|
+
- spec/integration/integration_spec.rb
|
128
|
+
- spec/unit/handler_spec.rb
|
119
129
|
- spec/websocket_spec.rb
|
120
130
|
- examples/echo.rb
|
121
131
|
- examples/multicast.rb
|