websocket_parser 0.0.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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +62 -0
- data/Rakefile +2 -0
- data/lib/websocket/client_handshake.rb +36 -0
- data/lib/websocket/message.rb +74 -0
- data/lib/websocket/parser.rb +239 -0
- data/lib/websocket/server_handshake.rb +9 -0
- data/lib/websocket/version.rb +3 -0
- data/lib/websocket_parser.rb +35 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/websocket/handshake_spec.rb +31 -0
- data/spec/websocket/message_spec.rb +20 -0
- data/spec/websocket/parser_spec.rb +112 -0
- data/websocket_parser.gemspec +22 -0
- metadata +119 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Alberto Fernandez-Capel
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# WebSocketParser
|
2
|
+
|
3
|
+
WebsocketParser is a RFC6455 compliant parser for websocket messages written in Ruby. It
|
4
|
+
is intended to write websockets servers in Ruby, but it only handles the parsing of the
|
5
|
+
WebSocket protocol, leaving I/O to the server.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'websocket_parser'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install websocket_parser
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
```
|
24
|
+
require 'websocket_parser'
|
25
|
+
|
26
|
+
socket = # Handle I/O with your server/event loop.
|
27
|
+
|
28
|
+
parser = WebSocket::Parser.new
|
29
|
+
|
30
|
+
parser.on_message do |m|
|
31
|
+
puts "Received message #{m}"
|
32
|
+
end
|
33
|
+
|
34
|
+
parser.on_error do |m|
|
35
|
+
puts "Received error #{m}"
|
36
|
+
socket.close!
|
37
|
+
end
|
38
|
+
|
39
|
+
parser.on_close do |m|
|
40
|
+
puts "Client closed connection. Reason: #{m}"
|
41
|
+
socket.close!
|
42
|
+
end
|
43
|
+
|
44
|
+
parser.on_ping do |m|
|
45
|
+
socket << WebSocket::Message.pong.to_data
|
46
|
+
end
|
47
|
+
|
48
|
+
parser << socket.read(4096)
|
49
|
+
|
50
|
+
# To send a message:
|
51
|
+
|
52
|
+
socket << WebSocket::Message.new('Hi there!').to_data
|
53
|
+
|
54
|
+
```
|
55
|
+
|
56
|
+
## Contributing
|
57
|
+
|
58
|
+
1. Fork it
|
59
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
60
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
61
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
62
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module WebSocket
|
4
|
+
class ClientHandshake < Http::Request
|
5
|
+
|
6
|
+
def initialize(method, uri, headers = {}, proxy = {}, body = nil, version = "1.1")
|
7
|
+
@method = method.to_s.downcase.to_sym
|
8
|
+
@uri = uri.is_a?(URI) ? uri : URI(uri.to_s)
|
9
|
+
|
10
|
+
@headers = headers
|
11
|
+
@proxy, @body, @version = proxy, body, version
|
12
|
+
end
|
13
|
+
|
14
|
+
def valid?
|
15
|
+
headers['Connection'] == 'Upgrade' &&
|
16
|
+
headers['Upgrade'] == 'websocket' &&
|
17
|
+
headers['Sec-WebSocket-Version'].to_i == PROTOCOL_VERSION
|
18
|
+
end
|
19
|
+
|
20
|
+
def accept_response
|
21
|
+
response_headers = {
|
22
|
+
'Upgrade' => 'websocket',
|
23
|
+
'Connection' => 'Upgrade',
|
24
|
+
'Sec-WebSocket-Accept' => accept_token
|
25
|
+
}
|
26
|
+
|
27
|
+
ServerHandshake.new(101, '1.1', response_headers)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def accept_token
|
33
|
+
Digest::SHA1.base64digest(headers['Sec-WebSocket-Key'].strip + GUID)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module WebSocket
|
2
|
+
class Message
|
3
|
+
attr_reader :type, :payload
|
4
|
+
|
5
|
+
# Return a new ping message
|
6
|
+
def self.ping(message = '')
|
7
|
+
new(message, :ping)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Return a new pong message
|
11
|
+
def self.pong(message = '')
|
12
|
+
new(message, :pong)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return a new close message
|
16
|
+
def self.close(reason = '')
|
17
|
+
new(reason, :close)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(message, type = :text)
|
21
|
+
@type, @payload = type, message.force_encoding("ASCII-8BIT")
|
22
|
+
end
|
23
|
+
|
24
|
+
def first_byte
|
25
|
+
@first_byte ||= if type == :continuation
|
26
|
+
OPCODE_VALUES[type]
|
27
|
+
else
|
28
|
+
0b10000000 | OPCODE_VALUES[type] # set FIN bit to true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def message_size
|
33
|
+
if payload_length < 126
|
34
|
+
:small
|
35
|
+
elsif payload_length < 65_536 # fits in 2 bytes
|
36
|
+
:medium
|
37
|
+
else
|
38
|
+
:large
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def second_byte
|
43
|
+
case message_size
|
44
|
+
when :small then payload_length
|
45
|
+
when :medium then 126
|
46
|
+
when :large then 127
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def payload_length
|
51
|
+
@payload.length
|
52
|
+
end
|
53
|
+
|
54
|
+
def extended_payload_length
|
55
|
+
message_size == :small ? nil : payload_length
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_a
|
59
|
+
[first_byte, second_byte, extended_payload_length, payload].compact
|
60
|
+
end
|
61
|
+
|
62
|
+
def pack_format
|
63
|
+
"#{FRAME_FORMAT[message_size]}#{payload_length}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_data
|
67
|
+
to_a.pack(pack_format)
|
68
|
+
end
|
69
|
+
|
70
|
+
def write(io)
|
71
|
+
io << to_data
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
module WebSocket
|
2
|
+
#
|
3
|
+
# This class parses WebSocket messages and frames.
|
4
|
+
#
|
5
|
+
# Each message is divied in frames as described in RFC 6455.
|
6
|
+
#
|
7
|
+
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
8
|
+
# +-+-+-+-+-------+-+-------------+-------------------------------+
|
9
|
+
# |F|R|R|R| opcode|M| Payload len | Extended payload length |
|
10
|
+
# |I|S|S|S| (4) |A| (7) | (16/64) |
|
11
|
+
# |N|V|V|V| |S| | (if payload len==126/127) |
|
12
|
+
# | |1|2|3| |K| | |
|
13
|
+
# +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
14
|
+
# | Extended payload length continued, if payload len == 127 |
|
15
|
+
# + - - - - - - - - - - - - - - - +-------------------------------+
|
16
|
+
# | |Masking-key, if MASK set to 1 |
|
17
|
+
# +-------------------------------+-------------------------------+
|
18
|
+
# | Masking-key (continued) | Payload Data |
|
19
|
+
# +-------------------------------- - - - - - - - - - - - - - - - +
|
20
|
+
# : Payload Data continued ... :
|
21
|
+
# + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|
22
|
+
# | Payload Data continued ... |
|
23
|
+
# +---------------------------------------------------------------+
|
24
|
+
#
|
25
|
+
# for more info on the frame format see: http://tools.ietf.org/html/rfc6455#section-5
|
26
|
+
#
|
27
|
+
class Parser
|
28
|
+
def initialize
|
29
|
+
@data = ''.force_encoding("ASCII-8BIT")
|
30
|
+
|
31
|
+
@on_message = Proc.new do |msg|
|
32
|
+
puts "Received message: #{msg}"
|
33
|
+
end
|
34
|
+
|
35
|
+
@on_error = Proc.new do |error|
|
36
|
+
puts "WebSocket error: #{error}"
|
37
|
+
end
|
38
|
+
|
39
|
+
@on_close = Proc.new do |reason|
|
40
|
+
puts "Should close connection. Reason: #{reason}"
|
41
|
+
end
|
42
|
+
|
43
|
+
@on_ping = Proc.new do |ping|
|
44
|
+
puts "Ping received"
|
45
|
+
end
|
46
|
+
|
47
|
+
@on_pong = Proc.new do |pong|
|
48
|
+
puts "Pong received"
|
49
|
+
end
|
50
|
+
|
51
|
+
@state = :header
|
52
|
+
end
|
53
|
+
|
54
|
+
def on_message(&callback)
|
55
|
+
@on_message = callback
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_error(&callback)
|
59
|
+
@on_error = callback
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_close(&callback)
|
63
|
+
@on_close = callback
|
64
|
+
end
|
65
|
+
|
66
|
+
def on_ping(&callback)
|
67
|
+
@on_ping = callback
|
68
|
+
end
|
69
|
+
|
70
|
+
def on_pong(&callback)
|
71
|
+
@on_pong = callback
|
72
|
+
end
|
73
|
+
|
74
|
+
def receive(data)
|
75
|
+
@data << data
|
76
|
+
|
77
|
+
read_header if @state == :header
|
78
|
+
read_payload_length if @state == :payload_length
|
79
|
+
read_payload if @state == :payload
|
80
|
+
|
81
|
+
process_frame if @state == :complete
|
82
|
+
end
|
83
|
+
|
84
|
+
alias_method :<<, :receive
|
85
|
+
|
86
|
+
def read_header
|
87
|
+
return unless @data.length >= 2 # Not enough data
|
88
|
+
|
89
|
+
@first_byte, @second_byte = @data.slice!(0,2).unpack('C2')
|
90
|
+
@state = :payload_length
|
91
|
+
end
|
92
|
+
|
93
|
+
def read_payload_length
|
94
|
+
@payload_length = if message_size == :small
|
95
|
+
payload_length_field
|
96
|
+
else
|
97
|
+
read_extended_payload_length
|
98
|
+
end
|
99
|
+
|
100
|
+
@state = :payload if @payload_length
|
101
|
+
end
|
102
|
+
|
103
|
+
def read_extended_payload_length
|
104
|
+
if message_size == :medium && @data.size >= 2
|
105
|
+
unpack_bytes(2,'S<')
|
106
|
+
elsif message_size == :large && @data.size >= 4
|
107
|
+
unpack_bytes(8,'Q<')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def read_payload
|
112
|
+
return unless @data.length >= @payload_length # Not enough data
|
113
|
+
|
114
|
+
payload_data = unpack_bytes(@payload_length, "a#{@payload_length}")
|
115
|
+
@payload = masked? ? unmask(payload_data) : payload_data
|
116
|
+
|
117
|
+
@state = :complete if @payload
|
118
|
+
end
|
119
|
+
|
120
|
+
def unpack_bytes(num, format)
|
121
|
+
@data.slice!(0,num).unpack(format).first
|
122
|
+
end
|
123
|
+
|
124
|
+
def completed_message?
|
125
|
+
fin? || payload
|
126
|
+
end
|
127
|
+
|
128
|
+
def control_frame?
|
129
|
+
[:close, :ping, :pong].include?(opcode)
|
130
|
+
end
|
131
|
+
|
132
|
+
def process_frame
|
133
|
+
if @current_message
|
134
|
+
@current_message << @payload
|
135
|
+
else
|
136
|
+
@current_message = @payload
|
137
|
+
end
|
138
|
+
|
139
|
+
if fin?
|
140
|
+
process_message
|
141
|
+
end
|
142
|
+
|
143
|
+
reset_frame!
|
144
|
+
end
|
145
|
+
|
146
|
+
def process_message
|
147
|
+
case opcode
|
148
|
+
when :text
|
149
|
+
@on_message.call(@current_message.force_encoding("UTF-8"))
|
150
|
+
when :binary
|
151
|
+
@on_message.call(@current_message)
|
152
|
+
when :close
|
153
|
+
@on_close.call(@current_message.encode("UTF-8"))
|
154
|
+
when :ping
|
155
|
+
@on_ping.call(@current_message.encode("UTF-8"))
|
156
|
+
when :pong
|
157
|
+
@on_pong.call(@current_message.encode("UTF-8"))
|
158
|
+
end
|
159
|
+
|
160
|
+
@current_message = nil
|
161
|
+
end
|
162
|
+
|
163
|
+
# Whether the FIN bit is set. The FIN bit indicates that
|
164
|
+
# this is the final fragment in a message
|
165
|
+
def fin?
|
166
|
+
@first_byte & 0b10000000 != 0
|
167
|
+
end
|
168
|
+
|
169
|
+
def svr1?
|
170
|
+
@first_byte & 0b01000000 != 0
|
171
|
+
end
|
172
|
+
|
173
|
+
def svr2?
|
174
|
+
@first_byte & 0b00100000 != 0
|
175
|
+
end
|
176
|
+
|
177
|
+
def svr3?
|
178
|
+
@first_byte & 0b00010000 != 0
|
179
|
+
end
|
180
|
+
|
181
|
+
def opcode
|
182
|
+
@opcode ||= OPCODES[@first_byte & 0b00001111]
|
183
|
+
end
|
184
|
+
|
185
|
+
def masked?
|
186
|
+
@second_byte & 0b10000000 != 0
|
187
|
+
end
|
188
|
+
|
189
|
+
def payload_length_field
|
190
|
+
@second_byte & 0b01111111
|
191
|
+
end
|
192
|
+
|
193
|
+
def mask_key
|
194
|
+
@mask_key ||= if masked?
|
195
|
+
@second_byte && read_uint32!
|
196
|
+
else
|
197
|
+
nil
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def message_size
|
202
|
+
if payload_length_field < 126
|
203
|
+
:small
|
204
|
+
elsif payload_length_field == 126
|
205
|
+
:medium
|
206
|
+
elsif payload_length_field == 127
|
207
|
+
:large
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def pack_format
|
212
|
+
"#{FRAME_FORMAT[message_size]}#{actual_payload_length}"
|
213
|
+
end
|
214
|
+
|
215
|
+
def mask(data)
|
216
|
+
masked_data = ''.encode!("ASCII-8BIT")
|
217
|
+
|
218
|
+
data.each_byte.each_with_index do |byte, i|
|
219
|
+
masked_data << byte ^ mask_key.chars[i%4]
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# The same algorithm applies regardless of the direction of the translation,
|
224
|
+
# e.g., the same steps are applied to mask the data as to unmask the data.
|
225
|
+
alias_method :unmask, :mask
|
226
|
+
|
227
|
+
def reset_frame!
|
228
|
+
@state = :header
|
229
|
+
|
230
|
+
@first_byte = nil
|
231
|
+
@second_byte = nil
|
232
|
+
|
233
|
+
@mask = nil
|
234
|
+
|
235
|
+
@payload_length = nil
|
236
|
+
@payload = nil
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "websocket/version"
|
2
|
+
require "websocket/client_handshake"
|
3
|
+
require "websocket/server_handshake"
|
4
|
+
require "websocket/message"
|
5
|
+
require "websocket/parser"
|
6
|
+
|
7
|
+
module WebSocket
|
8
|
+
PROTOCOL_VERSION = 13 # RFC 6455
|
9
|
+
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
10
|
+
|
11
|
+
# see http://tools.ietf.org/html/rfc6455#section-11.8
|
12
|
+
OPCODES = {
|
13
|
+
0 => :continuation,
|
14
|
+
1 => :text,
|
15
|
+
2 => :binary,
|
16
|
+
8 => :close,
|
17
|
+
9 => :ping,
|
18
|
+
10 => :pong
|
19
|
+
}
|
20
|
+
|
21
|
+
OPCODE_VALUES = {
|
22
|
+
:continuation => 0,
|
23
|
+
:text => 1,
|
24
|
+
:binary => 2,
|
25
|
+
:close => 8,
|
26
|
+
:ping => 9,
|
27
|
+
:pong => 10
|
28
|
+
}
|
29
|
+
|
30
|
+
FRAME_FORMAT = {
|
31
|
+
:small => 'CCa', # 2 bytes for header. N bytes for payload.
|
32
|
+
:medium => 'CCS<a', # 2 bytes for header. 2 bytes for extended length. N bytes for payload.
|
33
|
+
:large => 'CCQ<a' # 2 bytes for header. 4 bytes for extended length. N bytes for payload.
|
34
|
+
}
|
35
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WebSocket::ClientHandshake do
|
4
|
+
|
5
|
+
let :handshake_headers do
|
6
|
+
{
|
7
|
+
"Host" => "server.example.com",
|
8
|
+
"Upgrade" => "websocket",
|
9
|
+
"Connection" => "Upgrade",
|
10
|
+
"Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==",
|
11
|
+
"Origin" => "http://example.com",
|
12
|
+
"Sec-WebSocket-Protocol" => "chat, superchat",
|
13
|
+
"Sec-WebSocket-Version" => "13"
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:client_handshake) { WebSocket::ClientHandshake.new(:get, '/', handshake_headers) }
|
18
|
+
|
19
|
+
it "can validate handshake format" do
|
20
|
+
client_handshake.valid?.should be_true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "can generate an accept response for the client" do
|
24
|
+
response = client_handshake.accept_response
|
25
|
+
|
26
|
+
response.status.should == 101
|
27
|
+
response.headers['Upgrade'].should == 'websocket'
|
28
|
+
response.headers['Connection'].should == 'Upgrade'
|
29
|
+
response.headers['Sec-WebSocket-Accept'].should == 's3pPLMBiTxaQ9kYGzzhZRbK+xOo='
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WebSocket::Message do
|
4
|
+
|
5
|
+
it "knows its binary representation" do
|
6
|
+
text = 1500.times.collect { 'All work and no play makes Jack a dull boy.' }.join("\n\n")
|
7
|
+
message = WebSocket::Message.new(text)
|
8
|
+
data = message.to_data
|
9
|
+
|
10
|
+
# 2 bytes from header + 8 bytes from extended payload length + payload
|
11
|
+
data.size.should == 2 + 8 + text.length
|
12
|
+
|
13
|
+
first_byte, second_byte, ext_length, payload = data.unpack("CCQ<a#{text.length}")
|
14
|
+
|
15
|
+
first_byte.should == 0b10000001 # Text frame with FIN bit set
|
16
|
+
second_byte.should == 0b01111111 # Unmasked. Payload length 127.
|
17
|
+
ext_length.should == text.length
|
18
|
+
payload.should == text
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WebSocket::Parser do
|
4
|
+
|
5
|
+
let(:received_messages) { [] }
|
6
|
+
let(:received_errors) { [] }
|
7
|
+
let(:received_closes) { [] }
|
8
|
+
let(:received_pings) { [] }
|
9
|
+
let(:received_pongs) { [] }
|
10
|
+
|
11
|
+
let(:parser) do
|
12
|
+
parser = WebSocket::Parser.new
|
13
|
+
|
14
|
+
parser.on_message { |m| received_messages << m }
|
15
|
+
parser.on_error { |m| received_errors << m }
|
16
|
+
parser.on_close { |m| received_closes << m }
|
17
|
+
parser.on_ping { |m| received_pings << m }
|
18
|
+
parser.on_pong { |m| received_pongs << m }
|
19
|
+
|
20
|
+
parser
|
21
|
+
end
|
22
|
+
|
23
|
+
it "recognizes a text message" do
|
24
|
+
parser << WebSocket::Message.new('Once upon a time').to_data
|
25
|
+
|
26
|
+
received_messages.first.should == 'Once upon a time'
|
27
|
+
end
|
28
|
+
|
29
|
+
it "can receive a message in parts" do
|
30
|
+
data = WebSocket::Message.new('Once upon a time').to_data
|
31
|
+
parser << data.slice!(0, 5)
|
32
|
+
|
33
|
+
received_messages.should be_empty
|
34
|
+
|
35
|
+
parser << data
|
36
|
+
|
37
|
+
received_messages.first.should == 'Once upon a time'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can receive succesive messages" do
|
41
|
+
msg1 = WebSocket::Message.new('Now is the winter of our discontent')
|
42
|
+
msg2 = WebSocket::Message.new('Made glorious summer by this sun of York')
|
43
|
+
|
44
|
+
parser << msg1.to_data
|
45
|
+
parser << msg2.to_data
|
46
|
+
|
47
|
+
received_messages[0].should == 'Now is the winter of our discontent'
|
48
|
+
received_messages[1].should == 'Made glorious summer by this sun of York'
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can receive medium size messages" do
|
52
|
+
# Medium size messages has a payload length between 127 and 65_535 bytes
|
53
|
+
|
54
|
+
text = 4.times.collect { 'All work and no play makes Jack a dull boy.' }.join("\n\n")
|
55
|
+
text.length.should > 127
|
56
|
+
text.length.should < 65_536
|
57
|
+
|
58
|
+
parser << WebSocket::Message.new(text).to_data
|
59
|
+
received_messages.first.should == text
|
60
|
+
end
|
61
|
+
|
62
|
+
it "can receive large size messages" do
|
63
|
+
# Large size messages has a payload length greater than 65_535 bytes
|
64
|
+
|
65
|
+
text = 1500.times.collect { 'All work and no play makes Jack a dull boy.' }.join("\n\n")
|
66
|
+
text.length.should > 65_536
|
67
|
+
|
68
|
+
parser << WebSocket::Message.new(text).to_data
|
69
|
+
|
70
|
+
# Check lengths first to avoid gigantic error message
|
71
|
+
received_messages.first.length.should == text.length
|
72
|
+
received_messages.first.should == text
|
73
|
+
end
|
74
|
+
|
75
|
+
it "can receive multiframe messages" do
|
76
|
+
frame1 = WebSocket::Message.new("It is a truth universally acknowledged,", :continuation)
|
77
|
+
frame2 = WebSocket::Message.new("that a single man in possession of a good fortune,", :continuation)
|
78
|
+
frame3 = WebSocket::Message.new("must be in want of a wife.")
|
79
|
+
|
80
|
+
parser << frame1.to_data
|
81
|
+
|
82
|
+
received_messages.should be_empty
|
83
|
+
|
84
|
+
parser << frame2.to_data
|
85
|
+
|
86
|
+
received_messages.should be_empty
|
87
|
+
|
88
|
+
parser << frame3.to_data
|
89
|
+
|
90
|
+
received_messages.first.should == "It is a truth universally acknowledged," +
|
91
|
+
"that a single man in possession of a good fortune," +
|
92
|
+
"must be in want of a wife."
|
93
|
+
end
|
94
|
+
|
95
|
+
it "recognizes a ping message" do
|
96
|
+
parser << WebSocket::Message.ping('Oh, hai!').to_data
|
97
|
+
|
98
|
+
received_pings.first.should == 'Oh, hai!'
|
99
|
+
end
|
100
|
+
|
101
|
+
it "recognizes a pong message" do
|
102
|
+
parser << WebSocket::Message.pong('Hi there!').to_data
|
103
|
+
|
104
|
+
received_pongs.first.should == 'Hi there!'
|
105
|
+
end
|
106
|
+
|
107
|
+
it "recognizes a close message" do
|
108
|
+
parser << WebSocket::Message.close('Browser leaving page').to_data
|
109
|
+
|
110
|
+
received_closes.first.should == 'Browser leaving page'
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/websocket/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Alberto Fernandez-Capel"]
|
6
|
+
gem.email = ["afcapel@gmail.com"]
|
7
|
+
gem.description = %q{WebsocketParser is a RFC6455 compliant parser for websocket messages}
|
8
|
+
gem.summary = %q{Parse websockets messages in Ruby}
|
9
|
+
gem.homepage = "http://github.com/afcapel/websocket_parser"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "websocket_parser"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = WebSocket::VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency 'http'
|
19
|
+
|
20
|
+
gem.add_development_dependency 'rake'
|
21
|
+
gem.add_development_dependency 'rspec'
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: websocket_parser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alberto Fernandez-Capel
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: http
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: WebsocketParser is a RFC6455 compliant parser for websocket messages
|
63
|
+
email:
|
64
|
+
- afcapel@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE
|
72
|
+
- README.md
|
73
|
+
- Rakefile
|
74
|
+
- lib/websocket/client_handshake.rb
|
75
|
+
- lib/websocket/message.rb
|
76
|
+
- lib/websocket/parser.rb
|
77
|
+
- lib/websocket/server_handshake.rb
|
78
|
+
- lib/websocket/version.rb
|
79
|
+
- lib/websocket_parser.rb
|
80
|
+
- spec/spec_helper.rb
|
81
|
+
- spec/websocket/handshake_spec.rb
|
82
|
+
- spec/websocket/message_spec.rb
|
83
|
+
- spec/websocket/parser_spec.rb
|
84
|
+
- websocket_parser.gemspec
|
85
|
+
homepage: http://github.com/afcapel/websocket_parser
|
86
|
+
licenses: []
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
hash: 3979494630006513518
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ! '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
hash: 3979494630006513518
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 1.8.23
|
112
|
+
signing_key:
|
113
|
+
specification_version: 3
|
114
|
+
summary: Parse websockets messages in Ruby
|
115
|
+
test_files:
|
116
|
+
- spec/spec_helper.rb
|
117
|
+
- spec/websocket/handshake_spec.rb
|
118
|
+
- spec/websocket/message_spec.rb
|
119
|
+
- spec/websocket/parser_spec.rb
|