rfc-ws-client 0.0.2 → 1.0.0
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 +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
|