webtube 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/wsc ADDED
@@ -0,0 +1,239 @@
1
+ #! /usr/bin/ruby
2
+
3
+ # WebSocketCat, a primitive CLI tool for manually talking to WebSocket servers
4
+
5
+ require 'base64'
6
+ require 'getoptlong'
7
+ require 'webtube'
8
+
9
+ VERSION_DATA = "WebSocketCat 1.0.0 (Webtube 1.0.0)
10
+ Copyright (C) 2014 Andres Soolo
11
+ Copyright (C) 2014 Knitten Development Ltd.
12
+
13
+ Licensed under GPLv3+: GNU GPL version 3 or later
14
+ <http://gnu.org/licenses/gpl.html>
15
+
16
+ This is free software: you are free to change and
17
+ redistribute it.
18
+
19
+ There is NO WARRANTY, to the extent permitted by law.
20
+
21
+ "
22
+
23
+ USAGE = "Usage: wsc [options] ws[s]://host[:port][/path]
24
+
25
+ Interact with a WebSocket server, telnet-style.
26
+
27
+ --header, -H=name:value
28
+ Use the given HTTP header field in the request.
29
+
30
+ --insecure, -k
31
+ Allow connecting to an SSL server with invalid certificate.
32
+
33
+ --help
34
+ Print this usage.
35
+
36
+ --version
37
+ Show version data.
38
+
39
+ Report bugs to: <dig@mirky.net>
40
+
41
+ "
42
+
43
+ ONLINE_HELP = "WebSocketCat's commands are slash-prefixed.
44
+
45
+ /ping [message]
46
+ Send a ping frame to the server.
47
+
48
+ /close [status [explanation]]
49
+ Send a close frame to the server. The status code is specified as an
50
+ unsigned decimal number.
51
+
52
+ /N [payload]
53
+ Send a message or control frame of opcode [[N]], given as a single hex digit,
54
+ to the server. Per protocol specification, [[/1]] is text message, [[/2]] is
55
+ binary message, [[/8]] is close, [[/9]] is ping, [[/A]] is pong. Other
56
+ opcodes can have application-specific meaning. Note that the specification
57
+ requires kicking clients (or servers) who send messages so cryptic that the
58
+ server (or client) can't understand them.
59
+
60
+ /help
61
+ Show this online help.
62
+
63
+ If you need to start a text message with a slash, you can double it for escape,
64
+ or you can use the explicit [[/1]] command. EOF from stdin is equivalent to
65
+ [[/close 1000]].
66
+
67
+ "
68
+
69
+ $header = {} # lowercased field name => value
70
+ $insecure = false
71
+
72
+ $0 = 'wsc' # for [[GetoptLong]] error reporting
73
+ begin
74
+ GetoptLong.new(
75
+ ['--header', '-H', GetoptLong::REQUIRED_ARGUMENT],
76
+ ['--insecure', '-k', GetoptLong::NO_ARGUMENT],
77
+ ['--help', GetoptLong::NO_ARGUMENT],
78
+ ['--version', GetoptLong::NO_ARGUMENT],
79
+ ).each do |opt, arg|
80
+ case opt
81
+ when '--header' then
82
+ name, value = arg.split /\s*:\s*/, 2
83
+ if value.nil? then
84
+ $stderr.puts "wsc: colon missing in argument to --header"
85
+ exit 1
86
+ end
87
+ name.downcase!
88
+ if $header[name] then
89
+ # The value was specified multiple times.
90
+ $header[name] += ", " + value
91
+ else
92
+ $header[name] = value
93
+ end
94
+ when '--insecure' then
95
+ $insecure = true
96
+ when '--help' then
97
+ puts USAGE
98
+ exit 0
99
+ when '--version' then
100
+ puts VERSION_DATA
101
+ exit 0
102
+ else
103
+ raise 'assertion failed'
104
+ end
105
+ end
106
+ rescue GetoptLong::Error => e
107
+ # no need to display; it has already been reported
108
+ exit 1
109
+ end
110
+
111
+ unless ARGV.length == 1 then
112
+ $stderr.puts "wsc: argument mismatch (exactly one needed)"
113
+ exit 1
114
+ end
115
+
116
+ # The events incoming over the WebSocket will be listened to by this object,
117
+ # and promptly shown to the user.
118
+
119
+ class << $listener = Object.new
120
+ def onopen webtube
121
+ puts "*** open"
122
+ return
123
+ end
124
+
125
+ def onmessage webtube, content, opcode
126
+ if opcode == 1 then
127
+ puts "<<< #{content}"
128
+ else
129
+ puts "<#{opcode}< #{content.inspect}"
130
+ end
131
+ return
132
+ end
133
+
134
+ def oncontrolframe webtube, frame
135
+ # We'll ignore 9 (ping) and 10 (pong) here, as they are already processed
136
+ # by handlers of their own.
137
+ unless [9, 10].include? frame.opcode then
138
+ puts "*#{'%X' % opcode}* #{frame.payload.inspect}"
139
+ end
140
+ return
141
+ end
142
+
143
+ def onping webtube, frame
144
+ puts "*** ping #{frame.payload.inspect}"
145
+ return
146
+ end
147
+
148
+ def onpong webtube, frame
149
+ puts "*** pong #{frame.payload.inspect}"
150
+ return
151
+ end
152
+
153
+ def onannoyedclose webtube, frame
154
+ if frame.body.bytesize >= 2 then
155
+ status_code, = frame.body.unpack 'n'
156
+ message = frame.body.byteslice 2 .. -1
157
+ message.force_encoding 'UTF-8'
158
+ message.force_encoding 'ASCII-8BIT' unless message.valid_encoding?
159
+ message = nil if message.empty?
160
+ else
161
+ status_code = nil
162
+ message = nil
163
+ end
164
+ puts "*** annoyedclose #{status_code.inspect}" +
165
+ (message ? " #{message.inspect}" : '')
166
+ return
167
+ end
168
+
169
+ def onclose webtube
170
+ puts "*** close"
171
+ $send_thread.raise StopSendThread
172
+ return
173
+ end
174
+ end
175
+
176
+ class StopSendThread < Exception
177
+ end
178
+
179
+ puts "Connecting to #{ARGV.first} ..."
180
+
181
+ $webtube = Webtube.connect ARGV.first,
182
+ header_fields: $header,
183
+ allow_opcodes: 1 .. 15,
184
+ ssl_verify_mode: $insecure ? OpenSSL::SSL::VERIFY_NONE : nil,
185
+ on_http_response: proc{ |response|
186
+ # Show the HTTP response to the user
187
+ puts "| #{response.code} #{response.message}"
188
+ response.each_key do |key|
189
+ response.get_fields(key).each do |value|
190
+ puts "| #{key}: #{value}"
191
+ end
192
+ end
193
+ puts
194
+ }
195
+
196
+ # [[$listener]] will send us, via [[$send_thread]], the [[StopSendThread]]
197
+ # exception when the other side goes away.
198
+ $send_thread = Thread.current
199
+
200
+ # [[Webtube#run]] will hog the whole thread it runs on, so we'll give it a
201
+ # thread of its own.
202
+ $recv_thread = Thread.new do
203
+ begin
204
+ $webtube.run $listener
205
+ rescue Exception => e
206
+ $stderr.puts "Exception in receive thread: #{$!}", $@
207
+ $send_thread.exit 1 # terminate the main thread
208
+ end
209
+ end
210
+
211
+ # Now, read user input and interpret commands.
212
+
213
+ begin
214
+ until $stdin.eof? do
215
+ line = $stdin.readline.chomp!
216
+ case line
217
+ when /\A\/(\/)/ then
218
+ $webtube.send_message $1 + $'
219
+ when /\A\/([0-9a-f])\b\s*/i then
220
+ $webtube.send_message $', $1.hex
221
+ when /\A\/ping\b\s*/ then
222
+ $webtube.send_message $', Webtube::OPCODE_PING
223
+ puts "(Ping sent.)"
224
+ when /\A\/close\b\s*\Z/ then
225
+ $webtube.close
226
+ puts "(Close sent.)"
227
+ when /\A\/close\b\s+(\d+)\s*/ then
228
+ $webtube.close $1.to_i, $'
229
+ puts "(Close sent.)"
230
+ when /\A\/help\s*\Z/ then
231
+ puts ONLINE_HELP
232
+ else
233
+ $webtube.send_message line
234
+ end
235
+ end
236
+ $webtube.close
237
+ $recv_thread.join
238
+ rescue StopSendThread
239
+ end
@@ -0,0 +1,757 @@
1
+ # webtube.rb -- an implementation of the WebSocket extension of HTTP
2
+
3
+ require 'base64'
4
+ require 'digest/sha1'
5
+ require 'net/http'
6
+ require 'securerandom'
7
+ require 'thread'
8
+ require 'uri'
9
+ require 'webrick/httprequest'
10
+
11
+ class Webtube
12
+ # Not all the possible 16 values are defined by the standard.
13
+ OPCODE_CONTINUATION = 0x0
14
+ OPCODE_TEXT = 0x1
15
+ OPCODE_BINARY = 0x2
16
+ OPCODE_CLOSE = 0x8
17
+ OPCODE_PING = 0x9
18
+ OPCODE_PONG = 0xA
19
+
20
+ attr_accessor :allow_rsv_bits
21
+ attr_accessor :allow_opcodes
22
+
23
+ # The following three slots are not used by the [[Webtube]] infrastructrue.
24
+ # They have been defined purely so that application code could easily
25
+ # associate data it finds significant to [[Webtube]] instances.
26
+
27
+ attr_accessor :header # [[accept_webtube]] saves the request object here
28
+ attr_accessor :session
29
+ attr_accessor :context
30
+
31
+ def initialize socket,
32
+ serverp,
33
+ # If true, we will expect incoming data masked and will not mask
34
+ # outgoing data. If false, we will expect incoming data unmasked and
35
+ # will mask outgoing data.
36
+ allow_rsv_bits: 0,
37
+ allow_opcodes: [Webtube::OPCODE_TEXT],
38
+ close_socket: true
39
+ super()
40
+ @socket = socket
41
+ @serverp = serverp
42
+ @allow_rsv_bits = allow_rsv_bits
43
+ @allow_opcodes = allow_opcodes
44
+ @close_socket = close_socket
45
+ @defrag_buffer = []
46
+ @alive = true
47
+ @send_mutex = Mutex.new
48
+ # Guards message sending, so that fragmented messages won't get
49
+ # interleaved, and the [[@alive]] flag.
50
+ @run_mutex = Mutex.new
51
+ # Guards the main read loop.
52
+ @receiving_frame = false
53
+ # Are we currently receiving a frame for the [[Webtube#run]] main loop?
54
+ @reception_interrupt_mutex = Mutex.new
55
+ # guards [[@receiving_frame]]
56
+ return
57
+ end
58
+
59
+ # Run a loop to read all the messages and control frames coming in via this
60
+ # WebSocket, and hand events to the given [[listener]]. The listener can
61
+ # implement the following methods:
62
+ #
63
+ # - onopen(webtube) will be called as soon as the channel is set up.
64
+ #
65
+ # - onmessage(webtube, message_body, opcode) will be called with each
66
+ # arriving data message once it has been defragmented. The data will be
67
+ # passed to it as a [[String]], encoded in [[UTF-8]] for [[OPCODE_TEXT]]
68
+ # messages and in [[ASCII-8BIT]] for all the other message opcodes.
69
+ #
70
+ # - oncontrolframe(webtube, frame) will be called upon receipt of a control
71
+ # frame whose opcode is listed in the [[allow_opcodes]] parameter of this
72
+ # [[Webtube]] instance. The frame is repreented by an instance of
73
+ # [[Webtube::Frame]]. Note that [[Webtube]] handles connection closures
74
+ # ([[OPCODE_CLOSE]]) and ponging all the pings ([[OPCODE_PING]])
75
+ # automatically.
76
+ #
77
+ # - onping(webtube, frame) will be called upon receipt of an [[OPCODE_PING]]
78
+ # frame. [[Webtube]] will take care of ponging all the pings, but the
79
+ # listener may want to process such an event for statistical information.
80
+ #
81
+ # - onpong(webtube, frame) will be called upon receipt of an [[OPCODE_PONG]]
82
+ # frame.
83
+ #
84
+ # - onclose(webtube) will be called upon closure of the connection, for any
85
+ # reason.
86
+ #
87
+ # - onannoyedclose(webtube, frame) will be called upon receipt of an
88
+ # [[OPCODE_CLOSE]] frame with an explicit status code other than 1000.
89
+ # This typically indicates that the other side is annoyed, so the listener
90
+ # may want to log the condition for debugging or further analysis.
91
+ # Normally, once the handler returns, [[Webtube]] will respond with a close
92
+ # frame of the same status code and close the connection, but the handler
93
+ # may call [[Webtube#close]] to request a closure with a different status
94
+ # code or without one.
95
+ #
96
+ # - onexception(webtube, exception) will be called if an unhandled exception
97
+ # is raised during the [[Webtube]]'s lifecycle, including all of the
98
+ # listener event handlers. It may log the exception but should return
99
+ # normally so that the [[Webtube]] can issue a proper close frame for the
100
+ # other end and invoke the [[onclose]] handler, after which the exception
101
+ # will be raised again so the caller of [[Webtube#run]] will have a chance
102
+ # of handling it.
103
+ #
104
+ # Before calling any of the handlers, [[respond_to?]] will be used to check
105
+ # implementedness.
106
+ #
107
+ # If an exception occurs during processing, it may implement a specific
108
+ # status code to be passed to the other end via the [[OPCODE_CLOSE]] frame by
109
+ # implementing the [[websocket_close_status_code]] method returning the code
110
+ # as an integer. The default code, used if the exception does not specify
111
+ # one, is 1011 'unexpected condition'. An exception may explicitly suppress
112
+ # sending any code by having [[websocket_close_status_code]] return [[nil]]
113
+ # instead of an integer.
114
+ #
115
+ def run listener
116
+ @run_mutex.synchronize do
117
+ @thread = Thread.current
118
+ begin
119
+ listener.onopen self if listener.respond_to? :onopen
120
+ while @send_mutex.synchronize{@alive} do
121
+ begin
122
+ @reception_interrupt_mutex.synchronize do
123
+ @receiving_frame = true
124
+ end
125
+ frame = Webtube::Frame.read_from_socket @socket
126
+ ensure
127
+ @reception_interrupt_mutex.synchronize do
128
+ @receiving_frame = false
129
+ end
130
+ end
131
+ unless (frame.rsv & ~@allow_rsv_bits) == 0 then
132
+ raise Webtube::UnknownReservedBit.new(frame: frame)
133
+ end
134
+ if @serverp then
135
+ unless frame.masked?
136
+ raise Webtube::UnmaskedFrameToServer.new(frame: frame)
137
+ end
138
+ else
139
+ unless !frame.masked? then
140
+ raise Webtube::MaskedFrameToClient.new(frame: frame)
141
+ end
142
+ end
143
+ if !frame.control_frame? then
144
+ # data frame
145
+ if frame.opcode != Webtube::OPCODE_CONTINUATION then
146
+ # initial frame
147
+ unless @allow_opcodes.include? frame.opcode then
148
+ raise Webtube::UnknownOpcode.new(frame: frame)
149
+ end
150
+ unless @defrag_buffer.empty? then
151
+ raise Webtube::MissingContinuationFrame.new
152
+ end
153
+ else
154
+ # continuation frame
155
+ if @defrag_buffer.empty? then
156
+ raise Webtube::UnexpectedContinuationFrame.new(frame: frame)
157
+ end
158
+ end
159
+ @defrag_buffer.push frame
160
+ if frame.fin? then
161
+ opcode = @defrag_buffer.first.opcode
162
+ data = @defrag_buffer.map(&:payload).join ''
163
+ @defrag_buffer = []
164
+ if opcode == Webtube::OPCODE_TEXT then
165
+ # text messages must be encoded in UTF-8, as per RFC 6455
166
+ data.force_encoding 'UTF-8'
167
+ unless data.valid_encoding? then
168
+ data.force_encoding 'ASCII-8BIT'
169
+ raise Webtube::BadlyEncodedText.new(data: data)
170
+ end
171
+ end
172
+ listener.onmessage self, data, opcode \
173
+ if listener.respond_to? :onmessage
174
+ end
175
+ elsif (0x08 .. 0x0F).include? frame.opcode then
176
+ # control frame
177
+ unless frame.fin? then
178
+ raise Webtube::FragmentedControlFrame.new(frame: frame)
179
+ end
180
+ case frame.opcode
181
+ when Webtube::OPCODE_CLOSE then
182
+ message = frame.payload
183
+ if message.length >= 2 then
184
+ status_code, = message.unpack 'n'
185
+ unless status_code == 1000 then
186
+ listener.onannoyedclose self, frame \
187
+ if listener.respond_to? :onannoyedclose
188
+ end
189
+ else
190
+ status_code = 1000
191
+ end
192
+ close status_code
193
+ when Webtube::OPCODE_PING then
194
+ listener.onping self, frame if listener.respond_to? :onping
195
+ send_message frame.payload, Webtube::OPCODE_PONG
196
+ when Webtube::OPCODE_PONG then
197
+ listener.onpong self, frame if listener.respond_to? :onpong
198
+ # ignore
199
+ else
200
+ unless @allow_opcodes.include? frame.opcode then
201
+ raise Webtube::UnknownOpcode.new(frame: frame)
202
+ end
203
+ end
204
+ listener.oncontrolframe self, frame \
205
+ if @allow_opcodes.include?(frame.opcode) and
206
+ listener.respond_to?(:oncontrolframe)
207
+ else
208
+ raise 'assertion failed'
209
+ end
210
+ end
211
+ rescue AbortReceiveLoop
212
+ # we're out of the loop now, so nothing further to do
213
+ rescue Exception => e
214
+ status_code = if e.respond_to? :websocket_close_status_code then
215
+ e.websocket_close_status_code
216
+ else
217
+ 1011 # 'unexpected condition'
218
+ end
219
+ listener.onexception self, e if listener.respond_to? :onexception
220
+ begin
221
+ close status_code
222
+ rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN
223
+ # ignore, we have a bigger exception to handle
224
+ end
225
+ raise e
226
+ ensure
227
+ @thread = nil
228
+ listener.onclose self if listener.respond_to? :onclose
229
+ end
230
+ end
231
+ return
232
+ end
233
+
234
+ # Send a given message payload to the other party, using the given opcode.
235
+ # By default, the [[opcode]] is [[Webtube::OPCODE_TEXT]]. Re-encodes the
236
+ # payload if given in a non-UTF-8 encoding and [[opcode ==
237
+ # Webtube::OPCODE_TEXT]].
238
+ def send_message message, opcode = Webtube::OPCODE_TEXT
239
+ if opcode == Webtube::OPCODE_TEXT and message.encoding.name != 'UTF-8' then
240
+ message = message.encode 'UTF-8'
241
+ end
242
+ @send_mutex.synchronize do
243
+ raise 'WebSocket connection no longer live' unless @alive
244
+ # In order to ensure that the local kernel will treat our (data) frames
245
+ # atomically during the [[write]] syscall, we'll want to ensure that the
246
+ # frame size does not exceed 512 bytes -- the minimum permitted size for
247
+ # [[PIPE_BUF]]. At this frame size, the header size is up to four bytes
248
+ # for unmasked or eight bytes for masked frames.
249
+ Webtube::Frame.each_frame_for_message(
250
+ message: message,
251
+ opcode: opcode,
252
+ masked: !@serverp,
253
+ max_frame_body_size: 512 - (!@serverp ? 8 : 4)) do |frame|
254
+ @socket.write frame.header + frame.body
255
+ end
256
+ end
257
+ return
258
+ end
259
+
260
+ # Close the connection, thus preventing further processing.
261
+ #
262
+ # If [[status_code]] is supplied, it will be passed to the other side in the
263
+ # [[OPCODE_CLOSE]] frame. The default is 1000 which indicates normal
264
+ # closure. Sending a status code can be explicitly suppressed by passing
265
+ # [[nil]] instead of an integer; then, an empty close frame will be sent.
266
+ # Due to the way a close frame's payload is structured, this will also
267
+ # suppress delivery of [[close_explanation]], even if non-empty.
268
+ #
269
+ # Note that RFC 6455 requires the explanation to be encoded in UTF-8.
270
+ # Accordingly, this method will re-encode it unless it is already in UTF-8.
271
+ def close status_code = 1000, explanation = ""
272
+ # prepare the payload for the close frame
273
+ payload = ""
274
+ if status_code then
275
+ payload = [status_code].pack('n')
276
+ if explanation then
277
+ payload << explanation.encode('UTF-8')
278
+ end
279
+ end
280
+ # let the other side know we're closing
281
+ send_message payload, OPCODE_CLOSE
282
+ # break the main reception loop
283
+ @send_mutex.synchronize do
284
+ @alive = false
285
+ end
286
+ # if waiting for a frame (or parsing one), interrupt it
287
+ @reception_interrupt_mutex.synchronize do
288
+ @thread.raise AbortReceiveLoop.new if @receiving_frame
289
+ end
290
+ @socket.close if @close_socket
291
+ return
292
+ end
293
+
294
+ # Attempt to set up a [[WebSocket]] connection to the given [[url]]. Return
295
+ # the [[Webtube]] instance if successful or raise an appropriate
296
+ # [[Webtube::WebSocketUpgradeFailed]].
297
+ def self::connect url,
298
+ header_fields: {},
299
+ ssl_verify_mode: nil,
300
+ on_http_response: nil,
301
+ allow_rsv_bits: 0,
302
+ allow_opcodes: [Webtube::OPCODE_TEXT],
303
+ close_socket: true
304
+ # We'll replace the WebSocket protocol prefix with an HTTP-based one so
305
+ # [[URI::parse]] would know how to parse the rest of the URL.
306
+ case url
307
+ when /\Aws:/ then
308
+ hturl = 'http:' + $'
309
+ ssl = false
310
+ default_port = 80
311
+ when /\Awss:/ then
312
+ hturl = 'https:' + $'
313
+ ssl = true
314
+ default_port = 443
315
+ else
316
+ raise "unknown URI scheme; use ws: or wss: instead"
317
+ end
318
+ hturi = URI.parse hturl
319
+
320
+ reqhdr = {}
321
+
322
+ # Copy over the user-supplied header fields. Since Ruby hashes are
323
+ # case-sensitive but HTTP header field names are case-insensitive, we may
324
+ # have to combine fields whose names only differ in case.
325
+ header_fields.each_pair do |name, value|
326
+ name = name.downcase
327
+ if reqhdr.has_key? name then
328
+ reqhdr[name] += ', ' + value
329
+ else
330
+ reqhdr[name] = value
331
+ end
332
+ end
333
+
334
+ # Set up the WebSocket header fields (but we'll give user-specified values,
335
+ # if any, precedence)
336
+ reqhdr['host'] ||=
337
+ hturi.host + (hturi.port != default_port ? ":#{hturi.port}" : "")
338
+ reqhdr['upgrade'] ||= 'websocket'
339
+ reqhdr['connection'] ||= 'upgrade'
340
+ reqhdr['sec-websocket-key'] ||= SecureRandom.base64(16)
341
+ reqhdr['sec-websocket-version'] ||= '13'
342
+
343
+ start_options = {}
344
+ start_options[:use_ssl] = ssl
345
+ start_options[:verify_mode] = ssl_verify_mode if ssl and ssl_verify_mode
346
+ http = Net::HTTP.start hturi.host, hturi.port, **start_options
347
+
348
+ object_to_request = hturi.path
349
+ if object_to_request.empty? then
350
+ object_to_request = '/'
351
+ end
352
+ if hturi.query then
353
+ object_to_request += '?' + hturi.query
354
+ end
355
+ response = http.get object_to_request, reqhdr
356
+ on_http_response.call response if on_http_response
357
+
358
+ # Check that the server is seeing us now
359
+ unless response.code == '101' then
360
+ raise Webtube::WebSocketDeclined.new("the HTTP response code was not 101")
361
+ end
362
+ unless (response['Connection'] || '').downcase == 'upgrade' then
363
+ raise Webtube::WebSocketDeclined.new("the HTTP response did not say " +
364
+ "'Connection: upgrade'")
365
+ end
366
+ unless (response['Upgrade'] || '').downcase == 'websocket' then
367
+ raise Webtube::WebSocketDeclined.new("the HTTP response did not say " +
368
+ "'Upgrade: websocket'")
369
+ end
370
+ expected_accept = Digest::SHA1.base64digest(
371
+ reqhdr['sec-websocket-key'] +
372
+ '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
373
+ unless (response['Sec-WebSocket-Accept'] || '') == expected_accept then
374
+ raise Webtube::WebSocketDeclined.new("the HTTP response did not say " +
375
+ "'Sec-WebSocket-Accept: #{expected_accept}'")
376
+ end
377
+ unless (response['Sec-WebSocket-Version'] || '13').
378
+ split(/\s*,\s*/).include? '13' then
379
+ raise Webtube::WebSocketVersionMismatch.new(
380
+ "Sec-WebSocket-Version negotiation failed")
381
+ end
382
+
383
+ # The connection has been set up. Now let's set up the Webtube.
384
+ socket = http.instance_eval{@socket}
385
+ socket.read_timeout = nil # turn off timeout
386
+ return Webtube.new socket, false,
387
+ allow_rsv_bits: allow_rsv_bits,
388
+ allow_opcodes: allow_opcodes,
389
+ close_socket: close_socket
390
+ end
391
+
392
+ # The application may want to store many Webtube instances in a hash or a
393
+ # set. In order to facilitate this, we'll need [[hash]] and [[eql?]]. The
394
+ # latter is already adequately -- comparing by identity -- implemented by
395
+ # [[Object]]; in order to ensure the former hashes by identity, we'll
396
+ # override it.
397
+ def hash
398
+ return object_id
399
+ end
400
+
401
+ # A technical exception, raised by [[Webtube#close]] if [[Webtube#run]] is
402
+ # currently waiting for a frame.
403
+ class AbortReceiveLoop < Exception
404
+ end
405
+
406
+ # Note that [[body]] holds the /raw/ data; that is, if [[masked?]] is true,
407
+ # it will need to be unmasked to get the payload. Call [[payload]] in order
408
+ # to abstract this away.
409
+ Frame = Struct.new(:header, :body)
410
+ class Frame
411
+ def fin?
412
+ return (header.getbyte(0) & 0x80) != 0
413
+ end
414
+
415
+ def fin= new_value
416
+ header.setbyte 0, header.getbyte(0) & 0x7F | (new_value ? 0x80 : 0x00)
417
+ return new_value
418
+ end
419
+
420
+ def rsv1
421
+ return (header.getbyte(0) & 0x40) != 0
422
+ end
423
+
424
+ def rsv2
425
+ return (header.getbyte(0) & 0x20) != 0
426
+ end
427
+
428
+ def rsv3
429
+ return (header.getbyte(0) & 0x10) != 0
430
+ end
431
+
432
+ # The three reserved bits of the frame, shifted rightwards to meet the
433
+ # binary point
434
+ def rsv
435
+ return (header.getbyte(0) & 0x70) >> 4
436
+ end
437
+
438
+ def opcode
439
+ return header.getbyte(0) & 0x0F
440
+ end
441
+
442
+ def opcode= new_opcode
443
+ header.setbyte 0, (header.getbyte(0) & ~0x0F) | (new_opcode & 0x0F)
444
+ return new_opcode
445
+ end
446
+
447
+ def control_frame?
448
+ return opcode >= 0x8
449
+ end
450
+
451
+ def masked?
452
+ return (header.getbyte(1) & 0x80) != 0
453
+ end
454
+
455
+ # Determine the size of this frame's extended payload length field in bytes
456
+ # from the 7-bit short payload length field.
457
+ def extended_payload_length_field_size
458
+ return case header.getbyte(1) & 0x7F
459
+ when 126 then 2
460
+ when 127 then 8
461
+ else 0
462
+ end
463
+ end
464
+
465
+ # Extract the length of this frame's payload. Enough bytes of the header
466
+ # must already have been read; see [[extended_payload_lenth_field_size]].
467
+ def payload_length
468
+ return case base = header.getbyte(1) & 0x7F
469
+ when 126 then header.unpack('@2 n').first
470
+ when 127 then header.unpack('@2 @>').first
471
+ else base
472
+ end
473
+ end
474
+
475
+ # Extract the mask as a 4-byte [[ASCII-8BIT]] string from this frame. If
476
+ # the frame has the [[masked?]] bit unset, return [[nil]] instead.
477
+ def mask
478
+ if masked? then
479
+ mask_offset = 2 + case header.getbyte(1) & 0x7F
480
+ when 126 then 2
481
+ when 127 then 8
482
+ else 0
483
+ end
484
+ return header[mask_offset, 4]
485
+ else
486
+ return nil
487
+ end
488
+ end
489
+
490
+ # Extract the frame's payload and return it as a [[String]] instance of the
491
+ # [[ASCII-8BIT]] encoding. If the frame has the [[masked?]] bit set, this
492
+ # also involves demasking.
493
+ def payload
494
+ return Frame.apply_mask(body, mask)
495
+ end
496
+
497
+ # Apply the given [[mask]], specified as a four-byte (!) [[String]], to the
498
+ # given [[data]]. Note that since the underlying operation is [[XOR]], the
499
+ # operation can be repeated to reverse itself.
500
+ #
501
+ # [[nil]] can be supplied instead of [[mask]] to indicate that no
502
+ # processing is needed.
503
+ #
504
+ def self::apply_mask data, mask
505
+ return data if mask.nil?
506
+ raise 'invalid mask' unless mask.bytesize == 4
507
+ result = data.dup
508
+ (0 ... result.bytesize).each do |i|
509
+ result.setbyte i, result.getbyte(i) ^ mask.getbyte(i & 3)
510
+ end
511
+ return result
512
+ end
513
+
514
+ # Read all the bytes of one WebSocket frame from the given [[socket]] and
515
+ # return them in a [[Frame]] instance. In case traffic ends before the
516
+ # frame is complete, raise [[BrokenFrame]].
517
+ #
518
+ # Note that this will call [[socket.read]] twice or thrice, and assumes no
519
+ # other thread will consume bytes from the socket inbetween. In a
520
+ # multithreaded environment, it may be necessary to apply external
521
+ # locking.
522
+ #
523
+ def self::read_from_socket socket
524
+ header = socket.read(2)
525
+ unless header and header.bytesize == 2 then
526
+ header ||= String.new.force_encoding('ASCII-8BIT')
527
+ raise BrokenFrame.new(header)
528
+ end
529
+ frame = Frame.new header
530
+
531
+ header_tail_size = frame.extended_payload_length_field_size +
532
+ (frame.masked? ? 4 : 0)
533
+ unless header_tail_size.zero? then
534
+ header_tail = socket.read(header_tail_size)
535
+ frame.header << header_tail if header_tail
536
+ unless header_tail and header_tail.bytesize == header_tail_size then
537
+ raise BrokenFrame.new(frame.header)
538
+ end
539
+ end
540
+
541
+ data_size = frame.payload_length
542
+ frame.body = socket.read(data_size)
543
+ unless frame.body and frame.body.bytesize == data_size then
544
+ raise BrokenFrame.new(frame.body ?
545
+ frame.header + frame.body :
546
+ frame.header)
547
+ end
548
+
549
+ return frame
550
+ end
551
+
552
+ # Given a frame's payload, prepare the header and return a [[Frame]]
553
+ # instance representing such a frame. Optionally, some header fields can
554
+ # also be set.
555
+ #
556
+ # It's OK for the caller to modify some header fields, such as [[fin]] or
557
+ # [[opcode]], on the returned [[Frame]] by calling the appropriate methods.
558
+ # Its body should not be modified after construction, however, because its
559
+ # length and possibly its mask is already encoded in the header.
560
+ def self::prepare(
561
+ payload: '',
562
+ opcode: OPCODE_TEXT,
563
+ fin: true,
564
+ masked: false)
565
+ header = [0].pack 'C' # we'll fill out the first byte later
566
+ mask_flag = masked ? 0x80 : 0x00
567
+ header << if payload.bytesize <= 125 then
568
+ [mask_flag | payload.bytesize].pack 'C'
569
+ elsif payload.bytesize <= 0xFFFF then
570
+ [mask_flag | 126, payload.bytesize].pack 'C n'
571
+ elsif payload.bytesize <= 0x7FFF_FFFF_FFFF_FFFF then
572
+ [mask_flag | 127, payload.bytesize].pack 'C Q>'
573
+ else
574
+ raise 'attempted to prepare a WebSocket frame with too big payload'
575
+ end
576
+ frame = Frame.new(header)
577
+ unless masked then
578
+ frame.body = payload
579
+ else
580
+ mask = SecureRandom.random_bytes(4)
581
+ frame.header << mask
582
+ frame.body = apply_mask(payload, mask)
583
+ end
584
+
585
+ # now, it's time to fill out the first byte
586
+ frame.fin = fin
587
+ frame.opcode = opcode
588
+
589
+ return frame
590
+ end
591
+
592
+ # Given a message and attributes, break it up into frames, and yields each
593
+ # such [[Frame]] separately for processing by the caller -- usually,
594
+ # delivery to the other end via the socket. Takes care to not fragment
595
+ # control messages. If masking is required, uses
596
+ # [[SecureRandom.random_bytes]] to generate masks for each frame.
597
+ def self::each_frame_for_message message: '',
598
+ opcode: OPCODE_TEXT,
599
+ masked: false,
600
+ max_frame_body_size: nil
601
+ message = message.dup.force_encoding Encoding::ASCII_8BIT
602
+ offset = 0
603
+ fin = true
604
+ begin
605
+ frame_length = message.bytesize - offset
606
+ fin = !(opcode <= 0x07 and
607
+ max_frame_body_size and
608
+ frame_length > max_frame_body_size)
609
+ frame_length = max_frame_body_size unless fin
610
+ yield Webtube::Frame.prepare(
611
+ opcode: opcode,
612
+ payload: message[offset, frame_length],
613
+ fin: fin,
614
+ masked: masked)
615
+ offset += frame_length
616
+ opcode = 0x00 # for continuation frames
617
+ end until fin
618
+ return
619
+ end
620
+ end
621
+
622
+ class ConnectionNotAlive < RuntimeError
623
+ def initialize
624
+ super "WebSocket connection is no longer alive and can not transmit " +
625
+ "any more messages"
626
+ return
627
+ end
628
+ end
629
+
630
+ class ProtocolError < StandardError
631
+ def websocket_close_status_code
632
+ return 1002
633
+ end
634
+ end
635
+
636
+ # Indicates that a complete frame could not be read from the underlying TCP
637
+ # connection. [[Webtube::Frame::read_from_socket]] will also give it the
638
+ # partial frame as a string so it could be further analysed, but this is
639
+ # optional.
640
+ class BrokenFrame < ProtocolError
641
+ attr_reader :partial_frame
642
+
643
+ def initialize message = "no complete WebSocket frame was available",
644
+ partial_frame = nil
645
+ super message
646
+ @partial_frame = partial_frame
647
+ return
648
+ end
649
+ end
650
+
651
+ class UnknownReservedBit < ProtocolError
652
+ attr_reader :frame
653
+
654
+ def initialize message = "frame with unknown RSV bit arrived",
655
+ frame: nil
656
+ super message
657
+ @frame = frame
658
+ return
659
+ end
660
+
661
+ def websocket_close_status_code
662
+ return 1003
663
+ end
664
+ end
665
+
666
+ class UnknownOpcode < ProtocolError
667
+ attr_reader :frame
668
+
669
+ def initialize message = "frame with unknown opcode arrived",
670
+ frame: nil
671
+ super message
672
+ @frame = frame
673
+ return
674
+ end
675
+
676
+ def websocket_close_status_code
677
+ return 1003
678
+ end
679
+ end
680
+
681
+ class UnmaskedFrameToServer < ProtocolError
682
+ attr_reader :frame
683
+
684
+ def initialize message = "unmasked frame arrived but we're the server",
685
+ frame: nil
686
+ super message
687
+ @frame = frame
688
+ return
689
+ end
690
+ end
691
+
692
+ class MaskedFrameToClient < ProtocolError
693
+ attr_reader :frame
694
+
695
+ def initialize message = "masked frame arrived but we're the client",
696
+ frame: nil
697
+ super message
698
+ @frame = frame
699
+ return
700
+ end
701
+ end
702
+
703
+ class MissingContinuationFrame < ProtocolError
704
+ def initialize message = "a new initial data frame arrived while only " +
705
+ "parts of a previous fragmented message had arrived"
706
+ super message
707
+ return
708
+ end
709
+ end
710
+
711
+ class UnexpectedContinuationFrame < ProtocolError
712
+ attr_reader :frame
713
+
714
+ def initialize message = "a continuation frame arrived but there was no " +
715
+ "fragmented message pending",
716
+ frame: nil
717
+ super message
718
+ @frame = frame
719
+ return
720
+ end
721
+ end
722
+
723
+ class BadlyEncodedText < ProtocolError
724
+ attr_reader :data
725
+
726
+ def initialize message = "invalid UTF-8 in a text-type message",
727
+ data: data
728
+ super message
729
+ @data = data
730
+ return
731
+ end
732
+
733
+ def websocket_close_status_code
734
+ return 1007
735
+ end
736
+ end
737
+
738
+ class FragmentedControlFrame < ProtocolError
739
+ attr_reader :frame
740
+
741
+ def initialize message = "a control frame arrived without its FIN flag set",
742
+ frame: nil
743
+ super message
744
+ @frame = frame
745
+ return
746
+ end
747
+ end
748
+
749
+ class WebSocketUpgradeFailed < StandardError
750
+ end
751
+
752
+ class WebSocketDeclined < WebSocketUpgradeFailed
753
+ end
754
+
755
+ class WebSocketVersionMismatch < WebSocketUpgradeFailed
756
+ end
757
+ end