http server stuff

git-svn-id: file:///home/svn/incoming/trunk@2824 4d416f70-5f16-0410-b530-b9f4589650da
unstable
Matt Miller 2005-07-25 02:18:37 +00:00
parent ba794cc6d8
commit cfe5d10a48
5 changed files with 413 additions and 12 deletions

View File

@ -195,16 +195,34 @@ protected
# Serialize the headers
self.headers.from_s(head)
# Change states to processing the body
self.state = ParseState::ProcessingBody
# Extract the content length, if any.
if (self.headers['content-length'])
self.body_bytes_left = self.headers['content-length'].to_i
if (self.headers['Content-Length'])
self.body_bytes_left = self.headers['Content-Length'].to_i
else
self.body_bytes_left = -1
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))
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

View File

@ -1,3 +1,4 @@
require 'uri'
require 'rex/proto/http'
module Rex
@ -40,9 +41,10 @@ class Request < Packet
def initialize(method = 'GET', uri = '/', proto = DefaultProtocol)
super()
self.method = method
self.uri = uri
self.proto = proto
self.method = method
self.uri = uri
self.uri_parts = {}
self.proto = proto
end
#
@ -50,9 +52,22 @@ class Request < Packet
#
def update_cmd_parts(str)
if (md = str.match(/^(.+?)\s+(.+?)\s+HTTP\/(.+?)\r?\n?$/))
self.method = md[1]
self.uri = md[2]
self.proto = md[3]
self.method = md[1]
self.uri = URI.decode(md[2])
self.proto = md[3]
# If it has a query string, get the parts.
if ((self.uri) and (md = self.uri.match(/(.+?)\?(.*)$/)))
self.uri_parts['QueryString'] = parse_cgi_qstring(md[2])
self.uri_parts['Resource'] = md[1]
# Otherwise, just assume that the URI is equal to the resource being
# requested.
else
self.uri_parts['QueryString'] = nil
self.uri_parts['Resource'] = self.uri
end
else
raise RuntimeError, "Invalid request command string", caller
end
end
@ -63,10 +78,62 @@ class Request < Packet
"#{self.method} #{self.uri} HTTP/#{self.proto}\r\n"
end
#
# Returns the resource that is being requested.
#
def resource
self.uri_parts['Resource']
end
#
# If there were CGI parameters in the URI, this will hold a hash of each
# variable to value. If there is more than one value for a given variable,
# and array of each value is returned.
#
def qstring
self.uri_parts['QueryString']
end
attr_accessor :method
attr_accessor :uri
attr_accessor :uri_parts
attr_accessor :proto
protected
#
# Parses a CGI query string into the var/val combinations.
#
def parse_cgi_qstring(str)
qstring = {}
# Delimit on each variable
str.split(/&/).each { |vv|
var = vv
val = ''
if (md = vv.match(/(.+?)=(.+?)/))
var = md[1]
val = md[2]
end
# Add the item to the hash with logic to convert values to an array
# if so desired.
if (qstring.include?(var))
if (qstring[var].kind_of?(Array))
qstring[var] << val
else
curr = self.qstring[var]
qstring[var] = [ curr, val ]
end
else
qstring[var] = val
end
}
return qstring
end
end
end

View File

@ -54,6 +54,8 @@ class Response < Packet
self.message = md[3].gsub(/\r/, '')
self.code = md[2].to_i
self.proto = md[1]
else
raise RuntimeError, "Invalid response command string", caller
end
end

View File

