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 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