metasploit-framework/lib/rex/proto/http/client.rb

328 lines
6.5 KiB
Ruby

require 'rex/socket'
require 'rex/proto/http'
module Rex
module Proto
module Http
###
#
# Acts as a client to an HTTP server, sending requests and receiving
# responses. This is modeled somewhat after Net::HTTP.
#
###
class Client
include Proto
#
# Performs a block-based HTTP operation.
#
def self.start(host, &block)
c = Client.new(host)
begin
block.call(c)
ensure
c.stop
end
end
#
# Initializes a GET request and returns it to the caller.
#
def gen_get(uri = '/', proto = DefaultProtocol)
return init_request(Request::Get.new(uri, proto))
end
#
# Initializes a POST request and returns it to the caller.
#
def gen_post(uri = '/', proto = DefaultProtocol)
return init_request(Request::Post.new(uri, proto))
end
def initialize(host, port = 80, context = {}, ssl = nil)
self.hostname = host
self.port = port.to_i
self.context = context
self.ssl = ssl
self.request_config = {}
self.client_config = {}
end
#
# HTTP client.
#
def alias
"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'] || 'GET'
proto = chash['proto'] || self.request_config['proto'] || DefaultProtocol
uri = chash['uri'] || '/'
req = Rex::Proto::Http::Request.new(method, uri, proto)
# pass on the junk_pipeline config
if self.junk_pipeline
req.junk_pipeline = self.junk_pipeline
end
#
# 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.
#
def connect
# If we already have a connection and we aren't pipelining, close it.
if (self.conn)
if !pipelining?
close
else
return self.conn
end
end
self.conn = Rex::Socket::Tcp.create(
'PeerHost' => self.hostname,
'PeerPort' => self.port.to_i,
'LocalHost' => self.local_host,
'LocalPort' => self.local_port,
'Context' => self.context,
'SSL' => self.ssl
)
end
#
# Closes the connection to the remote server.
#
def close
if (self.conn)
self.conn.shutdown
self.conn.close
end
self.conn = nil
end
#
# Initializes a request by setting the host header and other cool things.
#
def init_request(req)
req['Host'] = "#{request_config.has_key?('vhost') ? request_config['vhost'] : hostname}:#{port}"
return req
end
#
# Transmits a request and reads in a response.
#
def send_request(req, t = -1)
resp = Response.new
resp.max_data = self.client_config['max-data']
# Connect to the server
connect
# build the request
req_string = req.to_s
# Send it on over
conn.put(req_string)
# 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((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)
warn "ERR : #{resp.error}"
raise RuntimeError, resp.error, caller
end
if resp.bufq.length > 0
rv = resp.parse('')
else
rv = resp.parse(conn.get)
end
end
if resp['Connection'] == 'close'
raise RuntimeError, "junk pipelined request ##{i} caused the server to close the connection", caller
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)
raise RuntimeError, resp.error, caller
end
end
end
rescue EOFError
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
end
#
# Cleans up any outstanding connections and other resources.
#
def stop
close
end
#
# Returns whether or not the conn is valid.
#
def conn?
conn != nil
end
#
# Whether or not connections should be pipelined.
#
def pipelining?
pipeline
end
#
# Whether or not pipelining is in use.
#
attr_accessor :pipeline
#
# The local host of the client.
#
attr_accessor :local_host
#
# The local port of the client.
#
attr_accessor :local_port
#
# Client configuration attributes.
#
attr_accessor :client_config
#
# The underlying connection.
#
attr_accessor :conn
#
# The calling context to pass to the socket
#
attr_accessor :context
# When parsing the request, thunk off the first response from the server, since junk
attr_accessor :junk_pipeline
protected
# https
attr_accessor :ssl
attr_accessor :hostname, :port # :nodoc:
attr_accessor :request_config, :client_config # :nodoc:
end
end
end
end