Source Code for FrontPage Apache Module

mod_frontpage.c

/* ====================================================================
 *
 * Apache FrontPage module.
 *
 * Copyright (c) 1996-1997 Microsoft Corporation -- All Rights Reserved.
 *
 * NO WARRANTIES. Microsoft expressly disclaims any warranty for this code and
 * information. This code and information and any related documentation is
 * provided "as is" without warranty of any kind, either express or implied,
 * including, without limitation, the implied warranties or merchantability,
 * fitness for a particular purpose, or noninfringement. The entire risk
 * arising out of use or performance of this code and information remains with
 * you.
 *
 * NO LIABILITY FOR DAMAGES. In no event shall Microsoft or its suppliers be
 * liable for any damages whatsoever (including, without limitation, damages
 * for loss of business profits, business interruption, loss of business
 * information, or any other pecuniary loss) arising out of the use of or
 * inability to use this Microsoft product, even if Microsoft has been advised
 * of the possibility of such damages. Because some states/jurisdictions do not
 * allow the exclusion or limitation of liability for consequential or
 * incidental damages, the above limitation may not apply to you.
 *
 * $Revision: 1.3 $
 * $Date: 1997/10/15 17:23:46 $
 *
 */


/*
 * User configurable items.  We will not run the server extensions with any
 * UID/GID less than LOWEST_VALID_UID/LOWEST_VALID_GID.
 */

#if defined(LINUX)
#define LOWEST_VALID_UID 15
#else
#define LOWEST_VALID_UID 11
#endif

#if defined(HPUX) || defined(IRIX) || defined(SUNOS4)
#define LOWEST_VALID_GID 20
#else
#if defined(SCO)
#define LOWEST_VALID_GID 24
#else
#define LOWEST_VALID_GID 21   /* Solaris, AIX, Alpha, Bsdi, etc. */
#endif
#endif

/*
 * End of user configurable items
 */


#include "httpd.h"
#include "http_config.h"
#include "http_conf_globals.h"

#include <stdio.h>
#include <sys/time.h>

#ifndef TRUE
#define TRUE 1
#endif

#ifndef FALSE
#define FALSE 0
#endif

#ifndef MAXPATHLEN
#define MAXPATHLEN 1024
#endif
#if (MAXPATHLEN < 1024)
#undef MAXPATHLEN
#define MAXPATHLEN 1024
#endif

#define KEYLEN 128                  /* Should be a multiple of sizeof(int) */

static char gszKeyVal[KEYLEN+1];    /* SUID key value used by this module */
static int  gfdKeyPipe[2];          /* Pipe to fpexe stub CGI */
static int  gbKeyPipeActive;        /* Pipe to fpexe stub CGI is active */
static int  gbEnabled;              /* TRUE when SUID scheme is enabled */
static int  giInitializeCount;      /* FrontPageInit called previously */

static const char* FP         =
          "/usr/local/frontpage/currentversion";
static const char* FPKEYDIR   =
          "/usr/local/frontpage/currentversion/apache-fp";
static const char* KEYFILEXOR =
          "/usr/local/frontpage/currentversion/apache-fp/suidkey";
static const char* KEYFILE    =
          "/usr/local/frontpage/currentversion/apache-fp/suidkey.%d";
static const char* FPSTUBDIR  =
          "/usr/local/frontpage/currentversion/apache-fp/_vti_bin";
static const char* FPSTUB     =
          "/usr/local/frontpage/currentversion/apache-fp/_vti_bin/fpexe";
static const char* VTI_BIN    =
          "/_vti_bin";
static const char* SHTML      =
          "/_vti_bin/shtml.exe";
static const char* FPCOUNT    =
          "/_vti_bin/fpcount.exe";
static const char* AUTHOR     =
          "/_vti_bin/_vti_aut/author.exe" ;
static const char* ADMIN      =
          "/_vti_bin/_vti_adm/admin.exe" ;


/*
 * Print a descriptive error in the httpd's error_log.  The format string
 * should be length limited so that it is no longer than 1800 bytes.
 */