@ -0,0 +1,268 @@
require 'rex/socket'
require 'rex/proto/http'
module Rex
module Proto
module Http
###
#
# ServerClient
# ------------
#
# Runtime extension of the HTTP clients that connect to the server.
#
###
module ServerClient
#
# Initialize a new request instance
#
def init_cli(server)
self.request = Request.new
self.server = server
self.keepalive = false
end
#
# Resets the parsing state
#
def reset_cli
self.request.reset
end
#
# Transmits a response and adds the appropriate headers
#
def send_response(response)
# Set the connection to close or keep-alive depending on what the client
# can support.
response['Connection'] = (keepalive) ? 'Keep-Alive' : 'close'
# Add any other standard response headers.
server.add_response_headers(response)
# Send it off.
put(response.to_s)
end
#
# The current request context
#
attr_accessor :request
#
# Boolean that indicates whether or not the connection supports keep-alive
#
attr_accessor :keepalive
#
# A reference to the server the client is associated with
#
attr_accessor :server
end
###
#
# Server
# ------
#
# Acts as an HTTP server, processing requests and dispatching them to
# registered procs.
#
###
class Server
DefaultServer = "Rex"
def initialize(port = 80, listen_host = '0.0.0.0')
self.listen_port = port
self.listen_host = listen_host
self.listener = nil
self.clients = []
self.clifds = []
self.fd2cli = {}
self.resources = {}
end
#
# Listens on the defined port and host and starts monitoring for clients.
#
def start
self.listener = Rex::Socket::TcpServer.create(
'LocalHost' => self.listen_host,
'LocalPort' => self.listen_port)
self.listener_thread = Thread.new {
monitor_listener
}
self.clients_thread = Thread.new {
monitor_clients
}
end
#
# Terminates the monitor thread and turns off the listener.
#
def stop
self.listener_thread.kill
self.clients_thread.kill
self.clients.each { |cli|
close_client(cli)
}
self.listener.close
end
#
# Closes the supplied client connection and removes it from the internal
# hashes and lists.
#
def close_client(cli)
if (cli)
self.fd2cli.delete(cli.sock)
self.clifds.delete(cli.sock)
self.clients.delete(cli)
cli.close
end
end
#
# Adds Server headers and stuff
#
def add_response_headers(resp)
resp['Server'] = DefaultServer
end
attr_accessor :listen_port, :listen_host
protected
attr_accessor :listener
attr_accessor :listener_thread, :clients_thread
attr_accessor :clients, :clifds, :fd2cli
attr_accessor :resources
#
# Monitors the listener for new connections
#
def monitor_listener
begin
sd = Rex::ThreadSafe.select([ listener.sock ])
# Accept the new client connection
if (sd[0].length > 0)
cli = listener.accept
next if (!cli)
cli.extend(ServerClient)
# Initialize the server client extension
cli.init_cli(self)
# Insert it into some lists
self.clients << cli
self.clifds << cli.sock
self.fd2cli[cli.sock] = cli
end
rescue
elog("Exception caught in HTTP server listener monitor: #{$!}")
end while true
end
#
# Monitors client connections for data
#
def monitor_clients
begin
if (clients.length == 0)
Rex::ThreadSafe::sleep(0.2)
next
end
sd = Rex::ThreadSafe.select(clifds)
sd[0].each { |fd|
process_client(self.fd2cli[fd])
}
rescue
elog("Exception caught in HTTP server clients monitor: #{$!}")
end while true
end
#
# Processes data coming in from a client
#
def process_client(cli)
begin
case cli.request.parse(cli.get)
when Packet::ParseCode::Completed
dispatch_request(cli, cli.request)
cli.reset_cli
when Packet::ParseCode::Error
close_client(cli)
end
rescue EOFError
if (cli.request.completed?)
dispatch_request(cli, cli.request)
cli.reset_cli
end
close_client(cli)
end
end
#
# Dispatches the supplied request for a given connection
#
def dispatch_request(cli, request)
# Is the client requesting keep-alive?
if ((request['Connection']) and
(request['Connection'].downcase == 'Keep-Alive'.downcase))
cli.keepalive = true
end
if (p = self.resources[request.resource])
if (p['LongCall'] == true)
Thread.new {
p['Proc'].call(cli, request)
}
else
p['Proc'].call(cli, request)
end
else
send_e404(cli, request)
end
# If keep-alive isn't enabled for this client, close the connection
if (cli.keepalive == false)
close_client(cli)
end
end
#
# Sends a 404 error to the client for a given request.
#
def send_e404(cli, request)
resp = Response::E404.new
resp.body =
"<html><head>" +
"<title>404 Not Found</title" +
"</head><body>" +
"<h1>Not found</h1>" +
"The requested URL #{request.resource} was not found on this server.<p><hr>" +
"</body></html>"
# Send the response to the client like what
cli.send_response(resp)
end
end
end
end
end

View File

@ -0,0 +1,46 @@
#!/usr/bin/ruby
$:.unshift(File.join(File.dirname(__FILE__), '..', '..', '..'))
require 'test/unit'
require 'rex/proto/http'
class Rex::Proto::Http::Server::UnitTest < Test::Unit::TestCase
ListenPort = 8090
ListenHost = '127.0.0.1'
SrvKlass = Rex::Proto::Http::Server
CliKlass = Rex::Proto::Http::Client
def test_server
begin
s = start_srv
c = CliKlass.new(ListenHost, ListenPort)
1.upto(10) {
req = Rex::Proto::Http::Request::Get.new('/')
res = c.send_request(req)
assert_not_nil(res)
assert_equal(404, res.code)
}
ensure
stop_srv
end
end
protected
def start_srv
self.srv = SrvKlass.new(ListenPort, ListenHost)
self.srv.start
self.srv
end
def stop_srv
self.srv.stop if (self.srv)
end
attr_accessor :srv
end