webtube 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/wsc CHANGED
@@ -1,14 +1,15 @@
1
1
  #! /usr/bin/ruby
2
2
 
3
- # WebSocketCat, a primitive CLI tool for manually talking to WebSocket servers
3
+ # WebSocketCat, a primitive CLI tool for manually talking to
4
+ # WebSocket servers
4
5
 
5
6
  require 'base64'
6
7
  require 'getoptlong'
7
8
  require 'webtube'
8
9
 
9
- VERSION_DATA = "WebSocketCat 1.0.0 (Webtube 1.0.0)
10
- Copyright (C) 2014 Andres Soolo
11
- Copyright (C) 2014 Knitten Development Ltd.
10
+ VERSION_DATA = "WebSocketCat 1.1.0 (Webtube 1.1.0)
11
+ Copyright (C) 2014-2018 Andres Soolo
12
+ Copyright (C) 2014-2018 Knitten Development Ltd.
12
13
 
13
14
  Licensed under GPLv3+: GNU GPL version 3 or later
14
15
  <http://gnu.org/licenses/gpl.html>
@@ -20,13 +21,16 @@ There is NO WARRANTY, to the extent permitted by law.
20
21
 
21
22
  "
22
23
 
23
- USAGE = "Usage: wsc [options] ws[s]://host[:port][/path]
24
+ USAGE = "Usage: wsc [options] ws[s]://host[:port][/path][?query]
24
25
 
25
26
  Interact with a WebSocket server, telnet-style.
26
27
 
27
28
  --header, -H=name:value
28
29
  Use the given HTTP header field in the request.
29
30
 
31
+ --cacert=FILENAME
32
+ Load trusted root certificate(s) from the given PEM file.
33
+
30
34
  --insecure, -k
31
35
  Allow connecting to an SSL server with invalid certificate.
32
36
 
@@ -46,33 +50,36 @@ ONLINE_HELP = "WebSocketCat's commands are slash-prefixed.
46
50
  Send a ping frame to the server.
47
51
 
48
52
  /close [status [explanation]]
49
- Send a close frame to the server. The status code is specified as an
50
- unsigned decimal number.
53
+ Send a close frame to the server. The status code is
54
+ specified as an unsigned decimal number.
51
55
 
52
56
  /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.
57
+ Send a message or control frame of opcode [[N]], given as a
58
+ single hex digit, to the server. Per protocol specification,
59
+ [[/1]] is text message, [[/2]] is binary message, [[/8]] is
60
+ close, [[/9]] is ping, [[/A]] is pong. Other opcodes can have
61
+ application-specific meaning. Note that the specification
62
+ requires kicking clients (or servers) who send messages so
63
+ cryptic that the server (or client) can't understand them.
59
64
 
60
65
  /help
61
66
  Show this online help.
62
67
 
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]].
68
+ If you need to start a text message with a slash, you can double
69
+ it for escape, or you can use the explicit [[/1]] command. EOF
70
+ from stdin is equivalent to [[/close 1000]].
66
71
 
67
72
  "
68
73
 
69
74
  $header = {} # lowercased field name => value
70
75
  $insecure = false
76
+ $cert_store = nil
71
77
 
72
78
  $0 = 'wsc' # for [[GetoptLong]] error reporting
73
79
  begin
