quark

quark web server
git clone git://git.suckless.org/quark
Log | Files | Refs | LICENSE

commit ccdb51b96d18c71f0155b669e3919cb604d8cc7b
parent d480539e3e33f602206efe8e32861dc780492704
Author: Laslo Hunhold <dev@frign.de>
Date:   Sun,  4 Feb 2018 21:27:33 +0100

Refactor the single source file into multiple modules

And many other things, too many to list here. For example, it now
properly logs uds instead of erroring out.
Separating concerns in many places definitely improves the readability.

Diffstat:
MLICENSE | 2++
MMakefile | 16++++++++++++----
Mconfig.mk | 2+-
Ahttp.c | 514+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttp.h | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain.c | 261+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dquark.c | 1166-------------------------------------------------------------------------------
Aresp.c | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aresp.h | 14++++++++++++++
Asock.c | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asock.h | 15+++++++++++++++
Autil.c | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autil.h | 27+++++++++++++++++++++++++++
13 files changed, 1345 insertions(+), 1171 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -2,6 +2,8 @@ ISC-License Copyright 2016-2017 Laslo Hunhold <dev@frign.de> +Copyright 2004 Ted Unangst <tedu@openbsd.org> +Copyright 2004 Todd C. Miller <Todd.Miller@courtesan.com> Copyright 2017 Hiltjo Posthuma <hiltjo@codemadness.org> Copyright 2017 Quentin Rameau <quinq@fifth.space> diff --git a/Makefile b/Makefile @@ -4,22 +4,30 @@ include config.mk +COMPONENTS = util sock http resp + all: quark -quark: quark.c arg.h config.h config.mk - $(CC) -o $@ $(CPPFLAGS) $(CFLAGS) quark.c $(LDFLAGS) +util.o: util.c util.h config.mk +sock.o: sock.c sock.h util.h config.mk +http.o: http.c http.h util.h http.h resp.h config.h config.mk +resp.o: resp.c resp.h util.h http.h config.mk +main.o: main.c util.h sock.h http.h arg.h config.h config.mk + +quark: $(COMPONENTS:=.o) $(COMPONENTS:=.h) main.o config.mk + $(CC) -o $@ $(CPPFLAGS) $(CFLAGS) $(COMPONENTS:=.o) main.o $(LDFLAGS) config.h: cp config.def.h $@ clean: - rm -f quark + rm -f quark main.o $(COMPONENTS:=.o) dist: rm -rf "quark-$(VERSION)" mkdir -p "quark-$(VERSION)" cp -R LICENSE Makefile arg.h config.def.h config.mk quark.1 \ - quark.c "quark-$(VERSION)" + $(COMPONENTS:=.c) $(COMPONENTS:=.h) main.c "quark-$(VERSION)" tar -cf - "quark-$(VERSION)" | gzip -c > "quark-$(VERSION).tar.gz" rm -rf "quark-$(VERSION)" diff --git a/config.mk b/config.mk @@ -9,7 +9,7 @@ MANPREFIX = $(PREFIX)/share/man # flags CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_DEFAULT_SOURCE -D_BSD_SOURCE -CFLAGS = -std=c99 -pedantic -Wall -Os +CFLAGS = -std=c99 -pedantic -Wno-unused-variable -Wno-implicit-function-declaration -Wall -Os LDFLAGS = -s # compiler and linker diff --git a/http.c b/http.c @@ -0,0 +1,514 @@ +/* See LICENSE file for copyright and license details. */ +#include <arpa/inet.h> +#include <ctype.h> +#include <errno.h> +#include <limits.h> +#include <regex.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "config.h" +#include "http.h" +#include "resp.h" +#include "util.h" + +const char *req_field_str[] = { + [REQ_HOST] = "Host", + [REQ_RANGE] = "Range", + [REQ_MOD] = "If-Modified-Since", +}; + +const char *req_method_str[] = { + [M_GET] = "GET", + [M_HEAD] = "HEAD", +}; + +const char *status_str[] = { + [S_OK] = "OK", + [S_PARTIAL_CONTENT] = "Partial Content", + [S_MOVED_PERMANENTLY] = "Moved Permanently", + [S_NOT_MODIFIED] = "Not Modified", + [S_BAD_REQUEST] = "Bad Request", + [S_FORBIDDEN] = "Forbidden", + [S_NOT_FOUND] = "Not Found", + [S_METHOD_NOT_ALLOWED] = "Method Not Allowed", + [S_REQUEST_TIMEOUT] = "Request Time-out", + [S_RANGE_NOT_SATISFIABLE] = "Range Not Satisfiable", + [S_REQUEST_TOO_LARGE] = "Request Header Fields Too Large", + [S_INTERNAL_SERVER_ERROR] = "Internal Server Error", + [S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported", +}; + +enum status +http_send_status(int fd, enum status s) +{ + static char t[TIMESTAMP_LEN]; + + if (dprintf(fd, + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Connection: close\r\n" + "%s" + "Content-Type: text/html\r\n" + "\r\n" + "<!DOCTYPE html>\n<html>\n\t<head>\n" + "\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n" + "\t\t<h1>%d %s</h1>\n\t</body>\n</html>\n", + s, status_str[s], timestamp(time(NULL), t), + (s == S_METHOD_NOT_ALLOWED) ? "Allow: HEAD, GET\r\n" : "", + s, status_str[s], s, status_str[s]) < 0) { + return S_REQUEST_TIMEOUT; + } + + return s; +} + +static void +decode(char src[PATH_MAX], char dest[PATH_MAX]) +{ + size_t i; + uint8_t n; + char *s; + + for (s = src, i = 0; *s; s++, i++) { + if (*s == '+') { + dest[i] = ' '; + } else if (*s == '%' && (sscanf(s + 1, "%2hhx", &n) == 1)) { + dest[i] = n; + s += 2; + } else { + dest[i] = *s; + } + } + dest[i] = '\0'; +} + +int +http_get_request(int fd, struct request *r) +{ + size_t hlen, i, mlen; + ssize_t off; + char h[HEADER_MAX], *p, *q; + + /* empty all fields */ + memset(r, 0, sizeof(*r)); + + /* + * receive header + */ + for (hlen = 0; ;) { + if ((off = read(fd, h + hlen, sizeof(h) - hlen)) < 0) { + return http_send_status(fd, S_REQUEST_TIMEOUT); + } else if (off == 0) { + break; + } + hlen += off; + if (hlen >= 4 && !memcmp(h + hlen - 4, "\r\n\r\n", 4)) { + break; + } + if (hlen == sizeof(h)) { + return http_send_status(fd, S_REQUEST_TOO_LARGE); + } + } + + /* remove terminating empty line */ + if (hlen < 2) { + return http_send_status(fd, S_BAD_REQUEST); + } + hlen -= 2; + + /* null-terminate the header */ + h[hlen] = '\0'; + + /* + * parse request line + */ + + /* METHOD */ + for (i = 0; i < NUM_REQ_METHODS; i++) { + mlen = strlen(req_method_str[i]); + if (!strncmp(req_method_str[i], h, mlen)) { + r->method = i; + break; + } + } + if (i == NUM_REQ_METHODS) { + return http_send_status(fd, S_METHOD_NOT_ALLOWED); + } + + /* a single space must follow the method */ + if (h[mlen] != ' ') { + return http_send_status(fd, S_BAD_REQUEST); + } + + /* basis for next step */ + p = h + mlen + 1; + + /* TARGET */ + if (!(q = strchr(p, ' '))) { + return http_send_status(fd, S_BAD_REQUEST); + } + *q = '\0'; + if (q - p + 1 > PATH_MAX) { + return http_send_status(fd, S_REQUEST_TOO_LARGE); + } + memcpy(r->target, p, q - p + 1); + decode(r->target, r->target); + + /* basis for next step */ + p = q + 1; + + /* HTTP-VERSION */ + if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { + return http_send_status(fd, S_BAD_REQUEST); + } + p += sizeof("HTTP/") - 1; + if (strncmp(p, "1.0", sizeof("1.0") - 1) && + strncmp(p, "1.1", sizeof("1.1") - 1)) { + return http_send_status(fd, S_VERSION_NOT_SUPPORTED); + } + p += sizeof("1.*") - 1; + + /* check terminator */ + if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { + return http_send_status(fd, S_BAD_REQUEST); + } + + /* basis for next step */ + p += sizeof("\r\n") - 1; + + /* + * parse request-fields + */ + + /* match field type */ + for (; *p != '\0';) { + for (i = 0; i < NUM_REQ_FIELDS; i++) { + if (!strncasecmp(p, req_field_str[i], + strlen(req_field_str[i]))) { + break; + } + } + if (i == NUM_REQ_FIELDS) { + /* unmatched field, skip this line */ + if (!(q = strstr(p, "\r\n"))) { + return http_send_status(fd, S_BAD_REQUEST); + } + p = q + (sizeof("\r\n") - 1); + continue; + } + + p += strlen(req_field_str[i]); + + /* a single colon must follow the field name */ + if (*p != ':') { + return http_send_status(fd, S_BAD_REQUEST); + } + + /* skip whitespace */ + for (++p; *p == ' ' || *p == '\t'; p++) + ; + + /* extract field content */ + if (!(q = strstr(p, "\r\n"))) { + return http_send_status(fd, S_BAD_REQUEST); + } + *q = '\0'; + if (q - p + 1 > FIELD_MAX) { + return http_send_status(fd, S_REQUEST_TOO_LARGE); + } + memcpy(r->field[i], p, q - p + 1); + + /* go to next line */ + p = q + (sizeof("\r\n") - 1); + } + + return 0; +} + +static void +encode(char src[PATH_MAX], char dest[PATH_MAX]) +{ + size_t i; + char *s; + + for (s = src, i = 0; *s && i < (PATH_MAX - 4); s++) { + if (iscntrl(*s) || (unsigned char)*s > 127) { + i += snprintf(dest + i, PATH_MAX - i, "%%%02X", + (unsigned char)*s); + } else { + dest[i] = *s; + i++; + } + } + dest[i] = '\0'; +} + +static int +normabspath(char *path) +{ + size_t len; + int last = 0; + char *p, *q; + + /* require and skip first slash */ + if (path[0] != '/') { + return 1; + } + p = path + 1; + + /* get length of path */ + len = strlen(p); + + for (; !last; ) { + /* bound path component within (p,q) */ + if (!(q = strchr(p, '/'))) { + q = strchr(p, '\0'); + last = 1; + } + + if (p == q || (q - p == 1 && p[0] == '.')) { + /* "/" or "./" */ + goto squash; + } else if (q - p == 2 && p[0] == '.' && p[1] == '.') { + /* "../" */ + if (p != path + 1) { + /* place p right after the previous / */ + for (p -= 2; p > path && *p != '/'; p--); + p++; + } + goto squash; + } else { + /* move on */ + p = q + 1; + continue; + } +squash: + /* squash (p,q) into void */ + if (last) { + *p = '\0'; + len = p - path; + } else { + memmove(p, q + 1, len - ((q + 1) - path) + 2); + len -= (q + 1) - p; + } + } + + return 0; +} + +#undef RELPATH +#define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1)) + +enum status +http_send_response(int fd, struct request *r) +{ + struct in6_addr res; + struct stat st; + struct tm tm; + size_t len, i; + off_t lower, upper; + int hasport, ipv6host; + static char realtarget[PATH_MAX], tmptarget[PATH_MAX], t[TIMESTAMP_LEN]; + char *p, *q, *mime; + const char *vhostmatch, *err; + + /* match vhost */ + vhostmatch = NULL; + if (vhosts) { + for (i = 0; i < LEN(vhost); i++) { + /* switch to vhost directory if there is a match */ + if (!regexec(&vhost[i].re, r->field[REQ_HOST], 0, + NULL, 0)) { + if (chdir(vhost[i].dir) < 0) { + return http_send_status(fd, (errno == EACCES) ? + S_FORBIDDEN : S_NOT_FOUND); + } + vhostmatch = vhost[i].name; + break; + } + } + if (i == LEN(vhost)) { + return http_send_status(fd, S_NOT_FOUND); + } + } + + /* normalize target */ + memcpy(realtarget, r->target, sizeof(realtarget)); + if (normabspath(realtarget)) { + return http_send_status(fd, S_BAD_REQUEST); + } + + /* reject hidden target */ + if (realtarget[0] == '.' || strstr(realtarget, "/.")) { + return http_send_status(fd, S_FORBIDDEN); + } + + /* stat the target */ + if (stat(RELPATH(realtarget), &st) < 0) { + return http_send_status(fd, (errno == EACCES) ? + S_FORBIDDEN : S_NOT_FOUND); + } + + if (S_ISDIR(st.st_mode)) { + /* add / to target if not present */ + len = strlen(realtarget); + if (len == PATH_MAX - 2) { + return http_send_status(fd, S_REQUEST_TOO_LARGE); + } + if (len && realtarget[len - 1] != '/') { + realtarget[len] = '/'; + realtarget[len + 1] = '\0'; + } + } + + /* redirect if targets differ or host is non-canonical */ + if (strcmp(r->target, realtarget) || (vhosts && vhostmatch && + strcmp(r->field[REQ_HOST], vhostmatch))) { + /* do we need to add a port to the Location? */ + hasport = strcmp(port, "80"); + + /* RFC 2732 specifies to use brackets for IPv6-addresses in + * URLs, so we need to check if our host is one and honor that + * later when we fill the "Location"-field */ + if ((ipv6host = inet_pton(AF_INET6, r->field[REQ_HOST][0] ? + r->field[REQ_HOST] : host, &res)) < 0) { + return http_send_status(fd, S_INTERNAL_SERVER_ERROR); + } + + /* encode realtarget */ + encode(realtarget, tmptarget); + + /* send redirection header */ + if (dprintf(fd, + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Connection: close\r\n" + "Location: http://%s%s%s%s%s%s\r\n" + "\r\n", + S_MOVED_PERMANENTLY, + status_str[S_MOVED_PERMANENTLY], + timestamp(time(NULL), t), ipv6host ? "[" : "", + r->field[REQ_HOST][0] ? (vhosts && vhostmatch) ? + vhostmatch : r->field[REQ_HOST] : host, + ipv6host ? "]" : "", hasport ? ":" : "", + hasport ? port : "", tmptarget) < 0) { + return S_REQUEST_TIMEOUT; + } + + return S_MOVED_PERMANENTLY; + } + + if (S_ISDIR(st.st_mode)) { + /* append docindex to target */ + if (snprintf(realtarget, sizeof(realtarget), "%s%s", + r->target, docindex) >= sizeof(realtarget)) { + return http_send_status(fd, S_REQUEST_TOO_LARGE); + } + + /* stat the docindex, which must be a regular file */ + if (stat(RELPATH(realtarget), &st) < 0 || !S_ISREG(st.st_mode)) { + if (listdirs) { + /* remove index suffix and serve dir */ + realtarget[strlen(realtarget) - + strlen(docindex)] = '\0'; + return resp_dir(fd, RELPATH(realtarget), r); + } else { + /* reject */ + if (!S_ISREG(st.st_mode) || errno == EACCES) { + return http_send_status(fd, S_FORBIDDEN); + } else { + return http_send_status(fd, S_NOT_FOUND); + } + } + } + } + + /* modified since */ + if (r->field[REQ_MOD][0]) { + /* parse field */ + if (!strptime(r->field[REQ_MOD], "%a, %d %b %Y %T GMT", &tm)) { + return http_send_status(fd, S_BAD_REQUEST); + } + + /* compare with last modification date of the file */ + if (difftime(st.st_mtim.tv_sec, mktime(&tm)) <= 0) { + if (dprintf(fd, + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Connection: close\r\n" + "\r\n", + S_NOT_MODIFIED, status_str[S_NOT_MODIFIED], + timestamp(time(NULL), t)) < 0) { + return S_REQUEST_TIMEOUT; + } + } + } + + /* range */ + lower = 0; + upper = st.st_size - 1; + if (r->field[REQ_RANGE][0]) { + /* parse field */ + p = r->field[REQ_RANGE]; + err = NULL; + + if (strncmp(p, "bytes=", sizeof("bytes=") - 1)) { + return http_send_status(fd, S_BAD_REQUEST); + } + p += sizeof("bytes=") - 1; + + if (!(q = strchr(p, '-'))) { + return http_send_status(fd, S_BAD_REQUEST); + } + *(q++) = '\0'; + if (p[0]) { + lower = strtonum(p, 0, LLONG_MAX, &err); + } + if (!err && q[0]) { + upper = strtonum(q, 0, LLONG_MAX, &err); + } + if (err) { + return http_send_status(fd, S_BAD_REQUEST); + } + + /* check range */ + if (lower < 0 || upper < 0 || lower > upper) { + if (dprintf(fd, + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Content-Range: bytes */%zu\r\n" + "Connection: close\r\n" + "\r\n", + S_RANGE_NOT_SATISFIABLE, + status_str[S_RANGE_NOT_SATISFIABLE], + timestamp(time(NULL), t), + st.st_size) < 0) { + return S_REQUEST_TIMEOUT; + } + return S_RANGE_NOT_SATISFIABLE; + } + + /* adjust upper limit */ + if (upper >= st.st_size) + upper = st.st_size-1; + } + + /* mime */ + mime = "application/octet-stream"; + if ((p = strrchr(realtarget, '.'))) { + for (i = 0; i < sizeof(mimes) / sizeof(*mimes); i++) { + if (!strcmp(mimes[i].ext, p + 1)) { + mime = mimes[i].type; + break; + } + } + } + + return resp_file(fd, RELPATH(realtarget), r, &st, mime, lower, upper); +} diff --git a/http.h b/http.h @@ -0,0 +1,55 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef HTTP_H +#define HTTP_H + +#include <limits.h> + +#define HEADER_MAX 4096 +#define FIELD_MAX 200 + +enum req_field { + REQ_HOST, + REQ_RANGE, + REQ_MOD, + NUM_REQ_FIELDS, +}; + +extern const char *req_field_str[]; + +enum req_method { + M_GET, + M_HEAD, + NUM_REQ_METHODS, +}; + +extern const char *req_method_str[]; + +struct request { + enum req_method method; + char target[PATH_MAX]; + char field[NUM_REQ_FIELDS][FIELD_MAX]; +}; + +enum status { + S_OK = 200, + S_PARTIAL_CONTENT = 206, + S_MOVED_PERMANENTLY = 301, + S_NOT_MODIFIED = 304, + S_BAD_REQUEST = 400, + S_FORBIDDEN = 403, + S_NOT_FOUND = 404, + S_METHOD_NOT_ALLOWED = 405, + S_REQUEST_TIMEOUT = 408, + S_RANGE_NOT_SATISFIABLE = 416, + S_REQUEST_TOO_LARGE = 431, + S_INTERNAL_SERVER_ERROR = 500, + S_VERSION_NOT_SUPPORTED = 505, +}; + +extern const char *status_str[]; + +enum status http_send_status(int, enum status); +int http_get_request(int, struct request *); +enum status http_send_response(int, struct request *); + +#endif /* HTTP_H */ diff --git a/main.c b/main.c @@ -0,0 +1,261 @@ +/* See LICENSE file for copyright and license details. */ +#include <errno.h> +#include <grp.h> +#include <netinet/in.h> +#include <pwd.h> +#include <regex.h> +#include <signal.h> +#include <sys/resource.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "http.h" +#include "sock.h" +#include "util.h" + +#include "config.h" + +static char *udsname; + +static void +serve(int insock) +{ + struct request r; + struct sockaddr_storage in_sa; + pid_t p; + socklen_t in_sa_len; + time_t t; + enum status status; + int infd; + char inaddr[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */]; + char tstmp[25]; + + while (1) { + /* accept incoming connections */ + in_sa_len = sizeof(in_sa); + if ((infd = accept(insock, (struct sockaddr *)&in_sa, + &in_sa_len)) < 0) { + warn("accept:"); + continue; + } + + /* fork and handle */ + switch ((p = fork())) { + case -1: + warn("fork:"); + break; + case 0: + close(insock); + + /* set connection timeout */ + if (sock_set_timeout(infd, 30)) { + goto cleanup; + } + + /* handle request */ + if (!(status = http_get_request(infd, &r))) { + status = http_send_response(infd, &r); + } + + /* write output to log */ + t = time(NULL); + if (!strftime(tstmp, sizeof(tstmp), "%Y-%m-%dT%H:%M:%S", + gmtime(&t))) { + warn("strftime: Exceeded buffer capacity"); + goto cleanup; + } + if (sock_get_inaddr_str(&in_sa, inaddr, LEN(inaddr))) { + goto cleanup; + } + printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inaddr, status, + r.field[REQ_HOST], r.target); +cleanup: + /* clean up and finish */ + shutdown(infd, SHUT_RD); + shutdown(infd, SHUT_WR); + close(infd); + exit(0); + default: + /* close the connection in the parent */ + close(infd); + } + } +} + +static void +cleanup(void) +{ + if (udsname) + sock_rem_uds(udsname); +} + +static void +sigcleanup(int sig) +{ + cleanup(); + kill(0, sig); + _exit(1); +} + +static void +handlesignals(void(*hdl)(int)) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_handler = hdl; + + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGHUP, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + sigaction(SIGQUIT, &sa, NULL); +} + +static void +usage(void) +{ + die("usage: %s [-l | -L] [-v | -V] [[[-h host] [-p port]] | [-U sockfile]] " + "[-d dir] [-u user] [-g group]", argv0); +} + +int +main(int argc, char *argv[]) +{ + struct passwd *pwd = NULL; + struct group *grp = NULL; + struct rlimit rlim; + pid_t cpid, wpid; + int i, insock, status = 0; + + ARGBEGIN { + case 'd': + servedir = EARGF(usage()); + break; + case 'g': + group = EARGF(usage()); + break; + case 'h': + host = EARGF(usage()); + break; + case 'l': + listdirs = 0; + break; + case 'L': + listdirs = 1; + break; + case 'p': + port = EARGF(usage()); + break; + case 'u': + user = EARGF(usage()); + break; + case 'U': + udsname = EARGF(usage()); + break; + case 'v': + vhosts = 0; + break; + case 'V': + vhosts = 1; + break; + default: + usage(); + } ARGEND + + if (argc) { + usage(); + } + + if (udsname && (!access(udsname, F_OK) || errno != ENOENT)) { + die("UNIX-domain socket: %s", errno ? + strerror(errno) : "file exists"); + } + + /* compile and check the supplied vhost regexes */ + if (vhosts) { + for (i = 0; i < LEN(vhost); i++) { + if (regcomp(&vhost[i].re, vhost[i].regex, + REG_EXTENDED | REG_ICASE | REG_NOSUB)) { + die("regcomp '%s': invalid regex", + vhost[i].regex); + } + } + } + + /* raise the process limit */ + rlim.rlim_cur = rlim.rlim_max = maxnprocs; + if (setrlimit(RLIMIT_NPROC, &rlim) < 0) { + die("setrlimit RLIMIT_NPROC:"); + } + + /* validate user and group */ + errno = 0; + if (user && !(pwd = getpwnam(user))) { + die("getpwnam '%s':", user); + } + errno = 0; + if (group && !(grp = getgrnam(group))) { + die("getgrnam '%s':", group); + } + + handlesignals(sigcleanup); + + /* bind socket */ + insock = udsname ? sock_get_uds(udsname, pwd->pw_uid, grp->gr_gid) : + sock_get_ips(host, port); + + switch (cpid = fork()) { + case -1: + warn("fork:"); + break; + case 0: + /* restore default handlers */ + handlesignals(SIG_DFL); + + /* reap children automatically */ + if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) { + die("signal: Failed to set SIG_IGN on SIGCHLD"); + } + + /* chroot */ + if (chdir(servedir) < 0) { + die("chdir '%s':", servedir); + } + if (chroot(".") < 0) { + die("chroot .:"); + } + + /* drop root */ + if (grp && setgroups(1, &(grp->gr_gid)) < 0) { + die("setgroups:"); + } + if (grp && setgid(grp->gr_gid) < 0) { + die("setgid:"); + } + if (pwd && setuid(pwd->pw_uid) < 0) { + die("setuid:"); + } + if (getuid() == 0) { + die("won't run as root user", argv0); + } + if (getgid() == 0) { + die("won't run as root group", argv0); + } + + serve(insock); + exit(0); + default: + while ((wpid = wait(&status)) > 0) + ; + } + + cleanup(); + return status; +} diff --git a/quark.c b/quark.c @@ -1,1166 +0,0 @@ -/* See LICENSE file for copyright and license details. */ -#include <netinet/in.h> -#include <sys/resource.h> -#include <sys/socket.h> -#include <sys/stat.h> -#include <sys/time.h> -#include <sys/types.h> -#include <sys/un.h> -#include <sys/wait.h> - -#include <arpa/inet.h> -#include <ctype.h> -#include <dirent.h> -#include <errno.h> -#include <fcntl.h> -#include <grp.h> -#include <inttypes.h> -#include <limits.h> -#include <netdb.h> -#include <pwd.h> -#include <regex.h> -#include <signal.h> -#include <stdarg.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <time.h> -#include <unistd.h> - -#include "arg.h" - -char *argv0; -static char *udsname; - -#include "config.h" - -#undef MIN -#define MIN(x,y) ((x) < (y) ? (x) : (y)) -#undef LEN -#define LEN(x) (sizeof (x) / sizeof *(x)) -#undef RELPATH -#define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1)) - -#define TIMESTAMP_LEN 30 - -enum req_field { - REQ_HOST, - REQ_RANGE, - REQ_MOD, - NUM_REQ_FIELDS, -}; - -static char *req_field_str[] = { - [REQ_HOST] = "Host", - [REQ_RANGE] = "Range", - [REQ_MOD] = "If-Modified-Since", -}; - -enum req_method { - M_GET, - M_HEAD, - NUM_REQ_METHODS, -}; - -static char *req_method_str[] = { - [M_GET] = "GET", - [M_HEAD] = "HEAD", -}; - -struct request { - enum req_method method; - char target[PATH_MAX]; - char field[NUM_REQ_FIELDS][FIELD_MAX]; -}; - -enum status { - S_OK = 200, - S_PARTIAL_CONTENT = 206, - S_MOVED_PERMANENTLY = 301, - S_NOT_MODIFIED = 304, - S_BAD_REQUEST = 400, - S_FORBIDDEN = 403, - S_NOT_FOUND = 404, - S_METHOD_NOT_ALLOWED = 405, - S_REQUEST_TIMEOUT = 408, - S_RANGE_NOT_SATISFIABLE = 416, - S_REQUEST_TOO_LARGE = 431, - S_INTERNAL_SERVER_ERROR = 500, - S_VERSION_NOT_SUPPORTED = 505, -}; - -static char *status_str[] = { - [S_OK] = "OK", - [S_PARTIAL_CONTENT] = "Partial Content", - [S_MOVED_PERMANENTLY] = "Moved Permanently", - [S_NOT_MODIFIED] = "Not Modified", - [S_BAD_REQUEST] = "Bad Request", - [S_FORBIDDEN] = "Forbidden", - [S_NOT_FOUND] = "Not Found", - [S_METHOD_NOT_ALLOWED] = "Method Not Allowed", - [S_REQUEST_TIMEOUT] = "Request Time-out", - [S_RANGE_NOT_SATISFIABLE] = "Range Not Satisfiable", - [S_REQUEST_TOO_LARGE] = "Request Header Fields Too Large", - [S_INTERNAL_SERVER_ERROR] = "Internal Server Error", - [S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported", -}; - -long long strtonum(const char *, long long, long long, const char **); - -static char * -timestamp(time_t t, char buf[TIMESTAMP_LEN]) -{ - strftime(buf, TIMESTAMP_LEN, "%a, %d %b %Y %T GMT", gmtime(&t)); - - return buf; -} - -static void -decode(char src[PATH_MAX], char dest[PATH_MAX]) -{ - size_t i; - uint8_t n; - char *s; - - for (s = src, i = 0; *s; s++, i++) { - if (*s == '+') { - dest[i] = ' '; - } else if (*s == '%' && (sscanf(s + 1, "%2hhx", &n) == 1)) { - dest[i] = n; - s += 2; - } else { - dest[i] = *s; - } - } - dest[i] = '\0'; -} - -static void -encode(char src[PATH_MAX], char dest[PATH_MAX]) -{ - size_t i; - char *s; - - for (s = src, i = 0; *s && i < (PATH_MAX - 4); s++) { - if (iscntrl(*s) || (unsigned char)*s > 127) { - i += snprintf(dest + i, PATH_MAX - i, "%%%02X", - (unsigned char)*s); - } else { - dest[i] = *s; - i++; - } - } - dest[i] = '\0'; -} - -static enum status -sendstatus(int fd, enum status s) -{ - static char t[TIMESTAMP_LEN]; - - if (dprintf(fd, - "HTTP/1.1 %d %s\r\n" - "Date: %s\r\n" - "Connection: close\r\n" - "%s" - "Content-Type: text/html\r\n" - "\r\n" - "<!DOCTYPE html>\n<html>\n\t<head>\n" - "\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n" - "\t\t<h1>%d %s</h1>\n\t</body>\n</html>\n", - s, status_str[s], timestamp(time(NULL), t), - (s == S_METHOD_NOT_ALLOWED) ? "Allow: HEAD, GET\r\n" : "", - s, status_str[s], s, status_str[s]) < 0) { - return S_REQUEST_TIMEOUT; - } - - return s; -} - -static int -getrequest(int fd, struct request *r) -{ - size_t hlen, i, mlen; - ssize_t off; - char h[HEADER_MAX], *p, *q; - - /* empty all fields */ - memset(r, 0, sizeof(*r)); - - /* - * receive header - */ - for (hlen = 0; ;) { - if ((off = read(fd, h + hlen, sizeof(h) - hlen)) < 0) { - return sendstatus(fd, S_REQUEST_TIMEOUT); - } else if (off == 0) { - break; - } - hlen += off; - if (hlen >= 4 && !memcmp(h + hlen - 4, "\r\n\r\n", 4)) { - break; - } - if (hlen == sizeof(h)) { - return sendstatus(fd, S_REQUEST_TOO_LARGE); - } - } - - /* remove terminating empty line */ - if (hlen < 2) { - return sendstatus(fd, S_BAD_REQUEST); - } - hlen -= 2; - - /* null-terminate the header */ - h[hlen] = '\0'; - - /* - * parse request line - */ - - /* METHOD */ - for (i = 0; i < NUM_REQ_METHODS; i++) { - mlen = strlen(req_method_str[i]); - if (!strncmp(req_method_str[i], h, mlen)) { - r->method = i; - break; - } - } - if (i == NUM_REQ_METHODS) { - return sendstatus(fd, S_METHOD_NOT_ALLOWED); - } - - /* a single space must follow the method */ - if (h[mlen] != ' ') { - return sendstatus(fd, S_BAD_REQUEST); - } - - /* basis for next step */ - p = h + mlen + 1; - - /* TARGET */ - if (!(q = strchr(p, ' '))) { - return sendstatus(fd, S_BAD_REQUEST); - } - *q = '\0'; - if (q - p + 1 > PATH_MAX) { - return sendstatus(fd, S_REQUEST_TOO_LARGE); - } - memcpy(r->target, p, q - p + 1); - decode(r->target, r->target); - - /* basis for next step */ - p = q + 1; - - /* HTTP-VERSION */ - if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { - return sendstatus(fd, S_BAD_REQUEST); - } - p += sizeof("HTTP/") - 1; - if (strncmp(p, "1.0", sizeof("1.0") - 1) && - strncmp(p, "1.1", sizeof("1.1") - 1)) { - return sendstatus(fd, S_VERSION_NOT_SUPPORTED); - } - p += sizeof("1.*") - 1; - - /* check terminator */ - if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { - return sendstatus(fd, S_BAD_REQUEST); - } - - /* basis for next step */ - p += sizeof("\r\n") - 1; - - /* - * parse request-fields - */ - - /* match field type */ - for (; *p != '\0';) { - for (i = 0; i < NUM_REQ_FIELDS; i++) { - if (!strncasecmp(p, req_field_str[i], - strlen(req_field_str[i]))) { - break; - } - } - if (i == NUM_REQ_FIELDS) { - /* unmatched field, skip this line */ - if (!(q = strstr(p, "\r\n"))) { - return sendstatus(fd, S_BAD_REQUEST); - } - p = q + (sizeof("\r\n") - 1); - continue; - } - - p += strlen(req_field_str[i]); - - /* a single colon must follow the field name */ - if (*p != ':') { - return sendstatus(fd, S_BAD_REQUEST); - } - - /* skip whitespace */ - for (++p; *p == ' ' || *p == '\t'; p++) - ; - - /* extract field content */ - if (!(q = strstr(p, "\r\n"))) { - return sendstatus(fd, S_BAD_REQUEST); - } - *q = '\0'; - if (q - p + 1 > FIELD_MAX) { - return sendstatus(fd, S_REQUEST_TOO_LARGE); - } - memcpy(r->field[i], p, q - p + 1); - - /* go to next line */ - p = q + (sizeof("\r\n") - 1); - } - - return 0; -} - -static int -compareent(const struct dirent **d1, const struct dirent **d2) -{ - int v; - - v = ((*d2)->d_type == DT_DIR ? 1 : -1) - - ((*d1)->d_type == DT_DIR ? 1 : -1); - if (v) { - return v; - } - - return strcmp((*d1)->d_name, (*d2)->d_name); -} - -static char * -suffix(int t) -{ - switch (t) { - case DT_FIFO: return "|"; - case DT_DIR: return "/"; - case DT_LNK: return "@"; - case DT_SOCK: return "="; - } - - return ""; -} - -static enum status -senddir(int fd, char *name, struct request *r) -{ - struct dirent **e; - size_t i; - int dirlen, s; - static char t[TIMESTAMP_LEN]; - - /* read directory */ - if ((dirlen = scandir(name, &e, NULL, compareent)) < 0) { - return sendstatus(fd, S_FORBIDDEN); - } - - /* send header as late as possible */ - if (dprintf(fd, - "HTTP/1.1 %d %s\r\n" - "Date: %s\r\n" - "Connection: close\r\n" - "Content-Type: text/html\r\n" - "\r\n", - S_OK, status_str[S_OK], timestamp(time(NULL), t)) < 0) { - s = S_REQUEST_TIMEOUT; - goto cleanup; - } - - if (r->method == M_GET) { - /* listing header */ - if (dprintf(fd, - "<!DOCTYPE html>\n<html>\n\t<head>" - "<title>Index of %s</title></head>\n" - "\t<body>\n\t\t<a href=\"..\">..</a>", - name) < 0) { - s = S_REQUEST_TIMEOUT; - goto cleanup; - } - - /* listing */ - for (i = 0; i < dirlen; i++) { - /* skip hidden files, "." and ".." */ - if (e[i]->d_name[0] == '.') { - continue; - } - - /* entry line */ - if (dprintf(fd, "<br />\n\t\t<a href=\"%s%s\">%s%s</a>", - e[i]->d_name, - (e[i]->d_type == DT_DIR) ? "/" : "", - e[i]->d_name, - suffix(e[i]->d_type)) < 0) { - s = S_REQUEST_TIMEOUT; - goto cleanup; - } - } - - /* listing footer */ - if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) { - s = S_REQUEST_TIMEOUT; - goto cleanup; - } - } - s = S_OK; - -cleanup: - while (dirlen--) { - free(e[dirlen]); - } - free(e); - - return s; -} - -static enum status -responsefile(int fd, char *name, struct request *r, struct stat *st, char *mime, - off_t lower, off_t upper) -{ - FILE *fp; - enum status s; - ssize_t bread, bwritten; - off_t remaining; - int range; - static char buf[BUFSIZ], *p, t1[TIMESTAMP_LEN], t2[TIMESTAMP_LEN]; - - /* open file */ - if (!(fp = fopen(name, "r"))) { - s = sendstatus(fd, S_FORBIDDEN); - goto cleanup; - } - - /* seek to lower bound */ - if (fseek(fp, lower, SEEK_SET)) { - s = sendstatus(fd, S_INTERNAL_SERVER_ERROR); - goto cleanup; - } - - /* send header as late as possible */ - range = r->field[REQ_RANGE][0]; - s = range ? S_PARTIAL_CONTENT : S_OK; - - if (dprintf(fd, - "HTTP/1.1 %d %s\r\n" - "Date: %s\r\n" - "Connection: close\r\n" - "Last-Modified: %s\r\n" - "Content-Type: %s\r\n" - "Content-Length: %zu\r\n", - s, status_str[s], timestamp(time(NULL), t1), - timestamp(st->st_mtim.tv_sec, t2), mime, - upper - lower + 1) < 0) { - s = S_REQUEST_TIMEOUT; - goto cleanup; - } - if (range) { - if (dprintf(fd, "Content-Range: bytes %zd-%zd/%zu\r\n", - lower, upper + (upper < 0), st->st_size) < 0) { - s = S_REQUEST_TIMEOUT; - goto cleanup; - } - } - if (dprintf(fd, "\r\n") < 0) { - s = S_REQUEST_TIMEOUT; - goto cleanup; - } - - if (r->method == M_GET) { - /* write data until upper bound is hit */ - remaining = upper - lower + 1; - - while ((bread = fread(buf, 1, MIN(sizeof(buf), remaining), fp))) { - if (bread < 0) { - return S_INTERNAL_SERVER_ERROR; - } - remaining -= bread; - p = buf; - while (bread > 0) { - bwritten = write(fd, p, bread); - if (bwritten <= 0) { - return S_REQUEST_TIMEOUT; - } - bread -= bwritten; - p += bwritten; - } - } - } -cleanup: - if (fp) { - fclose(fp); - } - - return s; -} - -static int -normabspath(char *path) -{ - size_t len; - int last = 0; - char *p, *q; - - /* require and skip first slash */ - if (path[0] != '/') { - return 1; - } - p = path + 1; - - /* get length of path */ - len = strlen(p); - - for (; !last; ) { - /* bound path component within (p,q) */ - if (!(q = strchr(p, '/'))) { - q = strchr(p, '\0'); - last = 1; - } - - if (p == q || (q - p == 1 && p[0] == '.')) { - /* "/" or "./" */ - goto squash; - } else if (q - p == 2 && p[0] == '.' && p[1] == '.') { - /* "../" */ - if (p != path + 1) { - /* place p right after the previous / */ - for (p -= 2; p > path && *p != '/'; p--); - p++; - } - goto squash; - } else { - /* move on */ - p = q + 1; - continue; - } -squash: - /* squash (p,q) into void */ - if (last) { - *p = '\0'; - len = p - path; - } else { - memmove(p, q + 1, len - ((q + 1) - path) + 2); - len -= (q + 1) - p; - } - } - - return 0; -} - -static enum status -sendresponse(int fd, struct request *r) -{ - struct in6_addr res; - struct stat st; - struct tm tm; - size_t len, i; - off_t lower, upper; - int hasport, ipv6host; - static char realtarget[PATH_MAX], tmptarget[PATH_MAX], t[TIMESTAMP_LEN]; - char *p, *q, *mime; - const char *vhostmatch, *err; - - /* match vhost */ - vhostmatch = NULL; - if (vhosts) { - for (i = 0; i < LEN(vhost); i++) { - /* switch to vhost directory if there is a match */ - if (!regexec(&vhost[i].re, r->field[REQ_HOST], 0, - NULL, 0)) { - if (chdir(vhost[i].dir) < 0) { - return sendstatus(fd, (errno == EACCES) ? - S_FORBIDDEN : S_NOT_FOUND); - } - vhostmatch = vhost[i].name; - break; - } - } - if (i == LEN(vhost)) { - return sendstatus(fd, S_NOT_FOUND); - } - } - - /* normalize target */ - memcpy(realtarget, r->target, sizeof(realtarget)); - if (normabspath(realtarget)) { - return sendstatus(fd, S_BAD_REQUEST); - } - - /* reject hidden target */ - if (realtarget[0] == '.' || strstr(realtarget, "/.")) { - return sendstatus(fd, S_FORBIDDEN); - } - - /* stat the target */ - if (stat(RELPATH(realtarget), &st) < 0) { - return sendstatus(fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND); - } - - if (S_ISDIR(st.st_mode)) { - /* add / to target if not present */ - len = strlen(realtarget); - if (len == PATH_MAX - 2) { - return sendstatus(fd, S_REQUEST_TOO_LARGE); - } - if (len && realtarget[len - 1] != '/') { - realtarget[len] = '/'; - realtarget[len + 1] = '\0'; - } - } - - /* redirect if targets differ or host is non-canonical */ - if (strcmp(r->target, realtarget) || (vhosts && vhostmatch && - strcmp(r->field[REQ_HOST], vhostmatch))) { - /* do we need to add a port to the Location? */ - hasport = strcmp(port, "80"); - - /* RFC 2732 specifies to use brackets for IPv6-addresses in - * URLs, so we need to check if our host is one and honor that - * later when we fill the "Location"-field */ - if ((ipv6host = inet_pton(AF_INET6, r->field[REQ_HOST][0] ? - r->field[REQ_HOST] : host, &res)) < 0) { - return sendstatus(fd, S_INTERNAL_SERVER_ERROR); - } - - /* encode realtarget */ - encode(realtarget, tmptarget); - - /* send redirection header */ - if (dprintf(fd, - "HTTP/1.1 %d %s\r\n" - "Date: %s\r\n" - "Connection: close\r\n" - "Location: http://%s%s%s%s%s%s\r\n" - "\r\n", - S_MOVED_PERMANENTLY, - status_str[S_MOVED_PERMANENTLY], - timestamp(time(NULL), t), ipv6host ? "[" : "", - r->field[REQ_HOST][0] ? (vhosts && vhostmatch) ? - vhostmatch : r->field[REQ_HOST] : host, - ipv6host ? "]" : "", hasport ? ":" : "", - hasport ? port : "", tmptarget) < 0) { - return S_REQUEST_TIMEOUT; - } - - return S_MOVED_PERMANENTLY; - } - - if (S_ISDIR(st.st_mode)) { - /* append docindex to target */ - if (snprintf(realtarget, sizeof(realtarget), "%s%s", - r->target, docindex) >= sizeof(realtarget)) { - return sendstatus(fd, S_REQUEST_TOO_LARGE); - } - - /* stat the docindex, which must be a regular file */ - if (stat(RELPATH(realtarget), &st) < 0 || !S_ISREG(st.st_mode)) { - if (listdirs) { - /* remove index suffix and serve dir */ - realtarget[strlen(realtarget) - - strlen(docindex)] = '\0'; - return senddir(fd, RELPATH(realtarget), r); - } else { - /* reject */ - if (!S_ISREG(st.st_mode) || errno == EACCES) { - return sendstatus(fd, S_FORBIDDEN); - } else { - return sendstatus(fd, S_NOT_FOUND); - } - } - } - } - - /* modified since */ - if (r->field[REQ_MOD][0]) { - /* parse field */ - if (!strptime(r->field[REQ_MOD], "%a, %d %b %Y %T GMT", &tm)) { - return sendstatus(fd, S_BAD_REQUEST); - } - - /* compare with last modification date of the file */ - if (difftime(st.st_mtim.tv_sec, mktime(&tm)) <= 0) { - if (dprintf(fd, - "HTTP/1.1 %d %s\r\n" - "Date: %s\r\n" - "Connection: close\r\n" - "\r\n", - S_NOT_MODIFIED, status_str[S_NOT_MODIFIED], - timestamp(time(NULL), t)) < 0) { - return S_REQUEST_TIMEOUT; - } - } - } - - /* range */ - lower = 0; - upper = st.st_size - 1; - if (r->field[REQ_RANGE][0]) { - /* parse field */ - p = r->field[REQ_RANGE]; - err = NULL; - - if (strncmp(p, "bytes=", sizeof("bytes=") - 1)) { - return sendstatus(fd, S_BAD_REQUEST); - } - p += sizeof("bytes=") - 1; - - if (!(q = strchr(p, '-'))) { - return sendstatus(fd, S_BAD_REQUEST); - } - *(q++) = '\0'; - if (p[0]) { - lower = strtonum(p, 0, LLONG_MAX, &err); - } - if (!err && q[0]) { - upper = strtonum(q, 0, LLONG_MAX, &err); - } - if (err) { - return sendstatus(fd, S_BAD_REQUEST); - } - - /* check range */ - if (lower < 0 || upper < 0 || lower > upper) { - if (dprintf(fd, - "HTTP/1.1 %d %s\r\n" - "Date: %s\r\n" - "Content-Range: bytes */%zu\r\n" - "Connection: close\r\n" - "\r\n", - S_RANGE_NOT_SATISFIABLE, - status_str[S_RANGE_NOT_SATISFIABLE], - timestamp(time(NULL), t), - st.st_size) < 0) { - return S_REQUEST_TIMEOUT; - } - return S_RANGE_NOT_SATISFIABLE; - } - - /* adjust upper limit */ - if (upper >= st.st_size) - upper = st.st_size-1; - } - - /* mime */ - mime = "application/octet-stream"; - if ((p = strrchr(realtarget, '.'))) { - for (i = 0; i < sizeof(mimes) / sizeof(*mimes); i++) { - if (!strcmp(mimes[i].ext, p + 1)) { - mime = mimes[i].type; - break; - } - } - } - - return responsefile(fd, RELPATH(realtarget), r, &st, mime, lower, upper); -} - -static void -serve(int insock) -{ - struct request r; - struct sockaddr_storage in_sa; - struct timeval tv; - pid_t p; - socklen_t in_sa_len; - time_t t; - enum status status; - int infd; - char inip4[INET_ADDRSTRLEN], inip6[INET6_ADDRSTRLEN], tstmp[25]; - - while (1) { - /* accept incoming connections */ - in_sa_len = sizeof(in_sa); - if ((infd = accept(insock, (struct sockaddr *)&in_sa, - &in_sa_len)) < 0) { - fprintf(stderr, "%s: accept: %s\n", argv0, - strerror(errno)); - continue; - } - - /* fork and handle */ - switch ((p = fork())) { - case -1: - fprintf(stderr, "%s: fork: %s\n", argv0, - strerror(errno)); - break; - case 0: - close(insock); - - /* set connection timeout */ - tv.tv_sec = 30; - tv.tv_usec = 0; - if (setsockopt(infd, SOL_SOCKET, SO_RCVTIMEO, &tv, - sizeof(tv)) < 0 || - setsockopt(infd, SOL_SOCKET, SO_SNDTIMEO, &tv, - sizeof(tv)) < 0) { - fprintf(stderr, "%s: setsockopt: %s\n", - argv0, strerror(errno)); - return; - } - - /* handle request */ - if (!(status = getrequest(infd, &r))) { - status = sendresponse(infd, &r); - } - - /* write output to log */ - t = time(NULL); - strftime(tstmp, sizeof(tstmp), "%Y-%m-%dT%H:%M:%S", - gmtime(&t)); - - if (in_sa.ss_family == AF_INET) { - inet_ntop(AF_INET, - &(((struct sockaddr_in *)&in_sa)->sin_addr), - inip4, sizeof(inip4)); - printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inip4, - status, r.field[REQ_HOST], r.target); - } else { - inet_ntop(AF_INET6, - &(((struct sockaddr_in6*)&in_sa)->sin6_addr), - inip6, sizeof(inip6)); - printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inip6, - status, r.field[REQ_HOST], r.target); - } - - /* clean up and finish */ - shutdown(infd, SHUT_RD); - shutdown(infd, SHUT_WR); - close(infd); - exit(0); - default: - /* close the connection in the parent */ - close(infd); - } - } -} - -static void -die(const char *errstr, ...) -{ - va_list ap; - - va_start(ap, errstr); - vfprintf(stderr, errstr, ap); - va_end(ap); - - exit(1); -} - -static int -getipsock(void) -{ - struct addrinfo hints, *ai, *p; - int ret, insock = 0, yes; - - memset(&hints, 0, sizeof(hints)); - hints.ai_flags = AI_NUMERICSERV; - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - - if ((ret = getaddrinfo(host, port, &hints, &ai))) { - die("%s: getaddrinfo: %s\n", argv0, gai_strerror(ret)); - } - - for (yes = 1, p = ai; p; p = p->ai_next) { - if ((insock = socket(p->ai_family, p->ai_socktype, - p->ai_protocol)) < 0) { - continue; - } - if (setsockopt(insock, SOL_SOCKET, SO_REUSEADDR, &yes, - sizeof(int)) < 0) { - die("%s: setsockopt: %s\n", argv0, strerror(errno)); - } - if (bind(insock, p->ai_addr, p->ai_addrlen) < 0) { - close(insock); - continue; - } - break; - } - freeaddrinfo(ai); - if (!p) { - die("%s: failed to bind\n", argv0); - } - - if (listen(insock, SOMAXCONN) < 0) { - die("%s: listen: %s\n", argv0, strerror(errno)); - } - - return insock; -} - -static void -cleanup(void) -{ - if (udsname) - unlink(udsname); -} - -static void -sigcleanup(int sig) -{ - cleanup(); - kill(0, sig); - _exit(1); -} - -static void -handlesignals(void(*hdl)(int)) -{ - struct sigaction sa; - - memset(&sa, 0, sizeof(sa)); - sigemptyset(&sa.sa_mask); - sa.sa_handler = hdl; - - sigaction(SIGTERM, &sa, NULL); - sigaction(SIGHUP, &sa, NULL); - sigaction(SIGINT, &sa, NULL); - sigaction(SIGQUIT, &sa, NULL); -} - -static int -getusock(char *udsname, uid_t uid, gid_t gid) -{ - struct sockaddr_un addr; - size_t udsnamelen; - int insock, sockmode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH; - - if ((insock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { - die("%s: socket: %s\n", argv0, strerror(errno)); - } - - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - - if ((udsnamelen = strlen(udsname)) > sizeof(addr.sun_path) - 1) { - die("%s: UNIX-domain socket name truncated\n", argv0); - } - memcpy(addr.sun_path, udsname, udsnamelen + 1); - - if (bind(insock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { - die("%s: bind %s: %s\n", argv0, udsname, strerror(errno)); - } - - if (listen(insock, SOMAXCONN) < 0) { - cleanup(); - die("%s: listen: %s\n", argv0, strerror(errno)); - } - - if (chmod(udsname, sockmode) < 0) { - cleanup(); - die("%s: chmod: %s\n", argv0, strerror(errno)); - } - - if (chown(udsname, uid, gid) < 0) { - cleanup(); - die("%s: chown: %s\n", argv0, strerror(errno)); - } - - return insock; -} - -static void -usage(void) -{ - die("usage: %s [-l | -L] [-v | -V] [[[-h host] [-p port]] | [-U sockfile]] " - "[-d dir] [-u user] [-g group]\n", argv0); -} - -int -main(int argc, char *argv[]) -{ - struct passwd *pwd = NULL; - struct group *grp = NULL; - struct rlimit rlim; - pid_t cpid, wpid; - int i, insock, status = 0; - - ARGBEGIN { - case 'd': - servedir = EARGF(usage()); - break; - case 'g': - group = EARGF(usage()); - break; - case 'h': - host = EARGF(usage()); - break; - case 'l': - listdirs = 0; - break; - case 'L': - listdirs = 1; - break; - case 'p': - port = EARGF(usage()); - break; - case 'u': - user = EARGF(usage()); - break; - case 'U': - udsname = EARGF(usage()); - break; - case 'v': - vhosts = 0; - break; - case 'V': - vhosts = 1; - break; - default: - usage(); - } ARGEND - - if (argc) { - usage(); - } - - if (udsname && (!access(udsname, F_OK) || errno != ENOENT)) { - die("%s: socket file: %s\n", - argv0, errno ? strerror(errno) : "file exists"); - } - - /* compile and check the supplied vhost regexes */ - if (vhosts) { - for (i = 0; i < LEN(vhost); i++) { - if (regcomp(&vhost[i].re, vhost[i].regex, - REG_EXTENDED | REG_ICASE | REG_NOSUB)) { - die("%s: regcomp '%s': invalid regex\n", argv0, - vhost[i].regex); - } - } - } - - /* raise the process limit */ - rlim.rlim_cur = rlim.rlim_max = maxnprocs; - if (setrlimit(RLIMIT_NPROC, &rlim) < 0) { - die("%s: setrlimit RLIMIT_NPROC: %s\n", argv0, strerror(errno)); - } - - /* validate user and group */ - errno = 0; - if (user && !(pwd = getpwnam(user))) { - die("%s: invalid user %s\n", argv0, user); - } - errno = 0; - if (group && !(grp = getgrnam(group))) { - die("%s: invalid group %s\n", argv0, group); - } - - handlesignals(sigcleanup); - - /* bind socket */ - insock = udsname ? getusock(udsname, pwd->pw_uid, grp->gr_gid) : - getipsock(); - - switch (cpid = fork()) { - case -1: - fprintf(stderr, "%s: fork: %s\n", argv0, strerror(errno)); - break; - case 0: - /* restore default handlers */ - handlesignals(SIG_DFL); - - /* reap children automatically */ - if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) { - die("%s: signal: Failed to set SIG_IGN on SIGCHLD\n", - argv0); - } - - /* chroot */ - if (chdir(servedir) < 0) { - die("%s: chdir %s: %s\n", argv0, servedir, strerror(errno)); - } - if (chroot(".") < 0) { - die("%s: chroot .: %s\n", argv0, strerror(errno)); - } - - /* drop root */ - if (grp && setgroups(1, &(grp->gr_gid)) < 0) { - die("%s: setgroups: %s\n", argv0, strerror(errno)); - } - if (grp && setgid(grp->gr_gid) < 0) { - die("%s: setgid: %s\n", argv0, strerror(errno)); - } - if (pwd && setuid(pwd->pw_uid) < 0) { - die("%s: setuid: %s\n", argv0, strerror(errno)); - } - if (getuid() == 0) { - die("%s: won't run as root user\n", argv0); - } - if (getgid() == 0) { - die("%s: won't run as root group\n", argv0); - } - - serve(insock); - exit(0); - default: - while ((wpid = wait(&status)) > 0) - ; - } - - cleanup(); - return status; -} - -/* - * Copyright (c) 2004 Ted Unangst and Todd Miller - * All rights reserved. - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ -#define INVALID 1 -#define TOOSMALL 2 -#define TOOLARGE 3 - -long long -strtonum(const char *numstr, long long minval, long long maxval, - const char **errstrp) -{ - long long ll = 0; - int error = 0; - char *ep; - struct errval { - const char *errstr; - int err; - } ev[4] = { - { NULL, 0 }, - { "invalid", EINVAL }, - { "too small", ERANGE }, - { "too large", ERANGE }, - }; - - ev[0].err = errno; - errno = 0; - if (minval > maxval) { - error = INVALID; - } else { - ll = strtoll(numstr, &ep, 10); - if (numstr == ep || *ep != '\0') - error = INVALID; - else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval) - error = TOOSMALL; - else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval) - error = TOOLARGE; - } - if (errstrp != NULL) - *errstrp = ev[error].errstr; - errno = ev[error].err; - if (error) - ll = 0; - - return (ll); -} diff --git a/resp.c b/resp.c @@ -0,0 +1,190 @@ +/* See LICENSE file for copyright and license details. */ +#include <dirent.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +#include "http.h" +#include "resp.h" +#include "util.h" + +static int +compareent(const struct dirent **d1, const struct dirent **d2) +{ + int v; + + v = ((*d2)->d_type == DT_DIR ? 1 : -1) - + ((*d1)->d_type == DT_DIR ? 1 : -1); + if (v) { + return v; + } + + return strcmp((*d1)->d_name, (*d2)->d_name); +} + +static char * +suffix(int t) +{ + switch (t) { + case DT_FIFO: return "|"; + case DT_DIR: return "/"; + case DT_LNK: return "@"; + case DT_SOCK: return "="; + } + + return ""; +} + +enum status +resp_dir(int fd, char *name, struct request *r) +{ + struct dirent **e; + size_t i; + int dirlen, s; + static char t[TIMESTAMP_LEN]; + + /* read directory */ + if ((dirlen = scandir(name, &e, NULL, compareent)) < 0) { + return http_send_status(fd, S_FORBIDDEN); + } + + /* send header as late as possible */ + if (dprintf(fd, + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Connection: close\r\n" + "Content-Type: text/html\r\n" + "\r\n", + S_OK, status_str[S_OK], timestamp(time(NULL), t)) < 0) { + s = S_REQUEST_TIMEOUT; + goto cleanup; + } + + if (r->method == M_GET) { + /* listing header */ + if (dprintf(fd, + "<!DOCTYPE html>\n<html>\n\t<head>" + "<title>Index of %s</title></head>\n" + "\t<body>\n\t\t<a href=\"..\">..</a>", + name) < 0) { + s = S_REQUEST_TIMEOUT; + goto cleanup; + } + + /* listing */ + for (i = 0; i < dirlen; i++) { + /* skip hidden files, "." and ".." */ + if (e[i]->d_name[0] == '.') { + continue; + } + + /* entry line */ + if (dprintf(fd, "<br />\n\t\t<a href=\"%s%s\">%s%s</a>", + e[i]->d_name, + (e[i]->d_type == DT_DIR) ? "/" : "", + e[i]->d_name, + suffix(e[i]->d_type)) < 0) { + s = S_REQUEST_TIMEOUT; + goto cleanup; + } + } + + /* listing footer */ + if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) { + s = S_REQUEST_TIMEOUT; + goto cleanup; + } + } + s = S_OK; + +cleanup: + while (dirlen--) { + free(e[dirlen]); + } + free(e); + + return s; +} + +enum status +resp_file(int fd, char *name, struct request *r, struct stat *st, char *mime, + off_t lower, off_t upper) +{ + FILE *fp; + enum status s; + ssize_t bread, bwritten; + off_t remaining; + int range; + static char buf[BUFSIZ], *p, t1[TIMESTAMP_LEN], t2[TIMESTAMP_LEN]; + + /* open file */ + if (!(fp = fopen(name, "r"))) { + s = http_send_status(fd, S_FORBIDDEN); + goto cleanup; + } + + /* seek to lower bound */ + if (fseek(fp, lower, SEEK_SET)) { + s = http_send_status(fd, S_INTERNAL_SERVER_ERROR); + goto cleanup; + } + + /* send header as late as possible */ + range = r->field[REQ_RANGE][0]; + s = range ? S_PARTIAL_CONTENT : S_OK; + + if (dprintf(fd, + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Connection: close\r\n" + "Last-Modified: %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %zu\r\n", + s, status_str[s], timestamp(time(NULL), t1), + timestamp(st->st_mtim.tv_sec, t2), mime, + upper - lower + 1) < 0) { + s = S_REQUEST_TIMEOUT; + goto cleanup; + } + if (range) { + if (dprintf(fd, "Content-Range: bytes %zd-%zd/%zu\r\n", + lower, upper + (upper < 0), st->st_size) < 0) { + s = S_REQUEST_TIMEOUT; + goto cleanup; + } + } + if (dprintf(fd, "\r\n") < 0) { + s = S_REQUEST_TIMEOUT; + goto cleanup; + } + + if (r->method == M_GET) { + /* write data until upper bound is hit */ + remaining = upper - lower + 1; + + while ((bread = fread(buf, 1, MIN(sizeof(buf), remaining), fp))) { + if (bread < 0) { + return S_INTERNAL_SERVER_ERROR; + } + remaining -= bread; + p = buf; + while (bread > 0) { + bwritten = write(fd, p, bread); + if (bwritten <= 0) { + return S_REQUEST_TIMEOUT; + } + bread -= bwritten; + p += bwritten; + } + } + } +cleanup: + if (fp) { + fclose(fp); + } + + return s; +} diff --git a/resp.h b/resp.h @@ -0,0 +1,14 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef RESP_H +#define RESP_H + +#include <sys/stat.h> +#include <sys/types.h> + +#include "http.h" + +enum status resp_dir(int, char *, struct request *); +enum status resp_file(int, char *, struct request *, struct stat *, char *, + off_t, off_t); + +#endif /* RESP_H */ diff --git a/sock.c b/sock.c @@ -0,0 +1,152 @@ +/* See LICENSE file for copyright and license details. */ +#include <arpa/inet.h> +#include <netdb.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/un.h> +#include <unistd.h> + +#include "sock.h" +#include "util.h" + +int +sock_get_ips(const char *host, const char* port) +{ + struct addrinfo hints, *ai, *p; + int ret, insock = 0, yes; + + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_NUMERICSERV; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + if ((ret = getaddrinfo(host, port, &hints, &ai))) { + die("getaddrinfo: %s", gai_strerror(ret)); + } + + for (yes = 1, p = ai; p; p = p->ai_next) { + if ((insock = socket(p->ai_family, p->ai_socktype, + p->ai_protocol)) < 0) { + continue; + } + if (setsockopt(insock, SOL_SOCKET, SO_REUSEADDR, &yes, + sizeof(int)) < 0) { + die("setsockopt:"); + } + if (bind(insock, p->ai_addr, p->ai_addrlen) < 0) { + if (close(insock) < 0) { + die("close:"); + } + continue; + } + break; + } + freeaddrinfo(ai); + if (!p) { + die("bind:"); + } + + if (listen(insock, SOMAXCONN) < 0) { + die("listen:"); + } + + return insock; +} + +void +sock_rem_uds(const char *udsname) +{ + if (unlink(udsname) < 0) { + die("unlink:"); + } +} + +int +sock_get_uds(const char *udsname, uid_t uid, gid_t gid) +{ + struct sockaddr_un addr; + size_t udsnamelen; + int insock, sockmode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH; + + if ((insock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { + die("socket:"); + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + + if ((udsnamelen = strlen(udsname)) > sizeof(addr.sun_path) - 1) { + die("UNIX-domain socket name truncated"); + } + memcpy(addr.sun_path, udsname, udsnamelen + 1); + + if (bind(insock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { + die("bind %s:", udsname); + } + + if (listen(insock, SOMAXCONN) < 0) { + sock_rem_uds(udsname); + die("listen:"); + } + + if (chmod(udsname, sockmode) < 0) { + sock_rem_uds(udsname); + die("chmod:"); + } + + if (chown(udsname, uid, gid) < 0) { + sock_rem_uds(udsname); + die("chown:"); + } + + return insock; +} + +int +sock_set_timeout(int fd, int sec) +{ + struct timeval tv; + + tv.tv_sec = sec; + tv.tv_usec = 0; + + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0 || + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) { + warn("setsockopt:"); + return 1; + } + + return 0; +} + +int +sock_get_inaddr_str(struct sockaddr_storage *in_sa, char *str, size_t len) +{ + switch (in_sa->ss_family) { + case AF_INET: + if (!inet_ntop(AF_INET, + &(((struct sockaddr_in *)in_sa)->sin_addr), + str, len)) { + warn("inet_ntop:"); + return 1; + } + break; + case AF_INET6: + if (!inet_ntop(AF_INET6, + &(((struct sockaddr_in6 *)in_sa)->sin6_addr), + str, len)) { + warn("inet_ntop:"); + return 1; + } + break; + default: + snprintf(str, len, "uds"); + } + + return 0; +} diff --git a/sock.h b/sock.h @@ -0,0 +1,15 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef SOCK_H +#define SOCK_H + +#include <stddef.h> +#include <sys/socket.h> +#include <sys/types.h> + +int sock_get_ips(const char *, const char *); +void sock_rem_uds(const char *); +int sock_get_uds(const char *, uid_t, gid_t); +int sock_set_timeout(int, int); +int sock_get_inaddr_str(struct sockaddr_storage *, char *, size_t); + +#endif /* SOCK_H */ diff --git a/util.c b/util.c @@ -0,0 +1,102 @@ +/* See LICENSE file for copyright and license details. */ +#include <errno.h> +#include <limits.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include "util.h" + +char *argv0; + +static void +verr(const char *fmt, va_list ap) +{ + if (argv0 && strncmp(fmt, "usage", strlen("usage"))) { + fprintf(stderr, "%s: ", argv0); + } + + vfprintf(stderr, fmt, ap); + + if (fmt[0] && fmt[strlen(fmt) - 1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } +} + +void +warn(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + verr(fmt, ap); + va_end(ap); +} + +void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + verr(fmt, ap); + va_end(ap); + + exit(1); +} + +#define INVALID 1 +#define TOOSMALL 2 +#define TOOLARGE 3 + +long long +strtonum(const char *numstr, long long minval, long long maxval, + const char **errstrp) +{ + long long ll = 0; + int error = 0; + char *ep; + struct errval { + const char *errstr; + int err; + } ev[4] = { + { NULL, 0 }, + { "invalid", EINVAL }, + { "too small", ERANGE }, + { "too large", ERANGE }, + }; + + ev[0].err = errno; + errno = 0; + if (minval > maxval) { + error = INVALID; + } else { + ll = strtoll(numstr, &ep, 10); + if (numstr == ep || *ep != '\0') + error = INVALID; + else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval) + error = TOOSMALL; + else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval) + error = TOOLARGE; + } + if (errstrp != NULL) + *errstrp = ev[error].errstr; + errno = ev[error].err; + if (error) + ll = 0; + + return ll; +} + +char * +timestamp(time_t t, char buf[TIMESTAMP_LEN]) +{ + strftime(buf, TIMESTAMP_LEN, "%a, %d %b %Y %T GMT", gmtime(&t)); + + return buf; +} diff --git a/util.h b/util.h @@ -0,0 +1,27 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef UTIL_H +#define UTIL_H + +#include <time.h> + +#include "arg.h" + +#undef MIN +#define MIN(x,y) ((x) < (y) ? (x) : (y)) +#undef MAX +#define MAX(x,y) ((x) > (y) ? (x) : (y)) +#undef LEN +#define LEN(x) (sizeof (x) / sizeof *(x)) + +extern char *argv0; + +void warn(const char *, ...); +void die(const char *, ...); + +long long strtonum(const char *, long long, long long, const char **); + +#define TIMESTAMP_LEN 30 + +char *timestamp(time_t, char buf[TIMESTAMP_LEN]); + +#endif /* UTIL_H */