diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0b028e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC-License + +(c) 2016 Laslo Hunhold + +Permission to use, copy, modify, and/or 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a4bb320 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +# quark - simple web server + +include config.mk + +all: options quark + +options: + @echo quark build options: + @echo "CFLAGS = ${CFLAGS}" + @echo "LDFLAGS = ${LDFLAGS}" + @echo "CC = ${CC}" + +quark: quark.o config.h config.mk + @echo CC -o $@ + @${CC} -o $@ quark.o ${LDFLAGS} + +quark.o: quark.c config.h config.mk + @echo CC -c quark.c + @${CC} -c ${CFLAGS} quark.c + +config.h: + @echo creating $@ from config.def.h + @cp config.def.h $@ + +clean: + @echo cleaning + @rm -f quark quark.o quark-${VERSION}.tar.gz + +dist: clean + @echo creating dist tarball + @mkdir -p quark-${VERSION} + @cp -R LICENSE Makefile arg.h config.h config.mk quark.1 quark.c quark-${VERSION} + @tar -cf quark-${VERSION}.tar quark-${VERSION} + @gzip quark-${VERSION}.tar + @rm -rf quark-${VERSION} + +install: all + @echo installing executable file to ${DESTDIR}${PREFIX}/bin + @mkdir -p ${DESTDIR}${PREFIX}/bin + @cp -f quark ${DESTDIR}${PREFIX}/bin + @chmod 755 ${DESTDIR}${PREFIX}/bin/quark + @echo installing manual page to ${DESTDIR}${MANPREFIX}/man1 + @mkdir -p ${DESTDIR}${MANPREFIX}/man1 + @cp quark.1 ${DESTDIR}${MANPREFIX}/man1/quark.1 + @chmod 644 ${DESTDIR}${MANPREFIX}/man1/quark.1 + +uninstall: + @echo removing executable file from ${DESTDIR}${PREFIX}/bin + @rm -f ${DESTDIR}${PREFIX}/bin/quark + @echo removing manual from ${DESTDIR}${MANPREFIX}/man1 + @rm -f ${DESTDIR}${MANPREFIX}/man1/quark.1 + +.PHONY: all options clean dist install uninstall diff --git a/arg.h b/arg.h new file mode 100644 index 0000000..0b23c53 --- /dev/null +++ b/arg.h @@ -0,0 +1,65 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + for (brk_ = 0, argv[0]++, argv_ = argv;\ + argv[0][0] && !brk_;\ + argv[0]++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][0];\ + switch (argc_) + +/* Handles obsolete -NUM syntax */ +#define ARGNUM case '0':\ + case '1':\ + case '2':\ + case '3':\ + case '4':\ + case '5':\ + case '6':\ + case '7':\ + case '8':\ + case '9' + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define ARGNUMF() (brk_ = 1, estrtonum(argv[0], 0, INT_MAX)) + +#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define LNGARG() &argv[0][0] + +#endif diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..d7237af --- /dev/null +++ b/config.def.h @@ -0,0 +1,35 @@ +static const char *host = "127.0.0.1"; +static const char *port = "80"; +static const char *servedir = "."; +static const char *docindex = "index.html"; +static const int listdirs = 1; +static const char *user = "nobody"; +static const char *group = "nogroup"; +static const int maxnprocs = 512; + +#define MAXREQLEN 4096 /* >= 4 */ + +static const struct { + char *ext; + char *type; +} mimes[] = { + { "xml", "application/xml" }, + { "xhtml", "application/xhtml+xml" }, + { "html", "text/html; charset=UTF-8" }, + { "html", "text/html; charset=UTF-8" }, + { "htm", "text/html; charset=UTF-8" }, + { "css", "text/css" }, + { "txt", "text/plain" }, + { "text", "text/plain" }, + { "md", "text/plain" }, + { "png", "image/png" }, + { "gif", "image/gif" }, + { "jpg", "image/jpg" }, + { "c", "text/plain" }, + { "h", "text/plain" }, + { "iso", "application/x-iso9660-image" }, + { "gz", "application/x-gtar" }, + { "pdf", "application/x-pdf" }, + { "tar", "application/tar" }, + { "mp3", "audio/mp3" }, +}; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..305f59b --- /dev/null +++ b/config.mk @@ -0,0 +1,20 @@ +# quark version +VERSION = 0 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +# includes and libs +INCS = -I. -I/usr/include +LIBS = -L/usr/lib -lc + +# flags +CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE +CFLAGS = -g -std=c99 -pedantic -Wall -Os ${INCS} ${CPPFLAGS} +LDFLAGS = ${LIBS} + +# compiler and linker +CC = cc diff --git a/quark.1 b/quark.1 new file mode 100644 index 0000000..e486bb8 --- /dev/null +++ b/quark.1 @@ -0,0 +1,56 @@ +.Dd 2016-09-02 +.Dt QUARK 1 +.Sh NAME +.Nm quark +.Nd simple web server +.Sh SYNOPSIS +.Nm +.Op Fl v +.Oo +.Oo +.Op Fl h Ar host +.Op Fl p Ar port +.Oc +| +.Op Fl U Ar udsocket +.Oc +.Op Fl d Ar dir +.Op Fl u Ar user +.Op Fl g Ar group +.Sh DESCRIPTION +.Nm +is a simple HTTP GET only web server that can be multiplexed using +UNIX-domain sockets. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl d Ar dir +Serve +.Ar dir +after chrooting into it. +.It Fl g Ar group +Set group ID to the ID of +.Ar group +when dropping privileges. +.It Fl h Ar host +Use +.Ar host +as the server hostname. +.It Fl p Ar port +Listen on port +.Ar port +for incoming connections. +.It Fl u Ar user +Set user ID to the ID of +.Ar user +when dropping privileges. +.It Fl U Ar udsocket +Create the UNIX-domain socket +.Ar udsocket +and listen on it for incoming connections. +.It Fl v +Print version information to stdout and exit. +.El +.Sh CUSTOMIZATION +.Nm +can be customized by creating a custom config.h from config.def.h and +(re)compiling the source code. This keeps it fast, secure and simple. diff --git a/quark.c b/quark.c new file mode 100644 index 0000000..6ff50f5 --- /dev/null +++ b/quark.c @@ -0,0 +1,744 @@ +/* See LICENSE file for 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 "arg.h" + +char *argv0; + +#include "config.h" + +static char *status[] = { + [200] = "OK", + [206] = "Partial Content", + [301] = "Moved Permanently", + [400] = "Bad Request", + [403] = "Forbidden", + [404] = "Not Found", + [405] = "Method Not Allowed", + [408] = "Request Time-out", + [431] = "Request Header Fields Too Large", + [500] = "Internal Server Error", + [505] = "HTTP Version not supported", + +}; + +#undef MIN +#define MIN(x,y) ((x) < (y) ? (x) : (y)) + +static char * +timestamp(time_t t) +{ + static char s[30]; + + if (!t) + t = time(NULL); + strftime(s, sizeof(s), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&t)); + + return s; +} + +static int +sendstatus(int code, int fd, ...) +{ + va_list ap; + char buf[4096]; + size_t written, buflen; + ssize_t ret; + long lower, upper, size; + + buflen = snprintf(buf, 4096, "HTTP/1.1 %d %s\r\n", code, + status[code]); + + buflen += snprintf(buf + buflen, 4096 - buflen, "Date: %s\r\n", + timestamp(0)); + va_start(ap, fd); + switch (code) { + case 200: /* arg-list: mime, size */ + buflen += snprintf(buf + buflen, 4096 - buflen, + "Content-Type: %s\r\n", + va_arg(ap, char *)); + if ((size = va_arg(ap, long)) >= 0) { + buflen += snprintf(buf + buflen, 4096 - buflen, + "Content-Length: %ld\r\n", + size); + } + break; + case 206: /* arg-list: mime, lower, upper, size */ + buflen += snprintf(buf + buflen, 4096 - buflen, + "Content-Type: %s\r\n", + va_arg(ap, char *)); + lower = va_arg(ap, long); + upper = va_arg(ap, long); + size = va_arg(ap, long); + buflen += snprintf(buf + buflen, 4096 - buflen, + "Content-Range: bytes %ld-%ld/%ld\r\n", + lower, upper, size); + buflen += snprintf(buf + buflen, 4096 - buflen, + "Content-Length: %ld\r\n", + (upper - lower) + 1); + break; + case 301: /* arg-list: host, url */ + if (!strcmp(port, "80")) { + buflen += snprintf(buf + buflen, 4096 - buflen, + "Location: http://%s%s\r\n", + va_arg(ap, char *), + va_arg(ap, char *)); + } else { + buflen += snprintf(buf + buflen, 4096 - buflen, + "Location: http://%s:%s%s\r\n", + va_arg(ap, char *), port, + va_arg(ap, char *)); + } + break; + case 405: /* arg-list: none */ + buflen += snprintf(buf + buflen, 4096 - buflen, + "Allow: GET\r\n"); + break; + } + va_end(ap); + + buflen += snprintf(buf + buflen, 4096 - buflen, + "Connection: close\r\n"); + + if (code != 200 && code != 206) { + buflen += snprintf(buf + buflen, 4096 - buflen, + "Content-Type: text/html\r\n"); + buflen += snprintf(buf + buflen, 4096 - buflen, + "\r\n\r\n\r\n" + "\t%d %s" + "\r\n\t

