http server stuff
git-svn-id: file:///home/svn/incoming/trunk@2824 4d416f70-5f16-0410-b530-b9f4589650daunstable
parent
ba794cc6d8
commit
cfe5d10a48
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue