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/packet.rb
Views: 11704
# -*- coding: binary -*-123module Rex4module Proto5module Http67DefaultProtocol = '1.1'89###10#11# This class represents an HTTP packet.12#13###14class Packet1516#17# Parser processing codes18#19module ParseCode20Completed = 121Partial = 222Error = 323end2425#26# Parser states27#28module ParseState29ProcessingHeader = 130ProcessingBody = 231Completed = 332end333435#36# Initializes an instance of an HTTP packet.37#38def initialize()39self.headers = Header.new40self.auto_cl = true4142reset43end4445#46# Return the associated header value, if any.47#48def [](key)49if (self.headers.include?(key))50return self.headers[key]51end5253self.headers.each_pair do |k,v|54if (k.downcase == key.downcase)55return v56end57end5859return nil60end6162#63# Set the associated header value.64#65def []=(key, value)66self.headers[key] = value67end6869#70# Parses the supplied buffer. Returns one of the two parser processing71# codes (Completed, Partial, or Error).72#73# @param [String] buf The buffer to parse; possibly not a complete request/response74# @param [Hash] opts Parsing options75# @option [Boolean] orig_method The HTTP method used in an associated request, if applicable76def parse(buf, opts={})7778# Append the incoming buffer to the buffer queue.79self.bufq += buf.to_s8081begin8283# Process the header84if(self.state == ParseState::ProcessingHeader)85parse_header(opts)86end8788# Continue on to the body if the header was processed89if(self.state == ParseState::ProcessingBody)90# Chunked encoding sets the parsing state on its own.91# HEAD requests can return immediately.92orig_method = opts.fetch(:orig_method) { '' }93if (self.body_bytes_left == 0 && (!self.transfer_chunked || orig_method == 'HEAD'))94self.state = ParseState::Completed95else96parse_body97end98end99rescue100# XXX: BUG: This rescue might be a problem because it will swallow TimeoutError101self.error = $!102return ParseCode::Error103end104105# Return completed or partial to the parsing status to the caller106(self.state == ParseState::Completed) ? ParseCode::Completed : ParseCode::Partial107end108109#110# Reset the parsing state and buffers.111#112def reset113self.state = ParseState::ProcessingHeader114self.transfer_chunked = false115self.inside_chunk = false116self.headers.reset117self.bufq = ''118self.body = ''119end120121#122# Reset the parsing state but leave the buffers.123#124def reset_except_queue125self.state = ParseState::ProcessingHeader126self.transfer_chunked = false127self.inside_chunk = false128self.headers.reset129self.body = ''130end131132#133# Returns whether or not parsing has completed.134#135def completed?136137return true if self.state == ParseState::Completed138139# If the parser state is processing the body and there are an140# undetermined number of bytes left to read, we just need to say that141# things are completed as it's hard to tell whether or not they really142# are.143if (self.state == ParseState::ProcessingBody and self.body_bytes_left < 0)144return true145end146147false148end149150#151# Build a 'Transfer-Encoding: chunked' payload with random chunk sizes152#153def chunk(str, min_size = 1, max_size = 1000)154chunked = ''155156# min chunk size is 1 byte157if (min_size < 1); min_size = 1; end158159# don't be dumb160if (max_size < min_size); max_size = min_size; end161162while (str.size > 0)163chunk = str.slice!(0, rand(max_size - min_size) + min_size)164chunked += sprintf("%x", chunk.size) + "\r\n" + chunk + "\r\n"165end166chunked += "0\r\n\r\n"167end168169#170# Outputs a readable string of the packet for terminal output171#172def to_terminal_output(headers_only: false)173output_packet(true, headers_only: headers_only)174end175176#177# Converts the packet to a string.178#179def to_s(headers_only: false)180output_packet(false, headers_only: headers_only)181end182183#184# Converts the packet to a string.185# If ignore_chunk is set the chunked encoding is omitted (for pretty print)186#187def output_packet(ignore_chunk = false, headers_only: false)188content = self.body.to_s.dup189190# Update the content length field in the header with the body length.191if (content)192if !self.compress.nil?193case self.compress194when 'gzip'195self.headers['Content-Encoding'] = 'gzip'196content = Rex::Text.gzip(content)197when 'deflate'198self.headers['Content-Encoding'] = 'deflate'199content = Rex::Text.zlib_deflate(content)200when 'none'201# this one is fine...202# when 'compress'203else204raise RuntimeError, 'Invalid Content-Encoding'205end206end207208unless ignore_chunk209if self.auto_cl && self.transfer_chunked210raise RuntimeError, "'Content-Length' and 'Transfer-Encoding: chunked' are incompatible"211end212213if self.auto_cl214self.headers['Content-Length'] = content.length215elsif self.transfer_chunked216if self.proto != '1.1'217raise RuntimeError, 'Chunked encoding is only available via 1.1'218end219self.headers['Transfer-Encoding'] = 'chunked'220content = self.chunk(content, self.chunk_min_size, self.chunk_max_size)221end222end223end224225str = self.headers.to_s(cmd_string)226str += content || '' unless headers_only227228str229end230231#232# Converts the packet from a string.233#234def from_s(str)235reset236parse(str)237end238239#240# Returns the command string, such as:241#242# HTTP/1.0 200 OK for a response243#244# or245#246# GET /foo HTTP/1.0 for a request247#248def cmd_string249self.headers.cmd_string250end251252attr_accessor :headers253attr_accessor :error254attr_accessor :state255attr_accessor :bufq256attr_accessor :body257attr_accessor :auto_cl258attr_accessor :max_data259attr_accessor :transfer_chunked260attr_accessor :compress261attr_reader :incomplete262263attr_accessor :chunk_min_size264attr_accessor :chunk_max_size265266protected267268attr_writer :incomplete269attr_accessor :body_bytes_left270attr_accessor :inside_chunk271attr_accessor :keepalive272273##274#275# Overridable methods276#277##278279#280# Allows derived classes to split apart the command string.281#282def update_cmd_parts(str)283end284285##286#287# Parse the HTTP header returned by the target server.288#289# @param [Hash] opts Parsing options290# @option [Boolean] orig_method The HTTP method used in an associated request, if applicable291##292def parse_header(opts)293294head,data = self.bufq.split(/\r?\n\r?\n/, 2)295296return if data.nil?297298self.headers.from_s(head)299self.bufq = data || ""300301# Set the content-length to -1 as a placeholder (read until EOF)302self.body_bytes_left = -1303orig_method = opts.fetch(:orig_method) { '' }304self.body_bytes_left = 0 if orig_method == 'HEAD'305306# Extract the content length if it was specified (ignoring it for HEAD requests, per RFC9110)307if (self.headers['Content-Length'] && orig_method != 'HEAD')308self.body_bytes_left = self.headers['Content-Length'].to_i309end310311# Look for a chunked transfer header312if (self.headers['Transfer-Encoding'].to_s.downcase == 'chunked')313self.transfer_chunked = true314self.auto_cl = false315end316317# Determine how to handle data when there is no length header318if (self.body_bytes_left == -1)319if (not self.transfer_chunked)320if (self.headers['Connection'].to_s.downcase.include?('keep-alive'))321# If we are using keep-alive, but have no content-length and322# no chunked transfer header, pretend this is the entire323# buffer and call it done324self.body_bytes_left = self.bufq.length325elsif (not self.headers['Content-Length'] and self.class == Rex::Proto::Http::Request)326# RFC 2616 says: "The presence of a message-body in a request327# is signaled by the inclusion of a Content-Length or328# Transfer-Encoding header field in the request's329# message-headers."330#331# So if we haven't seen either a Content-Length or a332# Transfer-Encoding header, there shouldn't be a message body.333self.body_bytes_left = 0334elsif (self.headers['Connection']&.downcase == 'upgrade' && self.headers['Upgrade']&.downcase == 'websocket')335# The server appears to be responding to a websocket request336self.body_bytes_left = 0337#else338# Otherwise we need to keep reading until EOF339end340end341end342343# Throw an error if we didnt parse the header properly344if !self.headers.cmd_string345raise RuntimeError, "Invalid command string", caller346end347348# Move the state into body processing349self.state = ParseState::ProcessingBody350351# Allow derived classes to update the parts of the command string352self.update_cmd_parts(self.headers.cmd_string)353end354355#356# Parses the body portion of the request.357#358def parse_body359# Just return if the buffer is empty360if (self.bufq.length == 0)361return362end363364# Handle chunked transfer-encoding responses365if (self.transfer_chunked and self.inside_chunk != 1 and self.bufq.length)366367# Remove any leading newlines or spaces368self.bufq.lstrip!369370# If we didn't get a newline, then this might not be the full371# length, go back and get more.372# e.g.373# first packet: "200"374# second packet: "0\r\n\r\n<html>..."375if not bufq.index("\n")376return377end378379# Extract the actual hexadecimal length value380clen = self.bufq.slice!(/^[a-fA-F0-9]+\r?\n/)381382clen.rstrip! if (clen)383384# if we happen to fall upon the end of the buffer for the next chunk len and have no data left, go get some more...385if clen.nil? and self.bufq.length == 0386return387end388389# Invalid chunk length, exit out early390if clen.nil?391self.state = ParseState::Completed392return393end394395self.body_bytes_left = clen.to_i(16)396397if (self.body_bytes_left == 0)398self.bufq.sub!(/^\r?\n/s,'')399self.state = ParseState::Completed400self.check_100401return402end403404self.inside_chunk = 1405end406407# If there are bytes remaining, slice as many as we can and append them408# to our body state.409if (self.body_bytes_left > 0)410part = self.bufq.slice!(0, self.body_bytes_left)411self.body += part412self.body_bytes_left -= part.length413# Otherwise, just read it all.414else415self.body += self.bufq416self.bufq = ''417end418419# Finish this chunk and move on to the next one420if (self.transfer_chunked and self.body_bytes_left == 0)421self.inside_chunk = 0422self.parse_body423return424end425426# If there are no more bytes left, then parsing has completed and we're427# ready to go.428if (not self.transfer_chunked and self.body_bytes_left == 0)429self.state = ParseState::Completed430self.check_100431return432end433end434435# Override this as needed436def check_100437end438439end440441end442end443end444445446