quark

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

commit 0ce86bba1549408a490f2ac8f97fc948323a9418
parent 0e8cac1ee4e2f4a76ea7417d9dc727694c8b69da
Author: FRIGN <dev@frign.de>
Date:   Fri,  2 Sep 2016 09:59:02 +0200

Initial commit of quark rewrite

Roughly 700 LOC (half of the old quark on the Hiltjo branch) in size,
this rewrite supports partial content and other good stuff that will
make it fun again to use quark for simple static purposes.
The error checking is rigorous and strict and it will report proper
error codes back to the client whenever there was a problem or the
request was invalid in some way.

A cool feature is the support for listening on a UNIX-domain socket,
which will in the long run allow us to solve problems with virtual hosts
and other things in separate programs. But until then, this should be
robust enough for most use-cases.

This resets quark's version to 0, but this was no problem as there
haven't been any quark releases yet.

Feedback is appreciated.

Diffstat:
ALICENSE | 15+++++++++++++++
AMakefile | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Aarg.h | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.def.h | 35+++++++++++++++++++++++++++++++++++
Aconfig.mk | 20++++++++++++++++++++
Aquark.1 | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aquark.c | 744+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 988 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,15 @@ +ISC-License + +(c) 2016 Laslo Hunhold <dev@frign.de> + +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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -0,0 +1,744 @@ +/* See LICENSE file for license details. */ +#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 <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 <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; + +#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<!DOCTYPE html>\r\n<html>\r\n" + "\t<head><title>%d %s</title></head>" + "\r\n\t<body><h1>%d %s</h1></body>\r\n" + "</html>\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), "<!DOCTYPE html>\r\n" + "<html>\r\n<head><title>Index of %s" + "</title></head>\r\n<body>\r\n" + "<a href=\"..\">..</a><br />\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), "<a href=\"%s" + "\">%s</a><br />\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</body></html>\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; +}