/*
 * 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 <sys/types.h>
#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#ifndef _WIN32
#include <stdio.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#ifdef HAVE_SIGNAL      /* XXX: must be defined; config.h not included here */
#include <signal.h>
#endif
#else /* _WIN32 */
#include <io.h>
#include <winsock2.h>
#include <basetsd.h> /* 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
}
