<%flags>
inherit => undef
</%flags>
/*
 * =====================================================================
 * mod_atrack - Advanced user tracking
 * Copyright (C) 1999 OpenWorld Ltd.
 * All Rights Reserved
 *
 * Based on mod_usertrack.c from www.apache.org
 * Development before 17/6/99 Mark Collins
 * Development after 17/6/99 Shevek <shevek@anarres.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of this source code or a derived source code must
 *     retain the above copyright notice, this list of conditions and the
 *     following disclaimer.
 *
 * 2. Redistributions of this module or a derived module in binary form
 *     must reproduce the above copyright notice, this list of conditions
 *     and the following disclaimer in the documentation, packaging and/or
 *     other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY OPEN WORLD LTD. ``AS IS'' AND
 * ANY EXPRESSED 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 OPEN WORLD LTD.
 * OR ITS EMPLOYEES 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.
 * =====================================================================
 *
 * Changes:
 * 1999-00-00  Early development
 * 2000-01-14  Changed no-Referer handling
 * 2000-02-17  First public release (1.0.0)
 * 2000-02-21  Format to Apache standard (1.0.1)
 */

/*
 * XXX BUG: Please note that this code won't compile on some of
 * the weirder architectures it attempts to support using #ifdef
 * because the time handling code is wrong.
 * If anyone wants to fix it, please do. I'm limited in only
 * having Linux and other *ixes.
 */

#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_request.h"
#include "http_log.h"

#if !defined(WIN32) && !defined(MPE)
#include <sys/time.h>
#endif

/* If this is defined, cookies will comply with the IETF standard,
    otherwise they will use the original Netscape standard */
/* #define RFC_2109 */

#define  COOKIE_NAME     "Apache="    /* Name of cookie to set */
#undef   ATRACK_DEBUG

#define TRUTH(x)         (!!(x))
#define STREQ(x, y)    (strcasecmp(x, y) == 0)
#define STREQF(x, y) (strncasecmp(x, y, 2) == 0)    /* Fast version */
#define GET_CONFIG(conf) \
    (atrack_state *)ap_get_module_config(conf, &atrack_module)

#ifndef FALSE
#define FALSE (0)
#endif
#ifndef TRUE
#define TRUE (!(FALSE))
#endif

typedef int (*table_callback)(void *r, const char *k, const char *v);

module MODULE_VAR_EXPORT atrack_module;

typedef struct _atrack_link {
    regex_t             *preg;
    struct _atrack_link *next;
} atrack_link;

typedef struct {
    int          enabled;         /* Are we enabled */
    time_t       expires;         /* Cookie expiry time */
    int          userlog;         /* Log every <n>th user */
    int          nolocal;         /* Don't send to local referrers */
    int          listed;          /* Track only listed referrers */
    atrack_link *reflist;        /* Referrer list */
} atrack_state;

#ifdef ATRACK_DEBUG
#define atrack_debug(r, foo) \
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG | APLOG_NOERRNO, r, foo)
#else
#define atrack_debug(r, foo) do { } while(0)
#endif

void *
make_atrack_state(pool *p, server_rec *s)
{
    atrack_state *cls =
         (atrack_state *)ap_palloc(p, sizeof(atrack_state));

    cls->enabled = 0;
    cls->expires = 0;

    cls->userlog = 1;

    cls->nolocal = 1;

    cls->listed = 0;
    cls->reflist = NULL;

    ap_log_error(APLOG_MARK, APLOG_DEBUG | APLOG_NOERRNO, s, "Boot");

    return (void *)cls;
}

static const char *
set_atrack_enabled(cmd_parms *parms, void *dummy, const char *arg)
{
    atrack_state *cls = GET_CONFIG(parms->server->module_config);
    cls->enabled = TRUTH(arg);
    return NULL;
}

static const char *
set_atrack_nolocal(cmd_parms *parms, void *dummy, const char *arg)
{
    atrack_state *cls = GET_CONFIG(parms->server->module_config);
    cls->nolocal = TRUTH(arg);
    return NULL;
}

static const char *
set_atrack_users(cmd_parms *parms, void *dummy, const char *arg)
{
    atrack_state *cls = GET_CONFIG(parms->server->module_config);
    int           foo = atoi(arg);

    if (foo > 0) {
        cls->userlog = foo;
        return NULL;
    }

    else
        return "bad value for directive `CookieUsers`. should be "
                        "integer > 0.";
}

