Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/rex/proto/http/web_socket.rb
Views: 11704
# -*- coding: binary -*-12require 'bindata'3require 'rex/post/channel'45module Rex::Proto::Http::WebSocket6class WebSocketError < StandardError7end89class ConnectionError < WebSocketError10def initialize(msg: 'The WebSocket connection failed', http_response: nil)11@message = msg12@http_response = http_response13end1415attr_accessor :message, :http_response16alias to_s message17end1819# This defines the interface that the standard socket is extended with to provide WebSocket functionality. It should be20# used on a socket when the server has already successfully handled a WebSocket upgrade request.21module Interface22#23# A channel object that allows reading and writing either text or binary data directly to the remote peer.24#25class Channel26include Rex::Post::Channel::StreamAbstraction2728module SocketInterface29include Rex::Post::Channel::SocketAbstraction::SocketInterface3031def type?32'tcp'33end34end3536# The socket parameters describing the underlying connection.37# @!attribute [r] params38# @return [Rex::Socket::Parameters]39attr_reader :params4041# @param [WebSocket::Interface] websocket the WebSocket that this channel is being opened on42# @param [nil, Symbol] read_type the data type(s) to read from the WebSocket, one of :binary, :text or nil (for both43# binary and text)44# @param [Symbol] write_type the data type to write to the WebSocket45def initialize(websocket, read_type: nil, write_type: :binary)46initialize_abstraction4748# a read type of nil will handle both binary and text frames that are received49raise ArgumentError, 'read_type must be nil, :binary or :text' unless [nil, :binary, :text].include?(read_type)50raise ArgumentError, 'write_type must be :binary or :text' unless %i[binary text].include?(write_type)5152@websocket = websocket53@read_type = read_type54@write_type = write_type55@mutex = Mutex.new5657# beware of: https://github.com/rapid7/rex-socket/issues/3258_, localhost, localport = websocket.getlocalname59_, peerhost, peerport = Rex::Socket.from_sockaddr(websocket.getpeername)60@params = Rex::Socket::Parameters.from_hash({61'LocalHost' => localhost,62'LocalPort' => localport,63'PeerHost' => peerhost,64'PeerPort' => peerport,65'SSL' => websocket.respond_to?(:sslctx) && !websocket.sslctx.nil?66})6768@thread = Rex::ThreadFactory.spawn("WebSocketChannel(#{localhost}->#{peerhost})", false) do69websocket.wsloop do |data, data_type|70next unless @read_type.nil? || data_type == @read_type7172data = on_data_read(data, data_type)73next if data.nil?7475rsock.syswrite(data)76end7778close79end8081lsock.extend(SocketInterface)82lsock.channel = self8384rsock.extend(SocketInterface)85rsock.channel = self86end8788def closed?89@websocket.nil?90end9192def close93@mutex.synchronize do94return if closed?9596@websocket.wsclose97@websocket = nil98end99100cleanup_abstraction101end102103#104# Close the channel for write operations. This sends a CONNECTION_CLOSE request, after which (per RFC 6455 section105# 5.5.1) this side must not send any more data frames.106#107def close_write108if closed?109raise IOError, 'Channel has been closed.', caller110end111112@websocket.put_wsframe(Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE }))113end114115#116# Write *buf* to the channel, optionally truncating it to *length* bytes.117#118# @param [String] buf The data to write to the channel.119# @param [Integer] length An optional length to truncate *data* to before120# sending it.121def write(buf, length = nil)122if closed?123raise IOError, 'Channel has been closed.', caller124end125126if !length.nil? && buf.length >= length127buf = buf[0..length]128end129130length = buf.length131buf = on_data_write(buf)132if @write_type == :binary133@websocket.put_wsbinary(buf)134elsif @write_type == :text135@websocket.put_wstext(buf)136end137138length139end140141#142# This provides a hook point that is called when data is read from the WebSocket peer. Subclasses can intercept and143# process the data. The default functionality does nothing.144#145# @param [String] data the data that was read146# @param [Symbol] data_type the type of data that was received, either :binary or :text147# @return [String, nil] if a string is returned, it's passed through the channel148def on_data_read(data, _data_type)149data150end151152#153# This provides a hook point that is called when data is written to the WebSocket peer. Subclasses can intercept and154# process the data. The default functionality does nothing.155#156# @param [String] data the data that is being written157# @return [String, nil] if a string is returned, it's passed through the channel158def on_data_write(data)159data160end161end162163#164# Send a WebSocket::Frame to the peer.165#166# @param [WebSocket::Frame] frame the frame to send to the peer.167def put_wsframe(frame, opts = {})168put(frame.to_binary_s, opts = opts)169end170171#172# Build a WebSocket::Frame representing the binary data and send it to the peer.173#174# @param [String] value the binary value to use as the frame payload.175def put_wsbinary(value, opts = {})176put_wsframe(Frame.from_binary(value), opts = opts)177end178179#180# Build a WebSocket::Frame representing the text data and send it to the peer.181#182# @param [String] value the binary value to use as the frame payload.183def put_wstext(value, opts = {})184put_wsframe(Frame.from_text(value), opts = opts)185end186187#188# Read a WebSocket::Frame from the peer.189#190# @return [Nil, WebSocket::Frame] the frame that was received from the peer.191def get_wsframe(_opts = {})192frame = Frame.new193frame.header.read(self)194payload_data = ''195while payload_data.length < frame.payload_len196chunk = read(frame.payload_len - payload_data.length)197if chunk.empty? # no partial reads!198elog('WebSocket::Interface#get_wsframe: received an empty websocket payload data chunk')199return nil200end201202payload_data << chunk203end204frame.payload_data.assign(payload_data)205frame206rescue ::IOError207wlog('WebSocket::Interface#get_wsframe: encountered an IOError while reading a websocket frame')208nil209end210211#212# Build a channel to allow reading and writing from the WebSocket. This provides high level functionality so the213# caller needn't worry about individual frames.214#215# @return [WebSocket::Interface::Channel]216def to_wschannel(**kwargs)217Channel.new(self, **kwargs)218end219220#221# Close the WebSocket. If the underlying TCP socket is still active a WebSocket CONNECTION_CLOSE request will be sent222# and then it will wait for a CONNECTION_CLOSE response. Once completed the underlying TCP socket will be closed.223#224def wsclose(opts = {})225return if closed? # there's nothing to do if the underlying TCP socket has already been closed226227# this implementation doesn't handle the optional close reasons at all228frame = Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE })229# close frames must be masked230# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1231frame.mask!232put_wsframe(frame, opts = opts)233while (frame = get_wsframe(opts))234break if frame.nil?235break if frame.header.opcode == Opcode::CONNECTION_CLOSE236# all other frames are dropped after our connection close request is sent237end238239close # close the underlying TCP socket240end241242#243# Run a loop to handle data from the remote end of the websocket. The loop will automatically handle fragmentation244# unmasking payload data and ping requests. When the remote connection is closed, the loop will exit. If specified the245# block will be passed data chunks and their data types.246#247def wsloop(opts = {}, &block)248buffer = ''249buffer_type = nil250251# since web sockets have their own tear down exchange, use a synchronization lock to ensure we aren't closed until252# either the remote socket is closed or the teardown takes place253@wsstream_lock = Rex::ReadWriteLock.new254@wsstream_lock.synchronize_read do255while (frame = get_wsframe(opts))256frame.unmask! if frame.header.masked == 1257258case frame.header.opcode259when Opcode::CONNECTION_CLOSE260put_wsframe(Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE }).tap { |f| f.mask! }, opts = opts)261break262when Opcode::CONTINUATION263# a continuation frame can only be sent for a data frames264# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4265raise WebSocketError, 'Received an unexpected continuation frame (uninitialized buffer)' if buffer_type.nil?266267buffer << frame.payload_data268when Opcode::BINARY269raise WebSocketError, 'Received an unexpected binary frame (incomplete buffer)' unless buffer_type.nil?270271buffer = frame.payload_data272buffer_type = :binary273when Opcode::TEXT274raise WebSocketError, 'Received an unexpected text frame (incomplete buffer)' unless buffer_type.nil?275276buffer = frame.payload_data277buffer_type = :text278when Opcode::PING279# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2280put_wsframe(frame.dup.tap { |f| f.header.opcode = Opcode::PONG }, opts = opts)281end282283next unless frame.header.fin == 1284285if block_given?286# text data is UTF-8 encoded287# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6288buffer.force_encoding('UTF-8') if buffer_type == :text289# release the stream lock before entering the callback, allowing it to close the socket if desired290@wsstream_lock.unlock_read291begin292block.call(buffer, buffer_type)293ensure294@wsstream_lock.lock_read295end296end297298buffer = ''299buffer_type = nil300end301end302303close304end305306def close307# if #wsloop was ever called, a synchronization lock will have been initialized308@wsstream_lock.lock_write unless @wsstream_lock.nil?309begin310super311ensure312@wsstream_lock.unlock_write unless @wsstream_lock.nil?313end314end315end316317class Opcode < BinData::Bit4318CONTINUATION = 0319TEXT = 1320BINARY = 2321CONNECTION_CLOSE = 8322PING = 9323PONG = 10324325default_parameter assert: -> { !Opcode.name(value).nil? }326327def self.name(value)328constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value }329end330331def to_sym332self.class.name(value)333end334end335336class Frame < BinData::Record337endian :big338339struct :header do340endian :big341hide :rsv1, :rsv2, :rsv3342343bit1 :fin, initial_value: 1344bit1 :rsv1345bit1 :rsv2346bit1 :rsv3347opcode :opcode348bit1 :masked349bit7 :payload_len_sm350uint16 :payload_len_md, onlyif: -> { payload_len_sm == 126 }351uint64 :payload_len_lg, onlyif: -> { payload_len_sm == 127 }352uint32 :masking_key, onlyif: -> { masked == 1 }353end354string :payload_data, read_length: -> { payload_len }355356class << self357private358359def from_opcode(opcode, payload, last: true, mask: true)360frame = Frame.new(header: { fin: (last ? 1 : 0), opcode: opcode })361frame.payload_len = payload.length362frame.payload_data = payload363364case mask365when TrueClass366frame.mask!367when Integer368frame.mask!(mask)369when FalseClass370else371raise ArgumentError, 'mask must be true, false or an integer (literal masking key)'372end373374frame375end376end377378def self.apply_masking_key(data, mask)379mask = [mask].pack('N').each_byte.to_a380xored = ''381data.each_byte.each_with_index do |byte, index|382xored << (byte ^ mask[index % 4]).chr383end384385xored386end387388def self.from_binary(value, last: true, mask: true)389from_opcode(Opcode::BINARY, value, last: last, mask: mask)390end391392def self.from_text(value, last: true, mask: true)393from_opcode(Opcode::TEXT, value, last: last, mask: mask)394end395396#397# Update the frame instance in place to apply a masking key to the payload data as defined in RFC 6455 section 5.3.398#399# @param [nil, Integer] key either an explicit 32-bit masking key or nil to generate a random one400# @return [String] the masked payload data is returned401def mask!(key = nil)402header.masked.assign(1)403key = rand(1..0xffffffff) if key.nil?404header.masking_key.assign(key)405payload_data.assign(self.class.apply_masking_key(payload_data, header.masking_key))406payload_data.value407end408409#410# Update the frame instance in place to apply a masking key to the payload data as defined in RFC 6455 section 5.3.411#412# @return [String] the unmasked payload data is returned413def unmask!414payload_data.assign(self.class.apply_masking_key(payload_data, header.masking_key))415header.masked.assign(0)416payload_data.value417end418419def payload_len420case header.payload_len_sm421when 127422header.payload_len_lg423when 126424header.payload_len_md425else426header.payload_len_sm427end428end429430def payload_len=(value)431if value < 126432header.payload_len_sm.assign(value)433elsif value < 0xffff434header.payload_len_sm.assign(126)435header.payload_len_md.assign(value)436elsif value < 0x7fffffffffffffff437header.payload_len_sm.assign(127)438header.payload_len_lg.assign(value)439else440raise ArgumentError, 'payload length is outside the acceptable range'441end442end443end444end445446447