/*
 * xcontents.c - a small HTTP/HTTPS file server.
 *
 * Build:   gcc -O2 -Wall -o xcontents xcpontents.c -lssl -lcrypto
 * Run:     ./xcontents [-t] [-p http_port] [-s https_port] [-c cert.pem] [-k key.pem]
 *                      [-m mediatypes.txt]
 *
 * Media types are read from mediatypes.txt (see that file for the format).
 *
 * Defaults: HTTP on port 80, HTTPS on port 443 (ports below 1024 normally
 * need privileges; use e.g. -p 8080 -s 8443 otherwise).
 *
 * The directory the server is started in is the root directory. Only files
 * below it are read or written. Certificate and key files are loaded at
 * startup; if they live inside the root directory they are never served.
 * If the certificate cannot be loaded, the server runs HTTP only.
 *
 * Methods: GET, HEAD, PUT, POST, DELETE, OPTIONS.
 *
 * Two modes:
 *   normal   (default): the cookie is ignored and never set; everything is
 *            relative to the root directory; DELETE is not permitted (403).
 *   tutorial (-t): cookie "xcontents" selects a direct subdirectory of the
 *            root that all paths are relative to (GET/HEAD fall back to the
 *            root). It is created on the first PUT/POST when absent. DELETE
 *            is permitted, but only inside the cookie subdirectory.
 *
 * One process is forked per connection; every connection is closed after
 * one response (Connection: close).
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/random.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#define MAX_HEADER   16384
#define MAX_PATH_LEN 4096
#define MAX_BODY     (64L * 1024 * 1024)      /* max request body        */
#define MAX_FILE     (256L * 1024 * 1024)     /* max file size for POST  */
#define IO_TIMEOUT   30                       /* seconds                 */
#define COOKIE_NAME  "xcontents"

typedef struct {
    int fd;
    SSL *ssl;               /* NULL for plain HTTP */
} Conn;

typedef struct {
    Conn *conn;
    char method[16];
    char *target;
    long content_length;    /* -1 if absent */
    int has_te;             /* Transfer-Encoding present */
    int expect100;
    const char *cookie;
    char origin[256];
    char acrh[512];         /* Access-Control-Request-Headers */
    char raw[MAX_PATH_LEN]; /* path as sent (still percent-encoded) */
    char path[MAX_PATH_LEN + 16]; /* decoded path, "index.html" appended */
    char setcookie[320];
    int head_only;
} Req;

static char g_root[PATH_MAX];        /* realpath of the root directory */
static char g_prot[2][PATH_MAX];     /* files that must never be served */
static int  g_nprot;
static SSL_CTX *g_ctx;
static int g_tutorial;               /* 1 = tutorial mode (-t) */

/* Client address of the current connection (set in the child process). */
static struct sockaddr_storage g_peer;
static socklen_t g_peerlen;
static char g_peername[NI_MAXHOST];
static int  g_peer_resolved;

/* ------------------------------------------------------------------ */
/* Connection I/O                                                      */
/* ------------------------------------------------------------------ */

static ssize_t conn_read(Conn *c, void *buf, size_t n)
{
    if (c->ssl) {
        int r = SSL_read(c->ssl, buf, n > INT_MAX ? INT_MAX : (int)n);
        if (r > 0) return r;
        return SSL_get_error(c->ssl, r) == SSL_ERROR_ZERO_RETURN ? 0 : -1;
    }
    for (;;) {
        ssize_t r = recv(c->fd, buf, n, 0);
        if (r < 0 && errno == EINTR) continue;
        return r;
    }
}

static int conn_write_all(Conn *c, const void *buf, size_t n)
{
    const char *p = buf;
    while (n > 0) {
        ssize_t w;
        if (c->ssl) {
            int r = SSL_write(c->ssl, p, n > INT_MAX ? INT_MAX : (int)n);
            if (r <= 0) return -1;
            w = r;
        } else {
            w = send(c->fd, p, n, MSG_NOSIGNAL);
            if (w < 0) {
                if (errno == EINTR) continue;
                return -1;
            }
        }
        p += w;
        n -= (size_t)w;
    }
    return 0;
}

/* ------------------------------------------------------------------ */
/* Response helpers                                                    */
/* ------------------------------------------------------------------ */

static const char *reason(int s)
{
    switch (s) {
    case 100: return "Continue";
    case 200: return "OK";
    case 201: return "Created";
    case 204: return "No Content";
    case 301: return "Moved Permanently";
    case 400: return "Bad Request";
    case 403: return "Forbidden";
    case 404: return "Not Found";
    case 405: return "Method Not Allowed";
    case 409: return "Conflict";
    case 411: return "Length Required";
    case 413: return "Payload Too Large";
    case 414: return "URI Too Long";
    case 431: return "Request Header Fields Too Large";
    case 500: return "Internal Server Error";
    case 501: return "Not Implemented";
    case 505: return "HTTP Version Not Supported";
    default:  return "Unknown";
    }
}

static void hprintf(char *buf, size_t cap, size_t *n, const char *fmt, ...)
{
    if (*n >= cap) return;
    va_list ap;
    va_start(ap, fmt);
    int w = vsnprintf(buf + *n, cap - *n, fmt, ap);
    va_end(ap);
    if (w > 0) {
        *n += (size_t)w;
        if (*n >= cap) *n = cap - 1;
    }
}

