[Source Code Walkthrough] TinyHttpd: The Simplest Web Server Implementation

Preface

Over the past couple of days, on a whim, I wanted to write an event-driven webserver with principles similar to nginx, so I read through a very simple webserver implementation circulating online: TinyHttpd. Recently I have been interested in reading a series of small, elegant programs like this to sharpen my coding skills, but the thing in this article obviously cannot be considered as sophisticated as the real nginx. It mainly includes the basic elements of a webserver as a demonstration and provides some error handling. But the code itself still contains many rough edges. When it comes to webservers, the last two chapters of CS:APP actually explain things clearly and in depth. All in all, though, this little thing is written quite well, and it is worth writing a blog post to record the code and annotations.

The organized source code can be downloaded by clicking here. It includes a correct CMake file and appropriate modifications, so it can be compiled on Windows with CLion; Linux should work as well. Reading code with a suitable IDE is indeed pleasing.

Source Code Walkthrough

/* J. David's webserver */
/* This is a simple webserver.
 * Created November 1999 by J. David Blackstone.
 * CSE 4344 (Network concepts), Prof. Zeigler
 * University of Texas at Arlington
 */
/* This program compiles for Sparc Solaris 2.6.
 * To compile for Linux:
 *  1) Comment out the #include <pthread.h> line.
 *  2) Comment out the line that defines the variable newthread.
 *  3) Comment out the two lines that run pthread_create().
 *  4) Uncomment the line that runs accept_request().
 *  5) Remove -lsocket from the Makefile.
 */
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h>

#define ISspace(x) isspace((int)(x))

#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"

void *accept_request(void *);

void bad_request(int);

void cat(int, FILE *);

void cannot_execute(int);

void error_die(const char *);

void execute_cgi(int, const char *, const char *, const char *);

int get_line(int, char *, int);

void headers(int, const char *);

void not_found(int);

void serve_file(int, const char *);

int startup(u_short *);

void unimplemented(int);

/**********************************************************************/
/* A request has caused a call to accept() on the server port to
 * return.  Process the request appropriately.
 * Parameters: the socket connected to the client */
/**********************************************************************/
// Thread handler function
void *accept_request(void *_client) {
    int client = *(int *) _client;
    char buf[1024];     // Buffer used when reading line data
    int numchars;       // How many characters were read
    char method[255];   // Stores the HTTP request method name (string)
    char url[255];
    char path[512];
    size_t i, j;
    struct stat st;
    int cgi = 0;      /* becomes true if server decides this is a CGI
                    * program */
    char *query_string = NULL;

    // Read the first line of the HTTP header: GET /index.php HTTP1.1
    numchars = get_line(client, buf, sizeof(buf));
    i = 0;
    j = 0;

    // First see what method it is, and copy out the method name
    while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) {
        method[i] = buf[j];
        i++;
        j++;
    }
    method[i] = '\0';

    // This check is a bit odd, because it cannot be true
    // You can tell the author was not being very careful
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) {
        unimplemented(client);
        return NULL;
    }

    // If it is a POST request, assume a CGI script needs to handle it
    if (strcasecmp(method, "POST") == 0)
        cgi = 1;

    i = 0;
    // Skip whitespace
    while (ISspace(buf[j]) && (j < sizeof(buf)))
        j++;

    // Read the URL out of the buffer
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) {
        url[i] = buf[j];
        i++;
        j++;
    }
    url[i] = '\0';



    // First handle the GET request case
    if (strcasecmp(method, "GET") == 0) {
        query_string = url;
        // Move the pointer to find GET parameters, i.e., the part after '?'
        while ((*query_string != '?') && (*query_string != '\0'))
            query_string++;
        // If found, it means this request also needs a script to handle it
        // Extract the query string separately
        if (*query_string == '?') {
            cgi = 1;
            // Directly terminate the string here, and keep the pointer position
            // That is, query_string points to the actual request parameters
            *query_string = '\0';
            query_string++;
        }
    }

    // Do path concatenation; because url starts with `/`, there is no need to add a new separator
    sprintf(path, "htdocs%s", url);
    // If the last character of the access path is `/`, complete it (default to index.html)
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");
    // Check whether the requested file exists
    if (stat(path, &st) == -1) {
        // If it does not exist, read the remaining request headers out of the buffer
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
        // Then return a 404
        not_found(client);
    }
    else {
        // If the file exists but is a directory, continue concatenating; default to index.html under that directory
        if ((st.st_mode & S_IFMT) == S_IFDIR)
            strcat(path, "/index.html");
        // If the file has execute permission, execute it
        if ((st.st_mode & S_IXUSR) ||
            (st.st_mode & S_IXGRP) ||
            (st.st_mode & S_IXOTH))
            cgi = 1;
        // Finally, decide whether to return a static file or invoke a script based on cgi
        if (!cgi)
            serve_file(client, path);
        else
            execute_cgi(client, path, method, query_string);
    }

    close(client);
    return NULL;
}

