Open Service Platform

OSP Web Server

Introduction

The Open Service Platform provides a built-in web server that can be extended with static pages and servlet-like request handler objects by any bundle. The web server is implemented in the following three bundles.

OSP Web Server Foundation

The OSP Web Server Foundation bundle (osp.web) provides request dispatching and browser session management. It does not contain the HTTP server itself. Instead, the actual HTTP and HTTPS servers each come in their own bundles.

OSP Web Server

The OSP Web Server bundle (osp.web.server) provides a HTTP server (based on the Poco::Net::HTTPServer class) that uses the features provided by the OSP Web Server Foundation bundle to dispatch incoming requests. Unless configured otherwise, the OSP Web Server is available on TCP port 22080 (http://localhost:22080/).

OSP Secure Web Server

The OSP Secure Web Server bundle (osp.web.server.secure) provides a SSL/TLS-based secure HTTP (HTTPS) server. Like the non-secure web server, it uses the OSP Web Server Foundation bundle to do most of the work.

Since both the non-secure and the secure web server use the OSP Web Server Foundation bundle for the actual request handling, any web pages and request handlers registered with the Web Server Foundation are visible on both web servers.

Unless configured otherwise, the OSP Secure Web Server is available on TCP port 22443 (https://localhost:22443/).

Registering Static Web Pages

The OSP Web Server Foundation bundle provides an extension point named osp.web.server.directory that can be used by a bundle to add its own static web pages and other resources (such as images and style sheets) to the server. A detailed description of the extension point can be found in the documentation for class Poco::OSP::Web::WebServerExtensionPoint.

The WebPage sample (found in OSP/samples/WebPage) shows how a bundle can add static web pages and resources to the web server. The WebPage sample bundle does not contain any code, it simply contains a HTML page (html/index.html, a style sheet (html/css/styles.css) and a few images (html/images/*), as well as an extensions.xml file. The extensions.xml file contains the extension for the osp.web.server.directory extension point:

<extensions>
    <extension point="osp.web.server.directory"
               path="/sample"
               resource="html"
               allowSpecialization="none"
               description="Sample Page"/>
</extensions>

The following attributes are supported for static resources:

  • path: The URI on the server to which the resource or handler is mapped.
  • description: User-readable description of resource or handler.
  • secure: If true, require a secure (HTTPS) connection to access the resource.
  • realm: Specify authentication realm (together with permission).
  • permission: Specify the necessary access permission for this resource.
  • session: Specify the name of the application for session-based authentication.
  • hidden: If true, path is not included by WebServerDispatcher::listVirtualPaths().
  • resource: Specify a directory within the bundle where the HTML, image and other files are located.
  • index: Specify the name of the default document (defaults to "index.html").
  • cache: If set to false, disable caching of this resource.
  • allowSpecialization: Control if and how sub directories can be mapped to resources.

The path attribute specifies the path under which the files provided by the bundle can be accessed on the server. The resource attribute specifies the directory under which the files for the web server can be found in the bundle. For example, the bundle resource html/index.html will be available on the web server under the path /sample/index.html. Using the allowSpecialization attribute, a bundle can specify whether other bundles can register themselves for subdirectories of the directory specified in path. The following values are supported:

  • none: it is impossible to map resources or request handlers to subdirectories
  • owner: only this bundle can map resources or request handlers to subdirectories
  • all: any bundle can map resources or request handlers to subdirectories

The description attribute can be used to provide a textual description of the resource to be found at the path. The path and description attributes can contain references to bundle properties (in the usual form ${property}).

If resource caching has been enabled in the OSP Web configuration (see below), resources will be kept in a cache in memory. This can significantly speed up the server, at the cost of increased memory usage. Resource directories can be excluded from caching by setting the cache attribute in the extension point to false.

Registering Request Handlers

Registering a request handler is similar to registering static resources. Instead of a path to a bundle resource, the name of a request handler factory class and the name of the library containing it must be specified. The extension point osp.web.server.requesthandler is used for that purpose.

The WebInfo sample (found in OSP/samples/WebInfo) shows how a bundle can add both static web content, as well as a request handler, to the web server.

The extensions.xml file contains the extension for both the osp.web.server.requesthandler and the osp.web.server.directory extension points:

<extensions>
    <extension point="osp.web.server.directory"
               path="/info/css"
               resource="css"
               allowSpecialization="none"
               hidden="true"/>
    <extension point="osp.web.server.directory"
               path="/info/images"
               resource="images"
               allowSpecialization="none"
               hidden="true"/>
    <extension point="osp.web.server.requesthandler"
               path="/info"
               class="InfoHandlerFactory"
               library="WebInfo"
               allowSpecialization="owner"
               description="OSP Server Information"/>
</extensions>

The following attributes are supported for request handlers:

  • path: The URI on the server to which the resource or handler is mapped.
  • pattern: A regular expression for matching requests to the handler.
  • description: User-readable description of resource or handler.
  • secure: If true, require a secure (HTTPS) connection to access the resource.
  • realm: Specify authentication realm (together with permission).
  • permission: Specify the necessary access permission for this resource.
  • authMethods: Specify the enabled authentication methods. A comma-separated list of method names (basic, session, bearer). If not specified the globally enabled authentication methods are used.
  • session: Specify the name of the application for session-based authentication.
  • hidden: If true, path is not included by WebServerDispatcher::listVirtualPaths().
  • methods: Only allow specific HTTP request methods.
  • class: The class name of the request handler factory.
  • library: The name of the shared library containing the request handler factory.
  • csrfProtection: If set to true, enable protection against CSRF (Cross-Site Request Forgery) attacks.
  • csrfTokenHeader: If CSRF protection is enabled, specifies the name of the HTTP request header containing the CSRF token. Defaults to "X-XSRF-TOKEN".

Instead of the resource attribute used with the osp.web.server.directory extension point, the osp.web.server.requesthandler extension element uses the class and library attributes to specify the name of the class and library containing the request handler factory. Despite containing code, the WebInfo bundle does not contain a bundle activator. The Web Server Foundation automatically finds and loads the class containing the request handler factory from the library in the bundle. For this to work, the shared library containing the request handler factory must provide a named manifest. The name of the manifest must be WebServer. This allows the same library to export a default (unnamed) manifest declaring the bundle activator, as well as a manifest declaring request handler factories.

The implementation of a request handler and its factory usually looks like the following example:

#include "Poco/Net/HTTPRequestHandler.h"
#include "Poco/Net/HTTPServerRequest.h"
#include "Poco/Net/HTTPServerResponse.h"
#include "Poco/OSP/Web/WebRequestHandlerFactory.h"
#include "Poco/ClassLibrary.h"

using Poco::Net::HTTPRequestHandler;
using Poco::Net::HTTPServerRequest;
using Poco::Net::HTTPServerResponse;

class MyRequestHandler: public HTTPRequestHandler
{
public:
    void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response)
    {
        response.setChunkedTransferEncoding(true);
        std::ostream& ostr = response.send();

        ostr << "<HTML><HEAD><TITLE>Sample</TITLE>"
                "</HEAD><BODY>"
                "<H1>Hello, world!</H1>"
                "</BODY></HTML>";
    }
};

class MyRequestHandlerFactory: public WebRequestHandlerFactory
{
public:
    HTTPRequestHandler* createRequestHandler(const HTTPServerRequest& request)
    {
        return new MyRequestHandler;
    }
};

POCO_BEGIN_NAMED_MANIFEST(WebServer, WebRequestHandlerFactory)
    POCO_EXPORT_CLASS(MyRequestHandlerFactory)
POCO_END_MANIFEST

Instead of a path, a regular expression can be used to match requests to request handlers, by using the pattern attribute instead of the path attribute. Note that patterns have a higher priority than virtual paths when searching for a request handler, so if a pattern matches a given request path, that will be used, even if a virtual path would also match.

It is also possible to restrict the methods a request handler supports via the methods attribute, which should contain a comma-separated list of request methods (e.g., "GET, POST"). If the given request method is not in the set of supported methods, a 405 Method Not Allowed response will be sent. The same pattern (but not virtual path) can be mapped multiple times with a different set of request methods. This means that for a given pattern, different request handlers can be used to handle GET and POST requests. Note that method names are case sensitive and HTTP uses all-uppercase method names.

Authentication and Authorization

The OSP Web Server Foundation supports user authentication and authorization based on HTTP Basic Authentication, sessions (using a session cookie) and bearer tokens. For both extension points, additional attributes, permission, realm and session can be specified. The permission attribute contains the necessary permission required to access the resource. For more information about permissions, please refer to the OSP Authorization and Authentication documentation. The optional realm attribute specifies name of the realm that is sent back to the browser in case of HTTP Basic Authentication. Detailed information about HTTP Basic Authentication can be found in RFC 2617. The optional session attribute enabled session-based authentication and specifies the application name used for obtaining the session from the Poco::OSP::Web::WebSessionManager service.

It's possible to enable only certain authentication methods, either globally, by setting the osp.web.authMethods configuration property, or using the authMethods attribute for a specific request handler.

HTTP Basic Authentication

When a permission is specified for a resource, the web server requires the browser to send user credentials with each request for such a protected resource. The user name/password combination sent by the browser (using HTTP Basic Authentication) is first validated using the Poco::OSP::Auth::AuthService. If successful, the server checks, also using the AuthService, whether the user has the necessary permission.

To protect user names and passwords, HTTP Basic authentication works best when used over an encrypted (HTTPS) connection. The secure attribute can be used to specify that a certain resource is only available over an encrypted connection.

Session-Based Authentication

For session-based authentication, the name of the application (which in turn determines the name of the session cookie) must be specified using the session attribute. See the Poco::OSP::Web::WebSessionManager class for more information regarding the application name (appName). If a valid session cookie, and a valid corresponding session are found, the username is extracted from the session's "username" property and the request is considered authenticated. For request handlers serving REST APIs or XMLHttpRequest, CSRF protection (see below) should be enabled as well if session-based authentication is enabled.

Bearer Token-Based Authentication

For authentication based on a bearer token, the token must be given in the HTTP Authorization header (Authorization: Bearer <token>). The token is then validated using the Poco::OSP::Web::TokenValidator service. The name of the token validator service must be configured using the osp.web.tokenValidatorName configuration property. If not set, token-based authentication will be disabled.

Obtaining the Authenticated User Name

If authentication and authorization was successful, a header named "X-OSP-Authorized-User" containing the name of the authenticated user is added to the Poco::Net::HTTPRequest, for use by downstream request handlers. Adding this header can be disabled by setting the osp.web.addAuthHeader configuration property to false.

Protection Against Cross-Site Request Forgery Attacks

The web server supports protection agains CSRF or XSRF (Cross-Site Request Forgery) attacks. To enable CSRF protection, the csrfProtection attribute must be set to true in the osp.web.server.requesthandler extension point. If enabled, the web server will check whether the request contains a CSRF token header (default header name is "X-XSRF-TOKEN") with a valid CSRF token. The token will be checked using the Poco::OSP::Web::WebSessionManager service. Requests will be rejected if they do not contain a valid token. The name of the header containing the token can be set with the csrfTokenHeader attribute. CSRF protection should be enabled for all REST API or XMLHttpRequest handlers that use session-based authentication.

Response Content Compression

For static content stored in bundles, the OSP Web Server Foundation supports gzip compression. If the browser or client indicates that it supports gzip content encoding, by including an appropriate Accept-Encoding header in the request, and if compression is enabled, the response content will be compressed using gzip.

Compression can be enabled for specific content types in the global application configuration file, using the osp.web.server.compressResponses and osp.web.server.compressedMediaTypes properties.

For dynamically generated content, compression has to be implemented in the request handler. The easiest way to do this is to use the following code fragment to send the response:

response.setChunkedTransferEncoding(true);
bool compressResponse(request.hasToken("Accept-Encoding", "gzip"));
if (compressResponse) response.set("Content-Encoding", "gzip");
std::ostream& realResponseStream = response.send();
Poco::DeflatingOutputStream gzipStream(responseStream, Poco::DeflatingStreamBuf::STREAM_GZIP, 1);
std::ostream& responseStream = compressResponse ? gzipStream : realResponseStream;

The POCO C++ Server Page Compiler also has support for response content compression.

Programmatic Access to The Web Server

The OSP Web Server Foundation bundle provides two services to other bundles, the Poco::OSP::Web::WebServerDispatcher and the Poco::OSP::Web::WebSessionManager service.

The WebServerDispatcher Service

The Poco::OSP::Web::WebServerDispatcher service allows a bundle to add static content or request handlers to the web server programmatically. The addVirtualPath() member function can be used to add either static content or a request handler.

The WebSessionManager Service

The Poco::OSP::Web::WebSessionManager service provides HTTP session management to request handlers. Session management allows a request handler to maintain state information between different requests from the same browser session. The state information is maintained using Poco::OSP::Web::WebSession objects, which can store arbitrary name/value pairs. For the value, the Poco::Any class is used, which allows to store values of almost any type.

The WebSession sample (found in OSP/samples/WebSession) shows how to use the Poco::OSP::Web::WebSessionManager and Poco::OSP::Web::WebSession classes.

Configuring The Web Server

The web server can be configured by setting the following properties in the global application configuration file:

  • osp.web.server.host: The hostname or IP address where the (non-secure) server is listening. Defaults to "0.0.0.0" (wildcard address).
  • osp.web.server.port: The TCP port where the (non-secure) server is listening. Defaults to 22080.
  • osp.web.server.secureHost: The hostname or IP address where the secure (HTTPS) server is listening. Defaults to "0.0.0.0" (wildcard address).
  • osp.web.server.securePort: The TCP port where the secure (HTTPS) server is listening. Defaults to 22443.
  • osp.web.server.maxQueued: The maximum number of queued requests (see Poco::Net::TCPServerParams). Defaults to 100.
  • osp.web.server.maxThreads: The maximum number of threads used by the server (see Poco::Net::TCPServerParams). Defaults to 8.
  • osp.web.server.keepAlive: Enable persistent connections (true or false). Defaults to true.
  • osp.web.server.keepAliveTime: Maximum time a persistent connection is kept open if no request arrives. Defaults to 10.
  • osp.web.server.maxKeepAlive: Maximum number of requests handled on a persistent connection. Defaults to 10.
  • osp.web.authServiceName: The name of the OSP authentication/authorization service to use. Defaults to "osp.auth".
  • osp.web.authMethods: Specify a comma-separated list of enabled authentication methods (basic, session, bearer). If not specified, all methods will be enabled.
  • osp.web.tokenValidatorName: The name of the token validation service to use. Defaults to none (empty).
  • osp.web.compressResponses: Enable (default) or disable response content compression using gzip content encoding. Specify true to enable or false to disable compression.
  • osp.web.compressedMediaTypes: A comma-separated list of media types for which compression is enabled. For the subtype, a wildcard (*) can be specified. Defaults to text/*.
  • osp.web.cacheResources: Enable or disable (default) caching of static resources from bundles. This can greatly speed up the server as it keeps all resources in memory. However, depending on the kind of resources served, memory usage may increase significantly. This can be a concern on less powerful embedded devices. Resources can be excluded from caching by setting the cache attribute in the extension point to false.
  • osp.web.sessionManager.cookiePersistence: Specifies whether session cookies used by the WebSessionManager are persistent (survive closing the browser) or transient (are removed when the browser is closed). Valid values are "persistent" (default) and "transient".
  • osp.web.csrfProtectionEnabled: Enable or disable Cross-Site Request Forgery protection. If enabled, the WebSessionManager will create and set a CSRF cookie.
  • osp.web.sessionManager.csrfCookie: The name of the CSRF cookie that will be automatically set by the WebSessionManager in order to prevent Cross-Site Request Forgery attacks. Defaults to X-XSRF-TOKEN.
  • osp.web.sessionManager.verifyAddress: Enable client address verification. If enabled (default), the WebSessionManager will keep track of the client IP address associated with the session cookie. If the client IP address changes, the session will be invalidated.
  • osp.web.sessionManager.sessionStore: Specifies an optional service name for a WebSessionStore service to be used by the WebSessionManager. This can be used to persistently store session data on the server.
  • osp.web.hsts.enable: Enable HTTP Strict Transport Security (HSTS). If enabled, a Strict-Transport-Security header will be included with every response the HTTP server sends. Defaults to false.
  • osp.web.hsts.maxAge: Specify the maximum age for HSTS. Defaults to 21772800 seconds or 36 weeks.
  • osp.web.hsts.includeSubdomains: If set to true (default), the Strict-Transport-Security includes the includeSubdomains optional parameter.
  • osp.web.xssProtection.enable: If set to true, a X-XSS-Protection header will be included with every response the HTTP server sends. Defaults to false.
  • osp.web.xssProtection.mode: Specifies the mode parameter for the X-XSS-Protection header and defaults to "block".
  • osp.web.contentTypeOptions: Specifies the value of the X-Content-Type-Options header sent with every HTTP response. Defaults to nosniff.
  • osp.web.addAuthHeader: If set to true (default), and authentication/authorization has been enabled for a resource and has been successful, a header named "X-OSP-Authorized-User" containing the authenticated and authorized username is added to the Poco::Net::HTTPRequest, for use by the downstream request handler.
  • osp.web.addSignature: If set to true (default), the server will include a signature in generated HTML error pages.

Request Logging

The web server supports logging of HTTP requests. For that purpose, the logger "osp.web.access" is used. The log message text is the HTTP request, including method, path and version. Additionally, the following log message properties are available for use in formatting:

  • username: The username sent by the client if HTTP Basic Authentication is used, otherwise "-".
  • status: The HTTP response status code (e.g., 200).
  • client: The client IP address.
  • size: The size of the response, from the response Content-Length header. If not set, "-".
  • referer: The content of the Referer HTTP request header, or an empty string if not set.
  • useragent: The content of the User-Agent HTTP request header, or an empty string if not set.

This information can be used to enable logging in Common or Extended log format, by using appropriate logger and formatter configurations in the application's configuration file. For example, to write the access log to the console, use the following configuration:

logging.loggers.access.name = osp.web.access
logging.loggers.access.channel = consoleAccess
logging.loggers.access.level = information
logging.channels.consoleAccess.class = ConsoleChannel
logging.channels.consoleAccess.pattern = %[client] - %[username] [%d/%b/%Y:%H:%M:%S %Z] "%t" %[status] %[size]

To use Extended log format and include referer and user agent, use the following pattern:

logging.channels.consoleAccess.pattern = %[client] - %[username] [%d/%b/%Y:%H:%M:%S %Z] "%t" %[status] %[size] "%[referer]" "%[useragent]"

Securely control IoT edge devices from anywhere   Connect a Device