/*=============================================================================
                             response
===============================================================================
  This module contains callbacks from and services for a request handler.

  Copyright information is at the end of the file
=============================================================================*/

#define _XOPEN_SOURCE 600  /* Make sure strdup() is in <string.h> */

#include <ctype.h>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <time.h>

#include "xmlrpc_config.h"
#include "bool.h"
#include "int.h"
#include "version.h"
#include "mallocvar.h"
#include "xmlrpc-c/string_int.h"
#include "xmlrpc-c/abyss.h"

#include "trace.h"
#include "server.h"
#include "session.h"
#include "file.h"
#include "conn.h"
#include "token.h"
#include "date.h"
#include "data.h"
#include "abyss_info.h"
#include "http.h"



void
ResponseError2(TSession *   const sessionP,
               const char * const explanation) {

    const char * errorDocument;

    ResponseAddField(sessionP, "Content-type", "text/html");

    ResponseWriteStart(sessionP);

    xmlrpc_asprintf(&errorDocument,
                    "<HTML><HEAD><TITLE>Error %d</TITLE></HEAD>"
                    "<BODY>"
                    "<H1>Error %d</H1>"
                    "<P>%s</P>" SERVER_HTML_INFO
                    "</BODY>"
                    "</HTML>",
                    sessionP->status, sessionP->status, explanation);

    ConnWrite(sessionP->connP, errorDocument, strlen(errorDocument),
              CONN_EXPECT_NOTHING);

    xmlrpc_strfree(errorDocument);
}



void
ResponseError(TSession * const sessionP) {

    ResponseError2(sessionP, HTTPReasonByStatus(sessionP->status));
}




abyss_bool
ResponseChunked(TSession * const sessionP) {
/*----------------------------------------------------------------------------
   Make the response chunked.

   This is only a hope, things will be real only after a call of
   ResponseWriteStart()
-----------------------------------------------------------------------------*/
    assert(!sessionP->responseStarted);

    sessionP->chunkedwrite =
        (sessionP->version.major > 1) ||
        (sessionP->version.major == 1 && (sessionP->version.minor >= 1));

    sessionP->chunkedwritemode = true;

    return true;
}



void
ResponseStatus(TSession *     const sessionP,
               unsigned short const code) {
/*----------------------------------------------------------------------------
   Set the status code for the response to 'code'.
-----------------------------------------------------------------------------*/
    sessionP->status = code;
}



xmlrpc_uint16_t
ResponseStatusFromErrno(int const errnoArg) {
/*----------------------------------------------------------------------------
   The appropriate HTTP status code for a POSIX errno of 'errnoArg'.
-----------------------------------------------------------------------------*/
    uint16_t code;

    switch (errnoArg) {
    case EACCES:
        code=403;
        break;
    case ENOENT:
        code=404;
        break;
    default:
        code=500;
    }
    return code;
}



void
ResponseStatusErrno(TSession * const sessionP) {
/*----------------------------------------------------------------------------
   Set the response state from the global 'errno' value (i.e. the result
   of the last system call Caller made).
-----------------------------------------------------------------------------*/
    ResponseStatus(sessionP, ResponseStatusFromErrno(errno));
}



static bool
isValidHttpToken(const char * const token) {

    char const separators[] = "()<>@,;:\\\"/[]?={} \t";
    const char * p;
    bool valid;

    for (p = &token[0], valid = true; *p; ++p) {
        if (!isprint(*p) || strchr(separators, *p))
            valid = false;
    }
    return valid;
}




static bool
isValidHttpText(const char * const text) {

    const char * p;
    bool valid;

    for (p = &text[0], valid = true; *p; ++p) {
        if (!isprint(*p))
            valid = false;
    }
    return valid;
}



abyss_bool
ResponseAddField(TSession *   const sessionP,
                 const char * const name,
                 const char * const value) {

    abyss_bool succeeded;

    if (!isValidHttpToken(name)) {
        TraceMsg("Supplied HTTP header field name is not a valid HTTP token");
        succeeded = false;
    } else if (!isValidHttpText(value)) {
        TraceMsg("Supplied HTTP header field value is not valid HTTP text");
        succeeded = false;
    } else {
        succeeded = TableAdd(&sessionP->responseHeaderFields, name, value);
    }
    return succeeded;
}



static void
addConnectionHeaderFld(TSession * const sessionP) {

    struct _TServer * const srvP = ConnServer(sessionP->connP)->srvP;

    if (HTTPKeepalive(sessionP)) {
        const char * keepaliveValue;

        ResponseAddField(sessionP, "Connection", "Keep-Alive");

        xmlrpc_asprintf(&keepaliveValue, "timeout=%u, max=%u",
                        srvP->keepalivetimeout, srvP->keepalivemaxconn);

        ResponseAddField(sessionP, "Keep-Alive", keepaliveValue);

        xmlrpc_strfree(keepaliveValue);
    } else
        ResponseAddField(sessionP, "Connection", "close");
}



