HTTP Server and Services

Implementing Services

The HTTP server dispatches incoming requests to services. It is the task of the service to process the request and build a reply. The service will do so by assigning a responder to the request to respond with a reply. The following example shows a simple responder, which always replies with the string "Hello World!":

class HelloResponder : public Pt::Http::Responder
{
public:
HelloResponder(Pt::Http::Service& s)
: Pt::Http::Responder(s)
{}
{}
{}
virtual void onBeginReply(const Pt::Http::Request& request, Pt::Http::Reply& reply, Pt::System::EventLoop& loop)
{
reply.body() << "Hello World!";
reply.beginSend(true);
}
virtual void onWriteReply(const Pt::Http::Request& request, Pt::Http::Reply& reply, Pt::System::EventLoop& loop)
{ }
};

Every responder has to be derived from the Pt::Http::Responder base class and four methods have to be implemented. These methods are called in the order, in which the request and reply have to be handled. First, onBeginRequest() is called, after the HTTP header of the request has been received. Responders might inspect the header fields to prepare the reply. If the request contains a body, onReadRequest() will called next. Note, that this method might be called multiple times, each time with the next chunk of the body. Once the request has been read completely, onBeginReply() is called, in which the reply should be started. The beginSend() method accepts a completion flag which indicates, whether more data has to be sent. If beginSend() is called with the completion flag set to true, the reply is considered to be finished and the responder is released. If beginSend() is called with the completion flag set to false, onWriteReply() will be called, when the server is ready to send the next chunk of body data. This allows to write large replies in chunked-encoding, which means, that the body of the reply is written in several chunks. Eventually, the responder should finish the reply by calling beginSend() with the completion flag set to true. Generally, the reply can be sent at any stage in the process, in which case no other methods of the responder will be called and the remaining request is ignored.

Specific responders are used by services to reply to a request. All services have to implement the Pt::Http::Service interface, which basically serves as a factory for responders. The next example shows how the HelloResponder is used by the HelloService:

class HelloService : public Pt::Http::Service
{
public:
HelloService()
{ }
protected:
{
return new HelloResponder(*this);
}
{
delete r;
}
};

Two factory methods have to be implemented, firstly onGetResponder() to create a responder to handle the incoming request and secondly onReleaseResponder() to release the responder, after the reply was sent. For simple cases, like this example, the Pt::Http::BasicService template can be used:

The template uses an allocator to create and destroy responders. If no allocator is specified, new/delete will be used just like in the previous example. However, the service interface is usually employed to create responders depending on the request headers. It is also possible to optimize the creation of responders, for example, responders could be cached or created and released in a specific way.

Servlets and Request Dispatch

A servlet determines, which requests are dispatched to a service. It combines a service with a mapping rule and can be added to a Pt::Http::Server. Services can be shared by multiple servlets, for example to make the same resources available under different names. The easiest servlet is Pt::Http::MapUrl, which maps requests for a specific URL to a service:

int main()
{
Pt::Http::Server server(loop, ep);
HelloService helloService;
Pt::Http::MapUrl mapHello("/hello", helloService);
server.addServlet(mapHello);
loop.run();
}

The server is set up to listen on port 80. Since all operations in the server are asynchronous, an event loop is required as well. The HelloService from the previous examples is packaged in a MapUrl servlet, which maps all requests for the resource URL "/hello". Finally, the servlet is added to the server.

To understand how custom servlets can be implemented, it is worthwhile to look at the implementation of the MapUrl servlet. All servlets have to implement the Pt::Http::Servlet interface, as shown in the following example:

class MapUrl : public Pt::Http::Servlet
{
public:
MapUrl(const std::string& url, Service& s)
: Pt::Http::Servlet(s)
, _url(url)
{}
protected:
virtual bool onRequest(const Request& request) const
{
return _url == request.url();
}
private:
std::string _url;
};

The virtual function onRequest() has to be implemented. It returns true if the request is to be dispatched to the service and false if not. In this case the request URL is compared to the mapping URL.

Secure Connections

The server can be set up to only accept secure connections, which is called the HTTPS protocol. HTTPS is built upon the Pt::Ssl module and all one has to do, is to assign a Pt::Ssl::Context to the server as shown here:

Pt::Ssl::Context& ctx = ...;
server.setSecure(ctx);

After setSecure() has been called, the server will only accept secure connections. Further usage of the HTTP server API is exactly the same as in case of normal HTTP.

Authorization

The server might request the user agent to authenticate itself, to determine whether it is authorized to access a resource. A Pt::Http::Authenticator is used on the server side, to ensure that only authorized users access a resource. It serves as a base class for all kinds of authorization schemes, most notably for the Pt::Http::BasicAuthorizer, which implements the basic authentication scheme. It needs to be subclassed further, as shown in the following example:

class AccessAuthorizer : public BasicAuthorizer
{
public:
AccessAuthorizer(const std::string& realm)
: BasicAuthorizer(realm)
{}
protected:
virtual Authorization* onAuthorizeCredentials(const Credential& cred, bool& granted)
{
// returns true if username and password are correct
granted = checkCredentials( cred.user(), cred.password() );
return 0;
}
virtual void onReleaseAuthorization(Authorization* auth)
{}
};

Two virtual functions need to be implemented. First, onAuthorizeCredentials() is called whenever user credentials have to be checked. In this example, it can immediately be determined, whether access can be granted, so a nullptr is returned and the boolean flag is set to the result of the check. The authorization can also be performed asynchronously, in which case a pointer to a Pt::Http::Authorization object has to be returned. It is the purpose of onReleaseAuthorization() to release this object, when the operation has completed.

An authorizer can be assigned to a servlet, when the servlet is constructed. It is possible to use the same authorizer for multiple servlets, to have it shared. The next example shows how a servlet is constructed to use an authorizer:

int main()
{
AccessAuthorizer auth("some-realm");
Pt::Http::Server server(loop, ep);
HelloService helloService;
Pt::Http::MapUrl mapHello("/hello", helloService, auth);
server.addServlet(mapHello);
loop.run();
}