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
data/CHANGELOG.rdoc
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
= Changelog
|
2
2
|
|
3
|
+
== 0.3.0 / 2011-05-06
|
4
|
+
|
5
|
+
- new features:
|
6
|
+
- Support WebSocket drafts 05 & 06
|
7
|
+
- changes:
|
8
|
+
- Accept request headers in a case insensitive manner
|
9
|
+
- Change handling of errors. Previously some application errors were caught
|
10
|
+
internally and were invisible unless an onerror callback was supplied. See
|
11
|
+
readme for details
|
12
|
+
|
3
13
|
== 0.2.1 / 2011-03-01
|
4
14
|
|
5
15
|
- bugfixes:
|
data/README.md
CHANGED
@@ -42,6 +42,22 @@ For example,
|
|
42
42
|
...
|
43
43
|
end
|
44
44
|
|
45
|
+
## Handling errors
|
46
|
+
|
47
|
+
There are two kinds of errors that need to be handled - errors caused by incompatible WebSocket clients sending invalid data and errors in application code. They are handled as follows:
|
48
|
+
|
49
|
+
Errors caused by invalid WebSocket data (for example invalid errors in the WebSocket handshake or invalid message frames) raise errors which descend from `EventMachine::WebSocket::WebSocketError`. Such errors are rescued internally and the WebSocket connection will be closed immediately or an error code sent to the browser in accordance to the WebSocket specification. However it is possible to be notified in application code on such errors by including an `onerror` callback.
|
50
|
+
|
51
|
+
ws.onerror { |error|
|
52
|
+
if e.kind_of?(EM::WebSocket::WebSocketError)
|
53
|
+
...
|
54
|
+
end
|
55
|
+
}
|
56
|
+
|
57
|
+
Application errors are treated differently. If no `onerror` callback has been defined these errors will propagate to the EventMachine reactor, typically causing your program to terminate. If you wish to handle exceptions, simply supply an `onerror callback` and check for exceptions which are not decendant from `EventMachine::WebSocket::WebSocketError`.
|
58
|
+
|
59
|
+
It is also possible to log all errors when developing by including the `:debug => true` option when initialising the WebSocket connection.
|
60
|
+
|
45
61
|
## Examples & Projects using em-websocket
|
46
62
|
|
47
63
|
* [Pusher](http://pusherapp.com) - Realtime client push
|
data/examples/test.html
CHANGED
@@ -1,26 +1,29 @@
|
|
1
1
|
<html>
|
2
2
|
<head>
|
3
|
-
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js'></script>
|
4
3
|
<script src='js/swfobject.js'></script>
|
5
4
|
<script src='js/FABridge.js'></script>
|
6
5
|
<script src='js/web_socket.js'></script>
|
7
6
|
<script>
|
8
|
-
|
9
|
-
function debug(
|
7
|
+
function init() {
|
8
|
+
function debug(string) {
|
9
|
+
var element = document.getElementById("debug");
|
10
|
+
var p = document.createElement("p");
|
11
|
+
p.appendChild(document.createTextNode(string));
|
12
|
+
element.appendChild(p);
|
13
|
+
}
|
10
14
|
|
11
15
|
ws = new WebSocket("ws://localhost:8080/");
|
12
|
-
ws.onmessage = function(evt) {
|
16
|
+
ws.onmessage = function(evt) { debug("Message: " + evt.data); };
|
13
17
|
ws.onclose = function() { debug("socket closed"); };
|
14
18
|
ws.onopen = function() {
|
15
19
|
debug("connected...");
|
16
20
|
ws.send("hello server");
|
17
21
|
ws.send("hello again");
|
18
22
|
};
|
19
|
-
}
|
23
|
+
};
|
20
24
|
</script>
|
21
25
|
</head>
|
22
|
-
<body>
|
26
|
+
<body onload="init();">
|
23
27
|
<div id="debug"></div>
|
24
|
-
<div id="msg"></div>
|
25
28
|
</body>
|
26
|
-
</html>
|
29
|
+
</html>
|
data/lib/em-websocket.rb
CHANGED
@@ -4,9 +4,12 @@ require "eventmachine"
|
|
4
4
|
|
5
5
|
%w[
|
6
6
|
debugger websocket connection
|
7
|
-
handshake75 handshake76
|
8
|
-
framing76 framing03
|
9
|
-
|
7
|
+
handshake75 handshake76 handshake04
|
8
|
+
framing76 framing03 framing04 framing05
|
9
|
+
close75 close03 close05 close06
|
10
|
+
masking04
|
11
|
+
message_processor_03 message_processor_06
|
12
|
+
handler_factory handler handler75 handler76 handler03 handler05 handler06
|
10
13
|
].each do |file|
|
11
14
|
require "em-websocket/#{file}"
|
12
15
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module WebSocket
|
3
|
+
module Close06
|
4
|
+
def close_websocket(code, body)
|
5
|
+
if code
|
6
|
+
close_data = [code].pack('n')
|
7
|
+
close_data << body if body
|
8
|
+
send_frame(:close, close_data)
|
9
|
+
else
|
10
|
+
send_frame(:close, '')
|
11
|
+
end
|
12
|
+
@state = :closing
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -20,6 +20,11 @@ module EventMachine
|
|
20
20
|
def trigger_on_close
|
21
21
|
@onclose.call if @onclose
|
22
22
|
end
|
23
|
+
def trigger_on_error(reason)
|
24
|
+
return false unless @onerror
|
25
|
+
@onerror.call(reason)
|
26
|
+
true
|
27
|
+
end
|
23
28
|
|
24
29
|
def initialize(options)
|
25
30
|
@options = options
|
@@ -34,13 +39,15 @@ module EventMachine
|
|
34
39
|
# Use this method to close the websocket connection cleanly
|
35
40
|
# This sends a close frame and waits for acknowlegement before closing
|
36
41
|
# the connection
|
37
|
-
def close_websocket
|
38
|
-
if
|
39
|
-
|
40
|
-
else
|
41
|
-
# The handshake hasn't completed - should be safe to terminate
|
42
|
-
close_connection
|
42
|
+
def close_websocket(code = nil, body = nil)
|
43
|
+
if code && !(4000..4999).include?(code)
|
44
|
+
raise "Application code may only use codes in the range 4000-4999"
|
43
45
|
end
|
46
|
+
|
47
|
+
# If code not defined then set to 1000 (normal closure)
|
48
|
+
code ||= 1000
|
49
|
+
|
50
|
+
close_websocket_private(code, body)
|
44
51
|
end
|
45
52
|
|
46
53
|
def post_init
|
@@ -55,12 +62,32 @@ module EventMachine
|
|
55
62
|
else
|
56
63
|
dispatch(data)
|
57
64
|
end
|
65
|
+
rescue HandshakeError => e
|
66
|
+
debug [:error, e]
|
67
|
+
trigger_on_error(e)
|
68
|
+
# Errors during the handshake require the connection to be aborted
|
69
|
+
abort
|
70
|
+
rescue WebSocketError => e
|
71
|
+
debug [:error, e]
|
72
|
+
trigger_on_error(e)
|
73
|
+
close_websocket_private(1002) # 1002 indicates a protocol error
|
74
|
+
rescue => e
|
75
|
+
debug [:error, e]
|
76
|
+
# These are application errors - raise unless onerror defined
|
77
|
+
trigger_on_error(e) || raise(e)
|
78
|
+
# There is no code defined for application errors, so use 3000
|
79
|
+
# (which is reserved for frameworks)
|
80
|
+
close_websocket_private(3000)
|
58
81
|
end
|
59
82
|
|
60
83
|
def unbind
|
61
84
|
debug [:unbind, :connection]
|
62
85
|
|
63
86
|
@handler.unbind if @handler
|
87
|
+
rescue => e
|
88
|
+
debug [:error, e]
|
89
|
+
# These are application errors - raise unless onerror defined
|
90
|
+
trigger_on_error(e) || raise(e)
|
64
91
|
end
|
65
92
|
|
66
93
|
def dispatch(data)
|
@@ -69,30 +96,18 @@ module EventMachine
|
|
69
96
|
return false
|
70
97
|
else
|
71
98
|
debug [:inbound_headers, data]
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
# The whole header has not been received yet.
|
77
|
-
return false
|
78
|
-
end
|
79
|
-
@data = nil
|
80
|
-
@handler.run
|
81
|
-
return true
|
82
|
-
rescue => e
|
83
|
-
debug [:error, e]
|
84
|
-
process_bad_request(e)
|
99
|
+
@data << data
|
100
|
+
@handler = HandlerFactory.build(self, @data, @secure, @debug)
|
101
|
+
unless @handler
|
102
|
+
# The whole header has not been received yet.
|
85
103
|
return false
|
86
104
|
end
|
105
|
+
@data = nil
|
106
|
+
@handler.run
|
107
|
+
return true
|
87
108
|
end
|
88
109
|
end
|
89
110
|
|
90
|
-
def process_bad_request(reason)
|
91
|
-
@onerror.call(reason) if @onerror
|
92
|
-
send_data "HTTP/1.1 400 Bad request\r\n\r\n"
|
93
|
-
close_connection_after_writing
|
94
|
-
end
|
95
|
-
|
96
111
|
def send_flash_cross_domain_file
|
97
112
|
file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
|
98
113
|
debug [:cross_domain, file]
|
@@ -105,8 +120,6 @@ module EventMachine
|
|
105
120
|
end
|
106
121
|
|
107
122
|
def send(data)
|
108
|
-
debug [:send, data]
|
109
|
-
|
110
123
|
if @handler
|
111
124
|
@handler.send_text_frame(data)
|
112
125
|
else
|
@@ -114,11 +127,6 @@ module EventMachine
|
|
114
127
|
end
|
115
128
|
end
|
116
129
|
|
117
|
-
def close_with_error(message)
|
118
|
-
@onerror.call(message) if @onerror
|
119
|
-
close_connection_after_writing
|
120
|
-
end
|
121
|
-
|
122
130
|
def request
|
123
131
|
@handler ? @handler.request : {}
|
124
132
|
end
|
@@ -126,6 +134,24 @@ module EventMachine
|
|
126
134
|
def state
|
127
135
|
@handler ? @handler.state : :handshake
|
128
136
|
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
# As definited in draft 06 7.2.2, some failures require that the server
|
141
|
+
# abort the websocket connection rather than close cleanly
|
142
|
+
def abort
|
143
|
+
close_connection
|
144
|
+
end
|
145
|
+
|
146
|
+
def close_websocket_private(code, body = nil)
|
147
|
+
if @handler
|
148
|
+
debug [:closing, code]
|
149
|
+
@handler.close_websocket(code, body)
|
150
|
+
else
|
151
|
+
# The handshake hasn't completed - should be safe to terminate
|
152
|
+
abort
|
153
|
+
end
|
154
|
+
end
|
129
155
|
end
|
130
156
|
end
|
131
157
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module EventMachine
|
4
4
|
module WebSocket
|
5
5
|
module Framing03
|
6
|
-
|
6
|
+
|
7
7
|
def initialize_framing
|
8
8
|
@data = ''
|
9
9
|
@application_data_buffer = '' # Used for MORE frames
|
@@ -15,7 +15,7 @@ module EventMachine
|
|
15
15
|
while !error && @data.size > 1
|
16
16
|
pointer = 0
|
17
17
|
|
18
|
-
more = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
|
18
|
+
more = ((@data.getbyte(pointer) & 0b10000000) == 0b10000000) ^ fin
|
19
19
|
# Ignoring rsv1-3 for now
|
20
20
|
opcode = @data.getbyte(0) & 0b00001111
|
21
21
|
pointer += 1
|
@@ -28,7 +28,7 @@ module EventMachine
|
|
28
28
|
when 127 # Length defined by 8 bytes
|
29
29
|
# Check buffer size
|
30
30
|
if @data.getbyte(pointer+8-1) == nil
|
31
|
-
debug [:buffer_incomplete, @data
|
31
|
+
debug [:buffer_incomplete, @data]
|
32
32
|
error = true
|
33
33
|
next
|
34
34
|
end
|
@@ -41,7 +41,7 @@ module EventMachine
|
|
41
41
|
when 126 # Length defined by 2 bytes
|
42
42
|
# Check buffer size
|
43
43
|
if @data.getbyte(pointer+2-1) == nil
|
44
|
-
debug [:buffer_incomplete, @data
|
44
|
+
debug [:buffer_incomplete, @data]
|
45
45
|
error = true
|
46
46
|
next
|
47
47
|
end
|
@@ -55,7 +55,7 @@ module EventMachine
|
|
55
55
|
|
56
56
|
# Check buffer size
|
57
57
|
if @data.getbyte(pointer+payload_length-1) == nil
|
58
|
-
debug [:buffer_incomplete, @data
|
58
|
+
debug [:buffer_incomplete, @data]
|
59
59
|
error = true
|
60
60
|
next
|
61
61
|
end
|
@@ -91,6 +91,8 @@ module EventMachine
|
|
91
91
|
end
|
92
92
|
|
93
93
|
def send_frame(frame_type, application_data)
|
94
|
+
debug [:sending_frame, frame_type, application_data]
|
95
|
+
|
94
96
|
if @state == :closing && data_frame?(frame_type)
|
95
97
|
raise WebSocketError, "Cannot send data frame since connection is closing"
|
96
98
|
end
|
@@ -124,31 +126,8 @@ module EventMachine
|
|
124
126
|
|
125
127
|
private
|
126
128
|
|
127
|
-
|
128
|
-
|
129
|
-
when :close
|
130
|
-
if @state == :closing
|
131
|
-
# TODO: Check that message body matches sent data
|
132
|
-
# We can close connection immediately since there is no more data
|
133
|
-
# is allowed to be sent or received on this connection
|
134
|
-
@connection.close_connection
|
135
|
-
@state = :closed
|
136
|
-
else
|
137
|
-
# Acknowlege close
|
138
|
-
# The connection is considered closed
|
139
|
-
send_frame(:close, application_data)
|
140
|
-
@state = :closed
|
141
|
-
@connection.close_connection_after_writing
|
142
|
-
end
|
143
|
-
when :ping
|
144
|
-
# Pong back the same data
|
145
|
-
send_frame(:pong, application_data)
|
146
|
-
when :pong
|
147
|
-
# TODO: Do something. Complete a deferrable established by a ping?
|
148
|
-
when :text, :binary
|
149
|
-
@connection.trigger_on_message(application_data)
|
150
|
-
end
|
151
|
-
end
|
129
|
+
# This allows flipping the more bit to fin for draft 04
|
130
|
+
def fin; false; end
|
152
131
|
|
153
132
|
FRAME_TYPES = {
|
154
133
|
:continuation => 0,
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module WebSocket
|
5
|
+
# The only difference between draft 03 framing and draft 04 framing is
|
6
|
+
# that the MORE bit has been changed to a FIN bit
|
7
|
+
module Framing04
|
8
|
+
include Framing03
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def fin; true; end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module WebSocket
|
5
|
+
module Framing05
|
6
|
+
|
7
|
+
def initialize_framing
|
8
|
+
@data = MaskedString.new
|
9
|
+
@application_data_buffer = '' # Used for MORE frames
|
10
|
+
end
|
11
|
+
|
12
|
+
def process_data(newdata)
|
13
|
+
error = false
|
14
|
+
|
15
|
+
while !error && @data.size > 5 # mask plus first byte present
|
16
|
+
pointer = 0
|
17
|
+
|
18
|
+
@data.read_mask
|
19
|
+
|
20
|
+
fin = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
|
21
|
+
# Ignoring rsv1-3 for now
|
22
|
+
opcode = @data.getbyte(pointer) & 0b00001111
|
23
|
+
pointer += 1
|
24
|
+
|
25
|
+
# Ignoring rsv4
|
26
|
+
length = @data.getbyte(pointer) & 0b01111111
|
27
|
+
pointer += 1
|
28
|
+
|
29
|
+
payload_length = case length
|
30
|
+
when 127 # Length defined by 8 bytes
|
31
|
+
# Check buffer size
|
32
|
+
if @data.getbyte(pointer+8-1) == nil
|
33
|
+
debug [:buffer_incomplete, @data]
|
34
|
+
error = true
|
35
|
+
next
|
36
|
+
end
|
37
|
+
|
38
|
+
# Only using the last 4 bytes for now, till I work out how to
|
39
|
+
# unpack 8 bytes. I'm sure 4GB frames will do for now :)
|
40
|
+
l = @data.getbytes(pointer+4, 4).unpack('N').first
|
41
|
+
pointer += 8
|
42
|
+
l
|
43
|
+
when 126 # Length defined by 2 bytes
|
44
|
+
# Check buffer size
|
45
|
+
if @data.getbyte(pointer+2-1) == nil
|
46
|
+
debug [:buffer_incomplete, @data]
|
47
|
+
error = true
|
48
|
+
next
|
49
|
+
end
|
50
|
+
|
51
|
+
l = @data.getbytes(pointer, 2).unpack('n').first
|
52
|
+
pointer += 2
|
53
|
+
l
|
54
|
+
else
|
55
|
+
length
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check buffer size
|
59
|
+
if @data.getbyte(pointer+payload_length-1) == nil
|
60
|
+
debug [:buffer_incomplete, @data]
|
61
|
+
error = true
|
62
|
+
next
|
63
|
+
end
|
64
|
+
|
65
|
+
# Read application data
|
66
|
+
application_data = @data.getbytes(pointer, payload_length)
|
67
|
+
pointer += payload_length
|
68
|
+
|
69
|
+
# Throw away data up to pointer
|
70
|
+
@data.slice!(0...(pointer + 4))
|
71
|
+
|
72
|
+
frame_type = opcode_to_type(opcode)
|
73
|
+
|
74
|
+
if frame_type == :continuation && !@frame_type
|
75
|
+
raise WebSocketError, 'Continuation frame not expected'
|
76
|
+
end
|
77
|
+
|
78
|
+
if !fin
|
79
|
+
debug [:moreframe, frame_type, application_data]
|
80
|
+
@application_data_buffer << application_data
|
81
|
+
@frame_type = frame_type
|
82
|
+
else
|
83
|
+
# Message is complete
|
84
|
+
if frame_type == :continuation
|
85
|
+
@application_data_buffer << application_data
|
86
|
+
message(@frame_type, '', @application_data_buffer)
|
87
|
+
@application_data_buffer = ''
|
88
|
+
@frame_type = nil
|
89
|
+
else
|
90
|
+
message(frame_type, '', application_data)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end # end while
|
94
|
+
end
|
95
|
+
|
96
|
+
def send_frame(frame_type, application_data)
|
97
|
+
debug [:sending_frame, frame_type, application_data]
|
98
|
+
|
99
|
+
if @state == :closing && data_frame?(frame_type)
|
100
|
+
raise WebSocketError, "Cannot send data frame since connection is closing"
|
101
|
+
end
|
102
|
+
|
103
|
+
frame = ''
|
104
|
+
|
105
|
+
opcode = type_to_opcode(frame_type)
|
106
|
+
byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0
|
107
|
+
frame << byte1
|
108
|
+
|
109
|
+
length = application_data.size
|
110
|
+
if length <= 125
|
111
|
+
byte2 = length # since rsv4 is 0
|
112
|
+
frame << byte2
|
113
|
+
elsif length < 65536 # write 2 byte length
|
114
|
+
frame << 126
|
115
|
+
frame << [length].pack('n')
|
116
|
+
else # write 8 byte length
|
117
|
+
frame << 127
|
118
|
+
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
119
|
+
end
|
120
|
+
|
121
|
+
frame << application_data
|
122
|
+
|
123
|
+
@connection.send_data(frame)
|
124
|
+
end
|
125
|
+
|
126
|
+
def send_text_frame(data)
|
127
|
+
send_frame(:text, data)
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
FRAME_TYPES = {
|
133
|
+
:continuation => 0,
|
134
|
+
:close => 1,
|
135
|
+
:ping => 2,
|
136
|
+
:pong => 3,
|
137
|
+
:text => 4,
|
138
|
+
:binary => 5
|
139
|
+
}
|
140
|
+
FRAME_TYPES_INVERSE = FRAME_TYPES.invert
|
141
|
+
# Frames are either data frames or control frames
|
142
|
+
DATA_FRAMES = [:text, :binary, :continuation]
|
143
|
+
|
144
|
+
def type_to_opcode(frame_type)
|
145
|
+
FRAME_TYPES[frame_type] || raise("Unknown frame type")
|
146
|
+
end
|
147
|
+
|
148
|
+
def opcode_to_type(opcode)
|
149
|
+
FRAME_TYPES_INVERSE[opcode] || raise("Unknown opcode")
|
150
|
+
end
|
151
|
+
|
152
|
+
def data_frame?(type)
|
153
|
+
DATA_FRAMES.include?(type)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|