webtube 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|