Updates to HTTP:

* chunked transfer support
	* generic Request factory Client.request
	* runtime configuration via Client.config

Still busted:
	* Enforcement of max_data length
	* Error handling on incomplete responses


git-svn-id: file:///home/svn/incoming/trunk@2849 4d416f70-5f16-0410-b530-b9f4589650da
unstable
HD Moore 2005-09-15 23:37:38 +00:00
parent 66ae46394a
commit 63f67869de
4 changed files with 211 additions and 28 deletions

View File

@ -30,7 +30,7 @@ class Client
c.stop
end
end
#
# Initializes a GET request and returns it to the caller.
#
@ -41,6 +41,8 @@ class Client
def initialize(host, port = 80)
self.hostname = host
self.port = port.to_i
self.request_config = {}
self.client_config = {}
end
#
@ -50,6 +52,72 @@ class Client
"HTTP Client"
end
#
# Configures the Client object and the Request factory
#
def config (chash)
req_opts = %w{ user-agent vhost cookie proto }
cli_opts = %w{ max-data }
chash.each_pair { |k,v|
req_opts.include?(k) ?
self.request_config[k] = v : self.client_config[k] = v
}
end
#
# Set parameters for the Request factory
#
def request_option(k, v)
(v != nil) ? self.request_config[k] = v : self.request_config[k]
end
#
# Set parameters for the actual Client
#
def client_option(k, v)
(v != nil) ? self.client_config[k] = v : self.client_config[k]
end
#
# The Request factory
#
def request (chash)
method = chash['method']
proto = chash['proto'] || self.request_config['proto']
uri = chash['uri']
req = Rex::Proto::Http::Request.new(method, uri, proto)
#
# Configure the request headers using the Client configuration
#
if self.request_config['cookie']
req['Cookie'] = self.request_config['cookie']
end
if self.request_config['user-agent']
req['User-Agent'] = self.request_config['user-agent']
end
#
# Configure the rest of the request based on config hash
#
req['Host'] = (self.request_config['vhost'] || self.hostname) + ':' + self.port.to_s
# Set the request body if a data chunk has been specified
if chash['data']
req.body = chash['data']
end
# Set the content-type
if chash['content-type']
req['Content-Type'] = chash['content-type']
end
req
end
#
# Connects to the remote server if possible.
#
@ -78,7 +146,7 @@ class Client
# Initializes a request by setting the host header and other cool things.
#
def init_request(req)
req['Host'] = "#{hostname}:#{port}"
req['Host'] = "#{request_config.has_key?('vhost') ? request_config['vhost'] : hostname}:#{port}"
return req
end
@ -88,7 +156,8 @@ class Client
#
def send_request(req, t = -1)
resp = Response.new
resp.max_data = self.client_config['max-data']
# Connect to the server
connect
@ -119,8 +188,13 @@ class Client
# Close our side if we aren't pipelining
close if (!pipelining?)
# Returns the response to the caller
return (resp.completed?) ? resp : nil
# 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
end
#
@ -147,12 +221,14 @@ class Client
attr_accessor :pipeline
attr_accessor :local_host
attr_accessor :local_port
attr_accessor :client_config
protected
attr_accessor :hostname, :port
attr_accessor :conn
attr_accessor :request_config, :client_config
end
end

View File

