This year I wanted to learn a bit more about programming in C. I bought "C Programming Language" book from Kernighan & Ritchie like two months ago then I've read like 100 pages, done some exercises and parked whole thing. I thought I won't be coming back to it but recently on X there were a lot of Zig/C related content (especially "I wrote numpy in C" kind of posts) and someone recommended CodeCrafters challenge. Usually challenges are paid, but this one was free in June. It's about writing your own HTTP Server through eleven stages. I tried to do it in C, I finished it and wanted to share my code.
Dependencies, data structures & helpers #
I will start with just sharing imported libraries and some data structures.
#include <errno.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <zlib.h>
#define PORT 8000
#define BUFFER_SIZE 4096
enum response_status {
OK = 0,
CREATED,
NOT_FOUND,
};
enum response_content_type {
TEXT_PLAIN,
APPLICATION_OCTET_STREAM,
};
// We will try to parse incoming request into this struct,
// and then later prepare proper response
struct request {
char method[6];
char path[256];
char *user_agent;
char *accept_encoding;
};
struct response {
enum response_status status;
enum response_content_type content_type;
char *body;
char *res_accept_encoding;
char *gzipped;
};
// Working with strings in C is so frustrating,
// I wrote these small helpers function just to avoid repeating myself.
// They work for purpose of this challenge, but I am not too familiar
// with C string issues (like buffer overflow) so don't use them in your code
bool starts_with(char *a, char *b) { return strncmp(a, b, strlen(b)) == 0; }
bool equals(char *a, char *b) { return strcmp(a, b) == 0; }
Gzip compression #
Now the hardest part - gzip compression. Most of the stages are quite easy, just read request or write response, do some routing based on path or http method. This one was quite challenging - I needed to google a lot and somehow I managed to finish it.
/// https://stackoverflow.com/a/57699371/7292958
size_t gzip(char *input, size_t input_size, char *output_pointer,
size_t output_size) {
z_stream z;
z.zalloc = Z_NULL;
z.zfree = Z_NULL;
z.opaque = Z_NULL;
z.avail_in = input_size;
z.next_in = (Bytef *)input;
z.avail_out = output_size;
z.next_out = (Bytef *)output_pointer;
deflateInit2(&z, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8,
Z_DEFAULT_STRATEGY);
deflate(&z, Z_FINISH);
deflateEnd(&z);
return (z.total_out);
}
Honestly I make it work by raw experimenting with it, reading C libs documentation is nightmare compares to node/python/go
content.
Parsing incoming request #
When we read
struct request parse_request(char *buffer) {
struct request req;
char *req_info_ptr = strtok(buffer, "\r\n");
// obviously request can have many more headers, but in this exercise
// we only care about those two
char *user_agent = NULL;
char *accept_encoding = NULL;
char *line;
while ((line = strtok(NULL, "\r\n")) != NULL) {
if (strncmp(line, "User-Agent", 10) == 0) {
user_agent = line + 12;
}
if (strncmp(line, "Accept-Encoding", 15) == 0) {
accept_encoding = line + 17;
}
}
char *method = strtok(req_info_ptr, " ");
char *path = strtok(NULL, " ");
if (method != NULL) {
strcpy(req.method, method);
}
if (path != NULL) {
strncpy(req.path, path, sizeof(req.path) - 1);
}
req.user_agent = user_agent;
req.accept_encoding = accept_encoding;
return req;
}
void prepare_response(struct request req, struct response *r, char *directory) {
if (equals(req.path, "/")) {
r->status = OK;
return;
}
if (starts_with(req.path, "/user-agent")) {
r->status = OK;
r->body = req.user_agent;
return;
}
if (starts_with(req.path, "/echo/")) {
r->status = OK;
r->body = req.path + 6;
return;
}
if (starts_with(req.path, "/files/")) {
if (directory == NULL) {
return;
}
char *sub_path = req.path + 7;
char file_path[BUFFER_SIZE] = {0};
strcat(file_path, directory);
strcat(file_path, sub_path);
if (equals(req.method, "POST")) {
FILE *file = fopen(file_path, "w");
if (file == NULL) {
return;
}
fprintf(file, "%s", r->body);
fclose(file);
r->status = CREATED;
return;
}
FILE *file = fopen(file_path, "r");
if (file == NULL) {
r->status = NOT_FOUND;
return;
}
char file_buffer[BUFFER_SIZE];
fgets(file_buffer, sizeof(file_buffer), file);
fclose(file);
r->content_type = APPLICATION_OCTET_STREAM;
r->body = file_buffer;
return;
}
}
struct request parse_request(char *buffer) {
struct request req;
char *req_info_ptr = strtok(buffer, "\r\n");
char *user_agent = NULL;
char *accept_encoding = NULL;
char *line;
while ((line = strtok(NULL, "\r\n")) != NULL) {
if (strncmp(line, "User-Agent", 10) == 0) {
user_agent = line + 12;
}
if (strncmp(line, "Accept-Encoding", 15) == 0) {
accept_encoding = line + 17;
}
}
char *method = strtok(req_info_ptr, " ");
char *path = strtok(NULL, " ");
if (method != NULL) {
strcpy(req.method, method);
}
if (path != NULL) {
strncpy(req.path, path, sizeof(req.path) - 1);
}
req.user_agent = user_agent;
req.accept_encoding = accept_encoding;
return req;
}
void send_response(int client_fd, struct response res) {
char *r = calloc(BUFFER_SIZE, sizeof(char));
strcat(r, "HTTP/1.1 ");
switch (res.status) {
case OK:
strcat(r, "200 OK\r\n");
break;
case CREATED:
strcat(r, "201 Created\r\n");
break;
case NOT_FOUND:
strcat(r, "404 Not Found\r\n");
break;
}
if (res.content_type == TEXT_PLAIN) {
strcat(r, "Content-Type: text/plain\r\n");
} else if (res.content_type == APPLICATION_OCTET_STREAM) {
strcat(r, "Content-Type: application/octet-stream\r\n");
}
bool send_gzipped = res.res_accept_encoding != NULL &&
strstr(res.res_accept_encoding, "gzip") != NULL &&
res.body != NULL;
if (send_gzipped) {
strcat(r, "Content-Encoding: gzip\r\n");
}
char content_length[36];
uLong con_len = 0;
if (res.body != NULL) {
con_len = strlen(res.body);
}
if (send_gzipped) {
size_t output_size = 128 + strlen(res.body);
char *gzipped = calloc(output_size, sizeof(char));
uLong output_len = gzip(res.body, strlen(res.body), gzipped, output_size);
res.gzipped = gzipped;
con_len = output_len;
res.body = "";
}
if (res.body != NULL || send_gzipped) {
sprintf(content_length, "Content-Length: %lu\r\n\r\n", con_len);
strcat(r, content_length);
strcat(r, res.body);
} else {
strcat(r, "\r\n");
}
send(client_fd, r, strlen(r), 0);
if (send_gzipped) {
send(client_fd, res.gzipped, con_len, 0);
}
}
int main(int argc, char *argv[]) {
setbuf(stdout, NULL);
setbuf(stderr, NULL);
char *directory = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--directory") == 0) {
directory = argv[i + 1];
break;
}
}
if (directory != NULL) {
printf("Directory: %s\n", directory);
} else {
printf("Error: --directory option not provided.\n");
}
int server_fd;
socklen_t client_addr_len;
struct sockaddr_in client_addr;
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
printf("Socket creation failed: %s...\n", strerror(errno));
return 1;
}
int reuse = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) <
0) {
printf("SO_REUSEADDR failed: %s \n", strerror(errno));
return 1;
}
struct sockaddr_in serv_addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr = {htonl(INADDR_ANY)},
};
if (bind(server_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0) {
printf("Bind failed: %s \n", strerror(errno));
return 1;
}
int connection_backlog = 5;
if (listen(server_fd, connection_backlog) != 0) {
printf("Listen failed: %s \n", strerror(errno));
return 1;
}
while (1) {
printf("Waiting for a client to connect...\n");
client_addr_len = sizeof(client_addr);
int client_fd =
accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd == -1) {
printf("Accept failed: %s\n", strerror(errno));
return 1;
}
printf("Client connected\n");
ssize_t bytes_read;
char buffer[BUFFER_SIZE];
bytes_read = recv(client_fd, buffer, BUFFER_SIZE, 0);
if (bytes_read == -1) {
printf("Receive failed: %s\n", strerror(errno));
return 1;
}
struct request req = parse_request(buffer);
struct response r = {.status = OK,
.body = NULL,
.content_type = TEXT_PLAIN,
.res_accept_encoding = req.accept_encoding};
prepare_response(req, &r, directory);
send_response(client_fd, r);
printf("Closing client connection...\n");
close(client_fd);
}
close(server_fd);
return 0;
}