Tinyhttpd study notes

Tinyhttpd

Tinyhttpd is a micro Web server written by J. David Blackstone in 1999, which realizes the function of a simple Http server. Its source code comes from SourceForge . This article is a version of learning notes found by the author on Github. The Github warehouse address is https://github.com/EZLippi/Tinyhttpd . With the help of the source code, the author reviews the knowledge of C + + network programming. The author's learning ideas are also based on the ideas provided by the warehouse. Thank the author for sharing.

1. Abstract

Tinyhttpd implements a simple Web Server, which can be used as a simple Web Server on Linux. After the server script is run, the client can remotely access the resources under the script folder with the help of the browser.

The functions contained and their functions are as follows:

  • Main: main function, the entry of code execution (of course, the constructor of global variables and other functions will be executed first);
  • accept_request: the function of the server to process the client, which is used to send the requested resources to the client (obtained by creating a thread here);
  • bad_request: the processing function of the client request error, which returns "HTTP/1.0 400 BAD REQUEST";
  • cat: send file function, read file information and send it to the client;
  • cannot_execute: unable to execute the processing function of cgi file, return "HTTP/1.0 500 Internal Server Error";
  • error_die: error message printed by the server;
  • execute_cgi: execute the function of cgi file and send the output information to the client;
  • get_line: the function of reading the message line by line;
  • headers: add the header of http message;
  • not_found: the processing function of non-existent resource returns "HTTP/1.0 404 NOT FOUND";
  • server_file: if the requested resource is not a cgi file, it will be sent directly to the client;
  • startup: server socket creation function, create socket, bind port number and listen to socket;
  • unimplemented: the return information of the method not implemented by the server;

The flow chart of TinyHttpd is as follows:

Above from Blog Thank you, blogger. You can easily see the whole file execution process from the above figure. The following is a running example of the source code.

2. Example

(before performing the following operations, you need to compile the text file to generate an executable file, and then enter make.)

First, run the httpd file on the server side. Here, I specify the port number 12345 in the source code.

Then enter the IP address and port number in the browser to access the "/ htdocs/index.html" file.

And enter the English color in the input box, such as "red", to execute the "/ htdocs/color.cgi" script on the server side:

At this time, the output log of the server is:

There is a bug stuck during operation. When connecting, keep the client connected. After entering Ctrl + C on the server, the following bug appears. This bug just closes the client:

3. Source code analysis

The source code interpretation order and Github Blogger The recommended is the same: Main - > startup - > accept_ request -> execute_ cgi. After understanding the general framework, take a closer look at the source code of each function.

main

int main(void)
{
  int server_sock = -1;
  u_short port = 12345;
  int client_sock = -1;
  struct sockaddr_in client_name;
  socklen_t client_name_len = sizeof(client_name);
  pthread_t newthread;
  server_sock = startup(&port);
  printf("httpd running on port %d\n", port);

  while (1)
  {
    //Accept request, function prototype
    //int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    client_sock = accept(server_sock,
                         (struct sockaddr *)&client_name,
                         &client_name_len);
    if (client_sock == -1)
      error_die("accept");
    /* accept_request(client_sock); */
    printf("Client's ip address is: %s\n", inet_ntoa(client_name.sin_addr));
    //Each time a request is received, a thread is created to process the received request
    //Client_ The sock is converted into an address and passed into pthread as a parameter_ create
    if (pthread_create(&newthread, NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
      perror("pthread_create");
  }
  close(server_sock);
  return (0);
}

startup

/**********************************************************************/
/* 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 server 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);
  // Bind socket
  if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
    error_die("bind");
  // If the port is not set, provide a random port
  if (*port == 0) /* if dynamically allocating a port */
  {
    socklen_t namelen = sizeof(name);
    if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
      error_die("getsockname");
    *port = ntohs(name.sin_port);
  }
  // monitor
  if (listen(httpd, 5) < 0)
    error_die("listen");
  return (httpd);
}

accept_request

// HTTP request message from Github
// GET / HTTP/1.1
// Host: 192.168.0.23:47310
// Connection: keep-alive
// Upgrade-Insecure-Requests: 1
// User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36
// Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*; q = 0.8
// Accept - Encoding: gzip, deflate, sdch
// Accept - Language : zh - CN, zh; q = 0.8
// Cookie: __guid = 179317988.1576506943281708800.1510107225903.8862; monitor_count = 5