74
80
  GetoptLong.new(
75
81
  ['--header', '-H', GetoptLong::REQUIRED_ARGUMENT],
82
+ ['--cacert', GetoptLong::REQUIRED_ARGUMENT],
76
83
  ['--insecure', '-k', GetoptLong::NO_ARGUMENT],
77
84
  ['--help', GetoptLong::NO_ARGUMENT],
78
85
  ['--version', GetoptLong::NO_ARGUMENT],
@@ -81,7 +88,8 @@ begin
81
88
  when '--header' then
82
89
  name, value = arg.split /\s*:\s*/, 2
83
90
  if value.nil? then
84
- $stderr.puts "wsc: colon missing in argument to --header"
91
+ $stderr.puts "wsc: colon missing in argument to " +
92
+ "--header"
85
93
  exit 1
86
94
  end
87
95
  name.downcase!
@@ -91,6 +99,9 @@ begin
91
99
  else
92
100
  $header[name] = value
93
101
  end
102
+ when '--cacert' then
103
+ $cert_store ||= OpenSSL::X509::Store.new
104
+ $cert_store.add_file arg
94
105
  when '--insecure' then
95
106
  $insecure = true
96
107
  when '--help' then
@@ -113,8 +124,8 @@ unless ARGV.length == 1 then
113
124
  exit 1
114
125
  end
115
126
 
116
- # The events incoming over the WebSocket will be listened to by this object,
117
- # and promptly shown to the user.
127
+ # The events incoming over the WebSocket will be listened to by
128
+ # this object, and promptly shown to the user.
118
129
 
119
130
  class << $listener = Object.new
120
131
  def onopen webtube
@@ -132,8 +143,8 @@ class << $listener = Object.new
132
143
  end
133
144
 
134
145
  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.
146
+ # We'll ignore 9 (ping) and 10 (pong) here, as they are
147
+ # already processed by handlers of their own.
137
148
  unless [9, 10].include? frame.opcode then
138
149
  puts "*#{'%X' % opcode}* #{frame.payload.inspect}"
139
150
  end
@@ -154,8 +165,9 @@ class << $listener = Object.new
154
165
  if frame.body.bytesize >= 2 then
155
166
  status_code, = frame.body.unpack 'n'
156
167
  message = frame.body.byteslice 2 .. -1
157
- message.force_encoding 'UTF-8'
158
- message.force_encoding 'ASCII-8BIT' unless message.valid_encoding?
168
+ message.force_encoding Encoding::UTF_8
169
+ message.force_encoding Encoding::ASCII_8BIT \
170
+ unless message.valid_encoding?
159
171
  message = nil if message.empty?
160
172
  else
161
173
  status_code = nil
@@ -179,26 +191,33 @@ end
179
191
  puts "Connecting to #{ARGV.first} ..."
180
192
 
181
193
  $webtube = Webtube.connect ARGV.first,
182
- header_fields: $header,
194
+ http_header: $header,
183
195
  allow_opcodes: 1 .. 15,
184
- ssl_verify_mode: $insecure ? OpenSSL::SSL::VERIFY_NONE : nil,
196
+ ssl_verify_mode: $insecure ?
197
+ OpenSSL::SSL::VERIFY_NONE :
198
+ OpenSSL::SSL::VERIFY_PEER,
199
+ ssl_cert_store: $cert_store,
200
+ on_http_request: proc{ |request|
201
+ # Show the HTTP request to the user
202
+ request.rstrip.each_line do |s|
203
+ puts "> #{s.rstrip}"
204
+ end
205
+ puts
206
+ },
185
207
  on_http_response: proc{ |response|
186
208
  # 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
209
+ response.rstrip.each_line do |s|
210
+ puts "< #{s.rstrip}"
192
211
  end
193
212
  puts
194
213
  }
195
214
 
196
- # [[$listener]] will send us, via [[$send_thread]], the [[StopSendThread]]
197
- # exception when the other side goes away.
215
+ # [[$listener]] will send us, via [[$send_thread]], the
216
+ # [[StopSendThread]] exception when the other side goes away.
198
217
  $send_thread = Thread.current
199
218
 
200
- # [[Webtube#run]] will hog the whole thread it runs on, so we'll give it a
201
- # thread of its own.
219
+ # [[Webtube#run]] will hog the whole thread it runs on, so we'll
220
+ # give it a thread of its own.
202
221
  $recv_thread = Thread.new do
203
222
  begin
204
223
  $webtube.run $listener
@@ -1,12 +1,11 @@
1
- # webtube.rb -- an implementation of the WebSocket extension of HTTP
1
+ # webtube.rb -- an implementation of the WebSocket extension of
2
+ # HTTP
2
3
 
3
- require 'base64'
4
- require 'digest/sha1'
5
4
  require 'net/http'
5
+ require 'openssl'
6
6
  require 'securerandom'
7
7
  require 'thread'
8
8
  require 'uri'
9
- require 'webrick/httprequest'
10
9
 
11
10
  class Webtube
12
11
  # Not all the possible 16 values are defined by the standard.
@@ -20,19 +19,27 @@ class Webtube
20
19
  attr_accessor :allow_rsv_bits
21
20
  attr_accessor :allow_opcodes
22
21
 
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.
22
+ attr_reader :url
23
+ # only available if the [[WebTube]] was instantiated via
24
+ # [[Webtube::connect]] as the client end of a WebSocket
25
+ # connection
26
26
 
27
- attr_accessor :header # [[accept_webtube]] saves the request object here
27
+ # The following three slots are not used by the [[Webtube]]
28
+ # infrastructrue. They have been defined purely so that
29
+ # application code could easily associate data it finds
30
+ # significant to [[Webtube]] instances.
31
+
32
+ attr_accessor :header
33
+ # [[accept_webtube]] saves the request object here
28
34
  attr_accessor :session
29
35
  attr_accessor :context
30
36
 
31
37
  def initialize socket,
32
38
  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.
39
+ # If true, we will expect incoming data masked and
40
+ # will not mask outgoing data. If false, we will
41
+ # expect incoming data unmasked and will mask outgoing
42
+ # data.
36
43
  allow_rsv_bits: 0,
37
44
  allow_opcodes: [Webtube::OPCODE_TEXT],
38
45
  close_socket: true
@@ -45,72 +52,84 @@ class Webtube
45
52
  @defrag_buffer = []
46
53
  @alive = true
47
54
  @send_mutex = Mutex.new
48
- # Guards message sending, so that fragmented messages won't get
49
- # interleaved, and the [[@alive]] flag.
55
+ # Guards message sending, so that fragmented messages
56
+ # won't get interleaved, and the [[@alive]] flag.
50
57
  @run_mutex = Mutex.new
51
58
  # Guards the main read loop.
52
59
  @receiving_frame = false
53
- # Are we currently receiving a frame for the [[Webtube#run]] main loop?
60
+ # Are we currently receiving a frame for the
61
+ # [[Webtube#run]] main loop?
54
62
  @reception_interrupt_mutex = Mutex.new
55
63
  # guards [[@receiving_frame]]
56
64
  return
57
65
  end
58
66
 
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:
67
+ # Run a loop to read all the messages and control frames
68
+ # coming in via this WebSocket, and hand events to the given
69
+ # [[listener]]. The listener can implement the following
70
+ # methods:
62
71
  #
63
- # - onopen(webtube) will be called as soon as the channel is set up.
72
+ # - onopen(webtube) will be called as soon as the channel is
73
+ # set up.
64
74
  #
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.
75
+ # - onmessage(webtube, message_body, opcode) will be called
76
+ # with each arriving data message once it has been
77
+ # defragmented. The data will be passed to it as a
78
+ # [[String]], encoded in [[UTF-8]] for [[OPCODE_TEXT]]
79
+ # messages and in [[ASCII-8BIT]] for all the other message
80
+ # opcodes.
69
81
  #
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.
82
+ # - oncontrolframe(webtube, frame) will be called upon receipt
83
+ # of a control frame whose opcode is listed in the
84
+ # [[allow_opcodes]] parameter of this [[Webtube]] instance.
85
+ # The frame is represented by an instance of
86
+ # [[Webtube::Frame]]. Note that [[Webtube]] handles
87
+ # connection closures ([[OPCODE_CLOSE]]) and ponging all the
88
+ # pings ([[OPCODE_PING]]) automatically.
76
89
  #
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.
90
+ # - onping(webtube, frame) will be called upon receipt of an
91
+ # [[OPCODE_PING]] frame. [[Webtube]] will take care of
92
+ # ponging all the pings, but the listener may want to
93
+ # process such an event for statistical information.
80
94
  #
81
- # - onpong(webtube, frame) will be called upon receipt of an [[OPCODE_PONG]]
82
- # frame.
95
+ # - onpong(webtube, frame) will be called upon receipt of an
96
+ # [[OPCODE_PONG]] frame.
83
97
  #
84
- # - onclose(webtube) will be called upon closure of the connection, for any
85
- # reason.
98
+ # - onclose(webtube) will be called upon closure of the
99
+ # connection, for any reason.
86
100
  #
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.
101
+ # - onannoyedclose(webtube, frame) will be called upon receipt
102
+ # of an [[OPCODE_CLOSE]] frame with an explicit status code
103
+ # other than 1000. This typically indicates that the other
104
+ # side is annoyed, so the listener may want to log the
105
+ # condition for debugging or further analysis. Normally,
106
+ # once the handler returns, [[Webtube]] will respond with a
107
+ # close frame of the same status code and close the
108
+ # connection, but the handler may call [[Webtube#close]] to
109
+ # request a closure with a different status code or without
110
+ # one.
95
111
  #
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.
112
+ # - onexception(webtube, exception) will be called if an
113
+ # unhandled exception is raised during the [[Webtube]]'s
114
+ # lifecycle, including all of the listener event handlers.
115
+ # It may log the exception but should return normally so
116
+ # that the [[Webtube]] can issue a proper close frame for
117
+ # the other end and invoke the [[onclose]] handler, after
118
+ # which the exception will be raised again so the caller of
119
+ # [[Webtube#run]] will have a chance to handle it.
103
120
  #
104
- # Before calling any of the handlers, [[respond_to?]] will be used to check
105
- # implementedness.
121
+ # Before calling any of the handlers, [[respond_to?]] will be
122
+ # used to check implementedness.
106
123
  #
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.
124
+ # If an exception occurs during processing, it (that is, the
125
+ # [[Exception]] instance) may implement a specific status code
126
+ # to be passed to the other end via the [[OPCODE_CLOSE]] frame
127
+ # by implementing the [[websocket_close_status_code]] method
128
+ # returning the code as an integer. The default code, used if
129
+ # the exception does not specify one, is 1011 'unexpected
130
+ # condition'. An exception may explicitly suppress sending
131
+ # any code by having [[websocket_close_status_code]] return
132
+ # [[nil]] instead of an integer.
114
133
  #
115
134
  def run listener
116
135
  @run_mutex.synchronize do
@@ -133,11 +152,13 @@ class Webtube
133
152
  end
134
153
  if @serverp then
135
154
  unless frame.masked?
136
- raise Webtube::UnmaskedFrameToServer.new(frame: frame)
155
+ raise Webtube::UnmaskedFrameToServer.new(
156
+ frame: frame)
137
157
  end
138
158
  else
139
159
  unless !frame.masked? then
140
- raise Webtube::MaskedFrameToClient.new(frame: frame)
160
+ raise Webtube::MaskedFrameToClient.new(
161
+ frame: frame)
141
162
  end
142
163
  end
143
164
  if !frame.control_frame? then
@@ -153,7 +174,8 @@ class Webtube
153
174
  else
154
175
  # continuation frame
155
176
  if @defrag_buffer.empty? then
156
- raise Webtube::UnexpectedContinuationFrame.new(frame: frame)
177
+ raise Webtube::UnexpectedContinuationFrame.new(
178
+ frame: frame)
157
179
  end
158
180
  end
159
181
  @defrag_buffer.push frame
@@ -162,11 +184,13 @@ class Webtube
162
184
  data = @defrag_buffer.map(&:payload).join ''
163
185
  @defrag_buffer = []
164
186
  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'
187
+ # text messages must be encoded in UTF-8, per
188
+ # RFC 6455
189
+ data.force_encoding Encoding::UTF_8
167
190
  unless data.valid_encoding? then
168
- data.force_encoding 'ASCII-8BIT'
169
- raise Webtube::BadlyEncodedText.new(data: data)
191
+ data.force_encoding Encoding::ASCII_8BIT
192
+ raise Webtube::BadlyEncodedText.new(
193
+ data: data)
170
194
  end
171
195
  end
172
196
  listener.onmessage self, data, opcode \
@@ -175,7 +199,8 @@ class Webtube
175
199
  elsif (0x08 .. 0x0F).include? frame.opcode then
176
200
  # control frame
177
201
  unless frame.fin? then
178
- raise Webtube::FragmentedControlFrame.new(frame: frame)
202
+ raise Webtube::FragmentedControlFrame.new(
203
+ frame: frame)
179
204
  end
180
205
  case frame.opcode
181
206
  when Webtube::OPCODE_CLOSE then
@@ -191,11 +216,12 @@ class Webtube
191
216
  end
192
217
  close status_code
193
218
  when Webtube::OPCODE_PING then
194
- listener.onping self, frame if listener.respond_to? :onping
219
+ listener.onping self, frame \
220
+ if listener.respond_to? :onping
195
221
  send_message frame.payload, Webtube::OPCODE_PONG
196
222
  when Webtube::OPCODE_PONG then
197
- listener.onpong self, frame if listener.respond_to? :onpong
198
- # ignore
223
+ listener.onpong self, frame \
224
+ if listener.respond_to? :onpong
199
225
  else
200
226
  unless @allow_opcodes.include? frame.opcode then
201
227
  raise Webtube::UnknownOpcode.new(frame: frame)
@@ -211,12 +237,14 @@ class Webtube
211
237
  rescue AbortReceiveLoop
212
238
  # we're out of the loop now, so nothing further to do
213
239
  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
240
+ status_code =
241
+ if e.respond_to? :websocket_close_status_code then
242
+ e.websocket_close_status_code
243
+ else
244
+ 1011 # 'unexpected condition'
245
+ end
246
+ listener.onexception self, e \
247
+ if listener.respond_to? :onexception
220
248
  begin
221
249
  close status_code
222
250
  rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN
@@ -225,56 +253,69 @@ class Webtube
225
253
  raise e
226
254
  ensure
227
255
  @thread = nil
228
- listener.onclose self if listener.respond_to? :onclose
256
+ listener.onclose self \
257
+ if listener.respond_to? :onclose
229
258
  end
230
259
  end
231
260
  return
232
261
  end
233
262
 
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 ==
263
+ # Send a given message payload to the other party, using the
264
+ # given opcode. By default, the [[opcode]] is
265
+ # [[Webtube::OPCODE_TEXT]]. Re-encodes the payload if given
266
+ # in a non-UTF-8 encoding and [[opcode ==
237
267
  # Webtube::OPCODE_TEXT]].
238
268
  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'
269
+ if opcode == Webtube::OPCODE_TEXT and
270
+ message.encoding != Encoding::UTF_8 then
271
+ message = message.encode Encoding::UTF_8
241
272
  end
242
273
  @send_mutex.synchronize do
243
274
  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.
275
+ # In order to ensure that the local kernel will treat our
276
+ # (data) frames atomically during the [[write]] syscall,
277
+ # we'll want to ensure that the frame size does not exceed
278
+ # 512 bytes -- the minimum permitted size for
279
+ # [[PIPE_BUF]]. At this frame size, the header size is up
280
+ # to four bytes for unmasked or eight bytes for masked
281
+ # frames.
282
+ #
283
+ # (FIXME: in retrospect, that seems like an unpractical
284
+ # consideration. We should probably use path MTU
285
+ # instead.)
249
286
  Webtube::Frame.each_frame_for_message(
250
287
  message: message,
251
288
  opcode: opcode,
252
289
  masked: !@serverp,
253
- max_frame_body_size: 512 - (!@serverp ? 8 : 4)) do |frame|
290
+ max_frame_body_size:
291
+ 512 - (!@serverp ? 8 : 4)) do |frame|
254
292
  @socket.write frame.header + frame.body
