/* * A minimal FastCGI responder, built on hio itself, for the test suite. * * It speaks just enough of the protocol to answer a request: * BEGIN_REQUEST / PARAMS / STDIN in, STDOUT / END_REQUEST out. The reply * body is a fixed marker so a test can tell that a request really * travelled through the fcgi task and the FastCGI protocol both ways * rather than being answered somewhere else. * * The client keeps a connection open across requests, so a connection is * served until the peer closes it rather than being torn down after one. * * Usage: hio-fcgis [ready-file] * * If ready-file is given it is created once the listener is accepting, * which lets a harness start requests without racing this process. */ #include #include #include #include #include #include #include #include #include #define RESPONSE_BODY "fcgi-ok\r\n" /* what a connection has accumulated but not yet consumed */ #define INBUF_CAPA 65536 typedef struct conn_xtn_t conn_xtn_t; struct conn_xtn_t { hio_uint8_t buf[INBUF_CAPA]; hio_oow_t len; /* a request is answerable once it has been begun and both of its * streams have ended. they are tracked separately because the order * they arrive in is not guaranteed - see the note in consume_records() */ hio_uint16_t id; unsigned int begun: 1; unsigned int params_done: 1; unsigned int stdin_done: 1; unsigned int want_big: 1; }; static void reset_request (conn_xtn_t* cx) { cx->id = 0; cx->begun = 0; cx->params_done = 0; cx->stdin_done = 0; cx->want_big = 0; } static hio_t* g_hio = HIO_NULL; static int g_verbose = 0; static void on_sigint (int sig) { if (g_hio) hio_stop (g_hio, HIO_STOPREQ_TERMINATION); } /* ------------------------------------------------------------------ */ /* The record layout is taken from hio-fcgi.h. Since both this responder * and the library's client read it from the same header, a packing * mistake would make the two agree with each other while disagreeing * with the wire. Check it against the specification once at startup so * that shows up as a loud failure instead of a silent one. */ static int check_wire_layout (void) { if (HIO_SIZEOF(hio_fcgi_record_header_t) != 8 || offsetof(hio_fcgi_record_header_t, version) != 0 || offsetof(hio_fcgi_record_header_t, type) != 1 || offsetof(hio_fcgi_record_header_t, id) != 2 || offsetof(hio_fcgi_record_header_t, content_len) != 4 || offsetof(hio_fcgi_record_header_t, padding_len) != 6) { fprintf (stderr, "hio_fcgi_record_header_t does not match the FastCGI wire layout\n"); return -1; } if (HIO_SIZEOF(hio_fcgi_begin_request_body_t) != 8 || HIO_SIZEOF(hio_fcgi_end_request_body_t) != 8) { fprintf (stderr, "FastCGI body structures do not match the wire layout\n"); return -1; } return 0; } static int write_record (hio_dev_sck_t* sck, int type, hio_uint16_t id, const void* data, hio_uint16_t len) { hio_fcgi_record_header_t hdr; memset (&hdr, 0, HIO_SIZEOF(hdr)); hdr.version = HIO_FCGI_VERSION; hdr.type = type; hdr.id = hio_hton16(id); hdr.content_len = hio_hton16(len); if (hio_dev_sck_write(sck, &hdr, HIO_SIZEOF(hdr), HIO_NULL, HIO_NULL) <= -1) return -1; if (len > 0 && hio_dev_sck_write(sck, data, len, HIO_NULL, HIO_NULL) <= -1) return -1; return 0; } /* big enough to clear the kernel socket buffer on loopback, so that bytes * actually pile up in the server's write queue and its backpressure path is * reached. see the note in s-002.sh. */ #define BIGBODY_LEN (8 * 1024 * 1024) static int respond_big (hio_dev_sck_t* sck, hio_uint16_t id) { char head[128]; char chunk[32768]; hio_oow_t sent = 0, i; int n; n = snprintf(head, HIO_COUNTOF(head), "Content-Type: application/octet-stream\r\nContent-Length: %d\r\n\r\n", (int)BIGBODY_LEN); if (write_record(sck, HIO_FCGI_STDOUT, id, head, n) <= -1) return -1; for (i = 0; i < HIO_COUNTOF(chunk); i++) chunk[i] = (char)('a' + (i % 26)); while (sent < BIGBODY_LEN) { /* a STDOUT record carries at most 65535 octets, so one body needs * many of them. */ hio_oow_t want = BIGBODY_LEN - sent; if (want > HIO_COUNTOF(chunk)) want = HIO_COUNTOF(chunk); /* keep the pattern keyed to the absolute offset */ for (i = 0; i < want; i++) chunk[i] = (char)('a' + ((sent + i) % 26)); if (write_record(sck, HIO_FCGI_STDOUT, id, chunk, want) <= -1) return -1; sent += want; } return 0; } static int respond (hio_dev_sck_t* sck, hio_uint16_t id, int want_big) { static const char out[] = "Content-Type: text/plain\r\n\r\n" RESPONSE_BODY; hio_fcgi_end_request_body_t end; if (want_big) { if (respond_big(sck, id) <= -1) return -1; } else if (write_record(sck, HIO_FCGI_STDOUT, id, out, HIO_SIZEOF(out) - 1) <= -1) return -1; if (write_record(sck, HIO_FCGI_STDOUT, id, HIO_NULL, 0) <= -1) return -1; /* end of stream */ memset (&end, 0, HIO_SIZEOF(end)); end.app_status = hio_hton32(0); end.proto_status = HIO_FCGI_REQUEST_COMPLETE; return write_record(sck, HIO_FCGI_END_REQUEST, id, &end, HIO_SIZEOF(end)); } /* consume as many whole records as the buffer holds */ static int consume_records (hio_dev_sck_t* sck, conn_xtn_t* cx) { while (cx->len >= HIO_SIZEOF(hio_fcgi_record_header_t)) { hio_fcgi_record_header_t hdr; hio_oow_t clen, plen, total; memcpy (&hdr, cx->buf, HIO_SIZEOF(hdr)); clen = hio_ntoh16(hdr.content_len); plen = hdr.padding_len; total = HIO_SIZEOF(hdr) + clen + plen; if (g_verbose) fprintf (stderr, "record type=%d id=%d clen=%d have=%d\n", (int)hdr.type, (int)hio_ntoh16(hdr.id), (int)clen, (int)cx->len); if (cx->len < total) break; /* wait for the rest of this record */ /* Answer once the request has been begun and both of its streams * have ended, rather than on any single record. A responder has no * business assuming the three arrive in one particular order, and * tracking them separately keeps this working whatever order a * client picks. */ switch (hdr.type) { case HIO_FCGI_BEGIN_REQUEST: cx->id = hio_ntoh16(hdr.id); cx->begun = 1; break; case HIO_FCGI_PARAMS: if (clen == 0) cx->params_done = 1; /* a proper responder would decode the name-value pairs. this * one only needs to spot a marker the harness puts in the * query string, so it scans the raw payload instead. */ else if (hio_find_bchars_in_bchars((const hio_bch_t*)&cx->buf[HIO_SIZEOF(hdr)], clen, "bigbody", 7, 0)) cx->want_big = 1; break; case HIO_FCGI_STDIN: if (clen == 0) cx->stdin_done = 1; break; } if (cx->begun && cx->params_done && cx->stdin_done) { if (respond(sck, cx->id, cx->want_big) <= -1) return -1; reset_request (cx); /* the connection may carry more requests */ } memmove (cx->buf, &cx->buf[total], cx->len - total); cx->len -= total; } return 0; } /* ------------------------------------------------------------------ */ static int on_read (hio_dev_sck_t* sck, const void* buf, hio_iolen_t len, const hio_skad_t* srcaddr) { conn_xtn_t* cx = hio_dev_sck_getxtn(sck); if (len <= 0) return 0; /* error or peer closed - let the device wind down */ if (cx->len + len > HIO_SIZEOF(cx->buf)) { /* a test request never gets near this; treat it as a protocol error */ hio_dev_sck_halt (sck); return 0; } memcpy (&cx->buf[cx->len], buf, len); cx->len += len; if (consume_records(sck, cx) <= -1) hio_dev_sck_halt (sck); return 0; } static int on_write (hio_dev_sck_t* sck, hio_iolen_t wrlen, void* wrctx, const hio_skad_t* dstaddr) { return 0; } static void on_connect (hio_dev_sck_t* sck) { if (sck->state & HIO_DEV_SCK_ACCEPTED) { conn_xtn_t* cx = hio_dev_sck_getxtn(sck); cx->len = 0; reset_request (cx); if (g_verbose) fprintf (stderr, "accepted\n"); } } static void on_disconnect (hio_dev_sck_t* sck) { } /* ------------------------------------------------------------------ */ int main (int argc, char* argv[]) { hio_dev_sck_t* svr; hio_dev_sck_make_t m; hio_dev_sck_bind_t b; hio_dev_sck_listen_t l; hio_skad_t addr; int ret = -1; if (argc < 2 || argc > 3) { fprintf (stderr, "Usage: %s [ready-file]\n", argv[0]); return -1; } if (check_wire_layout() <= -1) return -1; g_verbose = (getenv("HIO_FCGIS_VERBOSE") != HIO_NULL); g_hio = hio_open(HIO_NULL, 0, HIO_NULL, HIO_FEATURE_ALL, 256, HIO_NULL); if (!g_hio) { fprintf (stderr, "unable to open hio\n"); return -1; } signal (SIGINT, on_sigint); signal (SIGTERM, on_sigint); signal (SIGPIPE, SIG_IGN); if (hio_bcstrtoskad(g_hio, argv[1], &addr) <= -1) { fprintf (stderr, "invalid address - %s\n", argv[1]); goto oops; } memset (&m, 0, HIO_SIZEOF(m)); m.type = (hio_skad_get_family(&addr) == HIO_AF_INET)? HIO_DEV_SCK_TCP4: HIO_DEV_SCK_TCP6; m.on_read = on_read; m.on_write = on_write; m.on_connect = on_connect; m.on_disconnect = on_disconnect; svr = hio_dev_sck_make(g_hio, HIO_SIZEOF(conn_xtn_t), &m); if (!svr) { fprintf (stderr, "unable to make a socket - %s\n", hio_geterrbmsg(g_hio)); goto oops; } memset (&b, 0, HIO_SIZEOF(b)); b.localaddr = addr; b.options = HIO_DEV_SCK_BIND_REUSEADDR; if (hio_dev_sck_bind(svr, &b) <= -1) { fprintf (stderr, "unable to bind - %s\n", hio_geterrbmsg(g_hio)); goto oops; } memset (&l, 0, HIO_SIZEOF(l)); l.backlogs = 64; if (hio_dev_sck_listen(svr, &l) <= -1) { fprintf (stderr, "unable to listen - %s\n", hio_geterrbmsg(g_hio)); goto oops; } if (argc >= 3) { /* tell the harness the listener is up, so it never races us */ FILE* fp = fopen(argv[2], "w"); if (fp) { fputs ("ready\n", fp); fclose (fp); } } hio_loop (g_hio); ret = 0; oops: if (g_hio) hio_close (g_hio); return ret; }