/*============================================================================

    project:    mod_libpq
    file:       mod_libpq.c
    author:     Andrew Smith
    date:       2005-06-03
    language:   C


    NOTES

    Apache module for creating persistent connections to PostgreSQL.

    Copyright (c) 2005,2006, Andrew Smith <http://asmith.id.au>
    All rights reserved.


------------------------------------------------------------------------------

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.

    * 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.

    * Neither the name of Andrew Smith nor the names of his contributors
      may be used to endorse or promote products derived from this software
      without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
    OWNER 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.


------------------------------------------------------------------------------
-- INSTALLATION --------------------------------------------------------------


    # requires minimum of apache 1.3 and postgresql 7.4 development libraries

    apxs -c -I /usr/include/postgresql/ -lpq -o mod_libpq.so mod_libpq.c
    install -s -m 644 mod_libpq.so /usr/lib/apache/1.3/



------------------------------------------------------------------------------
-- CONFIGURATION (add to httpd.conf) -----------------------------------------


    LoadModule libpq_module /usr/lib/apache/1.3/mod_libpq.so

#
# provide the connection string for the database (mandatory)
#

    libpqConnection "host=localhost dbname=mydb"

#
# specify the connection mode (optional, defaults to Later)
#
#   Now     establish the database connection immediately
#   Later   establish the database connection on the first request
#   Never   disable the database connection (Service Unavailable)
#

#   libpqConnectWhen Later

#
# location specific parameters
#

    <Location "/index.html">
        SetHandler libpq-request

#
# specify an identifier for requests (optional, defaults to the request string)
#

#       libpqSystem "GET /index.html HTTP/1.1"

#
# specify an sql function to call to process the request headers (optional)
# the sql function must return cookie data for tracking the request
#
#   $1      value of libpqSystem or the entire request string
#   $2      the remote IP address as text
#   $3      the cookie data sent by the client software
#   $4      the arguments parsed from the request string
#   $5      the data submitted with the request
#   $6      the client software identification string
#   $7      the referer from the request
#

#       libpqHeadersQuery "SELECT cgi.Headers($1,$2,$3,$4,$5,$6,$7)"

#
# specify an sql function to call to obtain the response (optional)
# the sql function must return the text of the response to the request
#
#   $1      the tracking token returned by the libpqQuery function
#

#       libpqContentQuery "SELECT cgi.Content($1)"

#
# specify the content type of the document (optional, defaults to text/html)
#

#       libpqContentType "text/html; charset=utf-8"

    </Location>



------------------------------------------------------------------------------
-- IMPLEMENTATION (add to mydb) ----------------------------------------------


    CREATE SCHEMA cgi;

    SET SEARCH_PATH TO cgi;


    -- preprocess the request and return a unique token for the cookie

    CREATE FUNCTION Headers
    (
        aSystem TEXT,
        aAddress TEXT,
        aCookie TEXT,
        aQuery TEXT,
        aInput TEXT,
        aBrowser TEXT,
        aReferer TEXT
    )
        RETURNS TEXT AS $$
    DECLARE
        vToken TEXT;
    BEGIN
        --
        -- an 'x=' prefix is required for some browsers to recognise cookies
        --
        vToken := 'token='||TEXT(CURRENT_TIMESTAMP);
        RETURN vToken;
    END;
    $$ LANGUAGE plpgsql VOLATILE;


    -- generate the response for the request identified by the token

    CREATE FUNCTION Content
    (
        aToken TEXT
    )
        RETURNS TEXT AS $$
    DECLARE
        vDocument TEXT;
    BEGIN
        vDocument := 'Hello world!\nYour cookie is "'||aToken||'"';
        RETURN vDocument;
    END;
    $$ LANGUAGE plpgsql VOLATILE;


------------------------------------------------------------------------------


============================================================================*/



// include files -------------------------------------------------------------

#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_main.h"
#include "http_protocol.h"
#include "util_script.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <libpq-fe.h>


#define kVersion "mod_libpq-0.1a3"