/**********************************************************************/
/* 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(void *arg)
{
  // socket
  int client = (intptr_t)arg;
  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;

  //According to the above Get request, you can see that the first line is taken here
  //The first http message is being processed here
  //"GET / HTTP/1.1\n"
  numchars = get_line(client, buf, sizeof(buf)); // get_line reads a line and ends with '\ 0'
  i = 0;
  j = 0;
  //First line string extract Get 
  //Determine whether it is a space #define ISspace(x) isspace((int)(x))
  while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
  {
    method[i] = buf[j];
    i++;
    j++;
  }
  //end
  method[i] = '\0';

  //Judge whether it is Get or Post
  if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
  {
    unimplemented(client); // If not GET or POST
    return;
  }

  //If it is POST, cgi is set to 1
  if (strcasecmp(method, "POST") == 0)
    cgi = 1;

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

  //Get "/" note: if your HTTP address is http://192.168.0.23:47310/index.html
  //               Then the first HTTP message you get is get / index HTML http / 1.1, then
  //               The parsing result is / index html
  while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
  {
    url[i] = buf[j];
    i++;
    j++;
  }
  url[i] = '\0';

  //Judge Get request
  if (strcasecmp(method, "GET") == 0)
  {
    query_string = url;
    while ((*query_string != '?') && (*query_string != '\0'))
      query_string++;
    if (*query_string == '?') // Does the URL of the cgi file start with this
    {
      cgi = 1;
      *query_string = '\0';
      query_string++;
    }
  }

  //route
  sprintf(path, "htdocs%s", url);

  //The default address. If the resolved path is /, the index will be added automatically html
  if (path[strlen(path) - 1] == '/')
    strcat(path, "index.html");

  printf("Method is : %s\n", method);
  printf("URL is : %s\n", url);
  printf("Path is : %s\n", path);

  //Get file information
  if (stat(path, &st) == -1)
  {
    //Read out all http information and discard it
    while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
      numchars = get_line(client, buf, sizeof(buf));

    //Can't find
    not_found(client);
  }
  else
  {
    if ((st.st_mode & S_IFMT) == S_IFDIR)
      strcat(path, "/index.html");
    //If your file has execution permission by default, it will be automatically parsed into a cgi program. If you have execution permission but cannot execute, you will receive an error signal
    if ((st.st_mode & S_IXUSR) ||
        (st.st_mode & S_IXGRP) ||
        (st.st_mode & S_IXOTH))
      cgi = 1;
    if (!cgi)
      //The read file is returned to the requested http client
      serve_file(client, path);
    else
      //Execute cgi file
      execute_cgi(client, path, method, query_string);
  }
  //Close socket after execution
  close(client);
}

The above function only processes the first line of the Http message header, that is, it only reads the request mode, URL and even the version number. If the rest of the Http message is a cgi file, it will enter execute_cgi function, otherwise enter the server_ In the file function.

execute_cgi

// POST / color1.cgi HTTP / 1.1
// Host: 192.168.0.23 : 47310
// Connection : keep - alive
// Content - Length : 10
// Cache - Control : max - age = 0
// Origin : http ://192.168.0.23:40786
// Upgrade - Insecure - Requests : 1
// User - Agent : Mozilla / 5.0 (Windows NT 6.1; WOW64) AppleWebKit / 537.36 (KHTML, like Gecko) Chrome / 55.0.2883.87 Safari / 537.36
// Content - Type : application / x - www - form - urlencoded
// Accept : text / html, application / xhtml + xml, application / xml; q = 0.9, image / webp, */*;q=0.8
// Referer: http://192.168.0.23:47310/
// Accept-Encoding: gzip, deflate
// Accept-Language: zh-CN,zh;q=0.8
// Cookie: __guid=179317988.1576506943281708800.1510107225903.8862; monitor_count=281
// Form Data
// color=red {the input info}

/**********************************************************************/
/* 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)
{
  //buffer
  char buf[1024];

  //2 pipes
  int cgi_output[2];
  int cgi_input[2];

  //Process pid and status
  pid_t pid;
  int status;

  int i;
  char c;

  //Number of characters read
  int numchars = 1;

  //http content_length
  int content_length = -1;

  //Default character
  buf[0] = 'A';
  buf[1] = '\0';

  //Ignore case comparison strings
  if (strcasecmp(method, "GET") == 0)
    //Read the data, read out the whole header, think Get is dead, and directly read the index HTML, there is no need to analyze the remaining http information
    while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
      numchars = get_line(client, buf, sizeof(buf));
  else /* POST */
  {
    numchars = get_line(client, buf, sizeof(buf));
    while ((numchars > 0) && strcmp("\n", buf))
    {
      //According to the HTTP message, if the request method is POST, the message contains content length
      //If it is a POST request, you need to get content length. Content length: this string is 15 bits in total, so
      //After taking out the header sentence, set the terminator in bit 16 for comparison
      //The 16th position is the end
      buf[15] = '\0';
      if (strcasecmp(buf, "Content-Length:") == 0)
        //The length of memory starts from bit 17, and converting all strings starting from bit 17 into integers is content_length
        content_length = atoi(&(buf[16]));
      // Read out the rest of the line
      numchars = get_line(client, buf, sizeof(buf));
    }
    if (content_length == -1)
    {
      bad_request(client);
      return;
    }
  }

  sprintf(buf, "HTTP/1.0 200 OK\r\n");
  send(client, buf, strlen(buf), 0);
  //Establish output pipeline
  if (pipe(cgi_output) < 0)
  {
    cannot_execute(client);
    return;
  }
  //Establish input pipeline
  if (pipe(cgi_input) < 0)
  {
    cannot_execute(client);
    return;
  }
  // 	   The 1 end of the pipe is write and the 0 end is read
  //       After fork, the pipes are copied, which are the same
  //       The child process closes two useless ports to avoid waste
  //       ×<------------------------->1    output
  //       0<-------------------------->×   input

  //       The parent process closes two useless ports to avoid waste
  //       0<-------------------------->×   output
  //       ×<------------------------->1    input
  //       At this point, the parent-child process can communicate

  //fork process, a subprocess used to execute CGI
  //The parent process is used to receive data and send reply data processed by the child process
  if ((pid = fork()) < 0)
  {
    cannot_execute(client);
    return;
  }
  if (pid == 0) /* child: CGI script */
  {
    char meth_env[255];
    char query_env[255];
    char length_env[255];

    //The child process output is redirected to the 1 end of the output pipeline
    dup2(cgi_output[1], 1);
    //The child process input is redirected to the 0 end of the input pipeline
    dup2(cgi_input[0], 0);

    //Close the useless pipe port
    close(cgi_output[0]);
    close(cgi_input[1]);

    //CGI environment variable
    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);
    }
    //Replace execution path
    execl(path, path, NULL);
    //int m = execl(path, path, NULL);
    //If there is a problem with the path, for example, change the html page to executable, but m is - 1 after execution
    //After exiting the child process, the pipeline is damaged, but the parent process is still writing something into it, triggering Program received signal SIGPIPE, Broken pipe
    exit(0);
  }
  else
  { /* parent */

    //Close the useless pipe port
    close(cgi_output[1]);
    close(cgi_input[0]);
    if (strcasecmp(method, "POST") == 0)
      for (i = 0; i < content_length; i++)
      {
        //Get the post request data and write it to the input pipeline for use by the child process
        recv(client, &c, 1, 0);
        write(cgi_input[1], &c, 1);
      }
    //Read the processed information of the child process from the output pipeline, and then send it out
    while (read(cgi_output[0], &c, 1) > 0)
      send(client, &c, 1, 0);

    //Close the pipe after the operation is completed
    close(cgi_output[0]);
    close(cgi_input[1]);

    //Wait for the child process to return
    waitpid(pid, &status, 0);
  }
}

This function contains two processes. The child process first sets the running environment (parameters) of the cgi file, and then executes color cgi file and pipeline the script output to the parent process, and then the parent process sends the input to the customer.

color.cgi

#!/usr/bin/perl -Tw

use strict;
use CGI;

my($cgi) = new CGI;

print $cgi->header('text/html');
print $cgi->start_html(-title => "Example CGI script",
                       -BGCOLOR => 'red');
print $cgi->h1("CGI Example");
print $cgi->p, "This is an example of CGI\n";
print $cgi->p, "Parameters given to this script:\n";
print "<UL>\n";
foreach my $param ($cgi->param)
{
 print "<LI>", "$param ", $cgi->param($param), "\n";
}
print "</UL>";
print $cgi->end_html, "\n";