255
293
  end
256
294
  end
257
295
  return
258
296
  end
259
297
 
260
- # Close the connection, thus preventing further processing.
298
+ # Closes the connection, thus preventing further transmission.
261
299
  #
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.
300
+ # If [[status_code]] is supplied, it will be passed to the
301
+ # other side in the [[OPCODE_CLOSE]] frame. The default is
302
+ # 1000 which indicates normal closure. Sending a status code
303
+ # can be explicitly suppressed by passing [[nil]] instead of
304
+ # an integer; then, an empty close frame will be sent. Due to
305
+ # the way a close frame's payload is structured, this will
306
+ # also suppress delivery of [[close_explanation]], even if
307
+ # non-empty.
268
308
  #
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.
309
+ # Note that RFC 6455 requires the explanation to be encoded in
310
+ # UTF-8. Accordingly, this method will re-encode it unless it
311
+ # is already in UTF-8.
271
312
  def close status_code = 1000, explanation = ""
272
313
  # prepare the payload for the close frame
273
314
  payload = ""
274
315
  if status_code then
275
316
  payload = [status_code].pack('n')
276
317
  if explanation then
277
- payload << explanation.encode('UTF-8')
318
+ payload << explanation.encode(Encoding::UTF_8)
278
319
  end
279
320
  end