static const char *
set_atrack_expire(cmd_parms *parms, void *dummy, const char *arg)
{
    atrack_state   *cls = GET_CONFIG(parms->server->module_config);
    time_t          factor;
    time_t          num = 0;
    char           *word;
    const char     *cp;

    /* first test if it's a plain number */
    for (cp = arg; cp; cp++) {
        if (!ap_isdigit(*cp)) break;
    }

    /* If we got to the end of arg[0] before breaking */
    if (!(*cp)) {
        cls->expires = atol(arg);
        return NULL;
    }

    /* Harder case disabled in the interim till I go over it properly
     * and make sure it isn't broken. */

    // return "expected numeric value in seconds";

    /* harder case (ripped from mod_usertrack, which was ripped
     * from mod_expires). Specify time from now till expiry in words
     * e.g. "CookieExpires plus 4 days" will expire in 4 days */

    /* CookieExpires "[plus] {<num> <type>}*" */
    word = ap_getword_conf(parms->pool, &arg);
    if (STREQ(word, "plus"))
        word = ap_getword_conf(parms->pool, &arg);

    cls->expires = 0;

    while (word[0])
    {
        if (ap_isdigit(word[0]))
            num = atol(word);
        else
            return "bad expires code, numeric values expected";

        word = ap_getword_conf(parms->pool, &arg);

        if (!word[0])
            return "bad expires code, missing <type>";

        factor =
            (STREQF(word, "years"  ) ? (60 * 60 * 24 * 365) :
             STREQF(word, "months" ) ? (60 * 60 * 24 * 30) :
             STREQF(word, "days"   ) ? (60 * 60 * 24) :
             STREQF(word, "hours"  ) ? (60 * 60) :
             STREQF(word, "minutes") ? (60) :
             STREQF(word, "seconds") ? (1) :
             0);

        cls->expires += num * factor;

        /* next <num> */
        word = ap_getword_conf(parms->pool, &arg);
    }

    return NULL;
}

static const char *
set_atrack_slist(cmd_parms *parms, void *dummy, const char *arg)
{
    atrack_state *cls = GET_CONFIG(parms->server->module_config);
    cls->listed = TRUTH(arg);
    return NULL;
}

static const char *
set_atrack_server(cmd_parms *parms, void *dummy, const char *arg)
{
    atrack_link  *link;
    atrack_state *cls;
    int           errnum;
    int           errlen;
    char         *errbuf;

    cls = GET_CONFIG(parms->server->module_config);

    link = (atrack_link *)ap_palloc(parms->pool, sizeof(atrack_link));

    link->preg = ap_palloc(parms->pool, sizeof(regex_t));
    errnum = regcomp(link->preg, arg, REG_ICASE);
    if (errnum) {
        errlen = regerror(errnum, link->preg, NULL, 0) + 1;
        errbuf = ap_palloc(parms->pool, errlen * sizeof(char));
        regerror(errnum, link->preg, errbuf, errlen);
        return errbuf;
    }

    link->next = cls->reflist;
    cls->reflist = link;

    return NULL;
}