/* DNS name of the client, or its IP address if there is no name.
   Resolved lazily (and only once) because reverse DNS can be slow. */
static const char *peer_name(void)
{
    if (!g_peer_resolved) {
        struct sockaddr_storage ss = g_peer;
        socklen_t len = g_peerlen;
        g_peer_resolved = 1;

        /* A dual-stack socket reports IPv4 clients as ::ffff:a.b.c.d */
        if (ss.ss_family == AF_INET6) {
            struct sockaddr_in6 *a6 = (struct sockaddr_in6 *)&ss;
            if (IN6_IS_ADDR_V4MAPPED(&a6->sin6_addr)) {
                struct sockaddr_in a4;
                memset(&a4, 0, sizeof a4);
                a4.sin_family = AF_INET;
                a4.sin_port = a6->sin6_port;
                memcpy(&a4.sin_addr, &a6->sin6_addr.s6_addr[12], 4);
                memcpy(&ss, &a4, sizeof a4);
                len = sizeof a4;
            }
        }
        if (getnameinfo((struct sockaddr *)&ss, len, g_peername,
                        sizeof g_peername, NULL, 0, NI_NAMEREQD) != 0 &&
            getnameinfo((struct sockaddr *)&ss, len, g_peername,
                        sizeof g_peername, NULL, 0, NI_NUMERICHOST) != 0)
            snprintf(g_peername, sizeof g_peername, "unknown");

        for (char *p = g_peername; *p; p++)     /* keep the log line clean */
            if (!isprint((unsigned char)*p)) *p = '?';
    }
    return g_peername;
}

/* clen < 0: no Content-Length header. extra: complete header lines or NULL */
static void send_headers(Req *r, int status, const char *ctype,
                         long long clen, const char *extra)
{
    char h[8192], date[64];
    size_t n = 0;
    time_t now = time(NULL);
    struct tm tm;
    gmtime_r(&now, &tm);
    strftime(date, sizeof date, "%a, %d %b %Y %H:%M:%S GMT", &tm);

    hprintf(h, sizeof h, &n,
            "HTTP/1.1 %d %s\r\nServer: xcontents/2.0\r\nDate: %s\r\n"
            "Connection: close\r\n", status, reason(status), date);

    /* CORS: allow access from anywhere. Cookies need a concrete origin
       (a wildcard is not honoured for credentialed requests), so echo it. */
    if (g_tutorial && r->origin[0])
        hprintf(h, sizeof h, &n,
                "Access-Control-Allow-Origin: %s\r\n"
                "Access-Control-Allow-Credentials: true\r\n"
                "Vary: Origin\r\n", r->origin);
    else
        hprintf(h, sizeof h, &n, "Access-Control-Allow-Origin: *\r\n");

    if (r->setcookie[0])
        hprintf(h, sizeof h, &n, "Set-Cookie: %s\r\n", r->setcookie);
    if (ctype)
        hprintf(h, sizeof h, &n, "Content-Type: %s\r\n", ctype);
    if (clen >= 0)
        hprintf(h, sizeof h, &n, "Content-Length: %lld\r\n", clen);
    if (extra)
        hprintf(h, sizeof h, &n, "%s", extra);
    hprintf(h, sizeof h, &n, "\r\n");

    conn_write_all(r->conn, h, n);

    /* Log after sending, so a slow reverse-DNS lookup doesn't delay it. */
    char ts[48];
    struct tm lt;
    localtime_r(&now, &lt);
    strftime(ts, sizeof ts, "%Y-%m-%d %H:%M:%S %z", &lt);
    fprintf(stderr, "%s %s %s %s → %d\n", ts, peer_name(),
            r->method[0] ? r->method : "-", r->raw[0] ? r->raw : "-", status);
}

static void send_simple(Req *r, int status, const char *ctype,
                        const char *body, size_t len, const char *extra)
{
    int nobody = (status == 204);
    send_headers(r, status, ctype, nobody ? -1 : (long long)len, extra);
    if (!r->head_only && !nobody && len > 0)
        conn_write_all(r->conn, body, len);
}

static void send_error(Req *r, int status, const char *extra)
{
    char body[128];
    int n = snprintf(body, sizeof body, "%d %s\n", status, reason(status));
    send_simple(r, status, "text/plain", body, (size_t)n, extra);
}

static void send_resolve_error(Req *r, int rc)
{
    switch (rc) {
    case -1: send_error(r, 404, NULL); break;
    case -2: send_error(r, 403, NULL); break;
    case -3: send_error(r, 414, NULL); break;
    case -4: send_error(r, 409, NULL); break;
    default: send_error(r, 500, NULL); break;
    }
}

/* ------------------------------------------------------------------ */
/* Media types                                                         */
/* ------------------------------------------------------------------ */

typedef struct {
    char *ext;                         /* including the leading dot */
    char *type;
} MType;

static MType *g_mtypes;
static size_t g_nmtypes;

/* Used only if the media types file cannot be read. */
static const struct { const char *ext, *type; } builtin_mtypes[] = {
    {".txt",   "text/plain"},
    {".html",  "text/html"},
    {".css",   "text/css"},
    {".xml",   "text/xml"},
    {".xsd",   "text/xml"},
    {".xsl",   "text/xsl"},
    {".xhtml", "application/xhtml+xml"},
    {".js",    "application/javascript"},
    {".gif",   "image/gif"},
    {".jpg",   "image/jpg"},
    {".png",   "image/png"},
    {".svg",   "image/svg+xml"},
    {NULL, NULL}
};

static int add_mtype(const char *ext, const char *type)
{
    size_t el = strlen(ext);
    MType *p = realloc(g_mtypes, (g_nmtypes + 1) * sizeof *p);
    if (!p) return -1;
    g_mtypes = p;

    char *e = malloc(el + 2);
    char *t = strdup(type);
    if (!e || !t) { free(e); free(t); return -1; }
    snprintf(e, el + 2, "%s%s", ext[0] == '.' ? "" : ".", ext);

    g_mtypes[g_nmtypes].ext = e;
    g_mtypes[g_nmtypes].type = t;
    g_nmtypes++;
    return 0;
}

/* Read "<extension> <type>" lines. Returns -1 if the file can't be opened. */
static int load_mediatypes(const char *file)
{
    FILE *f = fopen(file, "r");
    if (!f) return -1;

    char *line = NULL;
    size_t cap = 0;
    int lineno = 0;
    while (getline(&line, &cap, f) != -1) {
        lineno++;
        char *hash = strchr(line, '#');
        if (hash) *hash = '\0';

        char ext[128], type[256], extra[2];
        int n = sscanf(line, "%127s %255s %1s", ext, type, extra);
        if (n == EOF || n == 0) continue;            /* blank or comment */
        if (n != 2 || !strchr(type, '/')) {
            fprintf(stderr, "%s:%d: ignoring malformed media type entry\n",
                    file, lineno);
            continue;
        }
        if (add_mtype(ext, type) != 0) break;
    }
    free(line);
    fclose(f);
    return 0;
}

static const char *media_type(const char *path)
{
    const char *slash = strrchr(path, '/');
    const char *dot = strrchr(path, '.');
    if (dot && (!slash || dot > slash))
        for (size_t i = g_nmtypes; i-- > 0; )        /* last entry wins */
            if (strcasecmp(dot, g_mtypes[i].ext) == 0)
                return g_mtypes[i].type;
    return "application/octet-stream";
}

/* ------------------------------------------------------------------ */
/* Path handling                                                       */
/* ------------------------------------------------------------------ */

static int within(const char *path, const char *dir)
{
    size_t n = strlen(dir);
    if (strncmp(path, dir, n) != 0) return 0;
    return path[n] == '/' || path[n] == '\0' || (n == 1 && dir[0] == '/');
}

static int is_protected(const char *real)
{
    for (int i = 0; i < g_nprot; i++)
        if (strcmp(real, g_prot[i]) == 0) return 1;
    return 0;
}

