#define MODULE "httpd"

#include "common.h"
#include "fw.h"
#include "httpd.h"
#include "config.h"
#include "boardinfo_esp.h"
#include "lwip/sockets.h"
#include "lwip/inet.h"

#include <incbin.h>
#include <unzipLIB.h>

#define HTTPD_PRIORITY	4

static httpd_handle_t httpd;
static const char fallback_language[] = "en"; /* For unknown language */
static const char redir_filename[] = "_redir"; /* For a directory "file" */
#define MAX_LANG_LEN 16

/* Looping version of httpd_send(); this is a hidden function in the server */
static esp_err_t httpd_send_all(httpd_req_t *req, const void *buf, size_t len)
{
    const char *p = buf;

    while (len) {
	int sent = httpd_send(req, p, len);
	if (sent <= 0)
	    return ESP_ERR_HTTPD_RESP_SEND;
	p += sent;
	len -= sent;
    }

    return ESP_OK;
}

/* Create a file pointer from an http request */
static ssize_t httpd_io_read(void *cookie, char *buf, size_t n)
{
    int rv = httpd_req_recv(cookie, buf, n);
    return rv < 0 ? -1 : rv;
}

static ssize_t httpd_io_write(void *cookie, const char *buf, size_t n)
{
    return httpd_resp_send_chunk(cookie, buf, n) ? 0 : n;
}

static int httpd_io_close_write(void *cookie)
{
    return httpd_resp_send_chunk(cookie, NULL, 0) ? -1 : 0;
}

static FILE *httpd_fopen_read(httpd_req_t *req)
{
    static const cookie_io_functions_t http_io_read_funcs = {
	.read  = httpd_io_read,
	.write = NULL,		/* Not writeable */
	.seek  = NULL,		/* Not seekable */
	.close = NULL,
    };
    return fopencookie((void *)req, "r", http_io_read_funcs);
}

static FILE *httpd_fopen_write(httpd_req_t *req)
{
    static const cookie_io_functions_t http_io_write_funcs = {
	.read  = httpd_io_read,
	.write = httpd_io_write,
	.seek  = NULL,		/* Not seekable */
	.close = httpd_io_close_write
    };
    return fopencookie((void *)req, "r+", http_io_write_funcs);
}

#define TIMEBUF_LEN 32
static const char *http_date(const struct tm *when)
{
    static char timebuf[32];

    strftime(timebuf, sizeof timebuf,
	     "%a, %d %b %Y %H:%M:%S GMT", when);

    return timebuf;
}

static const char *http_now(void)
{
    time_t t = time(NULL);
    return http_date(gmtime(&t));
}

static struct tm *set_weekday(struct tm *tm)
{
    /*
     * This is a variation on Zeller's congruence with a table lookup
     * for the month. The table contains the number of days since March 1,
     * mod 7 (for Jan and Feb, from March 1 of the *previous year*.)
     *
     * Sample test cases:
     *    Wed Mar  1 0000
     *    Thu Jan  1 1970
     *	  Wed Apr 27 2022
     *    Mon Feb 28 2000
     *    Wed Mar  1 2000
     *    Sun Feb 28 2100
     *    Mon Mar  1 2100
     */
    static const uint8_t md[12]	= { 5, 1, 0, 3, 5, 1, 3, 6, 2, 4, 0, 2 };
    unsigned int c, y, m, d;

    y = tm->tm_year + 1900;
    m = tm->tm_mon;
    d = tm->tm_mday;

    if (m < 2)
	y--;			/* Jan, Feb */

    c = y/100;

    /*
     * 2 represents the base date of Tue Feb 29 0000
     *
     * 0 = Sun, 6 = Sat
     */
    tm->tm_wday = (d + md[m] + y + (y >> 2) - c + (c >> 2) + 2) % 7;
    return tm;
}

