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.
- checksums.yaml +4 -4
- data/README +715 -560
- data/bin/wsc +53 -34
- data/lib/webtube.rb +510 -257
- data/lib/webtube/vital-statistics.rb +31 -24
- data/lib/webtube/webrick.rb +79 -59
- data/sample-server.rb +4 -4
- data/webtube.gemspec +6 -6
- metadata +9 -9
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
|
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.
|
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
|
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
|
54
|
-
to the server. Per protocol specification,
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
64
|
-
or you can use the explicit [[/1]] command. EOF
|
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
|
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
|
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
|
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
|
158
|
-
message.force_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
|
-
|
194
|
+
http_header: $header,
|
183
195
|
allow_opcodes: 1 .. 15,
|
184
|
-
ssl_verify_mode: $insecure ?
|
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
|
-
|
188
|
-
|
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
|
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
|
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
|
data/lib/webtube.rb
CHANGED
@@ -1,12 +1,11 @@
|
|
1
|
-
# webtube.rb -- an implementation of the WebSocket extension of
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
34
|
-
# outgoing data. If false, we will
|
35
|
-
# will mask outgoing
|
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
|
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
|
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
|
60
|
-
# WebSocket, and hand events to the given
|
61
|
-
# implement the following
|
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
|
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
|
66
|
-
# arriving data message once it has been
|
67
|
-
# passed to it as a
|
68
|
-
#
|
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
|
71
|
-
# frame whose opcode is listed in the
|
72
|
-
# [[
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
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
|
78
|
-
# frame. [[Webtube]] will take care of
|
79
|
-
#
|
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
|
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
|
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
|
88
|
-
# [[OPCODE_CLOSE]] frame with an explicit status code
|
89
|
-
# This typically indicates that the other
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
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
|
97
|
-
# is raised during the [[Webtube]]'s
|
98
|
-
#
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
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
|
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
|
108
|
-
#
|
109
|
-
#
|
110
|
-
#
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
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(
|
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(
|
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(
|
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,
|
166
|
-
|
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
|
169
|
-
raise Webtube::BadlyEncodedText.new(
|
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(
|
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
|
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
|
198
|
-
|
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 =
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
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
|
235
|
-
# By default, the [[opcode]] is
|
236
|
-
# payload if given
|
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
|
240
|
-
|
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
|
245
|
-
# atomically during the [[write]] syscall,
|
246
|
-
#
|
247
|
-
#
|
248
|
-
#
|
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:
|
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
|
-
#
|
298
|
+
# Closes the connection, thus preventing further transmission.
|
261
299
|
#
|
262
|
-
# If [[status_code]] is supplied, it will be passed to the
|
263
|
-
# [[OPCODE_CLOSE]] frame. The default is
|
264
|
-
# closure. Sending a status code
|
265
|
-
# [[nil]] instead of
|
266
|
-
#
|
267
|
-
#
|
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
|
270
|
-
# Accordingly, this method will re-encode it unless it
|
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(
|
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
|
-
|
295
|
-
|
296
|
-
|
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
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
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
|
-
#
|
335
|
-
#
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
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
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
-
|
349
|
-
|
350
|
-
|
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
|
-
|
353
|
-
|
464
|
+
unless (response['Connection'] || '').downcase ==
|
465
|
+
'upgrade' then
|
466
|
+
return "the HTTP response did not say " +
|
467
|
+
"'Connection: upgrade'"
|
354
468
|
end
|
355
|
-
response
|
356
|
-
|
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
|
-
|
359
|
-
|
360
|
-
|
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
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
367
|
-
|
368
|
-
|
529
|
+
|
530
|
+
def ssl?
|
531
|
+
return @ssl
|
369
532
|
end
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
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
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
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
|
393
|
-
# set. In order to facilitate this, we'll need
|
394
|
-
# latter is already adequately --
|
395
|
-
# [[Object]]; in order
|
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
|
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
|
407
|
-
# it will need to be unmasked to get
|
408
|
-
# to abstract this
|
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 |
|
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
|
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) |
|
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
|
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
|
466
|
-
# must already have been read; see
|
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
|
470
|
-
when 127 then header.unpack('@2
|
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
|
-
#
|
476
|
-
# the frame has the [[masked?]] bit unset,
|
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[
|
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]]
|
491
|
-
# [[ASCII-8BIT]] encoding. If the frame has
|
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
|
498
|
-
# given [[data]]. Note that since the underlying operation
|
499
|
-
# operation can be repeated to reverse
|
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
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
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
|
515
|
-
# return them in a [[Frame]] instance. In
|
516
|
-
# frame is complete, raise
|
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,
|
519
|
-
# other thread will consume bytes from the
|
520
|
-
# multithreaded environment, it may
|
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
|
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 =
|
532
|
-
|
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
|
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
|
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
|
553
|
-
# instance representing such a frame. Optionally,
|
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
|
557
|
-
# [[opcode]], on the returned [[Frame]] by
|
558
|
-
# Its body should not be
|
559
|
-
#
|
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
|
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
|
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 '
|
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,
|
593
|
-
# such [[Frame]] separately for processing
|
594
|
-
# delivery to the other end via
|
595
|
-
#
|
596
|
-
# [[SecureRandom
|
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
|
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
|
637
|
-
# connection.
|
638
|
-
#
|
639
|
-
#
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
715
|
-
|
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 =
|
727
|
-
|
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 =
|
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
|