Channels ▼
RSS

Web Development

Creating HTML User Interfaces for Server Programs

Source Code Accompanies This Article. Download It Now.


For both IIS and Apache

Michael is manager of applications interface services for emWare. He can be contacted at [email protected].


The success of server programs is dependent on their ability to be remotely managed. Given that clients can run one of many different operating systems, creating a Windows-only or Linux-only solution may not be enough. However, creating native client interfaces for numerous platforms can be very costly. First, you need developers that specialize in each platform you plan to support. Second, you also need installation and testing engineers to tackle your project. Finally, you have to sustain each platform. Sustaining multiple code bases can be a nightmare in itself. For these reasons, many companies have gone to an HTML user-interface (UI) solution.

The HTML UI lets you create one interface into your server application, significantly lowering the total cost of research, development, and sustaining of multiple code bases. Not only are you able to save money and man hours, but users can access the program remotely from any operating system without installing a client interface. Lastly, aside from a few differences in the browser presentation, the UI is identical on all platforms.

In this article, I'll step you through the process of creating HTML interfaces to server applications. In doing so, I use both Apache Web Server 1.3.14 (Apache) and Microsoft's Internet Information Services 5.0 (IIS). Although I wrote the sample code for Windows 2000, it can be modified to run on Linux and other platforms with minimal changes.

Overview

HTTP servers are the engines that serve up Internet content. This content includes, but isn't limited to, HTML files. IIS includes an HTTP server. At this time, IIS only runs on Windows. Apache, on the other hand, runs on many platforms. Apache is an HTTP server originally written for UNIX. Over time, Apache has been ported to other operating systems including Linux and Windows.

Sending a command from a browser to your server application is the same, regardless of the HTTP server you choose. Users send requests to HTTP servers via browsers. When the HTTP server receives the request, it puts the request through a series of filters, if applicable. The HTTP server in this article has one filter. Once the filter determines that the request needs to be handled by a special handler, the request is passed on to that handler. The handler, in this case, determines what command should be sent to the external server application. The command is sent and the server application handles the command via its own internal command handler. Once finished, the server application returns a response to the requester. The requester, or handler, then creates an HTML page in response. Finally, the HTTP server returns the dynamically created HTML page to the remote browser. Figure 1 depicts this process.

Server Application

Your server application must have an external or preferably a remote application interface. A remote interface lets your users isolate the HTTP server from the application server. I have provided a simple server application with a socket interface. As you can see in Listing One, the application supports three commands — login, logout, and gettime.

The login function takes a key that can be used for an encryption scheme. Listing One doesn't do anything with the passed-in key. The server application creates its own key and passes it back to the client. For this sample to work, the key that is passed back must be used when making future requests. You need to create your own encryption scheme. The gettime function validates the request by looking up the passed key in the list of user accounts. If the key exists, the application sends the time back to the client. The logout function validates the request and invalidates the key associated with the user.

This application assumes that all data is received with one call to recv. It also assumes that the correct amount of data is sent and the data received is well formed. To make your code robust, you need to create a protocol handler that lets you only handle valid requests. For example, if a rogue client is attempting to crash the sample code, the application could attempt to invoke a login function without specifying a key, username, or password. If any of the parameters are missing, the sample code may access uninitialized memory, possibly causing a memory read overrun. Clearly, this sort of behavior is not only undesirable but may cause the application to crash.

HTTP Server

The HTTP server is really the core of the HTML interface. It lets a request be passed to your handler code, which in turn communicates with your server application. Before you can begin your project, you need to decide which HTTP server to use — Apache or IIS. When I created emWare's HTML interface to its emGateway, an embedded device proxy server, I initially chose Apache. Why? Because like Apache, emGateway runs on Windows as well as Linux. You need to make your decisions based on your customers' requirements. Once you have successfully implemented a solution based on one of these HTTP servers, you will probably want to support the other.