/**********************************************************************/
/* Inform the client that a request it has made has a problem.
 * Parameters: client socket */
/**********************************************************************/
// Return a 400 error
void bad_request(int client) {
    char buf[1024];

    sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "Content-type: text/html\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "<P>Your browser sent a bad request, ");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "such as a POST without a Content-Length.\r\n");
    send(client, buf, sizeof(buf), 0);
}

/**********************************************************************/
/* Put the entire contents of a file out on a socket.  This function
 * is named after the UNIX "cat" command, because it might have been
 * easier just to do something like pipe, fork, and exec("cat").
 * Parameters: the client socket descriptor
 *             FILE pointer for the file to cat */
/**********************************************************************/
// This function reads the entire contents of the file and sends it to the client
// It could use the mmap system call; it might be more efficient
void cat(int client, FILE *resource) {
    char buf[1024];

    fgets(buf, sizeof(buf), resource);
    while (!feof(resource)) {
        send(client, buf, strlen(buf), 0);
        fgets(buf, sizeof(buf), resource);
    }
}

/**********************************************************************/
/* Inform the client that a CGI script could not be executed.
 * Parameter: the client socket descriptor. */
/**********************************************************************/
// Return a 500 error
void cannot_execute(int client) {
    char buf[1024];

    sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
    send(client, buf, strlen(buf), 0);
}

/**********************************************************************/
/* Print out an error message with perror() (for system errors; based
 * on value of errno, which indicates system call errors) and exit the
 * program indicating an error. */
/**********************************************************************/
// Report an error and exit
void error_die(const char *sc) {
    // A function from stdio; prints an error message to stderr based on errno and exits
    perror(sc);
    exit(1);
}

/**********************************************************************/
/* Execute a CGI script.  Will need to set environment variables as
 * appropriate.
 * Parameters: client socket descriptor
 *             path to the CGI script */
/**********************************************************************/
void execute_cgi(int client, const char *path,
                 const char *method, const char *query_string) {
    char buf[1024];
    int cgi_output[2];
    int cgi_input[2];
    pid_t pid;
    int status;
    int i;
    char c;
    int numchars = 1;
    int content_length = -1;

    // First, handle the request differently depending on whether it is GET or POST
    buf[0] = 'A';
    buf[1] = '\0';
    // For GET, ignore the remaining request headers
    if (strcasecmp(method, "GET") == 0)
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
    else    /* POST */
    {
        // For POST, read the request length, i.e., Content-Length
        numchars = get_line(client, buf, sizeof(buf));
        while ((numchars > 0) && strcmp("\n", buf)) {
            buf[15] = '\0';
            if (strcasecmp(buf, "Content-Length:") == 0)
                content_length = atoi(&(buf[16]));
            numchars = get_line(client, buf, sizeof(buf));
        }
        // If the request length is invalid (for example, not a number at all), return an error
        // The error handling is fairly complete
        if (content_length == -1) {
            bad_request(client);
            return;
        }
    }

    // After the checks above, return 200 to the client directly
    // But errors may occur below, so this is an imprecise detail
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);

    // Allocate pipes for input and output
    // Exit on error
    if (pipe(cgi_output) < 0) {
        cannot_execute(client);
        return;
    }
    if (pipe(cgi_input) < 0) {
        cannot_execute(client);
        return;
    }

    // Then fork itself to create two processes
    if ((pid = fork()) < 0) {
        cannot_execute(client);
        return;
    }
    // The child process calls the CGI script
    if (pid == 0)  /* child: CGI script */
    {
        // Environment-variable buffers; of course this risks overflow
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        // Redirect the pipes
        // Bind the parent's pipe descriptors to the child's stdin and stdout
        // dup2 is similar in purpose to freopen()
        dup2(cgi_output[1], 1);
        dup2(cgi_input[0], 0);
        // Close unnecessary descriptors
        // This may not be easy to understand; a pipe diagram would help
        close(cgi_output[0]);
        close(cgi_input[1]);
        // Set basic CGI environment variables here
        // Request method, query parameters, content length, etc.
        sprintf(meth_env, "REQUEST_METHOD=%s", method);
        putenv(meth_env);
        if (strcasecmp(method, "GET") == 0) {
            sprintf(query_env, "QUERY_STRING=%s", query_string);
            putenv(query_env);
        }
        else {   /* POST */
            sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
            putenv(length_env);
        }

        // Finally, the child process uses the exec family to invoke an external script
        execl(path, path, NULL);
        exit(0);
    } else {    /* parent */
        // On the parent side, also close unnecessary descriptors
        close(cgi_output[1]);
        close(cgi_input[0]);
        // For POST requests, write() directly to the child process
        // So the script invoked by the child process can read POST data from stdin
        if (strcasecmp(method, "POST") == 0)
            for (i = 0; i < content_length; i++) {
                recv(client, &c, 1, 0);
                write(cgi_input[1], &c, 1);
            }
        // Then the parent reads all results from the output pipe and returns them to the client
        while (read(cgi_output[0], &c, 1) > 0)
            send(client, &c, 1, 0);

        close(cgi_output[0]);
        close(cgi_input[1]);
        // Finally wait for the child process to finish
        waitpid(pid, &status, 0);
    }
}

/**********************************************************************/
/* Get a line from a socket, whether the line ends in a newline,
 * carriage return, or a CRLF combination.  Terminates the string read
 * with a null character.  If no newline indicator is found before the
 * end of the buffer, the string is terminated with a null.  If any of
 * the above three line terminators is read, the last character of the
 * string will be a linefeed and the string will be terminated with a
 * null character.
 * Parameters: the socket descriptor
 *             the buffer to save the data in
 *             the size of the buffer
 * Returns: the number of bytes stored (excluding null) */
/**********************************************************************/
// Read a line from the socket; performance will likely be poor because it reads one character at a time
int get_line(int sock, char *buf, int size) {
    int i = 0;
    char c = '\0';
    int n;

    while ((i < size - 1) && (c != '\n')) {
        // Read only one character each time (this will be very slow)
        n = recv(sock, &c, 1, 0);
        /* DEBUG printf("%02X\n", c); */
        if (n > 0) {
            // Detect whether a CRLF was read
            if (c == '\r') {
                /*
                 * Note the MSG_PEEK flag: it means not removing the data read from the TCP buffer
                 * That is, if you read again, you will get the same data as before
                 * You can first inspect what was read, then on the second read, read the amount you need
                 * Essentially peeking ahead at the next part of the data stream
                 */
                n = recv(sock, &c, 1, MSG_PEEK);
                /* DEBUG printf("%02X\n", c); */
                // If the next character is \n, everything is fine; read it and add it to the buffer
                if ((n > 0) && (c == '\n'))
                    recv(sock, &c, 1, 0);
                else
                    // If it is not, something is wrong: there is \r but no \n; manually add a \n
                    // Since the previous character has not been removed from the TCP buffer, the next recv will still read this character
                    c = '\n';
            }
            buf[i] = c;
            i++;
        }
        else
            // If no data is read, set the condition to exit directly
            // This is effectively a break
            c = '\n';
    }
    // Terminate the string
    buf[i] = '\0';
    return (i);
}

/**********************************************************************/
/* Return the informational HTTP headers about a file. */
/* Parameters: the socket to print the headers on
 *             the name of the file */
