bhttp

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 7a00b7ce2c4f163ee01e63376df753c8cb692125
Author: aabacchus <ben@bvnf.space>
Date:   Thu, 26 May 2022 18:22:17 +0100

init

Diffstat:
AMakefile | 19+++++++++++++++++++
Abhttp.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; +}