summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Kling <anton@kling.gg>2024-10-14 16:31:25 +0200
committerAnton Kling <anton@kling.gg>2024-10-14 16:31:25 +0200
commit3011c0589b8ab7cbfed127ce45a04f154441b406 (patch)
tree80167447952ccc80a8d6a5ec556dd3255cb50196
parenta67d4744d78fc58846f29667ae49cee70fa0bf16 (diff)
httpd: Add a simple http server
-rw-r--r--userland/httpd/Makefile13
-rw-r--r--userland/httpd/httpd.c342
2 files changed, 355 insertions, 0 deletions
diff --git a/userland/httpd/Makefile b/userland/httpd/Makefile
new file mode 100644
index 0000000..cc9eb16
--- /dev/null
+++ b/userland/httpd/Makefile
@@ -0,0 +1,13 @@
+CC="i686-sb-gcc"
+CFLAGS = -ggdb -O3 -flto -Wall -Wextra -Werror -Wno-pointer-sign -Wno-int-conversion -static
+BINS=httpd
+all: $(BINS)
+
+httpd.o: httpd.c
+ $(CC) $(CFLAGS) -o $@ -c $<
+
+httpd: httpd.o
+ $(CC) $(CFLAGS) -o $@ $^
+
+clean:
+ rm $(BINS) *.o
diff --git a/userland/httpd/httpd.c b/userland/httpd/httpd.c
new file mode 100644
index 0000000..845355a
--- /dev/null
+++ b/userland/httpd/httpd.c
@@ -0,0 +1,342 @@
+#include <arpa/inet.h>
+#include <assert.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <queue.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/sendfile.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <tb/sb.h>
+#include <tb/sv.h>
+#include <unistd.h>
+
+#define TYPE_LISTEN_SOCKET 1
+#define TYPE_LISTEN_CLIENT 2
+
+#define REQUEST_GET_DATA 0
+#define REQUEST_SEND_HEADER 1
+#define REQUEST_SEND_DATA 2
+
+struct sb response;
+
+struct sv default_error = C_TO_SV("ERROR");
+
+struct http_request {
+ int socket;
+ char buffer[8192];
+ struct sb client_buffer;
+ int state;
+
+ int file_fd;
+ int is_directory;
+ int status_code;
+ int header_sent;
+};
+
+int set_fd_nonblocking(int fd) {
+ int flags = fcntl(fd, F_GETFL, 0);
+ if (-1 == flags) {
+ return 0;
+ }
+ flags |= O_NONBLOCK;
+ if (0 != fcntl(fd, F_SETFL, flags)) {
+ return 0;
+ }
+ return 1;
+}
+
+void request_reinit(struct http_request *request, int q) {
+ request->state = REQUEST_GET_DATA;
+
+ sb_reset(&request->client_buffer);
+ struct queue_entry e;
+ e.fd = request->socket;
+ e.data = request;
+ e.data_type = TYPE_LISTEN_CLIENT;
+ e.listen = QUEUE_WAIT_READ | QUEUE_WAIT_CLOSE;
+ queue_mod_entries(q, QUEUE_MOD_CHANGE, &e, 1);
+}
+
+void httpd_handle_incoming(int socket, int q) {
+ for (;;) {
+ struct sockaddr_in cli;
+
+ socklen_t len = sizeof(cli);
+ int connfd = accept(socket, (struct sockaddr *)&cli, &len);
+ if (-1 == connfd) {
+ break;
+ }
+
+ int flag = 1;
+ assert(0 == setsockopt(connfd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag,
+ sizeof(int)));
+
+ struct queue_entry e;
+ e.fd = connfd;
+ struct http_request *request = malloc(sizeof(struct http_request));
+ assert(request); // TODO: Don't assume malloc succeeds
+ request->state = REQUEST_GET_DATA;
+
+ set_fd_nonblocking(connfd);
+ request->socket = connfd;
+
+ sb_init_buffer(&request->client_buffer, request->buffer,
+ sizeof(request->buffer));
+ e.data = request;
+ e.data_type = TYPE_LISTEN_CLIENT;
+ e.listen = QUEUE_WAIT_READ | QUEUE_WAIT_CLOSE;
+ queue_mod_entries(q, QUEUE_MOD_ADD, &e, 1);
+ }
+}
+
+void parse_incoming_request(struct http_request *request) {
+ struct sv l = SB_TO_SV(request->client_buffer);
+ struct sv method = sv_split_delim(l, &l, ' ');
+ struct sv path = sv_split_space(l, &l);
+
+ request->file_fd = -1;
+ request->is_directory = 0;
+
+ if (!sv_eq(method, C_TO_SV("GET"))) {
+ request->status_code = 400;
+ return;
+ }
+
+ char *p = SV_TO_C(path);
+ request->file_fd = open(p, O_RDONLY);
+ free(p);
+ if (-1 == request->file_fd) {
+ request->status_code = 404;
+ return;
+ }
+ struct stat statbuf;
+ if (-1 == fstat(request->file_fd, &statbuf)) {
+ close(request->file_fd);
+ request->status_code = 500;
+ // The only possible error(I believe)
+ assert(ENOMEM == errno);
+ return;
+ }
+ request->is_directory = S_ISDIR(statbuf.st_mode);
+ request->status_code = 200;
+ return;
+}
+
+void httpd_handle_client(int q, int fd, void *data) {
+ struct queue_entry e;
+ struct http_request *request = data;
+ if (REQUEST_GET_DATA == request->state) {
+ char buffer[4096];
+ int rc;
+ if (-1 == (rc = read(fd, buffer, 4096))) {
+ if (EWOULDBLOCK != errno) {
+ goto end_connection;
+ }
+ perror("read");
+ return;
+ }
+ sb_append_buffer(&request->client_buffer, buffer, rc);
+ struct sv string_view = SB_TO_SV(request->client_buffer);
+ if (!sv_eq(C_TO_SV("\r\n\r\n"), sv_take_end(string_view, NULL, 4))) {
+ return;
+ }
+
+ e.fd = fd;
+ e.data = data;
+ e.data_type = TYPE_LISTEN_CLIENT;
+ e.listen = QUEUE_WAIT_WRITE | QUEUE_WAIT_CLOSE;
+ queue_mod_entries(q, QUEUE_MOD_CHANGE, &e, 1);
+
+ parse_incoming_request(request);
+
+ request->header_sent = 0;
+ request->state = REQUEST_SEND_HEADER;
+ }
+ if (REQUEST_SEND_HEADER == request->state) {
+ char buffer[4096];
+ if (request->is_directory) {
+ sprintf(buffer, "HTTP/1.1 %d\r\nKeep-Alive: timeout=5, max=1000\r\n",
+ request->status_code);
+ } else if (200 != request->status_code) {
+ sprintf(buffer,
+ "HTTP/1.1 %d\r\nKeep-Alive: timeout=5, "
+ "max=1000\r\nContent-Length: %lu\r\n\r\n",
+ request->status_code, default_error.length);
+ } else {
+ struct stat statbuf;
+ if (-1 == fstat(request->file_fd, &statbuf)) { // TODO: Use the old
+ // fstat call
+ perror("fstat");
+ assert(0);
+ }
+ sprintf(buffer,
+ "HTTP/1.1 %d\r\nKeep-Alive: timeout=5, "
+ "max=1000\r\nContent-Length: %llu\r\n\r\n",
+ request->status_code, statbuf.st_size);
+ }
+ size_t left = strlen(buffer) - request->header_sent;
+ if (left > 0) {
+ int rc;
+ if (-1 == (rc = write(fd, buffer + request->header_sent, left))) {
+ if (EWOULDBLOCK != errno) {
+ goto end_connection;
+ }
+ return;
+ }
+ request->header_sent += rc;
+ }
+ request->state = REQUEST_SEND_DATA;
+ }
+ if (REQUEST_SEND_DATA == request->state) {
+ if (200 != request->status_code) {
+ if (-1 == write(fd, default_error.s, default_error.length)) {
+ if (EWOULDBLOCK != errno) {
+ goto end_connection;
+ }
+ return;
+ }
+ request_reinit(request, q);
+ return;
+ }
+
+ if (request->is_directory) {
+ // No attempt at buffering directory listing
+ // since that is annoying and not as if a
+ // directory listing is super big or
+ // important.
+ // TODO: Do this when I am less lazy
+ DIR *directory = fdopendir(request->file_fd);
+ char path[256];
+ (void)getcwd(path, 256);
+
+ sb_reset(&response);
+ sb_append_sv(&response, C_TO_SV("<!DOCTYPE html><html>List of "));
+ sb_append(&response, path);
+
+ for (;;) {
+ struct dirent entries[128];
+ int n = readdir_multi(directory, entries, 128);
+ if (-1 == n) {
+ goto end_connection;
+ }
+ if (0 == n) {
+ break;
+ }
+ for (int i = 0; i < n; i++) {
+ sb_append_sv(&response, C_TO_SV("<br><a href=\""));
+ sb_append(&response, path);
+ sb_append(&response, entries[i].d_name);
+ sb_append_sv(&response, C_TO_SV("\">"));
+ sb_append(&response, entries[i].d_name);
+
+ sb_append_sv(&response, C_TO_SV("</a>"));
+ }
+ }
+ closedir(directory);
+ request->file_fd = -1; // The fd gets closed by the closedir call.
+ sb_append_sv(&response, C_TO_SV("</html>"));
+
+ struct sv s = SB_TO_SV(response);
+ char buffer[4096];
+ int rc = sprintf(buffer, "Content-Length: %lu\r\n\r\n", s.length);
+
+ request_reinit(request, q);
+
+ sb_prepend_buffer(&response, buffer, rc);
+ s = SB_TO_SV(response);
+
+ if (-1 == write(request->socket, s.s, s.length)) {
+ if (EWOULDBLOCK != errno) {
+ goto end_connection;
+ }
+ return;
+ }
+ return;
+ }
+
+ int err;
+ int rc = sendfile(request->socket, request->file_fd, NULL, 8192, &err);
+ if (0 != err) {
+ if (EWOULDBLOCK != errno) {
+ goto end_connection;
+ }
+ return;
+ }
+ if (8192 == rc) {
+ return;
+ }
+ close(request->file_fd);
+ request->file_fd = -1;
+ request_reinit(request, q);
+ }
+ return;
+end_connection:
+ free(request);
+ close(fd);
+ e.fd = fd;
+ e.data = NULL;
+ e.data_type = TYPE_LISTEN_CLIENT;
+ e.listen = 0;
+ queue_mod_entries(q, QUEUE_MOD_DELETE, &e, 1);
+}
+
+void httpd_loop(int socket) {
+ int q = queue_create();
+ struct queue_entry e;
+ e.fd = socket;
+ e.data = NULL;
+ e.data_type = TYPE_LISTEN_SOCKET;
+ e.listen = QUEUE_WAIT_READ;
+ queue_mod_entries(q, QUEUE_MOD_ADD, &e, 1);
+ for (;;) {
+ struct queue_entry events[512];
+ int rc = queue_wait(q, events, 512);
+ if (0 == rc) {
+ continue;
+ }
+ for (int i = 0; i < rc; i++) {
+ switch (events[i].data_type) {
+ case TYPE_LISTEN_SOCKET:
+ httpd_handle_incoming(socket, q);
+ break;
+ case TYPE_LISTEN_CLIENT:
+ httpd_handle_client(q, events[i].fd, events[i].data);
+ break;
+ }
+ }
+ }
+}
+
+int main(void) {
+ sb_init(&response);
+
+ int sockfd = socket(AF_INET, SOCK_NONBLOCK | SOCK_STREAM, 0);
+ if (-1 == sockfd) {
+ perror("socket");
+ return 1;
+ }
+
+ struct sockaddr_in servaddr;
+ memset(&servaddr, 0, sizeof(servaddr));
+
+ servaddr.sin_family = AF_INET;
+ servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
+ servaddr.sin_port = htons(80);
+
+ if (-1 == bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) {
+ perror("bind");
+ return 1;
+ }
+
+ set_fd_nonblocking(sockfd);
+
+ httpd_loop(sockfd);
+
+ close(sockfd);
+ return 0;
+}