static void LogFrontPageError(
    server_rec* s,
    const char* szFormat,
    const char* szFile,
    const char* szRoutine,
    int bIsDisabled)
{
    char szBuf[MAXPATHLEN * 2];
    sprintf(szBuf, szFormat, szFile);
    strcat(szBuf, " in ");
    strcat(szBuf, szRoutine);
    strcat(szBuf, ".");
    if (bIsDisabled)
    {
        strcat(szBuf, "  Until this problem is fixed, the FrontPage security patch is disabled and the FrontPage extensions may not work correctly.");
        gbEnabled = FALSE;            /* Make double sure we're not enabled */
    }
    log_error(szBuf, s);
}


/*
 * Clean up stale keyfiles.  Failure to clean up stale keyfiles does not
 * stop the FrontPage SUID scheme.
 */
static void FrontPageCleanup(server_rec *s)
{
    DIR *d;
    struct DIR_TYPE *dstruct;
    int myPid = getpid();
    
    if (!(d = opendir(FPKEYDIR)))
    {
        /*
         * This should be a rare occurrence, because we're running as root and
         * should have access to the directory.  Stale key files can be
         * exploited.  User recovery: Check that the directory exists and is
         * properly protected (owned by root, permissions rwx--x--x), and that
         * there are no stale key files in it (suidkey.*, where * is a
         * non-existant PID).
         */
        LogFrontPageError(s, "Can't clean stale key files from directory \"%-.1024s\"",
                          FPKEYDIR, "FrontPageCleanup()", FALSE);
        return;
    }

    while ((dstruct = readdir(d)))
    {
        if (strncmp("suidkey.", dstruct->d_name, 8) == 0)
        {
            /*
             * Make sure the key file contains a pid number - otherwise
             * it is harmless and you can ignore it.
             */
            char* pEnd = 0;
            int pid = strtol(dstruct->d_name + 8, &pEnd, 10);
            if (!pEnd || *pEnd)
                continue;

            /*
             * Make sure there isn't some other server using this key file.
             * If the process group isn't alive, then the file is stale
             * and we want to remove it.
             */
            if (pid == myPid || kill(pid, 0) == -1)
            {
                char szBuf[MAXPATHLEN];
                sprintf(szBuf, "%-.500s/%-.500s", FPKEYDIR, dstruct->d_name);
                if (unlink(szBuf) == -1)
                {
                    /*
                     * This should be a rare occurrence, because we're running
                     * as root and should always have permission to delete the
                     * file.  Stale key files can be exploited.  User recovery:
                     * delete the offending file.
                     */
                    LogFrontPageError(s, "Can't unlink stale key file \"%-.1024s\"",
                                      szBuf, "FrontPageCleanup()", FALSE);
                }
            }
        }
    }

    closedir(d);
}

/*
 * Checks that all the permissions are currently correct for the FrontPage
 * fpexe SUID stub to run correctly.  If not, it logs an error and aborts
 * initialization, effectively disabling the FrontPage SUID scheme.
 * It checks both the file permissions (owned by root and not writable to
 * group, other) and that the directory is not writable.
 */
static int FrontPageCheckup(server_rec *s)
{
    struct stat fs;
 
    if (geteuid() != 0)
    {
        /*
         * We need to be root to have the security scheme work correctly.
         * User recovery: run the server as root.
         */
        LogFrontPageError(s, "Not running as root",
                          0, "FrontPageCheckup()", TRUE);
        return (FALSE);
    }

    if (lstat(FPKEYDIR, &fs) == -1          || /* We can't stat the key dir */
        fs.st_uid                           || /* key dir not owned by root */
        (fs.st_mode & (S_IRGRP | S_IROTH))  || /* key dir is readable */
        (fs.st_mode & (S_IWGRP | S_IWOTH))  || /* key dir is writable */
        !(fs.st_mode & (S_IXGRP | S_IXOTH)) || /* key dir is not executable */
        !(S_ISDIR(fs.st_mode)))
    {
        /*
         * User recovery: set directory to be owned by by root with permissions
         * rwx--x--x.  Note you need the execute bit for group and other so
         * that non-root programs can run apache-fp/_vti_bin/fpexe (even though
         * non-root cannot list the directory).
         */
        LogFrontPageError(s, "Incorrect permissions on key directory \"%-.1024s\"",
                          FPKEYDIR, "FrontPageCheckup()", TRUE);
        return (FALSE);
    }

    if (lstat(FPSTUBDIR, &fs) == -1         || /* We can't stat the stub dir */
        fs.st_uid                           || /* stub dir not owned by root */
        (fs.st_mode & (S_IWGRP | S_IWOTH))  || /* stub dir is writable */
        (!S_ISDIR(fs.st_mode)))
    {
        /*
         * User recovery: set directory to be owned by by root with permissions
         * r*x*-x*-x.
         */
        LogFrontPageError(s, "Incorrect permissions on stub directory \"%-.1024s\"",
                          FPSTUBDIR, "FrontPageCheckup()", TRUE);
        return (FALSE);
    }

    if (stat(FPSTUB, &fs) == -1             || /* We can't stat the stub */
        fs.st_uid                           || /* stub not owned by root */
        !(fs.st_mode & S_ISUID)             || /* stub is not set-uid */
        (fs.st_mode & S_ISGID)              || /* stub is set-gid */
        (fs.st_mode & (S_IWGRP | S_IWOTH))  || /* stub is writable */
        !(fs.st_mode & (S_IXGRP | S_IXOTH)))   /* stub is not executable */
    {
        /*
         * User recovery: set stub to be owned by by root with permissions
         * r*s*-x*-x.
         */
        LogFrontPageError(s, "Incorrect permissions on stub \"%-.1024s\"",
                          FPSTUB, "FrontPageCheckup()", TRUE);
        return (FALSE);
    }

    return (TRUE);
}


