[Source analysis] tinyhttpd-C language implements the simplest HTTP server

Keywords: socket Linux Makefile Unix

tinyhttpd is an ultra-lightweight Http Server with less than 500 lines. It's good for learning and can help us really understand the nature of server programs.

After looking at all the source code, I really feel that there are great gains, whether it is unix programming, or GET/POST Web processing process, are clear a lot. Don't talk nonsense, start our exploratory tour of Server.

(Limited level, if there are errors, please correct)

Project Home

http://sourceforge.net/projects/tinyhttpd/

Main functions

This is the declaration of all functions:

void accept_request(int);  
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);  

First, the function of each function is briefly explained.

accept_request: Handles an HTTP request that is monitored from a socket, where the server processes requests in a large part.

bad_request: This is an error request returned to the client. HTTP status 400 BAD REQUEST.

cat: Read a file on the server and write it to the socket socket.

cannot_execute: Mainly deals with errors that occur when executing cgi programs.

error_die: Write the error message to perror and exit.

execute_cgi: Processing that runs a CGI program is also a major function.

get_line: Read the line of the socket, and unify the return line to the end of the newline.

headers: Write the head of the HTTP response to the socket.

not_found: Mainly deals with situations where the requested file cannot be found.

sever_file: Call cat to return the server file to the browser.

startup: Initialize httpd services, including socket creation, port binding, monitoring, etc.

unimplemented: The method returned to the browser indicates that the HTTP request received is not supported.

Suggested source code reading order: main - > startup - > accept_request - > execute_cgi, after knowing the main work flow, carefully look at the source code of each function.

Workflow

(1) Server starts and binds httpd services on specified ports or randomly selected ports.

(2) When an HTTP request is received (actually the port accpet of listen), a thread is derived to run the accept_request function.

(3) Remove the method (GET or POST) and URL from the HTTP request. For GET methods, if you have parameters, then the query_string pointer points to the url? Later GET parameters.

(4) Format the url to path array to represent the server file path requested by the browser. In tinyhttpd, the server file is in the htdocs folder. When the url ends with / or is a directory, the default is to add index.html to the path to indicate access to the home page.

(5) If the file path is legitimate, for GET requests without parameters, output the server file directly to the browser, that is, write it to the socket in HTTP format and jump to (10). In other cases (GET with parameters, POST mode, url as executable file), the excute_cgi function is called to execute the CGI script.

(6) Read the entire HTTP request and discard it. If it is POST, find Content-Length. Write the HTTP 200 status code to the socket.

(7) Establish two pipelines, cgi_input and cgi_output, and fork a process.

(8) In the sub-process, STDOUT is redirected to the writing end of cgi_output, STDIN is redirected to the reading end of cgi_input, the writing end of cgi_input and the reading end of cgi_output are closed, the environment variables of request_method are set, the environment variables of query_string are set in GET, and the environment variables of content_length are set in POST. In order to call the CGI script, the CGI program is run with execl.

(9) In the parent process, close the read end of cgi_input and the write end of cgi_output. If POST, write POST data to cgi_input, which has been redirected to STDIN, read the pipeline output of cgi_output to the client, the pipeline input is STDOUT. Then close all pipes and wait for the child process to finish. This part is rather messy. See the following illustration:

 

Figure 1 Pipeline initial state

 

                          

Figure 2. Final state of pipeline

 

(10) Close the connection with the browser and complete an HTTP request and response because HTTP is connectionless.

 

Annotation version source code

The source code has been annotated and placed in Github: Here

Students who are too lazy to jump look at the following...