static int hexval(int c)
{
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

/* Extract, decode and validate the URL path. Returns 0 or an HTTP status. */
static int parse_target(Req *r)
{
    const char *t = r->target;

    /* Absolute-form target ("http://host/path"): skip scheme and host. */
    int absolute = 0;
    if (strncasecmp(t, "http://", 7) == 0) {
        t += 7;
        absolute = 1;
    } else if (strncasecmp(t, "https://", 8) == 0) {
        t += 8;
        absolute = 1;
    }
    if (absolute) {
        const char *slash = strchr(t, '/');
        t = slash ? slash : "/";
    }

    if (*t != '/') return 400;
    size_t n = strcspn(t, "?#");
    if (n >= sizeof r->raw) return 414;
    memcpy(r->raw, t, n);
    r->raw[n] = '\0';

    size_t o = 0;
    for (size_t i = 0; i < n; i++) {
        unsigned char ch = (unsigned char)r->raw[i];
        if (ch == '%') {
            if (i + 2 >= n) return 400;
            int a = hexval(r->raw[i + 1]), b = hexval(r->raw[i + 2]);
            if (a < 0 || b < 0) return 400;
            ch = (unsigned char)(a * 16 + b);
            if (ch == 0) return 400;
            i += 2;
        }
        if (o >= MAX_PATH_LEN) return 414;
        r->path[o++] = (char)ch;
    }
    r->path[o] = '\0';

    /* "/../" (or a trailing "/..") anywhere in the path: bad request */
    if (strstr(r->path, "/../") != NULL) return 400;
    if (o >= 3 && strcmp(r->path + o - 3, "/..") == 0) return 400;

    if (o > 0 && r->path[o - 1] == '/') {
        if (o + 10 >= sizeof r->path) return 414;
        strcpy(r->path + o, "index.html");
    }
    return 0;
}

/*
 * Resolve an existing file below base. Returns
 *   0 ok, -1 not found, -2 forbidden, -3 too long
 */
static int resolve_existing(const char *base, const char *rel, char *out)
{
    char full[2 * PATH_MAX];
    if ((size_t)snprintf(full, sizeof full, "%s%s", base, rel) >= sizeof full)
        return -3;
    if (!realpath(full, out)) {
        if (errno == ENOENT || errno == ENOTDIR) return -1;
        if (errno == ENAMETOOLONG) return -3;
        return -2;
    }
    if (!within(out, base) || is_protected(out)) return -2;
    return 0;
}

/*
 * Resolve a file to be created or overwritten below base, creating missing
 * directories. Returns 0 ok, -2 forbidden, -3 too long, -4 is a directory,
 * -5 other error. *existed tells whether the file already exists.
 */
static int resolve_for_write(const char *base, const char *rel,
                             char *out, int *existed)
{
    char cur[PATH_MAX], real[PATH_MAX];
    size_t bl = strlen(base);
    if (bl >= sizeof cur) return -3;
    memcpy(cur, base, bl + 1);

    const char *last = strrchr(rel, '/');
    if (!last) return -2;

    for (const char *p = rel; p < last; ) {
        const char *s = p + 1;
        const char *e = strchr(s, '/');      /* always found: at most `last` */
        size_t cl = (size_t)(e - s);
        p = e;
        if (cl == 0 || (cl == 1 && s[0] == '.')) continue;
        size_t cur_len = strlen(cur);
        if (cur_len + 1 + cl >= sizeof cur) return -3;
        cur[cur_len] = '/';
        memcpy(cur + cur_len + 1, s, cl);
        cur[cur_len + 1 + cl] = '\0';
        if (mkdir(cur, 0755) != 0 && errno != EEXIST)
            return errno == EACCES ? -2 : -5;
        if (!realpath(cur, real)) return -2;
        if (!within(real, base)) return -2;
        strcpy(cur, real);
    }

    const char *name = last + 1;
    if (*name == '\0' || strcmp(name, ".") == 0) return -2;
    size_t cur_len = strlen(cur);
    if (cur_len + 1 + strlen(name) >= PATH_MAX) return -3;
    snprintf(out, PATH_MAX, "%s/%s", cur, name);

    struct stat st;
    if (lstat(out, &st) == 0) {
        if (!realpath(out, real)) return -2;      /* dangling symlink */
        if (!within(real, base)) return -2;
        struct stat st2;
        if (stat(real, &st2) == 0 && S_ISDIR(st2.st_mode)) return -4;
        strcpy(out, real);
        *existed = 1;
    } else if (errno == ENOENT) {
        *existed = 0;
    } else {
        return -2;
    }
    if (is_protected(out)) return -2;
    return 0;
}

/* ------------------------------------------------------------------ */
/* Cookie handling                                                     */
/* ------------------------------------------------------------------ */

static int get_cookie(const char *hdr, const char *name, char *val, size_t cap)
{
    if (!hdr) return 0;
    size_t nl = strlen(name);
    const char *p = hdr;
    while (*p) {
        while (*p == ' ' || *p == ';' || *p == '\t') p++;
        if (strncmp(p, name, nl) == 0 && p[nl] == '=') {
            p += nl + 1;
            size_t n = strcspn(p, "; \t");
            if (n >= cap) { snprintf(val, cap, "!"); return 1; }
            memcpy(val, p, n);
            val[n] = '\0';
            return n > 0;                 /* empty value == not set */
        }
        p += strcspn(p, ";");
    }
    return 0;
}

static int valid_name(const char *s)
{
    if (!*s) return 0;
    for (; *s; s++)
        if (!isalnum((unsigned char)*s) && *s != '-' && *s != '_') return 0;
    return 1;
}

/*
 * Returns 0: cookie not set; 1: cookie set, sub = absolute subdirectory;
 * -1: cookie set but directory missing (error response already sent,
 * cookie deleted).
 */
static int resolve_cookie(Req *r, char *sub, size_t cap)
{
    char val[64], real[PATH_MAX];
    struct stat st;

    if (!get_cookie(r->cookie, COOKIE_NAME, val, sizeof val)) return 0;

    int ok = valid_name(val);
    if (ok) {
        snprintf(sub, cap, "%s/%s", g_root, val);
        ok = stat(sub, &st) == 0 && S_ISDIR(st.st_mode) &&
             realpath(sub, real) != NULL && strcmp(real, sub) == 0;
    }
    if (!ok) {
        snprintf(r->setcookie, sizeof r->setcookie,
                 COOKIE_NAME "=; Path=/; Max-Age=0; "
                 "Expires=Thu, 01 Jan 1970 00:00:00 GMT");
        const char *msg = "Content directory not found\n";
        send_simple(r, 404, "text/plain", msg, strlen(msg), NULL);
        return -1;
    }
    return 1;
}

static int make_random_dir(Req *r, char *sub, size_t cap)
{
    for (int attempt = 0; attempt < 10; attempt++) {
        unsigned char rnd[16];
        char name[33];
        if (getrandom(rnd, sizeof rnd, 0) != (ssize_t)sizeof rnd) return -1;
        for (int i = 0; i < 16; i++) snprintf(name + 2 * i, 3, "%02x", rnd[i]);
        snprintf(sub, cap, "%s/%s", g_root, name);
        if (mkdir(sub, 0755) == 0) {
            snprintf(r->setcookie, sizeof r->setcookie,
                     COOKIE_NAME "=%s; Path=/", name);
            return 0;
        }
        if (errno != EEXIST) return -1;
    }
    return -1;
}

/* ------------------------------------------------------------------ */
/* File helpers                                                        */
/* ------------------------------------------------------------------ */

static int write_full(int fd, const char *p, size_t n)
{
    while (n > 0) {
        ssize_t w = write(fd, p, n);
        if (w < 0) {
            if (errno == EINTR) continue;
            return -1;
        }
        p += w;
        n -= (size_t)w;
    }
    return 0;
}

/* Write a+b+c to a temp file next to path, then rename over path. */
static int write_atomic(const char *path,
                        const char *a, size_t al,
                        const char *b, size_t bl,
                        const char *c, size_t cl)
{
    char tmp[PATH_MAX + 16];
    snprintf(tmp, sizeof tmp, "%s.tmpXXXXXX", path);
    int fd = mkstemp(tmp);
    if (fd < 0) return -1;
    int ok = write_full(fd, a, al) == 0 && write_full(fd, b, bl) == 0 &&
             write_full(fd, c, cl) == 0 && fchmod(fd, 0644) == 0;
    if (close(fd) != 0) ok = 0;
    if (ok && rename(tmp, path) != 0) ok = 0;
    if (!ok) unlink(tmp);
    return ok ? 0 : -1;
}

/* If the file (ignoring trailing whitespace) ends with a closing XML tag,
   return 1 and the offset of its "<". */
static int find_closing_tag(const char *b, size_t len, size_t *pos)
{
    size_t end = len;
    while (end > 0 && isspace((unsigned char)b[end - 1])) end--;
    if (end < 4 || b[end - 1] != '>') return 0;
    size_t i = end - 1;
    while (i > 0) {
        i--;
        if (b[i] == '<') break;
        if (b[i] == '>') return 0;
    }
    if (b[i] != '<' || b[i + 1] != '/' || i + 2 >= end - 1) return 0;
    *pos = i;
    return 1;
}

/* ------------------------------------------------------------------ */
/* Method handlers                                                     */
/* ------------------------------------------------------------------ */

static const char *allowed_methods(void)
{
    return g_tutorial ? "GET, HEAD, PUT, POST, DELETE, OPTIONS"
                      : "GET, HEAD, PUT, POST, OPTIONS";
}

static void do_options(Req *r)
{
    char extra[1024];
    size_t n = 0;
    hprintf(extra, sizeof extra, &n,
            "Allow: %s\r\n"
            "Access-Control-Allow-Methods: %s\r\n"
            "Access-Control-Allow-Headers: %s\r\n"
            "Access-Control-Max-Age: 86400\r\n",
            allowed_methods(), allowed_methods(),
            r->acrh[0] ? r->acrh : "*");
    send_simple(r, 204, NULL, NULL, 0, extra);
}

static void do_get(Req *r, const char *sub)
{
    const char *bases[2];
    int nb = 0, found = 0;
    char full[PATH_MAX];

    if (sub) bases[nb++] = sub;
    bases[nb++] = g_root;              /* fallback search in the root */

    for (int i = 0; i < nb && !found; i++) {
        int rc = resolve_existing(bases[i], r->path, full);
        if (rc == 0) found = 1;
        else if (rc != -1) { send_resolve_error(r, rc); return; }
    }
    if (!found) { send_error(r, 404, NULL); return; }

    int fd = open(full, O_RDONLY | O_CLOEXEC);
    if (fd < 0) { send_error(r, errno == EACCES ? 403 : 404, NULL); return; }

    struct stat st;
    if (fstat(fd, &st) != 0) { close(fd); send_error(r, 500, NULL); return; }

    if (S_ISDIR(st.st_mode)) {
        close(fd);
        char extra[MAX_PATH_LEN + 64];
        snprintf(extra, sizeof extra, "Location: %s/\r\n", r->raw);
        send_error(r, 301, extra);
        return;
    }
    if (!S_ISREG(st.st_mode)) { close(fd); send_error(r, 403, NULL); return; }

    char extra[128], lm[64];
    struct tm tm;
    gmtime_r(&st.st_mtime, &tm);
    strftime(lm, sizeof lm, "%a, %d %b %Y %H:%M:%S GMT", &tm);
    snprintf(extra, sizeof extra, "Last-Modified: %s\r\n", lm);
    send_headers(r, 200, media_type(full), (long long)st.st_size, extra);

    if (!r->head_only) {
        char buf[65536];
        ssize_t n;
        while ((n = read(fd, buf, sizeof buf)) > 0)
            if (conn_write_all(r->conn, buf, (size_t)n) != 0) break;
    }
    close(fd);
}

static void do_put(Req *r, const char *base, const char *body, size_t len)
{
    char out[PATH_MAX];
    int existed = 0;
    int rc = resolve_for_write(base, r->path, out, &existed);
    if (rc != 0) { send_resolve_error(r, rc); return; }
    if (write_atomic(out, body, len, "", 0, "", 0) != 0) {
        send_error(r, 500, NULL);
        return;
    }
    if (existed) {
        send_simple(r, 204, NULL, NULL, 0, NULL);
    } else {
        const char *m = "Created\n";
        send_simple(r, 201, "text/plain", m, strlen(m), NULL);
    }
}

static void do_post(Req *r, const char *base, const char *body, size_t len)
{
    char out[PATH_MAX];
    int existed = 0;
    int rc = resolve_for_write(base, r->path, out, &existed);
    if (rc != 0) { send_resolve_error(r, rc); return; }

    int fd = existed ? open(out, O_RDONLY | O_CLOEXEC) : -1;
    if (fd < 0) {
        /* new file: <post>content</post> */
        if (write_atomic(out, "<post>", 6, body, len, "</post>", 7) != 0) {
            send_error(r, 500, NULL);
            return;
        }
        const char *m = "Created\n";
        send_simple(r, 201, "text/plain", m, strlen(m), NULL);
        return;
    }

    struct stat st;
    if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) {
        close(fd); send_error(r, 403, NULL); return;
    }
    if (st.st_size > MAX_FILE) { close(fd); send_error(r, 413, NULL); return; }

    size_t flen = (size_t)st.st_size, got = 0;
    char *fbuf = malloc(flen + 1);
    if (!fbuf) { close(fd); send_error(r, 500, NULL); return; }
    while (got < flen) {
        ssize_t n = read(fd, fbuf + got, flen - got);
        if (n < 0 && errno == EINTR) continue;
        if (n <= 0) break;
        got += (size_t)n;
    }
    close(fd);
    flen = got;

    size_t pos;
    int rc2;
    if (find_closing_tag(fbuf, flen, &pos))
        rc2 = write_atomic(out, fbuf, pos, body, len, fbuf + pos, flen - pos);
    else
        rc2 = write_atomic(out, fbuf, flen, "\n", 1, body, len);
    free(fbuf);

    if (rc2 != 0) { send_error(r, 500, NULL); return; }
    const char *m = "OK\n";
    send_simple(r, 200, "text/plain", m, strlen(m), NULL);
}

