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 = "&";
+ break;
+ case '<':
+ rep = "<";
+ break;
+ case '>':
+ rep = ">";
+ break;
+ case '"':
+ rep = """;
+ break;
+ case '\'':
+ rep = "'";
+ 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;
+}