/* 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(int);
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 */
/**********************************************************************/
void accept_request(int client)
{
    char buf[1024];
    int numchars;
    char method[255];
    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;

    /*The first line of the request*/
    numchars = get_line(client, buf, sizeof(buf));
    i = 0; j = 0;
    /*Save the client's request method to the method array*/
    while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
    {
        method[i] = buf[j];
        i++; j++;
    }
    method[i] = '\0';

    /*If it is neither GET nor POST, it cannot be handled. */
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
    {
        unimplemented(client);
        return;
    }

    /* POST Turn on cgi when it's time */
    if (strcasecmp(method, "POST") == 0)
        cgi = 1;

    /*Read url address*/
    i = 0;
    while (ISspace(buf[j]) && (j < sizeof(buf)))
        j++;
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
    {
        /*Save url */
        url[i] = buf[j];
        i++; j++;
    }
    url[i] = '\0';

    /*Processing GET Method*/
    if (strcasecmp(method, "GET") == 0)
    {
        /* The pending request is url */
        query_string = url;
        while ((*query_string != '?') && (*query_string != '\0'))
            query_string++;
        /* GET The characteristics of the method, followed by parameters*/
        if (*query_string == '?')
        {
            /*Open cgi */
            cgi = 1;
            *query_string = '\0';
            query_string++;
        }
    }

    /*Format url to path arrays, html files are in htdocs*/
    sprintf(path, "htdocs%s", url);
    /*The default is index.html */
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");
    /*Find the corresponding file according to the path */
    if (stat(path, &st) == -1) {
        /*Throw out all headers*/
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
        /*The response client could not find it*/
        not_found(client);
    }
    else
    {
        /*If it is a directory, the index.html file under that directory is used by default.*/
        if ((st.st_mode & S_IFMT) == S_IFDIR)
            strcat(path, "/index.html");
      if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)    )
          cgi = 1;
      /*Instead of cgi, return the server file directly, otherwise execute CGI */
      if (!cgi)
          serve_file(client, path);
      else
          execute_cgi(client, path, method, query_string);
    }

    /*Disconnect from client (HTTP feature: no connection)*/
    close(client);
}

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

    /*Response to client error HTTP requests */
    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 */
