/* * 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 25ms or less (or fail test) * - no read timeouts for request body; might block reading request body * - no write timeouts; might block writing response * - no retry if partial send * * Copyright(c) 2020 Glenn Strauss gstrauss()gluelogic.com All rights reserved * License: BSD 3-clause (same as lighttpd) */ #if defined(__sun) #define __EXTENSIONS__ #endif #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #define VC_EXTRALEAN #define _CRT_SECURE_NO_WARNINGS #ifdef _MSC_VER #pragma comment(lib, "ws2_32.lib") #pragma warning(disable:4267) #pragma warning(disable:5105) /* warning in winbase.h; good job MS */ #endif #endif #include #include #include #include #include #include #include #ifndef _WIN32 #include #include #include #include #include #ifdef HAVE_SIGNAL /* XXX: must be defined; config.h not included here */ #include #endif #else /* _WIN32 */ #include #include #include /* SSIZE_T */ #define ssize_t SSIZE_T #define poll WSAPoll /* fdopen() is not valid on _WIN32 SOCKET type; emulate fwrite(), * though note that the following does not handle partial send() */ typedef uintptr_t FILE; #define fwrite(ptr,sz,n,stream) \ (send((SOCKET)(stream), (const char *)(ptr), (int)((sz)*(n)), 0) \ == (int)((sz)*(n)) ? (n) : 0) #define fflush(x) do { } while (0) #endif /* _WIN32 */ #ifndef MSG_DONTWAIT #define MSG_DONTWAIT 0 #endif #include "../src/compat/fastcgi.h" static int finished; static unsigned char buf[65536]; #ifdef _WIN32 static int sock_nb_set (SOCKET fd, unsigned int nb) { u_long l = nb; return ioctlsocket(fd, FIONBIO, &l); } #else #if 0 /*(unused)*/ static int sock_nb_set (int fd, unsigned int nb) { return nb ? fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK) : fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK); } #endif #endif 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, (int)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 = (int)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 (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; } #ifdef _WIN32 static int fcgi_recv (const SOCKET fd, FILE * const stream) #else static int fcgi_recv (const int fd, FILE * const stream) #endif { ssize_t rd = 0, offset = 0; /* XXX: remain blocking */ /*sock_nb_set(fd, 1);*/ do { struct pollfd pfd = { fd, POLLIN, 0 }; switch (poll(&pfd, 1, 25)) { /* 25ms 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, (char *)buf+offset, sizeof(buf)-offset, MSG_DONTWAIT); } #ifdef _WIN32 while (rd < 0 && WSAGetLastError() == WSAEINTR); #else while (rd < 0 && errno == EINTR); #endif 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); } } #ifdef _WIN32 else if (0 == rd || WSAGetLastError() != WSAEWOULDBLOCK) #else else if (0 == rd || (errno != EAGAIN && errno != EWOULDBLOCK)) #endif break; } while (offset < (ssize_t)sizeof(buf)); return -1; } int main (void) { #ifdef _WIN32 WSADATA wsaData; WORD wVersionRequested = MAKEWORD(2, 2); if (0 != WSAStartup(wVersionRequested, &wsaData)) return -1; /* FCGI_LISTENSOCK_FILENO == STDIN_FILENO == 0 */ SOCKET lfd = (SOCKET)GetStdHandle(STD_INPUT_HANDLE); sock_nb_set(lfd, 0); SOCKET fd; do { fd = accept(lfd, NULL, NULL); if (fd == INVALID_SOCKET) continue; /* XXX: skip checking FCGI_WEB_SERVER_ADDRS; not implemented */ /* fdopen() is not valid on _WIN32 SOCKET; pass (FILE *)fd through */ fcgi_recv(fd, (FILE *)(uintptr_t)fd); } while (fd != INVALID_SOCKET ? 0 == closesocket(fd) && !finished : WSAGetLastError() == WSAEINTR); WSACleanup(); return 0; #else int fd; fcntl(FCGI_LISTENSOCK_FILENO, F_SETFL, fcntl(FCGI_LISTENSOCK_FILENO, F_GETFL) & ~O_NONBLOCK); #ifdef HAVE_SIGNAL signal(SIGINT, SIG_IGN); signal(SIGUSR1, SIG_IGN); #endif 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); return 0; #endif }