Mongoose: an Embeddable Web Server in C

Mongoose provides a Web server that can be embedded in your application, and it consists of a single C source file (and a header file) that you can compile and link with your application code. If you need a simple Web server, Mongoose may be the solution.

Callbacks, set with mg_set_uri_callback(), are functions that handle specific URI requests. The asterisk is used as a path wild card. In this simple example, there is a single callback handler that handles all URI requests starting at the root path for the Web server.

To complete this example of the Mongoose equivalent of the “Hello, World” program, all that is required is a function for printing a page back to the requesting Web browser:


void uriHandler()
{
  mg_printf(conn,
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: text/html\r\n\r\n"
    "<html>\r\n"
    "<body>\r\n"
    "Hello, World!\r\n"
    "</body>\r\n"
    "</html>\r\n"
    );
}

The first two lines are HTTP headers. These are not necessarily required for such a simple example, but if you do include them, remember to include a blank line between the Content-Type header and the start of the page content.

Two functions in the Mongoose library are used for sending results back to the browser: mg_printf() and mg_write(). The former provides printf() semantics for sending data back to the client, and the latter provides no limits on the amount of data that can be sent. If the server needs to know that the client closed the connection before all data was returned, or if the server needs to send more than MAX_REQUEST_SIZE (8Kb) of data, mg_write() should be used. Note that the API documentation says the maximum size for mg_printf() is 16Kb, but the Mongoose library source code defaults MAX_REQUEST_SIZE to 8Kb. Multiple mg_printf() or mg_write() calls are possible within a single callback; however, once the callback returns, the connection is closed by the worker thread.

Authentication and Authorization

In the Web world, authentication refers to validation of an incoming request as having come from a known entity. All that is required is that the entity identify itself with tokens kept by the system. In the case of digest authentication, that means a user name, password and realm. A realm is a symbolic name allowing the same user name/password to have different meanings to different areas of a server URI namespace. In practice, users need remember only the user name/password combination. The realm is managed by the server. Digest authentication has additional complexities related to how the server and client communicate, but from the standpoint of Mongoose users, this is not required knowledge. In summary, authentication is used to identify a user.

Authorization refers to the verification that an authenticated entity has permission to do what it is attempting to do. Although people may have a proper login to a server, they may not have permission to view certain areas of the Web site. Access to specific server functionality is handled by authorization.

Mongoose provides built-in support for digest authentication. If configured, a file containing a user name, password and realm is stored within reach of the server at runtime. The server checks this file for authentication based on HTTP Authentication headers from the browser. A global authentication file can be configured as well as per-URI authentication files. Mongoose users can generate these files using Apache's htdigest program. The location of the file is set during Mongoose initialization using the mg_set_option() function. The realm defaults to “mydomain.com” if not specified. Digest authentication is not required.

The auth_gpass option sets the location of a global authentication file. This file is used to authenticate requests for any URI. The argument for this option is the path to the file. To set authentication for specific URIs, use the protect option. The argument to this option is a collection of comma-separated URI=PATH pairs, with URI being relative to the Web server and containing wild cards, and PATH being a path to the authentication file to use for that URI. Paths should be fully qualified or relative to the directory from which the Mongoose-based server is started.

If an authentication option is set for a requested URI, Mongoose will tell the client browser to open a login dialog. The Mongoose library processes the login information from the user before passing control to the appropriate callback, if any. Once the browser user is authenticated, the only way to log out is to request the authentication again. It turns out that this form of authentication requires cookies to implement the logout process and force another login. Alternatively, cookies can be used to implement a page with HTML forms for the purpose of login and logout outside the use of digest authentication. If logout or a server-side form is required for login, digest authentication probably should not be set with Mongoose options. Digest authentication still can be used manually, but Mongoose does not expose API functions for this purpose.

Along with authentication, authorization can be implemented using a callback registered with the mg_set_auth_callback() function. The registered function is called before each URI callback to allow the server code to determine whether the incoming request should be authorized to access the requested URI. If authorization is granted, this function calls mg_authorize() on the provided mg_connection. If this is not done, Mongoose assumes authorization is not granted and will not call the configured callback for the requested URI:


static
void authorize(
       struct mg_connection *conn,
       const struct mg_request_info *ri,
       void *data)
{
    const char  *cookie, *domain;
    cookie = mg_get_header(conn, "Cookie");
    uri = ri->uri;

    if ( (strcmp(ri->uri, "/") == 0) ||
         (strncmp(ri->uri, "/images", 7) == 0)
       )
    {
        mg_authorize(conn);
    }
    else if (strncmp(ri->uri, "/logout", 7) == 0)
    {
        ... Verify login cookie ...
        ... redirect to front page ...
    }
    else if (cookie != NULL &&
             strstr(cookie, "UUID=") != NULL)
    {
        ... Get value from the cookie, if any ...
        if ( ...cookie okay ... )
            mg_authorize(conn);
        else
            ... redirect to /logout ...
    }
}

Note the arguments to the authorize() function. The first argument is the connection information. The second is a pointer to request information pulled from the incoming HTTP request. The third argument points to data provided when the callback was registered with mg_set_auth_callback(). These same arguments are used when URI callback functions are called.

In this example authorization function, any request for the front page or the images directory within the document root are authorized automatically. This allows images referenced in CSS, for example, to be retrieved by the browser without having to be inspected by this function or by having a registered URI callback for images. If no callback is registered for a URI, Mongoose attempts to serve the file found at the specified URI under the document root.

If the URI is the logout page, the login cookie is checked for, and if found, the user is redirected to the login page where that cookie is removed. If the cookie is not found, the server can redirect to the front page anyway or perform some other appropriate action.

The next test looks for a specific cookie, in this case named “UUID”. If this is found and has the correct value, the request is authorized. Otherwise, the user is redirected to the logout page, which in turn cleans up the login cookie and presents the login page again.

The mg_request_info structure is defined in mongoose.h and is filled by the worker thread with information gleaned from the HTTP request. This includes information such as the request method (POST, PUT, GET and so forth), a normalized URI, query string, post data and the IP address from which the request originated. It also includes an array holding the set of HTTP headers, which is how cookies are retrieved.

______________________

Webinar
One Click, Universal Protection: Implementing Centralized Security Policies on Linux Systems

As Linux continues to play an ever increasing role in corporate data centers and institutions, ensuring the integrity and protection of these systems must be a priority. With 60% of the world's websites and an increasing share of organization's mission-critical workloads running on Linux, failing to stop malware and other advanced threats on Linux can increasingly impact an organization's reputation and bottom line.

Learn More

Sponsored by Bit9

Webinar
Linux Backup and Recovery Webinar

Most companies incorporate backup procedures for critical data, which can be restored quickly if a loss occurs. However, fewer companies are prepared for catastrophic system failures, in which they lose all data, the entire operating system, applications, settings, patches and more, reducing their system(s) to “bare metal.” After all, before data can be restored to a system, there must be a system to restore it to.

In this one hour webinar, learn how to enhance your existing backup strategies for better disaster recovery preparedness using Storix System Backup Administrator (SBAdmin), a highly flexible bare-metal recovery solution for UNIX and Linux systems.

Learn More

Sponsored by Storix