commit 7a00b7ce2c4f163ee01e63376df753c8cb692125
Author: aabacchus <ben@bvnf.space>
Date: Thu, 26 May 2022 18:22:17 +0100
init
Diffstat:
A | Makefile | | | 19 | +++++++++++++++++++ |
A | bhttp.c | | | 350 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 369 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -0,0 +1,19 @@
+.POSIX:
+
+XCFLAGS = $(CFLAGS) -Wall -Wextra -pedantic -D_XOPEN_SOURCE=700
+XLDFLAGS = $(LDFLAGS)
+DEBUG = -Og -g -fsanitize=address
+
+BIN = bhttp
+OBJS = bhttp.o
+
+all: $(BIN)
+
+$(BIN): $(OBJS)
+ $(CC) $(XLDFLAGS) $(DEBUG) $(OBJS) -o $@
+
+.c.o:
+ $(CC) $(XCFLAGS) $(DEBUG) -c $<
+
+clean:
+ rm -f $(BIN) $(OBJS)
diff --git a/bhttp.c b/bhttp.c
@@ -0,0 +1,350 @@
+/* bhttp - shitty HTTP/1.0 server
+ *
+ * Written by phoebos (https://bvnf.space/) in 2022.
+ * This file is in the public domain.
+ *
+ * Whatever you do, don't use this!
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <netdb.h>
+#include <unistd.h>
+
+enum OP { GET, HEAD, UNKNOWN };
+
+struct header {
+ enum OP op;
+ char *path;
+ off_t size;
+};
+
+struct header *
+parse_header(char *b, size_t n) {
+ char *pathend;
+ char *space = memchr(b, ' ', n);
+ struct header *myhdr = malloc(sizeof(struct header));
+ if (myhdr == NULL) {
+ perror("malloc");
+ exit(1);
+ }
+ if (space == NULL) {
+ free(myhdr);
+ return NULL;
+ }
+ if (strncmp(b, "GET", space - b) == 0) {
+ myhdr->op = GET;
+ } else if (strncmp(b, "HEAD", space - b) == 0) {
+ myhdr->op = HEAD;
+ } else
+ myhdr->op = UNKNOWN;
+ if (space[1] != '/') {
+ free(myhdr);
+ return NULL;
+ }
+
+ pathend = memchr(space + 1, ' ', n - ((space + 1) - b));
+ if (pathend == NULL)
+ return NULL;
+ *pathend = '\0';
+ myhdr->path = malloc(pathend-space+1);
+ if (myhdr->path == NULL) {
+ perror("malloc");
+ exit(1);
+ }
+ if (snprintf(myhdr->path, pathend-space+1, ".%s", space+1) < 0) {
+ perror("snprintf");
+ exit(1);
+ }
+ fprintf(stderr, "request \"%s\"\n", myhdr->path+1);
+ return myhdr;
+}
+
+void
+status_reply(int code, int fd) {
+ if (code < 100 || code > 999) return;
+ if (code < 400) return;
+#define REASON_MAXLEN 21
+#define REPLY_MAXLEN (17+REASON_MAXLEN)
+ char *reason = NULL;
+ switch (code) {
+ break; case 400: reason = "Bad Request";
+ break; case 401: reason = "Unauthorized";
+ break; case 403: reason = "Forbidden";
+ break; case 404: reason = "Not Found";
+ break; case 500: reason = "Internal Server Error";
+ break; case 501: reason = "Not Implemented";
+ break; case 502: reason = "Bad Gateway";
+ break; case 503: reason = "Service Unavailable";
+ break; default: reason = "Internal Server Error";
+ }
+ char reply[REPLY_MAXLEN] = {'\0'};
+ int n = snprintf(reply, REPLY_MAXLEN, "HTTP/1.0 %0.3d %s\r\n\r\n", code, reason);
+ if (n < 0) {
+ perror("snprintf");
+ return;
+ }
+ if (send(fd, reply, n, 0) != n) {
+ perror("send");
+ return;
+ }
+ return;
+}
+
+void
+file_reply(int fd, struct header *h) {
+ if (access(h->path, R_OK) != 0) {
+ status_reply(404, fd);
+ return;
+ }
+ struct stat sb;
+redo_stat:
+ if (stat(h->path, &sb) != 0) {
+ status_reply(404, fd);
+ return;
+ }
+ if (S_ISDIR(sb.st_mode)) {
+ char *newpath = malloc(strlen(h->path) + 12);
+ if (newpath == NULL || snprintf(newpath, strlen(h->path) + 12, "%s/index.html", h->path) < 0) {
+ perror("strcat(dir,\"/index.html\")");
+ status_reply(500, fd);
+ return;
+ }
+ free(h->path);
+ h->path = newpath;
+ goto redo_stat;
+ }
+ int file = open(h->path, O_RDONLY);
+ if (file == -1) {
+ status_reply(404, fd);
+ return;
+ }
+
+ char *replyheader =
+ "HTTP/1.0 200 OK\r\n"
+ "Content-Length: %d\r\n"
+ /*"Content-Type: text/plain\r\n"*/
+ "Server: bhttp/0.1\r\n"
+ "\r\n";
+ int rn = snprintf(NULL, 0, replyheader, sb.st_size);
+ if (rn < 0) {
+ perror("snprintf");
+ exit(1);
+ }
+ char *reply = malloc(rn+1); /* +1 for the end null */
+ if (reply == NULL) {
+ perror("malloc");
+ exit(1);
+ }
+ snprintf(reply, rn+1, replyheader, sb.st_size);
+ if (send(fd, reply, rn, 0) != rn) {
+ perror("send");
+ return;
+ }
+
+ if (h->op != GET)
+ return; /* HEAD ends here. */
+
+ char *buf = malloc(sb.st_size);
+ if (buf == NULL) {
+ perror("malloc");
+ exit(1);
+ }
+
+ // TODO: handle r/w errors and partial r/ws
+ ssize_t n = read(file, buf, sb.st_size);
+ if (n == -1) {
+ perror("read");
+ free(buf);
+ exit(0);
+ }
+ write(fd, buf, (size_t)n);
+ free(buf);
+}
+
+void
+handle_conn(int fd) { /* called in child */
+ char buf[0x100] = {'\0'};
+ ssize_t n;
+ struct header *h = NULL;
+retry:
+ n = recv(fd, buf, 0x100, 0);
+ if (n == 0)
+ // we've been shutdown
+ return;
+ if (n == -1) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ goto retry;
+ perror("recv");
+ return;
+ }
+ h = parse_header(buf, (size_t)n);
+ if (h == NULL || h->path == NULL) {
+ status_reply(400, fd);
+ return;
+ }
+ if (h->op == GET || h->op == HEAD)
+ file_reply(fd, h);
+ free(h->path);
+ free(h);
+ return;
+}
+
+void
+usage(char *argv0) {
+ fprintf(stderr, "usage: %s [-p port] dir\n", argv0);
+}
+
+int
+start_listening(char *argv0, int port) {
+ int sfd = -1;
+ struct addrinfo hints = {0}, *result, *rp;
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_PASSIVE;
+ hints.ai_protocol = 0;
+
+ /* TODO: how long can a servname be? Do a n = snprintf(NULL, 0, ...) first
+ * to get the required size?
+ */
+ char port_string[10] = {'\0'};
+ snprintf(port_string, 10, "%d", port);
+
+ int s = getaddrinfo(NULL, port_string, &hints, &result);
+ if (s != 0) {
+ fprintf(stderr, "%s: getaddrinfo: %s\n", argv0, gai_strerror(s));
+ return -1;
+ }
+
+ for (rp = result; rp != NULL; rp = rp->ai_next) {
+ sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (sfd == -1)
+ continue;
+
+ if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0)
+ break;
+
+ if (close(sfd) == -1) {
+ perror("close");
+ freeaddrinfo(result);
+ return -1;
+ }
+ }
+
+ freeaddrinfo(result);
+
+ if (rp == NULL) {
+ fprintf(stderr, "%s: localhost:%s: connection refused\n", argv0, port_string);
+ return -1;
+ }
+
+ /* can have up to SOMAXCONN backlogged. */
+ if (listen(sfd, 10) == -1) {
+ fprintf(stderr, "%s: listen: %s\n", argv0, strerror(errno));
+ close(sfd);
+ return -1;
+ }
+
+ return sfd;
+}
+
+int
+accept_one(int sfd) {
+ int newfd;
+ struct sockaddr their_addr;
+ socklen_t addr_size = sizeof their_addr;
+ newfd = accept(sfd, &their_addr, &addr_size);
+ if (newfd == -1) {
+ perror("accept");
+ return -1;
+ }
+ // note: don't want to be DoS'd; unrestricted forking isn't a great idea.
+ switch (fork()) {
+ case -1:
+ return -1;
+ case 0:
+ close(sfd);
+ handle_conn(newfd);
+ shutdown(newfd, SHUT_RDWR);
+ close(newfd);
+ exit(0);
+ break;
+ default:
+ return 0;
+ }
+}
+
+int
+loop(int sfd) {
+ int pollret;
+ struct pollfd fds[1];
+ fds[0].fd = sfd;
+ fds[0].events = POLLIN;
+
+ while (1) {
+ fds[0].revents = 0;
+ pollret = poll(fds, 1, -1);
+ if (pollret < 0) {
+ if (errno == EAGAIN)
+ continue;
+ perror("poll");
+ return 1;
+ }
+ if (fds[0].revents) {
+ accept_one(sfd);
+ }
+ }
+ return 0;
+}
+
+int
+main(int argc, char **argv) {
+ int c, port, ret, sfd;
+ char *path;
+ ret = 0;
+ port = 80;
+
+ while ((c = getopt(argc, argv, "p:")) != -1) {
+ switch(c) {
+ case 'p':
+ port = atoi(optarg);
+ if (port <= 0) {
+ fprintf(stderr, "bad port number '%s'\n", optarg);
+ return 1;
+ }
+ break;
+ case '?':
+ usage(argv[0]);
+ return 1;
+ }
+ }
+ if (argc - optind != 1) {
+ usage(argv[0]);
+ return 1;
+ }
+
+ path = argv[optind];
+ if (chdir(path) == -1) {
+ fprintf(stderr, "couldn't chdir(%s): %s\n", path, strerror(errno));
+ return 1;
+ }
+
+ sfd = start_listening(argv[0], port);
+ if (sfd == -1) {
+ return 1;
+ }
+ fprintf(stderr, "bound to port %d\n", port);
+
+ if (loop(sfd) != 0)
+ ret = 1;
+
+ close(sfd);
+ return ret;
+}