em-websocket 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +5 -0
- data/README.md +40 -55
- data/lib/em-websocket/framing03.rb +1 -1
- data/lib/em-websocket/framing05.rb +4 -2
- data/lib/em-websocket/framing07.rb +168 -0
- data/lib/em-websocket/handler07.rb +10 -0
- data/lib/em-websocket/handler08.rb +10 -0
- data/lib/em-websocket/handler_factory.rb +4 -0
- data/lib/em-websocket/masking04.rb +15 -2
- data/lib/em-websocket/version.rb +1 -1
- data/lib/em-websocket.rb +2 -2
- data/spec/unit/framing_spec.rb +69 -0
- data/spec/unit/masking_spec.rb +13 -4
- metadata +5 -2
data/CHANGELOG.rdoc
CHANGED
data/README.md
CHANGED
@@ -6,23 +6,25 @@ EventMachine based, async, Ruby WebSocket server. Take a look at examples direct
|
|
6
6
|
|
7
7
|
## Simple server example
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
9
|
+
```ruby
|
10
|
+
EventMachine.run {
|
11
|
+
|
12
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080) do |ws|
|
13
|
+
ws.onopen {
|
14
|
+
puts "WebSocket connection open"
|
15
|
+
|
16
|
+
# publish message to the client
|
17
|
+
ws.send "Hello Client"
|
18
|
+
}
|
19
|
+
|
20
|
+
ws.onclose { puts "Connection closed" }
|
21
|
+
ws.onmessage { |msg|
|
22
|
+
puts "Recieved message: #{msg}"
|
23
|
+
ws.send "Pong: #{msg}"
|
24
|
+
}
|
25
|
+
end
|
26
|
+
}
|
27
|
+
```
|
26
28
|
|
27
29
|
## Secure server
|
28
30
|
|
@@ -30,17 +32,19 @@ It is possible to accept secure wss:// connections by passing :secure => true wh
|
|
30
32
|
|
31
33
|
For example,
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
}
|
42
|
-
|
43
|
-
|
35
|
+
```ruby
|
36
|
+
EventMachine::WebSocket.start({
|
37
|
+
:host => "0.0.0.0",
|
38
|
+
:port => 443
|
39
|
+
:secure => true,
|
40
|
+
:tls_options => {
|
41
|
+
:private_key_file => "/private/key",
|
42
|
+
:cert_chain_file => "/ssl/certificate"
|
43
|
+
}
|
44
|
+
}) do |ws|
|
45
|
+
...
|
46
|
+
end
|
47
|
+
```
|
44
48
|
|
45
49
|
## Handling errors
|
46
50
|
|
@@ -48,11 +52,13 @@ There are two kinds of errors that need to be handled - errors caused by incompa
|
|
48
52
|
|
49
53
|
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
54
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
55
|
+
```ruby
|
56
|
+
ws.onerror { |error|
|
57
|
+
if e.kind_of?(EM::WebSocket::WebSocketError)
|
58
|
+
...
|
59
|
+
end
|
60
|
+
}
|
61
|
+
```
|
56
62
|
|
57
63
|
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
64
|
|
@@ -68,25 +74,4 @@ It is also possible to log all errors when developing by including the `:debug =
|
|
68
74
|
|
69
75
|
# License
|
70
76
|
|
71
|
-
|
72
|
-
|
73
|
-
Copyright (c) 2009 Ilya Grigorik
|
74
|
-
|
75
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
76
|
-
a copy of this software and associated documentation files (the
|
77
|
-
'Software'), to deal in the Software without restriction, including
|
78
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
79
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
80
|
-
permit persons to whom the Software is furnished to do so, subject to
|
81
|
-
the following conditions:
|
82
|
-
|
83
|
-
The above copyright notice and this permission notice shall be
|
84
|
-
included in all copies or substantial portions of the Software.
|
85
|
-
|
86
|
-
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
87
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
88
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
89
|
-
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
90
|
-
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
91
|
-
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
92
|
-
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
77
|
+
The MIT License - Copyright (c) 2009 Ilya Grigorik
|
@@ -16,6 +16,7 @@ module EventMachine
|
|
16
16
|
pointer = 0
|
17
17
|
|
18
18
|
@data.read_mask
|
19
|
+
pointer += 4
|
19
20
|
|
20
21
|
fin = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
|
21
22
|
# Ignoring rsv1-3 for now
|
@@ -67,7 +68,8 @@ module EventMachine
|
|
67
68
|
pointer += payload_length
|
68
69
|
|
69
70
|
# Throw away data up to pointer
|
70
|
-
@data.
|
71
|
+
@data.unset_mask
|
72
|
+
@data.slice!(0...pointer)
|
71
73
|
|
72
74
|
frame_type = opcode_to_type(opcode)
|
73
75
|
|
@@ -146,7 +148,7 @@ module EventMachine
|
|
146
148
|
end
|
147
149
|
|
148
150
|
def opcode_to_type(opcode)
|
149
|
-
FRAME_TYPES_INVERSE[opcode] || raise("Unknown opcode")
|
151
|
+
FRAME_TYPES_INVERSE[opcode] || raise(DataError, "Unknown opcode")
|
150
152
|
end
|
151
153
|
|
152
154
|
def data_frame?(type)
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module WebSocket
|
5
|
+
module Framing07
|
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 >= 2
|
16
|
+
pointer = 0
|
17
|
+
|
18
|
+
fin = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
|
19
|
+
# Ignoring rsv1-3 for now
|
20
|
+
opcode = @data.getbyte(pointer) & 0b00001111
|
21
|
+
pointer += 1
|
22
|
+
|
23
|
+
mask = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
|
24
|
+
length = @data.getbyte(pointer) & 0b01111111
|
25
|
+
pointer += 1
|
26
|
+
|
27
|
+
# raise WebSocketError, 'Data from client must be masked' unless mask
|
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
|
+
# Compute the expected frame length
|
59
|
+
frame_length = pointer + payload_length
|
60
|
+
frame_length += 4 if mask
|
61
|
+
|
62
|
+
# Check buffer size
|
63
|
+
if @data.getbyte(frame_length - 1) == nil
|
64
|
+
debug [:buffer_incomplete, @data]
|
65
|
+
error = true
|
66
|
+
next
|
67
|
+
end
|
68
|
+
|
69
|
+
# Remove frame header
|
70
|
+
@data.slice!(0...pointer)
|
71
|
+
pointer = 0
|
72
|
+
|
73
|
+
# Read application data (unmasked if required)
|
74
|
+
@data.read_mask if mask
|
75
|
+
pointer += 4 if mask
|
76
|
+
application_data = @data.getbytes(pointer, payload_length)
|
77
|
+
pointer += payload_length
|
78
|
+
@data.unset_mask if mask
|
79
|
+
|
80
|
+
# Throw away data up to pointer
|
81
|
+
@data.slice!(0...pointer)
|
82
|
+
|
83
|
+
frame_type = opcode_to_type(opcode)
|
84
|
+
|
85
|
+
if frame_type == :continuation && !@frame_type
|
86
|
+
raise WebSocketError, 'Continuation frame not expected'
|
87
|
+
end
|
88
|
+
|
89
|
+
if !fin
|
90
|
+
debug [:moreframe, frame_type, application_data]
|
91
|
+
@application_data_buffer << application_data
|
92
|
+
@frame_type = frame_type
|
93
|
+
else
|
94
|
+
# Message is complete
|
95
|
+
if frame_type == :continuation
|
96
|
+
@application_data_buffer << application_data
|
97
|
+
message(@frame_type, '', @application_data_buffer)
|
98
|
+
@application_data_buffer = ''
|
99
|
+
@frame_type = nil
|
100
|
+
else
|
101
|
+
message(frame_type, '', application_data)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end # end while
|
105
|
+
end
|
106
|
+
|
107
|
+
def send_frame(frame_type, application_data)
|
108
|
+
debug [:sending_frame, frame_type, application_data]
|
109
|
+
|
110
|
+
if @state == :closing && data_frame?(frame_type)
|
111
|
+
raise WebSocketError, "Cannot send data frame since connection is closing"
|
112
|
+
end
|
113
|
+
|
114
|
+
frame = ''
|
115
|
+
|
116
|
+
opcode = type_to_opcode(frame_type)
|
117
|
+
byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0
|
118
|
+
frame << byte1
|
119
|
+
|
120
|
+
length = application_data.size
|
121
|
+
if length <= 125
|
122
|
+
byte2 = length # since rsv4 is 0
|
123
|
+
frame << byte2
|
124
|
+
elsif length < 65536 # write 2 byte length
|
125
|
+
frame << 126
|
126
|
+
frame << [length].pack('n')
|
127
|
+
else # write 8 byte length
|
128
|
+
frame << 127
|
129
|
+
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
130
|
+
end
|
131
|
+
|
132
|
+
frame << application_data
|
133
|
+
|
134
|
+
@connection.send_data(frame)
|
135
|
+
end
|
136
|
+
|
137
|
+
def send_text_frame(data)
|
138
|
+
send_frame(:text, data)
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
FRAME_TYPES = {
|
144
|
+
:continuation => 0,
|
145
|
+
:text => 1,
|
146
|
+
:binary => 2,
|
147
|
+
:close => 8,
|
148
|
+
:ping => 9,
|
149
|
+
:pong => 10,
|
150
|
+
}
|
151
|
+
FRAME_TYPES_INVERSE = FRAME_TYPES.invert
|
152
|
+
# Frames are either data frames or control frames
|
153
|
+
DATA_FRAMES = [:text, :binary, :continuation]
|
154
|
+
|
155
|
+
def type_to_opcode(frame_type)
|
156
|
+
FRAME_TYPES[frame_type] || raise("Unknown frame type")
|
157
|
+
end
|
158
|
+
|
159
|
+
def opcode_to_type(opcode)
|
160
|
+
FRAME_TYPES_INVERSE[opcode] || raise(DataError, "Unknown opcode")
|
161
|
+
end
|
162
|
+
|
163
|
+
def data_frame?(type)
|
164
|
+
DATA_FRAMES.include?(type)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -87,6 +87,10 @@ module EventMachine
|
|
87
87
|
Handler05.new(connection, request, debug)
|
88
88
|
when 6
|
89
89
|
Handler06.new(connection, request, debug)
|
90
|
+
when 7
|
91
|
+
Handler07.new(connection, request, debug)
|
92
|
+
when 8
|
93
|
+
Handler08.new(connection, request, debug)
|
90
94
|
else
|
91
95
|
# According to spec should abort the connection
|
92
96
|
raise WebSocketError, "Protocol version #{version} not supported"
|
@@ -1,18 +1,31 @@
|
|
1
1
|
module EventMachine
|
2
2
|
module WebSocket
|
3
3
|
class MaskedString < String
|
4
|
+
# Read a 4 bit XOR mask - further requested bytes will be unmasked
|
4
5
|
def read_mask
|
6
|
+
if respond_to?(:encoding) && encoding.name != "ASCII-8BIT"
|
7
|
+
raise "MaskedString only operates on BINARY strings"
|
8
|
+
end
|
5
9
|
raise "Too short" if bytesize < 4 # TODO - change
|
6
10
|
@masking_key = String.new(self[0..3])
|
7
11
|
end
|
8
12
|
|
13
|
+
# Removes the mask, behaves like a normal string again
|
14
|
+
def unset_mask
|
15
|
+
@masking_key = nil
|
16
|
+
end
|
17
|
+
|
9
18
|
def slice_mask
|
10
19
|
slice!(0, 4)
|
11
20
|
end
|
12
21
|
|
13
22
|
def getbyte(index)
|
14
|
-
|
15
|
-
|
23
|
+
if @masking_key
|
24
|
+
masked_char = super
|
25
|
+
masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
16
29
|
end
|
17
30
|
|
18
31
|
def getbytes(start_index, count)
|
data/lib/em-websocket/version.rb
CHANGED
data/lib/em-websocket.rb
CHANGED
@@ -5,11 +5,11 @@ require "eventmachine"
|
|
5
5
|
%w[
|
6
6
|
debugger websocket connection
|
7
7
|
handshake75 handshake76 handshake04
|
8
|
-
framing76 framing03 framing04 framing05
|
8
|
+
framing76 framing03 framing04 framing05 framing07
|
9
9
|
close75 close03 close05 close06
|
10
10
|
masking04
|
11
11
|
message_processor_03 message_processor_06
|
12
|
-
handler_factory handler handler75 handler76 handler03 handler05 handler06
|
12
|
+
handler_factory handler handler75 handler76 handler03 handler05 handler06 handler07 handler08
|
13
13
|
].each do |file|
|
14
14
|
require "em-websocket/#{file}"
|
15
15
|
end
|
data/spec/unit/framing_spec.rb
CHANGED
@@ -161,3 +161,72 @@ describe EM::WebSocket::Framing04 do
|
|
161
161
|
end
|
162
162
|
end
|
163
163
|
end
|
164
|
+
|
165
|
+
describe EM::WebSocket::Framing07 do
|
166
|
+
class FramingContainer07
|
167
|
+
include EM::WebSocket::Framing07
|
168
|
+
|
169
|
+
def <<(data)
|
170
|
+
@data << data
|
171
|
+
process_data(data)
|
172
|
+
end
|
173
|
+
|
174
|
+
def debug(*args); end
|
175
|
+
end
|
176
|
+
|
177
|
+
before :each do
|
178
|
+
@f = FramingContainer07.new
|
179
|
+
@f.initialize_framing
|
180
|
+
end
|
181
|
+
|
182
|
+
# These examples are straight from the spec
|
183
|
+
# http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07#section-4.6
|
184
|
+
describe "examples from the spec" do
|
185
|
+
it "a single-frame unmakedtext message" do
|
186
|
+
@f.should_receive(:message).with(:text, '', 'Hello')
|
187
|
+
@f << "\x81\x05\x48\x65\x6c\x6c\x6f" # "\x84\x05Hello"
|
188
|
+
end
|
189
|
+
|
190
|
+
it "a single-frame masked text message" do
|
191
|
+
@f.should_receive(:message).with(:text, '', 'Hello')
|
192
|
+
@f << "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58" # "\x84\x05Hello"
|
193
|
+
end
|
194
|
+
|
195
|
+
it "a fragmented unmasked text message" do
|
196
|
+
@f.should_receive(:message).with(:text, '', 'Hello')
|
197
|
+
@f << "\x01\x03Hel"
|
198
|
+
@f << "\x80\x02lo"
|
199
|
+
end
|
200
|
+
|
201
|
+
it "Ping request" do
|
202
|
+
@f.should_receive(:message).with(:ping, '', 'Hello')
|
203
|
+
@f << "\x89\x05Hello"
|
204
|
+
end
|
205
|
+
|
206
|
+
it "a pong response" do
|
207
|
+
@f.should_receive(:message).with(:pong, '', 'Hello')
|
208
|
+
@f << "\x8a\x05Hello"
|
209
|
+
end
|
210
|
+
|
211
|
+
it "256 bytes binary message in a single unmasked frame" do
|
212
|
+
data = "a"*256
|
213
|
+
@f.should_receive(:message).with(:binary, '', data)
|
214
|
+
@f << "\x82\x7E\x01\x00" + data
|
215
|
+
end
|
216
|
+
|
217
|
+
it "64KiB binary message in a single unmasked frame" do
|
218
|
+
data = "a"*65536
|
219
|
+
@f.should_receive(:message).with(:binary, '', data)
|
220
|
+
@f << "\x82\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
describe "other tests" do
|
225
|
+
it "should raise a DataError if an invalid frame type is requested" do
|
226
|
+
lambda {
|
227
|
+
# Opcode 3 is not supported by this draft
|
228
|
+
@f << "\x83\x05Hello"
|
229
|
+
}.should raise_error(EventMachine::WebSocket::DataError, "Unknown opcode")
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
data/spec/unit/masking_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
|
1
3
|
require 'helper'
|
2
4
|
|
3
5
|
describe EM::WebSocket::MaskedString do
|
@@ -5,14 +7,21 @@ describe EM::WebSocket::MaskedString do
|
|
5
7
|
t = EM::WebSocket::MaskedString.new("\x00\x00\x00\x01\x00\x01\x00\x01")
|
6
8
|
t.read_mask
|
7
9
|
t.getbyte(3).should == 0x00
|
8
|
-
t.getbytes(
|
9
|
-
t.getbytes(
|
10
|
+
t.getbytes(4, 4).should == "\x00\x01\x00\x00"
|
11
|
+
t.getbytes(5, 3).should == "\x01\x00\x00"
|
10
12
|
end
|
11
13
|
|
12
14
|
it "should return nil from getbyte if index requested is out of range" do
|
13
15
|
t = EM::WebSocket::MaskedString.new("\x00\x00\x00\x00\x53")
|
14
16
|
t.read_mask
|
15
|
-
t.getbyte(
|
16
|
-
t.getbyte(
|
17
|
+
t.getbyte(4).should == 0x53
|
18
|
+
t.getbyte(5).should == nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should allow switching masking on and off" do
|
22
|
+
t = EM::WebSocket::MaskedString.new("\x02\x00\x00\x00\x03")
|
23
|
+
t.getbyte(4).should == 0x03
|
24
|
+
t.read_mask
|
25
|
+
t.getbyte(4).should == 0x01
|
17
26
|
end
|
18
27
|
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: em-websocket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.3.
|
5
|
+
version: 0.3.1
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Ilya Grigorik
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-
|
13
|
+
date: 2011-07-28 00:00:00 +01:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -101,11 +101,14 @@ files:
|
|
101
101
|
- lib/em-websocket/framing03.rb
|
102
102
|
- lib/em-websocket/framing04.rb
|
103
103
|
- lib/em-websocket/framing05.rb
|
104
|
+
- lib/em-websocket/framing07.rb
|
104
105
|
- lib/em-websocket/framing76.rb
|
105
106
|
- lib/em-websocket/handler.rb
|
106
107
|
- lib/em-websocket/handler03.rb
|
107
108
|
- lib/em-websocket/handler05.rb
|
108
109
|
- lib/em-websocket/handler06.rb
|
110
|
+
- lib/em-websocket/handler07.rb
|
111
|
+
- lib/em-websocket/handler08.rb
|
109
112
|
- lib/em-websocket/handler75.rb
|
110
113
|
- lib/em-websocket/handler76.rb
|
111
114
|
- lib/em-websocket/handler_factory.rb
|