Path: blob/master/lib/rex/proto/http/web_socket.rb
32004 views
# -*- 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)46# a read type of nil will handle both binary and text frames that are received47raise ArgumentError, 'read_type must be nil, :binary or :text' unless [nil, :binary, :text].include?(read_type)48raise ArgumentError, 'write_type must be :binary or :text' unless %i[binary text].include?(write_type)4950@websocket = websocket51@read_type = read_type52@write_type = write_type53@mutex = Mutex.new5455# beware of: https://github.com/rapid7/rex-socket/issues/3256_, localhost, localport = websocket.getlocalname57_, peerhost, peerport = Rex::Socket.from_sockaddr(websocket.getpeername)58@params = Rex::Socket::Parameters.from_hash({59'LocalHost' => localhost,60'LocalPort' => localport,61'PeerHost' => peerhost,62'PeerPort' => peerport,63'SSL' => websocket.respond_to?(:sslctx) && !websocket.sslctx.nil?64})6566initialize_abstraction6768lsock.initinfo(Rex::Socket.to_authority(peerhost, peerport), Rex::Socket.to_authority(localhost, localport))6970@thread = Rex::ThreadFactory.spawn("WebSocketChannel(#{localhost}->#{peerhost})", false) do71websocket.wsloop do |data, data_type|72next unless @read_type.nil? || data_type == @read_type7374data = on_data_read(data, data_type)75next if data.nil?7677rsock.syswrite(data)78end7980close81end8283lsock.extend(SocketInterface)84lsock.channel = self8586rsock.extend(SocketInterface)87rsock.channel = self88end8990def closed?91@websocket.nil?92end9394def close95@mutex.synchronize do96return if closed?9798@websocket.wsclose99@websocket = nil100end101102cleanup_abstraction103end104105#106# Close the channel for write operations. This sends a CONNECTION_CLOSE request, after which (per RFC 6455 section107# 5.5.1) this side must not send any more data frames.108#109def close_write110if closed?111raise IOError, 'Channel has been closed.', caller112end113114@websocket.put_wsframe(Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE }))115end116117#118# Write *buf* to the channel, optionally truncating it to *length* bytes.119#120# @param [String] buf The data to write to the channel.121# @param [Integer] length An optional length to truncate *data* to before122# sending it.123def write(buf, length = nil)124if closed?125raise IOError, 'Channel has been closed.', caller126end127128if !length.nil? && buf.length >= length129buf = buf[0..length]130end131132length = buf.length133buf = on_data_write(buf)134if @write_type == :binary135@websocket.put_wsbinary(buf)136elsif @write_type == :text137@websocket.put_wstext(buf)138end139140length141end142143#144# This provides a hook point that is called when data is read from the WebSocket peer. Subclasses can intercept and145# process the data. The default functionality does nothing.146#147# @param [String] data the data that was read148# @param [Symbol] data_type the type of data that was received, either :binary or :text149# @return [String, nil] if a string is returned, it's passed through the channel150def on_data_read(data, _data_type)151data152end153154#155# This provides a hook point that is called when data is written to the WebSocket peer. Subclasses can intercept and156# process the data. The default functionality does nothing.157#158# @param [String] data the data that is being written159# @return [String, nil] if a string is returned, it's passed through the channel160def on_data_write(data)161data162end163end164165#166# Send a WebSocket::Frame to the peer.167#168# @param [WebSocket::Frame] frame the frame to send to the peer.169def put_wsframe(frame, opts = {})170put(frame.to_binary_s, opts = opts)171end172173#174# Build a WebSocket::Frame representing the binary data and send it to the peer.175#176# @param [String] value the binary value to use as the frame payload.177def put_wsbinary(value, opts = {})178put_wsframe(Frame.from_binary(value), opts = opts)179end180181#182# Build a WebSocket::Frame representing the text data and send it to the peer.183#184# @param [String] value the binary value to use as the frame payload.185def put_wstext(value, opts = {})186put_wsframe(Frame.from_text(value), opts = opts)187end188189#190# Read a WebSocket::Frame from the peer.191#192# @return [Nil, WebSocket::Frame] the frame that was received from the peer.193def get_wsframe(_opts = {})194frame = Frame.new195frame.header.read(self)196payload_data = ''197while payload_data.length < frame.payload_len198chunk = read(frame.payload_len - payload_data.length)199if chunk.empty? # no partial reads!200elog('WebSocket::Interface#get_wsframe: received an empty websocket payload data chunk')201return nil202end203204payload_data << chunk205end206frame.payload_data.assign(payload_data)207frame208rescue ::IOError209wlog('WebSocket::Interface#get_wsframe: encountered an IOError while reading a websocket frame')210nil211end212213#214# Build a channel to allow reading and writing from the WebSocket. This provides high level functionality so the215# caller needn't worry about individual frames.216#217# @return [WebSocket::Interface::Channel]218def to_wschannel(**kwargs)219Channel.new(self, **kwargs)220end221222#223# Close the WebSocket. If the underlying TCP socket is still active a WebSocket CONNECTION_CLOSE request will be sent224# and then it will wait for a CONNECTION_CLOSE response. Once completed the underlying TCP socket will be closed.225#226def wsclose(opts = {})227return if closed? # there's nothing to do if the underlying TCP socket has already been closed228229# this implementation doesn't handle the optional close reasons at all230frame = Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE })231# close frames must be masked232# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1233frame.mask!234put_wsframe(frame, opts = opts)235while (frame = get_wsframe(opts))236break if frame.nil?237break if frame.header.opcode == Opcode::CONNECTION_CLOSE238# all other frames are dropped after our connection close request is sent239end240241close # close the underlying TCP socket242end243244#245# Run a loop to handle data from the remote end of the websocket. The loop will automatically handle fragmentation246# unmasking payload data and ping requests. When the remote connection is closed, the loop will exit. If specified the247# block will be passed data chunks and their data types.248#249def wsloop(opts = {}, &block)250buffer = ''251buffer_type = nil252253# since web sockets have their own tear down exchange, use a synchronization lock to ensure we aren't closed until254# either the remote socket is closed or the teardown takes place255@wsstream_lock = Rex::ReadWriteLock.new256@wsstream_lock.synchronize_read do257while (frame = get_wsframe(opts))258frame.unmask! if frame.header.masked == 1259260case frame.header.opcode261when Opcode::CONNECTION_CLOSE262put_wsframe(Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE }).tap { |f| f.mask! }, opts = opts)263break264when Opcode::CONTINUATION265# a continuation frame can only be sent for a data frames266# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4267raise WebSocketError, 'Received an unexpected continuation frame (uninitialized buffer)' if buffer_type.nil?268269buffer << frame.payload_data270when Opcode::BINARY271raise WebSocketError, 'Received an unexpected binary frame (incomplete buffer)' unless buffer_type.nil?272273buffer = frame.payload_data274buffer_type = :binary275when Opcode::TEXT276raise WebSocketError, 'Received an unexpected text frame (incomplete buffer)' unless buffer_type.nil?277278buffer = frame.payload_data279buffer_type = :text280when Opcode::PING281# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2282put_wsframe(frame.dup.tap { |f| f.header.opcode = Opcode::PONG }, opts = opts)283end284285next unless frame.header.fin == 1286287if block_given?288# text data is UTF-8 encoded289# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6290buffer.force_encoding('UTF-8') if buffer_type == :text291# release the stream lock before entering the callback, allowing it to close the socket if desired292@wsstream_lock.unlock_read293begin294block.call(buffer, buffer_type)295ensure296@wsstream_lock.lock_read297end298end299300buffer = ''301buffer_type = nil302end303end304305close306end307308def close309# if #wsloop was ever called, a synchronization lock will have been initialized310@wsstream_lock.lock_write unless @wsstream_lock.nil?311begin312super313ensure314@wsstream_lock.unlock_write unless @wsstream_lock.nil?315end316end317end318319class Opcode < BinData::Bit4320CONTINUATION = 0321TEXT = 1322BINARY = 2323CONNECTION_CLOSE = 8324PING = 9325PONG = 10326327default_parameter assert: -> { !Opcode.name(value).nil? }328329def self.name(value)330constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value }331end332333def to_sym334self.class.name(value)335end336end337338class Frame < BinData::Record339endian :big340341struct :header do342endian :big343hide :rsv1, :rsv2, :rsv3344345bit1 :fin, initial_value: 1346bit1 :rsv1347bit1 :rsv2348bit1 :rsv3349opcode :opcode350bit1 :masked351bit7 :payload_len_sm352uint16 :payload_len_md, onlyif: -> { payload_len_sm == 126 }353uint64 :payload_len_lg, onlyif: -> { payload_len_sm == 127 }354uint32 :masking_key, onlyif: -> { masked == 1 }355end356string :payload_data, read_length: -> { payload_len }357358class << self359private360361def from_opcode(opcode, payload, last: true, mask: true)362frame = Frame.new(header: { fin: (last ? 1 : 0), opcode: opcode })363frame.payload_len = payload.length364frame.payload_data = payload365366case mask367when TrueClass368frame.mask!369when Integer370frame.mask!(mask)371when FalseClass372else373raise ArgumentError, 'mask must be true, false or an integer (literal masking key)'374end375376frame377end378end379380def self.apply_masking_key(data, mask)381mask = [mask].pack('N').each_byte.to_a382xored = ''383data.each_byte.each_with_index do |byte, index|384xored << (byte ^ mask[index % 4]).chr385end386387xored388end389390def self.from_binary(value, last: true, mask: true)391from_opcode(Opcode::BINARY, value, last: last, mask: mask)392end393394def self.from_text(value, last: true, mask: true)395from_opcode(Opcode::TEXT, value, last: last, mask: mask)396end397398#399# Update the frame instance in place to apply a masking key to the payload data as defined in RFC 6455 section 5.3.400#401# @param [nil, Integer] key either an explicit 32-bit masking key or nil to generate a random one402# @return [String] the masked payload data is returned403def mask!(key = nil)404header.masked.assign(1)405key = rand(1..0xffffffff) if key.nil?406header.masking_key.assign(key)407payload_data.assign(self.class.apply_masking_key(payload_data, header.masking_key))408payload_data.value409end410411#412# Update the frame instance in place to apply a masking key to the payload data as defined in RFC 6455 section 5.3.413#414# @return [String] the unmasked payload data is returned415def unmask!416payload_data.assign(self.class.apply_masking_key(payload_data, header.masking_key))417header.masked.assign(0)418payload_data.value419end420421def payload_len422case header.payload_len_sm423when 127424header.payload_len_lg425when 126426header.payload_len_md427else428header.payload_len_sm429end430end431432def payload_len=(value)433if value < 126434header.payload_len_sm.assign(value)435elsif value < 0xffff436header.payload_len_sm.assign(126)437header.payload_len_md.assign(value)438elsif value < 0x7fffffffffffffff439header.payload_len_sm.assign(127)440header.payload_len_lg.assign(value)441else442raise ArgumentError, 'payload length is outside the acceptable range'443end444end445end446end447448449