280
321
  # let the other side know we're closing
@@ -291,121 +332,312 @@ class Webtube
291
332
  return
292
333
  end
293
334
 
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]].
335
+ def inspect
336
+ s = "#<Webtube@0x%0x" % object_id
337
+ s << " " << (@server ? 'from' : 'to')
338
+ unless @url.nil? then
339
+ s << " " << @url
340
+ else
341
+ # [[@socket]] is a [[Net::BufferedIO]] instance, so
342
+ # [[@socket.io]] is either a plain socket or an SSL
343
+ # wrapper
344
+ af, port, hostname = @socket.io.peeraddr
345
+ s << " %s:%i" % [hostname, port]
346
+ end
347
+ s << " @allow_rsv_bits=%s" % @allow_rsv_bits.inspect \
348
+ unless @allow_rsv_bits.nil?
349
+ s << " @allow_opcodes=%s" % @allow_opcodes.inspect \
350
+ unless @allow_opcodes.nil?
351
+ s << ">"
352
+ return s
353
+ end
354
+
355
+ # Attempts to set up a [[WebSocket]] connection to the given
356
+ # [[url]]. Returns the [[Webtube]] instance if successful or
357
+ # raise an appropriate [[Webtube::WebSocketUpgradeFailed]].
297
358
  def self::connect url,
298
- header_fields: {},
299
- ssl_verify_mode: nil,
300
- on_http_response: nil,
301
359
  allow_rsv_bits: 0,
302
360
  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"
361
+ http_header: {},
362
+ ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
363
+ ssl_cert_store: nil,
364
+ # or an [[OpenSSL::X509::Store]] instance
365
+ tcp_connect_timeout: nil, # or number of seconds
366
+ tcp_nodelay: true,
367
+ close_socket: true,
368
+ on_http_request: nil,
369
+ on_http_response: nil,
370
+ on_ssl_handshake: nil,
371
+ on_tcp_connect: nil
372
+ loc = Webtube::Location.new url
373
+
374
+ socket = if tcp_connect_timeout.nil? then
375
+ TCPSocket.new loc.host, loc.port
376
+ else
377
+ Timeout.timeout tcp_connect_timeout, Net::OpenTimeout do
378
+ TCPSocket.new loc.host, loc.port
379
+ end
317
380
  end
