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
unstable
HD Moore 2009-11-30 21:15:06 +00:00
parent 1189ac1dcd
commit ed7b7ac6f0
3 changed files with 118 additions and 200 deletions

View File

@ -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.
return resp if not t
Timeout.timeout((t < 0) ? nil : t) do
rv = nil
while (
rv != Packet::ParseCode::Completed and
rv != Packet::ParseCode::Error
)
begin
if self.junk_pipeline
i = 0
self.junk_pipeline.times {
i += 1
rv = nil
buff = conn.get
rv = resp.parse( buff || '')
while rv != Packet::ParseCode::Completed
if (rv == Packet::ParseCode::Error)
return
# 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
if resp.bufq.length > 0
rv = resp.parse('')
else
rv = resp.parse(conn.get)
end
end
if resp['Connection'] == 'close'
return
end
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)
end
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

View File

@ -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',
@ -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

View File

@ -74,16 +74,16 @@ 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.
# Process the header
if(self.state == ParseState::ProcessingHeader)
parse_header_re
parse_header
end
# If we're processing the body (possibly after having finished
# processing headers), do that now.
# 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
@ -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
#
@ -197,7 +194,6 @@ class Packet
#
def from_s(str)
reset
parse(str)
end
@ -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
@ -231,10 +227,10 @@ class Packet
protected
attr_writer :headers
attr_writer :error
attr_writer :incomplete
attr_accessor :body_bytes_left
attr_accessor :inside_chunk
attr_accessor :keepalive
##
#
@ -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/)
head,data = self.bufq.split(/\r?\n\r?\n/, 2)
return if not data
if (idx and idx >= 0)
# Extract the header block
head = self.bufq.slice!(0, idx + 4)
# Serialize the headers
self.headers.from_s(head)
self.bufq = data || ""
# Extract the content length, if any.
# 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
@ -373,7 +317,13 @@ protected
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
@ -421,3 +371,4 @@ end
end
end
end