/*
 * Module-initializer: Create the suidkey file and local value.
 * Everything needs to be just right, or we don't create the key file, and
 * therefore, the fpexe SUID stub refuses to run.
 */
static void FrontPageInit(server_rec *s, pool *p)
{
    int fdPipe[2];
    pid_t pid;
    FILE *f;
    struct stat fs;
    int fd;
    char szKeyFile[MAXPATHLEN];
    int iRandom[5];
    char* szRandom = (char*)iRandom;
    struct timeval tp;
    struct timezone tz;
    
    (void)p;   /* p is unused */

    /*
     * Standalone servers call initialization twice: once in main() and again
     * in standalone_main().  The fully initializing on the the first call is a
     * waste of time, and a race condition can leave a stale suidkey.pgrpid
     * file around.
     */
    if (standalone && !giInitializeCount++)
        return;

    /*
     * Disable the suid scheme until everything falls perfectly into place.
     */
    gbEnabled = FALSE;
    gbKeyPipeActive = FALSE;

    /*
     * Clean up old key files before we start
     */
    FrontPageCleanup(s);
    if (!FrontPageCheckup(s))
        return;
    
    if (pipe(fdPipe) == -1)
    {
        /*
         * This should be a rare occurrence.  User recovery: check to see why
         * the system cannot allocate a pipe (is the file table full from
         * run-away processes?), and fix the problem or reboot, then try again.
         */
        LogFrontPageError(s, "pipe() failed", 0, "FrontPageInit()", TRUE);
        return;
    }
    
    gettimeofday(&tp, &tz);
    iRandom[0] = tp.tv_sec;
    iRandom[1] = tp.tv_usec | tp.tv_usec << 20;

    pid = fork();
    if (pid == -1)
    {
        /*
         * This should be a rare occurrence.  User recovery: check to see why
         * the system cannot allocate a process (is the process table full from
         * run-away processes?), and fix the problem or reboot, then try again.
         */
        LogFrontPageError(s, "fork() failed", 0, "FrontPageInit()", TRUE);
        return;
    }
    
    if (pid)
    {
        /*
         * I am the parent process.  Try to read a random number from the
         * child process.
         */

        unsigned int npos = (unsigned int)-1;
        unsigned int v1 = npos, v2 = npos, v3 = npos, v4 = npos;
        int stat;
        int iCount;

        close(fdPipe[1]);
        if (waitpid(pid, &stat, 0) == -1 ||
            (!WIFEXITED(stat) || WIFEXITED(stat) && WEXITSTATUS(stat)))
        {
            /*
             * This should be a rare occurrence.  User recovery: Make sure you
             * have a /bin/sh, or change the shell location in the execl
             * command below.  Try the commands defined in RAND_CMD in a
             * /bin/sh session to make sure they work properly.  Rebuild this
             * module and your httpd with the proper commands.
             */
            LogFrontPageError(s, "Random number generator exited abnormally", 0,
                              "FrontPageInit()", TRUE);
            return;
        }

        iCount = read(fdPipe[0], gszKeyVal, KEYLEN);
        close(fdPipe[0]);
        if (iCount < 0)
        {
            /*
             * This should be a rare occurrence.  See the above comment under
             * the waitpid failure condition for user recovery steps.
             */
            LogFrontPageError(s, "Could not read random numbers", 0,
                              "FrontPageInit()", TRUE);
            return;
        }
        gszKeyVal[iCount] = 0;

        sscanf(gszKeyVal, "%u %u %u %u", &v2, &v1, &v4, &v3);
        if (v1 == npos || v2 == npos || v3 == npos || v4 == npos)
        {
            /*
             * This should be a rare occurrence.  See the above comment under
             * the waitpid failure condition for user recovery steps.
             */
            LogFrontPageError(s, "Could not scan random numbers", 0,
                              "FrontPageInit()", TRUE);
            return;
        }

        iRandom[2] = (v1 << 16) + v2 + (v4 << 12) + v3;
    }
    else
    {
        /*
         * I am the child process.  Create a random number which shouldn't
         * be easily duplicated.
         */

        if (dup2(fdPipe[1], 1) == -1)
            exit(1);                    /* Parent picks up the error */

        close(fdPipe[0]);

#ifdef LINUX
#define RAND_CMD "/bin/ps laxww | /usr/bin/sum ; /bin/ps laxww | /usr/bin/sum"
#else
#ifdef bsdi
#define RAND_CMD "/bin/ps laxww | /usr/bin/cksum -o 1 ; /bin/ps laxww | /usr/bin/cksum -o 1"
#else
#define RAND_CMD "/bin/ps -ea | /bin/sum ; /bin/ps -ea | /bin/sum"
#endif
#endif
        execl("/bin/sh", "/bin/sh", "-c", RAND_CMD, NULL);
        exit(1);
    }

    gettimeofday(&tp, &tz);
    iRandom[3] = tp.tv_sec;
    iRandom[4] = tp.tv_usec | tp.tv_usec << 20;

    /*
     * See if there is an 'suidkey' file to merge into our key.
     */
    if (stat(KEYFILEXOR, &fs) == -1)
    {
        /*
         * It's a security violation if the key file is not present.  User
         * recovery: Make sure the key file is present and properly protected
         * (owned by root, permissions r**------).
         */
        LogFrontPageError(s, "The key file \"%-.1024s\" does not exist",
                          KEYFILEXOR, "FrontPageInit()", TRUE);
        return;
    }
    else
    {
        int i, iCount;
        char szBuf[KEYLEN];

        if ((fs.st_mode & (S_IRWXG | S_IRWXO)) || fs.st_uid)
        {
            /*
             * It's a security violation if the key file is not owned by root,
             * and is not protected from all other group. User recovery: Make
             * sure the key file is properly protected (owned by root,
             * permissions r**------).
             */
            LogFrontPageError(s, "The key file \"%-.1024s\" must not be accessible except by root",
                              KEYFILEXOR, "FrontPageInit()", TRUE);
            return;
        }

        if ((fd = open(KEYFILEXOR, O_RDONLY)) == -1)
        {
            /*
             * This should be a rare occurrence.  User recovery: Make sure
             * the key file exists, is properly owned and protected, and is
             * readable.
             */
            LogFrontPageError(s, "Cannot open key file \"%-.1024s\"",
                              KEYFILEXOR, "FrontPageInit()", TRUE);
            return;
        }

        iCount = read(fd, szBuf, KEYLEN);
        if (iCount < 8)
        {
            /*
             * The keyfile must be at least 8 bytes.  If it longer than 128
             * bytes, only the first 128 bytes will be used.  Any character
             * value from 0-255 is fine.  User recovery: Make sure the key file
             * is at least 8 bytes long.
             */
            LogFrontPageError(s, "Key file \"%-.1024s\" is unreadable or is too short",
                              KEYFILEXOR, "FrontPageInit()", TRUE);
            return;
        }

        /*
         * Now generate the effective key we'll be using by XORing your key
         * with 5 "random" 32-bit integers.  The primary security of this
         * scheme is your key; properly setting it and changing it often keeps
         * the FrontPage SUID scheme secure.  All this work above to generate 5
         * random 32-bit integers is soley to make your key somewhat harder to
         * crack (assuming the key files are properly protected).  If you don't
         * like the algorithm used to generate the 5 random integers, feel free
         * to substitute as appropriate (check out SGI's Lavarand (TM) at
         * lavarand.sgi.com).
         */
        for (i = 0;  i < KEYLEN;  i++)
            gszKeyVal[i] = szBuf[i % iCount] ^ szRandom[i % sizeof(iRandom)];
    }

#if defined(SUNOS4)
    pid = getpgrp(0);
#else
    pid = getpgrp();
#endif
    sprintf(szKeyFile, KEYFILE, (int)pid);

    fd = creat(szKeyFile, 0600);
    if (fd < 0)
    {
        /*
         * This should be a rare occurrence, because we're running as root and
         * should always have permission to create the file.  User recovery:
         * check that you are not out of disk space, or that the file is not
         * NFS-mounted on a share where you do not have permissions.
         */
        LogFrontPageError(s, "Could not create key file \"%-.1024s\"",
                          szKeyFile, "FrontPageInit()", TRUE);
        return;
    }

    if (write(fd, gszKeyVal, 128) != 128)
    {
        /*
         * This should be a rare occurrence.  User recovery: check that you are
         * not out of disk space.
         */
        close(fd);  
        unlink(szKeyFile);
        LogFrontPageError(s, "Could not write to key file \"%-.1024s\"",
                          szKeyFile, "FrontPageInit()", TRUE);
        return;
    }

    close(fd);  

    /*
     * Everything looks OK enough to start the suid scheme.
     */
    gbEnabled = TRUE;
}


