websocket_parser 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +13 -4
- data/lib/websocket/client_handshake.rb +5 -7
- data/lib/websocket/message.rb +65 -28
- data/lib/websocket/parser.rb +28 -46
- data/lib/websocket/server_handshake.rb +15 -1
- data/lib/websocket/version.rb +1 -1
- data/lib/websocket_parser.rb +51 -4
- data/spec/websocket/message_spec.rb +22 -0
- data/spec/websocket/parser_spec.rb +29 -10
- metadata +3 -3
data/README.md
CHANGED
@@ -20,7 +20,7 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
```
|
23
|
+
```ruby
|
24
24
|
require 'websocket_parser'
|
25
25
|
|
26
26
|
socket = # Handle I/O with your server/event loop.
|
@@ -36,12 +36,17 @@ parser.on_error do |m|
|
|
36
36
|
socket.close!
|
37
37
|
end
|
38
38
|
|
39
|
-
parser.on_close do |
|
40
|
-
|
39
|
+
parser.on_close do |status, message|
|
40
|
+
# According to the spec the server must respond with another
|
41
|
+
# close message before closing the connection
|
42
|
+
|
43
|
+
socket << WebSocket::Message.close.to_data
|
41
44
|
socket.close!
|
45
|
+
|
46
|
+
puts "Client closed connection. Status: #{status}. Reason: #{m}"
|
42
47
|
end
|
43
48
|
|
44
|
-
parser.on_ping do
|
49
|
+
parser.on_ping do
|
45
50
|
socket << WebSocket::Message.pong.to_data
|
46
51
|
end
|
47
52
|
|
@@ -53,6 +58,10 @@ socket << WebSocket::Message.new('Hi there!').to_data
|
|
53
58
|
|
54
59
|
```
|
55
60
|
|
61
|
+
## Status
|
62
|
+
|
63
|
+
Websocket Parser is still in early development phase.
|
64
|
+
|
56
65
|
## Contributing
|
57
66
|
|
58
67
|
1. Fork it
|
@@ -3,6 +3,10 @@ require 'digest/sha1'
|
|
3
3
|
module WebSocket
|
4
4
|
class ClientHandshake < Http::Request
|
5
5
|
|
6
|
+
def self.accept_token_for(websocket_key)
|
7
|
+
Digest::SHA1.base64digest(websocket_key.strip + GUID)
|
8
|
+
end
|
9
|
+
|
6
10
|
def initialize(method, uri, headers = {}, proxy = {}, body = nil, version = "1.1")
|
7
11
|
@method = method.to_s.downcase.to_sym
|
8
12
|
@uri = uri.is_a?(URI) ? uri : URI(uri.to_s)
|
@@ -21,16 +25,10 @@ module WebSocket
|
|
21
25
|
response_headers = {
|
22
26
|
'Upgrade' => 'websocket',
|
23
27
|
'Connection' => 'Upgrade',
|
24
|
-
'Sec-WebSocket-Accept' =>
|
28
|
+
'Sec-WebSocket-Accept' => ClientHandshake.accept_token_for(headers['Sec-WebSocket-Key'])
|
25
29
|
}
|
26
30
|
|
27
31
|
ServerHandshake.new(101, '1.1', response_headers)
|
28
32
|
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def accept_token
|
33
|
-
Digest::SHA1.base64digest(headers['Sec-WebSocket-Key'].strip + GUID)
|
34
|
-
end
|
35
33
|
end
|
36
34
|
end
|
data/lib/websocket/message.rb
CHANGED
@@ -1,34 +1,49 @@
|
|
1
1
|
module WebSocket
|
2
2
|
class Message
|
3
|
-
attr_reader :type, :payload
|
3
|
+
attr_reader :type, :mask_key, :status_code, :payload, :status_message
|
4
4
|
|
5
5
|
# Return a new ping message
|
6
|
-
def self.ping
|
7
|
-
new(
|
6
|
+
def self.ping
|
7
|
+
new('', :ping)
|
8
8
|
end
|
9
9
|
|
10
10
|
# Return a new pong message
|
11
|
-
def self.pong
|
12
|
-
new(
|
11
|
+
def self.pong
|
12
|
+
new('', :pong)
|
13
13
|
end
|
14
14
|
|
15
15
|
# Return a new close message
|
16
|
-
def self.close(reason =
|
17
|
-
|
18
|
-
|
16
|
+
def self.close(status_code = nil, reason = nil)
|
17
|
+
if status_code && STATUS_CODES[status_code] == nil
|
18
|
+
raise ArgumentError.new('Invalid status')
|
19
|
+
end
|
20
|
+
|
21
|
+
if reason && status_code == nil
|
22
|
+
raise ArgumentError.new("Can't set a status message without status code")
|
23
|
+
end
|
19
24
|
|
20
|
-
|
21
|
-
@type, @payload = type, message.force_encoding("ASCII-8BIT")
|
25
|
+
new(reason, :close, status_code)
|
22
26
|
end
|
23
27
|
|
24
|
-
def
|
25
|
-
@
|
26
|
-
|
28
|
+
def initialize(message = '', type = :text, status_code = nil)
|
29
|
+
@type = type
|
30
|
+
|
31
|
+
@payload = if status_code
|
32
|
+
@status_code = status_code
|
33
|
+
@status_message = message
|
34
|
+
|
35
|
+
[status_code, message].pack('S<a*')
|
27
36
|
else
|
28
|
-
|
37
|
+
message.force_encoding("ASCII-8BIT") if message
|
29
38
|
end
|
30
39
|
end
|
31
40
|
|
41
|
+
def mask!
|
42
|
+
@second_byte = second_byte | 0b10000000 # Set masked bit
|
43
|
+
@mask_key = Random.new.bytes(4)
|
44
|
+
@payload = WebSocket.mask(@payload, @mask_key)
|
45
|
+
end
|
46
|
+
|
32
47
|
def message_size
|
33
48
|
if payload_length < 126
|
34
49
|
:small
|
@@ -39,36 +54,58 @@ module WebSocket
|
|
39
54
|
end
|
40
55
|
end
|
41
56
|
|
42
|
-
def
|
43
|
-
|
44
|
-
when :small then payload_length
|
45
|
-
when :medium then 126
|
46
|
-
when :large then 127
|
47
|
-
end
|
57
|
+
def payload_length
|
58
|
+
@payload ? @payload.length : 0
|
48
59
|
end
|
49
60
|
|
50
|
-
def
|
51
|
-
|
61
|
+
def masked?
|
62
|
+
second_byte & 0b10000000 != 0
|
52
63
|
end
|
53
64
|
|
54
65
|
def extended_payload_length
|
55
66
|
message_size == :small ? nil : payload_length
|
56
67
|
end
|
57
68
|
|
69
|
+
def to_data
|
70
|
+
to_a.pack(pack_format)
|
71
|
+
end
|
72
|
+
|
73
|
+
def write(io)
|
74
|
+
io << to_data
|
75
|
+
end
|
76
|
+
|
77
|
+
def control_frame?
|
78
|
+
[:close, :ping, :pong].include?(type)
|
79
|
+
end
|
80
|
+
|
81
|
+
def status
|
82
|
+
STATUS_CODES[status_code]
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
58
87
|
def to_a
|
59
|
-
[first_byte, second_byte, extended_payload_length, payload].compact
|
88
|
+
[first_byte, second_byte, extended_payload_length, mask_key, payload].compact
|
60
89
|
end
|
61
90
|
|
62
91
|
def pack_format
|
63
|
-
|
92
|
+
WebSocket.frame_format(payload_length, masked?)
|
64
93
|
end
|
65
94
|
|
66
|
-
def
|
67
|
-
|
95
|
+
def first_byte
|
96
|
+
@first_byte ||= if type == :continuation
|
97
|
+
OPCODE_VALUES[type]
|
98
|
+
else
|
99
|
+
0b10000000 | OPCODE_VALUES[type] # set FIN bit to true
|
100
|
+
end
|
68
101
|
end
|
69
102
|
|
70
|
-
def
|
71
|
-
|
103
|
+
def second_byte
|
104
|
+
@second_byte ||= case message_size
|
105
|
+
when :small then payload_length
|
106
|
+
when :medium then 126
|
107
|
+
when :large then 127
|
108
|
+
end
|
72
109
|
end
|
73
110
|
end
|
74
111
|
end
|
data/lib/websocket/parser.rb
CHANGED
@@ -36,15 +36,15 @@ module WebSocket
|
|
36
36
|
puts "WebSocket error: #{error}"
|
37
37
|
end
|
38
38
|
|
39
|
-
@on_close = Proc.new do |
|
40
|
-
puts "Should close connection.
|
39
|
+
@on_close = Proc.new do |status, message|
|
40
|
+
puts "Should close connection. Status: #{status} Message: #{message}"
|
41
41
|
end
|
42
42
|
|
43
|
-
@on_ping = Proc.new do
|
43
|
+
@on_ping = Proc.new do
|
44
44
|
puts "Ping received"
|
45
45
|
end
|
46
46
|
|
47
|
-
@on_pong = Proc.new do
|
47
|
+
@on_pong = Proc.new do
|
48
48
|
puts "Pong received"
|
49
49
|
end
|
50
50
|
|
@@ -76,6 +76,7 @@ module WebSocket
|
|
76
76
|
|
77
77
|
read_header if @state == :header
|
78
78
|
read_payload_length if @state == :payload_length
|
79
|
+
read_mask_key if @state == :mask
|
79
80
|
read_payload if @state == :payload
|
80
81
|
|
81
82
|
process_frame if @state == :complete
|
@@ -97,7 +98,9 @@ module WebSocket
|
|
97
98
|
read_extended_payload_length
|
98
99
|
end
|
99
100
|
|
100
|
-
|
101
|
+
return unless @payload_length
|
102
|
+
|
103
|
+
@state = masked? ? :mask : :payload
|
101
104
|
end
|
102
105
|
|
103
106
|
def read_extended_payload_length
|
@@ -108,11 +111,23 @@ module WebSocket
|
|
108
111
|
end
|
109
112
|
end
|
110
113
|
|
114
|
+
def read_mask_key
|
115
|
+
return unless @data.size >= 4
|
116
|
+
|
117
|
+
@mask_key = unpack_bytes(4,'a4')
|
118
|
+
@state = :payload
|
119
|
+
end
|
120
|
+
|
111
121
|
def read_payload
|
112
122
|
return unless @data.length >= @payload_length # Not enough data
|
113
123
|
|
114
124
|
payload_data = unpack_bytes(@payload_length, "a#{@payload_length}")
|
115
|
-
|
125
|
+
|
126
|
+
@payload = if masked?
|
127
|
+
WebSocket.unmask(payload_data, @mask_key)
|
128
|
+
else
|
129
|
+
payload_data
|
130
|
+
end
|
116
131
|
|
117
132
|
@state = :complete if @payload
|
118
133
|
end
|
@@ -121,10 +136,6 @@ module WebSocket
|
|
121
136
|
@data.slice!(0,num).unpack(format).first
|
122
137
|
end
|
123
138
|
|
124
|
-
def completed_message?
|
125
|
-
fin? || payload
|
126
|
-
end
|
127
|
-
|
128
139
|
def control_frame?
|
129
140
|
[:close, :ping, :pong].include?(opcode)
|
130
141
|
end
|
@@ -150,11 +161,14 @@ module WebSocket
|
|
150
161
|
when :binary
|
151
162
|
@on_message.call(@current_message)
|
152
163
|
when :close
|
153
|
-
@
|
164
|
+
status_code, message = @current_message.unpack('S<a*')
|
165
|
+
status = STATUS_CODES[status_code]
|
166
|
+
|
167
|
+
@on_close.call(status, message)
|
154
168
|
when :ping
|
155
|
-
@on_ping.call
|
169
|
+
@on_ping.call
|
156
170
|
when :pong
|
157
|
-
@on_pong.call
|
171
|
+
@on_pong.call
|
158
172
|
end
|
159
173
|
|
160
174
|
@current_message = nil
|
@@ -166,18 +180,6 @@ module WebSocket
|
|
166
180
|
@first_byte & 0b10000000 != 0
|
167
181
|
end
|
168
182
|
|
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
183
|
def opcode
|
182
184
|
@opcode ||= OPCODES[@first_byte & 0b00001111]
|
183
185
|
end
|
@@ -190,14 +192,6 @@ module WebSocket
|
|
190
192
|
@second_byte & 0b01111111
|
191
193
|
end
|
192
194
|
|
193
|
-
def mask_key
|
194
|
-
@mask_key ||= if masked?
|
195
|
-
@second_byte && read_uint32!
|
196
|
-
else
|
197
|
-
nil
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
195
|
def message_size
|
202
196
|
if payload_length_field < 126
|
203
197
|
:small
|
@@ -209,21 +203,9 @@ module WebSocket
|
|
209
203
|
end
|
210
204
|
|
211
205
|
def pack_format
|
212
|
-
|
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
|
206
|
+
WebSocket.frame_format(actual_payload_length, masked?)
|
221
207
|
end
|
222
208
|
|
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
209
|
def reset_frame!
|
228
210
|
@state = :header
|
229
211
|
|
@@ -1,9 +1,23 @@
|
|
1
1
|
module WebSocket
|
2
2
|
class ServerHandshake < Http::Response
|
3
3
|
|
4
|
-
def initialize(status =
|
4
|
+
def initialize(status = 101, version = "1.1", headers = {}, body = nil, &body_proc)
|
5
5
|
@status, @version, @body, @body_proc = status, version, body, body_proc
|
6
6
|
@headers = headers
|
7
7
|
end
|
8
|
+
|
9
|
+
def render(out)
|
10
|
+
response_header = "#{@version} #{@status} #{@reason}#{CRLF}"
|
11
|
+
|
12
|
+
unless @headers.empty?
|
13
|
+
response_header << @headers.map do |header, value|
|
14
|
+
"#{header}: #{value}"
|
15
|
+
end.join(CRLF) << CRLF
|
16
|
+
end
|
17
|
+
|
18
|
+
response_header << CRLF
|
19
|
+
|
20
|
+
out << response_header
|
21
|
+
end
|
8
22
|
end
|
9
23
|
end
|
data/lib/websocket/version.rb
CHANGED
data/lib/websocket_parser.rb
CHANGED
@@ -5,6 +5,8 @@ require "websocket/message"
|
|
5
5
|
require "websocket/parser"
|
6
6
|
|
7
7
|
module WebSocket
|
8
|
+
extend self
|
9
|
+
|
8
10
|
PROTOCOL_VERSION = 13 # RFC 6455
|
9
11
|
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
10
12
|
|
@@ -27,9 +29,54 @@ module WebSocket
|
|
27
29
|
:pong => 10
|
28
30
|
}
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
32
|
+
# See: http://tools.ietf.org/html/rfc6455#section-7.4.1
|
33
|
+
STATUS_CODES = {
|
34
|
+
1000 => :normal_closure,
|
35
|
+
1001 => :peer_going_away,
|
36
|
+
1002 => :protocol_error,
|
37
|
+
1003 => :data_error,
|
38
|
+
1007 => :data_not_consistent,
|
39
|
+
1008 => :policy_violation,
|
40
|
+
1009 => :message_too_big,
|
41
|
+
1010 => :extension_required,
|
42
|
+
1011 => :unexpected_condition
|
34
43
|
}
|
44
|
+
|
45
|
+
# Determines how to unpack the frame depending on
|
46
|
+
# the payload length and wether the frame is masked
|
47
|
+
def frame_format(payload_length, masked = false)
|
48
|
+
format = 'CC'
|
49
|
+
|
50
|
+
if payload_length > 65_535
|
51
|
+
format += 'Q<'
|
52
|
+
elsif payload_length > 125
|
53
|
+
format += 'S<'
|
54
|
+
end
|
55
|
+
|
56
|
+
if masked
|
57
|
+
format += 'a4'
|
58
|
+
end
|
59
|
+
|
60
|
+
if payload_length > 0
|
61
|
+
format += "a#{payload_length}"
|
62
|
+
end
|
63
|
+
|
64
|
+
format
|
65
|
+
end
|
66
|
+
|
67
|
+
def mask(data, mask_key)
|
68
|
+
masked_data = ''.encode!("ASCII-8BIT")
|
69
|
+
mask_bytes = mask_key.bytes.to_a
|
70
|
+
|
71
|
+
data.bytes.each_with_index do |byte, i|
|
72
|
+
masked_data << (byte ^ mask_bytes[i%4])
|
73
|
+
end
|
74
|
+
|
75
|
+
masked_data
|
76
|
+
end
|
77
|
+
|
78
|
+
# The same algorithm applies regardless of the direction of the translation,
|
79
|
+
# e.g., the same steps are applied to mask the data as to unmask the data.
|
80
|
+
alias_method :unmask, :mask
|
81
|
+
|
35
82
|
end
|
@@ -17,4 +17,26 @@ describe WebSocket::Message do
|
|
17
17
|
ext_length.should == text.length
|
18
18
|
payload.should == text
|
19
19
|
end
|
20
|
+
|
21
|
+
it "can be masked" do
|
22
|
+
message = WebSocket::Message.new('The man with the Iron Mask')
|
23
|
+
message.masked?.should be_false
|
24
|
+
|
25
|
+
message.mask!
|
26
|
+
|
27
|
+
message.masked?.should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
it "allows status codes for control frames" do
|
31
|
+
msg = WebSocket::Message.close(1001, 'Bye')
|
32
|
+
|
33
|
+
msg.status_code.should == 1001
|
34
|
+
msg.payload.should == [1001, 'Bye'].pack('S<a*')
|
35
|
+
msg.status.should == :peer_going_away
|
36
|
+
msg.status_message.should == 'Bye'
|
37
|
+
end
|
38
|
+
|
39
|
+
it "does not allow a status message without status code" do
|
40
|
+
expect{ WebSocket::Message.close(nil, 'Bye') }.to raise_error(ArgumentError)
|
41
|
+
end
|
20
42
|
end
|
@@ -13,9 +13,9 @@ describe WebSocket::Parser do
|
|
13
13
|
|
14
14
|
parser.on_message { |m| received_messages << m }
|
15
15
|
parser.on_error { |m| received_errors << m }
|
16
|
-
parser.on_close { |
|
17
|
-
parser.on_ping {
|
18
|
-
parser.on_pong {
|
16
|
+
parser.on_close { |status, message| received_closes << [status, message] }
|
17
|
+
parser.on_ping { received_pings << 'ping' }
|
18
|
+
parser.on_pong { received_pongs << 'pong' }
|
19
19
|
|
20
20
|
parser
|
21
21
|
end
|
@@ -93,20 +93,39 @@ describe WebSocket::Parser do
|
|
93
93
|
end
|
94
94
|
|
95
95
|
it "recognizes a ping message" do
|
96
|
-
parser << WebSocket::Message.ping
|
96
|
+
parser << WebSocket::Message.ping.to_data
|
97
97
|
|
98
|
-
received_pings.
|
98
|
+
received_pings.size.should == 1
|
99
99
|
end
|
100
100
|
|
101
101
|
it "recognizes a pong message" do
|
102
|
-
parser << WebSocket::Message.pong
|
102
|
+
parser << WebSocket::Message.pong.to_data
|
103
103
|
|
104
|
-
received_pongs.
|
104
|
+
received_pongs.size.should == 1
|
105
105
|
end
|
106
106
|
|
107
|
-
it "recognizes a close message" do
|
108
|
-
parser << WebSocket::Message.close('Browser leaving page').to_data
|
107
|
+
it "recognizes a close message with status code and message" do
|
108
|
+
parser << WebSocket::Message.close(1001, 'Browser leaving page').to_data
|
109
109
|
|
110
|
-
received_closes.first
|
110
|
+
status, message = received_closes.first
|
111
|
+
status.should == :peer_going_away # Status code 1001
|
112
|
+
message.should == 'Browser leaving page'
|
113
|
+
end
|
114
|
+
|
115
|
+
it "recognizes a close message without status code" do
|
116
|
+
parser << WebSocket::Message.close.to_data
|
117
|
+
|
118
|
+
status, message = received_closes.first
|
119
|
+
status.should be_nil
|
120
|
+
message.should be_empty
|
121
|
+
end
|
122
|
+
|
123
|
+
it "recognizes a masked frame" do
|
124
|
+
msg = WebSocket::Message.new('Once upon a time')
|
125
|
+
msg.mask!
|
126
|
+
|
127
|
+
parser << msg.to_data
|
128
|
+
|
129
|
+
received_messages.first.should == 'Once upon a time'
|
111
130
|
end
|
112
131
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: websocket_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -96,7 +96,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
96
|
version: '0'
|
97
97
|
segments:
|
98
98
|
- 0
|
99
|
-
hash:
|
99
|
+
hash: -3359756691541156568
|
100
100
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
101
|
none: false
|
102
102
|
requirements:
|
@@ -105,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
105
|
version: '0'
|
106
106
|
segments:
|
107
107
|
- 0
|
108
|
-
hash:
|
108
|
+
hash: -3359756691541156568
|
109
109
|
requirements: []
|
110
110
|
rubyforge_project:
|
111
111
|
rubygems_version: 1.8.23
|