@ -11,13 +11,67 @@ class Rex::Proto::Http::Client::UnitTest < Test::Unit::TestCase
def test_parse
c = Klass.new('www.google.com')
r = Rex::Proto::Http::Request::Get.new('/')
# Set request factory parameters
c.config(
'vhost' => 'www.google.com',
'user-agent' => 'Metasploit Framework/3.0',
'proto' => '1.1',
'cookie' => 'NoCookie=NotACookie'
)
# Set client parameters
c.config(
'max-data' => 1024 * 1024
)
#
# Request the main web pagfe
#
r = c.request(
'method' => 'GET',
'uri' => '/'
)
resp = c.send_request(r)
assert_equal(200, resp.code)
assert_equal('OK', resp.message)
assert_equal('1.0', resp.proto)
assert_equal('1.1', resp.proto)
#
# Request a file that does not exist
#
r = c.request(
'method' => 'GET',
'uri' => '/NoFileHere.404'
)
resp = c.send_request(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
#
c = Klass.new('beta.microsoft.com')
c.request_option('vhost', 'beta.microsoft.com')
r = c.request(
'method' => 'POST',
'uri' => '/',
'data' => 'var=val',
'content-type' => 'application/x-www-form-urlencoded'
)
resp = c.send_request(r)
assert_equal(302, resp.code)
assert_equal('Object moved', resp.message)
assert_equal('1.1', resp.proto)
end
end

View File

@ -4,7 +4,7 @@ module Rex
module Proto
module Http
DefaultProtocol = '1.0'
DefaultProtocol = '1.1'
###
#
@ -38,7 +38,7 @@ class Packet
def initialize()
self.headers = Header.new
self.auto_cl = false
self.auto_cl = true
reset
end
@ -62,6 +62,7 @@ class Packet
# codes (Completed, Partial, or Error)
#
def parse(buf)
# Append the incoming buffer to the buffer queue.
self.bufq += buf
@ -96,6 +97,8 @@ class Packet
self.state = ParseState::ProcessingHeader
self.headers.reset
self.body = ''
self.transfer_chunked = nil
self.inside_chunk = nil
end
#
@ -109,7 +112,7 @@ class Packet
# 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))
(self.body_bytes_left < 0) )
comp = true
# Or, if the parser state actually is completed, then we're good.
elsif (self.state == ParseState::Completed)
@ -160,12 +163,17 @@ class Packet
attr_accessor :bufq
attr_accessor :body
attr_accessor :auto_cl
attr_accessor :max_data
attr_reader :incomplete
protected
attr_writer :headers
attr_writer :error
attr_writer :incomplete
attr_accessor :body_bytes_left
attr_accessor :transfer_chunked
attr_accessor :inside_chunk
##
#
@ -189,9 +197,19 @@ protected
# 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.
if ((head = self.bufq.slice!(/(.+\r\n\r\n)/m)))
idx = self.bufq.index(/\r*\n\r*\n/)
if (idx == -1)
self.headers.from_s(self.bufq)
end
if (idx >= 0)
# Extract the header block
head = self.bufq.slice!(0, idx)
# Serialize the headers
self.headers.from_s(head)
@ -202,30 +220,34 @@ protected
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 of
# the connection type is close.
if ((self.body_bytes_left > 0) or
(comp_on_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
# 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
# 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
#
@ -233,6 +255,30 @@ protected
#
def parse_body
# Just return if the buffer is empty
if (self.bufq.length == 0)
return
end
# 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!(/.*\r*\n/)
clen.rstrip!
self.body_bytes_left = clen.hex
if (self.body_bytes_left == 0)
self.bufq.rstrip!
return self.state = ParseState::Completed
end
self.inside_chunk = 1
end
# If there are bytes remaining, slice as many as we can and append them
# to our body state.
if (self.body_bytes_left > 0)
@ -246,10 +292,16 @@ protected
self.bufq = ''
end
# Finish this chunk and move on to the next one
if (self.transfer_chunked and self.body_bytes_left == 0)
self.inside_chunk = 0
return self.parse_body
end
# If there are no more bytes left, then parsing has completed and we're
# ready to go.
if (self.body_bytes_left == 0)
self.state = ParseState::Completed
if (self.transfer_chunked != 1 and self.body_bytes_left <= 0)
return self.state = ParseState::Completed
end
end

View File

@ -44,7 +44,8 @@ class Request < Packet
self.method = method
self.uri = uri
self.uri_parts = {}
self.proto = proto
self.proto = proto || DefaultProtocol
end
#