static void
addDateHeaderFld(TSession * const sessionP) {

    if (sessionP->status >= 200) {
        const char * dateValue;

        DateToString(sessionP->date, &dateValue);

        if (dateValue) {
            ResponseAddField(sessionP, "Date", dateValue);
            xmlrpc_strfree(dateValue);
        }
    }
}



static void
addServerHeaderFld(TSession * const sessionP) {

    const char * serverValue;

    xmlrpc_asprintf(&serverValue, "Xmlrpc-c_Abyss/%s", XMLRPC_C_VERSION);

    ResponseAddField(sessionP, "Server", serverValue);

    xmlrpc_strfree(serverValue);
}



static unsigned int
leadingWsCt(const char * const arg) {

    unsigned int i;

    for (i = 0; arg[i] && isspace(arg[i]); ++i);

    return i;
}



static unsigned int
trailingWsPos(const char * const arg) {

    unsigned int i;

    for (i = strlen(arg); i > 0 && isspace(arg[i-1]); --i);

    return i;
}



static const char *
formatFieldValue(const char * const unformatted) {
/*----------------------------------------------------------------------------
   Return the string of characters that goes after the colon on the
   HTTP header field line, given that 'unformatted' is its basic value.
-----------------------------------------------------------------------------*/
    const char * retval;

    /* An HTTP header field value may not have leading or trailing white
       space.
    */
    char * buffer;

    buffer = malloc(strlen(unformatted) + 1);

    if (buffer == NULL)
        retval = xmlrpc_strnomemval();
    else {
        unsigned int const lead  = leadingWsCt(unformatted);
        unsigned int const trail = trailingWsPos(unformatted);
        assert(trail >= lead);
        strncpy(buffer, &unformatted[lead], trail - lead);
        buffer[trail - lead] = '\0';
        retval = buffer;
    }
    return retval;
}



static void
sendHeader(TConn * const connP,
           TTable  const fields) {
/*----------------------------------------------------------------------------
   Send the HTTP response header whose fields are fields[].

   Don't include the blank line that separates the header from the body.

   fields[] contains syntactically valid HTTP header field names and values.
   But to the extent that int contains undefined field names or semantically
   invalid values, the header we send is invalid.

   Tell connection to expect more data for the same logical message.
-----------------------------------------------------------------------------*/
    unsigned int i;

    for (i = 0; i < fields.size; ++i) {
        TTableItem * const fieldP = &fields.item[i];
        const char * const fieldValue = formatFieldValue(fieldP->value);

        const char * line;

        xmlrpc_asprintf(&line, "%s: %s\r\n", fieldP->name, fieldValue);
        ConnWrite(connP, line, strlen(line), CONN_EXPECT_MORE);
        xmlrpc_strfree(line);
        xmlrpc_strfree(fieldValue);
    }
}


void
ResponseWriteStart(TSession * const sessionP) {
/*----------------------------------------------------------------------------
   Begin the process of sending the response for an HTTP transaction
   (i.e. Abyss session).

   As part of this, send the entire HTTP header for the response.
-----------------------------------------------------------------------------*/
    struct _TServer * const srvP = ConnServer(sessionP->connP)->srvP;

    assert(!sessionP->responseStarted);

    if (sessionP->status == 0) {
        /* Handler hasn't set status.  That's an error */
        TraceMsg("Abyss client called ResponseWriteStart() on "
                 "a session for which he has not set the request status "
                 "('status' member of TSession).  Using status 500\n");
        sessionP->status = 500;
    }

    sessionP->responseStarted = true;

    {
        const char * const reason = HTTPReasonByStatus(sessionP->status);
        const char * line;
        xmlrpc_asprintf(&line,"HTTP/1.1 %u %s\r\n", sessionP->status, reason);
        ConnWrite(sessionP->connP, line, strlen(line), CONN_EXPECT_MORE);
        xmlrpc_strfree(line);
    }

    addConnectionHeaderFld(sessionP);

    if (sessionP->chunkedwrite && sessionP->chunkedwritemode)
        ResponseAddField(sessionP, "Transfer-Encoding", "chunked");

    addDateHeaderFld(sessionP);

    if (srvP->advertise)
        addServerHeaderFld(sessionP);

    /* Note that sessionP->responseHeaderFields is defined to contain
       syntactically but not necessarily semantically valid header
       field names and values.
    */
    sendHeader(sessionP->connP, sessionP->responseHeaderFields);

    ConnWrite(sessionP->connP, "\r\n", 2, CONN_EXPECT_NOTHING);
}