static const char *http_dos_date(uint32_t dos_date)
{
    struct tm tm;

    tm.tm_sec   = (dos_date << 1) & 63;
    tm.tm_min   = (dos_date >> 5) & 63;
    tm.tm_hour  = (dos_date >> 11) & 31;
    tm.tm_mday  = (dos_date >> 16) & 31;
    tm.tm_mon   = ((dos_date >> 21) & 15) - 1;
    tm.tm_year  = (dos_date >> 25) + 80;
    tm.tm_isdst = 0;		/* Times are stored in GMT */

    return http_date(set_weekday(&tm));
}

static const char text_plain[] = "text/plain; charset=\"UTF-8\"";

static void httpd_print_request(httpd_req_t *req)
{
    /* Get the client address */
    union {
	struct sockaddr sa;
	struct sockaddr_in sin;
	struct sockaddr_in6 sin6;
	struct sockaddr_storage ss;
    } sa = { };
    char addrbuf[64];
    char *p = addrbuf;
    const char * const endp = addrbuf + sizeof addrbuf;
    int sock = httpd_req_to_sockfd(req);
    socklen_t sa_len = sizeof sa;

    if (getpeername(sock, &sa.sa, &sa_len))
	sa.sa.sa_family = AF_UNSPEC;

    /* lwip lacks getnameinfo() */

    switch (sa.sa.sa_family) {
    case AF_INET:
	inet_ntop(AF_INET, &sa.sin.sin_addr, p, endp - p);
	p = strchr(p, '\0');
	p += sprintf(p, ":%u", ntohs(sa.sin.sin_port));
	break;
    case AF_INET6:
	*p++ = '[';
	inet_ntop(AF_INET6, &sa.sin6.sin6_addr, p, endp - p);
	p = strchr(p, '\0');
	p += sprintf(p, "]:%u", ntohs(sa.sin6.sin6_port));
	break;
    case AF_UNSPEC:
	strcpy(p, "[not a socket?]");
	break;
    default:
	p += sprintf(p, "[AF %u?]", sa.sa.sa_family);
	break;
    }

    /* sa.sin.sin_port == sa.sin6.sin6_port */
    printf("[HTTP] %s %s %s\n",
	   addrbuf, http_method_str(req->method), req->uri);
}

static char *httpd_req_get_hdr(httpd_req_t *req, const char *field, size_t *lenp)
{
    size_t len = httpd_req_get_hdr_value_len(req, field);
    char *val  = NULL;
    if (len) {
	val = malloc(len+1);
	if (val) {
	    httpd_req_get_hdr_value_str(req, field, val, len+1);
	    val[len] = '\0';
	}
    }
    if (lenp)
	*lenp = len;
    return val;
}

enum hsp_flags {
    HSP_CRLF         = 1,    /* Append CR LF to the body */
    HSP_CLOSE        = 2,    /* Add a Connection: close header */
    HSP_REFERER      = 4,    /* Use referer as body (for redirects) */
    HSP_UNCACHE      = 8     /* Wipe caches, please... */
};

static esp_err_t httpd_send_plain(httpd_req_t *req,
				  unsigned int rcode,
				  const char *body, size_t blen,
				  enum hsp_flags flags, unsigned int refresh)
{
    char *header = NULL;
    esp_err_t err;
    int hlen;
    const char *now = http_now();

    MSG("http_send_plain %u \"%.*s\"\n", rcode, (int)blen, body);

    if (rcode > 499)
	flags |= HSP_CLOSE;

    const char *closer = flags & HSP_CLOSE ? "Connection: close\r\n" : "";
    bool redirect = rcode >= 300 && rcode <= 399;

    char *referer = NULL;
    char *refresher_buf = NULL;

    if (refresh || (flags & HSP_REFERER)) {
	size_t referer_len;
	referer = httpd_req_get_hdr(req, "Referer", &referer_len);

	/* "Effective" referer */
	const char * const ereferer = referer ? referer : "/";

	if (refresh) {
	    asprintf(&refresher_buf, "Refresh: %u;url=%s\r\n",
		     refresh, ereferer);
	}
	if (flags & HSP_REFERER) {
	    body = ereferer;
	    blen = referer ? referer_len : 1;
	}
    }

    const char * const refresher = refresher_buf ? refresher_buf : "";
    const char * const uncacher = (flags & HSP_UNCACHE)
	? "Clear-Site-Data: \"cache\"\r\n" : "";

    if (redirect) {
	/* \0 -> \n so don't include it */
	size_t blenadj = sizeof("3xx Redirect \r");

	/* Always CR LF */
	flags |= HSP_CRLF;

	/* Drop any CR LF already in the redirect string */
	for (size_t bchk = 0; bchk < blen; bchk++) {
	    if (body[bchk] == '\r' || body[bchk] == '\n') {
		blen = bchk;
		break;
	    }
	}

	hlen = asprintf(&header,
			"HTTP/1.1 %u\r\n"
			"Content-Type: %s\r\n"
			"Content-Length: %zu\r\n"
			"Date %s\r\n"
			"Location: %.*s\r\n"
			"%s%s%s"
			"\r\n"
			"%3u Redirect ",
			rcode, text_plain, blen + blenadj,
			now, (int)blen, body,
			closer, refresher, uncacher,
			rcode);
    } else {
	size_t blenadj = (flags & HSP_CRLF) ? 2 : 0;

	hlen = asprintf(&header,
			"HTTP/1.1 %u\r\n"
			"Content-Type: %s\r\n"
			"Content-Length: %zu\r\n"
			"Cache-Control: no-cache\r\n"
			"Date: %s\r\n"
			"%s%s%s"
			"\r\n",
			rcode, text_plain, blen + blenadj, now,
			closer, refresher, uncacher);
    }

    if (refresher_buf)
	free(refresher_buf);

    if (referer)
	free(referer);

    if (!header)
	return ESP_ERR_NO_MEM;

    err = httpd_send_all(req, header, hlen);
    if (!err && blen) {
	err = httpd_send_all(req, body, blen);
    }
    if (!err && (flags & HSP_CRLF)) {
	err = httpd_send_all(req, "\r\n", 2);
    }

    if (header)
	free(header);

    return err;
}

#define SL(s) (s), (sizeof(s)-1)
#define HTTP_ERR(r,e,s) httpd_send_plain((r), (e), SL(s), HSP_CRLF, 0)

static esp_err_t httpd_err_enoent(httpd_req_t *req)
{
    return HTTP_ERR(req, 404, "URI not found");
}

#if 0
static esp_err_t httpd_send_ok(httpd_req_t *req)
{
    return HTTP_ERR(req, 200, "OK");
}
#endif

static esp_err_t httpd_err_enomem(httpd_req_t *req)
{
    return HTTP_ERR(req, 503, "Out of memory");
}

static esp_err_t httpd_err_not_post(httpd_req_t *req)
{
    return HTTP_ERR(req, 405, "Only POST allowed");
}

#define HTTPD_ASSERT_POST(req)			\
    do {					\
	if ((req)->method != HTTP_POST)		\
	    return httpd_err_not_post(req);	\
    } while (0)

static esp_err_t httpd_update_done(httpd_req_t *req, const char *what, int err)
{
    char *response = NULL;
    int len;
    unsigned int reboot_time = reboot_delayed();

    if (err) {
	len = asprintf(&response,
		       "%s update failed: %s\r\n"
		       "Rebooting in %u seconds\r\n",
		       what, firmware_errstr(err), reboot_time);
    } else {
	len = asprintf(&response,
		       "%s update complete\r\n"
		       "Rebooting in %u seconds\r\n",
		       what, reboot_time);
    }

    if (!response)
	len = 0;

    esp_err_t rv = httpd_send_plain(req, err ? 400 : 200, response, len,
				    HSP_CLOSE|HSP_UNCACHE, reboot_time+5);
    if (response)
	free(response);

    return rv;
}

static esp_err_t httpd_firmware_update(httpd_req_t *req)
{
    int rv;

    HTTPD_ASSERT_POST(req);

    /* XXX: use httpd_fopen_read() here */
    rv = firmware_update_start((read_func_t)httpd_req_recv, (token_t)req, false);
    if (!rv)
	rv = firmware_update_wait(portMAX_DELAY);

    return httpd_update_done(req, "Firmware", rv);
}

static esp_err_t httpd_set_config(httpd_req_t *req, const char *query)
{
    FILE *f;

    int rv1 = 0;
    if (query) {
	rv1 = set_config_url_string(query);
    }

    int rv2 = 0;
    f = NULL;
    if (req->content_len) {
	f = httpd_fopen_read(req);
	if (!f)
	    return HTTP_ERR(req, 500, "Unable to get request handle");
    }
    rv2 = read_config(f, true);
    if (f)
	fclose(f);

    return httpd_update_done(req, "Configuration", rv1 ? rv1 : rv2);
}

static inline bool is_eol(int c)
{
    return c == EOF || c == '\0' || c == '\r' || c == '\n';
}

static esp_err_t httpd_set_board_rev(httpd_req_t *req)
{
    FILE *f = NULL;
    static const char rev_prefix[] = "max80.hw.ver";
    static const char rev_valid[] = "MAX80 v";
    char *rev_str;
    int err = Z_DATA_ERROR;
    enum sbr_parse_state {
	ps_start,
	ps_prefix,
	ps_string,
	ps_skipline
    };
    enum sbr_parse_state state;
    const char *match_ptr = NULL;
    char *p = NULL;
    int c;

    HTTPD_ASSERT_POST(req);

    rev_str = malloc(sizeof board_info.version_str);
    if (!rev_str)
	return httpd_err_enomem(req);

    f = httpd_fopen_read(req);
    if (!f) {
	free(rev_str);
	return HTTP_ERR(req, 500, "Unable to get request handle");
    }

    state = ps_start;

    do {
	bool eol;

	c = getc(f);
	eol = is_eol(c);

	switch (state) {
	case ps_start:
	    match_ptr = rev_prefix;
	    state = ps_prefix;
	    /* fall through */

	case ps_prefix:
	    if (eol) {
		state = ps_start;
	    } else if (*match_ptr && c == *match_ptr) {
		match_ptr++;
	    } else if (!*match_ptr && c == '=') {
		p = rev_str;
		state = ps_string;
	    } else {
		state = ps_skipline;
	    }
	    break;

	case ps_string:
	    if (eol) {
		*p = '\0';
		if (!memcmp(rev_str, rev_valid, sizeof rev_valid - 1)) {
		    /* Otherwise input truncated or invalid */
		    printf("[HTTP] setting board revision: %s\n", rev_str);
		    if (!board_info_set(rev_str)) {
			setvar_str(status_max80_hw_ver, board_info.version_str);
			err = Z_OK;
		    } else {
			err = FWUPDATE_ERR_CONFIG_SAVE;
		    }
		}
		state = ps_start;
	    } else if (p - rev_str >= sizeof board_info.version_str - 1) {
		state = ps_skipline;
	    } else {
		*p++ = c;
	    }
	    break;

	case ps_skipline:
	    if (eol)
		state = ps_start;
	    break;
	}
    } while (c != EOF);

    fclose(f);
    free(rev_str);

    return httpd_update_done(req, "board revision", err);
}

#define MIN_STATUS_REF 1	/* Minimum refresh time in s */

static void httpd_get_status_extra(FILE *f, httpd_req_t *req)
{
    char timebuf[64];
    size_t len;
    struct timeval tv;

    unsigned long statref;
    statref = Max(getvar_uint(config_http_status_refresh), MIN_STATUS_REF);

    if (httpd_req_get_url_query_str(req, timebuf, sizeof timebuf) == ESP_OK &&
	*timebuf) {
	char *ep;
	unsigned long newstatref = strtoul(timebuf, &ep, 10);
	if (!*ep && newstatref >= MIN_STATUS_REF && newstatref != statref) {
	    statref = newstatref;
	    setvar_uint(config_http_status_refresh, statref);
	    read_config(NULL, true); /* Save changed config */
	}
    }
    fprintf(f, "http.status.refresh=%lu\n", statref);

    fprintf(f, "TZ=%s\n", notempty(getenv("TZ")));

    gettimeofday(&tv,NULL);
    const struct tm *tm = localtime(&tv.tv_sec);
    len = strftime(timebuf, sizeof timebuf, "localtime=%Y-%m-%d %H:%M:%S.", tm);
    snprintf(timebuf+len, sizeof timebuf - len, "%06lu",
	     (unsigned long)tv.tv_usec);
    len += 3;
    len += strftime(timebuf+len, sizeof timebuf - len, " %z (%Z)\n", tm);
    fwrite(timebuf, 1, len, f);
}

static esp_err_t httpd_get_config_status(httpd_req_t *req, bool status)
{
    FILE *f = httpd_fopen_write(req);
    if (!f)
	return HTTP_ERR(req, 500, "Unable to get request handle");

    httpd_resp_set_type(req, text_plain);
    httpd_resp_set_hdr(req, "Cache-Control", "no-cache");

    if (status)
	httpd_get_status_extra(f, req);

    int rv = write_sysvars(f, status);
    fclose(f);
    return rv ? ESP_FAIL : ESP_OK;
}

static esp_err_t httpd_set_lang(httpd_req_t *req, const char *query)
{
    if (query) {
	int qlen = strlen(query);
	setvar_str(config_LANG, qlen && qlen <= MAX_LANG_LEN ? query : NULL);
	read_config(NULL, true);	/* Save configuration */
    }

    /*
     * 303 = "See other": proper return code saying "this is not what
     * you asked for, this is *about* what you asked for"; see spec
     * but this is exactly what we want here.
     */
    return httpd_send_plain(req, 303, NULL, 0, HSP_REFERER, 0);
}

static const char *get_lang(void)
{
    const char *lang = getvar_str(config_LANG);
    if (!lang)
	lang = fallback_language;
    return lang;
}

static esp_err_t httpd_get_lang(httpd_req_t *req)
{
    const char *lang = get_lang();
    return httpd_send_plain(req, 200, lang, strlen(lang), HSP_CRLF, 0);
}

static esp_err_t httpd_lang_redirect(httpd_req_t *req)
{
    char lang_buf[sizeof req->uri + MAX_LANG_LEN];
    int len = snprintf(lang_buf, sizeof lang_buf, "/lang/%s%s", get_lang(),
		       req->uri + (sizeof("/sys/lang")-1));

    return httpd_send_plain(req, 302, lang_buf, len, 0, 0);
}

#define STRING_MATCHES(str, len, what)					\
    (((len) == sizeof(what)-1) && !memcmp((str), (what), sizeof(what)-1))
#define STRING_MATCHES_PREFIX(str, len, what)				\
    (((len) >= sizeof(what)-1) && !memcmp((str), (what), sizeof(what)-1))

static esp_err_t httpd_sys_handler(httpd_req_t *req)
{
    httpd_print_request(req);

    if (req->method == HTTP_POST &&
	!httpd_req_get_hdr_value_len(req, "Content-Length")) {
	return HTTP_ERR(req, 411, "Length required");
    }

    const char *query = strchrnul(req->uri, '?');

    if (query < req->uri+5 || memcmp(req->uri, "/sys/", 5))
	return httpd_err_enoent(req); /* This should never happen */

    const char *file = req->uri + 5;
    size_t filelen = query - file;

    query = *query == '?' ? query+1 : NULL;

    if (STRING_MATCHES_PREFIX(file, filelen, "lang"))
	return httpd_lang_redirect(req);

    if (STRING_MATCHES(file, filelen, "getstatus"))
	return httpd_get_config_status(req, true);

    if (STRING_MATCHES(file, filelen, "getconfig"))
	return httpd_get_config_status(req, false);

    if (STRING_MATCHES(file, filelen, "fwupdate"))
	return httpd_firmware_update(req);

    if (STRING_MATCHES(file, filelen, "setconfig"))
	return httpd_set_config(req, query);

    if (STRING_MATCHES(file, filelen, "getlang"))
	return httpd_get_lang(req);

    if (STRING_MATCHES(file, filelen, "setlang"))
	return httpd_set_lang(req, query);

    if (STRING_MATCHES(file, filelen, "setboardrev"))
	return httpd_set_board_rev(req);

    return httpd_err_enoent(req);
}

INCBIN_EXTERN(wwwzip);

struct mime_type {
    const char *ext;
    uint16_t ext_len;
    uint16_t flags;
    const char *mime;
};

#define MT_CHARSET	1	/* Add charset to Content-Type */
#define MT_REDIR	2	/* It is a redirect */

static const struct mime_type mime_types[] = {
    { ".html",  5, MT_CHARSET, "text/html"                 },
    { ".xhtml", 6, MT_CHARSET, "text/html"                 },
    { ".css",   4, MT_CHARSET, "text/css"                  },
    { ".webp",  5, 0,          "image/webp"                },
    { ".jpg",   4, 0,          "image/jpeg"                },
    { ".png",   4, 0,          "image/png"                 },
    { ".ico",   4, 0,          "image/png"                 }, /* favicon.ico */
    { ".svg",   4, MT_CHARSET, "image/svg+xml"             },
    { ".otf",   4, 0,          "font/otf"                  },
    { ".ttf",   4, 0,          "font/ttf"                  },
    { ".woff",  5, 0,          "font/woff"                 },
    { ".woff2", 6, 0,          "font/woff2"                },
    { ".pdf",   4, 0,          "application/pdf"           },
    { ".js",    3, MT_CHARSET, "text/javascript"           },
    { ".mjs",   4, MT_CHARSET, "text/javascript"           },
    { ".json",  5, MT_CHARSET, "application/json"          },
    { ".xml",   4, MT_CHARSET, "text/xml"                  },
    { ".bin",   4, 0,          "application/octet-stream"  },
    { ".fw",    3, 0,          "application/octet-stream"  },
    { ".capt",  5, 0,          "application/captive+json"  },
    { "_redir", 6, MT_REDIR,   NULL                        },
    { NULL,     0, MT_CHARSET, "text/plain"                }  /* default */
};