318
- hturi = URI.parse hturl
319
-
320
- reqhdr = {}
381
+ if tcp_nodelay then
382
+ socket.setsockopt Socket::IPPROTO_TCP,
383
+ Socket::TCP_NODELAY, 1
384
+ end
385
+ on_tcp_connect.call socket if on_tcp_connect
321
386
 
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
387
+ if loc.ssl? then
388
+ # construct an SSL context
389
+ if ssl_cert_store.nil? then
390
+ ssl_cert_store = OpenSSL::X509::Store.new
391
+ ssl_cert_store.set_default_paths
392
+ end
393
+ ssl_context = OpenSSL::SSL::SSLContext.new
394
+ ssl_context.cert_store = ssl_cert_store
395
+ ssl_context.verify_mode = ssl_verify_mode
396
+ # wrap the socket
397
+ socket = OpenSSL::SSL::SSLSocket.new socket, ssl_context
398
+ socket.sync_close = true
399
+ socket.hostname = loc.host # Server Name Indication
400
+ socket.connect # perform SSL handshake
401
+ socket.post_connection_check loc.host
402
+ on_ssl_handshake.call socket if on_ssl_handshake
403
+ end
404
+
405
+ socket = Net::BufferedIO.new socket
406
+
407
+ # transmit the request
408
+ req = Webtube::Request.new loc, http_header
409
+ composed_request = req.to_s
410
+ socket.write composed_request
411
+ on_http_request.call composed_request if on_http_request
412
+
413
+ # wait for response
414
+ response = Net::HTTPResponse.read_new socket
415
+
416
+ if on_http_response then
417
+ # reconstitute the response as a string
418
+ #
419
+ # (XXX: this loses some diagnostically useful bits, but
420
+ # [[Net::HTTPResponse::read_new]] just doesn't preserve
421
+ # the pristine original)
422
+ s = "#{response.code} #{response.message}\r\n"
423
+ response.each_header do |k, v|
424
+ s << "#{k}: #{v}\r\n"
331
425
  end
426
+ s << "\r\n"
427
+ on_http_response.call s
332
428
  end
333
429
 
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'
430
+ # Check that the server is seeing us now
431
+ # FIXME: ensure that the socket will be closed in case of
432
+ # exception
433
+ d = rejection response, req.expected_accept
434
+ raise Webtube::WebSocketDeclined.new(d) \
435
+ if d
342
436
 
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
437
+ # Can the server speak our protocol version?
438
+ unless (response['Sec-WebSocket-Version'] || '13').
439
+ strip.split(/\s*,\s*/).include? '13' then
440
+ raise Webtube::WebSocketVersionMismatch.new(
441
+ "Sec-WebSocket-Version negotiation failed")
442
+ end
347
443
 
348
- object_to_request = hturi.path
349
- if object_to_request.empty? then
350
- object_to_request = '/'
444
+ # The connection has been set up. Now we can instantiate
445
+ # [[Webtube]].
446
+ wt = Webtube.new socket, false,
447
+ allow_rsv_bits: allow_rsv_bits,
448
+ allow_opcodes: allow_opcodes,
449
+ close_socket: close_socket
450
+ wt.instance_variable_set :@url, loc.to_s
451
+ return wt
452
+ end
453
+
454
+ # Checks whether the given [[Net::HTTPResponse]] represents a
455
+ # valid WebSocket upgrade acceptance. Returns [[nil]] if so,
456
+ # or a human-readable string explaining the issue if not.
457
+ # [[expected_accept]] is the value [[Sec-WebSocket-Accept]] is
458
+ # expected to hold, generated from the [[Sec-WebSocket-Key]]
459
+ # header field.
460
+ def self::rejection response, expected_accept
461
+ unless response.code == '101' then
462
+ return "the HTTP response code was not 101"
351
463
  end
352
- if hturi.query then
353
- object_to_request += '?' + hturi.query
464
+ unless (response['Connection'] || '').downcase ==
465
+ 'upgrade' then
466
+ return "the HTTP response did not say " +
467
+ "'Connection: upgrade'"
354
468
  end
355
- response = http.get object_to_request, reqhdr
356
- on_http_response.call response if on_http_response
469
+ unless (response['Upgrade'] || '').downcase ==
470
+ 'websocket' then
471
+ return "the HTTP response did not say " +
472
+ "'Upgrade: websocket'"
473
+ end
474
+ unless (response['Sec-WebSocket-Accept'] || '') ==
475
+ expected_accept then
476
+ return "the HTTP response did not say " +
477
+ "'Sec-WebSocket-Accept: #{expected_accept}'"
478
+ end
479
+ return nil
480
+ end
357
481
 
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")
482
+ # Represents a parsed WebSocket URL.
483
+ class Location
484
+ def initialize url
485
+ super()
486
+ # force into a single-byte encoding so urlencoding can
487
+ # work correctly
488
+ url = url.dup.force_encoding Encoding::ASCII_8BIT
489
+ # ensure that any whitespace, ASCII on-printables, and
490
+ # some popular text delimiters (parens, brokets, brackets,
491
+ # and broken bar) in [[url]] are properly urlencoded
492
+ url.gsub! ' ', '+'
493
+ url.gsub!(/[^\x21-\x7E]/){'%%%02X' % $&.ord}
494
+ url.gsub!(/[()<>\[\]\|]/){'%%%02X' % $&.ord}
495
+ # We'll replace the WebSocket protocol prefix with an
496
+ # HTTP-based one so [[URI::parse]] would know how to
497
+ # parse the rest of the URL.
498
+ case url
499
+ when /\A(ws|http):/ then
500
+ http_url = 'http:' + $'
501
+ @ssl = false
502
+ @default_port = 80
503
+ when /\A(wss|https):/ then
504
+ http_url = 'https:' + $'
505
+ @ssl = true
506
+ @default_port = 443
507
+ else
508
+ raise "unknown URI scheme; use ws: or wss: instead"
509
+ end
510
+ http_uri = URI.parse http_url
511
+ @host = http_uri.host
512
+ @port = http_uri.port
513
+ @requestee = http_uri.path
514
+ if @requestee.empty? then
515
+ @requestee = '/'
516
+ end
517
+ @requestee += '?' + http_uri.query \
518
+ if http_uri.query
519
+ return
361
520
  end
