Titan's Place


Creating an HTTP server in C

Introduction

Image

If you're anything like me, you are pretty interested in the lower level side of networking. C gives us the opportunity to look at what the computer handles networking from an application point of view. This allows us to see what's going on at a byte per byte level. This is why I wanted to build an HTTP server for myself.

Learning the basics of HTTP

The first thing we need to know is that HTTP uses different kinds of requests to ask what it wants. The most important of which is the GET request. This asks the server for the document specified at the URI (what comes after the domain name). It then responds with a message containing either an error or the document (usually html).

Let's look at a couple examples

GET /foo/bar HTTP/1.1
...

The word get is the request type (called a verb). This specifies what the computer wants the server to do. Next we have the URI. This is similar to how directories are listed in UNIX. It is requesting bar in the foo directory. The last part of the first line of the request is the HTTP version. The one most commonly used is HTTP 1.1. This is the one we will use in this post. After this would be the headers, but handling this is outside the scope of this post.

Next we have the response we have to build

HTTP 1.1 /1.1 200 OK
...

When the request is successful, the server replies with the code 200. The server then appends the requested document to the end of the response.

Getting to the code

We first have to figure out how to decode the URI. For the sake of simplicity we are simply just going to interpret this as a file system with the root as bin. All of our html files will be in here.

if(strcmp(client_request->html_page_path, "/") == 0){
    char* default_page = "/index.html";
    free(client_request->html_page_path);
    client_request->html_page_path = (char*) malloc(strlen(default_page) + 1);
    strcpy(client_request->html_page_path, default_page);
  }

//Adding the path to the website directory
 const char* website_directory = "bin";
char* tmp_path = (char*) malloc(strlen(client_request->html_page_path) + strlen(website_directory) + 1);
strcpy(tmp_path, website_directory);
strcat(tmp_path, client_request->html_page_path);

This is a good amount of code but the basic idea is simple. If the URI is the index or " / " we serve index.html or else it is going to keep the file name the user gave and prefix the file directory we want. This will give us the file location we want.

Here is the next piece of code.

if(access(client_request->html_page_path, F_OK) == 0){
    char http_header[70];
    snprintf(http_header, 70, "HTTP/1.1 200 OK\r\nContent-Type:%s\r\n\r\n", get_mime_type(client_request->html_page_path));
    char* html_document = NULL;
    int file_length = load_html_document(client_request->html_page_path, &html_document);
    message_length = strlen(http_header) + file_length + 1; 
    *full_http_message = (char*) malloc(message_length);
    strcpy(*full_http_message, http_header);
    memcpy(*full_http_message + strlen(http_header), html_document, file_length);

    free(html_document);
  }

The idea of this piece of code is also simple. If we find the file, we are going to reply with HTTP/1.1 200 OK. Then we are going to put what kind of file it is. This would usually be an html file but it could be JavaScript or CSS files. Finally we are going to append the raw bytes of the files to end.

Note: The file type, or the MIME type is very important, your browser will by default interpret everything as html files so if you don't include the MIME type, the browser will not register any pictures, JavaScript, or CSS.

Connecting to other computers

Now that we can parse HTTP requests and send a response we need to actually be able to send it. This is where the concept of a socket comes in handy. This lets us connect to another computer using TCP on the port we specify. Usually browsers expect the servers to be on Port 80. In this post, our server will bind to Port 8080 to avoid having to use any root privileges.

Note: Any ports below 1024 are reserved and require root to be able to bind to

 // Creating socket file descriptor
   if ((*server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
     perror("socket failed");
     exit(EXIT_FAILURE);
   }

   if (setsockopt(*server_fd, SOL_SOCKET,
     SO_REUSEADDR | SO_REUSEPORT, &opt,
     sizeof(opt))) {
     perror("setsockopt");
     exit(EXIT_FAILURE);
    }
    (*address).sin_family = AF_INET;
    (*address).sin_addr.s_addr = INADDR_ANY;
    (*address).sin_port = htons(PORT);

  if (bind(*server_fd, (struct sockaddr*)address,sizeof(*address))< 0) {
    perror("bind failed");
    exit(EXIT_FAILURE);
  }
  if (listen(*server_fd, 3) < 0) {
    perror("listen");
    exit(EXIT_FAILURE);
  }

The idea behind this code is that we are creating a socket that is using TCP and binding to the port specified in the PORT macro. Then we listen until someone connects.

After this all we need to do is read in the request from the client and return a response based on what we talked about above.

Once we do this we will get the full response if we use the following command:

$ curl -v 127.0.0.1

Here is the reponse:

 > GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.8.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Type:text/html
<
* no chunk, no close, no size. Assume close to signal end
<!DOCTYPE html>

<html>
  <head>
    <title>Supertitan's test</title>
    <link rel="shortcut icon" type="image/png" href="website_icon.png">
  </head>
  <body>
    <h1>This is the best test ever!!</h1>
  </body>
</html>

If we use a browser we will get the following:

Image

Full Source Code: Simple-HTTP


Leave a comment:

Comments: