websocket_parser 0.0.1 → 0.0.2
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/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
|