static void do_delete(Req *r, const char *base)
{
    char full[PATH_MAX];
    int rc = resolve_existing(base, r->path, full);
    if (rc != 0) { send_resolve_error(r, rc); return; }
    if (unlink(full) != 0) {
        send_error(r, (errno == ENOENT) ? 404 : 403, NULL);
        return;
    }
    send_simple(r, 204, NULL, NULL, 0, NULL);
}

/* ------------------------------------------------------------------ */
/* Request parsing and connection handling                             */
/* ------------------------------------------------------------------ */

/* Returns 0, -1 (connection problem) or -2 (headers too large). */
static int read_request_head(Conn *c, char *buf, size_t cap,
                             size_t *head_len, size_t *have)
{
    size_t h = 0;
    for (;;) {
        ssize_t n = conn_read(c, buf + h, cap - 1 - h);
        if (n <= 0) return -1;
        h += (size_t)n;
        buf[h] = '\0';
        char *p = strstr(buf, "\r\n\r\n");
        if (p) {
            *head_len = (size_t)(p - buf) + 4;
            *have = h;
            return 0;
        }
        if (h >= cap - 1) return -2;
    }
}

/* Returns 0 or an HTTP status code. Modifies buf in place. */
static int parse_request(char *buf, size_t head_len, Req *r)
{
    buf[head_len - 4] = '\0';
    char *line = buf;
    char *eol = strstr(line, "\r\n");
    if (eol) *eol = '\0';

    char *sp1 = strchr(line, ' ');
    if (!sp1) return 400;
    *sp1++ = '\0';
    char *sp2 = strchr(sp1, ' ');
    if (!sp2) return 400;
    *sp2++ = '\0';

    if (!*line || strlen(line) >= sizeof r->method || !*sp1) return 400;
    strcpy(r->method, line);
    r->target = sp1;
    if (strcmp(sp2, "HTTP/1.1") != 0 && strcmp(sp2, "HTTP/1.0") != 0)
        return strncmp(sp2, "HTTP/", 5) == 0 ? 505 : 400;

    r->content_length = -1;
    while (eol) {
        line = eol + 2;
        eol = strstr(line, "\r\n");
        if (eol) *eol = '\0';
        if (!*line) break;

        char *colon = strchr(line, ':');
        if (!colon) return 400;
        *colon = '\0';
        char *val = colon + 1;
        while (*val == ' ' || *val == '\t') val++;
        size_t vl = strlen(val);
        while (vl && (val[vl - 1] == ' ' || val[vl - 1] == '\t')) val[--vl] = '\0';

        if (strcasecmp(line, "Content-Length") == 0) {
            char *end;
            if (!isdigit((unsigned char)*val)) return 400;
            errno = 0;
            long v = strtol(val, &end, 10);
            if (*end || errno) return 400;
            if (r->content_length >= 0 && r->content_length != v) return 400;
            r->content_length = v;
        } else if (strcasecmp(line, "Transfer-Encoding") == 0) {
            r->has_te = 1;
        } else if (strcasecmp(line, "Expect") == 0) {
            if (strcasecmp(val, "100-continue") == 0) r->expect100 = 1;
        } else if (strcasecmp(line, "Cookie") == 0) {
            r->cookie = val;
        } else if (strcasecmp(line, "Origin") == 0) {
            if (strlen(val) < sizeof r->origin && !strpbrk(val, "\r\n"))
                strcpy(r->origin, val);
        } else if (strcasecmp(line, "Access-Control-Request-Headers") == 0) {
            if (strlen(val) < sizeof r->acrh && !strpbrk(val, "\r\n"))
                strcpy(r->acrh, val);
        }
    }
    return 0;
}