Why not just use CGI to interface with your server application? Using a CGI only solution will enable your customers to run with any HTTP server that supports CGI. However, CGI is optimal on systems where creating a new process is inexpensive. Such is the case with UNIX and Linux. On Windows, firing up a new process is expensive. In an environment where server resources are limited or server responsiveness is important, you want to avoid CGI. You will, however, still pass parameters to the HTTP server via POST/GET methods. Your HTTP server handler routines need to parse these parameters to create the appropriate pages and interface with your server application.

Cookies

Again, gettime and logout require a key to authenticate the request. One of the most popular ways to save state information between browsers and servers is to use cookies. One of the problems with cookies is that the state information is saved on the hard drive. Another way to save state information is to use a session cookie. A session cookie is the same as a regular cookie except it resides in memory. When the browser is closed, the cookie is lost. The HTTP server creates session cookies by adding a header to the request response; for example, Set-Cookie: YourData; path=/\r\n. To expire a session cookie, set the expiration date to a prior data; for example, Set-Cookie: YourData; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT\r\n.

Apache Implementation

When you install Apache (available at http://www.apache.org/), make sure you install the Web Server Source Code. Installing this source code lets you create Apache modules, and Apache modules let you execute C code when a specific HTTP request is sent to Apache. To interface with your server application, you need to create an Apache module such as AcmeModule.c (available electronically; see "Resource Center," page 5).

In AcmeModule.c, the acme_module variable defines the callbacks. The first callback in the structure is acme_create_dir_config and lets me initialize the configuration parameters. The acme_cmds structure defines a callback for each configuration parameter. After Apache reads the configuration parameters from the httpd.conf file, the callbacks are called and the values saved. Finally, the acme_handlers structure defines an array of handlers. In this case, only acme_handler is passed back. The handler is called whenever the filter determines the request that needs to be handled by this Apache module.

This can be confusing, but understanding the Apache module framework is the hard part. The rest is easy. The call to ap_get_module_config returns a pointer to the configuration that was saved off at startup. The util_read function reads any data that is sent via a POST method. If the data is sent via a GET method, the request_rec.args structure member points to the data. If the browser passes a cookie, the call to ap_table_get retrieves the value. At this point you have all the data you need.

The handle_request function determines the command and any specified parameters. Once determined, an outgoing packet is created and sent to the awaiting server application. Finally, the server application response is handled. If the user is attempting to log in and the command was successful, a key is passed back to the handler. The handler creates a cookie and sends back a successful HTML page. If the user is attempting to get the time from the server application, the time is returned in string format and an HTML page with the time is created. If the user is attempting to log out of the system, the handler invalidates the cookie and sends back a successful HTML page. If any of the commands fail, a failure HTML page is returned.

After you compile the module you must copy the file to the appropriate directory. On Linux, the module should be copied to /usr/lib/apache. On Windows, the default location is c:\program files\apache group\apache\modules.

At this point, Apache still doesn't know when to call the module. To do this, a filter must be created to enable Apache to redirect the request to the Apache module. To create a filter, you must modify the httpd.conf file. On Linux, this file is located in /etc/httpd/conf. On Windows, the default location is c:\program files\apache group\apache\conf. Example 1 shows the modifications. As mentioned earlier, configuration parameters are needed. These parameters are defined in the httpd.conf file. The port the server application runs on is acmeServerPort. I use the value of 28500 for this example. The host the server application is running on is acmeServerHost. Once you have made your modifications to the httpd.conf file, you must restart Apache. Refer to the Apache documentation for more information.

To confirm that the module is working correctly, start the server application. Once the server application is running, open your browser and type:

http://localhost/acme?cmd=login&user= Manny&pswd=manny

If all the pieces are working correctly, the browser displays "Successfully logged in." To invoke the gettime function, enter:

http://localhost/acme?cmd=gettime

Lastly, to log out type:

http://localhost/acme?cmd=logout

You can easily modify the sample code to return HTML pages, which include links to the login, gettime, and logout functionality.

IIS Implementation

If you don't already have IIS, you need to install and configure it before you can test the sample code. Refer to http://www.microsoft.com/ for more information.

When using IIS you must create an ISAPI extension and filter. Extensions are DLLs that let you execute C/C++ code. Users must explicitly call extensions. For example, to call an extension called "foo.dll" that is located in the Scripts directory, you must specify the URL http://localhost/scripts/foo.dll. Most likely, you won't want to place this burden on your users. For this reason you can create a filter. Among other things, filters let you rewrite the URL that the user specifies. For example, if the user specifies http://localhost/acme?cmd=gettime you can rewrite the URL to be http://localhost/scripts/acme.dll?cmd=gettime. When a filter changes the URL, only IIS is aware of the change. The browser and user never know the difference.

Using Developer Studio 6.0, you can use the ISAPI Extension Wizard to create an extension and filter. On the first page of the wizard, make sure you check "Generate a Filter object." On the second page, check the "Post-preprocessing of the request headers" option. Once you create the project, you are ready to add the handler code.

Open the source code and modify the GetFilterVersion to retrieve the server application host and port from the registry. For this sample, I decided to store the values in global variables. You will want to come up with your own scheme. Listing Two shows the modifications that I made. To implement the filter code that rewrites the URL, modify the OnPreprocHeaders function. Listing Three shows the functions I created. Now users are ready to implement the extension code (see Listing Four; available electronically). The first thing I did was remove the code in the Default function. Using the Class Wizard, I added a handler for the HttpExtensionProc message. In essence, the HttpExtensionProc handler is the same as the acme_handler previously mentioned.

The util_read function returns any parameters that were sent via the POST method. If the parameters were sent via a GET method, the data is retrieved from the QUERY_STRING server variable. The HTTP_COOKIE variable contains any cookies that were sent by the browser.

Once you have retrieved all the sent data, call handle_request. I didn't duplicate the code, but it is virtually the same. The only difference is the first parameter. For the Apache implementation, the server application parameters are retrieved through the Apache API and passed to the handle_request function. In the IIS implementation, the parameters are global variables so they don't need to be passed on the stack.

Once the request and response are handled, the HTTP headers are created. If the user is attempting to log in, the session cookie is created. If the user is attempting to logout, the session cookie is invalidated. The SendHttpHeaders function sends the headers to the browser. The last section of the code sends the HTML content to the browser.

After you finish implementing the code, compile the project and install the filter into IIS. For more information, refer to the IIS documentation. To test the filter and extension, use the same URLs specified earlier.

Conclusion

In this article, I've shown how to create the framework to create an HTML interface to your server application. You will still need to do a lot of work to complete your project. For example, you need to decide on a method for secure remote access. You also need to implement a protocol handler library so your extension DLL and Apache module are robust and fool proof against attacks. Finally, you need to create your HTML generation code. This portion is probably the most labor-intensive portion of the entire project (at least it was for me because I'm not an HTML guru). Still, you should be ready to get started on your HTML interface.