static void
atrack_cookie_make(request_rec *r)
{
     atrack_state *cls = GET_CONFIG(r->server->module_config);
#ifdef ATRACK_DEBUG
    char    *debug;
#endif


#if defined(NO_GETTIMEOFDAY) && !defined(NO_TIME)
    clock_t           mpe_times;
    struct tms        mpe_tms;
#elif !defined(WIN32)
    struct timeval    tv;
    struct timezone   tz = {0, 0};
#endif
    time_t            when;
    struct tm        *tms;
    char              cookiebuf[1024];
    char             *new_cookie;

#if defined(NO_GETTIMEOFDAY) && !defined(NO_TIME)
    /* As we don't have gettimeofday(), we must use time() to get
     * the epoch seconds, and then times() to get to get the CPU clock
     * ticks in milliseconds. Combine this together to obtain
     * a hopefully unique cookie id */
    mpe_times = times(&mpe_tms);

    ap_snprintf(cookiebuf, sizeof(cookiebuf), "%d%ld%ld", (int)getpid(),
                     (long)r->request_time, (long)mpe_tms.tms_utime);
#elif defined(WIN32)
/* We don't have gettimeofday OR times(), so we'll use a combination of
 * time() and GetTickCount(). This should be reletively unique */

    ap_snprintf(cookiebuf, sizeof(cookiebuf), "%d%ld%ld", (int)getpid(),
                     (long)r->request_time, (long)GetTickCount());
#else
    gettimeofday(&tv, &tz);

    ap_snprintf(cookiebuf, sizeof(cookiebuf), "%d%d%d", (int)getpid(),
                     (int)tv.tv_sec, (int)tv.tv_usec / 1000);
#endif

    if (cls->expires)
    {
        when = r->request_time + cls->expires;
        tms = localtime(&when);

#ifdef RFC_2109
        new_cookie = ap_psprintf(r->pool,
                "%s%s; Max-Age=%d; Path=/; Host=%s;",
                COOKIE_NAME, cookiebuf,
                cls->expires,
                r->server->server_hostname);
#else
        new_cookie = ap_psprintf(r->pool,
                "%s%s; expires=%s, %.2d-%s-%.4d %.2d:%.2d:%.2d GMT;"
                " path=/; host=%s;",
                COOKIE_NAME, cookiebuf, ap_day_snames[tms->tm_wday-1],
                tms->tm_mday, ap_month_snames[tms->tm_mon],
                tms->tm_year + 1900,
                tms->tm_hour, tms->tm_min, tms->tm_sec,
                r->server->server_hostname);
#endif
    }
    else {
        new_cookie = ap_psprintf(r->pool,
                "%s%s; path=/;", COOKIE_NAME, cookiebuf);
    }

#ifdef ATRACK_DEBUG
    debug = ap_psprintf(r->pool, "Setting cookie '%s'", new_cookie);
    atrack_debug(r, debug);
#endif

    ap_table_add(r->headers_out, "Set-Cookie", new_cookie);
    ap_table_add(r->notes, "cookie", ap_pstrdup(r->pool, cookiebuf));

    return;
}

static int
atrack_cookie_check(request_rec *r, const char *key, const char *value)
{
    char    *cookiebuf;
    char    *cookieend;
#ifdef ATRACK_DEBUG
    char    *debug;

    debug = ap_psprintf(r->pool, "Cookie callback got '%s'", value);
    atrack_debug(r, debug);
#endif

    if (strncmp(value, COOKIE_NAME, strlen(COOKIE_NAME)) == 0) {

        value += strlen(COOKIE_NAME);
        cookiebuf = ap_pstrdup(r->pool, value);

        /* Ignore everything after a ';' */
        cookieend = strchr(cookiebuf, ';');
        if (cookieend)
            *cookieend = '\0';

        /* Set the cookie in a note for logging from notes table */
        ap_table_setn(r->notes, "cookie", cookiebuf);

        /* Abort the ap_table_do() below */
        return FALSE;
    }
#ifdef ATRACK_DEBUG
    else {
        /* XXX DEBUGGING */
        ap_table_add(r->notes, "other-cookie", value);
    }
#endif

    return TRUE;
}