// Headers(System,Address,Query,Cookie,Submit,Browser,Referer) => Token
// Content(Token) => Content

#define kHeadersQuery "SELECT cgi.Headers($1,$2,$3,$4,$5,$6,$7)"
#define kContentQuery "SELECT cgi.Content($1)"
#define kContentType "text/html; charset=utf-8"
#define kConnectNow "now"
#define kConnectLater "later"
#define kConnectNever "never"

module MODULE_VAR_EXPORT libpq_module;


typedef struct tLocation
{
    char *SystemP;
    char *HeadersQueryP;
    char *ContentQueryP;
    char *ContentTypeP;
} tLocation;


typedef struct tConfig
{
    char *ConnectionP;
    char *ConnectWhenP;
} tConfig;


typedef struct tVirtual
{
    struct tVirtual *VirtualP;
    server_rec *ServerP;
    PGconn *DatabaseP;
} tVirtual;


static tVirtual *gVirtualP;


//----------------------------------------------------------------------------
// local functions -----------------------------------------------------------

#define kMaxRetries 3


// attempt to execute a query

static PGresult *CallSql
(
    PGconn *DatabaseP,
    const char *QueryP,
    const char **ArgumentsP,
    int Count
)
{
    PGresult *ResultP;
    int Retries = 0;

    ResultP = PQexecParams
    (
        DatabaseP,
        QueryP,
        Count,
        NULL,
        ArgumentsP,
        NULL,
        NULL,
        0
    );

    while
    (
        (Retries++ < kMaxRetries) &&
        (PQstatus(DatabaseP) == CONNECTION_BAD)
    )
    {
        PQclear(ResultP);
        PQreset(DatabaseP);
        ResultP = PQexecParams
        (
            DatabaseP,
            QueryP,
            Count,
            NULL,
            ArgumentsP,
            NULL,
            NULL,
            0
        );
    }

    if
    (
        (PQstatus(DatabaseP) == CONNECTION_BAD) ||
        (PQresultStatus(ResultP) != PGRES_TUPLES_OK)
    )
    {
        PQclear(ResultP);
        ResultP = NULL;
    }

    return ResultP;
}


// get the server configuration from a server record

static tConfig*
ServerConfigP(server_rec *ServerP)
{
    return (tConfig*)
        ap_get_module_config(ServerP->module_config,&libpq_module);
}


// get the location configuration from a request record

static tLocation*
LocationConfigP(request_rec *RequestP)
{
    return (tLocation*)
        ap_get_module_config(RequestP->per_dir_config,&libpq_module);
}



//----------------------------------------------------------------------------
// command handlers ----------------------------------------------------------


// receive the interface database connection string

static const char*
Connection(cmd_parms *CommandP, void *ModuleP, char *ValueP)
{
    server_rec *ServerP = CommandP->server;
    tConfig *ConfigP = ServerConfigP(ServerP);
    ConfigP->ConnectionP = ap_pstrdup(CommandP->pool,ValueP);
    return NULL;
}


// receive the database system code

static const char*
System(cmd_parms *CommandP, tLocation *LocationP, char *ValueP)
{
    LocationP->SystemP = ap_pstrdup(CommandP->pool,ValueP);
    return NULL;
}


// receive the headers query string

static const char*
HeadersQuery(cmd_parms *CommandP, tLocation *LocationP, char *ValueP)
{
    LocationP->HeadersQueryP = ap_pstrdup(CommandP->pool,ValueP);
    return NULL;
}


// receive the content query string

static const char*
ContentQuery(cmd_parms *CommandP, tLocation *LocationP, char *ValueP)
{
    LocationP->ContentQueryP = ap_pstrdup(CommandP->pool,ValueP);
    return NULL;
}


// receive the content type string

static const char*
ContentType(cmd_parms *CommandP, tLocation *LocationP, char *ValueP)
{
    LocationP->ContentTypeP = ap_pstrdup(CommandP->pool,ValueP);
    return NULL;
}


// receive the connect when string

