From cf2e3db35163031206e94e9e76b23acc2136d1a1 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 15 Dec 2022 20:52:52 +0000 Subject: rename mwrap_httpd.h to httpd.h No need to prefix our project name into every individual source file, especially since it's an auxilliary component. --- MANIFEST | 4 +- Mwrap.xs | 2 +- httpd.h | 1275 +++++++++++++++++++++++++++++++++++++++++++++ lib/Devel/Mwrap/PSGI.pm | 2 +- lib/Devel/Mwrap/Rproxy.pm | 4 +- mwrap_core.h | 2 +- mwrap_httpd.h | 1275 --------------------------------------------- mymalloc.h | 2 +- t/httpd.t | 165 ++++++ t/mwrap-httpd.t | 165 ------ 10 files changed, 1448 insertions(+), 1448 deletions(-) create mode 100644 httpd.h delete mode 100644 mwrap_httpd.h create mode 100644 t/httpd.t delete mode 100644 t/mwrap-httpd.t diff --git a/MANIFEST b/MANIFEST index 39473a5..4e3c487 100644 --- a/MANIFEST +++ b/MANIFEST @@ -8,12 +8,12 @@ check.h dlmalloc_c.h examples/mwrap.psgi gcc.h +httpd.h jhash.h lib/Devel/Mwrap.pm lib/Devel/Mwrap/PSGI.pm lib/Devel/Mwrap/Rproxy.pm mwrap_core.h -mwrap_httpd.h mymalloc.h picohttpparser.h picohttpparser_c.h @@ -21,7 +21,7 @@ ppport.h script/mwrap-perl script/mwrap-rproxy t/httpd-unit.t -t/mwrap-httpd.t +t/httpd.t t/mwrap.t t/source_location.perl t/test_common.perl diff --git a/Mwrap.xs b/Mwrap.xs index 9ebc082..568ec2b 100644 --- a/Mwrap.xs +++ b/Mwrap.xs @@ -12,7 +12,7 @@ */ typedef struct src_loc * Devel__Mwrap__SrcLoc; -/* keep this consistent with mwrap_httpd.h write_loc_name */ +/* keep this consistent with httpd.h write_loc_name */ static SV *location_string(struct src_loc *l) { SV *ret = newSV(0); diff --git a/httpd.h b/httpd.h new file mode 100644 index 0000000..4864e72 --- /dev/null +++ b/httpd.h @@ -0,0 +1,1275 @@ +/* + * Copyright (C) mwrap hackers + * License: GPL-2.0+ + * + * 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 *' + */ +#ifndef _DEFAULT_SOURCE +# define _DEFAULT_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "picohttpparser.h" +#include "picohttpparser_c.h" +#include +#include +#define URL "https://80x24.org/mwrap-perl.git/about" +#define TYPE_HTML "text/html; charset=UTF-8" +#define TYPE_CSV "text/csv" + +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 persist:1; /* HTTP/1.1 */ + unsigned has_input:1; + unsigned unused_:1; + 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; + 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 }; +static MWRAP_TSD struct mw_h1req *tsd_h1r; + +/* 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); + free(wbuf); + h1->wbuf = NULL; + return h1->persist ? MW_QEV_RD : 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" + "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" + "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); +} +#define VAL_EQ(h, VAL) val_eq(h, VAL, sizeof(VAL)-1) +static int val_eq(const struct phr_header *h, const char *val, size_t len) +{ + return h->value_len == len && !strncasecmp(val, h->value, 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" + "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" + "Content-Length: 9\r\n\r\n" "trimming\n"; + malloc_trim(0); + return h1_res_oneshot(h1, r200, sizeof(r200) - 1); +} + +#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("
", 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 == UINT_MAX) + FPUTS(":-", fp); + else + fprintf(fp, ":%zu", 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 " [$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, "

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("", fp); + write_html(fp, lb.ptr, lb.len); + FPUTS("

live allocations at:", fp); + if (bt_req_depth) FPUTS("
", fp); + else fputc(' ', fp); + write_html(fp, lb.ptr, lb.len); + + show_stats(fp); + FPUTS("" + "", 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, "\n", + size, h->as.live.gen, h->real); + } + rcu_read_unlock(); + FPUTS("
sizegenerationaddress
%zu%zu%p
", 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) { + fprintf(fp, "mwrap each >%lu" + "

mwrap each >%lu " + "(change `%lu' in URL to adjust filtering) - " + "MWRAP=bt:%u", min, min, min, (unsigned)bt_req_depth); + show_stats(fp); + /* need borders to distinguish multi-level traces */ + if (bt_req_depth) + FPUTS("", fp); + else /* save screen space if only tracing one line */ + FPUTS("", 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("", fp); + } + } + if (!csv) + FPUTS("", fp); + if (cmp) + qsort(hslv, hslc, sizeof(*hslv), cmp); + else if (!csv) + FPUTS("", 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, "" + "", + hsl->bytes, hsl->allocations, hsl->frees, + hsl->live, hsl->mean_life, hsl->max_life); + FPUTS("", fp); + } + FPUTS("
", fp); + if (fields[i].flen == sort_len && + !memcmp(fn, sort, sort_len)) { + cmp = fields[i].cmp; + fprintf(fp, "%s", fields[i].fname); + } else { + fprintf(fp, "%s", + min, fn, fn); + } + FPUTS("
sort= not understood
%zu%zu%zu%zu%0.3f%zusl->f, + src_loc_hash_len(hsl->sl)); + + FPUTS("\">", fp); + write_html(fp, hsl->loc_name, hsl->lname_len); + FPUTS("
", 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" + + FPUTS("mwrap demo" + "

mwrap demo", fp); + show_stats(fp); + FPUTS("

allocations >" + default_min " bytes" + "

" URL "", 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); + } + return h1_404(h1); +} + +/* + * 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) +{ + 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 { /* TODO: deal with pipelined requests */ + 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: + if (!h1->h1r) { + h1->h1r = h1r; + mwrap_assert(tsd_h1r == h1r); + tsd_h1r = NULL; + } + 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 enum mw_qev h1_parse_harder(struct mw_h1 *h1, struct mw_h1req *h1r) +{ + enum { HDR_IGN, HDR_CONN, HDR_XENC, HDR_CLEN } cur = HDR_IGN; + bool conn_set = false; + char *end; + struct phr_header *hdr = h1r->hdr; + + h1->prev_len = 0; + h1->has_input = 0; + h1->persist = h1r->minor_ver >= 1 ? 1 : 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, "Connection")) + cur = HDR_CONN; + else if (NAME_EQ(hdr, "Trailer")) + return h1_400(h1); + else if (hdr->name) + cur = HDR_IGN; + /* else: continuation line */ + if (!hdr->value_len) + continue; + switch (cur) { + case HDR_CONN: + if (conn_set) return h1_400(h1); + conn_set = true; + if (VAL_EQ(hdr, "close")) + h1->persist = 0; + else if (VAL_EQ(hdr, "keep-alive")) + h1->persist = 1; + else + return h1_400(h1); + break; + 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) + return h1_400(h1); + switch (*end) { + case '\r': case ' ': case '\t': case '\n': break; + default: 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); + } + 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); +} + +static enum mw_qev h1_event_step(struct mw_h1 *h1) +{ + 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); + /* + * The majority of requests can be served using TSD rbuf, + * no need for per-client allocations unless a client trickles + */ + h1r = h1->h1r ? h1->h1r : tsd_h1r; + if (!h1r) { + h1r = tsd_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); + 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 && !h1->h1r) { + h1->h1r = h1r; + mwrap_assert(tsd_h1r == h1r); + tsd_h1r = NULL; + } + 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); +} + +/* @env is getenv("MWRAP") */ +static int h1d_init(struct mw_h1d *h1d, const char *menv) +{ + union mw_sockaddr sa = { .un = { .sun_family = AF_UNIX } }; +#ifdef HAS_SOCKADDR_SA_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) < 0 && 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: + close(h1d->lfd); + h1d->lfd = -1; + 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); + 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) +{ + union mw_sockaddr sa; + socklen_t len = (socklen_t)sizeof(sa); + + if (g_h1d.lfd < 0 || !g_h1d.pid_len) + return; + if (getsockname(g_h1d.lfd, &sa.any, &len) < 0) + return; + + char p[sizeof(g_h1d.pid_str)]; + int rc = snprintf(p, sizeof(p), "%d", (int)getpid()); + + if (rc == (int)g_h1d.pid_len && !memcmp(p, g_h1d.pid_str, rc)) + unlink(sa.un.sun_path); +} + +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(); + } + (void)close(h1d->lfd); + h1d->lfd = -1; +} + +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)); + (void)close(g_h1d.lfd); + g_h1d.lfd = -1; + g_h1d.alive = 0; + } + } +} + +/* must be called with global_mtx held */ +static void h1d_atfork_parent(void) +{ + if (g_h1d.lfd < 0) + h1d_start(); +} diff --git a/lib/Devel/Mwrap/PSGI.pm b/lib/Devel/Mwrap/PSGI.pm index 3e8795e..f2be733 100644 --- a/lib/Devel/Mwrap/PSGI.pm +++ b/lib/Devel/Mwrap/PSGI.pm @@ -1,7 +1,7 @@ # Copyright (C) all contributors # License: GPL-2.0+ # -# Note: this is deprecated, use mwrap_httpd.h instead +# Note: this is deprecated, use httpd.h instead package Devel::Mwrap::PSGI; use v5.12; # strict use warnings; diff --git a/lib/Devel/Mwrap/Rproxy.pm b/lib/Devel/Mwrap/Rproxy.pm index f7f8a82..76b7d7f 100644 --- a/lib/Devel/Mwrap/Rproxy.pm +++ b/lib/Devel/Mwrap/Rproxy.pm @@ -1,7 +1,7 @@ # Copyright (C) mwrap hackers # License: GPL-2.0+ -# minimal reverse proxy to expose the embedded mwrap_httpd.h UNIX sockets +# minimal reverse proxy to expose the embedded httpd.h UNIX sockets # via PSGI (and thus TCP HTTP/1.x). This does not have a hard dependency # on Mwrap.so. # @@ -69,7 +69,7 @@ sub call { # PSGI entry point my $h = "$method $uri HTTP/1.0\n\n"; $s = send($c, $h, MSG_NOSIGNAL) // return r(500, "send: $!"); $s == length($h) or return r(500, "send $s <".length($h)); - # this only expects mwrap_httpd.h output, so no continuation lines: + # this only expects httpd.h output, so no continuation lines: $h = do { local $/ = "\r\n\r\n"; <$c> } // return r(500, "read: $!"); my ($code, @hdr) = split(/\r\n/, $h); (undef, $code, undef) = split(/ /, $code); diff --git a/mwrap_core.h b/mwrap_core.h index d798b79..1b3d98f 100644 --- a/mwrap_core.h +++ b/mwrap_core.h @@ -958,7 +958,7 @@ static struct src_loc *mwrap_get_bin(const char *buf, size_t len) } static const char *mwrap_env; -#include "mwrap_httpd.h" +#include "httpd.h" __attribute__((constructor)) static void mwrap_ctor(void) { diff --git a/mwrap_httpd.h b/mwrap_httpd.h deleted file mode 100644 index 4864e72..0000000 --- a/mwrap_httpd.h +++ /dev/null @@ -1,1275 +0,0 @@ -/* - * Copyright (C) mwrap hackers - * License: GPL-2.0+ - * - * 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 *' - */ -#ifndef _DEFAULT_SOURCE -# define _DEFAULT_SOURCE -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "picohttpparser.h" -#include "picohttpparser_c.h" -#include -#include -#define URL "https://80x24.org/mwrap-perl.git/about" -#define TYPE_HTML "text/html; charset=UTF-8" -#define TYPE_CSV "text/csv" - -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 persist:1; /* HTTP/1.1 */ - unsigned has_input:1; - unsigned unused_:1; - 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; - 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 }; -static MWRAP_TSD struct mw_h1req *tsd_h1r; - -/* 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); - free(wbuf); - h1->wbuf = NULL; - return h1->persist ? MW_QEV_RD : 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" - "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" - "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); -} -#define VAL_EQ(h, VAL) val_eq(h, VAL, sizeof(VAL)-1) -static int val_eq(const struct phr_header *h, const char *val, size_t len) -{ - return h->value_len == len && !strncasecmp(val, h->value, 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" - "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" - "Content-Length: 9\r\n\r\n" "trimming\n"; - malloc_trim(0); - return h1_res_oneshot(h1, r200, sizeof(r200) - 1); -} - -#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("
", 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 == UINT_MAX) - FPUTS(":-", fp); - else - fprintf(fp, ":%zu", 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 " [$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, "

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("", fp); - write_html(fp, lb.ptr, lb.len); - FPUTS("

live allocations at:", fp); - if (bt_req_depth) FPUTS("
", fp); - else fputc(' ', fp); - write_html(fp, lb.ptr, lb.len); - - show_stats(fp); - FPUTS("" - "", 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, "\n", - size, h->as.live.gen, h->real); - } - rcu_read_unlock(); - FPUTS("
sizegenerationaddress
%zu%zu%p
", 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) { - fprintf(fp, "mwrap each >%lu" - "

mwrap each >%lu " - "(change `%lu' in URL to adjust filtering) - " - "MWRAP=bt:%u", min, min, min, (unsigned)bt_req_depth); - show_stats(fp); - /* need borders to distinguish multi-level traces */ - if (bt_req_depth) - FPUTS("", fp); - else /* save screen space if only tracing one line */ - FPUTS("", 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("", fp); - } - } - if (!csv) - FPUTS("", fp); - if (cmp) - qsort(hslv, hslc, sizeof(*hslv), cmp); - else if (!csv) - FPUTS("", 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, "" - "", - hsl->bytes, hsl->allocations, hsl->frees, - hsl->live, hsl->mean_life, hsl->max_life); - FPUTS("", fp); - } - FPUTS("
", fp); - if (fields[i].flen == sort_len && - !memcmp(fn, sort, sort_len)) { - cmp = fields[i].cmp; - fprintf(fp, "%s", fields[i].fname); - } else { - fprintf(fp, "%s", - min, fn, fn); - } - FPUTS("
sort= not understood
%zu%zu%zu%zu%0.3f%zusl->f, - src_loc_hash_len(hsl->sl)); - - FPUTS("\">", fp); - write_html(fp, hsl->loc_name, hsl->lname_len); - FPUTS("
", 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" - - FPUTS("mwrap demo" - "

mwrap demo", fp); - show_stats(fp); - FPUTS("

allocations >" - default_min " bytes" - "

" URL "", 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); - } - return h1_404(h1); -} - -/* - * 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) -{ - 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 { /* TODO: deal with pipelined requests */ - 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: - if (!h1->h1r) { - h1->h1r = h1r; - mwrap_assert(tsd_h1r == h1r); - tsd_h1r = NULL; - } - 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 enum mw_qev h1_parse_harder(struct mw_h1 *h1, struct mw_h1req *h1r) -{ - enum { HDR_IGN, HDR_CONN, HDR_XENC, HDR_CLEN } cur = HDR_IGN; - bool conn_set = false; - char *end; - struct phr_header *hdr = h1r->hdr; - - h1->prev_len = 0; - h1->has_input = 0; - h1->persist = h1r->minor_ver >= 1 ? 1 : 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, "Connection")) - cur = HDR_CONN; - else if (NAME_EQ(hdr, "Trailer")) - return h1_400(h1); - else if (hdr->name) - cur = HDR_IGN; - /* else: continuation line */ - if (!hdr->value_len) - continue; - switch (cur) { - case HDR_CONN: - if (conn_set) return h1_400(h1); - conn_set = true; - if (VAL_EQ(hdr, "close")) - h1->persist = 0; - else if (VAL_EQ(hdr, "keep-alive")) - h1->persist = 1; - else - return h1_400(h1); - break; - 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) - return h1_400(h1); - switch (*end) { - case '\r': case ' ': case '\t': case '\n': break; - default: 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); - } - 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); -} - -static enum mw_qev h1_event_step(struct mw_h1 *h1) -{ - 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); - /* - * The majority of requests can be served using TSD rbuf, - * no need for per-client allocations unless a client trickles - */ - h1r = h1->h1r ? h1->h1r : tsd_h1r; - if (!h1r) { - h1r = tsd_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); - 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 && !h1->h1r) { - h1->h1r = h1r; - mwrap_assert(tsd_h1r == h1r); - tsd_h1r = NULL; - } - 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); -} - -/* @env is getenv("MWRAP") */ -static int h1d_init(struct mw_h1d *h1d, const char *menv) -{ - union mw_sockaddr sa = { .un = { .sun_family = AF_UNIX } }; -#ifdef HAS_SOCKADDR_SA_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) < 0 && 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: - close(h1d->lfd); - h1d->lfd = -1; - 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); - 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) -{ - union mw_sockaddr sa; - socklen_t len = (socklen_t)sizeof(sa); - - if (g_h1d.lfd < 0 || !g_h1d.pid_len) - return; - if (getsockname(g_h1d.lfd, &sa.any, &len) < 0) - return; - - char p[sizeof(g_h1d.pid_str)]; - int rc = snprintf(p, sizeof(p), "%d", (int)getpid()); - - if (rc == (int)g_h1d.pid_len && !memcmp(p, g_h1d.pid_str, rc)) - unlink(sa.un.sun_path); -} - -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(); - } - (void)close(h1d->lfd); - h1d->lfd = -1; -} - -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)); - (void)close(g_h1d.lfd); - g_h1d.lfd = -1; - g_h1d.alive = 0; - } - } -} - -/* must be called with global_mtx held */ -static void h1d_atfork_parent(void) -{ - if (g_h1d.lfd < 0) - h1d_start(); -} diff --git a/mymalloc.h b/mymalloc.h index 2e0517b..518cce3 100644 --- a/mymalloc.h +++ b/mymalloc.h @@ -126,7 +126,7 @@ ATTR_COLD static void mstate_tsd_dtor(void *p) CHECK(int, 0, pthread_mutex_unlock(&global_mtx)); } -/* see mwrap_httpd.h */ +/* see httpd.h */ static void h1d_atfork_prepare(void); static void h1d_atfork_parent(void); static void h1d_start(void); diff --git a/t/httpd.t b/t/httpd.t new file mode 100644 index 0000000..ca90cf0 --- /dev/null +++ b/t/httpd.t @@ -0,0 +1,165 @@ +#!perl -w +# Copyright (C) mwrap hackers +# License: GPL-2.0+ +use v5.12; +use IO::Socket::UNIX; +use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC); +use POSIX qw(dup2 _exit mkfifo); +BEGIN { require './t/test_common.perl' }; +my $env = { MWRAP => "socket_dir:$mwrap_tmp" }; +my $f1 = "$mwrap_tmp/f1"; +my $f2 = "$mwrap_tmp/f2"; +mkfifo($f1, 0600) // plan(skip_all => "mkfifo: $!"); +mkfifo($f2, 0600) // plan(skip_all => "mkfifo: $!"); +my $pid = mwrap_run('httpd test', $env, '-e', + "open my \$f1, '>', '$f1'; close \$f1; open my \$f2, '<', '$f2'"); +my $spid; +my $mw_exit; +my $cleanup = sub { + if (defined $spid) { + if (kill('TERM', $spid)) { + waitpid($spid, 0); + $? == 15 or warn "rproxy died with \$?=$?"; + } else { + warn "kill $spid: $!"; + } + undef $spid; + } + use autodie; + if (defined $pid) { + my $exit = $?; + open my $fh, '>', $f2; + close $fh; + waitpid($pid, 0); + $mw_exit = $?; + undef $pid; + diag "err: ".slurp($mwrap_err); + $? = $exit; + } +}; +END { $cleanup->() } + +my $sock = "$mwrap_tmp/$pid.sock"; +my %o = (Peer => $sock , Type => SOCK_STREAM); +local $SIG{PIPE} = 'IGNORE'; + +open my $fh, '<', $f1; +is(my $nil = <$fh>, undef, 'FIFO open'); +close $fh; +ok(-S $sock, 'socket created'); +my $c = IO::Socket::UNIX->new(%o); +ok($c, 'socket connected'); +is(send($c, 'GET', MSG_NOSIGNAL), 3, 'trickled 3 bytes') or diag "send: $!"; + +my $cout = "$mwrap_tmp/cout"; +my $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, + "http://0/$pid/each/2000"); +my $curl_unix; +SKIP: { + skip 'curl lacks --unix-socket support', 1 if $rc == 512; + is($rc, 0, 'curl /each'); + unlink($cout); + $curl_unix = 1; + + $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, + "http://0/$pid/each/2000"); + is($rc, 0, 'curl /each'); + unlink($cout); + + $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, + "http://0/$pid/"); + is($rc, 0, 'curl / (PID root)'); + like(slurp($cout), qr//, 'root shown'); + + $rc = system(qw(curl -vsSf -XPOST --unix-socket), $sock, '-o', $cout, + "http://0/$pid/trim"); + is($rc, 0, 'curl / (PID root)'); + like(slurp($cout), qr/trimming/, 'trim started'); + unlink($cout); +}; + +{ + my $req = " /$pid/each/20000 HTTP/1.0\r\n\r\n"; + is(send($c, $req, MSG_NOSIGNAL), length($req), + 'wrote rest of response') or diag "send: $!"; + my $x = do { local $/; <$c> } or diag "readline: $!"; + like($x, qr!\n?\z!s, 'got complete HTML response'); +} + +SKIP: { + eval { require HTTP::Tiny } or skip 'HTTP::Tiny missing', 1; + my $srv = IO::Socket::INET->new(LocalAddr => '127.0.0.1', + ReuseAddr => 1, Proto => 'tcp', + Type => SOCK_STREAM, + Listen => 1024); + $spid = fork; + if ($spid == 0) { + local $ENV{LISTEN_PID} = $$; + local $ENV{LISTEN_FDS} = 1; + my $fl = fcntl($srv, F_GETFD, 0); + fcntl($srv, F_SETFD, $fl &= ~FD_CLOEXEC); + if (fileno($srv) != 3) { + dup2(fileno($srv), 3) or die "dup2: $!"; + } + no warnings 'exec'; + exec $^X, '-w', './blib/script/mwrap-rproxy', + "--socket-dir=$mwrap_tmp"; + _exit(1); + } + my $http = HTTP::Tiny->new; + my ($h, $p) = ($srv->sockhost, $srv->sockport); + undef $srv; + my $res = $http->get("http://$h:$p/"); + ok($res->{success}, 'listing success'); + like($res->{content}, qr!/$pid/each/\d+!, 'got listing for each'); + $res = $http->get("http://$h:$p/$pid/each/1"); + ok($res->{success}, 'each/1 success'); + my $t = '/at/$LOCATION link in /each/$NUM'; + if ($res->{content} =~ m!href="\.\./at/([^"]+)"!) { + my $loc = $1; + ok($t); + $res = $http->get("http://$h:$p/$pid/at/$1"); + ok($res->{success}, '/at/$LOCATION endpoint'); + like($res->{content}, qr!\blive allocations at\b!, + 'live allocations shown'); + } else { + fail($t); + } + if ($ENV{INTERACTIVE}) { + diag "http://$h:$p/$pid/each/1 up for interactive testing"; + diag "- press Enter when done -"; + my $ok = ; + } +} + +SKIP: { + skip 'no reset w/o curl --unix-socket', 1 if !$curl_unix; + + $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, + "http://0/$pid/each/100.csv"); + is($rc, 0, '.csv retrieved') or skip 'CSV failed', 1; + my $db = "$mwrap_tmp/t.sqlite3"; + $rc = system(qw(sqlite3), $db, ".import --csv $cout mwrap_each"); + if ($rc == -1) { + diag 'sqlite3 missing'; + } else { + is($rc, 0, 'sqlite3 import'); + my $n = `sqlite3 $db 'SELECT COUNT(*) FROM mwrap_each'`; + is($?, 0, 'sqlite3 count'); + my $exp = split(/\n/, slurp($cout)); + is($n + 1, $exp, 'imported all rows into sqlite'); + my $n = `sqlite3 $db 'SELECT COUNT(*) FROM mwrap_each'`; + # diag `sqlite3 $db .schema`; + } + + $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, + '-d', 'x=y', "http://0/$pid/reset"); + is($rc, 0, 'curl /reset'); +}; + + +diag slurp($cout) if $ENV{V}; +$cleanup->(); +ok(!-e $sock, 'socket unlinked after cleanup'); +is($mw_exit, 0, 'perl exited with $?==0'); +done_testing; diff --git a/t/mwrap-httpd.t b/t/mwrap-httpd.t deleted file mode 100644 index ca90cf0..0000000 --- a/t/mwrap-httpd.t +++ /dev/null @@ -1,165 +0,0 @@ -#!perl -w -# Copyright (C) mwrap hackers -# License: GPL-2.0+ -use v5.12; -use IO::Socket::UNIX; -use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC); -use POSIX qw(dup2 _exit mkfifo); -BEGIN { require './t/test_common.perl' }; -my $env = { MWRAP => "socket_dir:$mwrap_tmp" }; -my $f1 = "$mwrap_tmp/f1"; -my $f2 = "$mwrap_tmp/f2"; -mkfifo($f1, 0600) // plan(skip_all => "mkfifo: $!"); -mkfifo($f2, 0600) // plan(skip_all => "mkfifo: $!"); -my $pid = mwrap_run('httpd test', $env, '-e', - "open my \$f1, '>', '$f1'; close \$f1; open my \$f2, '<', '$f2'"); -my $spid; -my $mw_exit; -my $cleanup = sub { - if (defined $spid) { - if (kill('TERM', $spid)) { - waitpid($spid, 0); - $? == 15 or warn "rproxy died with \$?=$?"; - } else { - warn "kill $spid: $!"; - } - undef $spid; - } - use autodie; - if (defined $pid) { - my $exit = $?; - open my $fh, '>', $f2; - close $fh; - waitpid($pid, 0); - $mw_exit = $?; - undef $pid; - diag "err: ".slurp($mwrap_err); - $? = $exit; - } -}; -END { $cleanup->() } - -my $sock = "$mwrap_tmp/$pid.sock"; -my %o = (Peer => $sock , Type => SOCK_STREAM); -local $SIG{PIPE} = 'IGNORE'; - -open my $fh, '<', $f1; -is(my $nil = <$fh>, undef, 'FIFO open'); -close $fh; -ok(-S $sock, 'socket created'); -my $c = IO::Socket::UNIX->new(%o); -ok($c, 'socket connected'); -is(send($c, 'GET', MSG_NOSIGNAL), 3, 'trickled 3 bytes') or diag "send: $!"; - -my $cout = "$mwrap_tmp/cout"; -my $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, - "http://0/$pid/each/2000"); -my $curl_unix; -SKIP: { - skip 'curl lacks --unix-socket support', 1 if $rc == 512; - is($rc, 0, 'curl /each'); - unlink($cout); - $curl_unix = 1; - - $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, - "http://0/$pid/each/2000"); - is($rc, 0, 'curl /each'); - unlink($cout); - - $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, - "http://0/$pid/"); - is($rc, 0, 'curl / (PID root)'); - like(slurp($cout), qr//, 'root shown'); - - $rc = system(qw(curl -vsSf -XPOST --unix-socket), $sock, '-o', $cout, - "http://0/$pid/trim"); - is($rc, 0, 'curl / (PID root)'); - like(slurp($cout), qr/trimming/, 'trim started'); - unlink($cout); -}; - -{ - my $req = " /$pid/each/20000 HTTP/1.0\r\n\r\n"; - is(send($c, $req, MSG_NOSIGNAL), length($req), - 'wrote rest of response') or diag "send: $!"; - my $x = do { local $/; <$c> } or diag "readline: $!"; - like($x, qr!\n?\z!s, 'got complete HTML response'); -} - -SKIP: { - eval { require HTTP::Tiny } or skip 'HTTP::Tiny missing', 1; - my $srv = IO::Socket::INET->new(LocalAddr => '127.0.0.1', - ReuseAddr => 1, Proto => 'tcp', - Type => SOCK_STREAM, - Listen => 1024); - $spid = fork; - if ($spid == 0) { - local $ENV{LISTEN_PID} = $$; - local $ENV{LISTEN_FDS} = 1; - my $fl = fcntl($srv, F_GETFD, 0); - fcntl($srv, F_SETFD, $fl &= ~FD_CLOEXEC); - if (fileno($srv) != 3) { - dup2(fileno($srv), 3) or die "dup2: $!"; - } - no warnings 'exec'; - exec $^X, '-w', './blib/script/mwrap-rproxy', - "--socket-dir=$mwrap_tmp"; - _exit(1); - } - my $http = HTTP::Tiny->new; - my ($h, $p) = ($srv->sockhost, $srv->sockport); - undef $srv; - my $res = $http->get("http://$h:$p/"); - ok($res->{success}, 'listing success'); - like($res->{content}, qr!/$pid/each/\d+!, 'got listing for each'); - $res = $http->get("http://$h:$p/$pid/each/1"); - ok($res->{success}, 'each/1 success'); - my $t = '/at/$LOCATION link in /each/$NUM'; - if ($res->{content} =~ m!href="\.\./at/([^"]+)"!) { - my $loc = $1; - ok($t); - $res = $http->get("http://$h:$p/$pid/at/$1"); - ok($res->{success}, '/at/$LOCATION endpoint'); - like($res->{content}, qr!\blive allocations at\b!, - 'live allocations shown'); - } else { - fail($t); - } - if ($ENV{INTERACTIVE}) { - diag "http://$h:$p/$pid/each/1 up for interactive testing"; - diag "- press Enter when done -"; - my $ok = ; - } -} - -SKIP: { - skip 'no reset w/o curl --unix-socket', 1 if !$curl_unix; - - $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, - "http://0/$pid/each/100.csv"); - is($rc, 0, '.csv retrieved') or skip 'CSV failed', 1; - my $db = "$mwrap_tmp/t.sqlite3"; - $rc = system(qw(sqlite3), $db, ".import --csv $cout mwrap_each"); - if ($rc == -1) { - diag 'sqlite3 missing'; - } else { - is($rc, 0, 'sqlite3 import'); - my $n = `sqlite3 $db 'SELECT COUNT(*) FROM mwrap_each'`; - is($?, 0, 'sqlite3 count'); - my $exp = split(/\n/, slurp($cout)); - is($n + 1, $exp, 'imported all rows into sqlite'); - my $n = `sqlite3 $db 'SELECT COUNT(*) FROM mwrap_each'`; - # diag `sqlite3 $db .schema`; - } - - $rc = system(qw(curl -vsSf --unix-socket), $sock, '-o', $cout, - '-d', 'x=y', "http://0/$pid/reset"); - is($rc, 0, 'curl /reset'); -}; - - -diag slurp($cout) if $ENV{V}; -$cleanup->(); -ok(!-e $sock, 'socket unlinked after cleanup'); -is($mw_exit, 0, 'perl exited with $?==0'); -done_testing; -- cgit v1.2.3-24-ge0c7