/*
 * Look for a valid FrontPage extensions scenario and fake a scriptalias if
 * appropriate.  If there are any problems, we silently decline.
 */
static int FrontPageAlias(
    request_rec* r,
    char* szCgi,
    const char* szFpexe)
{
    int iLen;
    struct stat webroot;
    struct stat vti_pvt;
    struct stat stub;
    char szBuf[MAXPATHLEN];
    char chSave;

    /*
     * Decline if we cannot run the stub, or it is writable.
     */
    if (stat(FPSTUB, &stub) == -1 || !(stub.st_mode & S_IXOTH) ||
        stub.st_mode & (S_IWGRP | S_IWOTH))
    {
        /*
         * The stub used to be correctly permissioned; what happened?  User
         * recovery: set stub to be owned by by root with permissions
         * r*s*-x*-x.
         */
        LogFrontPageError(r->server, "Incorrect permissions on stub \"%-.1024s\"",
                          FPSTUB, "FrontPageAlias()", FALSE);
        return DECLINED;
    }

    chSave = szCgi[1];
    szCgi[1] = '\0';
    translate_name(r);
    szCgi[1] = chSave;

    /*
     * Zap trailing slash that confuses some OSes.
     */
    iLen = strlen(r->filename);
    r->filename[--iLen] = 0;

    if (iLen > MAXPATHLEN - 10)
        return DECLINED;
    sprintf(szBuf, "%s/_vti_pvt", r->filename);

    /*
     * Decline if webroot and webroot/_vti_pvt don't have the same
     * user and group or uid < LOWEST_VALID_UID or gid < LOWEST_VALID_GID.
     */
    if (stat(szBuf, &vti_pvt) == -1       ||
        vti_pvt.st_uid < LOWEST_VALID_UID ||
        vti_pvt.st_gid < LOWEST_VALID_GID ||
        stat(r->filename, &webroot) != 0  ||
        webroot.st_uid != vti_pvt.st_uid  || 
        webroot.st_gid != vti_pvt.st_gid)
    {
        /*
         * The webroot and webroot/_vti_pvt don't match.  User recovery: fix
         * the owners and groups of both directories to match, and have both a
         * uid and gid in the allowable range.
         */
        LogFrontPageError(r->server, "Incorrect permissions on webroot \"%-.0124s\" and webroot's _vti_pvt directory",
                          szBuf, "FrontPageAlias()", FALSE);
        return DECLINED;
    }
 
    /*
     * If the pipe is active, it was because we previously executed a CGI.
     * That CGI must have finished by now (otherwise we wouldn't be processing
     * this next request), so we can and should close the pipe to avoid a
     * resource leak.
     */
    if (gbKeyPipeActive)
    {
        close(gfdKeyPipe[0]);
        gbKeyPipeActive = FALSE;
    }

    /*
     * If we can't get a pipe, that's really bad.  We'll log an error, and
     * decline.  This should be a rare occurrence.  User recovery: check to see
     * why the system cannot allocate a pipe (is the file table full from
     * run-away processes?), and fix the problem or reboot, then try again.
     */
    if (pipe(gfdKeyPipe) == -1)
    {
        LogFrontPageError(r->server, "pipe() failed", 0,
                          "FrontPageAlias()", FALSE);
        return DECLINED;
    }

    /*
     * Note: pstrdup allocates memory, but it checks for out of memory
     * conditions - it will not return if out of memory.
     */
    r->handler = pstrdup(r->pool, "cgi-script");
    table_set(r->notes, "alias-forced-type", r->handler);

    table_set(r->subprocess_env, "FPEXE", pstrdup(r->pool, szFpexe));
    sprintf(szBuf, "%d", webroot.st_uid );
    table_set(r->subprocess_env, "FPUID", pstrdup(r->pool, szBuf));
    sprintf(szBuf, "%d", webroot.st_gid );
    table_set(r->subprocess_env, "FPGID", pstrdup(r->pool, szBuf));
    sprintf(szBuf, "%d", gfdKeyPipe[0]);
    table_set(r->subprocess_env, "FPFD", pstrdup(r->pool, szBuf));

    r->execfilename = pstrcat(r->pool, FPSTUB, szCgi + strlen(szFpexe), NULL);
    r->filename = pstrcat(r->pool, r->filename, szCgi, NULL);

    if (write(gfdKeyPipe[1], gszKeyVal, 128) != 128)
    {
        /*
         * If we can't write to the pipe, that's really bad.  We'll log an
         * error, and decline.  This should be a rare occurrence.  User
         * recovery: check to see why the system cannot write to the pipe (is
         * the system being choked with too much load?), and fix the problem or
         * reboot, then try again.
         */
        LogFrontPageError(r->server, "Write to pipe failed", 0,
                          "FrontPageAlias()", FALSE);
        close (gfdKeyPipe[0]);
        close (gfdKeyPipe[1]);
        return DECLINED;
    }
    close(gfdKeyPipe[1]);

    gbKeyPipeActive = TRUE;
    return OK;
}


/*
 * This routine looks for shtml.exe, fpcount.exe, author.exe and admin.exe
 * in a URI, and if found we call FrontPageAlias() to check for a valid
 * FrontPage scenario.
 *
 * The return value is OK or DECLINED.
 */
static int FrontPageXlate(
    request_rec *r)
{
    char *szVti;
    char *szCgi;

    /*
     * Decline if we're improperly initialized.
     */
    if (!gbEnabled)
        return DECLINED;

    /*
     * Check once for anything with _vti_bin.  This is much faster than
     * checking all four paths, because anything without this is definitely
     * not a FrontPage scenario.
     */
    if (!(szVti = strstr(r->uri, VTI_BIN)))
        return DECLINED;

    /* 
     * Test for FrontPage server extenders:
     * .../_vti_bin/shtml.exe...
     * .../_vti_bin/fpcount.exe...
     * .../_vti_bin/_vti_aut/author.exe...
     * .../_vti_bin/_vti_adm/admin.exe...
     */
    if (szCgi = strstr(szVti, AUTHOR ))
        return FrontPageAlias(r, szCgi, AUTHOR);
    if (szCgi = strstr(szVti, SHTML  ))
        return FrontPageAlias(r, szCgi, SHTML);
    if (szCgi = strstr(szVti, ADMIN  ))
        return FrontPageAlias(r, szCgi, ADMIN);
    if (szCgi = strstr(szVti, FPCOUNT))
        return FrontPageAlias(r, szCgi, FPCOUNT);

    return DECLINED;    
}


/*
 * Declare ourselves so the configuration routines can find us.
 */
module frontpage_module = 
{
    STANDARD_MODULE_STUFF,
    FrontPageInit,             /* initializer */
    NULL,                      /* per-directory config creater */
    NULL,                      /* dir config merger - default is to override */
    NULL,                      /* server config creator */
    NULL,                      /* server config merger */
    NULL,                      /* command table */
    NULL,                      /* [6] list of handlers */
    FrontPageXlate,            /* [1] filename-to-URI translation */
    NULL,                      /* [4] check/validate HTTP user_id */
    NULL,                      /* [5] check HTTP user_id is valid *here* */
    NULL,                      /* [3] check access by host address, etc. */
    NULL,                      /* [6] MIME type checker/setter */
    NULL,                      /* [7] fixups */
    NULL,                      /* [9] logger */
    NULL,                      /* [2] header parser */
};