
#include <stdio.h>
#include <string.h>


#include "cgi_request.h"

#include "remote_process.h"
#include "http_request.h"
#include "string_collection.h"
#include "fd_reader.h"
#include "fd_watcher.h"

struct s_CgiRequest
{
    HttpRequest *request;
    RemoteProcess *process;

    FDReader *stdoutReader;
    FDReader *stderrReader;

    bool readHeaders;
    bool finished;

    unsigned int entityOffset;
};

static void cgi_request_addEnvFromVariable (CgiRequest *instance, 
        const char *envName, int envLen, const char *varName);

static void cgi_request_addEnv (CgiRequest *instance, const char *envName, 
        int envLen, const char *value, int valLen);

static void cgi_request_addEnvFromHeader (CgiRequest *instance, 
        const char *envName, int envLen, const char *varName);

static void cgi_request_initEnvironment (CgiRequest *instance);

static void cgi_request_addRequestVars (CgiRequest *instance);

static void cgi_request_handleStdError (void *context, int fd, short events);

static void cgi_request_add_env_from_parent (CgiRequest *instance, const char *name, int len);

CgiRequest *
cgi_request_new (HttpRequest *request)
{
    CgiRequest *instance = (CgiRequest *) malloc (sizeof (CgiRequest));

    instance->request = request;

    instance->process = NULL;

    instance->readHeaders = false;

    instance->stdoutReader = NULL;

    instance->stderrReader = NULL;

    instance->entityOffset = 0;

    instance->finished = false;

    return (instance);
}

void
cgi_request_destroy (void *ptr)
{
    CgiRequest *instance = (CgiRequest *) ptr;

    /* 
     * even if getStdOut or getStdIn return -1, it shouldn't hurt to remove
     * that 
     */
    if (instance->process != NULL)
    {
        fd_watcher_removeWatchFD (fd_watcher_getInstance(), 
                remote_process_getStdOut (instance->process));

        fd_watcher_removeWatchFD (fd_watcher_getInstance(), 
                remote_process_getStdIn (instance->process));

        remote_process_destroy (instance->process);
    }

    http_request_destroy (instance->request);

    if (instance->stdoutReader != NULL)
    {
        fd_reader_destroy (instance->stdoutReader);
    }

    if (instance->stderrReader != NULL)
    {
        fd_reader_destroy (instance->stderrReader);
    }

    free (instance);
}

void
cgi_request_handler (HttpRequest *request, __attribute__((unused)) void *context)
{
    CgiRequest *cgiReq = cgi_request_new (request);

    cgi_request_start (cgiReq);
}

static void
cgi_request_finish (CgiRequest *instance, int status)
{
    assert (instance->finished == false);

    /* If we got headers but never sent them (if we didn't have a body) */
    if (instance->readHeaders == true && http_request_headersSent (instance->request) == false)
    {
        /* BUGBUG: We should be using something better than sending an empty
         * string to flush the headers.
         *
         *  -eric@5stops.com 5.15.2004
         */
        http_request_writeData (instance->request, "", 0);
    }

    instance->finished = true;

    http_request_finishStatus (instance->request, status);

    cgi_request_destroy (instance);
}

static bool
cgi_request_setNonBlocking (int fd)
{
    int fdFlags;

    assert (fd >= 0);

    if ((fdFlags = fcntl (fd, F_GETFL)) == -1)
    {
        return (false);
    }

    fdFlags |= O_NONBLOCK;

    if (fcntl (fd, F_SETFL, fdFlags) == -1)
    {
        return (false);
    }

    return (true);
}

static int
cgi_request_readHeaders (CgiRequest *instance)
{
    while (true)
    {
        char reqLine[1024];

        char *name_p;
        char *value_p;
        char *state_p;

        ssize_t ret = fd_reader_readLine (instance->stdoutReader, reqLine, sizeof (reqLine));

        if (ret < 1)
        {
            return (ret);
        }

        if (reqLine[0] == '\n')
        {
            break;
        }

        name_p = strtok_r (reqLine, ":", &state_p);
        value_p = state_p;

        if (value_p != NULL)
        {
            while (*value_p == ' ' && *value_p != '\n' && *value_p != '\0')
            {
                value_p++;
            }
        }
        else
        {
            /* could not parse headers */
            return (-1);
        }

        if (name_p != NULL && value_p != NULL)
        {
            int value_len = (reqLine + ret) - value_p;

            strFstrip_crlf (value_p, value_len);

            if (strcasecmp (name_p, "Status") == 0)
            {
                char *status = strtok_r (NULL, " ", &value_p);

                http_request_setHttpStatus (instance->request, atoi (status), value_p);
            }
            else
            {
                http_request_setResponseHeaderData (instance->request, name_p, value_p, value_len);
            }
        }
    }

    return (0);
}

static ssize_t
cgi_request_readContent (CgiRequest *instance)
{
    char buffer[1024];

    while (true)
    {
        ssize_t ret = fd_reader_read (instance->stdoutReader, buffer, sizeof (buffer));

        if (ret > 0)
        {
            if (http_request_writeData (instance->request, buffer, ret) == false)
            {
                /* if we couldn't write, we're obviously done here */
                return (0);
            }
        }
        else
        {
            return (ret);
        }
    }
}

static void
cgi_request_handleStdError (void *context, MECHA_UNUSED_ARGUMENT int fd, MECHA_UNUSED_ARGUMENT short events)
{
    CgiRequest *instance = (CgiRequest *) context;
    char lineBuf[1024];

    while (true)
    {
        ssize_t ret = fd_reader_readLine (instance->stderrReader, lineBuf, sizeof (lineBuf));

        if (ret == 0)
        {
            return;
        }

        if (ret < 0 && ret == -EAGAIN)
        {
            fd_watcher_watchFD (fd_watcher_getInstance(), 
                    remote_process_getStdErr (instance->process), 
                    cgi_request_handleStdError, instance, POLLIN);

            return;
        }

        if (ret == 1)
        {
            continue;
        }

        lineBuf[ret - 1] = 0;

        logFmsg (CONT_LOG_ERROR, "%s: %s", remote_process_get_executable (instance->process), lineBuf);
    }
}

static void
cgi_request_handleStdOut (void *context, MECHA_UNUSED_ARGUMENT int fd, MECHA_UNUSED_ARGUMENT short events)
{
    CgiRequest *instance = context;
    ssize_t ret;

    if (instance->readHeaders == false)
    {
        ret = cgi_request_readHeaders (instance);

        if (ret == 0)
        {
            /* we're done finding headers */
            instance->readHeaders = true;
        }
    }

    if (instance->readHeaders == true)
    {
        ret = cgi_request_readContent (instance);

        if (ret == 0)
        {
            /* we're done with the request */
            cgi_request_finish (instance, STATUS_EXIT);

            return;
        }
    }

    if (ret < 0 && ret != -EAGAIN)
    {
        char buf[1024];

        snprintf (buf, sizeof (buf), "Error while processing cgi: %s (%d)", strerror (-ret), -ret);

        logFmsg (CONT_LOG_DEBUG, "mod/cgi: %s", buf);

        if (http_request_headersSent (instance->request) == false)
        {
            http_request_setHttpStatus (instance->request, 500, "Error");

            http_request_writeString (instance->request, buf);
        }

        /* we have to bail */
        cgi_request_finish (instance, STATUS_ERROR);
    }
    else
    {
        fd_watcher_watchFD (fd_watcher_getInstance(), 
                remote_process_getStdOut (instance->process), 
                cgi_request_handleStdOut, instance, POLLIN);
    }
}

static void
cgi_request_handleStdIn (void *context, __attribute__((unused)) int fd, __attribute__((unused)) short events)
{
    CgiRequest *instance = context;
    const dynTstring *entity;
    int len;

    entity = http_request_getEntireEntity (instance->request);

    if (entity == NULL)
    {
        remote_process_closeStdIn (instance->process);

        return;
    }
        
    len = write (remote_process_getStdIn (instance->process), 
            dynFgetstr (entity) + instance->entityOffset,
            dynFgetlen (entity) - instance->entityOffset);

    if (len < 1)
    {
        remote_process_closeStdIn (instance->process);

        return;
    }

    instance->entityOffset += len;

    if (dynFgetlen (entity) == instance->entityOffset)
    {
        remote_process_closeStdIn (instance->process);

        return;
    }

    fd_watcher_watchFD (fd_watcher_getInstance(), 
            remote_process_getStdIn (instance->process), 
            cgi_request_handleStdIn, instance, POLLOUT);

}