362
- unless (response['Connection'] || '').downcase == 'upgrade' then
363
- raise Webtube::WebSocketDeclined.new("the HTTP response did not say " +
364
- "'Connection: upgrade'")
521
+
522
+ def to_s
523
+ s = !ssl? ? 'ws:' : 'wss:'
524
+ s += '//' + host_and_maybe_port
525
+ s += @requestee \
526
+ unless @requestee == '/'
527
+ return s
365
528
  end
366
- unless (response['Upgrade'] || '').downcase == 'websocket' then
367
- raise Webtube::WebSocketDeclined.new("the HTTP response did not say " +
368
- "'Upgrade: websocket'")
529
+
530
+ def ssl?
531
+ return @ssl
369
532
  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}'")
533
+
534
+ attr_reader :default_port
535
+ attr_reader :host
536
+ attr_reader :port
537
+ attr_reader :requestee
538
+
539
+ # Returns the hostname and, if non-default, the port number
540
+ # separated by colon. This combination is used in HTTP 1.1
541
+ # [[Host]] header fields but also in URIs.
542
+ def host_and_maybe_port
543
+ h = @host
544
+ h += ":#@port" \
545
+ unless @port == @default_port
546
+ return h
376
547
  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")
548
+ end
549
+
550
+ # Represents an HTTP request (well, request header, since a
551
+ # WebSocket open request shouldn't have a body) being prepared
552
+ # to open a WebSocket connection.
553
+ class Request
554
+ attr_reader :location
555
+
556
+ def initialize location, custom_fields = {}
557
+ super()
558
+ @location = location
559
+ @fields = {} # capitalised-name => value
560
+ # Since Ruby hashes are case-sensitive but HTTP header
561
+ # field names are case-insensitive, we may have to combine
562
+ # fields whose names only differ in case.
563
+ custom_fields.each_pair do |name, value|
564
+ name = name.capitalize
565
+ if @fields.has_key? name then
566
+ @fields[name] += ', ' + value
567
+ else
568
+ @fields[name] = value
569
+ end
570
+ end
571
+
572
+ # Add in the WebSocket header fields but give precedence
573
+ # to user-specified values
574
+ @fields['Host'] ||= @location.host_and_maybe_port
575
+ @fields['Upgrade'] ||= 'websocket'
576
+ @fields['Connection'] ||= 'upgrade'
577
+ @fields['Sec-websocket-key'] ||=
578
+ SecureRandom.base64(16)
579
+ @fields['Sec-websocket-version'] ||= '13'
580
+
581
+ return
381
582
  end
382
583
 
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
584
+ def [] name
585
+ return @fields[name.capitalize]
586
+ end
587
+
588
+ def []= name, value
589
+ name = name.capitalize
590
+ unless value.nil? then
591
+ @fields[name] = value
592
+ else
593
+ @fields.delete name
594
+ end
595
+ return value
596
+ end
597
+
598
+ def each_pair &thunk
599
+ @fields.each_pair &thunk
600
+ return self
601
+ end
602
+
603
+ # Constructs an HTTP request header in string form, together
604
+ # with CRLF line terminators and the terminal blank line,
605
+ # ready to be transmitted to the server.
606
+ def to_s
607
+ s = ''
608
+ s << "GET #{@location.requestee} HTTP/1.1\r\n"
609
+ each_pair do |k, v|
610
+ s << "#{k}: #{v}\r\n"
611
+ end
612
+ s << "\r\n"
613
+ return s
614
+ end
615
+
616
+ def expected_accept
617
+ return OpenSSL::Digest::SHA1.base64digest(
618
+ self['Sec-WebSocket-Key'] +
619
+ '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
620
+ end
390
621
  end
391
622
 
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.
623
+ # The application may want to store many Webtube instances in
624
+ # a hash or a set. In order to facilitate this, we'll need
625
+ # [[hash]] and [[eql?]]. The latter is already adequately --
626
+ # comparing by identity -- implemented by [[Object]]; in order
627
+ # to ensure the former hashes by identity, we'll override it.
397
628
  def hash
398
629
  return object_id
399
630
  end
400
631
 
401
- # A technical exception, raised by [[Webtube#close]] if [[Webtube#run]] is
402
- # currently waiting for a frame.
632
+ # A technical exception, raised by [[Webtube#close]] if
633
+ # [[Webtube#run]] is currently waiting for a frame.
403
634
  class AbortReceiveLoop < Exception
404
635
  end
405
636
 
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.
637
+ # Note that [[body]] holds the /raw/ data; that is, if
638
+ # [[masked?]] is true, it will need to be unmasked to get
639
+ # the payload. Call [[payload]] in order to abstract this
640
+ # away.
409
641
  Frame = Struct.new(:header, :body)
410
642
  class Frame
411
643
  def fin?
@@ -413,7 +645,8 @@ class Webtube
413
645
  end
414
646
 
415
647
  def fin= new_value
416
- header.setbyte 0, header.getbyte(0) & 0x7F | (new_value ? 0x80 : 0x00)
648
+ header.setbyte 0, header.getbyte(0) & 0x7F |
649
+ (new_value ? 0x80 : 0x00)
417
650
  return new_value
418
651
  end
419
652
 
@@ -429,8 +662,8 @@ class Webtube
429
662
  return (header.getbyte(0) & 0x10) != 0
430
663
  end
431
664
 
432
- # The three reserved bits of the frame, shifted rightwards to meet the
433
- # binary point
665
+ # The three reserved bits of the frame, shifted rightwards
666
+ # to meet the binary point
434
667
  def rsv
435
668
  return (header.getbyte(0) & 0x70) >> 4
436
669
  end
@@ -440,7 +673,8 @@ class Webtube
440
673
  end
441
674
 
442
675
  def opcode= new_opcode
443
- header.setbyte 0, (header.getbyte(0) & ~0x0F) | (new_opcode & 0x0F)
676
+ header.setbyte 0, (header.getbyte(0) & ~0x0F) |
677
+ (new_opcode & 0x0F)
444
678
  return new_opcode
445
679
  end
446
680
 
@@ -452,8 +686,8 @@ class Webtube
452
686
  return (header.getbyte(1) & 0x80) != 0
453
687
  end
454
688
 
455
- # Determine the size of this frame's extended payload length field in bytes
456
- # from the 7-bit short payload length field.
689
+ # Determine the size of this frame's extended payload length
690
+ # field in bytes from the 7-bit short payload length field.
457
691
  def extended_payload_length_field_size
458
692
  return case header.getbyte(1) & 0x7F
459
693
  when 126 then 2
@@ -462,18 +696,20 @@ class Webtube
462
696
  end
463
697
  end
464
698
 
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]].
699
+ # Extract the length of this frame's payload. Enough bytes
700
+ # of the header must already have been read; see
701
+ # [[extended_payload_lenth_field_size]].
467
702
  def payload_length
468
703
  return case base = header.getbyte(1) & 0x7F
469
- when 126 then header.unpack('@2 n').first
470
- when 127 then header.unpack('@2 @>').first
704
+ when 126 then header.unpack('@2 S>')[0]
705
+ when 127 then header.unpack('@2 Q>')[0]
471
706
  else base
472
707
  end
473
708
  end
474
709
 
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.
710
+ # Extracts the mask as a tetrabyte integer from this frame.
711
+ # If the frame has the [[masked?]] bit unset, returns
712
+ # [[nil]] instead.
477
713
  def mask
478
714
  if masked? then
479
715
  mask_offset = 2 + case header.getbyte(1) & 0x7F
@@ -481,66 +717,69 @@ class Webtube
481
717
  when 127 then 8
482
718
  else 0
483
719
  end
484
- return header[mask_offset, 4]
720
+ return header.unpack('@%i L>' % mask_offset)[0]
485
721
  else
486
722
  return nil
487
723
  end
488
724
  end
489
725
 
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.
726
+ # Extract the frame's payload and return it as a [[String]]
727
+ # instance of the [[ASCII-8BIT]] encoding. If the frame has
728
+ # the [[masked?]] bit set, this also involves demasking.
493
729
  def payload
494
730
  return Frame.apply_mask(body, mask)
495
731
  end
496
732
 
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.
733
+ # Apply the given [[mask]], specified as an integer, to the
734
+ # given [[data]]. Note that since the underlying operation
735
+ # is [[XOR]], the operation can be repeated to reverse
736
+ # itself.
503
737
  #
738
+ # [[nil]] can be supplied instead of [[mask]] to indicate
739
+ # that no processing is needed.
504
740
  def self::apply_mask data, mask
505
741
  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
742
+ return (data + "\0\0\0"). # pad to full tetras
743
+ unpack('L>*'). # extract tetras
744
+ map!{|i| i ^ mask}. # XOR each with the mask
745
+ pack('L>*'). # pack back into a string
746
+ byteslice(0, data.bytesize) # remove padding
512
747
  end
513
748
 
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]].
749
+ # Read all the bytes of one WebSocket frame from the given
750
+ # [[socket]] and return them in a [[Frame]] instance. In
751
+ # case traffic ends before the frame is complete, raise
752
+ # [[BrokenFrame]].
517
753
  #
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.
754
+ # Note that this will call [[socket.read]] twice or thrice,
755
+ # and assumes no other thread will consume bytes from the
756
+ # socket inbetween. In a multithreaded environment, it may
757
+ # be necessary to apply external locking.
522
758
  #
523
759
  def self::read_from_socket socket
524
760
  header = socket.read(2)
525
761
  unless header and header.bytesize == 2 then
526
- header ||= String.new.force_encoding('ASCII-8BIT')
762
+ header ||= String.new encoding: Encoding::ASCII_8BIT
527
763
  raise BrokenFrame.new(header)
528
764
  end
529
765
  frame = Frame.new header
530
766
 
531
- header_tail_size = frame.extended_payload_length_field_size +
532
- (frame.masked? ? 4 : 0)
767
+ header_tail_size =
768
+ frame.extended_payload_length_field_size +
769
+ (frame.masked? ? 4 : 0)
533
770
  unless header_tail_size.zero? then
534
771
  header_tail = socket.read(header_tail_size)
535
772
  frame.header << header_tail if header_tail
536
- unless header_tail and header_tail.bytesize == header_tail_size then
773
+ unless header_tail and
774
+ header_tail.bytesize == header_tail_size then
537
775
  raise BrokenFrame.new(frame.header)
538
776
  end
539
777
  end
540
778
 
541
779
  data_size = frame.payload_length
542
780
  frame.body = socket.read(data_size)
543
- unless frame.body and frame.body.bytesize == data_size then
781
+ unless frame.body and
782
+ frame.body.bytesize == data_size then
544
783
  raise BrokenFrame.new(frame.body ?
545
784
  frame.header + frame.body :
546
785
  frame.header)
@@ -549,29 +788,30 @@ class Webtube
549
788
  return frame
550
789
  end
551
790
 
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.
791
+ # Given a frame's payload, prepare the header and return a
792
+ # [[Frame]] instance representing such a frame. Optionally,
793
+ # some header fields can also be set.
555
794
  #
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.
795
+ # It's OK for the caller to modify some header fields, such
796
+ # as [[fin]] or [[opcode]], on the returned [[Frame]] by
797
+ # calling the appropriate methods. Its body should not be
798
+ # modified after construction, however, because its length
799
+ # and possibly its mask is already encoded in the header.
560
800
  def self::prepare(
561
801
  payload: '',
562
802
  opcode: OPCODE_TEXT,
563
803
  fin: true,
564
804
  masked: false)
565
- header = [0].pack 'C' # we'll fill out the first byte later
805
+ header = [0].pack 'C' # we'll fill in the first byte later
566
806
  mask_flag = masked ? 0x80 : 0x00
567
807
  header << if payload.bytesize <= 125 then
568
808
  [mask_flag | payload.bytesize].pack 'C'
569
809
  elsif payload.bytesize <= 0xFFFF then