abyss_bool
ResponseWriteBody(TSession *      const sessionP,
                  const char *    const data,
                  xmlrpc_uint32_t const len) {

    return HTTPWriteBodyChunk(sessionP, data, len);
}



abyss_bool
ResponseWriteEnd(TSession * const sessionP) {

    return HTTPWriteEndChunk(sessionP);
}



abyss_bool
ResponseContentType(TSession *   const serverP,
                    const char * const type) {

    return ResponseAddField(serverP, "Content-type", type);
}



abyss_bool
ResponseContentLength(TSession *      const sessionP,
                      xmlrpc_uint64_t const len) {

    char contentLengthValue[32];

    sprintf(contentLengthValue, "%" PRIu64, len);

    return ResponseAddField(sessionP, "Content-length", contentLengthValue);
}



void
ResponseAccessControl(TSession *        const abyssSessionP,
                      ResponseAccessCtl const accessControl) {

    if (accessControl.allowOrigin) {
        ResponseAddField(abyssSessionP, "Access-Control-Allow-Origin",
                         accessControl.allowOrigin);
        ResponseAddField(abyssSessionP, "Access-Control-Allow-Methods",
                         "POST");
        ResponseAddField(abyssSessionP, "Access-Control-Allow-Headers",
                         "Content-Type");
        ResponseAddField(abyssSessionP, "Access-Control-Allow-Headers",
                         "Content-Length");
        if (accessControl.expires) {
            char buffer[64];
            sprintf(buffer, "%u", accessControl.maxAge);
            ResponseAddField(abyssSessionP, "Access-Control-Max-Age", buffer);
        }
    }
}



/*********************************************************************
** MIMEType
*********************************************************************/

struct MIMEType {
    TList typeList;
    TList extList;
    TPool pool;
};


static MIMEType * globalMimeTypeP = NULL;



MIMEType *
MIMETypeCreate(void) {

    MIMEType * MIMETypeP;

    MALLOCVAR(MIMETypeP);

    if (MIMETypeP) {
        ListInit(&MIMETypeP->typeList);
        ListInit(&MIMETypeP->extList);
        PoolCreate(&MIMETypeP->pool, 1024);
    }
    return MIMETypeP;
}



void
MIMETypeDestroy(MIMEType * const MIMETypeP) {

    PoolFree(&MIMETypeP->pool);

    free(MIMETypeP);
}



void
MIMETypeInit(void) {

    if (globalMimeTypeP != NULL)
        abort();

    globalMimeTypeP = MIMETypeCreate();
}



void
MIMETypeTerm(void) {

    if (globalMimeTypeP == NULL)
        abort();

    MIMETypeDestroy(globalMimeTypeP);

    globalMimeTypeP = NULL;
}



static void
mimeTypeAdd(MIMEType *   const MIMETypeP,
            const char * const type,
            const char * const ext,
            bool *       const successP) {

    uint16_t index;
    void * mimeTypesItem;
    bool typeIsInList;

    assert(MIMETypeP != NULL);

    typeIsInList = ListFindString(&MIMETypeP->typeList, type, &index);
    if (typeIsInList)
        mimeTypesItem = MIMETypeP->typeList.item[index];
    else
        mimeTypesItem = (void*)PoolStrdup(&MIMETypeP->pool, type);

    if (mimeTypesItem) {
        bool extIsInList;
        extIsInList = ListFindString(&MIMETypeP->extList, ext, &index);
        if (extIsInList) {
            MIMETypeP->typeList.item[index] = mimeTypesItem;
            *successP = true;
        } else {
            void * extItem = (void*)PoolStrdup(&MIMETypeP->pool, ext);
            if (extItem) {
                bool addedToMimeTypes;

                addedToMimeTypes =
                    ListAdd(&MIMETypeP->typeList, mimeTypesItem);
                if (addedToMimeTypes) {
                    bool addedToExt;

                    addedToExt = ListAdd(&MIMETypeP->extList, extItem);
                    *successP = addedToExt;
                    if (!*successP)
                        ListRemove(&MIMETypeP->typeList);
                } else
                    *successP = false;
                if (!*successP)
                    PoolReturn(&MIMETypeP->pool, extItem);
            } else
                *successP = false;
        }
    } else
        *successP = false;
}




abyss_bool
MIMETypeAdd2(MIMEType *   const MIMETypeArg,
             const char * const type,
             const char * const ext) {

    MIMEType * MIMETypeP = MIMETypeArg ? MIMETypeArg : globalMimeTypeP;

    bool success;

    if (MIMETypeP == NULL)
        success = false;
    else
        mimeTypeAdd(MIMETypeP, type, ext, &success);

    return success;
}



abyss_bool
MIMETypeAdd(const char * const type,
            const char * const ext) {

    return MIMETypeAdd2(globalMimeTypeP, type, ext);
}



