Are you even a systems engineer if you haven't built an HTTP server in C? This one's mine. I accidently discovered path traversal vulnerabilities through this project! Read more about it here: https://www.ayushr.dev/posts/path-traversal-vulnerability
This server uses non-blocking I/O to allow for constant time polling using kqueue. This allows the server to support concurrent connections without the need for separate threads and O(N) time waiting.
int kq = kqueue();
struct kevent event, events[MAX_EVENTS];
EV_SET(&event, serverfd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &event, 1, NULL, 0, NULL);
while (1) {
int nevn = kevent(kq, NULL, 0, events, MAX_EVENTS, NULL);
if (nevn == -1) {
perror("kevent wait");
break;
}
for (int i = 0; i < nevn; i++) {
if (events[i].ident == serverfd) {
// accept new connection
EV_SET(&event, clientfd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &event, 1, NULL, 0, NULL);
printf("Client connected!\n");
}
// existing connections
else {
// service client
// close connection
struct kevent event;
EV_SET(&event, events[i].ident, EVFILT_READ, EV_DELETE, 0, 0, NULL);
kevent(kq, &event, 1, NULL, 0, NULL);
close(clientfd);
}
}
}
Since network traffic can be unpredicatable and messages can be dropped, messages are sent until either the complete message gets sent or an error occurs. EINTR interrupts are ignored.
/*
Sends message persistently - packet drop resistant
Input: sockfd - receiver
msg - message to send
len - length of msg
flags - options
Output: null
*/
void send_message(int sockfd, char* msg, int len, int flags) {
int bytes_sent, total = 0;
while (total != len) {
bytes_sent = send(sockfd, msg + total, len - total, 0);
if (bytes_sent == -1 && errno == EINTR) {
continue;
}
if (bytes_sent == -1) {
printf("send error: %s\n", strerror(errno));
exit(1);
}
total += bytes_sent;
}
}
When files are bigger than the buffer size (1024 by default), files are split into chunks of 1024 bytes and streamed over multiple packets.
// Stream the file
long total_bytes_read = 0;
while (total_bytes_read < size) {
long read_size = size - total_bytes_read > BUF_SIZE ? BUF_SIZE : size - total_bytes_read;
size_t bytes_read = fread(file_buf, 1, read_size, f);
if (bytes_read == 0) {
if (feof(f)) {
break;
}
else {
printf("fread error: error reading file.\n");
fclose(f);
exit(1);
}
}
send_message(clientfd, file_buf, bytes_read, 0);
total_bytes_read += bytes_read;
}
fclose(f);
$ gcc -Wall -Werror -o main main.c
$ ./main
Responds with 200 OK with an empty body
Responds with 200 OK with str as the body
Responds with 200 OK with the value of the user-agent header as the body
Note: If you are using curl, you can use the --header flag to supply custom header values
Responds with 200 OK if the file exists in the files
directory, and returns a response with the Content-Type and Content-Length headers and the file contents as the body
Responds with 404 Not Found if file doesn't exist
Responds with 200 OK and adds the file to the files
directory
Responds with 405 Not Allowed if any other method is used or the filepath is not in files
directory