webtube 1.0.0 → 1.1.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 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