bool
cgi_request_start (CgiRequest *instance)
{
    bool ret;

    const dynTstring *path = http_request_getVariableString (
            instance->request, "path");

    if (path == NULL)
    {
        /* we'll just let this fall through */
        cgi_request_finish (instance, STATUS_PROCEED);

        return (false);
    }

    {
        struct stat sbuf;

        if (lstat (dynFgetstr (path), &sbuf) == -1 || 
                !S_ISREG (sbuf.st_mode) || 
                (sbuf.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) == 0)
        {
            /* we can't run this, so we won't try */
            cgi_request_finish (instance, STATUS_PROCEED);

            return (false);
        }
    }

    instance->process = remote_process_new (
            dynFgetstr (path));

    cgi_request_initEnvironment (instance);

    ret = remote_process_start (instance->process);

    if (ret == false)
    {
        /* we'll just let this fall through */
        cgi_request_finish (instance, STATUS_PROCEED);

        return (false);
    }

    cgi_request_setNonBlocking (remote_process_getStdIn (instance->process));

    cgi_request_setNonBlocking (remote_process_getStdOut (instance->process));

    cgi_request_setNonBlocking (remote_process_getStdErr (instance->process));

    instance->stdoutReader = fd_reader_new (
            remote_process_getStdOut (instance->process));

    instance->stderrReader = fd_reader_new (
            remote_process_getStdErr (instance->process));

    http_request_setHttpStatus (instance->request, 200, "OK OK");

    {
        const dynTstring *method = http_request_getVariableString (
                instance->request, "method");

        if (method != NULL && strcmp (dynFgetstr (method), "POST") == 0)
        {
            fd_watcher_watchFD (fd_watcher_getInstance(), 
                    remote_process_getStdIn (instance->process), 
                    cgi_request_handleStdIn, instance, POLLOUT);
        }
        else
        {
            remote_process_closeStdIn (instance->process);
        }
    }

    fd_watcher_watchFD (fd_watcher_getInstance(), 
            remote_process_getStdOut (instance->process), 
            cgi_request_handleStdOut, instance, POLLIN);

    fd_watcher_watchFD (fd_watcher_getInstance(), 
            remote_process_getStdErr (instance->process), 
            cgi_request_handleStdError, instance, POLLIN);

    return (true);
}