%d %s

\r\n" + "\r\n", code, status[code], + code, status[code]); + } else { + buflen += snprintf(buf + buflen, 4096 - buflen, "\r\n"); + } + + for (written = 0; buflen > 0; written += ret, buflen -= ret) { + if ((ret = write(fd, buf + written, buflen)) < 0) { + code = 408; + break; + } + } + + return code; +} + +static size_t +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] = (char)(n & 255); + s += 2; + } else { + dest[i] = *s; + } + } + dest[i] = '\0'; + + return i; +} + +static size_t +encode(char src[PATH_MAX], char dest[PATH_MAX]) +{ + size_t i; + char *s; + + for (s = src, i = 0; *s; s++) { + if (isalnum(*s) || *s == '~' || *s == '-' || *s == '.' || + *s == '_') { + i += snprintf(dest + i, PATH_MAX - i, "%%%02X", *s); + } else { + dest[i] = *s; + i++; + } + } + + return 0; +} + +static int +listdir(char *dir, int fd) +{ + struct dirent **e = NULL; + static char buf[BUFSIZ]; + size_t buflen; + ssize_t bread, written; + int dirlen, ret, i; + + if ((dirlen = scandir(dir, &e, NULL, alphasort)) < 0) { + return sendstatus(403, fd); + } + if ((ret = sendstatus(200, fd, "text/html", (long)-1)) != 200) { + return ret; + } + if ((buflen = snprintf(buf, sizeof(buf), "\r\n" + "\r\nIndex of %s" + "\r\n\r\n" + "..
\r\n", + dir)) >= sizeof(buf)) { + return 500; + } + written = 0; + while (buflen > 0) { + if ((bread = write(fd, buf + written, buflen)) < 0) { + return 408; + } + written += bread; + buflen -= bread; + } + + for (i = 0; i < dirlen; i++) { + if (e[i]->d_name[0] == '.') { /* hidden files, ., .. */ + continue; + } + if ((buflen = snprintf(buf, sizeof(buf), "%s
\r\n", e[i]->d_name, + e[i]->d_name)) >= sizeof(buf)) { + return 500; + } + written = 0; + while (buflen > 0) { + if ((bread = write(fd, buf + written, buflen)) < 0) { + return 408; + } + written += bread; + buflen -= bread; + } + } + + if ((buflen = snprintf(buf, sizeof(buf), "\r\n\r\n")) + >= sizeof(buf)) { + return 500; + } + written = 0; + while (buflen > 0) { + if ((bread = write(fd, buf + written, buflen)) < 0) { + return 408; + } + written += bread; + buflen -= bread; + } + + return 200; +} + +static int +handle(int infd, char **url) +{ + FILE *fp; + struct stat st; + size_t reqlen, urllen, i; + ssize_t off, buflen, written; + long lower, upper, fsize, remaining; + int needredirect, ret; + static char req[MAXREQLEN], buf[BUFSIZ], + urlenc[PATH_MAX], urldec[PATH_MAX], + urldecnorm[PATH_MAX], urldecnormind[PATH_MAX], + reqhost[256], range[128], modsince[30]; + char *p, *q, *mime; + + /* get request header */ + for (reqlen = 0; ;) { + if ((off = read(infd, req + reqlen, MAXREQLEN - reqlen)) < 0) { + return sendstatus(408, infd); + } else if (off == 0) { + break; + } + reqlen += off; + if (reqlen >= 4 && !memcmp(req + reqlen - 4, "\r\n\r\n", 4)) { + break; + } + if (reqlen == MAXREQLEN) { + return sendstatus(431, infd); + } + } + if (reqlen < 2) { + return sendstatus(400, infd); + } + reqlen -= 2; /* remove last \r\n */ + req[reqlen] = '\0'; /* make it safe */ + + /* parse request line */ + if (reqlen < 3) { + return sendstatus(400, infd); + } else if (strncmp(req, "GET", sizeof("GET") - 1)) { + return sendstatus(405, infd); + } else if (req[3] != ' ') { + return sendstatus(400, infd); + } + for (p = req + sizeof("GET ") - 1; *p && *p != ' '; p++) + ; + if (!*p) { + return sendstatus(400, infd); + } + *p = '\0'; + if (snprintf(urlenc, sizeof(urlenc), "%s", + req + sizeof("GET ") - 1) >= sizeof(urlenc)) { + return sendstatus(400, infd); + } + *url = urldecnorm; + if (!strlen(urlenc)) { + return sendstatus(400, infd); + } + p += sizeof(" ") - 1; + if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { + return sendstatus(400, infd); + } + p += sizeof("HTTP/") - 1; + if (strncmp(p, "1.0", sizeof("1.0") - 1) && + strncmp(p, "1.1", sizeof("1.1") - 1)) { + return sendstatus(505, infd); + } + p += sizeof("1.*") - 1; + if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { + return sendstatus(400, infd); + } + p += sizeof("\r\n") - 1; + + /* parse header fields */ + for (; (q = strstr(p, "\r\n")); p = q + sizeof("\r\n") - 1) { + *q = '\0'; + if (!strncmp(p, "Host:", sizeof("Host:") - 1)) { + p += sizeof("Host:") - 1; + while (isspace(*p)) { + p++; + } + if (snprintf(reqhost, sizeof(reqhost), "%s", p) >= + sizeof(reqhost)) { + return sendstatus(500, infd); + } + } else if (!strncmp(p, "Range:", sizeof("Range:") - 1)) { + p += sizeof("Range:") - 1; + while (isspace(*p)) { + p++; + } + if (snprintf(range, sizeof(range), "%s", p) >= + sizeof(range)) { + return sendstatus(500, infd); + } + } else if (!strncmp(p, "If-Modified-Since:", + sizeof("If-Modified-Since:") - 1)) { + p+= sizeof("If-Modified-Since:") - 1; + while (isspace(*p)) { + p++; + } + if (snprintf(modsince, sizeof(modsince), "%s", p) >= + sizeof(modsince)) { + return sendstatus(500, infd); + } + } + } + + /* normalization */ + needredirect = 0; + decode(urlenc, urldec); + if (!realpath(urldec, urldecnorm)) { + /* todo: break up the cases */ + return sendstatus((errno == EACCES) ? 403 : 404, infd); + } + + /* hidden path? */ + if (urldecnorm[0] == '.' || strstr(urldecnorm, "/.")) { + return sendstatus(403, infd); + } + /* check if file or directory */ + if (stat(urldecnorm, &st) < 0) { + /* todo: break up the cases */ + return sendstatus(404, infd); + } + if (S_ISDIR(st.st_mode)) { + /* add / at the end, was removed by realpath */ + urllen = strlen(urldecnorm); + if (urldecnorm[urllen - 1] != '/') { + urldecnorm[urllen + 1] = '\0'; + urldecnorm[urllen] = '/'; + } + + /* is a / at the end on the raw string? */ + urllen = strlen(urldec); + if (urldec[urllen - 1] != '/') { + needredirect = 1; + } else if (!needredirect) { + /* check index */ + if (snprintf(urldecnormind, sizeof(urldecnormind), + "%s/%s", urldecnorm, docindex) >= + sizeof(urldecnorm)) { + return sendstatus(400, infd); + } + if (stat(urldecnormind, &st) < 0) { + /* no index, serve dir */ + if (!listdirs) { + return sendstatus(403, infd); + } + return listdir(urldecnorm, infd); + } + } + } + if (strcmp(urldec, urldecnorm)) { + needredirect = 1; + } + if (needredirect) { + encode(urldecnorm, urlenc); + return sendstatus(301, infd, urlenc, + reqhost[0] ? reqhost : host); + } + + /* range */ + lower = 0; + upper = LONG_MAX; + if (range[0]) { + if (strncmp(range, "bytes=", sizeof("bytes=") - 1)) { + return sendstatus(400, infd); + } + p = range + sizeof("bytes=") - 1; + if (!(q = strchr(p, '-'))) { + return sendstatus(400, infd); + } + *(q++) = '\0'; + if (p[0]) { + lower = atoi(p); + } + if (q[0]) { + upper = atoi(q); + } + } + + /* serve file */ + if (!(fp = fopen(urldecnorm, "r"))) { + return sendstatus(403, infd); + } + mime = "text/plain"; + if ((p = strrchr(urldecnorm, '.'))) { + for (i = 0; i < sizeof(mimes)/sizeof(*mimes); i++) { + if (!strcmp(mimes[i].ext, p + 1)) { + mime = mimes[i].type; + break; + } + } + } + if (fseek(fp, 0, SEEK_END) || (fsize = ftell(fp)) < 0) { + return sendstatus(500, infd); + } + rewind(fp); + if (fsize && upper > fsize) { + upper = fsize - 1; + } + if (fseek(fp, lower, SEEK_SET)) { + return sendstatus(500, infd); + } + if (!range[0]) { + if ((ret = sendstatus(200, infd, mime, (long)fsize)) != 200) { + return ret; + } + } else { + if ((ret = sendstatus(206, infd, mime, lower, + upper, fsize)) != 206) { + return ret; + } + } + remaining = (upper - lower) + 1; + while ((buflen = fread(buf, 1, MIN(sizeof(buf), remaining), + fp))) { + remaining -= buflen; + if (buflen < 0) { + return 500; + } + p = buf; + while (buflen > 0) { + written = write(infd, p, buflen); + if (written <= 0) { + return 408; + } + buflen -= written; + p += written; + } + } + + return 200; +} + +static void +serve(int insock) +{ + struct sockaddr_storage in_sa; + struct timeval tv; + pid_t p; + socklen_t in_sa_len; + time_t t; + int infd, status; + char inip4[INET_ADDRSTRLEN], inip6[INET6_ADDRSTRLEN], *url = "", + tstmp[25]; + + while (1) { + 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; + } + + switch ((p = fork())) { + case -1: + fprintf(stderr, "%s: fork: %s", argv0, + strerror(errno)); + break; + case 0: + close(insock); + + /* timeouts */ + 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; + } + + status = handle(infd, &url); + + /* 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\n", tstmp, inip4, status, url); + } else { + inet_ntop(AF_INET6, + &(((struct sockaddr_in6*)&in_sa)->sin6_addr), + inip6, sizeof(inip6)); + printf("%s\t%s\t%d\t%s\n", tstmp, inip6, status, url); + } + + shutdown(infd, SHUT_RD); + shutdown(infd, SHUT_WR); + close(infd); + _exit(EXIT_SUCCESS); + default: + close(infd); + } + } +} + +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_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 int +getusock(char *udsname) +{ + struct sockaddr_un addr; + int insock; + + 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; + strncpy(addr.sun_path, udsname, sizeof(addr.sun_path) - 1); + + unlink(udsname); + + if (bind(insock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { + die("%s: bind: %s\n", argv0, strerror(errno)); + } + + if (listen(insock, SOMAXCONN) < 0) { + die("%s: listen: %s\n", argv0, strerror(errno)); + } + + return insock; +} + +static void +usage(void) +{ + fprintf(stderr, "usage: %s [-v] [[[-h host] [-p port]] | " + "[-U udsocket]] [-d dir] [-u user] [-g group]\n", argv0); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + struct passwd *pwd = NULL; + struct group *grp = NULL; + struct rlimit rlim; + int insock; + char *udsname = NULL; + + ARGBEGIN { + case 'd': + servedir = EARGF(usage()); + break; + case 'g': + group = EARGF(usage()); + break; + case 'h': + host = EARGF(usage()); + break; + case 'p': + port = EARGF(usage()); + break; + case 'u': + user = EARGF(usage()); + break; + case 'U': + udsname = EARGF(usage()); + break; + case 'v': + fputs("quark-"VERSION"\n", stderr); + return 0; + default: + usage(); + } ARGEND + + if (argc) + usage(); + + /* reap children automatically */ + if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) { + fprintf(stderr, "%s: signal: Failed to set SIG_IGN on" + "SIGCHLD\n", argv0); + return 1; + } + + /* raise the process limit */ + rlim.rlim_cur = rlim.rlim_max = maxnprocs; + if (setrlimit(RLIMIT_NPROC, &rlim) < 0) { + fprintf(stderr, "%s: setrlimit RLIMIT_NPROC: %s", argv0, + strerror(errno)); + return 1; + } + + /* 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); + } + + /* bind socket */ + insock = udsname ? getusock(udsname) : getipsock(); + + /* 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); + close(insock); + + return 0; +}