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