static const char *
mimeTypeFromExt(MIMEType *   const MIMETypeP,
                const char * const ext) {

    const char * retval;
    uint16_t extindex;
    bool extIsInList;

    assert(MIMETypeP != NULL);

    extIsInList = ListFindString(&MIMETypeP->extList, ext, &extindex);
    if (!extIsInList)
        retval = NULL;
    else
        retval = MIMETypeP->typeList.item[extindex];

    return retval;
}



const char *
MIMETypeFromExt2(MIMEType *   const MIMETypeArg,
                 const char * const ext) {

    const char * retval;

    MIMEType * MIMETypeP = MIMETypeArg ? MIMETypeArg : globalMimeTypeP;

    if (MIMETypeP == NULL)
        retval = NULL;
    else
        retval = mimeTypeFromExt(MIMETypeP, ext);

    return retval;
}



const char *
MIMETypeFromExt(const char * const ext) {

    return MIMETypeFromExt2(globalMimeTypeP, ext);
}



static void
findExtension(const char *  const fileName,
              const char ** const extP) {

    unsigned int extPos = 0;  /* stifle unset variable warning */
        /* Running estimation of where in fileName[] the extension starts */
    bool extFound;
    unsigned int i;

    /* We're looking for the last dot after the last slash */
    for (i = 0, extFound = false; fileName[i]; ++i) {
        char const c = fileName[i];

        if (c == '.') {
            extFound = true;
            extPos = i + 1;
        }
        if (c == '/')
            extFound = false;
    }

    if (extFound)
        *extP = &fileName[extPos];
    else
        *extP = NULL;
}



static const char *
mimeTypeFromFileName(MIMEType *   const MIMETypeP,
                     const char * const fileName) {

    const char * retval;
    const char * ext;

    assert(MIMETypeP != NULL);

    findExtension(fileName, &ext);

    if (ext)
        retval = MIMETypeFromExt2(MIMETypeP, ext);
    else
        retval = "application/octet-stream";

    return retval;
}



const char *
MIMETypeFromFileName2(MIMEType *   const MIMETypeArg,
                      const char * const fileName) {

    const char * retval;

    MIMEType * MIMETypeP = MIMETypeArg ? MIMETypeArg : globalMimeTypeP;

    if (MIMETypeP == NULL)
        retval = NULL;
    else
        retval = mimeTypeFromFileName(MIMETypeP, fileName);

    return retval;
}



const char *
MIMETypeFromFileName(const char * const fileName) {

    return MIMETypeFromFileName2(globalMimeTypeP, fileName);
}



static bool
fileContainsText(const char * const fileName) {
/*----------------------------------------------------------------------------
   Return true iff we can read the contents of the file named 'fileName'
   and see that it appears to be composed of plain text characters.
-----------------------------------------------------------------------------*/
    bool retval;
    bool fileOpened;
    TFile * fileP;

    fileOpened = FileOpen(&fileP, fileName, O_BINARY | O_RDONLY);
    if (fileOpened) {
        char const ctlZ = 26;
        unsigned char buffer[80];
        int32_t readRc;
        unsigned int i;

        readRc = FileRead(fileP, buffer, sizeof(buffer));

        if (readRc >= 0) {
            unsigned int bytesRead = readRc;
            bool nonTextFound;

            nonTextFound = false;  /* initial value */

            for (i = 0; i < bytesRead; ++i) {
                char const c = buffer[i];
                if (c < ' ' && !isspace(c) && c != ctlZ)
                    nonTextFound = true;
            }
            retval = !nonTextFound;
        } else
            retval = false;
        FileClose(fileP);
    } else
        retval = false;

    return retval;
}



static const char *
mimeTypeGuessFromFile(MIMEType *   const MIMETypeP,
                      const char * const fileName) {

    const char * retval;
    const char * ext;

    findExtension(fileName, &ext);

    retval = NULL;

    if (ext && MIMETypeP)
        retval = MIMETypeFromExt2(MIMETypeP, ext);

    if (!retval) {
        if (fileContainsText(fileName))
            retval = "text/plain";
        else
            retval = "application/octet-stream";
    }
    return retval;
}



const char *
MIMETypeGuessFromFile2(MIMEType *   const MIMETypeArg,
                       const char * const fileName) {

    return mimeTypeGuessFromFile(MIMETypeArg ? MIMETypeArg : globalMimeTypeP,
                                 fileName);
}



const char *
MIMETypeGuessFromFile(const char * const fileName) {

    return mimeTypeGuessFromFile(globalMimeTypeP, fileName);
}



/******************************************************************************
**
** http.c
**
** Copyright (C) 2000 by Moez Mahfoudh <mmoez@bigfoot.com>.
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
** ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
** OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
** SUCH DAMAGE.
**
******************************************************************************/
