diff options
Diffstat (limited to 'ext/mwrap/httpd.h')
-rw-r--r-- | ext/mwrap/httpd.h | 1349 |
1 files changed, 1349 insertions, 0 deletions
diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h new file mode 100644 index 0000000..03aef9f --- /dev/null +++ b/ext/mwrap/httpd.h @@ -0,0 +1,1349 @@ +/* + * Copyright (C) mwrap hackers <mwrap-perl@80x24.org> + * License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt> + * + * Single-threaded multiplexing HTTP/1.x AF_UNIX server. + * Not using epoll|kqueue here since we don't want to be wasting another + * FD for a few clients. + * + * stdio (via open_memstream) is used for all vector management, + * thus everything is a `FILE *' + * + * Buffering is naive: write in full to a memstream to get an accurate + * Content-Length, then write out the header and sendmsg it off. + * I'm avoiding a streaming + lazy buffering design based on fopencookie(3) + * since that adds more complexity and uses icache. + * Supporting gzip would be nice, but linking zlib is not an option since + * there's a risk of conflicts if the application links against a different + * zlib version. posix_spawn+gzip isn't an option, either, since we don't + * want to generate intrusive SIGCHLD. + */ +#ifndef _DEFAULT_SOURCE +# define _DEFAULT_SOURCE +#endif +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/un.h> +#include <poll.h> +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <stdint.h> +#include <errno.h> +#include <string.h> +#include <math.h> +#include <urcu/list.h> +#include "picohttpparser.h" +#include "picohttpparser_c.h" +#include <pthread.h> +#include <stdbool.h> +#define URL "https://80x24.org/mwrap-perl.git/about" +#define TYPE_HTML "text/html; charset=UTF-8" +#define TYPE_CSV "text/csv" +#define TYPE_PLAIN "text/plain" + +enum mw_qev { + MW_QEV_IGNORE = 0, + MW_QEV_RD = POLLIN, + MW_QEV_WR = POLLOUT +}; + +struct mw_fbuf { + char *ptr; + size_t len; + FILE *fp; +}; + +struct mw_wbuf { /* for response headers + bodies */ + struct iovec iov[2]; + unsigned iov_nr; + unsigned iov_written; + char bytes[]; +}; + +#define MW_RBUF_SIZE 8192 +#define MW_NR_NAME 8 +struct mw_h1req { /* HTTP/1.x request (TSD in common (fast) case) */ + const char *method, *path, *qstr; + size_t method_len, path_len, qlen; + uint16_t rbuf_len; /* capped by MW_RBUF_SIZE */ + int pret, minor_ver; + size_t nr_hdr; + struct phr_header hdr[MW_NR_NAME]; + char rbuf[MW_RBUF_SIZE]; /* read(2) in to this */ +}; + +struct mw_h1 { /* each HTTP/1.x client (heap) */ + int fd; + short events; /* for poll */ + unsigned prev_len:13; /* capped by MW_RBUF_SIZE */ + unsigned has_input:1; + unsigned unused_:2; + struct mw_h1req *h1r; /* only for slow clients */ + unsigned long in_len; + struct mw_wbuf *wbuf; + struct cds_list_head nd; /* <=> mw_h1d.conn */ +}; + +struct mw_h1d { /* the daemon + listener, a singleton */ + int lfd; + uint8_t alive; /* set by parent */ + uint8_t running; /* cleared by child */ + struct cds_list_head conn; /* <=> mw_h1.nd */ + /* use open_memstream + fwrite to implement a growing pollfd array */ + struct mw_fbuf pb; /* pollfd vector */ + pthread_t tid; + struct mw_h1req *shared_h1r; /* shared by all fast clients */ + size_t pid_len; + char pid_str[10]; +}; + +union mw_sockaddr { /* cast-avoiding convenience :> */ + struct sockaddr_un un; + struct sockaddr any; +}; + +static struct mw_h1d g_h1d = { .lfd = -1 }; + +/* sortable snapshot version of struct src_loc */ +struct h1_src_loc { + double mean_life; + size_t bytes; + size_t allocations; + size_t frees; + size_t live; + size_t max_life; + off_t lname_len; + const struct src_loc *sl; + char *loc_name; +}; + +/* sort numeric stuff descending */ +#define CMP_FN(F) static int cmp_##F(const void *x, const void *y) \ +{ \ + const struct h1_src_loc *a = x, *b = y; \ + if (a->F < b->F) return 1; \ + return (a->F > b->F) ? -1 : 0; \ +} +CMP_FN(bytes) +CMP_FN(allocations) +CMP_FN(frees) +CMP_FN(live) +CMP_FN(max_life) +CMP_FN(mean_life) +#undef CMP_FN + +static int cmp_location(const void *x, const void *y) +{ + const struct h1_src_loc *a = x, *b = y; + return strcmp(a->loc_name, b->loc_name); +} + +/* fields for /each/$MIN{,.csv} endpoints */ +struct h1_tbl { + const char *fname; + size_t flen; + int (*cmp)(const void *, const void *); +} fields[] = { +#define F(n) { #n, sizeof(#n) - 1, cmp_##n } + F(bytes), + F(allocations), + F(frees), + F(live), + F(mean_life), + F(max_life), + F(location) +#undef F +}; + +static enum mw_qev h1_close(struct mw_h1 *h1) +{ + mwrap_assert(h1->fd >= 0); + cds_list_del(&h1->nd); /* drop from h1d->conn */ + close(h1->fd); + free(h1->wbuf); + free(h1->h1r); + free(h1); + return MW_QEV_IGNORE; +} + +static enum mw_qev h1_400(struct mw_h1 *h1) +{ + /* best-effort response, so no checking send() */ + static const char r400[] = "HTTP/1.1 400 Bad Request\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 12\r\n" + "Connection: close\r\n\r\n" "Bad Request\n"; + (void)send(h1->fd, r400, sizeof(r400) - 1, MSG_NOSIGNAL); + return h1_close(h1); +} + +static enum mw_qev h1_send_flush(struct mw_h1 *h1) +{ + struct mw_wbuf *wbuf = h1->wbuf; + struct msghdr mh = { 0 }; + + free(h1->h1r); + h1->h1r = NULL; + + mh.msg_iov = wbuf->iov + wbuf->iov_written; + mh.msg_iovlen = wbuf->iov_nr; + do { + ssize_t w = sendmsg(h1->fd, &mh, MSG_NOSIGNAL); + if (w < 0) + return errno == EAGAIN ? MW_QEV_WR : h1_close(h1); + if (w == 0) + return h1_close(h1); + while (w > 0) { + if ((size_t)w >= mh.msg_iov->iov_len) { + w -= mh.msg_iov->iov_len; + ++mh.msg_iov; + --mh.msg_iovlen; + ++wbuf->iov_written; + --wbuf->iov_nr; + } else { + uintptr_t x = (uintptr_t)mh.msg_iov->iov_base; + mh.msg_iov->iov_base = (void *)(x + w); + mh.msg_iov->iov_len -= w; + w = 0; + } + } + } while (mh.msg_iovlen); + return h1_close(h1); +} + +static FILE *fbuf_init(struct mw_fbuf *fb) +{ + fb->ptr = NULL; + fb->fp = open_memstream(&fb->ptr, &fb->len); + if (!fb->fp) fprintf(stderr, "open_memstream: %m\n"); + return fb->fp; +} + +static FILE *wbuf_init(struct mw_fbuf *fb) +{ + static const struct mw_wbuf pad; + if (fbuf_init(fb)) /* pad space is populated before h1_send_flush */ + fwrite(&pad, 1, sizeof(pad), fb->fp); + return fb->fp; +} + +static int fbuf_close(struct mw_fbuf *fb) +{ + int e = ferror(fb->fp) | fclose(fb->fp); + fb->fp = NULL; + if (e) fprintf(stderr, "ferror|fclose: %m\n"); + return e; +} + +/* supported by modern gcc + clang */ +#define AUTO_CLOFREE __attribute__((__cleanup__(cleanup_clofree))) +static void cleanup_clofree(void *ptr) +{ + struct mw_fbuf *fb = ptr; + if (fb->fp) fclose(fb->fp); + free(fb->ptr); +} + +static enum mw_qev h1_res_oneshot(struct mw_h1 *h1, const char *buf, size_t len) +{ + struct mw_fbuf fb; + + if (!wbuf_init(&fb)) + return h1_close(h1); + + fwrite(buf, 1, len, fb.fp); + if (fbuf_close(&fb)) + return h1_close(h1); + + /* fill in the zero padding we added at wbuf_init */ + mwrap_assert(!h1->wbuf); + struct mw_wbuf *wbuf = h1->wbuf = (struct mw_wbuf *)fb.ptr; + wbuf->iov_nr = 1; + wbuf->iov[0].iov_len = fb.len - sizeof(*wbuf); + wbuf->iov[0].iov_base = wbuf->bytes; + return h1_send_flush(h1); +} + +#define FPUTS(STR, fp) fwrite(STR, sizeof(STR) - 1, 1, fp) +static enum mw_qev h1_200(struct mw_h1 *h1, struct mw_fbuf *fb, const char *ct) +{ + /* + * the HTTP header goes at the END of the body buffer, + * we'll rely on iovecs via sendmsg(2) to reorder and clamp it + */ + off_t clen = ftello(fb->fp); + if (clen < 0) { + fprintf(stderr, "ftello: %m\n"); + fbuf_close(fb); + return h1_close(h1); + } + clen -= sizeof(struct mw_wbuf); + mwrap_assert(clen >= 0); + FPUTS("HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Expires: Fri, 01 Jan 1980 00:00:00 GMT\r\n" + "Pragma: no-cache\r\n" + "Cache-Control: no-cache, max-age=0, must-revalidate\r\n" + "Content-Type: ", fb->fp); + fprintf(fb->fp, "%s\r\nContent-Length: %zu\r\n\r\n", ct, (size_t)clen); + + if (fbuf_close(fb)) + return h1_close(h1); + + /* fill in the zero-padding we added at wbuf_init */ + mwrap_assert(!h1->wbuf); + struct mw_wbuf *wbuf = h1->wbuf = (struct mw_wbuf *)fb->ptr; + wbuf->iov_nr = 2; + wbuf->iov[0].iov_len = fb->len - ((size_t)clen + sizeof(*wbuf)); + wbuf->iov[0].iov_base = wbuf->bytes + (size_t)clen; + wbuf->iov[1].iov_len = clen; + wbuf->iov[1].iov_base = wbuf->bytes; + return h1_send_flush(h1); +} + +static enum mw_qev h1_404(struct mw_h1 *h1) +{ + static const char r404[] = "HTTP/1.1 404 Not Found\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n" + "Content-Length: 10\r\n\r\n" "Not Found\n"; + return h1_res_oneshot(h1, r404, sizeof(r404) - 1); +} + +#define NAME_EQ(h, NAME) name_eq(h, NAME, sizeof(NAME)-1) +static int name_eq(const struct phr_header *h, const char *name, size_t len) +{ + return h->name_len == len && !strncasecmp(name, h->name, len); +} + +static enum mw_qev h1_do_reset(struct mw_h1 *h1) +{ + static const char r200[] = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Connection: close\r\n" + "Content-Length: 6\r\n\r\n" "reset\n"; + mwrap_reset(); + return h1_res_oneshot(h1, r200, sizeof(r200) - 1); +} + +static enum mw_qev h1_do_trim(struct mw_h1 *h1) +{ + static const char r200[] = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Connection: close\r\n" + "Content-Length: 9\r\n\r\n" "trimming\n"; + malloc_trim(0); + return h1_res_oneshot(h1, r200, sizeof(r200) - 1); +} + +static enum mw_qev h1_do_ctl_finish(struct mw_h1 *h1) +{ + struct mw_fbuf plain; + FILE *fp = wbuf_init(&plain); + if (!fp) return h1_close(h1); + fprintf(fp, "MWRAP=bt:%u\n", (unsigned)CMM_LOAD_SHARED(bt_req_depth)); + return h1_200(h1, &plain, TYPE_PLAIN); +} + +#define PATH_SKIP(h1r, pfx) path_skip(h1r, pfx, sizeof(pfx) - 1) +static const char *path_skip(struct mw_h1req *h1r, const char *pfx, size_t len) +{ + if (h1r->path_len > len && !memcmp(pfx, h1r->path, len)) + return h1r->path + len; + return NULL; +} + +static void write_html(FILE *fp, const char *s, size_t len) +{ + for (; len--; ++s) { + switch (*s) { + case '&': FPUTS("&", fp); break; + case '<': FPUTS("<", fp); break; + case '>': FPUTS(">", fp); break; + case '"': FPUTS(""", fp); break; + case '\'': FPUTS("'", fp); break; + case '\n': FPUTS("<br>", fp); break; + default: fputc(*s, fp); + } + } +} + +/* + * quotes multi-line backtraces for CSV (and `\' and `"' in case + * we encounter nasty file names). + */ +static void write_q_csv(FILE *fp, const char *s, size_t len) +{ + fputc('"', fp); + for (; len--; ++s) { + switch (*s) { + case '\n': fputs("\\n", fp); break; + case '\\': fputs("\\\\", fp); break; + case '"': fputs("\\\"", fp); break; + default: fputc(*s, fp); + } + } + fputc('"', fp); +} + + +/* URI-safe base-64 (RFC 4648) */ +static void write_b64_url(FILE *fp, const uint8_t *in, size_t len) +{ + static const uint8_t b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" "0123456789-_"; + uint8_t o[4]; + while (len > 3) { + o[0] = b64[in[0] >> 2]; + o[1] = b64[((in[0] << 4) | (in[1] >> 4)) & 0x3f]; + o[2] = b64[((in[1] << 2) | (in[2] >> 6)) & 0x3f]; + o[3] = b64[in[2] & 0x3f]; + fwrite(o, sizeof(o), 1, fp); + len -= 3; + in += 3; + } + if (len) { + size_t i = 2; + + o[0] = b64[in[0] >> 2]; + o[1] = b64[((in[0] << 4) | (--len ? (in[1] >> 4) : 0)) & 0x3f]; + if (len) + o[i++] = b64[((in[1] << 2) | + (--len ? in[2] >> 6 : 0)) & 0x3f]; + if (len) + o[i++] = b64[in[2] & 0x3f]; + fwrite(o, i, 1, fp); + } +} + +/* unescapes @s in-place and adjusts @len */ +static bool b64_url_decode(const void *ptr, size_t *len) +{ + union { const void *in; uint8_t *out; } deconst; + const uint8_t *in = ptr; + uint8_t u = 0; + + deconst.in = ptr; + uint8_t *out = deconst.out; + + for (size_t i = 0; i < *len; ++i) { + uint8_t c = in[i]; + + switch (c) { + case 'A' ... 'Z': c -= 'A'; break; + case 'a' ... 'z': c -= ('a' - 26); break; + case '0' ... '9': c -= ('0' - 52); break; + case '-': c = 62; break; + case '_': c = 63; break; + default: return false; + } + + mwrap_assert(c <= 63); + switch (i % 4) { + case 0: u = c << 2; break; + case 1: + *out++ = u | c >> 4; + u = c << 4; + break; + case 2: + *out++ = u | c >> 2; + u = c << 6; + break; + case 3: *out++ = u | c; + } + } + *len = out - in; + return true; +} + +/* keep this consistent with Mwrap.xs location_string */ +static off_t write_loc_name(FILE *fp, const struct src_loc *l) +{ + off_t beg = ftello(fp); + + if (beg < 0) { + fprintf(stderr, "ftello: %m\n"); + return beg; + } + if (l->f) { + fputs(l->f->fn, fp); + if (l->lineno == U24_MAX) + FPUTS(":-", fp); + else + fprintf(fp, ":%u", l->lineno); + } + if (l->bt_len) { + AUTO_FREE char **s = bt_syms(l->bt, l->bt_len); + if (!s) return -1; + if (l->f) fputc('\n', fp); + + /* omit local " [RETURN_ADDRESS]" if doing deep backtraces */ + for (uint32_t i = 0; i < l->bt_len; ++i) { + char *c = memrchr(s[i], '[', strlen(s[i])); + if (c && c > (s[i] + 2) && c[-1] == ' ') + c[-1] = '\0'; + } + + fputs(s[0], fp); + for (uint32_t i = 1; i < l->bt_len; ++i) { + fputc('\n', fp); + fputs(s[i], fp); + } + } + off_t end = ftello(fp); + if (end < 0) { + fprintf(stderr, "ftello: %m\n"); + return end; + } + return end - beg; +} + +static struct h1_src_loc *accumulate(unsigned long min, size_t *hslc, FILE *lp) +{ + struct mw_fbuf fb; + if (!fbuf_init(&fb)) return NULL; + rcu_read_lock(); + struct cds_lfht *t = CMM_LOAD_SHARED(totals); + struct cds_lfht_iter iter; + struct src_loc *l; + if (t) cds_lfht_for_each_entry(t, &iter, l, hnode) { + size_t freed = uatomic_read(&l->freed_bytes); + size_t total = uatomic_read(&l->total); + struct h1_src_loc hsl; + + if (total < min) continue; + hsl.bytes = total - freed; + hsl.allocations = uatomic_read(&l->allocations); + hsl.frees = uatomic_read(&l->frees); + hsl.live = hsl.allocations - hsl.frees; + hsl.mean_life = hsl.frees ? + ((double)uatomic_read(&l->age_total) / + (double)hsl.frees) : + HUGE_VAL; + hsl.max_life = uatomic_read(&l->max_lifespan); + hsl.sl = l; + hsl.lname_len = write_loc_name(lp, l); + fwrite(&hsl, sizeof(hsl), 1, fb.fp); + } + rcu_read_unlock(); + + struct h1_src_loc *hslv; + if (fbuf_close(&fb)) { + hslv = NULL; + } else { + *hslc = fb.len / sizeof(*hslv); + mwrap_assert((fb.len % sizeof(*hslv)) == 0); + hslv = (struct h1_src_loc *)fb.ptr; + } + return hslv; +} + +static void show_stats(FILE *fp) +{ + size_t dec = uatomic_read(&total_bytes_dec); + size_t inc = uatomic_read(&total_bytes_inc); + fprintf(fp, "<p>Current age: %zu (live: %zu) " + "/ files: %zu / locations: %zu", + inc , inc - dec, + uatomic_read(&nr_file), uatomic_read(&nr_src_loc)); +} + +/* /$PID/at/$LOCATION endpoint */ +static enum mw_qev each_at(struct mw_h1 *h1, struct mw_h1req *h1r) +{ + const char *loc = h1r->path + sizeof("/at/") - 1; + size_t len = h1r->path_len - (sizeof("/at/") - 1); + size_t min = 0; + + if (!b64_url_decode(loc, &len) || len >= PATH_MAX) + return h1_400(h1); + + struct src_loc *l = mwrap_get_bin(loc, len); + + if (!l) return h1_404(h1); + + AUTO_CLOFREE struct mw_fbuf lb; + if (!fbuf_init(&lb)) return h1_close(h1); + if (write_loc_name(lb.fp, l) < 0) return h1_close(h1); + if (fbuf_close(&lb)) + return h1_close(h1); + + struct mw_fbuf html; + FILE *fp = wbuf_init(&html); + if (!fp) return h1_close(h1); + FPUTS("<html><head><title>", fp); + write_html(fp, lb.ptr, lb.len); + FPUTS("</title></head><body><p>live allocations at:", fp); + if (l->bt_len > 1 || (l->bt_len == 1 && l->f)) FPUTS("<br/>", fp); + else fputc(' ', fp); + write_html(fp, lb.ptr, lb.len); + + show_stats(fp); + FPUTS("<table><tr><th>size</th><th>generation</th>" + "<th>address</th></tr>", fp); + + rcu_read_lock(); + struct alloc_hdr *h; + cds_list_for_each_entry_rcu(h, &l->allocs, anode) { + size_t size = uatomic_read(&h->size); + if (size > min) + fprintf(fp, "<tr><td>%zu</td><td>%zu</td><td>%p</td>\n", + size, h->as.live.gen, h->real); + } + rcu_read_unlock(); + FPUTS("</table></body></html>", fp); + return h1_200(h1, &html, TYPE_HTML); +} + +/* /$PID/each/$MIN endpoint */ +static enum mw_qev each_gt(struct mw_h1 *h1, struct mw_h1req *h1r, + unsigned long min, bool csv) +{ + static const char default_sort[] = "bytes"; + const char *sort = default_sort; + size_t sort_len = sizeof(default_sort) - 1; + + if (h1r->qstr && h1r->qlen > 5 && !memcmp(h1r->qstr, "sort=", 5)) { + sort = h1r->qstr + 5; + sort_len = h1r->qlen - 5; + } + + size_t hslc; + AUTO_CLOFREE struct mw_fbuf lb; + if (!fbuf_init(&lb)) return h1_close(h1); + AUTO_FREE struct h1_src_loc *hslv = accumulate(min, &hslc, lb.fp); + if (!hslv) + return h1_close(h1); + + if (fbuf_close(&lb)) + return h1_close(h1); + + char *n = lb.ptr; + for (size_t i = 0; i < hslc; ++i) { + hslv[i].loc_name = n; + n += hslv[i].lname_len; + if (hslv[i].lname_len < 0) + return h1_close(h1); + } + + struct mw_fbuf bdy; + FILE *fp = wbuf_init(&bdy); + if (!fp) return h1_close(h1); + + if (!csv) { + unsigned depth = (unsigned)CMM_LOAD_SHARED(bt_req_depth); + fprintf(fp, "<html><head><title>mwrap each >%lu" + "</title></head><body><p>mwrap each >%lu " + "(change `%lu' in URL to adjust filtering) - " + "MWRAP=bt:%u", min, min, min, depth); + show_stats(fp); + /* need borders to distinguish multi-level traces */ + if (depth) + FPUTS("<table\nborder=1><tr>", fp); + else /* save screen space if only tracing one line */ + FPUTS("<table><tr>", fp); + } + + int (*cmp)(const void *, const void *) = NULL; + if (csv) { + for (size_t i = 0; i < CAA_ARRAY_SIZE(fields); i++) { + const char *fn = fields[i].fname; + if (i) + fputc(',', fp); + fputs(fn, fp); + if (fields[i].flen == sort_len && + !memcmp(fn, sort, sort_len)) + cmp = fields[i].cmp; + } + fputc('\n', fp); + } else { + for (size_t i = 0; i < CAA_ARRAY_SIZE(fields); i++) { + const char *fn = fields[i].fname; + FPUTS("<th>", fp); + if (fields[i].flen == sort_len && + !memcmp(fn, sort, sort_len)) { + cmp = fields[i].cmp; + fprintf(fp, "<b>%s</b>", fields[i].fname); + } else { + fprintf(fp, "<a\nhref=\"./%lu?sort=%s\">%s</a>", + min, fn, fn); + } + FPUTS("</th>", fp); + } + } + if (!csv) + FPUTS("</tr>", fp); + if (cmp) + qsort(hslv, hslc, sizeof(*hslv), cmp); + else if (!csv) + FPUTS("<tr><td>sort= not understood</td></tr>", fp); + if (csv) { + for (size_t i = 0; i < hslc; i++) { + struct h1_src_loc *hsl = &hslv[i]; + + fprintf(fp, "%zu,%zu,%zu,%zu,%0.3f,%zu,", + hsl->bytes, hsl->allocations, hsl->frees, + hsl->live, hsl->mean_life, hsl->max_life); + write_q_csv(fp, hsl->loc_name, hsl->lname_len); + fputc('\n', fp); + } + } else { + for (size_t i = 0; i < hslc; i++) { + struct h1_src_loc *hsl = &hslv[i]; + + fprintf(fp, "<tr><td>%zu</td><td>%zu</td><td>%zu</td>" + "<td>%zu</td><td>%0.3f</td><td>%zu</td>", + hsl->bytes, hsl->allocations, hsl->frees, + hsl->live, hsl->mean_life, hsl->max_life); + FPUTS("<td><a\nhref=\"../at/", fp); + + write_b64_url(fp, src_loc_hash_tip(hsl->sl), + src_loc_hash_len(hsl->sl)); + + FPUTS("\">", fp); + write_html(fp, hsl->loc_name, hsl->lname_len); + FPUTS("</a></td></tr>", fp); + } + FPUTS("</table></body></html>", fp); + } + return h1_200(h1, &bdy, csv ? TYPE_CSV : TYPE_HTML); +} + +/* /$PID/ root endpoint */ +static enum mw_qev pid_root(struct mw_h1 *h1, struct mw_h1req *h1r) +{ + struct mw_fbuf html; + FILE *fp = wbuf_init(&html); + if (!fp) return h1_close(h1); +#define default_min "2000" + + int pid = (int)getpid(); + fprintf(fp, "<html><head><title>mwrap PID:%d</title></head><body>" + "<pre>mwrap PID:%d", pid, pid); + show_stats(fp); + FPUTS("\n\n<a\nhref=\"each/" default_min "\">allocations >" + default_min " bytes</a>""</pre><pre\nid=help>" +"To get source file and line info for native backtraces, consult your\n" +"distro for -dbg, -dbgsym, or -debug packages.\n" +"And/or rebuild your code with debug flags (e.g. `-ggdb3' if using gcc)\n" +"and don't strip the resulting binaries.\n" +"You should see locations from the backtrace_symbols(3) function\n" +"in the form of FILENAME(+OFFSET) or FILENAME(SYMBOL+OFFSET)\n" +"(e.g. /usr/lib/foo.so(+0xdead) or /usr/lib/foo.so(func+(0xbeef))\n" +"\n" +"Any version of addr2line should decode FILENAME(+OFFSET) locations:\n" +"\n" +" addr2line -e FILENAME OFFSET\n" +"\n" +"SYMBOL+OFFSET requires addr2line from GNU binutils 2.39+ (Aug 2022):\n" +"\n" +" addr2line -e FILENAME SYMBOL+OFFSET\n", fp); + + FPUTS("\n<a\nhref=\"" URL "\">" URL "</a></pre></body></html>", fp); + return h1_200(h1, &html, TYPE_HTML); +#undef default_min +} + +/* @e is not NUL-terminated */ +static bool sfx_eq(const char *e, const char *sfx) +{ + for (const char *m = sfx; *m; m++, e++) + if (*e != *m) + return false; + return true; +} + +static enum mw_qev h1_dispatch(struct mw_h1 *h1, struct mw_h1req *h1r) +{ + if (h1r->method_len == 3 && !memcmp(h1r->method, "GET", 3)) { + const char *c; + + if ((c = PATH_SKIP(h1r, "/each/"))) { + errno = 0; + char *e; + unsigned long min = strtoul(c, &e, 10); + if (!errno) { + if (*e == ' ' || *e == '?') + return each_gt(h1, h1r, min, false); + if (sfx_eq(e, ".csv") && + (e[4] == ' ' || e[4] == '?')) + return each_gt(h1, h1r, min, true); + } + } else if ((PATH_SKIP(h1r, "/at/"))) { + return each_at(h1, h1r); + } else if (h1r->path_len == 1 && h1r->path[0] == '/') { + return pid_root(h1, h1r); + } + } else if (h1r->method_len == 4 && !memcmp(h1r->method, "POST", 4)) { + if (h1r->path_len == 6 && !memcmp(h1r->path, "/reset", 6)) + return h1_do_reset(h1); + if (h1r->path_len == 5 && !memcmp(h1r->path, "/trim", 5)) + return h1_do_trim(h1); + if (h1r->path_len == 4 && !memcmp(h1r->path, "/ctl", 4)) + return h1_do_ctl_finish(h1); + } + return h1_404(h1); +} + +static void +prep_trickle(struct mw_h1 *h1, struct mw_h1req *h1r, struct mw_h1d *h1d) +{ + if (h1->h1r) return; /* already trickling */ + h1->h1r = h1r; + mwrap_assert(h1d->shared_h1r == h1r); + h1d->shared_h1r = NULL; +} + +/* + * nothing in the PSGI app actually reads input, but clients tend + * to send something in the body of POST requests anyways, so we + * just drain it + */ +static enum mw_qev h1_drain_input(struct mw_h1 *h1, struct mw_h1req *h1r, + struct mw_h1d *h1d) +{ + if (h1r) { /* initial */ + ssize_t overread = h1r->rbuf_len - h1r->pret; + mwrap_assert(overread >= 0); + if ((size_t)overread <= h1->in_len) + h1->in_len -= overread; + else /* pipelining not supported */ + return h1_400(h1); + } else { /* continue dealing with a trickle */ + h1r = h1->h1r; + mwrap_assert(h1r); + } + while (h1->in_len > 0) { + char ibuf[BUFSIZ]; + size_t len = h1->in_len; + ssize_t r; + + mwrap_assert(h1->has_input); + if (len > sizeof(ibuf)) + len = sizeof(ibuf); + + r = read(h1->fd, ibuf, len); + if (r > 0) { /* just discard the input */ + h1->in_len -= r; + } else if (r == 0) { + return h1_close(h1); + } else { + switch (errno) { + case EAGAIN: + prep_trickle(h1, h1r, h1d); + return MW_QEV_RD; + case ECONNRESET: /* common */ + case ENOTCONN: + return h1_close(h1); + default: /* ENOMEM, ENOBUFS, ... */ + assert(errno != EBADF); + fprintf(stderr, "read: %m\n"); + return h1_close(h1); + } + } + } + h1->has_input = 0; /* all done with input */ + return h1_dispatch(h1, h1r); +} + +static bool valid_end(const char *end) +{ + switch (*end) { + case '\r': case ' ': case '\t': case '\n': return true; + default: return false; + } +} + +/* no error reporting, too much code */ +static void ctl_set(struct mw_h1 *h1, long n) +{ + if (n >= 0) { + if (n > MWRAP_BT_MAX) + n = MWRAP_BT_MAX; + CMM_STORE_SHARED(bt_req_depth, (uint32_t)n); + } +} + +static enum mw_qev h1_parse_harder(struct mw_h1 *h1, struct mw_h1req *h1r, + struct mw_h1d *h1d) +{ + enum { HDR_IGN, HDR_XENC, HDR_CLEN } cur = HDR_IGN; + char *end; + struct phr_header *hdr = h1r->hdr; + long depth = -1; + + h1->prev_len = 0; + h1->has_input = 0; + h1->in_len = 0; + + for (hdr = h1r->hdr; h1r->nr_hdr--; hdr++) { + if (NAME_EQ(hdr, "Transfer-Encoding")) + cur = HDR_XENC; + else if (NAME_EQ(hdr, "Content-Length")) + cur = HDR_CLEN; + else if (NAME_EQ(hdr, "Trailer")) + return h1_400(h1); + else if (hdr->name) { + cur = HDR_IGN; + /* + * don't want to increase code to deal with POST + * request bodies, so let pico handle parameters in + * HTTP request headers, instead. + */ + if (NAME_EQ(hdr, "X-Mwrap-BT-Depth")) { + errno = 0; + depth = strtol(hdr->value, &end, 10); + if (errno || !valid_end(end)) + depth = -1; + } + } + + /* else: continuation line */ + if (!hdr->value_len) + continue; + switch (cur) { + case HDR_XENC: + return h1_400(h1); + case HDR_CLEN: + if (h1->has_input) return h1_400(h1); + h1->has_input = 1; + errno = 0; + h1->in_len = strtoul(hdr->value, &end, 10); + if (errno || !valid_end(end)) + return h1_400(h1); + break; + case HDR_IGN: + break; + } + } + if (h1r->path_len < (g_h1d.pid_len + 2)) + return h1_404(h1); + + /* skip "/$PID" prefix */ + if (*h1r->path == '/' && + !memcmp(h1r->path+1, g_h1d.pid_str, g_h1d.pid_len) && + h1r->path[1 + g_h1d.pid_len] == '/') { + h1r->path += 1 + g_h1d.pid_len; + h1r->path_len -= 1 + g_h1d.pid_len; + } else { + return h1_404(h1); + } + + /* + * special case for /ctl, since I don't feel like parsing queries + * in the request body (ensure no query string, too) + */ + if (h1r->method_len == 4 && !memcmp(h1r->method, "POST", 4)) { + if (h1r->path_len == 4 && !memcmp(h1r->path, "/ctl", 4)) + ctl_set(h1, depth); + } + + /* break off QUERY_STRING */ + h1r->qstr = memchr(h1r->path, '?', h1r->path_len); + if (h1r->qstr) { + ++h1r->qstr; /* ignore '?' */ + h1r->qlen = h1r->path + h1r->path_len - h1r->qstr; + h1r->path_len -= (h1r->qlen + 1); + } + return h1_drain_input(h1, h1r, h1d); +} + +static enum mw_qev h1_event_step(struct mw_h1 *h1, struct mw_h1d *h1d) +{ + struct mw_h1req *h1r; + + /* + * simple rule to avoid trivial DoS in HTTP/1.x: never process a + * new request until you've written out your previous response + * (and this is why I'm too stupid to do HTTP/2) + */ + if (h1->wbuf) + return h1_send_flush(h1); + + if (h1->has_input) + return h1_drain_input(h1, NULL, h1d); + /* + * The majority of requests can be served using per-daemon rbuf, + * no need for per-client allocations unless a client trickles + */ + h1r = h1->h1r ? h1->h1r : h1d->shared_h1r; + if (!h1r) { + h1r = h1d->shared_h1r = malloc(sizeof(*h1r)); + if (!h1r) { + fprintf(stderr, "h1r malloc: %m\n"); + return h1_close(h1); + } + } + for (;;) { + size_t n = MW_RBUF_SIZE - h1->prev_len; + ssize_t r = read(h1->fd, &h1r->rbuf[h1->prev_len], n); + + if (r > 0) { + h1r->rbuf_len = h1->prev_len + r; + h1r->nr_hdr = MW_NR_NAME; + h1r->pret = phr_parse_request(h1r->rbuf, h1r->rbuf_len, + &h1r->method, &h1r->method_len, + &h1r->path, &h1r->path_len, + &h1r->minor_ver, h1r->hdr, + &h1r->nr_hdr, h1->prev_len); + if (h1r->pret > 0) + return h1_parse_harder(h1, h1r, h1d); + if (h1r->pret == -1) + return h1_400(h1); /* parser error */ + + mwrap_assert(h1r->pret == -2); /* incomplete */ + mwrap_assert(h1r->rbuf_len <= MW_RBUF_SIZE && + "bad math"); + + /* this should be 413 or 414, don't need the bloat */ + if (h1r->rbuf_len == MW_RBUF_SIZE) + return h1_400(h1); + mwrap_assert(h1r->rbuf_len < MW_RBUF_SIZE); + h1->prev_len = h1r->rbuf_len; + /* loop again */ + } else if (r == 0) { + return h1_close(h1); + } else { /* r < 0 */ + switch (errno) { + case EAGAIN: /* likely, detach to per-client buffer */ + if (h1->prev_len) + prep_trickle(h1, h1r, h1d); + return MW_QEV_RD; + case ECONNRESET: /* common */ + case ENOTCONN: + return h1_close(h1); + default: /* ENOMEM, ENOBUFS, ... */ + assert(errno != EBADF); + fprintf(stderr, "read: %m\n"); + return h1_close(h1); + } + } + } + + return MW_QEV_RD; +} + +static int poll_add(struct mw_h1d *h1d, int fd, short events) +{ + struct pollfd pfd; + + if (!h1d->pb.fp && !fbuf_init(&h1d->pb)) + return -1; + pfd.fd = fd; + pfd.events = events; + fwrite(&pfd, 1, sizeof(pfd), h1d->pb.fp); + return 0; /* success */ +} + +static struct pollfd *poll_detach(struct mw_h1d *h1d, nfds_t *nfds) +{ + struct pollfd *pfd = NULL; /* our return value */ + + /* not sure how to best recover from ENOMEM errors in stdio */ + if (h1d->pb.fp) { + if (fbuf_close(&h1d->pb)) { + exit(EXIT_FAILURE); + } else { + mwrap_assert(h1d->pb.len % sizeof(*pfd) == 0); + pfd = (struct pollfd *)h1d->pb.ptr; + *nfds = h1d->pb.len / sizeof(*pfd); + } + } + + /* prepare a new poll buffer the next loop */ + memset(&h1d->pb, 0, sizeof(h1d->pb)); + + return pfd; +} + +static void non_fatal_pause(const char *fail_fn) +{ + fprintf(stderr, "%s: %m (non-fatal, pausing mwrap-httpd)\n", fail_fn); + poll(NULL, 0, 1000); +} + +static void h1d_event_step(struct mw_h1d *h1d) +{ + union mw_sockaddr sa; + const char *fail_fn = NULL; + + while (!fail_fn) { + socklen_t len = (socklen_t)sizeof(sa); + int fd = accept4(h1d->lfd, &sa.any, &len, + SOCK_NONBLOCK|SOCK_CLOEXEC); + + if (fd >= 0) { + struct mw_h1 *h1 = calloc(1, sizeof(*h1)); + + if (h1) { + h1->fd = fd; + h1->events = POLLIN; + cds_list_add_tail(&h1->nd, &h1d->conn); + } else { + int err = errno; + fail_fn = "malloc"; + close(fd); + errno = err; + } + } else { + switch (errno) { + case EAGAIN: /* likely */ + return; + case ECONNABORTED: /* common w/ TCP */ + continue; + case EMFILE: + case ENFILE: + case ENOBUFS: + case ENOMEM: + case EPERM: + fail_fn = "accept4"; + break; + /* + * EINVAL, EBADF, ENOTSOCK, EOPNOTSUPP are all fatal + * bugs. The last 3 would be wayward closes in the + * application being traced + */ + default: + fprintf(stderr, + "accept4: %m (fatal in mwrap-httpd)\n"); + abort(); + } + } + } + /* hope other cleanup work gets done by other threads: */ + non_fatal_pause(fail_fn); +} + +static void h1d_unlink(struct mw_h1d *h1d, bool do_close) +{ + union mw_sockaddr sa; + socklen_t len = (socklen_t)sizeof(sa); + + if (h1d->lfd < 0 || !h1d->pid_len) + return; + if (getsockname(h1d->lfd, &sa.any, &len) < 0) { + fprintf(stderr, "getsockname: %m\n"); + return; + } + if (do_close) { /* only safe to close if thread isn't running */ + (void)close(h1d->lfd); + h1d->lfd = -1; + } + + char p[sizeof(h1d->pid_str)]; + int rc = snprintf(p, sizeof(p), "%d", (int)getpid()); + + if (rc == (int)h1d->pid_len && !memcmp(p, h1d->pid_str, rc)) + if (unlink(sa.un.sun_path) && errno != ENOENT) + fprintf(stderr, "unlink(%s): %m\n", sa.un.sun_path); + h1d->pid_len = 0; +} + +/* @env is getenv("MWRAP") */ +static int h1d_init(struct mw_h1d *h1d, const char *menv) +{ + union mw_sockaddr sa = { .un = { .sun_family = AF_UNIX } }; +#if defined(HAS_SOCKADDR_SA_LEN) || defined(HAVE_STRUCT_SOCKADDR_UN_SUN_LEN) + sa.un.sun_len = (unsigned char)sizeof(struct sockaddr_un); +#endif + const char *env = strstr(menv, "socket_dir:"); + if (!env) return 1; + if (env != menv && env[-1] != ',') + return 1; + env += sizeof("socket_dir"); + if (!*env) return 1; + const char *end = strchr(env, ','); + size_t len = end ? (size_t)(end - env) : strlen(env); + if (len == 0) + return fprintf(stderr, "socket_dir: cannot be empty\n"); + if (len >= sizeof(sa.un.sun_path)) + return fprintf(stderr, "socket_dir:%s too long(%zu)\n", + env, len); + + char *p = mempcpy(sa.un.sun_path, env, len); + if (p[-1] != '/') + *p++ = '/'; + struct stat sb; + if (stat(sa.un.sun_path, &sb) < 0) { + if (errno != ENOENT) + return fprintf(stderr, "stat(%s): %m\n", + sa.un.sun_path); + if (mkdir(sa.un.sun_path, 0700) < 0) + return fprintf(stderr, "mkdir(%s): %m\n", + sa.un.sun_path); + } else if (!S_ISDIR(sb.st_mode)) { + return fprintf(stderr, "socket_dir:%s is not a directory\n", + sa.un.sun_path); + } + len = sizeof(sa.un.sun_path) - (p - sa.un.sun_path); + int rc = snprintf(p, len, "%d.sock", (int)getpid()); + if (rc >= (int)len) + return fprintf(stderr, + "socket_dir too long rc=%d > len=%zu\n", rc, len); + if (rc < 0) + return fprintf(stderr, "we suck at snprintf: %m\n"); + h1d->pid_len = rc - sizeof(".sock") + 1; + memcpy(h1d->pid_str, p, h1d->pid_len); + if (unlink(sa.un.sun_path) && errno != ENOENT) + return fprintf(stderr, "unlink(%s): %m\n", sa.un.sun_path); + h1d->lfd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (h1d->lfd < 0) + return fprintf(stderr, "socket: %m\n"); + if (bind(h1d->lfd, &sa.any, (socklen_t)sizeof(sa)) < 0) { + fprintf(stderr, "bind: %m\n"); + goto close_fail; + } + if (listen(h1d->lfd, 1024) < 0) { + fprintf(stderr, "listen: %m\n"); + goto close_fail; + } + h1d->alive = 1; /* runs in parent, before pthread_create */ + h1d->running = 1; + CDS_INIT_LIST_HEAD(&h1d->conn); + return 0; +close_fail: + h1d_unlink(h1d, true); + return 1; +} + +/* + * epoll|kqueue would make this O(n) function unnecessary, but our (n) is + * expected to be tiny (<10): no need to waste kernel memory on epoll|kqueue + */ +static struct mw_h1 *h1_lookup(const struct mw_h1d *h1d, int fd) +{ + struct mw_h1 *h1 = NULL; + + cds_list_for_each_entry(h1, &h1d->conn, nd) + if (h1->fd == fd) + break; + mwrap_assert(h1 && h1->fd == fd && "bad FD"); + return h1; +} + +static void *h1d_run(void *x) /* pthread_create cb */ +{ + struct mw_h1d *h1d = x; + nfds_t i, nfds; + int rc; + struct mw_h1 *h1, *nxt; + enum mw_qev ev; + locating = 1; /* don't report our own memory use */ + + for (; uatomic_read(&h1d->alive); ) { + while (poll_add(h1d, h1d->lfd, POLLIN)) + non_fatal_pause("poll_add(lfd)"); + cds_list_for_each_entry_safe(h1, nxt, &h1d->conn, nd) + if (poll_add(h1d, h1->fd, h1->events)) + h1_close(h1); + AUTO_FREE struct pollfd *pfd = poll_detach(h1d, &nfds); + rc = pfd ? poll(pfd, nfds, -1) : -1; + + if (rc < 0) { + switch (errno) { + case EINTR: break; /* shouldn't happen, actually */ + case ENOMEM: /* may be common */ + case EINVAL: /* RLIMIT_NOFILE hit */ + non_fatal_pause("poll"); + break; /* to forloop where rc<0 */ + default: /* EFAULT is a fatal bug */ + fprintf(stderr, + "poll: %m (fatal in mwrap-httpd)\n"); + abort(); + } + } else { + for (i = 0; i < nfds && + uatomic_read(&h1d->alive); i++) { + if (!pfd[i].revents) + continue; + if (pfd[i].fd == h1d->lfd) { + h1d_event_step(h1d); + } else { + h1 = h1_lookup(h1d, pfd[i].fd); + ev = h1_event_step(h1, h1d); + if (ev == MW_QEV_IGNORE) + continue; + h1->events = ev; + } + } + } + } + uatomic_set(&h1d->running, 0); + free(poll_detach(h1d, &nfds)); + cds_list_for_each_entry_safe(h1, nxt, &h1d->conn, nd) + h1_close(h1); + return NULL; +} + +static void h1d_atexit(void) +{ + h1d_unlink(&g_h1d, false); +} + +static void h1d_stop_join(struct mw_h1d *h1d) +{ + union mw_sockaddr sa; + socklen_t len = (socklen_t)sizeof(sa); + int e, sfd; + void *ret; +#define ERR ": (stopping mwrap-httpd before fork): " + + mwrap_assert(uatomic_read(&h1d->alive) == 0); + while (getsockname(h1d->lfd, &sa.any, &len) < 0) { + non_fatal_pause("getsockname"ERR); + if (!uatomic_read(&h1d->running)) + goto join_thread; + } +retry_socket: + while ((sfd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0)) < 0) { + non_fatal_pause("socket"ERR); + if (!uatomic_read(&h1d->running)) + goto join_thread; + } + if (connect(sfd, &sa.any, len) < 0) { + int e = errno; + close(sfd); + errno = e; + non_fatal_pause("connect"ERR); + if (!uatomic_read(&h1d->running)) + goto join_thread; + goto retry_socket; + } +#undef ERR + (void)close(sfd); +join_thread: + e = pthread_join(h1d->tid, &ret); + if (e) { /* EDEADLK, EINVAL, ESRCH are all fatal bugs */ + fprintf(stderr, "BUG? pthread_join: %s\n", strerror(e)); + abort(); + } + h1d_unlink(h1d, true); +} + +static void h1d_atfork_prepare(void) +{ + if (uatomic_cmpxchg(&g_h1d.alive, 1, 0)) + h1d_stop_join(&g_h1d); +} + +static void h1d_start(void) /* may be called as pthread_atfork child cb */ +{ + if (mwrap_env && !h1d_init(&g_h1d, mwrap_env) && g_h1d.alive) { + int rc = pthread_create(&g_h1d.tid, NULL, h1d_run, &g_h1d); + if (rc) { /* non-fatal */ + fprintf(stderr, "pthread_create: %s\n", strerror(rc)); + g_h1d.alive = 0; + g_h1d.running = 0; + h1d_unlink(&g_h1d, true); + } + } +} + +/* must be called with global_mtx held */ +static void h1d_atfork_parent(void) +{ + if (g_h1d.lfd < 0) + h1d_start(); +} |