static void
cgi_request_initEnvironment (CgiRequest *instance)
{
    cgi_request_add_env_from_parent (instance, "LD_PRELOAD", 10);
    cgi_request_add_env_from_parent (instance, "LD_LIBRARY_PATH", 15);
    cgi_request_add_env_from_parent (instance, "VG_ARGS", 7);

    cgi_request_add_env_from_parent (instance, "TERM", 4);
    cgi_request_add_env_from_parent (instance, "PATH", 4);
    cgi_request_add_env_from_parent (instance, "HOSTTYPE", 8);
    cgi_request_add_env_from_parent (instance, "VENDOR", 6);
    cgi_request_add_env_from_parent (instance, "OSTYPE", 6);
    cgi_request_add_env_from_parent (instance, "LANG", 4);
    cgi_request_add_env_from_parent (instance, "MACHTYPE", 8);
    cgi_request_add_env_from_parent (instance, "PWD", 3);
    cgi_request_add_env_from_parent (instance, "HOST", 4);
    cgi_request_add_env_from_parent (instance, "HOSTNAME", 8);
    cgi_request_add_env_from_parent (instance, "TZ", 2);

    cgi_request_addEnvFromVariable (instance, "SERVER_SOFTWARE", 
            15, "server");

    /* SERVER_NAME */

    cgi_request_addEnv (instance, "GATEWAY_INTERFACE", 
            17, "CGI/1.1", 7);

    cgi_request_addEnvFromVariable (instance, "SERVER_PROTOCOL", 
            15, "protocol");

    /*
    cgi_request_addEnvFromVariable (instance, "SERVER_PORT", 
            11, "");
            */

    cgi_request_addEnvFromVariable (instance, "REQUEST_METHOD", 
            14, "method");

    cgi_request_addEnvFromVariable (instance, "PATH_INFO", 
            9, "path_info");

    cgi_request_addEnvFromVariable (instance, "PATH_TRANSLATED", 
            15, "path_translated");

    cgi_request_addEnvFromVariable (instance, "SCRIPT_NAME", 
            11, "uri");

    cgi_request_addEnvFromVariable (instance, "QUERY_STRING", 
            12, "query");

    /*
     * we don't have the resolved version of REMOTE_ADDR from the webserver,
     * and there isn't any benefit of resolving it ourselves.
     *
     * ----
     *
    cgi_request_addEnvFromVariable (instance, "REMOTE_HOST", 
            11, "");
            */

    cgi_request_addEnv (instance, "REMOTE_ADDR", 
            11, http_request_getRemoteIP (instance->request),
            strlen (http_request_getRemoteIP (instance->request)));

    /*
    cgi_request_addEnvFromVariable (instance, "AUTH_TYPE", 
            9, "");
            */

    /*
    cgi_request_addEnvFromVariable (instance, "REMOTE_USER", 
            11, "");
            */

    /*
    cgi_request_addEnvFromVariable (instance, "REMOTE_IDENT", 
            12, "");
            */

    cgi_request_addEnvFromHeader (instance, "CONTENT_TYPE", 
            12, "content-type");

    cgi_request_addEnvFromHeader (instance, "CONTENT_LENGTH", 
            14, "content-length");

    /* This is added by apache and seems like a good idea */
    cgi_request_addEnvFromVariable (instance, "SCRIPT_FILENAME", 
            15, "path");

    cgi_request_addRequestVars (instance);
}

static void
cgi_request_add_env_from_parent (CgiRequest *instance, const char *name, int len)
{
    char *e = getenv (name);

    if (e != NULL)
    {
        cgi_request_addEnv (instance, name, len, e, strlen (e));
    }
}

static void
cgi_request_addRequestVars (CgiRequest *instance)
{
    hshTiterator *iter = http_request_getRequestHeaderIterator (
            instance->request);

    dynTstring *newKey = dynFinit();

    while (hshFiterator_next (iter) == 1)
    {
        const char *key = (const char *) hshFiterator_current_key (iter);
        const dynTstring *value = (const dynTstring *)
            hshFiterator_current_value (iter);

        dynFreset (newKey);
        dynFappend (newKey, "HTTP_", 5);

        while (*key != 0)
        {
            if (*key == '-')
            {
                dynFappend (newKey, "_", 1);
            }
            else
            {
                char c = toupper (*key);

                dynFappend (newKey, &c, 1);
            }

            key++;
        }

        cgi_request_addEnv (instance, dynFgetstr (newKey),
                dynFgetlen (newKey), dynFgetstr (value),
                dynFgetlen (value));
    }

    hshFiterator_free (iter);
    dynFfree (newKey);
}

static void
cgi_request_addEnv (CgiRequest *instance, const char *envName, 
        int envLen, const char *value, int valLen)
{
    dynTstring *envString = dynFinit();

    dynFappend (envString, envName, envLen);

    dynFappend (envString, "=", 1);

    if (valLen > 0)
    {
        dynFappend (envString, value, valLen);
    }

    remote_process_addEnvironmentData (instance->process,
            dynFgetstr (envString), dynFgetlen (envString));

    dynFfree (envString);
}

static void
cgi_request_addEnvFromHeader (CgiRequest *instance, const char *envName, 
        int envLen, const char *varName)
{
    const dynTstring *value = http_request_getRequestHeaderString (
            instance->request, varName);

    if (value != NULL)
    {
        cgi_request_addEnv (instance, envName, envLen, 
                dynFgetstr (value), dynFgetlen (value));
    }
}

static void
cgi_request_addEnvFromVariable (CgiRequest *instance, const char *envName, 
        int envLen, const char *varName)
{
    const dynTstring *value = http_request_getVariableString (
            instance->request, varName);

    if (value != NULL)
    {
        cgi_request_addEnv (instance, envName, envLen, 
                dynFgetstr (value), dynFgetlen (value));
    }
}