static esp_err_t httpd_static_handler(httpd_req_t *req)
{
    size_t buffer_size = UNZ_BUFSIZE;
    const char *uri, *enduri;
    bool is_dir;
    char *buffer = NULL;
    ZIPFILE *zip = NULL;
    unzFile unz  = NULL;
    bool file_open = false;
    int err = 0;
    size_t len;

    httpd_print_request(req);

    uri = req->uri;
    while (*uri == '/')
	uri++;			/* Skip leading slashes */

    const char *first_slash = NULL, *last_slash = NULL;

    for (enduri = uri; *enduri; enduri++) {
	if (*enduri == '/') {
	    last_slash = enduri;
	    if (!first_slash)
		first_slash = enduri;
	}
    }
    if (enduri == uri) {
	is_dir = true;
    } else if (last_slash == enduri-1) {
	is_dir = true;
	enduri--;		/* Drop terminal slash */
	if (first_slash == last_slash)
	    first_slash = NULL;
    } else {
	is_dir = false;	/* Try the plain filename first */
    }

    const size_t filename_buffer_size =
	(enduri - uri) + 2 + sizeof redir_filename;

    if (buffer_size < filename_buffer_size)
	buffer_size = filename_buffer_size;

    buffer = malloc(buffer_size);
    zip = malloc(sizeof *zip);
    if (!buffer || !zip) {
	err = httpd_err_enomem(req);
	goto out;
    }

    char * const filebase = buffer + 1;
    char * const endbase  = mempcpy(filebase, uri, enduri - uri);
    filebase[-1] = '/';

    unz = unzOpen(NULL, (void *)gwwwzipData, gwwwzipSize,
		  zip, NULL, NULL, NULL, NULL);
    if (!unz) {
	MSG("[HTTP] unzOpen failed!\n");
	err = HTTP_ERR(req, 500, "Cannot open content archive");
	goto out;
    }

    char * const filename = filebase; /* Separate for future needs */
    char *endfile = endbase;

    unsigned int m;
    bool found = false;
    for (m = is_dir ? 2 : 0; m < 3; m++) {
	char *sx;
	switch (m) {
	default:		/* filename = /url */
	    endfile = endbase;
	    break;
	case 1:			/* filename = /url_redir */
	    sx = endbase;
	    endfile = mempcpy(sx, redir_filename, sizeof redir_filename) - 1;
	    break;
	case 2:			/* filename = /url/_redir */
	    sx = endbase - (endbase[-1] == '/');
	    *sx++ = '/';
	    endfile = mempcpy(sx, redir_filename, sizeof redir_filename) - 1;
	    break;
	}
	*endfile = '\0';

	if (!filename)
	    continue;

	filename[-1] = '/';
	MSG("trying to open: %s... ", filename);
	if (unzLocateFile(unz, filename, 1) == UNZ_OK) {
	    CMSG("found\n");
	    found = true;
	    break;
	} else {
	    CMSG("not found\n");
	}
    }

    if (!found) {
	err = httpd_err_enoent(req);
	goto out;
    }

    size_t filelen = endfile - filename;

    const struct mime_type *mime_type = mime_types;
    /* The default entry with length 0 will always match */
    while (mime_type->ext_len) {
	len = mime_type->ext_len;

	if (len <= filelen && !memcmp(endfile - len, mime_type->ext, len))
	    break;

	mime_type++;
    }

    MSG("found %s ext %s type %s\n",
	filename,
	mime_type->ext ? mime_type->ext : "(none)",
	mime_type->mime ? mime_type->mime : "(none)");

    unz_file_info fileinfo;
    memset(&fileinfo, 0, sizeof fileinfo);
    unzGetCurrentFileInfo(unz, &fileinfo, NULL, 0, NULL, 0, NULL, 0);

    MSG("len %u compressed %u\n",
	fileinfo.uncompressed_size,
	fileinfo.compressed_size);

    /*
     * Is it a redirect?
     */
    if (mime_type->flags & MT_REDIR) {
	if (fileinfo.uncompressed_size > buffer_size ||
	    unzOpenCurrentFile(unz) != UNZ_OK) {
	    err = HTTP_ERR(req, 500, "Cannot open file in archive");
	    goto out;
	}
	file_open = true;

	len = fileinfo.uncompressed_size;
	if (unzReadCurrentFile(unz, buffer, len) != len) {
	    err = ESP_ERR_HTTPD_RESULT_TRUNC;
	    goto out;
	}

	MSG("redirect: %.*s\n", (int)len, buffer);

	err = httpd_send_plain(req, 302, buffer, len, 0, 0);
	goto out;
    }

    /*
     * Hopefully the combination of date and CRC
     * is strong enough to quality for a "strong" ETag
     */
    char etag[16+3+1];
    snprintf(etag, sizeof etag, "\"%08lx:%08lx\"",
	     fileinfo.dosDate, fileinfo.crc);

    bool skip_body = req->method == HTTP_HEAD || !fileinfo.uncompressed_size;
    bool skip_meta = false;
    const char *response = "200 OK";

    if (httpd_req_get_hdr_value_str(req, "If-None-Match",
				    buffer, buffer_size) == ESP_OK &&
	strstr(buffer, etag)) {
	skip_body = skip_meta = true;
	response = "304 Not Modified";
    }

    len = snprintf(buffer, buffer_size-2,
		   "HTTP/1.1 %s\r\n"
		   "Date: %s\r\n"
		   "Cache-Control: max-age=10, immutable\r\n"
		   "ETag: %s\r\n",
		   response,
		   http_now(),
		   etag);

    if (len < buffer_size-2 && !skip_meta) {
	const char *mime_extra =
	    mime_type->flags & MT_CHARSET ? "; charset=\"UTF-8\"" : "";

	len += snprintf(buffer + len, buffer_size-2 - len,
			"Content-Type: %s%s\r\n"
			"Content-Length: %lu\r\n"
			"Allow: GET, HEAD\r\n"
			"Connection: close\r\n"
			"Last-Modified: %s\r\n",
			mime_type->mime, mime_extra,
			fileinfo.uncompressed_size,
			http_dos_date(fileinfo.dosDate));
    }

    if (len >= buffer_size-2) {
	err = HTTP_ERR(req, 500, "buffer_size too small");
	goto out;
    }

    buffer[len++] = '\r';
    buffer[len++] = '\n';

    err = httpd_send_all(req, buffer, len);
    if (skip_body || err) {
	/* No need to spend time uncompressing the file content */
	goto out;
    }

    if (unzOpenCurrentFile(unz) != UNZ_OK) {
	err = HTTP_ERR(req, 400, "Cannot open file in archive");
	goto out;
    }
    file_open = true;

    len = fileinfo.uncompressed_size;
    while (len) {
	size_t chunk = len;
	if (chunk > buffer_size)
	    chunk = buffer_size;

	if (unzReadCurrentFile(unz, buffer, chunk) != chunk) {
	    err = ESP_ERR_HTTPD_RESULT_TRUNC;
	    goto out;
	}

	err = httpd_send_all(req, buffer, chunk);
	if (err)
	    goto out;

	len -= chunk;
    }
    err = ESP_OK;		/* All good! */

out:
    if (file_open)
	unzCloseCurrentFile(unz);
    if (unz)
	unzClose(unz);
    if (zip)
	free(zip);
    if (buffer)
	free(buffer);

    return err;
}

