Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/proto/http/web_socket.rb
32004 views
1
# -*- coding: binary -*-
2
3
require 'bindata'
4
require 'rex/post/channel'
5
6
module Rex::Proto::Http::WebSocket
7
class WebSocketError < StandardError
8
end
9
10
class ConnectionError < WebSocketError
11
def initialize(msg: 'The WebSocket connection failed', http_response: nil)
12
@message = msg
13
@http_response = http_response
14
end
15
16
attr_accessor :message, :http_response
17
alias to_s message
18
end
19
20
# This defines the interface that the standard socket is extended with to provide WebSocket functionality. It should be
21
# used on a socket when the server has already successfully handled a WebSocket upgrade request.
22
module Interface
23
#
24
# A channel object that allows reading and writing either text or binary data directly to the remote peer.
25
#
26
class Channel
27
include Rex::Post::Channel::StreamAbstraction
28
29
module SocketInterface
30
include Rex::Post::Channel::SocketAbstraction::SocketInterface
31
32
def type?
33
'tcp'
34
end
35
end
36
37
# The socket parameters describing the underlying connection.
38
# @!attribute [r] params
39
# @return [Rex::Socket::Parameters]
40
attr_reader :params
41
42
# @param [WebSocket::Interface] websocket the WebSocket that this channel is being opened on
43
# @param [nil, Symbol] read_type the data type(s) to read from the WebSocket, one of :binary, :text or nil (for both
44
# binary and text)
45
# @param [Symbol] write_type the data type to write to the WebSocket
46
def initialize(websocket, read_type: nil, write_type: :binary)
47
# a read type of nil will handle both binary and text frames that are received
48
raise ArgumentError, 'read_type must be nil, :binary or :text' unless [nil, :binary, :text].include?(read_type)
49
raise ArgumentError, 'write_type must be :binary or :text' unless %i[binary text].include?(write_type)
50
51
@websocket = websocket
52
@read_type = read_type
53
@write_type = write_type
54
@mutex = Mutex.new
55
56
# beware of: https://github.com/rapid7/rex-socket/issues/32
57
_, localhost, localport = websocket.getlocalname
58
_, peerhost, peerport = Rex::Socket.from_sockaddr(websocket.getpeername)
59
@params = Rex::Socket::Parameters.from_hash({
60
'LocalHost' => localhost,
61
'LocalPort' => localport,
62
'PeerHost' => peerhost,
63
'PeerPort' => peerport,
64
'SSL' => websocket.respond_to?(:sslctx) && !websocket.sslctx.nil?
65
})
66
67
initialize_abstraction
68
69
lsock.initinfo(Rex::Socket.to_authority(peerhost, peerport), Rex::Socket.to_authority(localhost, localport))
70
71
@thread = Rex::ThreadFactory.spawn("WebSocketChannel(#{localhost}->#{peerhost})", false) do
72
websocket.wsloop do |data, data_type|
73
next unless @read_type.nil? || data_type == @read_type
74
75
data = on_data_read(data, data_type)
76
next if data.nil?
77
78
rsock.syswrite(data)
79
end
80
81
close
82
end
83
84
lsock.extend(SocketInterface)
85
lsock.channel = self
86
87
rsock.extend(SocketInterface)
88
rsock.channel = self
89
end
90
91
def closed?
92
@websocket.nil?
93
end
94
95
def close
96
@mutex.synchronize do
97
return if closed?
98
99
@websocket.wsclose
100
@websocket = nil
101
end
102
103
cleanup_abstraction
104
end
105
106
#
107
# Close the channel for write operations. This sends a CONNECTION_CLOSE request, after which (per RFC 6455 section
108
# 5.5.1) this side must not send any more data frames.
109
#
110
def close_write
111
if closed?
112
raise IOError, 'Channel has been closed.', caller
113
end
114
115
@websocket.put_wsframe(Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE }))
116
end
117
118
#
119
# Write *buf* to the channel, optionally truncating it to *length* bytes.
120
#
121
# @param [String] buf The data to write to the channel.
122
# @param [Integer] length An optional length to truncate *data* to before
123
# sending it.
124
def write(buf, length = nil)
125
if closed?
126
raise IOError, 'Channel has been closed.', caller
127
end
128
129
if !length.nil? && buf.length >= length
130
buf = buf[0..length]
131
end
132
133
length = buf.length
134
buf = on_data_write(buf)
135
if @write_type == :binary
136
@websocket.put_wsbinary(buf)
137
elsif @write_type == :text
138
@websocket.put_wstext(buf)
139
end
140
141
length
142
end
143
144
#
145
# This provides a hook point that is called when data is read from the WebSocket peer. Subclasses can intercept and
146
# process the data. The default functionality does nothing.
147
#
148
# @param [String] data the data that was read
149
# @param [Symbol] data_type the type of data that was received, either :binary or :text
150
# @return [String, nil] if a string is returned, it's passed through the channel
151
def on_data_read(data, _data_type)
152
data
153
end
154
155
#
156
# This provides a hook point that is called when data is written to the WebSocket peer. Subclasses can intercept and
157
# process the data. The default functionality does nothing.
158
#
159
# @param [String] data the data that is being written
160
# @return [String, nil] if a string is returned, it's passed through the channel
161
def on_data_write(data)
162
data
163
end
164
end
165
166
#
167
# Send a WebSocket::Frame to the peer.
168
#
169
# @param [WebSocket::Frame] frame the frame to send to the peer.
170
def put_wsframe(frame, opts = {})
171
put(frame.to_binary_s, opts = opts)
172
end
173
174
#
175
# Build a WebSocket::Frame representing the binary data and send it to the peer.
176
#
177
# @param [String] value the binary value to use as the frame payload.
178
def put_wsbinary(value, opts = {})
179
put_wsframe(Frame.from_binary(value), opts = opts)
180
end
181
182
#
183
# Build a WebSocket::Frame representing the text data and send it to the peer.
184
#
185
# @param [String] value the binary value to use as the frame payload.
186
def put_wstext(value, opts = {})
187
put_wsframe(Frame.from_text(value), opts = opts)
188
end
189
190
#
191
# Read a WebSocket::Frame from the peer.
192
#
193
# @return [Nil, WebSocket::Frame] the frame that was received from the peer.
194
def get_wsframe(_opts = {})
195
frame = Frame.new
196
frame.header.read(self)
197
payload_data = ''
198
while payload_data.length < frame.payload_len
199
chunk = read(frame.payload_len - payload_data.length)
200
if chunk.empty? # no partial reads!
201
elog('WebSocket::Interface#get_wsframe: received an empty websocket payload data chunk')
202
return nil
203
end
204
205
payload_data << chunk
206
end
207
frame.payload_data.assign(payload_data)
208
frame
209
rescue ::IOError
210
wlog('WebSocket::Interface#get_wsframe: encountered an IOError while reading a websocket frame')
211
nil
212
end
213
214
#
215
# Build a channel to allow reading and writing from the WebSocket. This provides high level functionality so the
216
# caller needn't worry about individual frames.
217
#
218
# @return [WebSocket::Interface::Channel]
219
def to_wschannel(**kwargs)
220
Channel.new(self, **kwargs)
221
end
222
223
#
224
# Close the WebSocket. If the underlying TCP socket is still active a WebSocket CONNECTION_CLOSE request will be sent
225
# and then it will wait for a CONNECTION_CLOSE response. Once completed the underlying TCP socket will be closed.
226
#
227
def wsclose(opts = {})
228
return if closed? # there's nothing to do if the underlying TCP socket has already been closed
229
230
# this implementation doesn't handle the optional close reasons at all
231
frame = Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE })
232
# close frames must be masked
233
# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1
234
frame.mask!
235
put_wsframe(frame, opts = opts)
236
while (frame = get_wsframe(opts))
237
break if frame.nil?
238
break if frame.header.opcode == Opcode::CONNECTION_CLOSE
239
# all other frames are dropped after our connection close request is sent
240
end
241
242
close # close the underlying TCP socket
243
end
244
245
#
246
# Run a loop to handle data from the remote end of the websocket. The loop will automatically handle fragmentation
247
# unmasking payload data and ping requests. When the remote connection is closed, the loop will exit. If specified the
248
# block will be passed data chunks and their data types.
249
#
250
def wsloop(opts = {}, &block)
251
buffer = ''
252
buffer_type = nil
253
254
# since web sockets have their own tear down exchange, use a synchronization lock to ensure we aren't closed until
255
# either the remote socket is closed or the teardown takes place
256
@wsstream_lock = Rex::ReadWriteLock.new
257
@wsstream_lock.synchronize_read do
258
while (frame = get_wsframe(opts))
259
frame.unmask! if frame.header.masked == 1
260
261
case frame.header.opcode
262
when Opcode::CONNECTION_CLOSE
263
put_wsframe(Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE }).tap { |f| f.mask! }, opts = opts)
264
break
265
when Opcode::CONTINUATION
266
# a continuation frame can only be sent for a data frames
267
# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4
268
raise WebSocketError, 'Received an unexpected continuation frame (uninitialized buffer)' if buffer_type.nil?
269
270
buffer << frame.payload_data
271
when Opcode::BINARY
272
raise WebSocketError, 'Received an unexpected binary frame (incomplete buffer)' unless buffer_type.nil?
273
274
buffer = frame.payload_data
275
buffer_type = :binary
276
when Opcode::TEXT
277
raise WebSocketError, 'Received an unexpected text frame (incomplete buffer)' unless buffer_type.nil?
278
279
buffer = frame.payload_data
280
buffer_type = :text
281
when Opcode::PING
282
# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2
283
put_wsframe(frame.dup.tap { |f| f.header.opcode = Opcode::PONG }, opts = opts)
284
end
285
286
next unless frame.header.fin == 1
287
288
if block_given?
289
# text data is UTF-8 encoded
290
# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
291
buffer.force_encoding('UTF-8') if buffer_type == :text
292
# release the stream lock before entering the callback, allowing it to close the socket if desired
293
@wsstream_lock.unlock_read
294
begin
295
block.call(buffer, buffer_type)
296
ensure
297
@wsstream_lock.lock_read
298
end
299
end
300
301
buffer = ''
302
buffer_type = nil
303
end
304
end
305
306
close
307
end
308
309
def close
310
# if #wsloop was ever called, a synchronization lock will have been initialized
311
@wsstream_lock.lock_write unless @wsstream_lock.nil?
312
begin
313
super
314
ensure
315
@wsstream_lock.unlock_write unless @wsstream_lock.nil?
316
end
317
end
318
end
319
320
class Opcode < BinData::Bit4
321
CONTINUATION = 0
322
TEXT = 1
323
BINARY = 2
324
CONNECTION_CLOSE = 8
325
PING = 9
326
PONG = 10
327
328
default_parameter assert: -> { !Opcode.name(value).nil? }
329
330
def self.name(value)
331
constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value }
332
end
333
334
def to_sym
335
self.class.name(value)
336
end
337
end
338
339
class Frame < BinData::Record
340
endian :big
341
342
struct :header do
343
endian :big
344
hide :rsv1, :rsv2, :rsv3
345
346
bit1 :fin, initial_value: 1
347
bit1 :rsv1
348
bit1 :rsv2
349
bit1 :rsv3
350
opcode :opcode
351
bit1 :masked
352
bit7 :payload_len_sm
353
uint16 :payload_len_md, onlyif: -> { payload_len_sm == 126 }
354
uint64 :payload_len_lg, onlyif: -> { payload_len_sm == 127 }
355
uint32 :masking_key, onlyif: -> { masked == 1 }
356
end
357
string :payload_data, read_length: -> { payload_len }
358
359
class << self
360
private
361
362
def from_opcode(opcode, payload, last: true, mask: true)
363
frame = Frame.new(header: { fin: (last ? 1 : 0), opcode: opcode })
364
frame.payload_len = payload.length
365
frame.payload_data = payload
366
367
case mask
368
when TrueClass
369
frame.mask!
370
when Integer
371
frame.mask!(mask)
372
when FalseClass
373
else
374
raise ArgumentError, 'mask must be true, false or an integer (literal masking key)'
375
end
376
377
frame
378
end
379
end
380
381
def self.apply_masking_key(data, mask)
382
mask = [mask].pack('N').each_byte.to_a
383
xored = ''
384
data.each_byte.each_with_index do |byte, index|
385
xored << (byte ^ mask[index % 4]).chr
386
end
387
388
xored
389
end
390
391
def self.from_binary(value, last: true, mask: true)
392
from_opcode(Opcode::BINARY, value, last: last, mask: mask)
393
end
394
395
def self.from_text(value, last: true, mask: true)
396
from_opcode(Opcode::TEXT, value, last: last, mask: mask)
397
end
398
399
#
400
# Update the frame instance in place to apply a masking key to the payload data as defined in RFC 6455 section 5.3.
401
#
402
# @param [nil, Integer] key either an explicit 32-bit masking key or nil to generate a random one
403
# @return [String] the masked payload data is returned
404
def mask!(key = nil)
405
header.masked.assign(1)
406
key = rand(1..0xffffffff) if key.nil?
407
header.masking_key.assign(key)
408
payload_data.assign(self.class.apply_masking_key(payload_data, header.masking_key))
409
payload_data.value
410
end
411
412
#
413
# Update the frame instance in place to apply a masking key to the payload data as defined in RFC 6455 section 5.3.
414
#
415
# @return [String] the unmasked payload data is returned
416
def unmask!
417
payload_data.assign(self.class.apply_masking_key(payload_data, header.masking_key))
418
header.masked.assign(0)
419
payload_data.value
420
end
421
422
def payload_len
423
case header.payload_len_sm
424
when 127
425
header.payload_len_lg
426
when 126
427
header.payload_len_md
428
else
429
header.payload_len_sm
430
end
431
end
432
433
def payload_len=(value)
434
if value < 126
435
header.payload_len_sm.assign(value)
436
elsif value < 0xffff
437
header.payload_len_sm.assign(126)
438
header.payload_len_md.assign(value)
439
elsif value < 0x7fffffffffffffff
440
header.payload_len_sm.assign(127)
441
header.payload_len_lg.assign(value)
442
else
443
raise ArgumentError, 'payload length is outside the acceptable range'
444
end
445
end
446
end
447
end
448
449