diff --git a/LICENSE b/LICENSE index f1f5603..5669b86 100644 --- a/LICENSE +++ b/LICENSE @@ -2,6 +2,8 @@ ISC-License Copyright 2016-2017 Laslo Hunhold +Copyright 2004 Ted Unangst +Copyright 2004 Todd C. Miller Copyright 2017 Hiltjo Posthuma Copyright 2017 Quentin Rameau diff --git a/Makefile b/Makefile index 5f0d796..cf57f13 100644 --- 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 index 3387e74..4a29c83 100644 --- 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 new file mode 100644 index 0000000..6f2b7c2 --- /dev/null +++ b/http.c @@ -0,0 +1,514 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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" + "\n\n\t\n" + "\t\t%d %s\n\t\n\t\n" + "\t\t

%d %s

\n\t\n\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 new file mode 100644 index 0000000..cd1ba22 --- /dev/null +++ b/http.h @@ -0,0 +1,55 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef HTTP_H +#define HTTP_H + +#include + +#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 new file mode 100644 index 0000000..5dc9034 --- /dev/null +++ b/main.c @@ -0,0 +1,261 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 deleted file mode 100644 index ad98640..0000000 --- a/quark.c +++ /dev/null @@ -1,1166 +0,0 @@ -/* See LICENSE file for copyright and license details. */ -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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" - "\n\n\t\n" - "\t\t%d %s\n\t\n\t\n" - "\t\t

%d %s

\n\t\n\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, - "\n\n\t" - "Index of %s\n" - "\t\n\t\t..", - 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, "
\n\t\t%s%s", - 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\n\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 new file mode 100644 index 0000000..7a7d7a2 --- /dev/null +++ b/resp.c @@ -0,0 +1,190 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include + +#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, + "\n\n\t" + "Index of %s\n" + "\t\n\t\t..", + 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, "
\n\t\t%s%s", + 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\n\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 new file mode 100644 index 0000000..d5928ef --- /dev/null +++ b/resp.h @@ -0,0 +1,14 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef RESP_H +#define RESP_H + +#include +#include + +#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 new file mode 100644 index 0000000..13e5bfb --- /dev/null +++ b/sock.c @@ -0,0 +1,152 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 new file mode 100644 index 0000000..a39aec9 --- /dev/null +++ b/sock.h @@ -0,0 +1,15 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef SOCK_H +#define SOCK_H + +#include +#include +#include + +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 new file mode 100644 index 0000000..1529767 --- /dev/null +++ b/util.c @@ -0,0 +1,102 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include + +#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 new file mode 100644 index 0000000..7aa8584 --- /dev/null +++ b/util.h @@ -0,0 +1,27 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef UTIL_H +#define UTIL_H + +#include + +#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 */