/* Returns 0 on success or an HTTP status code. */
static int read_body(Req *r, const char *leftover, size_t left,
                     char **out, size_t *outlen)
{
    if (r->has_te) return 501;                    /* chunked not supported */
    if (r->content_length < 0) return 411;
    if (r->content_length > MAX_BODY) return 413;

    size_t len = (size_t)r->content_length;
    char *body = malloc(len + 1);
    if (!body) return 500;

    size_t got = left < len ? left : len;
    memcpy(body, leftover, got);
    if (got < len && r->expect100) {
        static const char cont[] = "HTTP/1.1 100 Continue\r\n\r\n";
        conn_write_all(r->conn, cont, sizeof cont - 1);
    }
    while (got < len) {
        ssize_t n = conn_read(r->conn, body + got, len - got);
        if (n <= 0) { free(body); return 400; }
        got += (size_t)n;
    }
    body[len] = '\0';
    *out = body;
    *outlen = len;
    return 0;
}

static void handle_connection(Conn *c)
{
    char buf[MAX_HEADER + 1];
    Req r;
    memset(&r, 0, sizeof r);
    r.conn = c;

    size_t head_len, have;
    int rc = read_request_head(c, buf, sizeof buf, &head_len, &have);
    if (rc == -1) return;
    if (rc == -2) { send_error(&r, 431, NULL); return; }

    int st = parse_request(buf, head_len, &r);
    if (st) { send_error(&r, st, NULL); return; }

    int is_get = strcmp(r.method, "GET") == 0;
    int is_head = strcmp(r.method, "HEAD") == 0;
    int is_put = strcmp(r.method, "PUT") == 0;
    int is_post = strcmp(r.method, "POST") == 0;
    int is_del = strcmp(r.method, "DELETE") == 0;
    int is_opt = strcmp(r.method, "OPTIONS") == 0;
    r.head_only = is_head;

    if (!(is_get || is_head || is_put || is_post || is_del || is_opt)) {
        char allow[128];
        snprintf(allow, sizeof allow, "Allow: %s\r\n", allowed_methods());
        send_error(&r, 501, allow);
        return;
    }

    if (is_opt && strcmp(r.target, "*") == 0) { do_options(&r); return; }

    st = parse_target(&r);
    if (st) { send_error(&r, st, NULL); return; }

    if (is_opt) { do_options(&r); return; }

    /* Body for PUT / POST */
    char *body = NULL;
    size_t blen = 0;
    if (is_put || is_post) {
        st = read_body(&r, buf + head_len, have - head_len, &body, &blen);
        if (st) { send_error(&r, st, NULL); return; }
    }

    char sub[PATH_MAX];
    int ck = 0;                               /* normal mode: cookie ignored */
    if (g_tutorial) {
        ck = resolve_cookie(&r, sub, sizeof sub);
        if (ck < 0) { free(body); return; }
    }

    if (is_get || is_head) {
        do_get(&r, ck ? sub : NULL);
    } else if (is_del) {
        /* normal mode: never; tutorial mode: only inside the cookie dir */
        if (!g_tutorial || !ck) send_error(&r, 403, NULL);
        else                    do_delete(&r, sub);
    } else {                                  /* PUT or POST */
        const char *base = g_root;
        int fail = 0;
        if (g_tutorial) {
            if (!ck && make_random_dir(&r, sub, sizeof sub) != 0) fail = 1;
            base = sub;
        }
        if (fail)        send_error(&r, 500, NULL);
        else if (is_put) do_put(&r, base, body, blen);
        else             do_post(&r, base, body, blen);
    }
    free(body);
}

