git.strcat.st

/strcat/httpd.git/ - summarytreelogarchive

subject
we on git now nurga
commit
ed418c9c18afd3fc2188e04e4c308c8afee80a4d
date
2026-04-17T20:04:48Z
message
diff
 Makefile  |   22 +
 README.md |   37 ++
 config.h  |   31 +
 khttpd.c  | 1935 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 2025 insertions(+)

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a1b5bd3
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,22 @@
+CC	?=	 cc
+CFLAGS	?=	 -O2 -Wall -Wextra -pedantic
+LDFLAGS	?=	 -static
+PREFIX	?=	 /usr
+BINDIR	?=	 $(PREFIX)/bin
+PTHREAD_FLAGS ?= -pthread
+
+.PHONY: all clean info install uninstall
+
+all: khttpd
+
+khttpd: khttpd.c
+	$(CC) $(CFLAGS) $(PTHREAD_FLAGS) -o $@ $< $(LDFLAGS)
+
+clean:
+	rm -f khttpd khttpd.o
+
+install: khttpd
+	install -m 0755 khttpd $(BINDIR)/khttpd
+
+uninstall:
+	rm -f $(BINDIR)/khttpd
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d1a8c2d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+````
+khttpd - KISS http daemon
+
+tiny HTTP server in ANSI compliant C using POLL(2).
+
+build:
+	make
+
+make (un)install:
+	make install
+	make uninstall
+
+usage:
+
+	khttpd args.. -r /var/www
+
+arguments:
+	-D		daemonize
+	-H		send Server header
+	-M		use text/plain for unknown types
+	-l		enable directory listings
+	--css file	listing page css	
+	-a addr		bind address 			[default: 0.0.0.0   ]
+	-b bytes	max request header bytes	[default: 4095      ]
+	-p port		bind port			[default: 8080      ]
+	-r root		web root			[default: cwd       ]
+	-c		enable chroot
+	-u user		drop privileges to user
+	-g group	drop privileges to group
+	-m max		max concurrent connections	[default: 128       ]
+	-i index	index filename			[default: index.html]
+	-t seconds	idle read timeout		[default: 10        ]
+	-T threads      number of threads/workers       [default: 1         ]
+
+notes:
+	no TLS, use stunnel or something.
+```
diff --git a/config.h b/config.h
new file mode 100644
index 0000000..a7b8582
--- /dev/null
+++ b/config.h
@@ -0,0 +1,31 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define KHTTPD_NAME				"khttpd"
+#define KHTTPD_HTTP_VERSION			"HTTP/1.0"
+#define KHTTPD_SERVER_HEADER			"Server: " KHTTPD_NAME "\r\n"
+
+#define KHTTPD_STATUS_OK			"OK"
+#define KHTTPD_STATUS_BAD_REQUEST		"Bad Request"
+#define KHTTPD_STATUS_FORBIDDEN			"Forbidden"
+#define KHTTPD_STATUS_NOT_FOUND			"Not Found"
+#define KHTTPD_STATUS_METHOD_NOT_ALLOWED	"Method Not Allowed"
+#define KHTTPD_STATUS_MOVED_PERMANENTLY		"Moved Permanently"
+#define KHTTPD_STATUS_PARTIAL_CONTENT		"Partial Content"
+#define KHTTPD_STATUS_NOT_MODIFIED		"Not Modified"
+#define KHTTPD_STATUS_URI_TOO_LONG		"URI Too Long"
+#define KHTTPD_STATUS_RANGE_NOT_SATISFIABLE	"Range Not Satisfiable"
+#define KHTTPD_STATUS_INTERNAL_ERROR		"Internal Server Error"
+#define KHTTPD_STATUS_HTTP_UNSUPPORTED		"HTTP Version Not Supported"
+#define KHTTPD_STATUS_ERROR			"Error"
+
+#define KHTTPD_MSG_BAD_REQUEST			"bad request\n"
+#define KHTTPD_MSG_FORBIDDEN			"forbidden\n"
+#define KHTTPD_MSG_NOT_FOUND			"not found\n"
+#define KHTTPD_MSG_METHOD_NOT_ALLOWED		"method not allowed\n"
+#define KHTTPD_MSG_SERVER_BUSY			"server busy\n"
+#define KHTTPD_MSG_SERVER_ERROR			"server error\n"
+#define KHTTPD_MSG_HTTP_UNSUPPORTED		"http version not supported\n"
+#define KHTTPD_MSG_URI_TOO_LONG			"uri too long\n"
+
+#endif
diff --git a/khttpd.c b/khttpd.c
new file mode 100644
index 0000000..9b94ad1
--- /dev/null
+++ b/khttpd.c
@@ -0,0 +1,1935 @@
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <limits.h>
+#include <poll.h>
+#include <pthread.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#define REQ_BUF		4096
+#define RESP_BUF	512
+#define DEFAULT_PORT	8080
+#define DEFAULT_MAX_CONN 128
+#define DEFAULT_THREADS	4
+#define DEFAULT_MAX_JOBS	1024
+
+struct client {
+	int	fd;
+	int	used;
+	time_t	last;
+	size_t	len;
+	char	buf[REQ_BUF];
+};
+
+struct job {
+	int		fd;
+	int		idx;
+	struct client	*clients;
+	int		*cur_conn;
+	char		req[REQ_BUF];
+	struct job	*next;
+};
+
+static char		g_root_buf[PATH_MAX] = ".";
+static const char	*g_root = g_root_buf;
+static const char	*g_index = "index.html";
+static int		g_max_conn = DEFAULT_MAX_CONN;
+static int		g_dirlist = 0;
+static const char	*g_css = NULL;
+static int		g_plain = 0;
+static int		g_server_hdr = 0;
+static int		g_timeout = 10;
+static size_t		g_max_hdr = REQ_BUF - 1;
+static const char	*g_progname = KHTTPD_NAME;
+static int		g_threads = DEFAULT_THREADS;
+static int		g_max_jobs = DEFAULT_MAX_JOBS;
+static pthread_mutex_t	g_job_mtx = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t	g_job_cv = PTHREAD_COND_INITIALIZER;
+static struct job	*g_job_head = NULL;
+static struct job	*g_job_tail = NULL;
+static size_t		g_job_len = 0;
+static pthread_mutex_t	g_conn_mtx = PTHREAD_MUTEX_INITIALIZER;
+
+static void		usage(void);
+static int		set_nonblock(int);
+static uid_t		parse_uid(const char *);
+static gid_t		parse_gid(const char *);
+static int		bad_path(const char *);
+static const char	*status_msg(int);
+static int		request_complete(const char *);
+static void		write_hdr(int, const char *, size_t, int);
+static size_t		xstrlcpy(char *, const char *, size_t);
+static size_t		xstrlcat(char *, const char *, size_t);
+static int		path_within_root(const char *);
+static int		resolve_path_under_root(const char *, char *);
+static int		write_all(int, const void *, size_t);
+static void		write_html_escaped(int, const char *);
+static void		write_url_escaped(int, const char *);
+static void		parent_url(const char *, char *, size_t);
+static const char	*mime_type(const char *);
+static int		encoding_allowed(const char *, const char *);
+static int		parse_range_header(const char *, off_t, off_t *, off_t *);
+static int		month_idx(const char *);
+static long long	days_from_civil(int, unsigned, unsigned);
+static int		parse_http_date(const char *, time_t *);
+static void		format_http_date(time_t, char *, size_t);
+static void		make_etag(const struct stat *, char *, size_t);
+static int		etag_matches(const char *, const char *);
+static void		send_redirect(int, const char *);
+static void		send_simple(int, int, const char *);
+static int		serve_file(int, const char *, const char *, int, const char *,
+		    	const char *, int, time_t, const char *);
+static int		serve_dir(int, const char *, const char *, int);
+static void		handle_request(int, const char *);
+static int		setup_listener(const char *, int);
+static int		parse_int(const char *, int, int);
+static void		daemonize(void);
+static void		close_client(struct client *, int, int *);
+static ssize_t		read_request(int, struct client *);
+static void		release_client(struct client *, int, int *);
+static int		enqueue_job(int, struct client *, int, int *, const char *);
+static struct job	*dequeue_job(void);
+static void		*worker_main(void *);
+
+static void
+usage(void)
+{
+	fprintf(stderr,
+	    "usage: %s [-DHMchl] [-a addr] [-b bytes] [--css file]\n"
+	    "       [-g group] [-i index] [-m max] [-p port] [-r root]\n"
+	    "       [-t seconds] [-u user] [-T threads]\n",
+	    g_progname);
+	exit(1);
+}
+
+static int
+set_nonblock(int fd)
+{
+	int flags;
+
+	flags = fcntl(fd, F_GETFL, 0);
+	if (flags < 0)
+		return -1;
+	return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+}
+
+static int
+parse_int(const char *s, int min, int max)
+{
+	char *end;
+	long v;
+
+	errno = 0;
+	v = strtol(s, &end, 10);
+	if (errno || s == end || *end != '\0')
+		errx(1, "invalid number: %s", s);
+	if (v < min || v > max)
+		errx(1, "number out of range: %s", s);
+	return (int)v;
+}
+
+static size_t
+xstrlcpy(char *dst, const char *src, size_t dsize)
+{
+	size_t i;
+	size_t slen;
+
+	slen = 0;
+	while (src[slen] != '\0')
+		slen++;
+	if (dsize != 0) {
+		i = 0;
+		while (i + 1 < dsize && src[i] != '\0') {
+			dst[i] = src[i];
+			i++;
+		}
+		dst[i] = '\0';
+	}
+	return slen;
+}
+
+static size_t
+xstrlcat(char *dst, const char *src, size_t dsize)
+{
+	size_t dlen;
+	size_t slen;
+	size_t i;
+
+	dlen = 0;
+	while (dlen < dsize && dst[dlen] != '\0')
+		dlen++;
+	slen = 0;
+	while (src[slen] != '\0')
+		slen++;
+	if (dlen == dsize)
+		return dsize + slen;
+	i = 0;
+	while (dlen + i + 1 < dsize && src[i] != '\0') {
+		dst[dlen + i] = src[i];
+		i++;
+	}
+	dst[dlen + i] = '\0';
+	return dlen + slen;
+}
+
+static int
+path_within_root(const char *path)
+{
+	size_t rlen;
+
+	if (strcmp(g_root, "/") == 0)
+		return 1;
+	rlen = strlen(g_root);
+	if (strncmp(path, g_root, rlen) != 0)
+		return 0;
+	if (path[rlen] == '\0' || path[rlen] == '/')
+		return 1;
+	return 0;
+}
+
+static int
+resolve_path_under_root(const char *in, char *out)
+{
+	if (realpath(in, out) == NULL) {
+		if (errno == ENAMETOOLONG)
+			return 414;
+		if (errno == ENOENT || errno == ENOTDIR)
+			return 404;
+		if (errno == EACCES)
+			return 403;
+		return 500;
+	}
+	if (!path_within_root(out))
+		return 403;
+	return 0;
+}
+
+static void
+daemonize(void)
+{
+	int fd;
+
+	switch (fork()) {
+	case -1:
+		err(1, "fork");
+	case 0:
+		break;
+	default:
+		_exit(0);
+	}
+	if (setsid() < 0)
+		err(1, "setsid");
+	(void)chdir("/");
+	fd = open("/dev/null", O_RDWR);
+	if (fd >= 0) {
+		(void)dup2(fd, STDIN_FILENO);
+		(void)dup2(fd, STDOUT_FILENO);
+		(void)dup2(fd, STDERR_FILENO);
+		if (fd != STDIN_FILENO && fd != STDOUT_FILENO &&
+		    fd != STDERR_FILENO)
+			close(fd);
+	}
+}
+
+static uid_t
+parse_uid(const char *s)
+{
+	char *end;
+	unsigned long v;
+	struct passwd *pw;
+
+	errno = 0;
+	v = strtoul(s, &end, 10);
+	if (s[0] != '\0' && end != NULL && *end == '\0') {
+		if (errno == ERANGE || (uid_t)v != v)
+			errx(1, "uid out of range: %s", s);
+		return (uid_t)v;
+	}
+
+	pw = getpwnam(s);
+	if (pw == NULL)
+		errx(1, "unknown user: %s", s);
+	return pw->pw_uid;
+}
+
+static gid_t
+parse_gid(const char *s)
+{
+	char *end;
+	unsigned long v;
+	struct group *gr;
+
+	errno = 0;
+	v = strtoul(s, &end, 10);
+	if (s[0] != '\0' && end != NULL && *end == '\0') {
+		if (errno == ERANGE || (gid_t)v != v)
+			errx(1, "gid out of range: %s", s);
+		return (gid_t)v;
+	}
+
+	gr = getgrnam(s);
+	if (gr == NULL)
+		errx(1, "unknown group: %s", s);
+	return gr->gr_gid;
+}
+
+static int
+bad_path(const char *path)
+{
+	const char *seg;
+	const char *p;
+	size_t len;
+
+	if (*path != '/')
+		return 1;
+	seg = path + 1;
+	for (p = path; *p != '\0'; p++) {
+		if (*p == '\\')
+			return 1;
+		if ((unsigned char)*p < 0x20 || *p == 0x7f)
+			return 1;
+		if (*p == '/') {
+			len = (size_t)(p - seg);
+			if ((len == 1 && seg[0] == '.') ||
+			    (len == 2 && seg[0] == '.' && seg[1] == '.'))
+				return 1;
+			seg = p + 1;
+		}
+	}
+	len = (size_t)(p - seg);
+	if ((len == 1 && seg[0] == '.') ||
+	    (len == 2 && seg[0] == '.' && seg[1] == '.'))
+		return 1;
+	return 0;
+}
+
+static const char *
+status_msg(int code)
+{
+	switch (code) {
+	case 206:
+		return KHTTPD_STATUS_PARTIAL_CONTENT;
+	case 304:
+		return KHTTPD_STATUS_NOT_MODIFIED;
+	case 414:
+		return KHTTPD_STATUS_URI_TOO_LONG;
+	case 200:
+		return KHTTPD_STATUS_OK;
+	case 400:
+		return KHTTPD_STATUS_BAD_REQUEST;
+	case 403:
+		return KHTTPD_STATUS_FORBIDDEN;
+	case 404:
+		return KHTTPD_STATUS_NOT_FOUND;
+	case 405:
+		return KHTTPD_STATUS_METHOD_NOT_ALLOWED;
+	case 416:
+		return KHTTPD_STATUS_RANGE_NOT_SATISFIABLE;
+	case 505:
+		return KHTTPD_STATUS_HTTP_UNSUPPORTED;
+	case 500:
+		return KHTTPD_STATUS_INTERNAL_ERROR;
+	default:
+		return KHTTPD_STATUS_ERROR;
+	}
+}
+
+static int
+request_complete(const char *req)
+{
+	return strstr(req, "\r\n\r\n") != NULL || strstr(req, "\n\n") != NULL;
+}
+
+static void
+write_hdr(int fd, const char *buf, size_t bufsz, int n)
+{
+	size_t out;
+
+	if (n <= 0)
+		return;
+	out = (size_t)n;
+	if (out >= bufsz)
+		out = bufsz - 1;
+	write_all(fd, buf, out);
+}
+
+static int
+encoding_allowed(const char *hdr, const char *enc)
+{
+	const char *p;
+	size_t enclen;
+
+	if (hdr == NULL || *hdr == '\0')
+		return 0;
+	p = hdr;
+	enclen = strlen(enc);
+	while (*p != '\0') {
+		const char *tok;
+		const char *end;
+		size_t len;
+		int qzero;
+
+		while (*p == ' ' || *p == '\t' || *p == ',')
+			p++;
+		if (*p == '\0')
+			break;
+		tok = p;
+		while (*p != '\0' && *p != ',' && *p != ';')
+			p++;
+		end = p;
+		while (end > tok && (end[-1] == ' ' || end[-1] == '\t'))
+			end--;
+		len = (size_t)(end - tok);
+		qzero = 0;
+		while (*p != '\0' && *p != ',') {
+			if ((p[0] == 'q' || p[0] == 'Q') && p[1] == '=') {
+				const char *qv;
+
+				qv = p + 2;
+				while (*qv == ' ' || *qv == '\t')
+					qv++;
+				if (*qv == '0')
+					qzero = 1;
+			}
+			p++;
+		}
+		if (*p == ',')
+			p++;
+		if (qzero)
+			continue;
+		if (len == enclen && strncasecmp(tok, enc, enclen) == 0)
+			return 1;
+		if (len == 1 && tok[0] == '*')
+			return 1;
+	}
+	return 0;
+}
+
+static int
+parse_range_header(const char *hdr, off_t size, off_t *start, off_t *end)
+{
+	const char *p;
+	char *q;
+	long long a;
+	long long b;
+
+	if (hdr == NULL)
+		return 0;
+	while (*hdr == ' ' || *hdr == '\t')
+		hdr++;
+	if (strncasecmp(hdr, "bytes=", 6) != 0)
+		return 0;
+	p = hdr + 6;
+	if (size <= 0)
+		return -1;
+	if (strchr(p, ',') != NULL)
+		return 0;
+	if (*p == '-') {
+		long long suf;
+
+		p++;
+		errno = 0;
+		suf = strtoll(p, &q, 10);
+		if (errno || q == p || *q != '\0' || suf <= 0)
+			return 0;
+		if ((off_t)suf >= size) {
+			*start = 0;
+			*end = size - 1;
+		} else {
+			*start = size - (off_t)suf;
+			*end = size - 1;
+		}
+		return 1;
+	}
+	errno = 0;
+	a = strtoll(p, &q, 10);
+	if (errno || q == p || a < 0)
+		return 0;
+	if (*q != '-')
+		return 0;
+	p = q + 1;
+	if (*p == '\0') {
+		if ((off_t)a >= size)
+			return -1;
+		*start = (off_t)a;
+		*end = size - 1;
+		return 1;
+	}
+	errno = 0;
+	b = strtoll(p, &q, 10);
+	if (errno || q == p || *q != '\0' || b < a)
+		return 0;
+	if ((off_t)a >= size)
+		return -1;
+	*start = (off_t)a;
+	*end = (off_t)b;
+	if (*end >= size)
+		*end = size - 1;
+	return 1;
+}
+
+static int
+month_idx(const char *m)
+{
+	static const char *months[] = {
+		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+	};
+	int i;
+
+	for (i = 0; i < 12; i++) {
+		if (strcasecmp(m, months[i]) == 0)
+			return i + 1;
+	}
+	return 0;
+}
+
+static long long
+days_from_civil(int y, unsigned m, unsigned d)
+{
+	int era;
+	int mi;
+	unsigned yoe;
+	unsigned doy;
+	unsigned doe;
+
+	y -= m <= 2;
+	era = (y >= 0 ? y : y - 399) / 400;
+	yoe = (unsigned)(y - era * 400);
+	mi = (int)m;
+	doy = (153U * (unsigned)(mi + (mi > 2 ? -3 : 9)) + 2) / 5 + d - 1;
+	doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
+	return (long long)era * 146097 + (long long)doe - 719468;
+}
+
+static int
+parse_http_date(const char *s, time_t *out)
+{
+	char mon[4];
+	int day;
+	int year;
+	int hh;
+	int mm;
+	int ss;
+	long long days;
+	long long total;
+
+	if (sscanf(s, "%*3s, %d %3s %d %d:%d:%d GMT",
+	    &day, mon, &year, &hh, &mm, &ss) != 6)
+		return 0;
+	mon[3] = '\0';
+	if (day < 1 || day > 31 || year < 1970 ||
+	    hh < 0 || hh > 23 || mm < 0 || mm > 59 || ss < 0 || ss > 60)
+		return 0;
+	{
+		int m;
+
+		m = month_idx(mon);
+		if (m == 0)
+			return 0;
+		days = days_from_civil(year, (unsigned)m, (unsigned)day);
+	}
+	total = days * 86400LL + hh * 3600LL + mm * 60LL + ss;
+	if (total < 0)
+		return 0;
+	*out = (time_t)total;
+	return 1;
+}
+
+static void
+format_http_date(time_t t, char *out, size_t outsz)
+{
+	struct tm tmv;
+
+	gmtime_r(&t, &tmv);
+	if (strftime(out, outsz, "%a, %d %b %Y %H:%M:%S GMT", &tmv) == 0 &&
+	    outsz > 0)
+		out[0] = '\0';
+}
+
+static void
+make_etag(const struct stat *st, char *out, size_t outsz)
+{
+	snprintf(out, outsz, "\"%lx-%lx-%lx\"",
+	    (unsigned long)st->st_ino,
+	    (unsigned long)st->st_size,
+	    (unsigned long)st->st_mtime);
+}
+
+static int
+etag_matches(const char *hdr, const char *etag)
+{
+	const char *p;
+	size_t elen;
+
+	if (hdr == NULL || etag == NULL)
+		return 0;
+	p = hdr;
+	elen = strlen(etag);
+	while (*p != '\0') {
+		const char *tok;
+		const char *end;
+		size_t len;
+
+		while (*p == ' ' || *p == '\t' || *p == ',')
+			p++;
+		if (*p == '\0')
+			break;
+		tok = p;
+		while (*p != '\0' && *p != ',')
+			p++;
+		end = p;
+		while (end > tok && (end[-1] == ' ' || end[-1] == '\t'))
+			end--;
+		len = (size_t)(end - tok);
+		if (len == 1 && tok[0] == '*')
+			return 1;
+		if (len == elen && strncmp(tok, etag, elen) == 0)
+			return 1;
+		if (*p == ',')
+			p++;
+	}
+	return 0;
+}
+
+static const char *
+mime_type(const char *path)
+{
+	const char *ext;
+
+	ext = strrchr(path, '.');
+	if (ext == NULL || ext[1] == '\0')
+		return "application/octet-stream";
+	ext++;
+	if (strcasecmp(ext, "html") == 0 || strcasecmp(ext, "htm") == 0)
+		return "text/html";
+	if (strcasecmp(ext, "css") == 0)
+		return "text/css";
+	if (strcasecmp(ext, "js") == 0)
+		return "application/javascript";
+	if (strcasecmp(ext, "json") == 0)
+		return "application/json";
+	if (strcasecmp(ext, "txt") == 0)
+		return "text/plain";
+	if (strcasecmp(ext, "png") == 0)
+		return "image/png";
+	if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0)
+		return "image/jpeg";
+	if (strcasecmp(ext, "gif") == 0)
+		return "image/gif";
+	if (strcasecmp(ext, "svg") == 0)
+		return "image/svg+xml";
+	if (strcasecmp(ext, "ico") == 0)
+		return "image/x-icon";
+	return g_plain ? "text/plain" : "application/octet-stream";
+}
+
+static int
+write_all(int fd, const void *buf, size_t len)
+{
+	const char *p;
+	ssize_t n;
+
+	p = buf;
+	while (len > 0) {
+		n = write(fd, p, len);
+		if (n < 0) {
+			if (errno == EINTR)
+				continue;
+			if (errno == EAGAIN || errno == EWOULDBLOCK)
+				return -1;
+			return -1;
+		}
+		if (n == 0)
+			return -1;
+		p += (size_t)n;
+		len -= (size_t)n;
+	}
+	return 0;
+}
+
+static void
+close_client(struct client *clients, int idx, int *cur_conn)
+{
+	pthread_mutex_lock(&g_conn_mtx);
+	close(clients[idx].fd);
+	clients[idx].used = 0;
+	(*cur_conn)--;
+	pthread_mutex_unlock(&g_conn_mtx);
+}
+
+static void
+release_client(struct client *clients, int idx, int *cur_conn)
+{
+	(void)cur_conn;
+	pthread_mutex_lock(&g_conn_mtx);
+	clients[idx].used = 2;
+	pthread_mutex_unlock(&g_conn_mtx);
+}
+
+static ssize_t
+read_request(int fd, struct client *c)
+{
+	size_t maxread;
+	ssize_t n;
+
+	maxread = sizeof(c->buf) - c->len - 1;
+	if (g_max_hdr < sizeof(c->buf) - 1) {
+		if (c->len >= g_max_hdr)
+			return -2;
+		if (maxread > g_max_hdr - c->len)
+			maxread = g_max_hdr - c->len;
+	}
+	if (maxread == 0)
+		return -2;
+
+	n = read(fd, c->buf + c->len, maxread);
+	if (n < 0) {
+		if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
+			return -3;
+	}
+	if (n > 0) {
+		c->len += (size_t)n;
+		c->buf[c->len] = '\0';
+		c->last = time(NULL);
+	}
+	return n;
+}
+
+static int
+enqueue_job(int fd, struct client *clients, int idx, int *cur_conn,
+    const char *req)
+{
+	struct job *j;
+
+	pthread_mutex_lock(&g_job_mtx);
+	if ((int)g_job_len >= g_max_jobs) {
+		pthread_mutex_unlock(&g_job_mtx);
+		return -1;
+	}
+	pthread_mutex_unlock(&g_job_mtx);
+
+	j = calloc(1, sizeof(*j));
+	if (j == NULL)
+		return -1;
+	j->fd = fd;
+	j->idx = idx;
+	j->clients = clients;
+	j->cur_conn = cur_conn;
+	(void)xstrlcpy(j->req, req, sizeof(j->req));
+	j->next = NULL;
+
+	pthread_mutex_lock(&g_job_mtx);
+	if (g_job_tail == NULL) {
+		g_job_head = j;
+		g_job_tail = j;
+	} else {
+		g_job_tail->next = j;
+		g_job_tail = j;
+	}
+	g_job_len++;
+	pthread_cond_signal(&g_job_cv);
+	pthread_mutex_unlock(&g_job_mtx);
+	return 0;
+}
+
+static struct job *
+dequeue_job(void)
+{
+	struct job *j;
+
+	pthread_mutex_lock(&g_job_mtx);
+	while (g_job_head == NULL)
+		pthread_cond_wait(&g_job_cv, &g_job_mtx);
+	j = g_job_head;
+	g_job_head = j->next;
+	if (g_job_head == NULL)
+		g_job_tail = NULL;
+	if (g_job_len > 0)
+		g_job_len--;
+	pthread_mutex_unlock(&g_job_mtx);
+	return j;
+}
+
+static void *
+worker_main(void *arg)
+{
+	struct job *j;
+
+	(void)arg;
+	for (;;) {
+		j = dequeue_job();
+		handle_request(j->fd, j->req);
+		close(j->fd);
+		pthread_mutex_lock(&g_conn_mtx);
+		j->clients[j->idx].used = 0;
+		(*j->cur_conn)--;
+		pthread_mutex_unlock(&g_conn_mtx);
+		free(j);
+	}
+	return NULL;
+}
+
+static void
+write_html_escaped(int fd, const char *s)
+{
+	const char *rep;
+	char ch;
+
+	while (*s != '\0') {
+		ch = *s++;
+		switch (ch) {
+		case '&':
+			rep = "&amp;";
+			break;
+		case '<':
+			rep = "&lt;";
+			break;
+		case '>':
+			rep = "&gt;";
+			break;
+		case '"':
+			rep = "&quot;";
+			break;
+		case '\'':
+			rep = "&#39;";
+			break;
+		default:
+			if (write_all(fd, &ch, 1) < 0)
+				return;
+			continue;
+		}
+		if (write_all(fd, rep, strlen(rep)) < 0)
+			return;
+	}
+}
+
+static void
+write_url_escaped(int fd, const char *s)
+{
+	static const char hex[] = "0123456789ABCDEF";
+	unsigned char c;
+	char buf[3];
+
+	while (*s != '\0') {
+		c = (unsigned char)*s++;
+		if (isalnum(c) || c == '-' || c == '_' || c == '.' ||
+		    c == '~' || c == '/') {
+			if (write_all(fd, &c, 1) < 0)
+				return;
+		} else {
+			buf[0] = '%';
+			buf[1] = hex[c >> 4];
+			buf[2] = hex[c & 0x0f];
+			if (write_all(fd, buf, sizeof(buf)) < 0)
+				return;
+		}
+	}
+}
+
+static void
+parent_url(const char *req_path, char *out, size_t outsz)
+{
+	size_t len;
+
+	len = strlen(req_path);
+	if (len > 1 && req_path[len - 1] == '/')
+		len--;
+	while (len > 1 && req_path[len - 1] != '/')
+		len--;
+	if (len == 0)
+		len = 1;
+	if (len >= outsz)
+		len = outsz - 1;
+	memcpy(out, req_path, len);
+	out[len] = '\0';
+	if (len + 1 < outsz && out[len - 1] != '/') {
+		out[len] = '/';
+		out[len + 1] = '\0';
+	}
+}
+
+static void
+send_simple(int fd, int code, const char *extra)
+{
+	char hdr[RESP_BUF];
+	const char *msg;
+	int n;
+	size_t len;
+
+	if (g_css != NULL) {
+		static const char head1[] =
+		    "<!doctype html>\n<html><head><meta charset=\"utf-8\">"
+		    "<title>";
+		static const char head2[] = "</title>";
+		static const char css1[] = "<link rel=\"stylesheet\" href=\"";
+		static const char css2[] = "\">";
+		static const char body1[] = "</head><body><div class=\"box\"><h1>";
+		static const char body2[] = "</h1>";
+		static const char pre1[] = "<pre>";
+		static const char pre2[] = "</pre>";
+		static const char tail[] = "</div></body></html>\n";
+
+		msg = status_msg(code);
+		n = snprintf(hdr, sizeof(hdr),
+		    KHTTPD_HTTP_VERSION " %d %s\r\n"
+		    "Content-Type: text/html\r\n"
+		    "%s"
+		    "Connection: close\r\n\r\n",
+		    code, msg, g_server_hdr ? KHTTPD_SERVER_HEADER : "");
+		if (n > 0)
+			write_hdr(fd, hdr, sizeof(hdr), n);
+		if (write_all(fd, head1, sizeof(head1) - 1) < 0)
+			return;
+		if (write_all(fd, msg, strlen(msg)) < 0)
+			return;
+		if (write_all(fd, head2, sizeof(head2) - 1) < 0)
+			return;
+		if (write_all(fd, css1, sizeof(css1) - 1) < 0)
+			return;
+		write_html_escaped(fd, g_css);
+		if (write_all(fd, css2, sizeof(css2) - 1) < 0)
+			return;
+		if (write_all(fd, body1, sizeof(body1) - 1) < 0)
+			return;
+		if (write_all(fd, msg, strlen(msg)) < 0)
+			return;
+		if (write_all(fd, body2, sizeof(body2) - 1) < 0)
+			return;
+		if (extra != NULL && *extra != '\0') {
+			if (write_all(fd, pre1, sizeof(pre1) - 1) < 0)
+				return;
+			write_html_escaped(fd, extra);
+			if (write_all(fd, pre2, sizeof(pre2) - 1) < 0)
+				return;
+		}
+		(void)write_all(fd, tail, sizeof(tail) - 1);
+		return;
+	}
+
+	len = extra != NULL ? strlen(extra) : 0;
+	n = snprintf(hdr, sizeof(hdr),
+	    KHTTPD_HTTP_VERSION " %d %s\r\n"
+	    "Content-Length: %zu\r\n"
+	    "Content-Type: text/plain\r\n"
+	    "%s"
+	    "Connection: close\r\n\r\n"
+	    "%s",
+	    code, status_msg(code), len,
+	    g_server_hdr ? KHTTPD_SERVER_HEADER : "",
+	    extra != NULL ? extra : "");
+	if (n > 0)
+		write_hdr(fd, hdr, sizeof(hdr), n);
+}
+
+static void
+send_redirect(int fd, const char *location)
+{
+	char hdr[RESP_BUF];
+	int n;
+
+	n = snprintf(hdr, sizeof(hdr),
+	    KHTTPD_HTTP_VERSION " 301 "
+	    KHTTPD_STATUS_MOVED_PERMANENTLY "\r\n"
+	    "Location: %s\r\n"
+	    "%s"
+	    "Content-Length: 0\r\n"
+	    "Connection: close\r\n\r\n",
+	    location, g_server_hdr ? KHTTPD_SERVER_HEADER : "");
+	if (n > 0)
+		write_hdr(fd, hdr, sizeof(hdr), n);
+}
+
+static int
+serve_file(int fd, const char *path, const char *mime_path, int head_only,
+    const char *accept_encoding, const char *if_none_match,
+    int has_if_modified_since, time_t if_modified_since, const char *range_hdr)
+{
+	char enc_path[PATH_MAX];
+	char hdr[RESP_BUF];
+	char resolved_path[PATH_MAX];
+	struct stat lst;
+	struct stat st;
+	const char *content_encoding;
+	const char *served_path;
+	off_t range_start;
+	off_t range_end;
+	off_t body_len;
+	int have_range;
+	int ffd;
+	int n;
+	int status;
+
+	served_path = path;
+	content_encoding = NULL;
+	range_start = 0;
+	range_end = 0;
+	have_range = 0;
+	status = 200;
+
+	if (accept_encoding != NULL && *accept_encoding != '\0') {
+		struct stat cst;
+		int want_br;
+		int want_gz;
+
+		want_br = encoding_allowed(accept_encoding, "br");
+		want_gz = encoding_allowed(accept_encoding, "gzip");
+		if (want_br &&
+		    xstrlcpy(enc_path, path, sizeof(enc_path)) <
+		    sizeof(enc_path) &&
+		    xstrlcat(enc_path, ".br", sizeof(enc_path)) <
+		    sizeof(enc_path) &&
+		    stat(enc_path, &cst) == 0 && S_ISREG(cst.st_mode)) {
+			served_path = enc_path;
+			content_encoding = "br";
+		} else if (want_gz &&
+		    xstrlcpy(enc_path, path, sizeof(enc_path)) <
+		    sizeof(enc_path) &&
+		    xstrlcat(enc_path, ".gz", sizeof(enc_path)) <
+		    sizeof(enc_path) &&
+		    stat(enc_path, &cst) == 0 && S_ISREG(cst.st_mode)) {
+			served_path = enc_path;
+			content_encoding = "gzip";
+		}
+	}
+
+	{
+		int rrc;
+
+		rrc = resolve_path_under_root(served_path, resolved_path);
+		if (rrc != 0)
+			return rrc;
+	}
+	if (lstat(resolved_path, &lst) < 0) {
+		if (errno == ENAMETOOLONG)
+			return 414;
+		if (errno == EACCES)
+			return 403;
+		if (errno == ENOENT || errno == ENOTDIR)
+			return 404;
+		return 500;
+	}
+	if (!S_ISREG(lst.st_mode))
+		return 403;
+
+	ffd = open(resolved_path, O_RDONLY);
+	if (ffd < 0) {
+		if (errno == ENAMETOOLONG)
+			return 414;
+		if (errno == EACCES)
+			return 403;
+		return 404;
+	}
+	if (fstat(ffd, &st) < 0) {
+		close(ffd);
+		return 500;
+	}
+	if (st.st_dev != lst.st_dev || st.st_ino != lst.st_ino) {
+		close(ffd);
+		return 500;
+	}
+	if (!S_ISREG(st.st_mode)) {
+		close(ffd);
+		return 403;
+	}
+
+	{
+		char etag[96];
+		char lm[64];
+		int not_modified;
+
+		not_modified = 0;
+		make_etag(&st, etag, sizeof(etag));
+		format_http_date(st.st_mtime, lm, sizeof(lm));
+		if (if_none_match != NULL && *if_none_match != '\0') {
+			if (etag_matches(if_none_match, etag))
+				not_modified = 1;
+		} else if (has_if_modified_since &&
+		    st.st_mtime <= if_modified_since) {
+			not_modified = 1;
+		}
+		if (not_modified) {
+			n = snprintf(hdr, sizeof(hdr),
+			    KHTTPD_HTTP_VERSION " 304 "
+			    KHTTPD_STATUS_NOT_MODIFIED "\r\n"
+			    "ETag: %s\r\n"
+			    "Last-Modified: %s\r\n"
+			    "%s%s%s"
+			    "%s"
+			    "Connection: close\r\n\r\n",
+			    etag, lm,
+			    content_encoding != NULL ? "Content-Encoding: " : "",
+			    content_encoding != NULL ? content_encoding : "",
+			    content_encoding != NULL ? "\r\nVary: Accept-Encoding\r\n" : "",
+			    g_server_hdr ? KHTTPD_SERVER_HEADER : "");
+			if (n > 0)
+				write_hdr(fd, hdr, sizeof(hdr), n);
+			close(ffd);
+			return 304;
+		}
+		if (range_hdr != NULL && *range_hdr != '\0') {
+			int pr;
+
+			pr = parse_range_header(range_hdr, st.st_size,
+			    &range_start, &range_end);
+			if (pr < 0) {
+				n = snprintf(hdr, sizeof(hdr),
+				    KHTTPD_HTTP_VERSION " 416 "
+				    KHTTPD_STATUS_RANGE_NOT_SATISFIABLE "\r\n"
+				    "Content-Range: bytes */%ld\r\n"
+				    "ETag: %s\r\n"
+				    "Last-Modified: %s\r\n"
+				    "%s"
+				    "Connection: close\r\n\r\n",
+				    (long)st.st_size, etag, lm,
+				    g_server_hdr ? KHTTPD_SERVER_HEADER : "");
+				if (n > 0)
+					write_hdr(fd, hdr, sizeof(hdr), n);
+				close(ffd);
+				return 416;
+			}
+			if (pr > 0) {
+				have_range = 1;
+				status = 206;
+			}
+		}
+
+		body_len = st.st_size;
+		if (have_range)
+			body_len = range_end - range_start + 1;
+
+		if (have_range) {
+			n = snprintf(hdr, sizeof(hdr),
+			    KHTTPD_HTTP_VERSION " 206 "
+			    KHTTPD_STATUS_PARTIAL_CONTENT "\r\n"
+			    "Content-Length: %ld\r\n"
+			    "Content-Range: bytes %ld-%ld/%ld\r\n"
+			    "Content-Type: %s\r\n"
+			    "ETag: %s\r\n"
+			    "Last-Modified: %s\r\n"
+			    "Accept-Ranges: bytes\r\n"
+			    "%s%s%s"
+			    "%s"
+			    "Connection: close\r\n\r\n",
+			    (long)body_len,
+			    (long)range_start, (long)range_end, (long)st.st_size,
+			    mime_type(mime_path), etag, lm,
+			    content_encoding != NULL ? "Content-Encoding: " : "",
+			    content_encoding != NULL ? content_encoding : "",
+			    content_encoding != NULL ? "\r\nVary: Accept-Encoding\r\n" : "",
+			    g_server_hdr ? KHTTPD_SERVER_HEADER : "");
+		} else {
+			n = snprintf(hdr, sizeof(hdr),
+			    KHTTPD_HTTP_VERSION " 200 " KHTTPD_STATUS_OK "\r\n"
+			    "Content-Length: %ld\r\n"
+			    "Content-Type: %s\r\n"
+			    "ETag: %s\r\n"
+			    "Last-Modified: %s\r\n"
+			    "Accept-Ranges: bytes\r\n"
+			    "%s%s%s"
+			    "%s"
+			    "Connection: close\r\n\r\n",
+			    (long)body_len, mime_type(mime_path), etag, lm,
+			    content_encoding != NULL ? "Content-Encoding: " : "",
+			    content_encoding != NULL ? content_encoding : "",
+			    content_encoding != NULL ? "\r\nVary: Accept-Encoding\r\n" : "",
+			    g_server_hdr ? KHTTPD_SERVER_HEADER : "");
+		}
+		if (n > 0)
+			write_hdr(fd, hdr, sizeof(hdr), n);
+	}
+
+	if (!head_only) {
+		char buf[4096];
+		ssize_t nr;
+		off_t left;
+
+		if (have_range && lseek(ffd, range_start, SEEK_SET) < 0) {
+			close(ffd);
+			return 500;
+		}
+		left = body_len;
+
+		while (left > 0 &&
+		    (nr = read(ffd, buf,
+		    left < (off_t)sizeof(buf) ? (size_t)left : sizeof(buf))) > 0) {
+			if (write_all(fd, buf, (size_t)nr) < 0)
+				break;
+			left -= nr;
+		}
+	}
+	close(ffd);
+	return status;
+}
+
+static int
+serve_dir(int fd, const char *fs_path, const char *req_path, int head_only)
+{
+	char hdr[RESP_BUF];
+	char pathbuf[PATH_MAX];
+	char parent[PATH_MAX];
+	char urlbuf[PATH_MAX];
+	struct dirent *de;
+	struct stat dst;
+	struct stat lst;
+	struct stat st;
+	DIR *d;
+	int n;
+	static const char head1[] =
+	    "<!doctype html>\n<html><head><meta charset=\"utf-8\">"
+	    "<title>Index of ";
+	static const char head2[] =
+	    "</title>";
+	static const char css1[] = "<link rel=\"stylesheet\" href=\"";
+	static const char css2[] = "\">";
+	static const char head3[] = "</head><body><div class=\"box\"><h1>Index of ";
+	static const char head4[] = "</h1><ul>\n";
+	static const char tail[] = "</ul></div></body></html>\n";
+	static const char link1[] = "<li><a href=\"";
+	static const char link2[] = "\">";
+	static const char link3[] = "</a></li>\n";
+	static const char parentlink[] = "\">..</a></li>\n";
+
+	d = opendir(fs_path);
+	if (d == NULL) {
+		if (errno == EACCES)
+			return 403;
+		return 404;
+	}
+	if (lstat(fs_path, &lst) < 0 || fstat(dirfd(d), &dst) < 0) {
+		closedir(d);
+		return 500;
+	}
+	if (!S_ISDIR(lst.st_mode) || !S_ISDIR(dst.st_mode) ||
+	    lst.st_dev != dst.st_dev || lst.st_ino != dst.st_ino) {
+		closedir(d);
+		return 403;
+	}
+
+	n = snprintf(hdr, sizeof(hdr),
+	    KHTTPD_HTTP_VERSION " 200 " KHTTPD_STATUS_OK "\r\n"
+	    "Content-Type: text/html\r\n"
+	    "%s"
+	    "Connection: close\r\n\r\n",
+	    g_server_hdr ? KHTTPD_SERVER_HEADER : "");
+	if (n > 0)
+		write_hdr(fd, hdr, sizeof(hdr), n);
+	if (head_only) {
+		closedir(d);
+		return 200;
+	}
+
+	if (write_all(fd, head1, sizeof(head1) - 1) < 0) {
+		closedir(d);
+		return 200;
+	}
+	write_html_escaped(fd, req_path);
+	if (write_all(fd, head2, sizeof(head2) - 1) < 0) {
+		closedir(d);
+		return 200;
+	}
+	if (g_css != NULL) {
+		if (write_all(fd, css1, sizeof(css1) - 1) < 0) {
+			closedir(d);
+			return 200;
+		}
+		write_html_escaped(fd, g_css);
+		if (write_all(fd, css2, sizeof(css2) - 1) < 0) {
+			closedir(d);
+			return 200;
+		}
+	}
+	if (write_all(fd, head3, sizeof(head3) - 1) < 0) {
+		closedir(d);
+		return 200;
+	}
+	write_html_escaped(fd, req_path);
+	if (write_all(fd, head4, sizeof(head4) - 1) < 0) {
+		closedir(d);
+		return 200;
+	}
+
+	if (strcmp(req_path, "/") != 0) {
+		parent_url(req_path, parent, sizeof(parent));
+		if (write_all(fd, link1, sizeof(link1) - 1) < 0) {
+			closedir(d);
+			return 200;
+		}
+		write_url_escaped(fd, parent);
+		if (write_all(fd, parentlink, sizeof(parentlink) - 1) < 0) {
+			closedir(d);
+			return 200;
+		}
+	}
+
+	while ((de = readdir(d)) != NULL) {
+		if (strcmp(de->d_name, ".") == 0)
+			continue;
+		if (strcmp(de->d_name, "..") == 0)
+			continue;
+		if (xstrlcpy(pathbuf, fs_path, sizeof(pathbuf)) >=
+		    sizeof(pathbuf))
+			continue;
+		if (xstrlcat(pathbuf, "/", sizeof(pathbuf)) >=
+		    sizeof(pathbuf))
+			continue;
+		if (xstrlcat(pathbuf, de->d_name, sizeof(pathbuf)) >=
+		    sizeof(pathbuf))
+			continue;
+		if (xstrlcpy(urlbuf, req_path, sizeof(urlbuf)) >=
+		    sizeof(urlbuf))
+			continue;
+		if (xstrlcat(urlbuf, de->d_name, sizeof(urlbuf)) >=
+		    sizeof(urlbuf))
+			continue;
+		if (stat(pathbuf, &st) < 0)
+			continue;
+		if (write_all(fd, link1, sizeof(link1) - 1) < 0)
+			break;
+		write_url_escaped(fd, urlbuf);
+		if (S_ISDIR(st.st_mode))
+			if (write_all(fd, "/", 1) < 0)
+				break;
+		if (write_all(fd, link2, sizeof(link2) - 1) < 0)
+			break;
+		write_html_escaped(fd, de->d_name);
+		if (S_ISDIR(st.st_mode))
+			if (write_all(fd, "/", 1) < 0)
+				break;
+		if (write_all(fd, link3, sizeof(link3) - 1) < 0)
+			break;
+	}
+
+	(void)write_all(fd, tail, sizeof(tail) - 1);
+	closedir(d);
+	return 200;
+}
+
+static void
+handle_request(int cfd, const char *req)
+{
+	char accept_encoding[256];
+	char file[PATH_MAX];
+	char if_none_match[256];
+	char line[REQ_BUF];
+	char *query;
+	char range_hdr[128];
+	char method[8];
+	char path[PATH_MAX];
+	const char *hdrp;
+	const char *line_end;
+	const char *sp1;
+	const char *sp2;
+	int has_if_modified_since;
+	int head_only;
+	int host_count;
+	int is_http11;
+	time_t if_modified_since;
+	int rc;
+
+	accept_encoding[0] = '\0';
+	if_none_match[0] = '\0';
+	range_hdr[0] = '\0';
+	has_if_modified_since = 0;
+	if_modified_since = 0;
+	host_count = 0;
+	is_http11 = 0;
+
+	line_end = strstr(req, "\r\n");
+	if (line_end == NULL)
+		line_end = strchr(req, '\n');
+	if (line_end == NULL) {
+		send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST);
+		return;
+	}
+	if ((size_t)(line_end - req) >= sizeof(line)) {
+		send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG);
+		return;
+	}
+	memcpy(line, req, (size_t)(line_end - req));
+	line[line_end - req] = '\0';
+
+	sp1 = strchr(line, ' ');
+	if (sp1 == NULL || sp1 == line || (size_t)(sp1 - line) >= sizeof(method)) {
+		send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST);
+		return;
+	}
+	memcpy(method, line, (size_t)(sp1 - line));
+	method[sp1 - line] = '\0';
+
+	while (*sp1 == ' ')
+		sp1++;
+	sp2 = strchr(sp1, ' ');
+	if (sp2 == NULL || sp2 == sp1) {
+		send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST);
+		return;
+	}
+	if ((size_t)(sp2 - sp1) >= sizeof(path)) {
+		send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG);
+		return;
+	}
+	memcpy(path, sp1, (size_t)(sp2 - sp1));
+	path[sp2 - sp1] = '\0';
+
+	while (*sp2 == ' ')
+		sp2++;
+	if (strcmp(sp2, "HTTP/1.1") == 0)
+		is_http11 = 1;
+	else if (strcmp(sp2, "HTTP/1.0") != 0) {
+		send_simple(cfd, 505, KHTTPD_MSG_HTTP_UNSUPPORTED);
+		return;
+	}
+
+	if (strcmp(method, "GET") == 0)
+		head_only = 0;
+	else if (strcmp(method, "HEAD") == 0)
+		head_only = 1;
+	else {
+		send_simple(cfd, 405, KHTTPD_MSG_METHOD_NOT_ALLOWED);
+		return;
+	}
+
+	query = strchr(path, '?');
+	if (query != NULL) {
+		*query = '\0';
+	}
+
+	if (bad_path(path)) {
+		send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST);
+		return;
+	}
+
+	if (xstrlcpy(file, g_root, sizeof(file)) >= sizeof(file) ||
+	    xstrlcat(file, path, sizeof(file)) >= sizeof(file)) {
+		send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG);
+		return;
+	}
+
+	hdrp = line_end;
+	if (hdrp[0] == '\r' && hdrp[1] == '\n')
+		hdrp += 2;
+	else if (hdrp[0] == '\n')
+		hdrp++;
+	while (hdrp != NULL && *hdrp != '\0') {
+		char *colon;
+		const char *eol;
+		const char *val;
+		size_t llen;
+
+		eol = strchr(hdrp, '\n');
+		if (eol == NULL)
+			break;
+		llen = (size_t)(eol - hdrp);
+		if (llen > 0 && hdrp[llen - 1] == '\r')
+			llen--;
+		if (llen == 0)
+			break;
+		if (llen >= sizeof(line))
+			llen = sizeof(line) - 1;
+		memcpy(line, hdrp, llen);
+		line[llen] = '\0';
+		colon = strchr(line, ':');
+		if (colon == NULL) {
+			send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST);
+			return;
+		}
+		colon[0] = '\0';
+		val = colon + 1;
+		while (*val == ' ' || *val == '\t')
+			val++;
+		if (strcasecmp(line, "Accept-Encoding") == 0) {
+			xstrlcpy(accept_encoding, val,
+			    sizeof(accept_encoding));
+		} else if (strcasecmp(line, "If-None-Match") == 0) {
+			xstrlcpy(if_none_match, val,
+			    sizeof(if_none_match));
+		} else if (strcasecmp(line, "If-Modified-Since") == 0) {
+			has_if_modified_since =
+			    parse_http_date(val, &if_modified_since);
+		} else if (strcasecmp(line, "Range") == 0) {
+			xstrlcpy(range_hdr, val, sizeof(range_hdr));
+		} else if (strcasecmp(line, "Host") == 0) {
+			if (*val == '\0') {
+				send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST);
+				return;
+			}
+			host_count++;
+		}
+		hdrp = eol + 1;
+	}
+	if (is_http11 && host_count != 1) {
+		send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST);
+		return;
+	}
+
+	{
+		char index[PATH_MAX];
+		char resolved_file[PATH_MAX];
+		char reqdir[PATH_MAX];
+		const char *slash;
+		struct stat ist;
+		struct stat st;
+		size_t len;
+		int rrc;
+
+		rrc = resolve_path_under_root(file, resolved_file);
+		if (rrc == 0 && stat(resolved_file, &st) == 0 &&
+		    S_ISDIR(st.st_mode)) {
+			len = strlen(path);
+			slash = (len > 0 && path[len - 1] == '/') ? "" : "/";
+			if (xstrlcpy(reqdir, path, sizeof(reqdir)) >=
+			    sizeof(reqdir) ||
+			    xstrlcat(reqdir, slash, sizeof(reqdir)) >=
+			    sizeof(reqdir)) {
+				send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST);
+				return;
+			}
+
+			if (path[len - 1] != '/') {
+				send_redirect(cfd, reqdir);
+				return;
+			}
+
+			len = strlen(resolved_file);
+			slash = (len > 0 && resolved_file[len - 1] == '/') ? "" : "/";
+			if (xstrlcpy(index, resolved_file, sizeof(index)) >=
+			    sizeof(index) ||
+			    xstrlcat(index, slash, sizeof(index)) >=
+			    sizeof(index) ||
+			    xstrlcat(index, g_index, sizeof(index)) >=
+			    sizeof(index)) {
+				send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST);
+				return;
+			}
+
+			if (stat(index, &ist) == 0 && S_ISREG(ist.st_mode)) {
+				rc = serve_file(cfd, index, index, head_only,
+				    accept_encoding, if_none_match,
+				    has_if_modified_since, if_modified_since,
+				    range_hdr);
+			} else if (g_dirlist)
+				rc = serve_dir(cfd, resolved_file, reqdir,
+				    head_only);
+			else
+				rc = 403;
+
+			if (rc != 200 && rc != 206 && rc != 304 &&
+			    rc != 416) {
+				if (rc == 404)
+					send_simple(cfd, 404, KHTTPD_MSG_NOT_FOUND);
+				else if (rc == 403)
+					send_simple(cfd, 403, KHTTPD_MSG_FORBIDDEN);
+				else if (rc == 414)
+					send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG);
+				else
+					send_simple(cfd, 500, KHTTPD_MSG_SERVER_ERROR);
+			}
+			return;
+		}
+	}
+
+	{
+		rc = serve_file(cfd, file, file, head_only, accept_encoding,
+		    if_none_match, has_if_modified_since, if_modified_since,
+		    range_hdr);
+	}
+	if (rc != 200 && rc != 206 && rc != 304 && rc != 416) {
+		if (rc == 404)
+			send_simple(cfd, 404, KHTTPD_MSG_NOT_FOUND);
+		else if (rc == 403)
+			send_simple(cfd, 403, KHTTPD_MSG_FORBIDDEN);
+		else if (rc == 414)
+			send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG);
+		else
+			send_simple(cfd, 500, KHTTPD_MSG_SERVER_ERROR);
+	}
+}
+
+static int
+setup_listener(const char *addr, int port)
+{
+	struct sockaddr_in sa4;
+	int fd;
+	int yes;
+
+	fd = socket(AF_INET, SOCK_STREAM, 0);
+	if (fd < 0)
+		err(1, "socket");
+
+	yes = 1;
+	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
+
+	memset(&sa4, 0, sizeof(sa4));
+	sa4.sin_family = AF_INET;
+	sa4.sin_port = htons((unsigned short)port);
+	if (addr != NULL) {
+		if (inet_pton(AF_INET, addr, &sa4.sin_addr) != 1)
+			errx(1, "bad addr: %s", addr);
+	} else
+		sa4.sin_addr.s_addr = htonl(INADDR_ANY);
+
+	if (bind(fd, (struct sockaddr *)&sa4, sizeof(sa4)) < 0)
+		err(1, "bind");
+	if (listen(fd, g_max_conn) < 0)
+		err(1, "listen");
+
+	return fd;
+}
+
+int
+main(int argc, char **argv)
+{
+	struct client *clients;
+	struct pollfd *pfds;
+	pthread_t *workers;
+	const char *addr;
+	const char *group;
+	const char *root;
+	const char *user;
+	int cur_conn;
+	int do_chroot;
+	int have_gid;
+	int have_uid;
+	int i;
+	int idx;
+	int lfd;
+	int opt;
+	int port;
+	int j;
+	int ti;
+	gid_t drop_gid;
+	uid_t drop_uid;
+
+	if (argv[0] != NULL)
+		g_progname = argv[0];
+
+	addr = NULL;
+	root = ".";
+	do_chroot = 0;
+	have_uid = 0;
+	have_gid = 0;
+	user = NULL;
+	group = NULL;
+	port = DEFAULT_PORT;
+	cur_conn = 0;
+
+	for (i = 1; i < argc; i++) {
+		if (strcmp(argv[i], "--css") != 0)
+			continue;
+		if (i + 1 >= argc)
+			usage();
+		g_css = argv[i + 1];
+		for (j = i; j + 2 <= argc; j++)
+			argv[j] = argv[j + 2];
+		argc -= 2;
+		i--;
+	}
+
+	while ((opt = getopt(argc, argv, "DHMchla:b:g:i:m:p:r:t:u:T:")) != -1) {
+		switch (opt) {
+		case 'D':
+			daemonize();
+			break;
+		case 'H':
+			g_server_hdr = 1;
+			break;
+		case 'M':
+			g_plain = 1;
+			break;
+		case 'c':
+			do_chroot = 1;
+			break;
+		case 'h':
+			usage();
+			break;
+		case 'l':
+			g_dirlist = 1;
+			break;
+		case 'a':
+			addr = optarg;
+			break;
+		case 'b':
+			g_max_hdr = (size_t)parse_int(optarg, 1, REQ_BUF - 1);
+			break;
+		case 'g':
+			group = optarg;
+			break;
+		case 'i':
+			g_index = optarg;
+			break;
+		case 'm':
+			g_max_conn = parse_int(optarg, 1, 4096);
+			break;
+		case 'p':
+			port = parse_int(optarg, 1, 65535);
+			break;
+		case 'r':
+			root = optarg;
+			break;
+		case 't':
+			g_timeout = parse_int(optarg, 1, 3600);
+			break;
+		case 'u':
+			user = optarg;
+			break;
+		case 'T':
+			g_threads = parse_int(optarg, 1, 256);
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	if (argc != 0)
+		usage();
+	g_max_jobs = g_max_conn;
+
+	signal(SIGPIPE, SIG_IGN);
+
+	if (group != NULL) {
+		drop_gid = parse_gid(group);
+		have_gid = 1;
+	}
+	if (user != NULL) {
+		drop_uid = parse_uid(user);
+		have_uid = 1;
+	}
+	if (geteuid() == 0 && (!have_uid || drop_uid == 0))
+		errx(1, "refusing to run as root; use non-root -u");
+
+	if (do_chroot) {
+		if (chroot(root) < 0)
+			err(1, "chroot");
+		if (chdir("/") < 0)
+			err(1, "chdir");
+		root = ".";
+	}
+
+	if (realpath(root, g_root_buf) == NULL)
+		err(1, "realpath: %s", root);
+	g_root = g_root_buf;
+
+#ifdef __OpenBSD__
+	if (unveil(g_root, "r") < 0)
+		err(1, "unveil");
+	if (unveil(NULL, NULL) < 0)
+		err(1, "unveil");
+#endif
+
+	if (have_gid) {
+		if (have_uid) {
+			struct passwd *pw;
+
+			pw = getpwuid(drop_uid);
+			if (pw == NULL)
+				errx(1, "unknown uid: %lu", (unsigned long)drop_uid);
+			if (initgroups(pw->pw_name, drop_gid) < 0)
+				err(1, "initgroups");
+		} else {
+			if (setgroups(0, NULL) < 0)
+				err(1, "setgroups");
+		}
+		if (setgid(drop_gid) < 0)
+			err(1, "setgid");
+	}
+	if (have_uid) {
+		if (!have_gid) {
+			struct passwd *pw;
+
+			pw = getpwuid(drop_uid);
+			if (pw == NULL)
+				errx(1, "unknown uid: %lu", (unsigned long)drop_uid);
+			if (initgroups(pw->pw_name, pw->pw_gid) < 0)
+				err(1, "initgroups");
+			if (setgid(pw->pw_gid) < 0)
+				err(1, "setgid");
+		}
+		if (setuid(drop_uid) < 0)
+			err(1, "setuid");
+	}
+
+#ifdef __OpenBSD__
+	if (pledge("stdio rpath inet", NULL) < 0)
+		err(1, "pledge");
+#endif
+
+	lfd = setup_listener(addr, port);
+	if (set_nonblock(lfd) < 0)
+		err(1, "nonblock");
+
+	clients = calloc((size_t)g_max_conn, sizeof(struct client));
+	if (clients == NULL)
+		err(1, "calloc");
+	pfds = calloc((size_t)g_max_conn + 1, sizeof(struct pollfd));
+	if (pfds == NULL)
+		err(1, "calloc");
+	workers = calloc((size_t)g_threads, sizeof(*workers));
+	if (workers == NULL)
+		err(1, "calloc");
+	for (ti = 0; ti < g_threads; ti++) {
+		if (pthread_create(&workers[ti], NULL, worker_main, NULL) != 0)
+			err(1, "pthread_create");
+		(void)pthread_detach(workers[ti]);
+	}
+
+	for (;;) {
+		time_t now;
+		int nready;
+
+		pfds[0].fd = lfd;
+		pfds[0].events = POLLIN;
+		pfds[0].revents = 0;
+		idx = 1;
+		for (i = 0; i < g_max_conn; i++) {
+			int used;
+			int fd;
+
+			pthread_mutex_lock(&g_conn_mtx);
+			used = clients[i].used;
+			fd = clients[i].fd;
+			pthread_mutex_unlock(&g_conn_mtx);
+			if (used != 1)
+				continue;
+			pfds[idx].fd = fd;
+			pfds[idx].events = POLLIN;
+			pfds[idx].revents = 0;
+			idx++;
+		}
+		nready = poll(pfds, (nfds_t)idx, g_timeout > 0 ? 1000 : -1);
+		if (nready < 0) {
+			if (errno == EINTR)
+				continue;
+			err(1, "poll");
+		}
+
+		if (pfds[0].revents & POLLIN) {
+			int cfd;
+			int slot;
+
+			for (;;) {
+				cfd = accept(lfd, NULL, NULL);
+				if (cfd < 0) {
+					if (errno == EAGAIN || errno == EWOULDBLOCK ||
+					    errno == EINTR)
+						break;
+					break;
+				}
+				if (set_nonblock(cfd) < 0) {
+					close(cfd);
+					continue;
+				}
+				pthread_mutex_lock(&g_conn_mtx);
+				if (cur_conn >= g_max_conn)
+					slot = -1;
+				else {
+					slot = -2;
+					for (i = 0; i < g_max_conn; i++) {
+						if (clients[i].used == 0) {
+							slot = i;
+							break;
+						}
+					}
+					if (slot >= 0) {
+						clients[slot].fd = cfd;
+						clients[slot].used = 1;
+						clients[slot].len = 0;
+						clients[slot].last = time(NULL);
+						cur_conn++;
+					}
+				}
+				pthread_mutex_unlock(&g_conn_mtx);
+				if (slot == -1) {
+					send_simple(cfd, 500, KHTTPD_MSG_SERVER_BUSY);
+					close(cfd);
+				} else if (slot == -2)
+					close(cfd);
+			}
+		}
+
+		idx = 1;
+		for (i = 0; i < g_max_conn; i++) {
+			ssize_t n;
+			int used;
+
+			pthread_mutex_lock(&g_conn_mtx);
+			used = clients[i].used;
+			pthread_mutex_unlock(&g_conn_mtx);
+			if (used != 1)
+				continue;
+			if ((pfds[idx].revents & (POLLIN | POLLHUP | POLLERR)) == 0) {
+				idx++;
+				continue;
+			}
+			n = read_request(clients[i].fd, &clients[i]);
+			if (n == -3) {
+				idx++;
+				continue;
+			}
+			if (n == -2) {
+				send_simple(clients[i].fd, 400,
+				    KHTTPD_MSG_BAD_REQUEST);
+				close_client(clients, i, &cur_conn);
+				idx++;
+				continue;
+			}
+			if (n <= 0) {
+				close_client(clients, i, &cur_conn);
+				idx++;
+				continue;
+			}
+			if (request_complete(clients[i].buf)) {
+				if (enqueue_job(clients[i].fd, clients, i, &cur_conn,
+				    clients[i].buf) < 0) {
+					send_simple(clients[i].fd, 500,
+					    KHTTPD_MSG_SERVER_ERROR);
+					close_client(clients, i, &cur_conn);
+				} else
+					release_client(clients, i, &cur_conn);
+			} else if (clients[i].len >= sizeof(clients[i].buf) - 2) {
+				send_simple(clients[i].fd, 400,
+				    KHTTPD_MSG_BAD_REQUEST);
+				close_client(clients, i, &cur_conn);
+			}
+			idx++;
+		}
+
+		if (g_timeout > 0) {
+			now = time(NULL);
+			for (i = 0; i < g_max_conn; i++) {
+				int used;
+				time_t last;
+
+				pthread_mutex_lock(&g_conn_mtx);
+				used = clients[i].used;
+				last = clients[i].last;
+				pthread_mutex_unlock(&g_conn_mtx);
+				if (used != 1)
+					continue;
+				if ((int)(now - last) <= g_timeout)
+					continue;
+				close_client(clients, i, &cur_conn);
+			}
+		}
+	}
+
+	return 0;
+}