static int
spot_cookie(request_rec *r)
{
    atrack_state    *cls;
    atrack_link     *reflist;            /* Referrer list */
    char            *cookie;
    const char      *referer_uri;
    char            *hostname;
    char            *cp;
    uri_components   uri;
    long             ran;
#ifdef ATRACK_DEBUG
    char            *debug;
#endif

    cls = GET_CONFIG(r->server->module_config);

    /* Disabled, no cookie */
    if (!cls->enabled)
        return DECLINED;

    ap_table_do((table_callback)atrack_cookie_check, r,
                    r->headers_in, "Cookie", NULL);

    /* Careful, someone else might set this field */
    cookie = (char *)ap_table_get(r->notes, "cookie");
    if (cookie) {
        atrack_debug(r, "Already have cookie in notes table");
        return DECLINED;
    }

    /* If user doesn't allready have a cookie, check if they're a
     * new comer to the site. If so, randomly decide whether
     * the user needs a cookie.  If so, give them one */

    ran = random();
#ifdef ATRACK_DEBUG
    debug = ap_psprintf(r->pool, "Random: %ld, Num: %d, Modulus: %ld",
                    ran, cls->userlog, ran % cls->userlog);
    atrack_debug(r, debug);
#endif
    /* Take 1 : cls->userlog chance to send a cookie */
    if ((ran % cls->userlog) != 0)
        return DECLINED;

    /* Get a referer, in case we need it later, but don't act on it,
     * we might have neither nolocal nor listed set. */
    referer_uri = ap_table_get(r->headers_in, "Referer");
    if (!referer_uri) {
        uri.hostname = NULL;
        atrack_debug(r, "No referer_uri in HTTP headers");
    }
    else if (ap_parse_uri_components(r->pool,referer_uri,&uri) != 200) {
        atrack_debug(r, "Could not parse referer_uri from header");
    }
    else if (!uri.hostname) {
        atrack_debug(r, "No hostname in parsed uri");
    }

    if (cls->nolocal) {
        /* No referer, we can't test for local, so play safe */
        if (!uri.hostname)
            return DECLINED;

        /* XXX We need HTTP/1.0 too */
        hostname = (char *)ap_table_get(r->headers_in, "Host");
        // r->server->server_hostname
        /* No HTTP/1.1 header, no cookie */
        if (!hostname) {
            atrack_debug(r, "No local server hostname in headers");
            return DECLINED;
        }
        hostname = ap_pstrdup(r->pool, hostname);

        cp = strchr(hostname, ':');
        if (cp) *cp = '\0';

#ifdef ATRACK_DEBUG
        debug = ap_psprintf(r->pool, "Host: '%s', Ref: '%s'",
                        hostname, uri.hostname);
        atrack_debug(r, debug);
#endif

        /* Local, no cookie */
        if (STREQ(uri.hostname, hostname)) {
            atrack_debug(r, "Referal was local");
            return DECLINED;
        }
    }

    if (cls->listed) {
        /* No referer, we can't test for lists, play safe */
        if (!uri.hostname)
            return DECLINED;

        reflist = cls->reflist;
        while (1) {
            /* If we've hit the end of the list, break */
            if (!reflist) {
                atrack_debug(r, "Referer does not match list");
                return DECLINED;
            }
            /* If we match, break out */
            if (regexec(reflist->preg, uri.hostname, 0, NULL, 0) == 0) {
#ifdef ATRACK_DEBUG
                debug = ap_psprintf(r->pool, "Referer '%s' matched "
                                "a regex", uri.hostname);
                atrack_debug(r, debug);
#endif
                break;
            }
            reflist = reflist->next;
        }
    }

    atrack_cookie_make(r);
    return OK;
}

command_rec atrack_commands[] = {
     { "CookieExpires", set_atrack_expire, NULL, OR_ALL, TAKE1,
       "an expire date code" },
     { "CookieTracking", set_atrack_enabled, NULL, OR_ALL, FLAG,
       "whether or not to track cookies" },
     { "CookieUsers", set_atrack_users, NULL, OR_ALL, TAKE1,
       "track every <n>th user" },
     { "CookieNoLocalRefer", set_atrack_nolocal, NULL, OR_ALL, FLAG,
       "disable tracking of local referers" },
     { "CookieServerList", set_atrack_slist, NULL, OR_ALL, FLAG,
       "only track users referred from listed servers" },
     { "CookieServerAdd", set_atrack_server, NULL, OR_ALL, TAKE1,
       "add <regex> to the referring server list for tracking" },

     {NULL}
};

module MODULE_VAR_EXPORT atrack_module = {
     STANDARD_MODULE_STUFF,
     NULL,                    /* initialiser */
     NULL,                    /* create per-dir config */
     NULL,                    /* merge per-dir config */
     make_atrack_state,       /* server config */
     NULL,                    /* merge server config */
     atrack_commands,         /* command table */
     NULL,                    /* handlers */
     NULL,                    /* filename translation */
     NULL,                    /* check user id */
     NULL,                    /* check auth */
     NULL,                    /* check access */
     NULL,                    /* type checker */
     spot_cookie,             /* fixups */
     NULL,                    /* logger */
     NULL,                    /* header parser */
     NULL,                    /* child_init */
     NULL,                    /* child_exit */
     NULL                     /* post read request */
};