static const char*
ConnectWhen(cmd_parms *CommandP, void *ModuleP, char *ValueP)
{
    server_rec *ServerP = CommandP->server;
    tConfig *ConfigP = ServerConfigP(ServerP);
    ConfigP->ConnectWhenP = ap_pstrdup(CommandP->pool,ValueP);
    ap_str_tolower(ConfigP->ConnectWhenP);
    return NULL;
}


// command handler descriptors

static const command_rec CommandHandlers[] =
{
    {
        "LibpqConnection",
        Connection,
        NULL,
        RSRC_CONF,
        TAKE1,
        "interface database connection string"
    },
    {
        "LibpqSystem",
        System,
        NULL,
        RSRC_CONF,
        TAKE1,
        "database system code"
    },
    {
        "LibpqHeadersQuery",
        HeadersQuery,
        NULL,
        ACCESS_CONF|RSRC_CONF,
        TAKE1,
        "query to process and return headers"
    },
    {
        "LibpqContentQuery",
        ContentQuery,
        NULL,
        ACCESS_CONF|RSRC_CONF,
        TAKE1,
        "query to process and return content"
    },
    {
        "LibpqContentType",
        ContentType,
        NULL,
        ACCESS_CONF|RSRC_CONF,
        TAKE1,
        "the document type that is returned"
    },
    {
        "LibpqConnectWhen",
        ConnectWhen,
        NULL,
        RSRC_CONF,
        TAKE1,
        "connect when started (Now), at first request (Later), or never"
    },
    {NULL}
};



//----------------------------------------------------------------------------
// content handlers ----------------------------------------------------------


enum tArgument
{
    kSystemArgument,
    kAddressArgument,
    kCookieArgument,
    kQueryArgument,
    kContentArgument,
    kAgentArgument,
    kRefererArgument,
    kMaxArgument
};


static int
Request(request_rec *RequestP)
{
    server_rec *ServerP = RequestP->server;
    tConfig *ConfigP = ServerConfigP(ServerP);
    tLocation *LocationP = LocationConfigP(RequestP);
    const char *Arguments[kMaxArgument];
    PGresult *ResultP = NULL;
    PGconn *DatabaseP = NULL;
    char *ContentP = NULL;
    char *CookieP = NULL;
    tVirtual *VirtualP;
    int ResultCode;

    // get the database connection for the server

    for
    (
        VirtualP = gVirtualP;
        VirtualP != NULL && VirtualP->ServerP != ServerP;
        VirtualP = VirtualP->VirtualP
    );

    if (VirtualP != NULL)
    {
        if
        (
            VirtualP->DatabaseP == NULL &&
            strcmp(ConfigP->ConnectWhenP,kConnectNever) != 0
        )
            VirtualP->DatabaseP = PQconnectdb(ConfigP->ConnectionP);
        DatabaseP = VirtualP->DatabaseP;
    }

    if (DatabaseP == NULL)
        return HTTP_SERVICE_UNAVAILABLE;

    // collect the form data

    ResultCode = ap_setup_client_block(RequestP,REQUEST_CHUNKED_ERROR);
    if (ResultCode != OK) return ResultCode;

    ap_hard_timeout("mod_libpq get request",RequestP);
    if (ap_should_client_block(RequestP))
    {
        int Length = RequestP->remaining;
        ContentP = ap_pcalloc(RequestP->pool,Length + 32);
        if (ContentP != NULL)
            ap_get_client_block(RequestP,ContentP,Length);
        else
        {
            ap_kill_timeout(RequestP);
            return HTTP_INSUFFICIENT_STORAGE;
        }
    }
    ap_kill_timeout(RequestP);

    // collect the request headers

    ap_soft_timeout("mod_libpq send response",RequestP);
    RequestP->no_cache = 1;
    RequestP->content_type = LocationP->ContentTypeP;

    Arguments[kSystemArgument] =
        LocationP->SystemP == NULL ?
        RequestP->the_request :
        LocationP->SystemP;

    Arguments[kAddressArgument] = RequestP->connection->remote_ip;
    Arguments[kCookieArgument] = ap_table_get(RequestP->headers_in,"Cookie");
    Arguments[kQueryArgument] = RequestP->args;
    Arguments[kContentArgument] = ContentP;
    Arguments[kAgentArgument] = ap_table_get(RequestP->headers_in,"User-Agent");
    Arguments[kRefererArgument] = ap_table_get(RequestP->headers_in,"Referer");

    // get and send the response headers

    ResultP = CallSql
    (
        DatabaseP,
        LocationP->HeadersQueryP,
        Arguments,
        kMaxArgument
    );
    if (ResultP != NULL)
    {
        if (PQgetisnull(ResultP,0,0) == 0)
        {
            CookieP = ap_pstrdup(RequestP->pool,PQgetvalue(ResultP,0,0));
            if (CookieP != NULL)
                ap_table_set(RequestP->headers_out,"Set-Cookie",CookieP);
        }
        PQclear(ResultP);
    }
    ap_send_http_header(RequestP);

    // get and send the response content

    if (RequestP->header_only == 0)
    {
        if (CookieP == NULL)
            ap_rputs("failed to process the request",RequestP);
        else
        {
            Arguments[0] = CookieP;
            ResultP = CallSql
            (
                DatabaseP,
                LocationP->ContentQueryP,
                Arguments,
                1
            );
            if (ResultP == NULL)
                ap_rputs("failed to process the response",RequestP);
            else
            {
                ap_rputs(PQgetvalue(ResultP,0,0),RequestP);
                PQclear(ResultP);
            }
        }
    }

    // clean up and leave

    ap_kill_timeout(RequestP);
    return OK;
}