server_file

/**********************************************************************/
/* 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];

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

  resource = fopen(filename, "r");
  if (resource == NULL)
    not_found(client);
  else
  {
    headers(client, filename);
    cat(client, resource);
  }
  fclose(resource);
}

If it is not a CGI file, this function directly reads the file and returns it to the requested http client.

The above are the main functions of the whole process. Some other sub functions are called in these functions, including error feedback, character reading and so on. Some of these sub functions are briefly introduced below.

get_line

/**********************************************************************/
/* 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) */
/**********************************************************************/

//Get a row of data. As long as c is found to be \ n, it is considered to be the end of a row. If you read it, \ r, use MSG again_ Read a character by peek. If it is \ n, read it from the socket
//If it is the next character, it will not be processed. Set c to \ N and end. If the read data is 0 or less than 0, it is also regarded as the end, and c is set to \ n
int get_line(int sock, char *buf, int size)
{
  int i = 0;
  char c = '\0';
  int n;

  while ((i < size - 1) && (c != '\n'))
  {
    n = recv(sock, &c, 1, 0);
    /* DEBUG printf("%02X\n", c); */
    if (n > 0)
    {
      if (c == '\r')
      {
        //Peek at a byte. If it is \ n read away, otherwise, add a new line directly after the character array
        n = recv(sock, &c, 1, MSG_PEEK);
        /* DEBUG printf("%02X\n", c); */
        if ((n > 0) && (c == '\n'))
          recv(sock, &c, 1, 0);
        else
          //Not \ n (read the next line of characters) or not, set c to \ n jump out of the loop and complete the reading of one line
          c = '\n';
      }
      buf[i] = c;
      i++;
    }
    else
      c = '\n';
  }
  buf[i] = '\0';
  return (i);
}

cat

/**********************************************************************/
/* 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 */
/**********************************************************************/

//Get the file content and send it
void cat(int client, FILE *resource)
{
  char buf[1024];

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

headers

/**********************************************************************/
/* Return the informational HTTP headers about a file. */
/* Parameters: the socket to print the headers on
 *             the name of the file */
/**********************************************************************/

//Add http headers
void headers(int client, const char *filename)
{
  char buf[1024];
  (void)filename; /* could use filename to determine file type */

  strcpy(buf, "HTTP/1.0 200 OK\r\n");
  send(client, buf, strlen(buf), 0);
  // #define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"
  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);
}

cannot_execute

/**********************************************************************/
/* Inform the client that a CGI script could not be executed.
 * Parameter: the client socket descriptor. */
/**********************************************************************/
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);
}

unimplemented

/**********************************************************************/
/* Inform the client that the requested web method has not been
 * implemented.
 * Parameter: the client socket */
/**********************************************************************/

//If the method is not implemented, this information is returned
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);
}

not_found

/**********************************************************************/
/* Give a client a 404 not found status message. */
/**********************************************************************/

//If the resource is not found, return the following information to the client
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);
}

bad_request

/**********************************************************************/
/* Inform the client that a request it has made has a problem.
 * Parameters: client socket */
/**********************************************************************/
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);
}

error_die

/**********************************************************************/
/* 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)
{
  perror(sc);
  exit(1);
}

Output the error information to the server window.

4. Makefile

Of course, Makefile needs to be used for C/C + + compilation under Linux. But the author has been delaying the study in this field before, so now I pick it up again.

all: httpd client
LIBS = -pthread # -lsocket
httpd: httpd.c
	# $(LIBS) take the value corresponding to LIBS
	# $@ represents the target file
	# $^ all dependent files
	# $< first dependent file
	gcc -g -W -Wall $(LIBS) -o $@ $<

client: simpleclient.c
	gcc -W -Wall -o $@ $<
clean:
	rm httpd

The main syntax of Makefile is as follows:

# Target target file
# Prerequisites files or targets required to generate target
# command make command to execute
target: prerequisties
	command

Here is a brief introduction to Makefile. Because there is too much content in it, I will write an article or a series of articles next time.

summary

TinyHttpd is a very simple Web Server, which realizes the functions of sending, receiving and processing Http messages. It uses network programming and multi process technology. This example is a very simple example of network programming.

Keywords: C Linux server

Added by jibster on Wed, 12 Jan 2022 13:58:48 +0200