For More Information

Stein, Lincoln and Doug MacEachern. Writing Apache Modules With Perl and C, O'Reilly & Associates, 1999.

DDJ

Listing One

/* The sample code provided is for Windows only */
/* main.c */


#include <winsock2.h>
#include <conio.h>
#include <stdio.h>
#include "acmeprotocol.h"

/* user entry which contains a key and a name */
typedef struct {
    DWORD key;
    char *user;
    char *pswd;
}USER_T;
/* static list that contains three users */
static USER_T users[] = {
    { 0xFFFFFFFF, "Manny", "manny" },
    { 0xFFFFFFFF, "Moe", "moe" },
    { 0xFFFFFFFF, "Jack", "jack" }
};
int main(int argc, char **argv) {
    ACMEPROTOCOL_T command, response;
    BOOL bQuit = FALSE;
    BYTE *pbt;
    char input, packet[256], *pswd, *user;
    DWORD key, localkey = 0, *pdw;
    int fromlen, i, rc;
    FD_SET fdsErr, fdsRead, fdSet;
    struct sockaddr_in  from, local;
    SOCKET s, msgsock;
    SYSTEMTIME ts;
    struct timeval t = { 1, 0};
    WORD *pw;
    WSADATA wsadata;
    /* create the server */
    if (WSAStartup(MAKEWORD(2,2), &wsadata))
        return -1;
    local.sin_family = AF_INET;
    local.sin_port = htons(28500);
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    s = socket(AF_INET, SOCK_STREAM, 0);
    if(INVALID_SOCKET == s) {
        WSACleanup(); return -1;
    }
    if (bind(s,(struct sockaddr*)&local,sizeof(local) ) == SOCKET_ERROR)
        bQuit = TRUE;    
    if (listen(s,SOMAXCONN) == SOCKET_ERROR)
        bQuit = TRUE;    
    FD_ZERO(&fdSet);
    FD_SET(s, &fdSet);
    while(!bQuit) {
        while(!kbhit()) {         
            memcpy((void*)&fdsRead,(void*)&fdSet, sizeof(fd_set));
            memcpy((void*)&fdsErr, (void*)&fdSet, sizeof(fd_set));
            rc = select(0, &fdsRead, NULL, &fdsErr, &t);
            if (SOCKET_ERROR == rc) {
                bQuit = TRUE;
                break;
            }
            if (!rc)
                continue;            
            if (FD_ISSET(s, &fdsErr)) {
                bQuit = TRUE;
                break;
            }
            if (!FD_ISSET(s, &fdsRead))
                continue;
            fromlen = sizeof(from);
            msgsock = accept(s,(struct sockaddr*)&from, &fromlen);
            if (msgsock == INVALID_SOCKET) {
                bQuit = TRUE;
                break;
            }
            /* Get the command.  Note that the data must be sent in one 
            packet otherwise this program will fail. This program also 
            assumes the correct data is sent. You will need to write a packet 
            handler to deal with these issues */
            rc = recv(msgsock,packet,sizeof(packet),0 );
            if (SOCKET_ERROR != rc && rc && 
                rc >= sizeof(ACMEPROTOCOL_T) + sizeof(DWORD)) {
                /* handle the command */
                pbt = packet;
                pw = (WORD*)pbt;
                command.cmd = (WORD)ntohs(*pw);
                pbt += sizeof(WORD);
                pw = (WORD*)pbt;
                command.status = (WORD)ntohs(*pw);
                pbt += sizeof(WORD);
                pdw = (DWORD*)pbt;
                command.size = (DWORD)ntohl(*pdw);
                pbt += sizeof(DWORD);
                pdw = (DWORD*)pbt;
                key = (DWORD)ntohl(*pdw);
                pbt += sizeof(DWORD);

                response.cmd = command.cmd;
                response.status = 0;
                response.size = 0;
                switch (command.cmd)
                {
                case CMD_LOGIN:
                    user = pbt;   /* protocol handler required */
                    pbt += strlen(user) + 1;
                    pswd = pbt;   /* protocol handler required */
                    for (i = 0; i < 3; i++) {
                        if (!strcmp(users[i].user, user)) {
                            if (!strcmp(users[i].pswd, pswd)) {
                                users[i].key = ++localkey;
                                pbt = packet;
                                pbt += sizeof(ACMEPROTOCOL_T);
                                pdw = (DWORD*)pbt;
                                *pdw = htonl(users[i].key);
                                response.size = sizeof(DWORD);
                                response.status = 1;
                            }
                            break;
                        }
                    }
                    break;
                case CMD_LOGOUT:
                    for (i = 0; i < 3; i++) {
                        if (users[i].key == key) {
                            users[i].key = 0xFFFFFFFF;
                            response.status = 1;
                            break;
                        }
                    }
                    break;
                case CMD_GETTIME:
                    for (i = 0; i < 3; i++) {
                      if (users[i].key == key) {
                        GetSystemTime(&ts);
                        pbt = packet;
                        pbt += sizeof(ACMEPROTOCOL_T);
                        _snprintf(pbt, 
                          sizeof(packet) - sizeof(ACMEPROTOCOL_T), 
                          "%02d:%02d:%02d", ts.wHour, ts.wMinute, ts.wSecond);
                        pbt[sizeof(packet) - sizeof(ACMEPROTOCOL_T) - 1] = 0;
                        response.size = strlen(pbt) + 1;
                        response.status = 1;
                        break;
                      }
                    }
                    break;
                }
                pbt = packet;
                pw = (WORD*)pbt;
                *pw = htons(response.cmd);
                pbt += sizeof(WORD);
                pw = (WORD*)pbt;
                *pw = htons(response.status);
                pbt += sizeof(WORD);
                pdw = (DWORD*)pbt;
                *pdw = htonl(response.size);
                pbt += sizeof(DWORD);
                send(msgsock,packet,sizeof(ACMEPROTOCOL_T) + response.size,0);
            }
            closesocket(msgsock);
        }
        input = getch();
        if (input == 'q')
            break;
    }
    closesocket(s); 
    WSACleanup(); 
    return 0;
}
/* acmdprotocol.h */
#ifndef ACMEPROTOCOL_H
#define ACMEPROTOCOL_H
typedef struct {
    WORD cmd;
    WORD status;
    DWORD size;
}ACMEPROTOCOL_T;
typedef enum {
    CMD_LOGIN, CMD_LOGOUT, CMD_GETTIME,
}CMD;
#endif

