rfc-ws-client 0.0.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +11 -5
- data/examples/{echo.rb → autobahn.rb} +2 -1
- data/lib/rfc-ws-client/version.rb +1 -1
- data/lib/rfc-ws-client.rb +105 -76
- metadata +2 -2
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# RFC WebSocket Client (rfc-ws-client)
|
2
2
|
|
3
|
-
A simple
|
4
|
-
|
5
|
-
Currently doesn't support fragmentation.
|
3
|
+
A simple RFC 6455 compatible client without external dependencies.
|
6
4
|
|
7
5
|
Includes source code from [em-ws-client](https://github.com/dansimpson/em-ws-client) and [web-socket-ruby](https://github.com/gimite/web-socket-ruby).
|
8
6
|
|
@@ -18,11 +16,19 @@ gem 'rfc-ws-client'
|
|
18
16
|
|
19
17
|
```ruby
|
20
18
|
ws = RfcWebSocket::WebSocket.new("wss://echo.websocket.org")
|
21
|
-
ws.send_message("test")
|
22
|
-
ws.receive # => "test"
|
19
|
+
ws.send_message("test", binary: false)
|
20
|
+
msg, binary = ws.receive # => "test", false
|
23
21
|
ws.close
|
24
22
|
```
|
25
23
|
|
24
|
+
## Testing
|
25
|
+
|
26
|
+
```bash
|
27
|
+
wstest -m fuzzingserver
|
28
|
+
# in different console
|
29
|
+
ruby examples/autobahn.rb
|
30
|
+
```
|
31
|
+
|
26
32
|
## Contributing
|
27
33
|
|
28
34
|
1. Fork it
|
data/lib/rfc-ws-client.rb
CHANGED
@@ -9,9 +9,18 @@ require 'rainbow'
|
|
9
9
|
require 'base64'
|
10
10
|
|
11
11
|
module RfcWebSocket
|
12
|
+
class WebSocketError < RuntimeError
|
13
|
+
attr_reader :code
|
14
|
+
|
15
|
+
def initialize(text, code = 1002)
|
16
|
+
super(text)
|
17
|
+
@code = code
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
12
21
|
class WebSocket
|
13
22
|
WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
14
|
-
OPCODE_CONTINUATION =
|
23
|
+
OPCODE_CONTINUATION = 0x00
|
15
24
|
OPCODE_TEXT = 0x01
|
16
25
|
OPCODE_BINARY = 0x02
|
17
26
|
OPCODE_CLOSE = 0x08
|
@@ -28,7 +37,7 @@ module RfcWebSocket
|
|
28
37
|
elsif uri.scheme = "wss"
|
29
38
|
default_port = 443
|
30
39
|
else
|
31
|
-
raise "unsupported scheme: #{uri.scheme}"
|
40
|
+
raise WebSocketError.new("unsupported scheme: #{uri.scheme}")
|
32
41
|
end
|
33
42
|
host = uri.host + ((!uri.port || uri.port == default_port) ? "" : ":#{uri.port}")
|
34
43
|
path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
|
@@ -45,23 +54,23 @@ module RfcWebSocket
|
|
45
54
|
flush()
|
46
55
|
|
47
56
|
status_line = gets.chomp
|
48
|
-
raise "bad response: #{status_line}" unless status_line.start_with?("HTTP/1.1 101")
|
57
|
+
raise WebSocketError.new("bad response: #{status_line}") unless status_line.start_with?("HTTP/1.1 101")
|
49
58
|
|
50
59
|
header = {}
|
51
60
|
while line = gets
|
52
61
|
line.chomp!
|
53
62
|
break if line.empty?
|
54
63
|
if !(line =~ /\A(\S+): (.*)\z/n)
|
55
|
-
raise "invalid response: #{line}"
|
64
|
+
raise WebSocketError.new("invalid response: #{line}")
|
56
65
|
end
|
57
66
|
header[$1.downcase] = $2
|
58
67
|
end
|
59
|
-
raise "upgrade missing" unless header["upgrade"]
|
60
|
-
raise "connection missing" unless header["connection"]
|
68
|
+
raise WebSocketError.new("upgrade missing") unless header["upgrade"]
|
69
|
+
raise WebSocketError.new("connection missing") unless header["connection"]
|
61
70
|
accept = header["sec-websocket-accept"]
|
62
|
-
raise "sec-websocket-accept missing" unless accept
|
71
|
+
raise WebSocketError.new("sec-websocket-accept missing") unless accept
|
63
72
|
expected_accept = Digest::SHA1.base64digest(request_key + WEB_SOCKET_GUID)
|
64
|
-
raise "sec-websocket-accept is invalid, actual: #{accept}, expected: #{expected_accept}" unless accept == expected_accept
|
73
|
+
raise WebSocketError.new("sec-websocket-accept is invalid, actual: #{accept}, expected: #{expected_accept}") unless accept == expected_accept
|
65
74
|
end
|
66
75
|
|
67
76
|
def send_message(message, opts = {binary: false})
|
@@ -70,66 +79,91 @@ module RfcWebSocket
|
|
70
79
|
|
71
80
|
def receive
|
72
81
|
begin
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
raise "
|
82
|
+
buffer = ""
|
83
|
+
fragmented = nil
|
84
|
+
# Loop until something returns
|
85
|
+
while true
|
86
|
+
b1, b2 = read(2).unpack("CC")
|
87
|
+
puts "b1: #{b1.to_s(2).rjust(8, "0")}, b2: #{b2.to_s(2).rjust(8, "0")}" if DEBUG
|
88
|
+
# first byte
|
89
|
+
fin = (b1 & 0x80) != 0
|
90
|
+
raise WebSocketError.new("reserved bits must be 0") if (b1 & 0b01110000) != 0
|
91
|
+
opcode = b1 & 0x0f
|
92
|
+
# second byte
|
93
|
+
mask = (b2 & 0x80) != 0
|
94
|
+
# we're a client
|
95
|
+
raise WebSocketError.new("server->client must not be masked!") if mask
|
96
|
+
length = b2 & 0x7f
|
97
|
+
if opcode > 7
|
98
|
+
raise WebSocketError.new("control frame cannot be fragmented") unless fin
|
99
|
+
raise WebSocketError.new("control frame is too large: #{length}") if length > 125
|
100
|
+
raise WebSocketError.new("unexpected reserved opcode: #{opcode}") if opcode > 0xA
|
101
|
+
raise WebSocketError.new("close frame with payload length 1") if length == 1 and opcode == OPCODE_CLOSE
|
102
|
+
elsif opcode != OPCODE_CONTINUATION && opcode != OPCODE_TEXT && opcode != OPCODE_BINARY
|
103
|
+
raise WebSocketError.new("unexpected reserved opcode: #{opcode}")
|
90
104
|
end
|
91
|
-
|
92
|
-
if
|
93
|
-
|
105
|
+
# extended payload length
|
106
|
+
if length == 126
|
107
|
+
length = read(2).unpack("n")[0]
|
108
|
+
elsif length == 127
|
109
|
+
high, low = *read(8).unpack("NN")
|
110
|
+
length = high * (2 ** 32) + low
|
94
111
|
end
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
112
|
+
# payload
|
113
|
+
payload = read(length)
|
114
|
+
case opcode
|
115
|
+
when OPCODE_CONTINUATION
|
116
|
+
raise WebSocketError.new("no frame to continue") unless fragmented
|
117
|
+
if fragmented == :binary
|
118
|
+
buffer << payload
|
119
|
+
else
|
120
|
+
buffer << payload.force_encoding("UTF-8")
|
121
|
+
end
|
122
|
+
if fin
|
123
|
+
raise WebSocketError.new("invalid utf8", 1007) if fragmented == :text and !valid_utf8?(buffer)
|
124
|
+
return buffer, fragmented == :binary
|
125
|
+
else
|
126
|
+
next
|
127
|
+
end
|
128
|
+
when OPCODE_TEXT
|
129
|
+
raise WebSocketError.new("unexpected opcode in continuation mode") if fragmented
|
130
|
+
if !fin
|
131
|
+
fragmented = :text
|
132
|
+
buffer << payload.force_encoding("UTF-8")
|
133
|
+
next
|
134
|
+
else
|
135
|
+
raise WebSocketError.new("invalid utf8", 1007) unless valid_utf8?(payload)
|
136
|
+
return payload, false
|
137
|
+
end
|
138
|
+
when OPCODE_BINARY
|
139
|
+
raise WebSocketError.new("unexpected opcode in continuation mode") if fragmented
|
140
|
+
if !fin
|
141
|
+
fragmented = :binary
|
142
|
+
buffer << payload
|
143
|
+
else
|
144
|
+
return payload, true
|
145
|
+
end
|
146
|
+
when OPCODE_CLOSE
|
147
|
+
code, explain = payload.unpack("nA*")
|
148
|
+
if explain && !valid_utf8?(explain)
|
149
|
+
close(1007)
|
150
|
+
else
|
151
|
+
close(response_close_code(code))
|
152
|
+
end
|
153
|
+
return nil, nil
|
154
|
+
when OPCODE_PING
|
155
|
+
write(encode(payload, OPCODE_PONG))
|
156
|
+
next
|
157
|
+
when OPCODE_PONG
|
158
|
+
next
|
116
159
|
else
|
117
|
-
|
160
|
+
raise WebSocketError.new("received unknown opcode: #{opcode}")
|
118
161
|
end
|
119
|
-
return nil, nil
|
120
|
-
when OPCODE_PING
|
121
|
-
write(encode(payload, OPCODE_PONG))
|
122
|
-
#TODO fix recursion
|
123
|
-
return receive
|
124
|
-
when OPCODE_PONG
|
125
|
-
return receive
|
126
|
-
else
|
127
|
-
raise "received unknown opcode: #{opcode}"
|
128
162
|
end
|
129
163
|
rescue EOFError
|
130
164
|
return nil, nil
|
131
|
-
rescue => e
|
132
|
-
close(
|
165
|
+
rescue WebSocketError => e
|
166
|
+
close(e.code)
|
133
167
|
raise e
|
134
168
|
end
|
135
169
|
end
|
@@ -180,18 +214,9 @@ module RfcWebSocket
|
|
180
214
|
end
|
181
215
|
|
182
216
|
def encode(data, opcode)
|
183
|
-
|
184
|
-
frame
|
185
|
-
|
217
|
+
raise WebSocketError.new("invalud utf8") if opcode == OPCODE_TEXT and !valid_utf8?(data)
|
218
|
+
frame = [opcode | 0x80]
|
186
219
|
packr = "CC"
|
187
|
-
|
188
|
-
if opcode == OPCODE_TEXT
|
189
|
-
data.force_encoding("UTF-8")
|
190
|
-
if !data.valid_encoding?
|
191
|
-
raise "Invalid UTF!"
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
220
|
# append frame length and mask bit 0x80
|
196
221
|
len = data ? data.bytesize : 0
|
197
222
|
if len <= 125
|
@@ -205,19 +230,15 @@ module RfcWebSocket
|
|
205
230
|
frame << len
|
206
231
|
packr << "L!>"
|
207
232
|
end
|
208
|
-
|
209
233
|
# generate a masking key
|
210
234
|
key = rand(2 ** 31)
|
211
|
-
|
212
235
|
# mask each byte with the key
|
213
236
|
frame << key
|
214
237
|
packr << "N"
|
215
|
-
|
216
238
|
# Apply the masking key to every byte
|
217
239
|
len.times do |i|
|
218
240
|
frame << ((data.getbyte(i) ^ (key >> ((3 - (i % 4)) * 8))) & 0xFF)
|
219
241
|
end
|
220
|
-
|
221
242
|
frame.pack("#{packr}C*")
|
222
243
|
end
|
223
244
|
|
@@ -235,5 +256,13 @@ module RfcWebSocket
|
|
235
256
|
1002
|
236
257
|
end
|
237
258
|
end
|
259
|
+
|
260
|
+
def force_utf8(str)
|
261
|
+
str.force_encoding("UTF-8")
|
262
|
+
end
|
263
|
+
|
264
|
+
def valid_utf8?(str)
|
265
|
+
force_utf8(str).valid_encoding?
|
266
|
+
end
|
238
267
|
end
|
239
268
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rfc-ws-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -23,7 +23,7 @@ files:
|
|
23
23
|
- LICENSE.txt
|
24
24
|
- README.md
|
25
25
|
- Rakefile
|
26
|
-
- examples/
|
26
|
+
- examples/autobahn.rb
|
27
27
|
- lib/rfc-ws-client.rb
|
28
28
|
- lib/rfc-ws-client/version.rb
|
29
29
|
- rfc-ws-client.gemspec
|