diff --git a/lib/rex/proto/http/packet.rb b/lib/rex/proto/http/packet.rb index 95f853c5f8..136a476a4a 100644 --- a/lib/rex/proto/http/packet.rb +++ b/lib/rex/proto/http/packet.rb @@ -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 diff --git a/lib/rex/proto/http/request.rb b/lib/rex/proto/http/request.rb index 2125b764e5..b2591ef6a3 100644 --- a/lib/rex/proto/http/request.rb +++ b/lib/rex/proto/http/request.rb @@ -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 diff --git a/lib/rex/proto/http/response.rb b/lib/rex/proto/http/response.rb index 76b846be0a..691e1091f8 100644 --- a/lib/rex/proto/http/response.rb +++ b/lib/rex/proto/http/response.rb @@ -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 diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index e69de29bb2..5e71db5d4b 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -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 = + "" + + "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 diff --git a/lib/rex/proto/http/server.rb.ut.rb b/lib/rex/proto/http/server.rb.ut.rb new file mode 100644 index 0000000000..cbc2fddafd --- /dev/null +++ b/lib/rex/proto/http/server.rb.ut.rb @@ -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