
/* =======================================================================
 * Copyright (c) 1996,1997 Vidya Media Ventures, Inc. 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 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 VIDYA MEDIA VENTURES, INC. ``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 VIDYA MEDIA VENTURES, INC.
 * 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.
 * =======================================================================
 *
 * This software is a contribution to and makes use of the Apache HTTP
 * server which is written, maintained and copywritten by the Apache Group.
 * See http://www.apache.org/ for more information.
 *
 */

/*
 * Session key maintenance module
 *
 * Version 0.5 (December 1996)
 *
 * Adam Sussman (asussman@vidya.com)
 *
 * Requires Apache 1.2 or later.
 *
 * Outline:
 *
 * This module does its best to insure that a consistent identifier is
 * maintained by the client no matter what kind of browser that client is
 * and no matter what path they take through the session controlled areas
 * of the site.  This identifier, refered to as a "session key", is always
 * made present as an environment variable (r->subprocess_env internaly
 * and the actual process enviroment for CGI, etc).  This session key is
 * always held in the SESSION_KEY environment.
 *
 * Session keys can be logged by adding a %{SESSION_KEY}n to the log format.
 *
 *
 * Basic Theory of Operation:
 *
 * There are a few ways to make a browser maintain and retransmit a session key.
 * The perferable way is to use HTTP cookies.  However, many browsers do not support
 * cookies (and even older versions of browsers that do don't, and are still out there).
 * In this case, the session key can be stored in the URLs that the browsers see in
 * the HTML documents.  Doing it this way is a little more complicated as we have
 * to make sure that the URLs in a given document always have this session key information.
 * There are additional complications using imagemaps in this scenario.  This module
 * attempts to deal with all of these cases.
 * 
 * There are also a few other benefits and desirable qualities in a session
 * controlled environment.  One of these that this module provides is the
 * ability to force entry only at at certain URLs.  Thus, if a browser
 * makes a request for a URL without providing a session key, they can be
 * redirected to a specific entrance point where a new session key will be
 * created just for them.
 * 
 *
 * The Session Algorithm:
 *
 * The flow of operation in this module is as follows.  Most parts of this
 * process can be altered to suit the user's needs.
 * 
 * 1) Optional Point of Entry Control and Session Key Demand
 *
 *    + enabled with 'SessionTop' and 'SessionValidEntry'
 *    + modified with 'SessionExemptTypes' and 'SessionExemptLocations'
 * 
 *      If a request is made for a URL without providing a session
 *      key, the URL is checked against a list of valid entry points.
 *      If it does not match one, the browser is redirected to the URL
 *      defined by 'SessionTop'.
 * 
 *      Optionaly, certain mime-types, locations and handlers can be made exempt
 *      from this redirection.  The reason for this is that when session
 *      key information is appended to a URL, it can interfere with
 *      caching mechanisms.  This would hinder the caching of images by
 *      a proxy server independant of session keys.  By default, only
 *      mime types image/.* are exempted, but a more complete list can be
 *      substituted with the 'SessionExemptTypes' directive.
 *      The 'SessionExemptLocations' command can be used to exempt portions
 *      of the directory structure.
 *
 *      In certain cases, type exemption may not produce the
 *      results you are looking for because the final type of a requested
 *      object can't always be determined by this module.  For example, if
 *      you are exempting image/.* from session control but the only way to
 *      get at an image is a via a type map file (.var), this module doesn't
 *      know that the end result is an image type.  It only sees the .var file.
 *      If you group all you .var files in a common area however, you can exempt
 *      them explicitly by speicifying that directory with 'SessionExemptLocations'.
 *
 *      Certain clients can be exempted from this process as well.
 *      It may not be desirable for search engine bots to be forced
 *      to have session keys (and they almost never support cookies).
 *      When these bots are given URL based session keys, these same keys
 *      are often stored in the index that the search engines use to
 *      respond to queries!
 *
 *      You can use the BrowserMatch command with the argument
 *      'nosessioncontrol' to disable session control for selected
 *      clients.
 *
 *      Note that clients exempted with 'nosessioncontrol' will never be issued
 *      session keys or cookies.
 *
 *      Note also that objects exempted from session control may not have any
 *      session key information to make available to the logging.
 * 
 *      If you opt not to use Point of Entry Control at all, original
 *      requests will be issued a new session key and a new cookie without
 *      being redirected anywhere first.
 *
 *
 * 2) Session Key Detection
 *
 *    + controlled by 'SessionCookieName' and 'SessionUrlSidName'
 *    + modified by 'SessionCookieExpire' and 'SessionUrlExpire'
 * 
 *      To find the session key, the cookie header and the URL
 *      are examined.  When the key is found, it is placed into the
 *      SESSION_KEY environment variable.  Another environment variable,
 *      SESSION_KEY_METHOD is set to COOKIE or URL to indicate where
 *      the key was found.  These values are also set in the requests
 *      internal notes table and can be logged by adding these items
 *      to the log format: %{SESSION_KEY}n and %{SESSION_KEY_METHOD}n
 * 
 *      If the key was not found and the URL is a valid entry point,
 *      a new session key is generated.  The browser will be sent
 *      a Set-Cookie header.  The SESSION_KEY_METHOD variable will
 *      always be set to URL in this case because it takes two requests
 *      from the browser to determine that it has a cookie mechanism.
 *      The first response should always be processed to include URL
 *      session keys, just in case.  Session keys in cookie headers
 *      always take precedence over URL keys.
 *
 *      Session keys can also be forcibly expired after a certain
 *      amount of time using the 'SessionCookieExpire' and 'SessionUrlExpire'
 *      command.  When this happens, the module will create a brand
 *      new session key.
 *
 * 3) Special Case Requests when using URL session keys
 *
 *    + 'SessionFilter' and 'SessionFilterTypes'
 * 
 *      When the session key is being stored in the URL there are two
 *      tasks that need to be performed.
 * 
 *      Firstly, any HTML files should be post processed to append the
 *      session key information in their URLs.  It is not currently
 *      possible to do this within the server and it must therefore
 *      be done externaly.  This module employs a mechanism similar to
 *      mod_actions.  All requests of a certain mime type(s) (controlled
 *      by 'SessionFilterTypes') are passed through an external CGI filter
 *      (defined by 'SessionFilter').  This filter must do two things:
 * 
 *              1) Append the session key information to any HREFs' and
 *                 ACTIONs' query string that is not an imagemap call
 *                 in the form:
 * 
 *                      <SessionUrlSidName>=<SESSION_KEY environment variable>
 * 
 *              2) Append the session key information to any imagemap HREF's
 *                 path_info (*not* query string) in the same form.  This is
 *                 only necessary for links to the map file, not SHAPE HREFs.
 *                 (see next point on why)
 * 
 *      Secondly, special provisions must be made for imagemaps.
 *      As a general rule, browsers treat query strings on .map URLs
 *      inconsistently and most just don't pass it on to the server.
 *      The only consistent way to preserve session key information in
 *      an imagemap call is to put it in the path info of that call.
 *      This browser will recognize that information and make sure that
 *      it gets appended to the new URL generated by the imagemap module.
 * 
 *
 * Directives:
 *
 * (cookie related commands)
 *
 * SessionCookieName            The name to give the HTTP cookie.
 *
 * SessionCookieDomain          The domain in which the cookie is valid
 *
 * SessionCookiePath            The path this cookie is valid in.
 *
 * SessionCookieExpire          An interval, in seconds after which the cookie
 *                              should expire.  This expiration date is set in the
 *                              cookie and it will also be forced by this module.
 *                              A new session key will be generated and if set, the
 *                              client will be redirected to SessionTop.
 *
 * SessionMillenialCookies      Normally, cookie expiration dates use two digit years.
 *                              This won't work beyond 1999.  This flag will make
 *                              the expiration date a four digit one.  Be warned however,
 *                              not all browsers will recognize this yet and, at worst,
 *                              might ignore the cookie altogether.
 *
 * SessionDisableCookies        Disable the use of cookies by this module entirely.  This
 *                              is usefull for debugging filter scripts and exemptions.
 *
 * (url session key commands)
 *
 * SessionUrlSidName            Variable name used to hold the session key within URLs.
 *
 * SessionUrlExpire             An interval, in seconds after which the session key
 *                              embeded in a URL.  A new session key will be generated and
 *                              if set, the client will be redirected to SessionTop.
 *
 * (url and session key commands)
 *
 * SessionMoreDigits            Use more time digits (usec) in the session key.
 *
 *
 * (Entry Control commands)
 *
 * SessionTop                   Full URL to redirect browsers to when they try to enter
 *                              the site without a session key at the wrong place.
 *
 * SessionValidEntry            A space seperated list of valid entry URIs where browsers
 *                              can safely enter and where new session_keys can be
 *                              generated.  You can use regular expressions here.
 *
 * SessionExemptLocations       A space seperated list of URIs and URI regular expressions
 *                              which are to be exempted from session and entry control.
 *
 * SessionExemptTypes           A space seperated list of mime types and handlers which
 *                              are exmpted from session and entry control.  You can
 *                              use regular expressions here.  The default type
 *                              is image/.*.
 *
 * (URL session key filtering rules)
 *
 * SessionFilter                The URI of the CGI script to use as a filter to put URL
 *                              based session keys in the URLs of a document.
 *
 * SessionFilterTypes           A space seperated list of mime-types which must be passed
 *                              through the above filter.  You can use regular expressions here.
 *                              The default is text/html.
 *
 *
 * Sample Configuration:
 *
 *      <Location /Some/place/>
 *
 *      SessionCookieName       mysession
 *      SessionCookiePath       /Some/Place/
 *      SessionCookieDomain     .mydomain.com
 *      SessionCookieExpire     8400
 *
 *      SessionUrlSidName       sid
 *      SessionUrlExpire        8400
 *
 *      SessionTop              http://www.mydomain.com/Some/Place/
 *      SessionValidEntry       /Some/Place/ /Some/Place/index.html /Some/Place/else/ /cgi-bin/neat-script
 *
 *      SessionExemptLocations  /Some/Place/images/.*
 *      SessionExemptTypes      image/.* audio/.* proxy_handler
 *
 *      SessionFilter           /cgi-bin/session_php_filter
 *      SessionFilterTypes      text/html text/html3 application/x-httpd-php text/phtml
 *
 *      </Location>
 *
 *      BrowserMatchNoCase      infoseek/.*     nosessioncontrol
 *      BrowserMatchNoCase      .*spider.*      nosessioncontrol
 *      BrowserMatchNoCase      .*spyder.*      nosessioncontrol
 *      BrowserMatchNoCase      .*bot.*         nosessioncontrol
 *      BrowserMatchNoCase      .*harvest.*     nosessioncontrol
 *      BrowserMatchNoCase      .*crawler.*     nosessioncontrol
 *      BrowserMatchNoCase      .*yahoo.*       nosessioncontrol
 *
 * Note that the filter configured in this example parses the output of a php script.
 *
 * Note that you can collapse some of the lengthy lists above by making more complicated
 * regular expressions.  For example, if you wanted these three locations as valid
 * entry points: /Some/Place, /Some/Place/, and /Some/Place/index.html you could use
 * this regular expression: /Some/Place(/(index.html)?)?
 *
 *
 * Compilation Notes:
 *
 * This module has to be placed in a certain order within the list of modules
 * in Configuration for the best results.  It should always be listed BEFORE 
 * mod_browser and mod_usertrack.
 *
 * 
 * Trouble Shooting
 *
 * + The session key cookie is never sent to the client!
 *
 *      1) Make sure that 'SessionDisableCookies' is not set to 'on'
 *
 *      2) mod_usertrack tends to stomp on other module's Set-Cookie headers.
 *         Mod_session can interact nicely with mod_usertrack but not vice-versa.
 *         If you think your clients are not getting sent cookies and you use
 *         mod_usertrack, this might be your problem.  To fix this, make sure
 *         that mod_session is listed before mod_usertrack in the Configuration
 *         file before compilation.
 *
 * + mod_session seems to be ignoring my BrowserMatch 'nosessioncontrol' commands!
 *
 *      1) mod_browser needs to run before mod_session if you want your 'nosessioncontrol'
 *         directives to work.  The only way to do this is to make sure that mod_session
 *         is listed before mod_browser in the Configuration file before compilation.
 *
 * + I keep getting broken image icons on my page!
 *
 *      1) Check to see if the client is trying to access the image without a
 *         session key (easy to see of you have cookies turned off).  If it is,
 *         make sure that you are exempting image/.* with 'SessionExemptTypes'.
 *
 *      2) If that didn't help and you are using multiViews (either automatic or
 *         with .var files), you should exempt these files by location with
 *         'SessionExemptLocations'.  Be carefull not to exempt html and other
 *         filtered files at the same time though!
 *
 *
 * Adam Sussman (asussman@vidya.com) Jun, 1996
 *
 * Version 0.1  (Jun 1996)  Inital imagemap handling.
 *
 *         0.2  (Aug 1996)  Full conversion from perl libs with the exception of
 *                          URL post parsing which is still implemented externaly.
 *
 *         0.3  (Sep 1996)  Altered key checks to search upwards along r->prev.
 *                          Added more digits option.
 *                          Added SessionUrlExpire and key expiration mechanisms
 *
 *         0.4  (Oct 1996)  Altered format of session key to put a '_' between the
 *                            time stamp and the host name to make parsing easier.
 *                            This is necessary because we can't necessarily match
 *                            the session key to the remote host name in a proxy
 *                            environment (especialy AOL's).
 *
 *         0.5  (Dec 1996)  Some cosmetic changes to make apache 1.2 happy.
 *                          Restricted module to 1.2 or later.
 *                          Added SESSION_KEY and SESSION_KEY_METHOD to the
 *                            request's notes table so that they can be logged
 *                            by mod_log_config.
 *                          Added ability to disable session control for certain
 *                            user agents via mod_browser settings. 'nosessioncontrol'
 *                          Regexed everything, including defaults.
 *                          Changed SessionSkipTypes to SessionExemptTypes.
 *                          Changed SessionSkipLocations to SessionExemptLocations.
 *                          session_imap_handler tended to get its own handler back
 *                            from sub_req_lookup_uri.  This is a departure from pre 1.2
 *                            behaviour. :(  Changed the fixup not to hijack imap in the
 *                            presence of r->main.
 *                          Needed to look for session info in r->main for mod_dir, among
 *                            others.
 *
 */


#include "httpd.h"
#include "http_config.h"
#include "http_request.h"
#include "http_core.h"
#include "http_log.h"
#include <stdio.h>
#include <time.h>


#if (MODULE_MAGIC_NUMBER < 19960725)
#error "This module is for Apache 1.2 or greater!"
#endif

#define BROWSER_EXEMPT  "nosessioncontrol"

/*
 * Todo:
 *      session_check_valid_entry should be smart enough to know that
 *      a uri of /dir/ is the same thing as /dir/index.html.  Can't
 *      really do this without access to mod_dir and its handler though :(
 *
 */

module session_module;

typedef struct rlist {
    regex_t *expression;
    struct rlist *next;
} rlist;

typedef struct {
    char *path;                 /* used to determine applicability in the fixup */
    char *cookie_name;
    char *cookie_domain;
    char *cookie_path;
    long cookie_expire;         /* interval in seconds */
    int use_millenial_dates;
    int more_digits;
    int disable_cookies;
    char *url_sid_name;
    long url_expire;
    char *session_top;
    rlist *valid_entry;
    rlist *exempt_locations;
    rlist *exempt_types;
    regex_t *default_exempt_types;
    char *external_filter;
    rlist *filter_types;
    regex_t *default_filter_types;
} session_config_rec;

/* Configuration Support */

static int session_isnum(char *str)
{
    register int i, j;

    j = strlen(str);
    for (i = 0; i < j; i++)
        if (!isdigit(str[i]))
            return 0;
    return 1;
}

static const char *session_set_cookie_expires(cmd_parms *cmd, session_config_rec * conf, char *arg)
{
    if (!session_isnum(arg))
        return "argument must be a positive integer.";
    conf->cookie_expire = atol(arg);
    if (conf->cookie_expire <= 0)
        return "argument must be a positive integer.";
    return NULL;
}

static const char *session_set_url_expires(cmd_parms *cmd, session_config_rec * conf, char *arg)
{
    if (!session_isnum(arg))
        return "argument must be a positive integer.";

    conf->url_expire = atol(arg);

    if (conf->url_expire <= 0)
        return "argument must be a positive integer.";

    return NULL;
}

static const
char *session_add_regex_slot(cmd_parms *cmd, char *conf, char *arg)
{
    rlist *tmp;
    rlist **rr;
    int offset;
    char pat[256];

    offset = (int) cmd->info;
    rr = (rlist **) (conf + offset);
    tmp = *rr;

    if (tmp == NULL) {
        tmp = (rlist *) ap_pcalloc(cmd->pool, sizeof(rlist));
        if (!tmp)
            return "Memory allocation error.";
        *rr = tmp;
    }
    else {
        /* skip to end of list */
        while (tmp->next != NULL)
            tmp = tmp->next;
        tmp->next = (rlist *) ap_pcalloc(cmd->pool, sizeof(rlist));
        if (!tmp->next)
            return "Memory allocation error.";
        tmp = tmp->next;
    }

    /* force all expressions to match -entire- argument */
    sprintf(pat, "^%s$", arg);
    tmp->expression = ap_pregcomp(cmd->pool, pat, REG_EXTENDED | REG_ICASE | REG_NOSUB);
    if (!tmp->expression)
        return ap_pstrcat(cmd->pool, "Error in regular expression: ", arg);
    return NULL;
}


/* Per dir configuration */
static void *session_create_dir_config(pool *p, char *d)
{
    session_config_rec *new;
    new = (session_config_rec *) ap_pcalloc(p, sizeof(session_config_rec));

    new->path = ap_pstrdup(p, d);
    new->cookie_expire = -1;
    new->use_millenial_dates = 0;
    new->disable_cookies = 0;

    new->url_expire = -1;
    new->exempt_locations = NULL;
    new->valid_entry = NULL;

    /* set defaults */

    /* we can do this conveniently with the command handler, but
     * we need to fake a cmd_parms structure.
     */

    new->default_exempt_types = ap_pregcomp(p, "^image/.*$", REG_EXTENDED | REG_ICASE | REG_NOSUB);
    new->default_filter_types = ap_pregcomp(p, "^text/html$", REG_EXTENDED | REG_ICASE | REG_NOSUB);

    return new;
}

/* The configuration commands */
static command_rec session_cmds[] =
{
    {"SessionCookieName", ap_set_string_slot,
     (void *) XtOffsetOf(session_config_rec, cookie_name),
     OR_AUTHCFG, TAKE1, "the name to give the session cookie."},
    {"SessionCookieDomain", ap_set_string_slot,
     (void *) XtOffsetOf(session_config_rec, cookie_domain),
     OR_AUTHCFG, TAKE1, "the domain to assign to the session cookie."},
    {"SessionCookiePath", ap_set_string_slot,
     (void *) XtOffsetOf(session_config_rec, cookie_path),
     OR_AUTHCFG, TAKE1, "the path to assign to the session cookie."},
    {"SessionCookieExpire", session_set_cookie_expires,
     NULL,
     OR_AUTHCFG, TAKE1, "the amount of time (in seconds) after which the session cookie will expire."},
    {"SessionMillenialCookies", ap_set_flag_slot,
     (void *) XtOffsetOf(session_config_rec, use_millenial_dates),
     OR_AUTHCFG, FLAG, "on or off."},
    {"SessionMoreDigits", ap_set_flag_slot,
     (void *) XtOffsetOf(session_config_rec, more_digits),
     OR_AUTHCFG, FLAG, "on or off."},
    {"SessionDisableCookies", ap_set_flag_slot,
     (void *) XtOffsetOf(session_config_rec, disable_cookies),
     OR_AUTHCFG, FLAG, "on or off."},
    {"SessionUrlSidName", ap_set_string_slot,
     (void *) XtOffsetOf(session_config_rec, url_sid_name),
     OR_AUTHCFG, TAKE1, "the name of the session identifier to use in the URL cookie."},
    {"SessionUrlExpire", session_set_url_expires,
     NULL,
     OR_AUTHCFG, TAKE1, "the amount of time (in seconds) after which the session url cookie will expire."},
    {"SessionTop", ap_set_string_slot,
     (void *) XtOffsetOf(session_config_rec, session_top),
     OR_AUTHCFG, TAKE1, "the url to redirect to when someone tries to come in through an invalid entry point without a cookie."},
    {"SessionValidEntry", session_add_regex_slot,
     (void *) XtOffsetOf(session_config_rec, valid_entry),
     OR_AUTHCFG, ITERATE, "a list of valid entry points without a session identifier"},
    {"SessionFilter", ap_set_string_slot,
     (void *) XtOffsetOf(session_config_rec, external_filter),
     OR_AUTHCFG, TAKE1, "a cgi program to use as a URL SID filter."},
    {"SessionFilterTypes", session_add_regex_slot,
     (void *) XtOffsetOf(session_config_rec, filter_types),
     OR_AUTHCFG, ITERATE, "a cgi program to use as a URL SID filter."},
    {"SessionExemptLocations", session_add_regex_slot,
     (void *) XtOffsetOf(session_config_rec, exempt_locations),
     OR_AUTHCFG, ITERATE, "a cgi program to use as a URL SID filter."},
    {"SessionExemptTypes", session_add_regex_slot,
     (void *) XtOffsetOf(session_config_rec, exempt_types),
     OR_AUTHCFG, ITERATE, "a cgi program to use as a URL SID filter."},
    {NULL},
};

/* session_filter_handler

 * Handle post parsing of certain documents when SESSION_KEY_METHOD is URL.
 * This handler should never be set anywhere but in session_fixup.
 *
 * Post parsing is currently outsourced to a CGI like in mod_actions. Eventually,
 * this will be done internally.
 *
 */
static int session_filter_handler(request_rec *r)
{
    session_config_rec *conf;

    conf = (session_config_rec *) ap_get_module_config(r->per_dir_config, &session_module);

    if (!conf->external_filter) {
        ap_log_error_old("No external filter defined.  Do not use the session-postparse handler directly.", r->server);
        return SERVER_ERROR;
    }

    if (!ap_table_get(r->subprocess_env, "SESSION_KEY_METHOD") ||
        strcmp(ap_table_get(r->subprocess_env, "SESSION_KEY_METHOD"), "URL"))
        return DECLINED;

    /* some sanity checking */
    if (r->finfo.st_mode == 0) {
        ap_log_reason("File does not exist", r->filename, r);
        return NOT_FOUND;
    }

    /* avoid looping */
    if (r->prev && r->prev->prev)
        return DECLINED;

    /* redirect to the cgi script */
    ap_internal_redirect(ap_pstrcat(r->pool, conf->external_filter, ap_escape_uri(r->pool,
                          r->uri), r->args ? "?" : NULL, r->args, NULL), r);

    return OK;
}

/* session_imap_handler

 * When SESSION_KEY_METHOD is URL, we need to hijack the imagemap process.  As a general
 * rule, client handling of imagemap urls with a query string attached is spotty at best.
 * PATH_INFO on the other hand is always preserved, so this module expects to see the
 * SESSION_KEY embeded in the url's path info.  All of this is handled by session_fixup
 * and session_filter_handler.  The task of this handler is to run the actual image map
 * request through mod_imap and then append the SESSION_KEY to the redirect that results.
 * 
 */
static int session_imap_handler(request_rec *r)
{
    request_rec *rr;
    char work[MAX_STRING_LEN];
    int ret;
    session_config_rec *conf;

    conf = (session_config_rec *) ap_get_module_config(r->per_dir_config, &session_module);

    /* build the real imagemap request */

    /* we don't want the subrequest to hijack imap this time */
    ap_table_set(r->notes, "session_imap_subreq", "");

    /* do the subrequest */
    rr = ap_sub_req_lookup_uri(r->uri, r);
    if (rr->status != 200) {

        ret = rr->status;
        ap_destroy_sub_req(rr);
        return ret;
    }

    /* set up the sub_request to be run by transfering
     * the coordinate args.  The appropriate content
     * type should have been set by the sub_req_lookup_uri
     * function, and no overriding handler should be present.
     */
    if ((rr->handler != NULL) && strcmp(rr->handler, "imap-file")) {

        sprintf(work, "session imap sub-request got wrong handler: %s", rr->handler);
        ap_log_error_old(work, r->server);
        return SERVER_ERROR;
    }

    /* run the imap handler */
    rr->args = ap_pstrdup(rr->pool, r->args);
    ret = ap_run_sub_req(rr);

    /* if a successfull redirect was returned, tack the path_info onto it.
     * otherwise just pass the results on.
     */
    if (ret == REDIRECT) {

        /* transfer results to parent request */
        r->status = REDIRECT;

        /* add path_info as query string to Location header */
        strcpy(work, ap_table_get(rr->headers_out, "Location"));
        if (strchr(work, '?'))
            sprintf(work, "%s&%s=%s", work, conf->url_sid_name,
                    ap_table_get(r->subprocess_env, "SESSION_KEY"));
        else
            sprintf(work, "%s?%s=%s", work, conf->url_sid_name,
                    ap_table_get(r->subprocess_env, "SESSION_KEY"));

        ap_table_set(r->headers_out, "Location", work);
    }

    ap_destroy_sub_req(rr);
    return ret;
}

/* key management functions */

/*
 * create_key
 *
 * Create the session identifier as well as the Set-Cookie header.
 *
 * Session identifiers have the general form of:
 *
 *      unix_timestamp + [extra usec digits] + '_' + first part of hostname/IP number
 *
 */
static void session_create_key(request_rec *r, session_config_rec * conf)
{
    struct timeval tv;
    time_t expire;
    struct tm *gtime;
    char *dot;
    char buffer[MAX_STRING_LEN];
    char new_key[40];
    char *rname;
    struct timezone tz =
    {0, 0};


    rname = ap_pstrdup(r->pool, ap_get_remote_host(r->connection, r->per_dir_config, REMOTE_NAME));

    /*
     * create an identifier.  The current unix time
     * plus a portion of the remote host name is usualy
     * sufficient.  u_secs can be added for even better odds.
     */

    if ((dot = strchr(rname, '.')))
        /* get just the first part */
        *dot = '\0';

    gettimeofday(&tv, &tz);
    if (conf->more_digits)
        sprintf(new_key, "%ld%03d_%s", (long) tv.tv_sec, (int) tv.tv_usec / 1000, rname);
    else
        sprintf(new_key, "%ld_%s", (long) tv.tv_sec, rname);

    /*
     * set the session key in the environment.  We set the method
     * to URL since we cannot be 100% sure that a cookie mechanism
     * is available on the other end until the next request.
     */

    ap_table_set(r->subprocess_env, "SESSION_KEY", new_key);
    ap_table_set(r->subprocess_env, "SESSION_KEY_METHOD", "URL");
    ap_table_set(r->subprocess_env, "SESSION_KEY_NAME", conf->cookie_name);

    /* set these values in the notes table as well so that they
     * can be logged my mod_log_config
     */
    ap_table_set(r->notes, "SESSION_KEY", new_key);
    ap_table_set(r->notes, "SESSION_KEY_METHOD", "URL");
    ap_table_set(r->notes, "SESSION_KEY_NAME", conf->cookie_name);


    /* Now create the Set-Cookie token */
    if (conf->disable_cookies)
        return;

    /* Basic header */
    sprintf(buffer, "%s=%s", conf->cookie_name, new_key);

    /* optional domain */
    if (conf->cookie_domain) {
        /* we have to make sure that the domain starts with a . otherwise
         * some versions of Netscape (at least) will not take it.
         */
        if (*(conf->cookie_domain) != '.')
            sprintf(buffer, "%s; domain=.%s", buffer, conf->cookie_domain);
        else
            sprintf(buffer, "%s; domain=%s", buffer, conf->cookie_domain);
	}

    /* optional expiration date */
    if (conf->cookie_expire > 0) {

        static const char *const days[7] =
        {"Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"};

        expire = time(NULL) + conf->cookie_expire;
        gtime = gmtime(&expire);
        if (conf->use_millenial_dates) {

            /* use a 4 digit year.  Also need to make sure
             * that the year is properly digited.
             */
            if ((expire >= 946684800) && (gtime->tm_year < 100))
                gtime->tm_year += 2000;

            if ((expire < 946684800) && (gtime->tm_year < 100))
                gtime->tm_year += 1900;

            sprintf(buffer, "%s; expires=%s, %02d-%s-%04d %02d:%02d:%02d GMT",
                    buffer, days[gtime->tm_wday], gtime->tm_mday,
                ap_month_snames[gtime->tm_mon], gtime->tm_year, gtime->tm_hour,
                    gtime->tm_min, gtime->tm_sec);
        }
        else
            sprintf(buffer, "%s; expires=%s, %02d-%s-%02d %02d:%02d:%02d GMT",
                    buffer, days[gtime->tm_wday], gtime->tm_mday,
                ap_month_snames[gtime->tm_mon], gtime->tm_year, gtime->tm_hour,
                    gtime->tm_min, gtime->tm_sec);
    }

    /* need to have a path at the end */
    sprintf(buffer, "%s; path=%s", buffer, conf->cookie_path);

    ap_table_merge(r->headers_out, "Set-Cookie", buffer);
}


/*
 * detect_key
 *
 * There are five places a session key created by this module could be:
 *
 *      1) In the subprocess_env of a parent request
 *         (case of an internal redirect from the filter handler)
 *      2) In r->main of a parent request
 *      3) In a cookie header in r->headers_in
 *      4) In r->args (query string)
 *      5) In r->path_info (special case imagemap handling)
 *
 * Additionaly, if this is an internal redirect, the parent might have
 * called session_create_cookie, in which case we must propagate
 * the Set-Cookie header.
 *
 * We also need to propagate the notes settings just like subprocess_env
 * and Set-Cookie headers so that no matter when logging happens, the
 * information will be there.
 *
 */
static int session_detect_key(request_rec *r, session_config_rec * conf)
{
    const char *str;
    char *ptr;
    char *val;
    char buffer[MAX_STRING_LEN];
    request_rec *rp;


    /* 1) Look at parent requests */
    rp = r->prev;
    while (rp) {
        /* Propagate any parent request Set-Cookie headers */
        if (ap_table_get(rp->headers_out, "Set-Cookie"))
            ap_table_set(r->headers_out, "Set-Cookie",
                      ap_table_get(rp->headers_out, "Set-Cookie"));

        /* Look for existing session key info */
        if (ap_table_get(rp->subprocess_env, "SESSION_KEY") &&
            ap_table_get(rp->subprocess_env, "SESSION_KEY_METHOD")) {

            ap_table_set(r->subprocess_env, "SESSION_KEY",
                      ap_table_get(rp->subprocess_env, "SESSION_KEY"));
            ap_table_set(r->subprocess_env, "SESSION_KEY_METHOD",
                      ap_table_get(rp->subprocess_env, "SESSION_KEY_METHOD"));
            ap_table_set(r->subprocess_env, "SESSION_KEY_NAME",
                      ap_table_get(rp->subprocess_env, "SESSION_KEY_NAME"));

            /* propagate notes settings as well */
            ap_table_set(r->notes, "SESSION_KEY",
                      ap_table_get(rp->notes, "SESSION_KEY"));
            ap_table_set(r->notes, "SESSION_KEY_METHOD",
                      ap_table_get(rp->notes, "SESSION_KEY_METHOD"));
            ap_table_set(r->notes, "SESSION_KEY_NAME",
                      ap_table_get(rp->notes, "SESSION_KEY_NAME"));

            return 1;
        }

        rp = rp->prev;
    }

    /* 2) r->main */
    if (r->main) {
        /* Propagate any parent request Set-Cookie headers */
        if (ap_table_get(r->main->headers_out, "Set-Cookie"))
            ap_table_set(r->headers_out, "Set-Cookie",
                      ap_table_get(r->main->headers_out, "Set-Cookie"));

        /* Look for existing session key info */
        if (ap_table_get(r->main->subprocess_env, "SESSION_KEY") &&
            ap_table_get(r->main->subprocess_env, "SESSION_KEY_METHOD")) {

            ap_table_set(r->subprocess_env, "SESSION_KEY",
                      ap_table_get(r->main->subprocess_env, "SESSION_KEY"));
            ap_table_set(r->subprocess_env, "SESSION_KEY_METHOD",
                  ap_table_get(r->main->subprocess_env, "SESSION_KEY_METHOD"));
            ap_table_set(r->subprocess_env, "SESSION_KEY_NAME",
                    ap_table_get(r->main->subprocess_env, "SESSION_KEY_NAME"));

            /* propagate notes settings as well */
            ap_table_set(r->notes, "SESSION_KEY",
                      ap_table_get(r->main->notes, "SESSION_KEY"));
            ap_table_set(r->notes, "SESSION_KEY_METHOD",
                      ap_table_get(r->main->notes, "SESSION_KEY_METHOD"));
            ap_table_set(r->notes, "SESSION_KEY_NAME",
                      ap_table_get(r->main->notes, "SESSION_KEY_NAME"));

            return 1;
        }
    }


    /* 3) Look for a HTTP Cookie header */
    if (!conf->disable_cookies)
        if ((str = ap_table_get(r->headers_in, "Cookie"))) {
            sprintf(buffer, "%s=", conf->cookie_name);
            if ((val = strstr(str, buffer))) {
                val += strlen(buffer);
                ptr = val + 1;
                /* skip forward to end of cookie value */
                while ((*ptr != ';') && (*ptr != '\0'))
                    ptr++;
                strncpy(buffer, val, ptr - val);
                buffer[ptr - val] = '\0';

                ap_table_set(r->subprocess_env, "SESSION_KEY", buffer);
                ap_table_set(r->subprocess_env, "SESSION_KEY_METHOD", "COOKIE");
                ap_table_set(r->subprocess_env, "SESSION_KEY_NAME", conf->cookie_name);

                /* set notes for logging */
                ap_table_set(r->notes, "SESSION_KEY", buffer);
                ap_table_set(r->notes, "SESSION_KEY_METHOD", "COOKIE");
                ap_table_set(r->notes, "SESSION_KEY_NAME", conf->cookie_name);

                return 1;
            }
        }


    /* 4) Look in the query string */
    if (r->args) {
        sprintf(buffer, "%s=", conf->url_sid_name);
        if ((val = strstr(r->args, buffer))) {
            val += strlen(buffer);
            ptr = val + 1;
            /* skip forward to end of cookie value */
            while ((*ptr != '&') && (*ptr != '\0'))
                ptr++;
            strncpy(buffer, val, ptr - val);
            buffer[ptr - val] = '\0';

            ap_table_set(r->subprocess_env, "SESSION_KEY", buffer);
            ap_table_set(r->subprocess_env, "SESSION_KEY_METHOD", "URL");
            ap_table_set(r->subprocess_env, "SESSION_KEY_NAME", conf->url_sid_name);

            /* set notes for logging */
            ap_table_set(r->notes, "SESSION_KEY", buffer);
            ap_table_set(r->notes, "SESSION_KEY_METHOD", "URL");
            ap_table_set(r->notes, "SESSION_KEY_NAME", conf->url_sid_name);

            return 1;
        }
    }

    /* 5) Look in the path info (special case imagemap) */
    if (r->path_info) {
        sprintf(buffer, "%s=", conf->url_sid_name);
        if ((val = strstr(r->path_info, buffer))) {
            val += strlen(buffer);
            ptr = val + 1;
            /* skip forward to end of cookie value */
            while ((*ptr != '&') && (*ptr != '/') && (*ptr != '\0'))
                ptr++;
            strncpy(buffer, val, ptr - val);
            buffer[ptr - val] = '\0';

            ap_table_set(r->subprocess_env, "SESSION_KEY", buffer);
            ap_table_set(r->subprocess_env, "SESSION_KEY_METHOD", "URL");
            ap_table_set(r->subprocess_env, "SESSION_KEY_NAME", conf->url_sid_name);

            /* set notes for logging */
            ap_table_set(r->notes, "SESSION_KEY", buffer);
            ap_table_set(r->notes, "SESSION_KEY_METHOD", "URL");
            ap_table_set(r->notes, "SESSION_KEY_NAME", conf->url_sid_name);

            return 1;
        }
    }

    return 0;
}

/*
 * session_match_iterate
 *
 * Try to find a matching regexp in an rlist
 *
 */
static int session_match_iterate(rlist * regexps, const char *arg)
{
    if (regexps == NULL)
        return 0;

    while (regexps != NULL) {
        if (regexps->expression == NULL)
            continue;

        if (!regexec(regexps->expression, arg, 0, NULL, 0))
            return 1;

        regexps = regexps->next;
    }

    return 0;
}


/* 
 * session_check_valid_entry:
 *
 * Is the current request for one of the locations where an up
 * front session key is not absolutly required?
 *
 */
static int session_check_valid_entry(request_rec *r, session_config_rec * conf)
{
    return session_match_iterate(conf->valid_entry, r->uri);
}


/*
 * is_exempt
 *
 * exempt certain things from session control and top level redirection
 * when a session key is *not* present.
 *
 * This is usefull for things we really want cached on the client/proxy
 * end (like images).  Forcing a session key in the URL would impede this.
 *
 * It is also usefull when we don't want to force session control or keys
 * onto seach engine robots.
 *
 */
static int session_is_exempt(request_rec *r, session_config_rec * conf)
{
    /* exempt content types or handlers */
    if (conf->exempt_types) {
        if (r->content_type &&
            session_match_iterate(conf->exempt_types, r->content_type))
            return 1;
        if (r->handler &&
            session_match_iterate(conf->exempt_types, r->handler))
            return 1;
    }
    else {
        if (r->content_type &&
          !regexec(conf->default_exempt_types, r->content_type, 0, NULL, 0))
            return 1;
        if (r->handler &&
            !regexec(conf->default_exempt_types, r->handler, 0, NULL, 0))
            return 1;
    }

    /* exempt locations */
    if ((conf->exempt_locations) &&
        (session_match_iterate(conf->exempt_locations, r->uri)))
        return 1;

    return 0;
}

/*
 * session_must_filter:
 *
 * Check to see if the mime type of the file in this request mandates a
 * pass through our external filter.
 *
 */
static int session_must_filter(request_rec *r, session_config_rec * conf)
{
    /* check mime type */
    if (r->content_type) {
        if (conf->filter_types)
            return session_match_iterate(conf->filter_types, r->content_type);
        else
            return !regexec(conf->default_filter_types, r->content_type, 0, NULL, 0);
	}

    /* check handlers */
    if (r->handler) {
        if (conf->filter_types)
            return session_match_iterate(conf->filter_types, r->handler);
        else
            return !regexec(conf->default_filter_types, r->handler, 0, NULL, 0);
	}

    return 0;
}

/*
 * session_has_expired
 *
 * Check to see if a session key has expired and needs to be forcibly
 * re-issued.  This function has a side effect of forcefully expiring
 * the sessionkey when the hostname of the client does not match the one in the 
 * recieved session key (that's on purpose too).
 *
 */
static int session_has_expired(request_rec *r, session_config_rec * conf)
{
    const char *skey;
    char *digits;
    char *ptr;
    char *rname;

    rname = ap_pstrdup(r->pool, ap_get_remote_host(r->connection, r->per_dir_config, REMOTE_NAME));

    /* get creation time of session key */
    /* time stamp is everything up to first '_' character */

    skey = ap_table_get(r->subprocess_env, "SESSION_KEY");
    ptr = strchr(skey, '_');

    if (ptr == NULL)
        /* can't get the time, force expiration */
        return 1;

    if (conf->more_digits)
        digits = ap_pstrndup(r->pool, skey, ptr - skey - 3);
    else
        digits = ap_pstrndup(r->pool, skey, ptr - skey);

    if (!strcmp(ap_table_get(r->subprocess_env, "SESSION_KEY_METHOD"), "COOKIE") &&
        (conf->cookie_expire > 0)) {
        if (atol(digits) + conf->cookie_expire < time(NULL))
            return 1;
    }
    else if (!strcmp(ap_table_get(r->subprocess_env, "SESSION_KEY_METHOD"), "URL") &&
             (conf->url_expire > 0))
        if (atol(digits) + conf->url_expire < time(NULL))
            return 1;

    return 0;
}

/*
 * session_fixup:
 *
 * The main event.
 *
 */
static int session_fixup(request_rec *r)
{
    request_rec *rp;
    session_config_rec *conf = NULL;

    /* user-agent exemptions set with BrowserMatch */
    if (ap_table_get(r->subprocess_env, BROWSER_EXEMPT))
        return DECLINED;

    /*
     * First we need to decide if we are relevant here.  There are three
     * cases in which we are:
     *
     * 1) some parent request via r->prev has session key info in it.
     *
     * 2) conf->path matches r->uri.  This happens when this module
     *    was configured in a <Location> directive.
     *
     * 3) conf->path matches r->filename.  This happens when this module
     *    was configured in a .htaccess file.
     *
     */

    /* case 1 */
    if (r->prev) {
        rp = r->prev;
        while (rp) {

            if (ap_table_get(rp->subprocess_env, "SESSION_KEY") &&
                ap_table_get(rp->subprocess_env, "SESSION_KEY_METHOD")) {

                /* get the configuration we need from the parent request */
                conf = (session_config_rec *) ap_get_module_config(rp->per_dir_config, &session_module);
                break;
            }
            rp = rp->prev;
        }
    }

    /* cases 2) and 3) */
    if (!conf) {
        conf = (session_config_rec *) ap_get_module_config(r->per_dir_config, &session_module);
        if (!conf->path ||
            ((strncmp(r->uri, conf->path, strlen(conf->path)) != 0) &&
             (strncmp(r->filename, conf->path, strlen(conf->path)) != 0)))
            return DECLINED;
    }

    /* Now check that the required configs are set */
    if (!conf->cookie_name) {
        ap_log_reason("missing SessionCookieName directive", r->filename, r);
        return SERVER_ERROR;
    }

    if (!conf->cookie_path) {
        ap_log_reason("missing SessionCookiePath directive", r->filename, r);
        return SERVER_ERROR;
    }

    if (!conf->url_sid_name) {
        ap_log_reason("missing SessionUrlSidName directive", r->filename, r);
        return SERVER_ERROR;
    }

    if (!conf->external_filter) {
        ap_log_reason("missing SessionFilter directive", r->filename, r);
        return SERVER_ERROR;
    }

    /* required pairs */
    if ((conf->valid_entry && !conf->session_top) ||
        (!conf->valid_entry && conf->session_top)) {
        ap_log_reason("both or none of SessionTop and SessionValidEntry must be set",
                   r->filename, r);
        return SERVER_ERROR;
    }

    /* Now the hocus pocus */
    if (!session_detect_key(r, conf) || session_has_expired(r, conf)) {
        /* If under entry point control and this isn't a valid entry point */
        if (conf->valid_entry && conf->session_top &&
            !session_check_valid_entry(r, conf)) {
            /* special types, handlers and locations are exempt */
            if (session_is_exempt(r, conf))
                return OK;
            else {
                ap_table_set(r->headers_out, "Location", conf->session_top);
                return REDIRECT;
            }
		}

        /* create the session key */
        session_create_key(r, conf);
    }

    /* set up special handling of imagemaps and external filtering if the
     * session key came from a URL.
     */

    if (!strcmp(ap_table_get(r->subprocess_env, "SESSION_KEY_METHOD"), "URL")) {
        /* imagemap (check for looped subrequest first) */
        if ((!r->main || (!ap_table_get(r->main->notes, "session_imap_subreq"))) &&
            ((r->handler && !strcmp(r->handler, "imap-file")) ||
             (r->content_type && !strcmp(r->content_type, "application/x-httpd-imap"))))
            /* hijack this imagemap request */
            r->handler = ap_pstrdup(r->pool, "session-imap-file");

    /* post filtered types */
        else if (session_must_filter(r, conf))
            /* hijack this request */
            r->handler = ap_pstrdup(r->pool, "session-postparse");
	}

    return OK;
}

static handler_rec session_handlers[] =
{
    {"session-imap-file", session_imap_handler},
    {"session-postparse", session_filter_handler},
    {NULL}
};

module session_module =
{
    STANDARD_MODULE_STUFF,
    NULL,                       /* initializer */
    session_create_dir_config,  /* dir config creater */
    NULL,                       /* dir merger --- default is to override */
    NULL,                       /* server config */
    NULL,                       /* merge server config */
    session_cmds,               /* command table */
    session_handlers,           /* handlers */
    NULL,                       /* filename translation */
    NULL,                       /* check_user_id */
    NULL,                       /* check auth */
    NULL,                       /* check access */
    NULL,                       /* type_checker */
    session_fixup,              /* fixups */
    NULL,                       /* logger */
    NULL,                       /* header parse */
    NULL,                       /* child_init */
    NULL,                       /* child_exit */
    NULL                        /* post read-request */
};
