Chapter 67. HTTP servers

Index

Processing HTTP requests
Processing file uploads
Sending cookies to the client
Implementing basic authentication
Implementing digest authentication
Creating a x::http::serverauth
Invoking check_authentication()
Using both digest and basic authentication
Proxy authentication
Maximum request size limits

These classes implement a framework for HTTP servers. They are not meant to be building blocks for high speed, generic web content servers, but rather as application servers that use HTTP as a standard means of communication. Here's an example of a generic, bare-bones HTTP server. Bonus: it also handles HTTP over TLS. When started, it starts listening on two randomly chosen ports, a plain HTTP, and an encrypted HTTP, with a temporary, on the fly generated, self-signed certificate:

#include <x/fdlistener.H>
#include <x/netaddr.H>
#include <x/http/fdserver.H>
#include <x/http/fdserverimpl.H>
#include <x/http/fdtlsserver.H>
#include <x/http/fdtlsserverimpl.H>
#include <x/http/form.H>
#include <x/gnutls/session.H>
#include <x/xml/escape.H>
#include <iterator>

class listenersObj : virtual public x::obj {

public:
	x::gnutls::session::base::factory factory;

	x::fdlistener http_listener;
	x::fdlistener https_listener;

	static x::fdlistener create_listener(const char *port)
	{
		std::list<x::fd> socketlist;

		x::netaddr::create("", 0)->bind(socketlist, false);

		std::cout << "Listening on " << port << " port "
			  << socketlist.front()->getsockname()->port()
			  << std::endl;

		return x::fdlistener::create(socketlist);
	}

	static x::gnutls::session::base::factory create_factory()
	{
		std::cout << "Creating certificate" << std::endl;

		x::gnutls::credentials::certificate
			cred(x::gnutls::credentials::certificate::create());

		x::gnutls::x509::privkey
			key(x::gnutls::x509::privkey::create());

		key->generate(GNUTLS_PK_RSA, GNUTLS_SEC_PARAM_NORMAL);
		key->fix();

		x::gnutls::x509::crt cert(x::gnutls::x509::crt::create());

		time_t now=time(NULL);

		cert->set_basic_constraints(true, 1);
		cert->set_activation_time(now-60);
		cert->set_expiration_time(now + 60 * 60 * 24 * 30);
		cert->set_dn_by_oid(GNUTLS_OID_X520_COMMON_NAME, "localhost");
		cert->set_issuer_dn_by_oid(GNUTLS_OID_X520_COMMON_NAME,
					   "localhost");
		cert->set_key(key);
		cert->set_serial(1);
		cert->set_version();
		cert->sign(cert, key);

		std::list<x::gnutls::x509::crt> certchain;

		certchain.push_back(cert);

		cred->set_key(certchain, key);

		x::gnutls::session::base::factory f(x::gnutls::session::base
						    ::factory::create());

		f->credentials_set(cred);
		return f;
	}

	listenersObj()
		: factory(create_factory()),
		  http_listener(create_listener("http")),
		  https_listener(create_listener("https"))
	{
	}

	~listenersObj() noexcept {}
};

typedef x::ref<listenersObj> listeners;

template<typename impl_class>
class myserverimpl : public impl_class, virtual public x::obj {

public:
	listeners daemons;

	myserverimpl(const listeners &daemonsArg) : daemons(daemonsArg)
	{
	}

	~myserverimpl() noexcept
	{
	}

	void received(const x::http::requestimpl &req, bool hasbody)
	{
		std::pair<x::http::form::parameters, bool>
			form=this->getform(req, hasbody);

		if (!form.second && hasbody)
		{
			impl_class::received_unknown_request(req, hasbody);
			// Throws an exception.
			return;
		}

		std::stringstream o;

		o << "<html><head><title>Your request</title></head><body>";

		bool stop=form.first->find("stop") != form.first->end();

		if (stop)
		{
			o << "<h1>Goodbye</h1>";
		}
		else
		{
			o << "<h1>Your request headers:</h1>"
				"<p>URI: "
			  << x::xml::escapestr(x::tostring(req.getURI()))
			  << "</p><table><thead><tr><th>Header</th><th>Contents</th></tr></thead><tbody>";

			for (auto hdr:req)
			{
				o << "<tr><td>" << x::xml::escapestr(hdr.first)
				  << "</td><td>"
				  << x::xml::escapestr(hdr.second.begin(),
						       hdr.second.end())
				  << "</td></tr>";
			}
			o << "</tbody></table>"
			  << "<h1>Your form parameters</h1>"
				"<table><thead><tr><th>Parameter</th><th>Value</th></tr></thead><tbody>";

			for (auto p:*form.first)
			{
				o << "<tr><td>" << x::xml::escapestr(p.first)
				  << "</td><td>"
				  << x::xml::escapestr(p.second)
				  << "</td></tr>";
			}
			o << "</tbody></table><h2>Submit a form, and see what happens:</h2><form method='post'><table><tbody><tr><td>Text input:</td><td><input type=text name=textfield /></td></tr>"
				"<tr><td>Checkbox:</td><td><input type=checkbox name=yesno /></td></tr>"
				"<tr><td colspan=2><input type=submit name=submit value='Submit Form' /></td></tr>"
				"<tr><td colspan=2><hr /></td></tr><tr><td colspan=2><input type=submit name=stop value='Stop server' /></td></tr></tbody></table>";
		}

		o << "</body></html>";

		this->send(req, 
			   "text/html; charset=utf-8",
			   std::istreambuf_iterator<char>(o.rdbuf()),
			   std::istreambuf_iterator<char>());

		if (stop)
		{
			daemons->http_listener->stop();
			daemons->https_listener->stop();
		}
	}
};