// content handler descriptors

static const handler_rec ContentHandlers[] =
{
    {"libpq-request", Request},
    {NULL}
};



//----------------------------------------------------------------------------
// callback handlers ---------------------------------------------------------


// initialise a location instance

static void*
CreateLocation(pool *PoolP, char *PathNameP)
{
    tLocation *LocationP = (tLocation*) ap_pcalloc(PoolP,sizeof(tLocation));
    LocationP->HeadersQueryP = ap_pstrdup(PoolP,kHeadersQuery);
    LocationP->ContentQueryP = ap_pstrdup(PoolP,kContentQuery);
    LocationP->ContentTypeP = ap_pstrdup(PoolP,kContentType);
    return (void*) LocationP;
}


// merge location instances

static void*
MergeLocations(pool *PoolP, void *Location1P, void *Location2P)
{
    tLocation *OldP = (tLocation*) Location1P;
    tLocation *NewP = (tLocation*) Location2P;
    tLocation *LocationP = (tLocation*) ap_pcalloc(PoolP,sizeof(tLocation));

    if (NewP->SystemP != NULL)
        LocationP->SystemP = ap_pstrdup(PoolP,NewP->SystemP);
    else if (OldP->SystemP != NULL)
        LocationP->SystemP = ap_pstrdup(PoolP,OldP->SystemP);

    if (NewP->HeadersQueryP != NULL)
        LocationP->HeadersQueryP = ap_pstrdup(PoolP,NewP->HeadersQueryP);
    else if (OldP->HeadersQueryP != NULL)
        LocationP->HeadersQueryP = ap_pstrdup(PoolP,OldP->HeadersQueryP);
    else
        LocationP->HeadersQueryP = ap_pstrdup(PoolP,kHeadersQuery);

    if (NewP->ContentQueryP != NULL)
        LocationP->ContentQueryP = ap_pstrdup(PoolP,NewP->ContentQueryP);
    else if (OldP->ContentQueryP != NULL)
        LocationP->ContentQueryP = ap_pstrdup(PoolP,OldP->ContentQueryP);
    else
        LocationP->ContentQueryP = ap_pstrdup(PoolP,kContentQuery);

    if (NewP->ContentTypeP != NULL)
        LocationP->ContentTypeP = ap_pstrdup(PoolP,NewP->ContentTypeP);
    else if (OldP->ContentTypeP != NULL)
        LocationP->ContentTypeP = ap_pstrdup(PoolP,OldP->ContentTypeP);
    else
        LocationP->ContentTypeP = ap_pstrdup(PoolP,kContentType);

    return (void*) LocationP;
}