570
- [mask_flag | 126, payload.bytesize].pack 'C n'
810
+ [mask_flag | 126, payload.bytesize].pack 'C S>'
571
811
  elsif payload.bytesize <= 0x7FFF_FFFF_FFFF_FFFF then
572
812
  [mask_flag | 127, payload.bytesize].pack 'C Q>'
573
813
  else
574
- raise 'attempted to prepare a WebSocket frame with too big payload'
814
+ raise 'payload too big for a WebSocket frame'
575
815
  end
576
816
  frame = Frame.new(header)
577
817
  unless masked then
@@ -579,7 +819,7 @@ class Webtube
579
819
  else
580
820
  mask = SecureRandom.random_bytes(4)
581
821
  frame.header << mask
582
- frame.body = apply_mask(payload, mask)
822
+ frame.body = apply_mask(payload, mask.unpack('L>')[0])
583
823
  end
584
824
 
585
825
  # now, it's time to fill out the first byte
@@ -589,11 +829,12 @@ class Webtube
589
829
  return frame
590
830
  end
591
831
 
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.
832
+ # Given a message and attributes, break it up into frames,
833
+ # and yields each such [[Frame]] separately for processing
834
+ # by the caller -- usually, delivery to the other end via
835
+ # the socket. Takes care to not fragment control messages.
836
+ # If masking is required, uses [[SecureRandom]] to generate
837
+ # masks for each frame.
597
838
  def self::each_frame_for_message message: '',
598
839
  opcode: OPCODE_TEXT,
599
840
  masked: false,
@@ -621,7 +862,7 @@ class Webtube
621
862
 
622
863
  class ConnectionNotAlive < RuntimeError
623
864
  def initialize
624
- super "WebSocket connection is no longer alive and can not transmit " +
865
+ super "WebSocket connection has died and can't convey " +
625
866
  "any more messages"
626
867
  return
627
868
  end
@@ -633,14 +874,16 @@ class Webtube
633
874
  end
634
875
  end
635
876
 
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.
877
+ # Indicates that a complete frame could not be read from the
878
+ # underlying TCP connection.
879
+ # [[Webtube::Frame::read_from_socket]] will also give it the
880
+ # partial frame as a string so it could be further analysed,
881
+ # but this is optional.
640
882
  class BrokenFrame < ProtocolError
641
883
  attr_reader :partial_frame
642
884
 
643
- def initialize message = "no complete WebSocket frame was available",
885
+ def initialize message =
886
+ "no complete WebSocket frame was available",
644
887
  partial_frame = nil
645
888
  super message
646
889
  @partial_frame = partial_frame
@@ -651,7 +894,8 @@ class Webtube
651
894
  class UnknownReservedBit < ProtocolError
652
895
  attr_reader :frame
653
896
 
654
- def initialize message = "frame with unknown RSV bit arrived",
897
+ def initialize message =
898
+ "frame with unknown RSV bit arrived",
655
899
  frame: nil
656
900
  super message
657
901
  @frame = frame
@@ -666,7 +910,8 @@ class Webtube
666
910
  class UnknownOpcode < ProtocolError
667
911
  attr_reader :frame
668
912
 
669
- def initialize message = "frame with unknown opcode arrived",
913
+ def initialize message =
914
+ "frame with unknown opcode arrived",
670
915
  frame: nil
671
916
  super message
672
917
  @frame = frame
@@ -681,7 +926,8 @@ class Webtube
681
926
  class UnmaskedFrameToServer < ProtocolError
682
927
  attr_reader :frame
683
928
 
684
- def initialize message = "unmasked frame arrived but we're the server",
929
+ def initialize message =
930
+ "unmasked frame arrived but we're the server",
685
931
  frame: nil
686
932
  super message
687
933
  @frame = frame
@@ -692,7 +938,8 @@ class Webtube
692
938
  class MaskedFrameToClient < ProtocolError
693
939
  attr_reader :frame
694
940
 
695
- def initialize message = "masked frame arrived but we're the client",
941
+ def initialize message =
942
+ "masked frame arrived but we're the client",
696
943
  frame: nil
697
944
  super message
698
945
  @frame = frame
@@ -701,7 +948,8 @@ class Webtube
701
948
  end
702
949
 
703
950
  class MissingContinuationFrame < ProtocolError
704
- def initialize message = "a new initial data frame arrived while only " +
951
+ def initialize message =
952
+ "a new initial data frame arrived while only " +
705
953
  "parts of a previous fragmented message had arrived"
706
954
  super message
707
955
  return
@@ -711,8 +959,9 @@ class Webtube
711
959
  class UnexpectedContinuationFrame < ProtocolError
712
960
  attr_reader :frame
713
961
 
714
- def initialize message = "a continuation frame arrived but there was no " +
715
- "fragmented message pending",
962
+ def initialize message =
963
+ "a continuation frame arrived but there was no " +
964
+ "fragmented message pending",
716
965
  frame: nil
717
966
  super message
718
967
  @frame = frame
@@ -723,8 +972,9 @@ class Webtube
723
972
  class BadlyEncodedText < ProtocolError
724
973
  attr_reader :data
725
974
 
726
- def initialize message = "invalid UTF-8 in a text-type message",
727
- data: data
975
+ def initialize message =
976
+ "UTF-8 encoding error in a text-type message",
977
+ data: nil
728
978
  super message
729
979
  @data = data
730
980
  return
@@ -738,7 +988,8 @@ class Webtube
738
988
  class FragmentedControlFrame < ProtocolError
739
989
  attr_reader :frame
740
990
 
741
- def initialize message = "a control frame arrived without its FIN flag set",
991
+ def initialize message =
992
+ "a control frame arrived without its FIN flag set",
742
993
  frame: nil
743
994
  super message
744
995
  @frame = frame
@@ -754,4 +1005,6 @@ class Webtube
754
1005
 
755
1006
  class WebSocketVersionMismatch < WebSocketUpgradeFailed
756
1007
  end
1008
+
1009
+ VERSION = '1.1.0'
757
1010
  end