template<typename server_type>
class myhttpserver : virtual public x::obj {

public:
	listeners daemons;

	myhttpserver(const listeners &daemonsArg) : daemons(daemonsArg)
	{
	}

	~myhttpserver() noexcept {}

	server_type create()
	{
		return server_type::create(daemons);
	}
};

typedef x::ref<myhttpserver< x::ref<myserverimpl<x::http::fdserverimpl> >
			     > > http_instance;

typedef x::ref<myhttpserver< x::ref<myserverimpl<x::gnutls::http
						 ::fdtlsserverimpl> >
			     > > https_instance;

int main()
{
	try {
		auto daemons(listeners::create());

		daemons->http_listener
			->start(x::http::fdserver::create(),
				http_instance::create(daemons));

		daemons->https_listener
			->start(x::gnutls::http::fdtlsserver::create(),
				daemons->factory,
				https_instance::create(daemons));

		daemons->http_listener->wait();
		daemons->https_listener->wait();

	} catch (const x::exception &e)
	{
		std::cerr << e << std::endl;
		exit(1);
	}
	return 0;
}

x::http::fdserverObj and x::gnutls::http::fdtlsserverObj implement the appropriate run() method that makes them directly usable with x::fdlistenerObj. x::http::fdserverObj instantiates an HTTP server, x::gnutls::http::fdtlsserverObj instantiates an HTTP over TLS server.

The above example instantiates a separate subclass for both of them. The general approach is:

The above example uses templates to instantiate both the x::http::fdserverObj/x::http::fdserverimpl and the x::gnutls::http::fdtlsserverObj/x::gnutls::http::fdtlsserverimpl implementions from a single code base, resulting in an identical server on both the plain and the encrypted HTTP port.

A new instance of the server class implementation object gets instantiated for each connection. The implementation object's received() method gets invoked when the thread receives the HTTP request from the client. Clients that support persistent HTTP 1.1 connections may send multiple requests. After received() returns, it may get called against in the same thread and for the same implementation object, upon receiving the next request from the client over the same persistent connection. However, this is not to be relied upon, since clients may employ multiple connections, which will use different threads.

Processing HTTP requests

class myserverimpl : public x::http::fdserverimpl, virtual public x::obj {

public:

// ...

	void received(const x::http::requestimpl &req, bool hasbody)
	{
// ...

		send(req, 
		     "text/html; charset=utf-8",
                     container.begin(),
                     container.end());
        }
};

received(), in a subclass of a x::http::fdserverimpl or a x::gnutls::http::fdtlsserverimpl, processes an HTTP request. It receives an instance of x::http::requestimpl and a flag indicating whether the request included content. Before it returns, received() must invoke send() exactly once, specifying the response to the request.

The content of an HTTP request is obtained by invoking begin() and end():

void received(const x::http::requestimpl &req, bool hasbody)
{
    if (hasbody)
    {
        iterator b=begin(), e=end();

// ...

begin() and end() define an input sequence over the request content. They may be invoked only once. They read from the underlying network connection, which may experience delays.

For the most common use case of HTTP content consisting of form input, getform() provides a convenient way to parse it.

void received(const x::http::requestimpl &req, bool hasbody)
{
    std::pair<x::http::form::parameters, bool>
        form=getform(req, hasbody);

    if (form.second)
        hasbody=false;

//...

getform() returns a reference to a form parameter object, and a flag. getform() sets the flag to true if it processed the contents of the request, so its content has been read and is no longer available. A false is not necessarily an indication that the request is not a form submission. An HTTP GET request places form parameters in the query string portion of its URI, which getform() retrieves.

This getform() example does not process file uploads, they will throw an exception that results in a 404 not found response. See the section called “Processing file uploads” for a version of getform() that handles file uploads.

Most applications will want to check req.getMethod() to obtain the type of the request (x::http::GET, x::http::POST, x::http::HEAD, and others), first, before processing response. The headers of the HTTP request are in req. They are generally as they were received from the client. For convenience, the Host header, if present, is removed and incorporated into the URI returned by getURI().

The heavily-overloaded send() sends the response to the HTTP request, and must be called exactly once, before received() terminates. Its parameters are:

  • The original request, a three digit HTTP status code, and a brief reason phrase. This is used to send canned error messages, like 404, that do not require any additional headers:

    send(req, 404, "Not found");
  • The original request, and a x::http::response_exception object. This is typically used to handle thrown standard exceptions:

    // ...
    catch (const x::http::response_exception &e)
    {
        send(req, e);
    }

    This would handle the canned exceptions thrown by the static member methods in x::http::responseimpl.

  • The original request, a content type string, and additional arguments described below. This formats an HTTP 200 response:

    send(req, "text/plain; charset=utf-8", buffer);
  • A reference to a x::http::responseimpl object, the original request, and additional arguments described below. This gives complete control over the response message:

    x::http::responseimpl resp(307, "Temporarily redirected");
    
    resp.append("Location", "http://example.com");
    
    send(resp, req, buffer);

    Note

    The response object gets passed by reference, and may be modified during sending. send() typically adds headers such as Content-Length, or Transfer-Encoding, depending on the response.

The latter two versions of send() sends content of the response, specified either as a container, or as a beginning iterator and an ending iterator. The response to a HEAD should be prepared as if it was to a GET or a POST. send() sets the HTTP protocol headers based on the container or the iterators, but will not actually send the content.