Back to Article

Listing Two

static char g_host[256];
static WORD g_port = 28500;
BOOL CAcmeISAPIFilter::GetFilterVersion(PHTTP_FILTER_VERSION pVer)
{
    // Call default implementation for initialization
    CHttpFilter::GetFilterVersion(pVer);
    // Clear the flags set by base class
    pVer->dwFlags &= ~SF_NOTIFY_ORDER_MASK;
    // Set the flags we are interested in
    pVer->dwFlags |= SF_NOTIFY_ORDER_LOW | SF_NOTIFY_SECURE_PORT |
                     SF_NOTIFY_NONSECURE_PORT | SF_NOTIFY_PREPROC_HEADERS;
    // Load description string
    TCHAR sz[SF_MAX_FILTER_DESC_LEN+1];
    ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),
            IDS_FILTER, sz, SF_MAX_FILTER_DESC_LEN));
    _tcscpy(pVer->lpszFilterDesc, sz);
    DWORD   port, type, size;
    HKEY    hKey; 
    g_host[0] = 0;
    // Open the registry
    if (ERROR_SUCCESS != RegOpenKeyEx(HKEY_LOCAL_MACHINE, 
                               _T("software\\acme"), 0, KEY_READ, &hKey))
        return FALSE;
    // Get the server application host
    size = sizeof(g_host);
    if (ERROR_SUCCESS != RegQueryValueEx(hKey, _T("host"), 
                                       0, &type, (BYTE*)&g_host, &size)) {
        RegCloseKey(hKey);
        return FALSE;
    }
    // Get the mas port
    size = sizeof(port);
    if (ERROR_SUCCESS != RegQueryValueEx(hKey, _T("port"), 
                                      0, &type, (BYTE*)&port, &size)) {
        RegCloseKey(hKey);
        return FALSE;
    }
    g_port = (WORD)port;
    // Close the registry
    RegCloseKey(hKey);     
    return TRUE;
}

Back to Article

Listing Three

DWORD CAcmeISAPIFilter::OnPreprocHeaders(CHttpFilterContext* pfc, 
                                    PHTTP_FILTER_PREPROC_HEADERS pHeaders) 
{
    TCHAR url[256], newurl[256], *test;
    DWORD size = sizeof(url);
    if (pHeaders->GetHeader(pfc->m_pFC,_T("URL"),url,&size)) {
        test = _tcsstr(url, _T("/acme"));
        if (test) { 
            test += _tcslen(_T("/acme"));
            if (test[0]) {
                _sntprintf(newurl, 256, _T("/Scripts/AcmeISAPI.dll%s"), test);
                newurl[255] = 0;
            } else
                _tcscpy(newurl, _T("/Scripts/AcmeISAPI.dll"));
            pHeaders->SetHeader(pfc->m_pFC, _T("URL"), newurl);
        }
    }
    return CHttpFilter::OnPreprocHeaders(pfc, pHeaders);
}


Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.