From ed7b7ac6f09888031f62d6bc439aa2277f79420d Mon Sep 17 00:00:00 2001 From: HD Moore Date: Mon, 30 Nov 2009 21:15:06 +0000 Subject: [PATCH] Fixes #491 and fixes #543 by updating the HTTP stack and validating configuration options git-svn-id: file:///home/svn/framework3/trunk@7652 4d416f70-5f16-0410-b530-b9f4589650da --- lib/rex/proto/http/client.rb | 117 +++++++----------- lib/rex/proto/http/client.rb.ut.rb | 16 +-- lib/rex/proto/http/packet.rb | 185 +++++++++++------------------ 3 files changed, 118 insertions(+), 200 deletions(-) diff --git a/lib/rex/proto/http/client.rb b/lib/rex/proto/http/client.rb index 0552e07451..e92a7dc91c 100644 --- a/lib/rex/proto/http/client.rb +++ b/lib/rex/proto/http/client.rb @@ -96,8 +96,25 @@ class Client # def set_config(opts = {}) opts.each_pair do |var,val| + typ = self.config_types[var] || 'string' + + if(typ.class.to_s == 'Array') + if not typ.include?(val) + raise RuntimeError, "The specified value for #{var} is not one of the valid choices" + end + end + + if(typ == 'bool') + val = (val =~ /^(t|y|1)$/i ? true : false) + end + + if(typ == 'integer') + val = val.to_i + end + self.config[var]=val end + end # @@ -113,7 +130,7 @@ class Client c_qs = opts['query'] c_ag = opts['agent'] || config['agent'] c_cook = opts['cookie'] || config['cookie'] - c_host = opts['vhost'] || config['vhost'] + c_host = opts['vhost'] || config['vhost'] || self.host c_head = opts['headers'] || config['headers'] || {} c_rawh = opts['raw_headers']|| config['raw_headers'] || '' c_conn = opts['connection'] @@ -293,19 +310,10 @@ class Client # # Send a HTTP request to the server - # TODO: - # * Handle junk pipeline requests + # def send_request(req) - # Connect to the server connect - - # build the request - req_string = req.to_s - - # Send it on over - ret = conn.put(req_string) - - ret + conn.put(req.to_s) end # @@ -316,81 +324,38 @@ class Client resp = Response.new resp.max_data = config['read_max_data'] - # Tell the remote side if we aren't pipelining - #conn.shutdown(::Socket::SHUT_WR) if (!pipelining?) - # Wait at most t seconds for the full response to be read in. We only # do this if t was specified as a negative value indicating an infinite # wait cycle. If t were specified as nil it would indicate that no # response parsing is required. - Timeout.timeout((t < 0) ? nil : t) { - # Now, read in the response until we're good to go. - begin - if self.junk_pipeline - i = 0 - self.junk_pipeline.times { - i += 1 - rv = nil - while rv != Packet::ParseCode::Completed - if (rv == Packet::ParseCode::Error) - return - end + return resp if not t - if resp.bufq.length > 0 - rv = resp.parse('') - else - rv = resp.parse(conn.get) - end - end + Timeout.timeout((t < 0) ? nil : t) do - if resp['Connection'] == 'close' - return - end + rv = nil + while ( + rv != Packet::ParseCode::Completed and + rv != Packet::ParseCode::Error + ) + begin + buff = conn.get + rv = resp.parse( buff || '') - buf = resp.bufq - resp.reset - resp.bufq = buf - } - end - - rv = nil - if resp.bufq.length > 0 - rv = resp.parse('') - end - - if rv != Packet::ParseCode::Completed - # Keep running until we finish parsing or EOF is reached - while ((rv = resp.parse(conn.get)) != Packet::ParseCode::Completed) - # Parsing error? Raise an exception, our job is done. - if (rv == Packet::ParseCode::Error) - break - end - select(nil, nil, nil, 0.10) + # Handle unexpected disconnects + rescue ::Errno::EPIPE, ::EOFError, ::IOError + case resp.state + when Packet::ParseState::ProcessingHeader + resp = nil + when Packet::ParseState::ProcessingBody + # truncated request, good enough + resp.error = :truncated end + break end - rescue EOFError - return nil - rescue ::Timeout::Error - #$stdout.puts("timeout\n") end - } if (t) - - # Close our side if we aren't pipelining - #close if (!pipelining?) - - # if the server said stop pipelining, we listen... - if resp['Connection'] == 'close' - #close end - - # XXX - How should we handle this? - if (not resp.completed?) - # raise RuntimeError, resp.error, caller - end - - # Always return the Response object back to the client - return resp + resp end # @@ -461,7 +426,7 @@ class Client if (self.config['uri_full_url']) url = self.ssl ? "https" : "http" url << self.config['vhost'] - url << (self.port == 80) ? "" : ":#{self.port}" + url << ((self.port == 80) ? "" : ":#{self.port}") url << uri url else diff --git a/lib/rex/proto/http/client.rb.ut.rb b/lib/rex/proto/http/client.rb.ut.rb index f627ebbe3a..fbed48b176 100644 --- a/lib/rex/proto/http/client.rb.ut.rb +++ b/lib/rex/proto/http/client.rb.ut.rb @@ -26,7 +26,7 @@ class Rex::Proto::Http::Client::UnitTest < Test::Unit::TestCase ) # - # Request the main web pagfe + # Request the main web page # r = c.request_raw( 'method' => 'GET', @@ -48,12 +48,12 @@ class Rex::Proto::Http::Client::UnitTest < Test::Unit::TestCase ) resp = c.send_recv(r) - + assert_equal(404, resp.code) assert_equal('Not Found', resp.message) assert_equal('1.1', resp.proto) - - + + # # Send a POST request that results in a 302 # @@ -75,17 +75,19 @@ class Rex::Proto::Http::Client::UnitTest < Test::Unit::TestCase end def test_ssl - c = Klass.new('www.geotrust.com', 443, {}, true) - c.set_config('vhost' => 'www.geotrust.com') + c = Klass.new('www.metasploit.com', 443, {}, true) + c.set_config('vhost' => 'www.metasploit.com') r = c.request_raw( 'method' => 'GET', 'uri' => '/' ) resp = c.send_recv(r) + assert_equal(200, resp.code) assert_equal('OK', resp.message) - assert_equal('1.1', resp.proto) + assert_equal('1.0', resp.proto) c.close end end + diff --git a/lib/rex/proto/http/packet.rb b/lib/rex/proto/http/packet.rb index cc959af1a1..cba67c4389 100644 --- a/lib/rex/proto/http/packet.rb +++ b/lib/rex/proto/http/packet.rb @@ -50,13 +50,13 @@ class Packet if (self.headers.include?(key)) return self.headers[key] end - + self.headers.each_pair do |k,v| if (k.downcase == key.downcase) return v end end - + return nil end @@ -74,17 +74,17 @@ class Packet def parse(buf) # Append the incoming buffer to the buffer queue. - self.bufq += buf + self.bufq += buf.to_s begin - # If we're processing headers, do that now. - if (self.state == ParseState::ProcessingHeader) - parse_header_re + + # Process the header + if(self.state == ParseState::ProcessingHeader) + parse_header end - - # If we're processing the body (possibly after having finished - # processing headers), do that now. - if (self.state == ParseState::ProcessingBody) + + # Continue on to the body if the header was processed + if(self.state == ParseState::ProcessingBody) if (self.body_bytes_left == 0) self.state = ParseState::Completed else @@ -104,33 +104,30 @@ class Packet # Reset the parsing state and buffers. # def reset - self.bufq = '' self.state = ParseState::ProcessingHeader + self.transfer_chunked = false + self.inside_chunk = false self.headers.reset + self.bufq = '' self.body = '' - self.transfer_chunked = nil - self.inside_chunk = nil end # # Returns whether or not parsing has completed. # def completed? - comp = false + + return true if self.state == ParseState::Completed # If the parser state is processing the body and there are an # undetermined number of bytes left to read, we just need to say that # things are completed as it's hard to tell whether or not they really # are. - if ((self.state == ParseState::ProcessingBody) and - (self.body_bytes_left < 0) ) - comp = true - # Or, if the parser state actually is completed, then we're good. - elsif (self.state == ParseState::Completed) - comp = true + if (self.state == ParseState::ProcessingBody and self.body_bytes_left < 0) + return true end - return comp + false end # @@ -168,7 +165,7 @@ class Packet self.headers['Content-Encoding'] = 'deflate' content = Rex::Text.zlib_deflate(content) when 'none' - # this one is fine... + # this one is fine... # when 'compress' else raise RuntimeError, 'Invalid Content-Encoding' @@ -197,7 +194,6 @@ class Packet # def from_s(str) reset - parse(str) end @@ -207,7 +203,7 @@ class Packet # HTTP/1.0 200 OK for a response # # or - # + # # GET /foo HTTP/1.0 for a request # def cmd_string @@ -215,7 +211,7 @@ class Packet end attr_reader :headers - attr_reader :error + attr_accessor :error attr_accessor :state attr_accessor :bufq attr_accessor :body @@ -227,21 +223,21 @@ class Packet attr_accessor :chunk_min_size attr_accessor :chunk_max_size - + protected attr_writer :headers - attr_writer :error attr_writer :incomplete attr_accessor :body_bytes_left attr_accessor :inside_chunk + attr_accessor :keepalive ## # # Overridable methods # ## - + # # Allows derived classes to split apart the command string. # @@ -254,100 +250,48 @@ protected # ## - def parse_header_re - m = /(.*?)\r?\n\r?\n(.*)/smi.match(self.bufq) - if m != nil - self.headers.from_s(m[1]) - self.bufq = m[2] - - # Extract the content length, if any. - if (self.headers['Content-Length']) - self.body_bytes_left = self.headers['Content-Length'].to_i - else - self.body_bytes_left = self.bufq.length - end - - if (self.headers['Transfer-Encoding']) - self.transfer_chunked = 1 if self.headers['Transfer-Encoding'] =~ /chunked/i - end - - connection = self.headers['Connection'] - - comp_on_close = false - if (connection and connection.downcase == 'close') - comp_on_close = true - end - - # Change states to processing the body if we have a content length or - # the connection type is close. - if ((self.body_bytes_left > 0) or self.transfer_chunked) - self.state = ParseState::ProcessingBody - else - self.state = ParseState::Completed - end - else - self.headers.from_s(self.bufq) - end - - # No command string? Wack. - if (self.headers.cmd_string == nil) - raise RuntimeError, "Invalid command string", caller - end - - # Allow derived classes to update the parts of the command string - self.update_cmd_parts(self.headers.cmd_string) - end - - # - # Parses the header portion of the request. - # def parse_header - # Does the buffer queue contain the entire header? If so, parse it and - # transition to the body parsing phase. - idx = self.bufq.index(/\r?\n\r?\n/) - - if (idx and idx >= 0) - # Extract the header block - head = self.bufq.slice!(0, idx + 4) + head,data = self.bufq.split(/\r?\n\r?\n/, 2) + return if not data - # Serialize the headers - self.headers.from_s(head) + self.headers.from_s(head) + self.bufq = data || "" - # Extract the content length, if any. - if (self.headers['Content-Length']) - self.body_bytes_left = self.headers['Content-Length'].to_i + # Set the content-length to -1 as a placeholder (read until EOF) + self.body_bytes_left = -1 + + # Extract the content length if it was specified + if (self.headers['Content-Length']) + self.body_bytes_left = self.headers['Content-Length'].to_i + end + + # Look for a chunked transfer header + if (self.headers['Transfer-Encoding'].to_s.downcase == 'chunked') + self.transfer_chunked = true + end + + # Determine how to handle data when there is no length header + if(self.body_bytes_left == -1 and self.transfer_chunked != true) + if(self.headers['Connection'].to_s.downcase == 'keep-alive') + # If we are using keep-alive, but have no content-length and + # no chunked transfer header, pretend this is the entire + # buffer and call it done + self.body_bytes_left = self.bufq.length else + # Otherwise we need to keep reading until EOF self.body_bytes_left = -1 end - - if (self.headers['Transfer-Encoding']) - self.transfer_chunked = 1 if self.headers['Transfer-Encoding'] =~ /chunked/i - end - - connection = self.headers['Connection'] - comp_on_close = false - - if (connection and connection == 'close') - comp_on_close = true - end - - # Change states to processing the body if we have a content length or - # the connection type is close. - if ((self.body_bytes_left > 0) or (comp_on_close) or self.transfer_chunked) - self.state = ParseState::ProcessingBody - else - self.state = ParseState::Completed - end - else - return ParseState::ProcessingHeader end - - # No command string? Wack. - if (self.headers.cmd_string == nil) + + # Throw an error if we didnt parse the header properly + if !self.headers.cmd_string raise RuntimeError, "Invalid command string", caller end + # Move the state into body processing + self.state = ParseState::ProcessingBody + # Allow derived classes to update the parts of the command string self.update_cmd_parts(self.headers.cmd_string) end @@ -363,17 +307,23 @@ protected # Handle chunked transfer-encoding responses if (self.transfer_chunked and self.inside_chunk != 1 and self.bufq.length) - + # Remove any leading newlines or spaces self.bufq.lstrip! - + # Extract the actual hexadecimal length value clen = self.bufq.slice!(/^[a-zA-Z0-9]*\r?\n/) clen.rstrip! if (clen) # 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... - if clen == nil and self.bufq.length == 0 + if clen.nil? and self.bufq.length == 0 + return + end + + # Invalid chunk length, exit out early + if clen.nil? + self.state = ParseState::Completed return end @@ -382,9 +332,9 @@ protected if (self.body_bytes_left == 0) self.bufq.sub!(/^\r?\n/s,'') self.state = ParseState::Completed - return + return end - + self.inside_chunk = 1 end @@ -412,7 +362,7 @@ protected # ready to go. if (self.transfer_chunked != 1 and self.body_bytes_left <= 0) self.state = ParseState::Completed - return + return end end @@ -421,3 +371,4 @@ end end end end +