/**********************************************************************/
void cat(int client, FILE *resource)
{
    char buf[1024];

    /*Read all the data in the file and write it to socket */
    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. */
/**********************************************************************/
void cannot_execute(int client)
{
    char buf[1024];

    /* Response client cgi cannot be executed*/
    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. */
/**********************************************************************/
void error_die(const char *sc)
{
    /*Error Information Processing */
    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;

    buf[0] = 'A'; buf[1] = '\0';
    if (strcasecmp(method, "GET") == 0)
        /*Read and discard all HTTP header s*/
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
    else    /* POST */
    {
        /* Find content_length in HTTP request for POST */
        numchars = get_line(client, buf, sizeof(buf));
        while ((numchars > 0) && strcmp("\n", buf))
        {
            /*Separation by0 */
            buf[15] = '\0';
            /* HTTP Characteristics of requests*/
            if (strcasecmp(buf, "Content-Length:") == 0)
                content_length = atoi(&(buf[16]));
            numchars = get_line(client, buf, sizeof(buf));
        }
        /*No content_length was found */
        if (content_length == -1) {
            /*Wrong request*/
            bad_request(client);
            return;
        }
    }

    /* Correct, HTTP status code 200 */
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);

    /* Build pipes*/
    if (pipe(cgi_output) < 0) {
        /*error handling*/
        cannot_execute(client);
        return;
    }
    /*Build pipes*/
    if (pipe(cgi_input) < 0) {
        /*error handling*/
        cannot_execute(client);
        return;
    }

    if ((pid = fork()) < 0 ) {
        /*error handling*/
        cannot_execute(client);
        return;
    }
    if (pid == 0)  /* child: CGI script */
    {
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        /* Redirect STDOUT to the write side of cgi_output */
        dup2(cgi_output[1], 1);
        /* Redirect STDIN to the reader of cgi_input */
        dup2(cgi_input[0], 0);
        /* Close the write end of cgi_input and the read end of cgi_output */
        close(cgi_output[0]);
        close(cgi_input[1]);
        /*Setting the environment variable of request_method*/
        sprintf(meth_env, "REQUEST_METHOD=%s", method);
        putenv(meth_env);
        if (strcasecmp(method, "GET") == 0) {
            /*Setting environment variables for query_string*/
            sprintf(query_env, "QUERY_STRING=%s", query_string);
            putenv(query_env);
        }
        else {   /* POST */
            /*Setting environment variables for content_length*/
            sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
            putenv(length_env);
        }
        /*Running cgi program with execl*/
        execl(path, path, NULL);
        exit(0);
    } else {    /* parent */
        /* Close the read end of cgi_input and the write end of cgi_output */
        close(cgi_output[1]);
        close(cgi_input[0]);
        if (strcasecmp(method, "POST") == 0)
            /*Receiving data from POST*/
            for (i = 0; i < content_length; i++) {
                recv(client, &c, 1, 0);
                /*Write POST data to cgi_input, and now redirect to STDIN */
                write(cgi_input[1], &c, 1);
            }
        /*The pipeline that reads cgi_output is exported to the client, and the pipeline input is STDOUT */
        while (read(cgi_output[0], &c, 1) > 0)
            send(client, &c, 1, 0);

        /*Close the pipeline*/
        close(cgi_output[0]);
        close(cgi_input[1]);
        /*Waiting for subprocesses*/
        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) */
/**********************************************************************/
int get_line(int sock, char *buf, int size)
{
    int i = 0;
    char c = '\0';
    int n;

    /*Unify termination conditions to n newline character and standardize buf arrays*/
    while ((i < size - 1) && (c != '\n'))
    {
        /*Receive only one byte at a time*/
        n = recv(sock, &c, 1, 0);
        /* DEBUG printf("%02X\n", c); */
        if (n > 0)
        {
            /*Receivingr continues to receive the next byte, because the newline character may bern */
            if (c == '\r')
            {
                /*The MSG_PEEK flag is used to make the next reading still get the content of this reading. It can be considered that the receiving window does not slide.*/
                n = recv(sock, &c, 1, MSG_PEEK);
                /* DEBUG printf("%02X\n", c); */
                /*But if it's a newline character, absorb it.*/
                if ((n > 0) && (c == '\n'))
                    recv(sock, &c, 1, 0);
                else
                    c = '\n';
            }
            /*Save to Buffer*/
            buf[i] = c;
            i++;
        }
        else
            c = '\n';
    }
    buf[i] = '\0';

    /*Returns the buf array size*/
    return(i);
}

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

    /*Normal HTTP header */
    strcpy(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    /*server information*/
    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. */
/**********************************************************************/
void not_found(int client)
{
    char buf[1024];

    /* 404 page */
    sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
    send(client, buf, strlen(buf), 0);
    /*server information*/
    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 */
/**********************************************************************/
void serve_file(int client, const char *filename)
{
    FILE *resource = NULL;
    int numchars = 1;
    char buf[1024];

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

    /*Open sever file*/
    resource = fopen(filename, "r");
    if (resource == NULL)
        not_found(client);
    else
    {
        /*Write HTTP header */
        headers(client, filename);
        /*Copy file*/
        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;

    /*Establishing socket */
    httpd = socket(PF_INET, SOCK_STREAM, 0);
    if (httpd == -1)
        error_die("socket");
    memset(&name, 0, sizeof(name));
    name.sin_family = AF_INET;
    name.sin_port = htons(*port);
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
        error_die("bind");
    /*If the current specified port is 0, a port is dynamically randomly allocated*/
    if (*port == 0)  /* if dynamically allocating a port */
    {
        int namelen = sizeof(name);
        if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
            error_die("getsockname");
        *port = ntohs(name.sin_port);
    }
    /*Start monitoring*/
    if (listen(httpd, 5) < 0)
        error_die("listen");
    /*Return socket id */
    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];

    /* HTTP method Not supported*/
    sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
    send(client, buf, strlen(buf), 0);
    /*server information*/
    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;
    int client_name_len = sizeof(client_name);
    pthread_t newthread;

    /*Establishing httpd service on corresponding ports*/
    server_sock = startup(&port);
    printf("httpd running on port %d\n", port);

    while (1)
    {
        /*Socket receives client connection request*/
        client_sock = accept(server_sock,(struct sockaddr *)&client_name,&client_name_len);
        if (client_sock == -1)
            error_die("accept");
        /*Derived new threads process new requests with accept_request function*/
        /* accept_request(client_sock); */
        if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)
            perror("pthread_create");
    }

    close(server_sock);

    return(0);
}

Compile and run

How can we get this server running? Looking at the comment area, there should be a lot of people encountering problems. Here I will briefly talk about them.

First, you need to modify the Makefile file file. If you execute make directly, you will encounter the error: cannot find-lsocket. Simply put, the solution is Linux There is no such library in the system, and the library is in the linux The implementation in libc is included by default at compile time, so - lsocket can be removed directly from Makefile. See: stackoverflow

In addition, under the htdocs file, there are cgi programs and html code. cgi is written in perl, but the location of the Perl executor declared in the file is wrong for me. Here, the Perl script is located in / usr/bin (which perl can be viewed), so change the first line of the cgi file to:

#!/usr/bin/perl -Tw  

It's all right now. After compiling mkae, run with. / httpd and enter 127.0.0.1: port in the browser, you can see the web page:

                                           

                                              

Posted by jcavard on Fri, 31 May 2019 14:48:08 -0700