/*
 * Match a URL against a path prefix. To keep httpd from refusing to
 * register subpaths, the root template does not include the leading
 * '/', but uri is required to have it. Do not include a trailing /
 * in prefix; it is implied.
 */
static bool httpd_uri_match_prefix(const char *template, const char *uri,
				   size_t len)
{
#if 0
    printf("[HTTP] matching URI \"%.*s\" against template \"%s\"\n",
	   len, uri, template);
#endif

    if (!len-- || *uri++ != '/')
	return false;

    /* Previous template character (leading '/' implied) */
    unsigned char tp = '/';

    while (1) {
	unsigned char t = *template++;
	unsigned char u;

	if (!len-- || !(u = *uri++)) {
	    return !t;
	} else if (!t) {
	    return tp == '/' || u == '/';
	} else if (t != u) {
	    return false;
	}
	tp = t;
    }
}

/* Do not include leading or trailing /; most specific prefix first */
static const httpd_uri_t uri_handlers[] = {
    {
	.uri      = "sys",
	.method   = HTTP_GET,
	.handler  = httpd_sys_handler,
	.user_ctx = NULL
    },
    {
	.uri      = "",
	.method   = HTTP_GET,
	.handler  = httpd_static_handler,
	.user_ctx = NULL
    },
    {
	.uri      = "",
	.method   = HTTP_HEAD,
	.handler  = httpd_static_handler,
	.user_ctx = NULL
    },
    {
	.uri      = "sys",
	.method   = HTTP_POST,
	.handler  = httpd_sys_handler,
	.user_ctx = NULL
    },
};

void my_httpd_stop(void)
{
    if (httpd) {
	esp_unregister_shutdown_handler(my_httpd_stop);
	httpd_stop(httpd);
	httpd = NULL;
    }
}

void my_httpd_start(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    httpd_handle_t server;

    if (httpd)
	return;

    config.task_priority    = HTTPD_PRIORITY;
    config.max_open_sockets = 10;

    printf("[HTTP] Default stack size: %zu\n", config.stack_size);
    config.stack_size <<= 2;
    printf("[HTTP] Requesting stack size: %zu\n", config.stack_size);

    config.uri_match_fn = httpd_uri_match_prefix;

    if (httpd_start(&server, &config) != ESP_OK)
	return;

    esp_register_shutdown_handler(my_httpd_stop);

    httpd = server;
    for (size_t i = 0; i < ARRAY_SIZE(uri_handlers); i++) {
	const httpd_uri_t * const handler = &uri_handlers[i];
	if (httpd_register_uri_handler(httpd, handler))
	    printf("[HTTP] failed to register URI handler: %s %s\n",
		   http_method_str(handler->method), handler->uri);
    }

    printf("[HTTP] httpd started\n");
}
