/* 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 #include #include #include #include #include #include #include #include #include #include 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; }