#define _POSIX_C_SOURCE 200809L
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

/* Bound both individual lines and the total request headers. */
static int
read_line(int fd, char *line, size_t size, size_t *total)
{
    size_t len = 0;
    for (;;) {
        unsigned char c;
        ssize_t n = read(fd, &c, 1);
        if (n < 0 && errno == EINTR)
            continue;
        if (n != 1 || ++*total > 65536 || c == '\0')
            return -1;
        if (c == '\n') {
            if (len == 0 || line[len - 1] != '\r')
                return -1;
            line[len - 1] = '\0';
            return 0;
        }
        if (len + 1 >= size)
            return -1;
        line[len++] = (char)c;
    }
}

static int
send_headers(int fd, int status, const char *reason, const char *type,
             off_t length)
{
    char buf[512];
    int n = snprintf(buf, sizeof(buf),
        "HTTP/1.0 %d %s\r\nContent-Length: %jd\r\n"
        "Content-Type: %s\r\nConnection: close\r\n%s\r\n",
        status, reason, (intmax_t)length, type,
        status == 405 ? "Allow: GET\r\n" : "");
    if (n < 0 || (size_t)n >= sizeof(buf))
        return -1;
    return write_all(fd, buf, (size_t)n);
}

static void
send_error(int fd, int status)
{
    const char *reason;
    char body[80];
    int n;
    switch (status) {
    case 400: reason = "Bad Request"; break;
    case 403: reason = "Forbidden"; break;
    case 404: reason = "Not Found"; break;
    case 405: reason = "Method Not Allowed"; break;
    default: status = 500; reason = "Internal Server Error"; break;
    }
    n = snprintf(body, sizeof(body), "%d %s\n", status, reason);
    if (send_headers(fd, status, reason, "text/plain", n) == 0)
        (void)write_all(fd, body, (size_t)n);
}

static const char *
mime_type(const char *path)
{
    const char *ext = strrchr(path, '.');
    if (ext == NULL) return "application/octet-stream";
    if (strcmp(ext, ".html") == 0) return "text/html";
    if (strcmp(ext, ".css") == 0) return "text/css";
    if (strcmp(ext, ".js") == 0) return "text/javascript";
    if (strcmp(ext, ".txt") == 0) return "text/plain";
    if (strcmp(ext, ".png") == 0) return "image/png";
    if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0)
        return "image/jpeg";
    if (strcmp(ext, ".gif") == 0) return "image/gif";
    if (strcmp(ext, ".svg") == 0) return "image/svg+xml";
    return "application/octet-stream";
}

static int
hex_digit(unsigned char 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;
}

/* Decode before checking components, so encoded '..' cannot bypass checks. */
static int
decode_path(char *path)
{
    char *src = path, *dst = path;
    if (*src != '/')
        return -1;
    while (*src && *src != '?') {
        unsigned char c = (unsigned char)*src++;
        if (c == '%') {
            int hi, lo;
            if (!src[0] || !src[1]) return -1;
            hi = hex_digit((unsigned char)src[0]);
            lo = hex_digit((unsigned char)src[1]);
            if (hi < 0 || lo < 0) return -1;
            c = (unsigned char)(hi * 16 + lo);
            src += 2;
        }
        if (c < 32 || c == 127 || c == '\\' || c == '#')
            return -1;
        *dst++ = (char)c;
    }
    *dst = '\0';
    if (strcmp(path, "/") == 0)
        strcpy(path, "/index.html");
    return 0;
}

static int
file_error(int error)
{
    if (error == ENOENT || error == ENOTDIR) return 404;
    if (error == EACCES || error == EPERM || error == ELOOP) return 403;
    return 500;
}

/* Walk relative to the root descriptor; never follow symbolic links. */
static int
open_file(int root, char *path, int *status)
{
    char *part, *next;
    int dir = dup(root), fd;
    if (dir < 0) {
        *status = 500;
        return -1;
    }
    part = path + 1;
    for (;;) {
        next = strchr(part, '/');
        if (next != NULL) *next = '\0';
        if (*part == '\0' || strcmp(part, ".") == 0 ||
            strcmp(part, "..") == 0) {
            *status = 403;
            close(dir);
            return -1;
        }
        do {
            fd = openat(dir, part, O_RDONLY | O_NOFOLLOW | O_NONBLOCK |
                        (next != NULL ? O_DIRECTORY : 0));
        } while (fd < 0 && errno == EINTR);
        if (fd < 0) *status = file_error(errno);
        close(dir);
        if (fd < 0 || next == NULL)
            return fd;
        dir = fd;
        part = next + 1;
    }
}