/**********************************************************************/
// This function simply returns HTTP header information
void headers(int client, const char *filename) {
    char buf[1024];
    // This line is meaningless
    (void) filename;  /* could use filename to determine file type */

    strcpy(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
}

/**********************************************************************/
/* Give a client a 404 not found status message. */
/**********************************************************************/
// Return a 404 error
// Writing it this way is really awkward...
void not_found(int client) {
    char buf[1024];

    sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "your request because the resource specified\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "is unavailable or nonexistent.\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client, buf, strlen(buf), 0);
}

/**********************************************************************/
/* Send a regular file to the client.  Use headers, and report
 * errors to client if they occur.
 * Parameters: a pointer to a file structure produced from the socket
 *              file descriptor
 *             the name of the file to serve */
/**********************************************************************/
// This function is simple: read a static file and return it
void serve_file(int client, const char *filename) {
    FILE *resource = NULL;
    int numchars = 1;
    char buf[1024];

    buf[0] = 'A';
    buf[1] = '\0';
    // First read and discard all request headers
    while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
        numchars = get_line(client, buf, sizeof(buf));

    // Then open the file
    resource = fopen(filename, "r");

    // Still checks whether the file exists, but it is actually meaningless at this point
    if (resource == NULL)
        not_found(client);
    else {
        headers(client, filename);
        cat(client, resource);
    }
    fclose(resource);
}

/**********************************************************************/
/* This function starts the process of listening for web connections
 * on a specified port.  If the port is 0, then dynamically allocate a
 * port and modify the original port variable to reflect the actual
 * port.
 * Parameters: pointer to variable containing the port to connect on
 * Returns: the socket */
/**********************************************************************/
int startup(u_short *port) {
    int httpd = 0;
    struct sockaddr_in name;
    // Create a socket
    httpd = socket(PF_INET, SOCK_STREAM, 0);
    if (httpd == -1)
        error_die("socket");
    // Fill the struct
    memset(&name, 0, sizeof(name));
    name.sin_family = AF_INET;
    name.sin_port = htons(*port);
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    // Bind the socket to the corresponding port
    if (bind(httpd, (struct sockaddr *) &name, sizeof(name)) < 0)
        error_die("bind");
    if (*port == 0)  /* if dynamically allocating a port */
    {
        int namelen = sizeof(name);
        /*
         *  1. getsockname() can obtain an address associated with a socket
         *    1) On the server side, it can be used to get the relevant client address
         *    2) On the client side, it can get the IP and port of the currently connected socket
         *  2. It is useful when the client does not bind and connects directly to the server, and the client needs to know which IP is being used for communication (e.g., multiple NICs).
         */
        if (getsockname(httpd, (struct sockaddr *) &name, &namelen) == -1)
            error_die("getsockname");
        *port = ntohs(name.sin_port);
    }
    // Finally start listening
    if (listen(httpd, 5) < 0)
        error_die("listen");
    return (httpd);
}

/**********************************************************************/
/* Inform the client that the requested web method has not been
 * implemented.
 * Parameter: the client socket */
/**********************************************************************/
void unimplemented(int client) {
    char buf[1024];

    sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</TITLE></HEAD>\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client, buf, strlen(buf), 0);
}

/**********************************************************************/

int main(void) {
    int server_sock = -1;
    u_short port = 0;
    int client_sock = -1;
    struct sockaddr_in client_name;
    socklen_t client_name_len = sizeof(client_name);
    pthread_t newthread;

    // If port = 0, the port is random
    server_sock = startup(&port);
    printf("httpd running on port %d\n", port);

    // Infinite loop: one request creates one loop
    // Set an exit condition here to avoid syntax-check issues
    while (1) {
        client_sock = accept(server_sock,
                             (struct sockaddr *) &client_name,
                             &client_name_len);
        if (client_sock == -1)
            error_die("accept");
        /* accept_request(client_sock); */
        // Create a thread: accept_request is the thread handler; client_sock is the argument; note the argument type cast here
        if (pthread_create(&newthread, NULL, accept_request, (void *) &client_sock) != 0) {
            perror("pthread_create");
            break;
        }
    }

    // On unexpected exit, remember to close the socket
    close(server_sock);

    return (0);
}
comments powered by Disqus
Published:
2015-07-25
Category:
Tag:
C7