static void serve(int cfd, int tls)
{
    struct timeval tv = { IO_TIMEOUT, 0 };
    setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
    setsockopt(cfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);

    Conn c = { cfd, NULL };
    if (tls) {
        c.ssl = SSL_new(g_ctx);
        if (!c.ssl) { close(cfd); return; }
        SSL_set_fd(c.ssl, cfd);
        if (SSL_accept(c.ssl) <= 0) {
            SSL_free(c.ssl);
            close(cfd);
            return;
        }
    }
    handle_connection(&c);
    if (c.ssl) {
        SSL_shutdown(c.ssl);
        SSL_free(c.ssl);
    }
    close(cfd);
}

/* ------------------------------------------------------------------ */
/* Startup                                                             */
/* ------------------------------------------------------------------ */

static int listen_on(int port)
{
    int fd = socket(AF_INET6, SOCK_STREAM, 0);
    if (fd >= 0) {
        int off = 0, on = 1;
        struct sockaddr_in6 a;
        memset(&a, 0, sizeof a);
        a.sin6_family = AF_INET6;
        a.sin6_addr = in6addr_any;
        a.sin6_port = htons((uint16_t)port);
        setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof off);
        setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
        if (bind(fd, (struct sockaddr *)&a, sizeof a) == 0 && listen(fd, 128) == 0)
            return fd;
        close(fd);
    }
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) return -1;
    int on = 1;
    struct sockaddr_in a4;
    memset(&a4, 0, sizeof a4);
    a4.sin_family = AF_INET;
    a4.sin_addr.s_addr = htonl(INADDR_ANY);
    a4.sin_port = htons((uint16_t)port);
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
    if (bind(fd, (struct sockaddr *)&a4, sizeof a4) == 0 && listen(fd, 128) == 0)
        return fd;
    close(fd);
    return -1;
}