// initialise a server instance

static void*
CreateServer(pool *PoolP, server_rec *ServerP)
{
    tConfig *ConfigP = (tConfig*) ap_pcalloc(PoolP,sizeof(tConfig));
    ConfigP->ConnectWhenP = ap_pstrdup(PoolP,kConnectLater);
    return (void*) ConfigP;
}


// merge server instances

static void*
MergeServers(pool *PoolP, void *Config1P, void *Config2P)
{
    tConfig *OldP = (tConfig*) Config1P;
    tConfig *NewP = (tConfig*) Config2P;
    tConfig *ConfigP = (tConfig*) ap_pcalloc(PoolP,sizeof(tConfig));

    if (NewP->ConnectionP != NULL)
        ConfigP->ConnectionP = ap_pstrdup(PoolP,NewP->ConnectionP);
    else if (OldP->ConnectionP != NULL)
        ConfigP->ConnectionP = ap_pstrdup(PoolP,OldP->ConnectionP);

    if (NewP->ConnectWhenP != NULL)
        ConfigP->ConnectWhenP = ap_pstrdup(PoolP,NewP->ConnectWhenP);
    else if (OldP->ConnectWhenP != NULL)
        ConfigP->ConnectWhenP = ap_pstrdup(PoolP,OldP->ConnectWhenP);
    else
        ConfigP->ConnectWhenP = ap_pstrdup(PoolP,kConnectLater);

    return (void*) ConfigP;
}


// initialise the module

static void
Initialise(server_rec *ServerP, pool *PoolP)
{
    ap_add_version_component(kVersion);
}


// initialise a child process
// create a database connection for the main server and every virtual server

static void
InitialiseChild(server_rec *ServerP, pool *PoolP)
{
    tConfig *ConfigP;
    tVirtual *VirtualP;
    for (; ServerP != NULL; ServerP = ServerP->next)
    {
        ConfigP = ServerConfigP(ServerP);
        if (ConfigP->ConnectionP != NULL)
        {
            VirtualP = (tVirtual*) ap_pcalloc(PoolP,sizeof(tVirtual));
            if (VirtualP != NULL)
            {
                VirtualP->ServerP = ServerP;
                VirtualP->DatabaseP =
                    strcmp(ConfigP->ConnectWhenP,kConnectNow) == 0 ?
                    PQconnectdb(ConfigP->ConnectionP) :
                    NULL;
                VirtualP->VirtualP = gVirtualP;
                gVirtualP = VirtualP;
            }
        }
    }
}


// finalise a child process
// close the database connections for the main server and every virtual server

static void
FinaliseChild(server_rec *ServerP, pool *PoolP)
{
    tVirtual *VirtualP;
    for (VirtualP = gVirtualP; VirtualP != NULL; VirtualP = VirtualP->VirtualP)
        if (VirtualP->DatabaseP != NULL)
            PQfinish(VirtualP->DatabaseP);
}



//----------------------------------------------------------------------------
// module descriptor ---------------------------------------------------------

module MODULE_VAR_EXPORT libpq_module =
{
    STANDARD_MODULE_STUFF,
    Initialise,
    CreateLocation,
    MergeLocations,
    CreateServer,
    MergeServers,
    CommandHandlers,
    ContentHandlers,
    NULL, // URL translation handler,
    NULL, // check identity,
    NULL, // check authorisation,
    NULL, // check access,
    NULL, // check types,
    NULL, // fix stuff,
    NULL, // log the request,
    NULL, // parse the header,
    InitialiseChild,
    FinaliseChild,
    NULL  // post process request
};


//----------------------------------------------------------------------------


/*==========================================================================*/
