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 CHANGED
@@ -1,8 +1,6 @@
1
1
  # RFC WebSocket Client (rfc-ws-client)
2
2
 
3
- A simple (more-or-less) RFC 6455 (WebSocket) compatible client without external dependencies.
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
@@ -15,7 +15,8 @@ ws.close
15
15
  break if data.nil?
16
16
  ws.send_message data, binary: binary
17
17
  end
18
- rescue
18
+ rescue => e
19
+ puts e
19
20
  end
20
21
  end
21
22
 
@@ -1,3 +1,3 @@
1
1
  module RfcWebSocket
2
- VERSION = "0.0.2"
2
+ VERSION = "1.0.0"
3
3
  end
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 = 0x01
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
- bytes = read(2).unpack("C*")
74
- fin = (bytes[0] & 0x80) != 0
75
- opcode = bytes[0] & 0x0f
76
- mask = (bytes[1] & 0x80) != 0
77
- length = bytes[1] & 0x7f
78
- if bytes[0] & 0b01110000 != 0
79
- raise "reserved bits must be 0"
80
- end
81
- if opcode > 7
82
- if !fin
83
- raise "control frame cannot be fragmented"
84
- elsif length > 125
85
- raise "Control frame is too large #{length}"
86
- elsif opcode > 0xA
87
- raise "Unexpected reserved opcode #{opcode}"
88
- elsif opcode == OPCODE_CLOSE && length == 1
89
- raise "Close control frame with payload of length 1"
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
- else
92
- if opcode != OPCODE_CONTINUATION && opcode != OPCODE_TEXT && opcode != OPCODE_BINARY
93
- raise "Unexpected reserved opcode #{opcode}"
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
- end
96
- if length == 126
97
- bytes = read(2)
98
- length = bytes.unpack("n")[0]
99
- elsif length == 127
100
- bytes = read(8)
101
- (high, low) = bytes.unpack("NN")
102
- length = high * (2 ** 32) + low
103
- end
104
- mask_key = mask ? read(4).unpack("C*") : nil
105
- payload = read(length)
106
- payload = apply_mask(payload, mask_key) if mask
107
- case opcode
108
- when OPCODE_TEXT
109
- return payload.force_encoding("UTF-8"), false
110
- when OPCODE_BINARY
111
- return payload, true
112
- when OPCODE_CLOSE
113
- code, explain = payload.unpack("nA*")
114
- if explain && !explain.force_encoding("UTF-8").valid_encoding?
115
- close(1007)
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
- close(response_close_code(code))
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(1002)
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
- frame = []
184
- frame << (opcode | 0x80)
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.2
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/echo.rb
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