From fc01b820ecf497c0b3bf4dbf82f83852d2c7aefd Mon Sep 17 00:00:00 2001 From: Glenn Strauss Date: Sun, 20 Dec 2020 21:32:50 -0500 Subject: [tests] remove FastCGI test dependency on libfcgi - rewrite fcgi-responder as standalone app fcgi-responder is now a minimal, standalone FastCGI server for tests - remove dependency on fcgi-devel package - merge fcgi-auth into fcgi-responder --- tests/fcgi-responder.c | 426 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 374 insertions(+), 52 deletions(-) (limited to 'tests/fcgi-responder.c') diff --git a/tests/fcgi-responder.c b/tests/fcgi-responder.c index be626ee2..64bcc2a9 100644 --- a/tests/fcgi-responder.c +++ b/tests/fcgi-responder.c @@ -1,56 +1,378 @@ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif -#ifdef HAVE_FASTCGI_FASTCGI_H -#include -#else -#include -#endif +/* + * simple and trivial FastCGI server w/ hard-coded results for use in unit tests + * - processes a single FastCGI request at a time (serially) + * - listens on FCGI_LISTENSOCK_FILENO + * (socket on FCGI_LISTENSOCK_FILENO must be set up by invoker) + * expects to be started w/ listening socket already on FCGI_LISTENSOCK_FILENO + * - expect recv data for request headers every 10ms or less + * - no read timeouts for request body; might block reading request body + * - no write timeouts; might block writing response + * + * Copyright(c) 2020 Glenn Strauss gstrauss()gluelogic.com All rights reserved + * License: BSD 3-clause (same as lighttpd) + */ + +#include +#include +#include +#include +#include +#include +#include #include -#include +#include +#include #include +#include + +#ifndef MSG_DONTWAIT +#define MSG_DONTWAIT 0 +#endif + +#include "../src/compat/fastcgi.h" + +static int finished; +static unsigned char buf[65536]; + + +static void +fcgi_header (FCGI_Header * const header, const unsigned char type, const int request_id, const int contentLength, const unsigned char paddingLength) +{ + /*force_assert(contentLength <= FCGI_MAX_LENGTH);*/ + + header->version = FCGI_VERSION_1; + header->type = type; + header->requestIdB1 = (request_id >> 8) & 0xff; + header->requestIdB0 = request_id & 0xff; + header->contentLengthB1 = (contentLength >> 8) & 0xff; + header->contentLengthB0 = contentLength & 0xff; + header->paddingLength = paddingLength; + header->reserved = 0; +} + + +static void +fcgi_unknown_type_rec (FCGI_UnknownTypeRecord * const rec, const int req_id, const unsigned char type) +{ + fcgi_header(&rec->header, FCGI_UNKNOWN_TYPE, req_id, sizeof(rec->header), 0); + memset(&rec->body.reserved, 0, sizeof(rec->body.reserved)); + rec->body.type = type; +} + + +static void +fcgi_end_request_rec (FCGI_EndRequestRecord * const rec, const int req_id, const uint32_t appStatus, const unsigned char protocolStatus) +{ + fcgi_header(&rec->header, FCGI_END_REQUEST, req_id, sizeof(rec->header), 0); + rec->body.appStatusB3 = (appStatus >> 24) & 0xff; + rec->body.appStatusB2 = (appStatus >> 16) & 0xff; + rec->body.appStatusB1 = (appStatus >> 8) & 0xff; + rec->body.appStatusB0 = appStatus & 0xff; + rec->body.protocolStatus = protocolStatus; + rec->body.reserved[0] = 0; + rec->body.reserved[1] = 0; + rec->body.reserved[2] = 0; +} + + +static int +fcgi_puts (const int req_id, const char * const str, size_t len, FILE * const stream) +{ + if (NULL == str) return -1; + + FCGI_Header header; + + for (size_t offset = 0, part; offset != len; offset += part) { + part = len - offset > FCGI_MAX_LENGTH ? FCGI_MAX_LENGTH : len - offset; + fcgi_header(&header, FCGI_STDOUT, req_id, part, 0); + if (1 != fwrite(&header, sizeof(header), 1, stream)) + return -1; + if (part != fwrite(str+offset, 1, part, stream)) + return -1; + } + + return 0; +} + + +static const char * +fcgi_getenv(const unsigned char * const r, const uint32_t rlen, const char * const name, int nlen, int *len) +{ + /* simple search; + * if many lookups are done, then should use more efficient data structure*/ + for (uint32_t i = 0; i < rlen; ) { + int klen = r[i]; + if (!(r[i] & 0x80)) + ++i; + else { + klen = ((r[i] & ~0x80)<<24) | (r[i+1]<<16) | (r[i+2]<<8) | r[i+3]; + i += 4; + } + int vlen = r[i]; + if (!(r[i] & 0x80)) + ++i; + else { + vlen = ((r[i] & ~0x80)<<24) | (r[i+1]<<16) | (r[i+2]<<8) | r[i+3]; + i += 4; + } + + if (klen == nlen && 0 == memcmp(r+i, name, klen)) { + *len = vlen; + return (const char *)r+i+klen; + } + + i += klen + vlen; + } + + char s[256]; + if (nlen > (int)sizeof(s)-1) + return NULL; + memcpy(s, name, nlen); + s[nlen] = '\0'; + char *e = getenv(s); + if (e) *len = strlen(e); + return e; +} + + +static int +fcgi_process_params (FILE * const stream, int req_id, int role, unsigned char * const r, uint32_t rlen) +{ + const char *p = NULL; + int len; + + /* (FCGI_STDIN currently ignored in these FastCGI unit test responses, so + * generate response here based on query string values (indicating test) */ + + const char *cdata = NULL; + + if (NULL != (p = fcgi_getenv(r, rlen, "QUERY_STRING", 12, &len))) { + if (2 == len && 0 == memcmp(p, "lf", 2)) + cdata = "Status: 200 OK\n\n"; + else if (4 == len && 0 == memcmp(p, "crlf", 4)) + cdata = "Status: 200 OK\r\n\r\n"; + else if (7 == len && 0 == memcmp(p, "slow-lf", 7)) { + cdata = "Status: 200 OK\n"; + if (0 != fcgi_puts(req_id, cdata, strlen(cdata), stream)) + return -1; + fflush(stdout); + cdata = "\n"; + } + else if (9 == len && 0 == memcmp(p, "slow-crlf", 9)) { + cdata = "Status: 200 OK\r\n"; + if (0 != fcgi_puts(req_id, cdata, strlen(cdata), stream)) + return -1; + fflush(stdout); + cdata = "\r\n"; + } + else if (10 == len && 0 == memcmp(p, "die-at-end", 10)) { + cdata = "Status: 200 OK\r\n\r\n"; + finished = 1; + } + else if (role == FCGI_AUTHORIZER + && len >= 5 && 0 == memcmp(p, "auth-", 5)) { + if (7 == len && 0 == memcmp(p, "auth-ok", 7)) + cdata = "Status: 200 OK\r\n\r\n"; + else if (8 == len && 0 == memcmp(p, "auth-var", 8)) { + /* Status: 200 OK to allow access is implied + * if Status header is not included in response */ + cdata = "Variable-X-LIGHTTPD-FCGI-AUTH: " + "LighttpdTestContent\r\n\r\n"; + p = NULL; + } + else { + cdata = "Status: 403 Forbidden\r\n\r\n"; + p = NULL; + } + } + else + cdata = "Status: 200 OK\r\n\r\n"; + } + else { + cdata = "Status: 500 Internal Foo\r\n\r\n"; + p = NULL; + } + + if (cdata && 0 != fcgi_puts(req_id, cdata, strlen(cdata), stream)) + return -1; + + if (NULL == p) + cdata = NULL; + else if (9 == len && 0 == memcmp(p, "path_info", 9)) + cdata = fcgi_getenv(r, rlen, "PATH_INFO", 9, &len); + else if (11 == len && 0 == memcmp(p, "script_name", 11)) + cdata = fcgi_getenv(r, rlen, "SCRIPT_NAME", 11, &len); + else if (len > 4 && 0 == memcmp(p, "env=", 4)) + cdata = fcgi_getenv(r, rlen, p+4, len-4, &len); + else if (8 == len && 0 == memcmp(p, "auth-var", 8)) + cdata = fcgi_getenv(r, rlen, "X_LIGHTTPD_FCGI_AUTH", 20, &len); + else { + cdata = "test123"; + len = sizeof("test123")-1; + } + + if (cdata && 0 != fcgi_puts(req_id, cdata, (size_t)len, stream)) + return -1; + + /*(XXX: always sending appStatus 0)*/ + FCGI_EndRequestRecord endrec; + fcgi_end_request_rec(&endrec, req_id, 0, FCGI_REQUEST_COMPLETE); + if (1 != fwrite(&endrec, sizeof(endrec), 1, stream)) + return -1; /* error writing FCGI_END_REQUEST; ignore */ + + return -2; /* done */ +} + + +static int +fcgi_dispatch_packet (FILE *stream, ssize_t offset, uint32_t len) +{ + FCGI_Header * const h = (FCGI_Header *)(buf+offset); + int req_id = (h->requestIdB1 << 8) | h->requestIdB0; + int type = h->type; + + if (type > FCGI_MAXTYPE) { + FCGI_UnknownTypeRecord unkrec; + fcgi_unknown_type_rec(&unkrec, req_id, type); + if (1 != fwrite(&unkrec, sizeof(unkrec), 1, stream)) + return -1; + return 0; + } + + if (0 == req_id) { + /* not implemented: FCGI_GET_VALUES + * FCGI_GET_VALUES_RESULT + * FCGI_MAX_CONNS: 1 + * FCGI_MAX_REQS: 1 + * FCGI_MPXS_CONNS: 0 + * ... + */ + return 0; + } + + /* XXX: save role from FCGI_BEGIN_REQUEST; should keep independent state */ + static int role; + + switch (type) { + case FCGI_BEGIN_REQUEST: + role = (buf[offset+FCGI_HEADER_LEN] << 8) + | buf[offset+FCGI_HEADER_LEN+1]; + return 0; /* ignore; could save req_id and match further packets */ + case FCGI_ABORT_REQUEST: + return -2; /* done */ + case FCGI_END_REQUEST: + return -1; /* unexpected; this server is not sending FastCGI requests */ + case FCGI_PARAMS: + return fcgi_process_params(stream, req_id, role, + buf+offset+FCGI_HEADER_LEN, len); + case FCGI_STDIN: + /* XXX: TODO read and discard request body + * (currently ignored in these FastCGI unit tests) + * (make basic effort to read body; ignore any timeouts or errors) */ + return -1; /* unexpected; this server is not expecting request body */ + case FCGI_STDOUT: + return -1; /* unexpected; this server is not sending FastCGI requests */ + case FCGI_STDERR: + return -1; /* unexpected; this server is not sending FastCGI requests */ + case FCGI_DATA: + return -1; /* unexpected; this server is not sending FastCGI requests */ + case FCGI_GET_VALUES: + return 0; /* ignore; not implemented */ + case FCGI_GET_VALUES_RESULT: + return 0; /* ignore; not implemented */ + default: + return -1; /* unexpected */ + } +} + + +static ssize_t +fcgi_recv_packet (FILE * const stream, ssize_t sz) +{ + ssize_t offset = 0; + while (sz - offset >= (ssize_t)FCGI_HEADER_LEN) { + FCGI_Header * const h = (FCGI_Header *)(buf+offset); + uint32_t pad = h->paddingLength; + uint32_t len = (h->contentLengthB1 << 8) | h->contentLengthB0; + if (sz - offset < (ssize_t)(FCGI_HEADER_LEN + len + pad)) + break; + int rc = fcgi_dispatch_packet(stream, offset, len); + if (rc < 0) + return rc; + offset += (ssize_t)(FCGI_HEADER_LEN + len + pad); + } + return offset; +} + + +static int +fcgi_recv (const int fd, FILE * const stream) +{ + ssize_t rd = 0, offset = 0; + + /* XXX: remain blocking */ + /*fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);*/ + + do { + struct pollfd pfd = { fd, POLLIN, 0 }; + switch (poll(&pfd, 1, 10)) { /* 10ms timeout */ + default: /* 1; the only pfd has revents */ + break; + case -1: /* error */ + case 0: /* timeout */ + pfd.revents |= POLLERR; + break; + } + if (!(pfd.revents & POLLIN)) + break; + + do { + rd = recv(fd, buf+offset, sizeof(buf)-offset, MSG_DONTWAIT); + } while (rd < 0 && errno == EINTR); + + if (rd > 0) { + offset += rd; + rd = fcgi_recv_packet(stream, offset); + if (rd < 0) + return (-2 == rd) ? 0 : -1; /*(-2 indicates done)*/ + if (rd > 0) { + offset -= rd; + if (offset) + memmove(buf, buf+rd, offset); + } + } + else if (0 == rd || (errno != EAGAIN && errno != EWOULDBLOCK)) + break; + } while (offset < (ssize_t)sizeof(buf)); + + return -1; +} + + +int +main (void) +{ + int fd; + fcntl(FCGI_LISTENSOCK_FILENO, F_SETFL, + fcntl(FCGI_LISTENSOCK_FILENO, F_GETFL) & ~O_NONBLOCK); + + do { + fd = accept(FCGI_LISTENSOCK_FILENO, NULL, NULL); + if (fd < 0) + continue; + /* XXX: skip checking FCGI_WEB_SERVER_ADDRS; not implemented */ + + /* uses stdio to retain prior behavior of output buffering (default) + * and flushing with fflush() at specific points */ + FILE *stream = fdopen(fd, "r+"); + if (NULL == stream) { + close(fd); + continue; + } + fcgi_recv(fd, stream); + fflush(stream); + fclose(stream); + } while (fd > 0 ? !finished : errno == EINTR); -int main (void) { - - while (FCGI_Accept() >= 0) { - char* p; - - if (NULL != (p = getenv("QUERY_STRING"))) { - if (0 == strcmp(p, "lf")) { - printf("Status: 200 OK\n\n"); - } else if (0 == strcmp(p, "crlf")) { - printf("Status: 200 OK\r\n\r\n"); - } else if (0 == strcmp(p, "slow-lf")) { - printf("Status: 200 OK\n"); - fflush(stdout); - printf("\n"); - } else if (0 == strcmp(p,"slow-crlf")) { - printf("Status: 200 OK\r\n"); - fflush(stdout); - printf("\r\n"); - } else if (0 == strcmp(p, "die-at-end")) { - printf("Status: 200 OK\r\n\r\n"); - printf("test123"); - FCGI_Finish(); - break; - } else { - printf("Status: 200 OK\r\n\r\n"); - } - } else { - printf("Status: 500 Internal Foo\r\n\r\n"); - } - - if (0 == strcmp(p, "path_info")) { - printf("%s", getenv("PATH_INFO")); - } else if (0 == strcmp(p, "script_name")) { - printf("%s", getenv("SCRIPT_NAME")); - } else if (0 == strcmp(p, "var")) { - p = getenv("X_LIGHTTPD_FCGI_AUTH"); - printf("%s", p ? p : "(no value)"); - } else { - printf("test123"); - } - } - - return 0; + return 0; } -- cgit v1.2.1