static void
handle_client(int client, int root)
{
    char line[8192], method[32], path[8192], version[16], buf[16384];
    const char *type;
    size_t total = 0;
    struct stat st;
    int end = 0, fd, status = 500;
    off_t remaining;

    if (read_line(client, line, sizeof(line), &total) < 0 ||
        sscanf(line, "%31s %8191s %15s%n", method, path, version, &end) != 3 ||
        line[end] != '\0' ||
        (strcmp(version, "HTTP/1.0") != 0 &&
         strcmp(version, "HTTP/1.1") != 0)) {
        send_error(client, 400);
        return;
    }
    for (;;) {
        if (read_line(client, line, sizeof(line), &total) < 0) {
            send_error(client, 400);
            return;
        }
        if (*line == '\0') break;
        if (strchr(line, ':') == NULL || line[0] == ':') {
            send_error(client, 400);
            return;
        }
    }
    if (strcmp(method, "GET") != 0) {
        send_error(client, 405);
        return;
    }
    if (decode_path(path) < 0) {
        send_error(client, 400);
        return;
    }
    type = mime_type(path);
    fd = open_file(root, path, &status);
    if (fd < 0) {
        send_error(client, status);
        return;
    }
    if (fstat(fd, &st) < 0) {
        send_error(client, 500);
        close(fd);
        return;
    }
    if (!S_ISREG(st.st_mode)) {
        send_error(client, 403);
        close(fd);
        return;
    }
    remaining = st.st_size;
    if (send_headers(client, 200, "OK", type, remaining) == 0) {
        while (remaining > 0) {
            size_t count = remaining < (off_t)sizeof(buf) ?
                           (size_t)remaining : sizeof(buf);
            ssize_t n = read(fd, buf, count);
            if (n < 0 && errno == EINTR) continue;
            if (n <= 0 || write_all(client, buf, (size_t)n) < 0) break;
            remaining -= n;
        }
    }
    close(fd);
}

static void
reap_children(int sig)
{
    int saved = errno;
    pid_t pid;
    (void)sig;
    do {
        pid = waitpid(-1, NULL, WNOHANG);
    } while (pid > 0 || (pid < 0 && errno == EINTR));
    errno = saved;
}

int
main(int argc, char **argv)
{
    const char *directory = argc > 2 ? argv[2] : ".";
    long port = 8080;
    char *end;
    int root, listener, one = 1;
    struct sockaddr_in address = {0};
    struct sigaction action = {0};

    if (argc > 3) {
        fprintf(stderr, "usage: %s [port] [directory]\n", argv[0]);
        return 1;
    }
    if (argc > 1) {
        errno = 0;
        port = strtol(argv[1], &end, 10);
        if (errno || *argv[1] == '\0' || *end || port < 1 || port > 65535) {
            fprintf(stderr, "invalid port: %s\n", argv[1]);
            return 1;
        }
    }
    root = open(directory, O_RDONLY | O_DIRECTORY);
    if (root < 0) { perror(directory); return 1; }
    action.sa_handler = SIG_IGN;
    sigemptyset(&action.sa_mask);
    if (sigaction(SIGPIPE, &action, NULL) < 0) goto fail;
    action.sa_handler = reap_children;
    action.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    if (sigaction(SIGCHLD, &action, NULL) < 0) goto fail;
    listener = socket(AF_INET, SOCK_STREAM, 0);
    if (listener < 0) goto fail;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    address.sin_port = htons((unsigned short)port);
    if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0 ||
        bind(listener, (struct sockaddr *)&address, sizeof(address)) < 0 ||
        listen(listener, 64) < 0) {
        int saved = errno;
        close(listener);
        errno = saved;
        goto fail;
    }
    printf("listening on port %ld\n", port);
    fflush(stdout);
    for (;;) {
        int client = accept(listener, NULL, NULL);
        pid_t pid;
        if (client < 0) {
            if (errno == EINTR || errno == ECONNABORTED) continue;
            perror("accept");
            break;
        }
        pid = fork();
        if (pid == 0) {
            close(listener);
            handle_client(client, root);
            close(client);
            close(root);
            _exit(0);
        }
        if (pid < 0) perror("fork");
        close(client);
    }
    close(listener);
    close(root);
    return 1;
fail:
    perror("httpd");
    close(root);
    return 1;
}