static int parse_port(const char *s)
{
    char *end;
    long v = strtol(s, &end, 10);
    if (*s == '\0' || *end || v < 1 || v > 65535) {
        fprintf(stderr, "invalid port: %s\n", s);
        exit(2);
    }
    return (int)v;
}

static int init_tls(const char *cert, const char *key)
{
    OPENSSL_init_ssl(0, NULL);
    g_ctx = SSL_CTX_new(TLS_server_method());
    if (!g_ctx) return -1;
    SSL_CTX_set_min_proto_version(g_ctx, TLS1_2_VERSION);
    if (SSL_CTX_use_certificate_chain_file(g_ctx, cert) != 1 ||
        SSL_CTX_use_PrivateKey_file(g_ctx, key, SSL_FILETYPE_PEM) != 1 ||
        SSL_CTX_check_private_key(g_ctx) != 1) {
        ERR_print_errors_fp(stderr);
        SSL_CTX_free(g_ctx);
        g_ctx = NULL;
        return -1;
    }
    return 0;
}

int main(int argc, char **argv)
{
    int http_port = 80, https_port = 443;
    const char *cert = "cert.pem", *key = "key.pem";
    const char *mtfile = "mediatypes.txt";
    int opt;

    while ((opt = getopt(argc, argv, "tp:s:c:k:m:h")) != -1) {
        switch (opt) {
        case 't': g_tutorial = 1; break;
        case 'p': http_port = parse_port(optarg); break;
        case 's': https_port = parse_port(optarg); break;
        case 'c': cert = optarg; break;
        case 'k': key = optarg; break;
        case 'm': mtfile = optarg; break;
        default:
            fprintf(stderr,
                    "usage: %s [-t] [-p http_port] [-s https_port] "
                    "[-c cert.pem] [-k key.pem] [-m mediatypes.txt]\n"
                    "  -t  tutorial mode (cookie subdirectories, DELETE)\n", argv[0]);
            return opt == 'h' ? 0 : 2;
        }
    }

    char cwd[PATH_MAX];
    if (!getcwd(cwd, sizeof cwd) || !realpath(cwd, g_root)) {
        perror("root directory");
        return 1;
    }

    /* Never serve the key or certificate, even if inside the root. */
    const char *pf[2] = { cert, key };
    for (int i = 0; i < 2; i++)
        if (realpath(pf[i], g_prot[g_nprot])) g_nprot++;

    if (load_mediatypes(mtfile) < 0) {
        fprintf(stderr, "warning: cannot read %s (%s); using built-in media types\n",
                mtfile, strerror(errno));
        for (int i = 0; builtin_mtypes[i].ext; i++)
            add_mtype(builtin_mtypes[i].ext, builtin_mtypes[i].type);
    }

    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);            /* auto-reap children */

    struct pollfd pf_[2];
    int is_tls[2], nfds = 0;

    int hfd = listen_on(http_port);
    if (hfd < 0) { perror("HTTP listen"); return 1; }
    pf_[nfds].fd = hfd; pf_[nfds].events = POLLIN; is_tls[nfds++] = 0;
    fprintf(stderr, "HTTP  listening on port %d\n", http_port);

    if (init_tls(cert, key) == 0) {
        int sfd = listen_on(https_port);
        if (sfd < 0) { perror("HTTPS listen"); return 1; }
        pf_[nfds].fd = sfd; pf_[nfds].events = POLLIN; is_tls[nfds++] = 1;
        fprintf(stderr, "HTTPS listening on port %d\n", https_port);
    } else {
        fprintf(stderr, "warning: could not load %s / %s; HTTPS disabled\n",
                cert, key);
    }
    fprintf(stderr, "root directory: %s\n", g_root);
    fprintf(stderr, "mode: %s\n", g_tutorial ? "tutorial" : "normal");
    fprintf(stderr, "media types: %zu entries\n", g_nmtypes);

    for (;;) {
        int n = poll(pf_, (nfds_t)nfds, -1);
        if (n < 0) {
            if (errno == EINTR) continue;
            perror("poll");
            break;
        }
        for (int i = 0; i < nfds; i++) {
            if (!(pf_[i].revents & POLLIN)) continue;
            struct sockaddr_storage peer;
            socklen_t peerlen = sizeof peer;
            int cfd = accept(pf_[i].fd, (struct sockaddr *)&peer, &peerlen);
            if (cfd < 0) continue;
            pid_t pid = fork();
            if (pid == 0) {
                for (int j = 0; j < nfds; j++) close(pf_[j].fd);
                g_peer = peer;
                g_peerlen = peerlen;
                serve(cfd, is_tls[i]);
                _exit(0);
            }
            close(cfd);
        }
    }
    return 1;
}
