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