/*
 * Handle reading and parsing of rule file.
 *
 *    int http_parse_elements ( int limit, char *line, string *elements );
 *    int http_read_rules ( char *rule_file );
 *
 * Revised:  6-APR-1994			 Added redirect and exec support.
 * Revised: 26-MAY-1994			Added protect rule. and AddType syn.
 * Revised: 27-MAY-1994			Make rule names case insensitive
 * Revised: 2-JUN-1994			Log stats of rule file.
 * Revised: 7-JUN-1994			Added validity checks to read_rule_file
 * Revised: 27-JUN-1994
 * Revised:  2-AUG-1994			Support decnet_searchlist module.
 * Revised: 24-AUG-1994			Define taskname for presentation rules.
 * Revised:  4-NOV-1994			Initialize index file search list.
 * Revised:  5-NOV-1994			Support welcome rule.
 * Revised: 16-NOV-1994			Support DirAccess rule.
 * Revised:  6-JAN-1994			Support scriptname for search.
 * Revised: 12-JAN-1995			Added George Carrette's mods for
 *					hostname, localhost (gjc@village.com).
 * Revised: 25-JAN-1995			Fixed bug in tracelevel rule.
 * Revised: 24-FEB-1995			Support port configuration rule.
 * Revised: 26-FEB-1995			Support usermap directive.
 * Revised: 24-MAR-1995			Added mapimage rule.
 * Revised: 21-APR-1995			Added counter rule.
 * Revised: 11-MAY-1995			Hack for OSF version.
 * Revised: 15-MAY-1995			Conditionally compile counters.
 *					(skip if SCRIPTSERVER defined).
 * Revised:  3-JUN-1995			Add threadpool and service rules
 * Revised: 12-JUN-1995			Support continuation lines.
 * Revised:  15-JUL-1995		Add multihomed hacks.
 * Revised:  24-NOV-1995		Add scan routine.
 * Revised:  19-FEB-1996		Add Timelimit rule.
 * Revised:   1-MAR-1996		whitespace must precede comment delim.
 * Revised:  18-JUN-1996		Support 'pre-processor' (.define,
 *					.expand, .ignore, .iterate, .next ).
 * Revised:  24-JUL-1996		Support MANAGE directive.
 * Revised:  11-AUG-1996		Support METHOD directive.
 * Revised:  16-AUG-1996		Remove default PUT method, give each
 *					include file its own namespace for
 *					pre-processor directives.
 * Revised:  24-aug-1996		Flag when localaddress argument begins
 *					with '@'.
 * Revised:   5-SEP-1996		Force internal DirAccess to ON when
 *					presentation for text/file-directory
 *					defined.
 * Revised:  26-SEP-1996		Fix conditional that inadvertantly
 *					disabled usermap rule.
 * Revised:   8-OCT-1996		Support cname-based localaddress.
 * Revised:  18-NOV-1996		Support log extension mask on accesslog.
 * Revised:   13-DEC-1996		Give warning when Diraccess rule
 *					overridden.
 * Revised:   13-JAN-1997		Support multiple rules/line (up to 8)
 *					using bare semi-colons as separator.
 * Revised:   12-MAR-1997		Make access log flags apply to
 *					succeeding access logs unless overridden.
 * Revised:   2-APR-1997		Support scavenge mode hack for keepalives.
 *					Track listen threads in global variable
 *					http_local_ip_addresses.
 * Revised:   8-APR-1997		Optimize multihome translations.
 * Revised:  12-APR-1997		Remove multihome tracking to new module.
 * Revised:  17-MAY-1997		Add Script time limits.
 * Revised: 16-SEP-1997			Add multiple time format for filecache
 *					refresh.
 * Revised: 23-DEC-1997			Support configurable cache chunk.
 * Revised: 16-FEB-1998			Added proxyscript rule.
 * Revised: 20-FEB-1998			Added null rule.
 * Revised: 25-FEB-1998			Switch to http_set_port_attributes func.
 * Revised 1-MAR-1998			Add proxylog rule (http_gateway_log).
 * Revsied: 27-MAR-1998			Add timelimit scriptreuse rule.
 * Revised: 21-JUN-1998			Fix bug in preprocess_full realloc.
 * Revised: 22-APR-1999			Tweak for Digital Unix.
 * Revsied: 29-APR-1999			Tweak access log trace message,
 *					add errorpage rule.
 * Revised: 27-JUL-1999			Add filecache exclude rule.
 * Revised: 19-FEB-2000			Add hostmatch rule.
 * Revised: 16-MAR-2000			Add eventcounter flags
 * Revised: 30-SEP-2000			Fix to fileexpire rule.
 * Revised: 31-OCT-2000			Fix access log redirects in localaddres
 *					blocks.
 * Revised:  6-NOV-2000			Fix access log extension support.
 * Revised: 26-FEB-2001			Fix rule_file keepalive parsing.
 * Revised: 11-JAN-2002			Remove initial calls to
 *					http_set_port_attributes.
 * Revised: 28-FEB-2002			Authenticator cache.
 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include "ctype_np.h"
#include "session.h"
#include "ident_map.h"
#include "decnet_searchlist.h"
#ifndef SCRIPTSERVER
#include "file_cache.h"
#include "file_access.h"
#include "tserver_tcp.h"
#include "counters.h"
#include "script_manage.h"
#include "multihome.h"
#include "errorpage.h"
#include "authcache.h"
#endif
/*
 * Work around problems with DECC and forbidden (non-ANSI) names.
 */
#ifdef VMS
#ifdef __DECC
#ifndef fgetname
#define fgetname decc$fgetname
char *fgetname ( FILE *f, char *fname );
#endif
#endif
#else
#ifdef VMS
char *fgetname();
#else
#define fgetname(a,b) tu_strcpy(b,"rule_file")
#endif
#endif

#ifdef VMS
#define VMS_FOPEN_OPT , "mbc=64", "dna=.conf"
#else
#define VMS_FOPEN_OPT
#ifndef vaxc$errno
#define vaxc$errno 20
#endif
#endif

/*
 * GLobal variables visible to others.
 */
char *http_search_script;		/* GLobal variable, search enable. */
char **http_index_filename;		/* Filename for index files */
int http_dir_access;
int http_ports[2];			/* TCP/IP port numbers */
int http_request_time_limit;		/* timeout for reading request */
int http_response_time_limit;		/* timeout for sending data */
int http_keepalive_limit;		/* Max keepalives allowed or 0 */
int http_keepalive_time;		/* time limit for keepalive */
int http_script_time_limit1;		/* Decnet script dialog timelimit */
int http_script_time_limit2;		/* Decnet script response timelimit */
char *http_dir_access_file;		/* Directory access check file */
char *http_authenticator_image;		/* Global variable, level 2 access */
char *http_default_host;		/* server name to advertise */
char *http_gateway_script;		/* Script name for proxy gateway */
int http_gateway_log;			/* access log number for proxies */
int http_dns_enable;			/* flag to control host name lookups */
int http_manage_port;			/* Manage port number */
char *http_manage_host;			/* Management host */
int http_log_level, tlog_putlog();	/* Global variable, logger detail level */
int *http_log_extension;		/* if set, use alternate log format */
int http_local_ip_addresses;		/* Number of IP local addresses */
int http_cache_params[4];
info_ptr http_rule_list;		/* Rule blocks */

int http_define_suffix(), http_define_presentation(), http_define_ident();
int http_load_dynamic_service();
int http_define_method();
int http_set_port_attributes();
static int ext_alloc;			/* size of http_log_extension array */
/*
 * Prototype for callback function used in command table.
 */
typedef int cmd_handler( 
	int tcnt, string *tok,  	/* tokenized command line */
	int arg1_index, 
	int cdata, char *err );
typedef cmd_handler *cmd_handlerp;
/*
 * Command table stores list of valid commands.
 */
struct cmd_def {
    char *name;				/* must be in alphabetical order */
    int min_arg, max_arg;
    char *arg1;				/* keyword list or "*" */
    cmd_handlerp handler;
    int cdata;				/* extra argument for handler */
};
static cmd_handler trans_rule, catchall, exec_rule, prot_rule, suffix_rule,
	task_rule, method_rule, include_rule, log_rule;
#ifndef SCRIPTSERVER
static cmd_handler service_rule, localaddress_rule, fileexpire_rule,
	errorpage_rule, prot_rule, method_rule, dynamap_rule, timelimit_rule,
	filecache_rule, authenticator_rule;
#define IFSRV(x) x
#else
#define IFSRV(x) (cmd_handlerp) 0
#endif
static struct cmd_def command_table[] = {
   { "SUFFIX", 2, 4, "*", suffix_rule, 1 },
   { "ADDTYPE", 2, 4, "*", suffix_rule, 0 },
   { "MAP", 2, 2, "*", trans_rule, IDENT_MAP_RULE_MAP },
   { "PASS", 1, 2, "*", trans_rule, IDENT_MAP_RULE_PASS },
   { "FAIL", 1, 1, "*", trans_rule, IDENT_MAP_RULE_FAIL },
   { "REDIRECT", 2, 2, "*", trans_rule, IDENT_MAP_RULE_REDIRECT },
   { "HTBIN", 1, 1, "*", exec_rule, 0 },
   { "EXEC", 2, 2, "*", exec_rule, 1 },
   { "MAPIMAGE", 0, 1, "*", IFSRV(exec_rule), 2 },
   { "DYNAMAP", 3, 3, "*", IFSRV(dynamap_rule), 0 },
   { "USERMAP", 3, 3, "*", IFSRV(dynamap_rule), 0 },
   { "USERDIR", 1, 1, "*", catchall, 4 },
   { "PROTECT", 2, 2, "*", prot_rule, IDENT_MAP_RULE_PROTECT },
   { "DEFPROT", 2, 2, "*", trans_rule, IDENT_MAP_RULE_DEFPROT },
   { "HOSTPROT", 2, 2, "*", trans_rule, IDENT_MAP_RULE_PROTECT },
   { "SEARCH", 1, 1, "*", task_rule, 2 },
   { "PRESENTATION", 2, 2, "*", suffix_rule, 2 },
   { "METHOD", 2, 2, "*", method_rule, 0 },
   { "AUTHENTICATOR", 1, 3, "*", IFSRV(authenticator_rule), 0 },
   { "INCLUDE", 1, 1, "*", include_rule, 0 },
   { "ACCESSLOG", 1, 2, "*", log_rule, 1 },
   { "PROXYLOG", 1, 1, "*", log_rule, 2 },
   { "TRACELEVEL", 1, 2, "#", log_rule, 0 },
   { "DNSLOOKUP", 1, 1, "OFF,ON", catchall, 3 },
   { "WELCOME", 1, 1, "*", catchall, 1 },
   { "DIRACCESS", 1, 2, "ON,SELECTIVE,OFF", catchall, 5 },
   { "PORT", 1, 2, "*", catchall, 2 },
   { "LOCALADDRESS", 0, 3, "*", IFSRV(localaddress_rule), 0 },
   { "HOSTMATCH", 1, 1, "*", IFSRV(catchall), 6 },
   { "HOSTNAME", 1, 1, "*", task_rule, 1 },
   { "EVENTCOUNTER", 1, 3, "CLIENTS,HOSTCLASS,FLAGS", IFSRV(catchall), 8 },
   { "THREADPOOL", 1, 5, "*", IFSRV(service_rule), 0 },
   { "SERVICE", 1, 5, "*", IFSRV(service_rule), 0 },
   { "TIMELIMIT", 2, 4, 
     "REQUEST,RESPONSE,SCRIPTDIALOG,SCRIPTOUTPUT,KEEPALIVE,KEEP-ALIVE,SCRIPTREUSE",
	IFSRV(timelimit_rule), 0 },
   { "FILECACHE", 2, 3, "REFRESH,SIZE,LIMIT,MAXREC,EXCLUDE", 
		 IFSRV(filecache_rule), 0 },
   { "FILEEXPIRE", 2, 2, "*", IFSRV(fileexpire_rule), 0 },
   { "MANAGE", 2, 2, "PORT,HOST,SCRIPT", IFSRV(catchall), 7 },
   { "PROXYSCRIPT", 1, 1, "*", task_rule, 3 },
   { "ERRORPAGE", 2, 3, "*", IFSRV(errorpage_rule), 0 },
   { "FILEFLAGS", 1, 1, "#", catchall, 9 },
   { "NULL", 0, 1000, "*", catchall, 0 },
   { "", -1, 0, "", (cmd_handlerp) 0, 0 }  /* end of list */
};
/*
 * Global variables for communication within module.
 */
static int index_file_alloc=0, index_file_count;
static int counter_flags = 0;
static char *default_index_file[] = { "index.html", "index.htmlx", (char *) 0 };
static int preprocess_rule ( int count, int limit, string *elem );
/***********************************************************************/
/* Parse line into whitespace-delimited tokens, trimming leading and trailing
 * whitespace.  Each token parse is explicitly null-terminataed.  Function 
 * value returned is number of elements found.  May be 1 more than limit if 
 * last element does not end line.
 */
int http_parse_elements (
	int limit,	    /* Max number of element to delimit */
	char *line,	    /* input line to parse */
	string *elem )	    /* Output array. */
{
    int tcnt, in_token, length, i;
    char *ptr;
    /* Delimit up to three tokens */
    for ( in_token = tcnt = length = 0, ptr = line; *ptr; ptr++ ) {
	if ( in_token ) {
	    if ( isspace ( *ptr ) ) {
		/* End current token */
		*ptr = '\0';  /* terminate string */
		elem[tcnt++].l = length;
		in_token = 0;
	    }
	    else length++;
	} else {
	    if ( !isspace ( *ptr ) ) {
		/* start next token */
		if ( tcnt >= limit ) {
		    /* more tokens than expected */
		    tcnt++;
		    break;
		}
		elem[tcnt].s = ptr;
		in_token = 1;
		length = 1;
	    }
        }
    }
    /*
     * Make final adjust to element count and make remaining elment null.
     */
    if ( in_token ) { elem[tcnt++].l = length; }
    for ( i = tcnt; i < limit; i++ ) { elem[i].l = 0; elem[i].s = ptr; }
    return tcnt;
}
/***********************************************************************/
/* Read next line from nested stack of input file.  Trim line of comments
 * and final linefeed and concatentate continuation lines if
 * last character is a backslash (\).
 */
static char *read_nested 
	( char *line, int linesize, FILE **fp, int *sp, int *lines )
{
    char *result;
    int filled, trim;
    for ( *lines = filled = 0; filled < (linesize-1); ) {
	result = fgets ( &line[filled], linesize-filled, fp[*sp] );
	if ( !result ) {
	    /*
	     * End-of-file.
	     */
	    if ( filled > 0 ) break;		/* return what we got. */
	    if ( *sp == 0 ) return result;	/* final EOF */
	    /*
	     * Resume with previous file, restore pre-processor name space.
	     */
	    *lines = 0;
	    fclose ( fp[*sp] );
	    *sp = *sp - 1;
	    preprocess_rule ( -1, 0, (string *) 0);
	} else {
	    *lines = *lines + 1;
	    /*
	     * Trim comments and check for continuation line.
	     */
	    for (trim=(-1); line[filled] && (line[filled]!='\n'); filled++) {
		if ( (line[filled] == '#') && (trim < 0) ) {
		    if ( filled <= 0 ) trim = filled;
		    else if ( isspace(line[filled-1]) ) trim = filled;
		}
	    }
	    if ( filled > 0 ) if ( line[filled-1] == '\\' )  {
		filled = ( trim < 0 ) ? (filled-1) : trim;
		for ( ; filled > 0; --filled ) 
			if ( !isspace(line[filled-1]) ) break;
		continue;
	    }
	    if ( trim >= 0 ) filled = trim;
	    break;
	}
    }
    line[filled] = '\0';
    return line;
}
/***********************************************************************/
/* Extract next set of strings (tokens) from uber_token delimited by strings
 * consisting of just semi-colon (;).  Return 0 when no more tokens.
 */
static int partition_tlist(string *uber_token, int t_count,
	int *t_pos, string *token, int limit, int *out_count )
{
    int i, j;
    char *nptr;
    j = *t_pos;
    if ( j >= t_count ) return 0;	/* remaining list empty */
    for ( i = 0; j < t_count; j++ ) {
	if ( uber_token[j].l == 1 && uber_token[j].s[0] == ';' ) {
	    j++;
	    if (j <  t_count) tu_strupcase (uber_token[j].s, uber_token[j].s);
	    break;
	}
	if ( i < limit ) token[i++] = uber_token[j];
    }
    *t_pos = j;
    *out_count = i;
    nptr = &token[0].s[token[0].l];
    for ( ; i < limit; i++ ) { token[i].l = 0; token[i].s = nptr; }
    return 1;
}
/***********************************************************************/
/* Validate command and process it.
 */
static int process_command ( struct cmd_def *cmd, string *token, int tcount,
    char errmsg[256] )
{
    int i, arg1_ndx, plen;
    /*
     * Upcase token and search table, resetting def pointer to matching entry.
     */
    for ( i = 0; tu_strncmp ( token[0].s, cmd[i].name, 80 ) != 0; i++ ) {
	if ( cmd[i].min_arg < 0 ) {
	    sprintf(errmsg, "unrecognized rule: '%s'", token[0].s );
	    return 0;
	}
    }
    cmd = &cmd[i];
    /*
     * recognize but don't process command if no handler routine defined.
     */
    if ( !cmd->handler ) return 3;
    /*
     * Validate second argument if present.
     */
    if ( tcount > 1 ) {
	char keyword[80], pattern[40], *plist;
	tu_strnzcpy ( keyword, token[1].s, sizeof(keyword)-1 );
	tu_strupcase ( keyword, keyword );
	i = 0;
	arg1_ndx = -1;
	for ( plist = cmd->arg1; *plist; plist += plen ) {
	    /*
	     * Parse out next comma-delimited pattern.
	     */
	    for ( plen = 0; plist[plen]; plen++ ) {
		if ( plist[plen] == ',' ) break;
		pattern[plen] = plist[plen];
	    }
	    pattern[plen] = '\0';
	    if ( plist[plen] == ',' ) plen++;
	    /*
	     * See if numeric type called for.
	     */
	    if ( pattern[0] == '#' ) {
		if ( (keyword[0] < '0') || (keyword[0] > '9') ) {
		    char *val;
		    val = getenv ( token[1].s );
		    if ( !val ) {
			sprintf ( "invalid env. variable: '%s'", token[1].s );
			return 0;
		    }
		    arg1_ndx = atoi(val);
		} else arg1_ndx = atoi ( keyword );
		break;
	    }
	    /*
             * See if it matches keyword and set arg1_ndx to position in list.
	     */
	    if ( tu_strmatchwild(keyword,pattern) == 0 ) {
		arg1_ndx = i;
		break;
	    }
	    i++;
	}
	if ( arg1_ndx < 0 ) {
	    sprintf(errmsg,"invalid keyword: '%s'\n", keyword );
	    return 0;
	}
    } else {
	/*
	 * Command doesn't take arguments, set index to zero.
	 */
	arg1_ndx = 0;
    }
    /*
     * check argument count.
     */
    if ( cmd->min_arg > (tcount-1) ) {
	sprintf(errmsg,"missing command argument(s)" );
	return 0;
    } else if ( cmd->max_arg < (tcount-1) ) {
	sprintf(errmsg,"too many arguments (%d > %d)", (tcount-1),
		cmd->max_arg );
	return 0;
    }
    /* call handler. */
    return cmd->handler ( tcount, token, arg1_ndx, cmd->cdata,  errmsg );
}
/***********************************************************************/
/* Convert argument of timelimit command into seconds.  Argument may be:
 *    1. 'nn', decimal number of seconds
 *    2. '[hh:]mm:ss', hours, minutes, seconds
 *    3. 'symbol', getenv symbols, value is retrieve and must be form 1 or 2.
 */
static int parse_timelimit ( char *arg, char *temp ) 
{
    int secs, i, j;
    if ( (*arg < '0') || (*arg  > '9') ) {
	/* Assume argument isenvironment variable. */
	char *val = getenv(arg);
	if ( !val ) tlog_putlog(0,
		"Invalid variable name in TimeLimit rule: '!AZ'!/", arg );
	arg = val ? arg : "0";
    }
    /*
     * Scan for colons, using to delimit a radix 60 number.
     */
    secs = 0;
    for ( j = i = 0; i < 11 && arg[i]; i++ ) {
	temp[j] = arg[i];
	if ( arg[i] == ':' ) {
	    temp[j+1] = '\0';
	    secs = (secs*60) + atoi(temp);
	    j = 0;
        } else if ( (arg[i] < '0') || (arg[i]  > '9') ) {
	    tlog_putlog ( 0, 
	      	"Invalid time value in TimeLimit rule: '!AZ'!/", arg );
	    return 0;
	} else j++;
    }
    temp[j] = '\0';
    secs = (secs*60) + atoi(temp);
    return secs;
}
/***********************************************************************/
/*  Append filename to list of index files (welcome pages) to search for
 *  when doing directory listings.
 */
static void add_index_file ( char *fname )
{
    /*  Allocate enough for all names plus a null */
    if ( index_file_count+2 >= index_file_alloc ) {
	index_file_alloc += 20;
	if ( index_file_alloc == 20 ) 		/* first call */
	    http_index_filename = (char **) malloc ( sizeof(char *)*index_file_alloc );
	else
	    http_index_filename = (char **) realloc ( http_index_filename, 
		sizeof(char *)*index_file_alloc );

	if ( !http_index_filename ) {
	   tlog_putlog ( 0, "error allocating index file list!/");
	   index_file_alloc = 0;
	   http_index_filename = default_index_file;
	   return;
	}
    }
    /*
     * Copy string and append to list.  Mark end of list with null.
     */
    http_index_filename[index_file_count] = malloc (tu_strlen(fname)+1);
    tu_strcpy ( http_index_filename[index_file_count], fname );
    index_file_count++;
    http_index_filename[index_file_count] = (char *) 0;
}
/***********************************************************************/
/* the following group of functions are called by the process_comand routine
 * via the handler argument of the command definition.  Function returns value
 * with low bit clear to indicate error, filling in the errmsg with diagnostic
 * text.
 */
static int acc_local_log;		/* true if inside local address blk */
static int acc_log_count;		/* Number of access log files */
static int suffix_count;
static FILE *new_rf;			/* file to push onto stack */

static int trans_rule ( int tcount, string *token, int arg1_ndx,
    int code, char errmsg[256] )
{
    http_define_ident ( token[1].s, code, token[2].s );
    return 1;
}

static int include_rule ( int tcount, string *token, int arg1_ndx,
    int code, char errmsg[256] )
{
    new_rf = fopen ( token[1].s, "r" VMS_FOPEN_OPT );
    if ( !new_rf ) {
	sprintf ( errmsg, "Error opening file" );
	return 0;
    }
    return 1;
}

static int prot_rule ( int tcount, string *token, int arg1_ndx,
    int code, char errmsg[256] )
{
    /*
     * Level 2 protection.  Prepend '+' to setup filename.
     */
    char l2_setup[512];
    l2_setup[0] = '+';
    tu_strnzcpy ( &l2_setup[1], token[2].s, 500 );
    http_define_ident ( token[1].s, IDENT_MAP_RULE_PROTECT, l2_setup ); 
    return 1;
}

static int exec_rule ( int tc, string *token, int arg1, int code, char
    errmsg[256] )
{
    if ( code == 0 ) {		/* HTBIN method */
	http_define_ident ( "/htbin/*", IDENT_MAP_RULE_EXEC, token[1].s );
	if ( token[1].s[0] != '%' ) dnetx_define_task ( token[1].s );
    } else if ( code == 1 ) {	/* exec rule */
	http_define_ident ( token[1].s, IDENT_MAP_RULE_EXEC, token[2].s );
	if ( token[2].s[0] != '%' ) dnetx_define_task ( token[2].s );
    } else if ( code == 2 ) {	/* mapimage hack */
	char tline[512];
	string srv_token[8];
	int count;
	/*
	 * Define the builtin service for mapimage.
	 */
#ifndef SCRIPTSERVER
	tu_strcpy ( tline, "SERVICE mapimage builtin=mapimage" );
	count = http_parse_elements ( 7, tline, srv_token );
	if ( 0 == http_load_dynamic_service ( srv_token, count, tline ) ) {
	    sprintf ( errmsg, "error loading mapimage service" );
	    return 0;
	}
#endif
	/*
	 * Add map rule for %mapimage:arg
	 */
	tu_strcpy ( tline, "%mapimage:" );
	tu_strnzcpy ( &tline[10], token[1].s, sizeof(tline)-11 );
	http_define_ident ( "/$mapimage/*", IDENT_MAP_RULE_EXEC, tline );
    }
    return 1;
}

static int method_rule ( int tcount, string *token, int arg1_ndx, int code,
	char errmsg[256] )
{
    int status;

    if ( (tu_strncmp ( token[1].s, "GET", 4 ) == 0) ||
	    (tu_strncmp ( token[1].s, "HEAD", 5 ) == 0) ) {
	sprintf("%s method cannot be re-defined", token[1].s );
	status = 0;
    } else {
	status = http_define_method ( token[1].s, token[2].s );
	if ( status == 3 ) tlog_putlog ( 0, 
	    "Previous definition for !AZ method replaced!/", token[1].s);

	 if ( (status&1) && (token[2].s[0] != '%') )
		dnetx_define_task ( token[2].s );
    }
    return status;
}

static int log_rule ( int tcount, string *token, int arg1_ndx, int code,
	char errmsg[256] )
{
    int status, ndx, tlog_initlog();
    char temp[512];
    static char *log_type[] = { "trace", "proxy", "access", "unknown" };
    /*
     * Handle rules dealing with log files.
     */
    status = 1;
    switch ( code ) {
	case 0:			/* tracelevel rule */
	    http_log_level = arg1_ndx;
	    ndx = http_log_level;
	    tu_strnzcpy ( temp, token[2].s, (tcount>2) ? token[2].l : 0 );
	    break;

	case 1:			/* accesslog */
	    tu_strnzcpy ( temp, token[1].s, token[1].l );
	    if ( acc_local_log ) {
		/* We are inside localadress block.
		 */
		acc_log_count++;
		ndx = (0 - acc_log_count);
#ifndef SCRIPTSERVER
		http_set_multihome_accesslog ( ndx );
#endif
	    } else ndx = -1;
	    break;
	case 2:			/* proxy proxylog */
	    tu_strnzcpy ( temp, token[1].s, token[1].l );
	    acc_log_count++;
	    ndx = (0 - acc_log_count);
#ifndef SCRIPTSERVER
	    http_set_multihome_accesslog ( ndx );
#endif
	    break;
	default:
	    code = 3;
	    status = 36;
	    sprintf(errmsg,"internal error");
	    break;
    }
    if (  (status&1) == 1 ) {
	status = tlog_initlog ( ndx, temp );
	if ( (status&1) == 0 ) {
	    sprintf(errmsg, "error opening %s log file", log_type[code] );
	} else if ( (ndx>=0) && (status==3) ) {
	    sprintf(errmsg, "trace log already active" );
	    status = 2;
	} else if ( (ndx < 0) && (tcount > 2) ) {
	    /*
	     * Extra parameter specifed extended option mask for file.h
	     */
	    int flags, i;
	    if ( acc_log_count >= ext_alloc ) {
		/*
		 * create/extend log_extension array.
		 */
	        ext_alloc += 10;
	        if ( http_log_extension ) {
		    http_log_extension = 
			(int *) realloc (
			http_log_extension, ext_alloc*sizeof(int) );
	  	} else {
		    http_log_extension = 
			(int *) malloc ( ext_alloc*sizeof(int) );
			    http_log_extension[0] = 0;
		}
	        for (i=1; i <= 10; i++) http_log_extension[ext_alloc-i] = 0;
	    }
	    /*
	     * Decode argument and save in array.
	     */
	    tu_strnzcpy ( temp, token[2].s, sizeof(temp)-1 );
	    flags = atoi ( temp );
	    for (i = -1-ndx; i < ext_alloc; i++) http_log_extension[i] = flags;
	    tlog_putlog ( 0, "!AZ extension mask[!SL]: !XL!/",
			log_type[code], ndx, http_log_extension[-1-ndx] );
	} else if ( (ndx < 0 ) && http_log_extension ) {
	    tlog_putlog ( 0, "!AZ extension mask[!SL]: !XL!/",
			log_type[code], ndx, http_log_extension[-1-ndx] );
	}
    }
    return status;
}

static int suffix_rule ( int tcount, string *token, int arg1, int code,
	char errmsg[256] )
{
   if ( code == 2 ) {
	http_define_presentation ( token[1].s, token[2].s );
	if ( token[2].s[0] != '%' ) dnetx_define_task ( token[2].s );
   } else {
       http_define_suffix ( token[1].s, token[2].s, token[3].s, token[4].s );
	suffix_count++;
    }
    return 1;
}

static int task_rule ( int tcount, string *token, int arg1, int code,
	char errmsg[256] )
{
    /*
     * Copy argument 1 to global string, setup DECnet if script.
     */
    static struct {
	char **ptr; int task;
    } var[4] = { { &http_authenticator_image, 0 }, { &http_default_host, 0 },
	{ &http_search_script, 1 }, {&http_gateway_script, 1} };
    int status, i;

    *var[code].ptr = malloc ( token[1].l + 1 );
    tu_strnzcpy ( *var[code].ptr, token[1].s, token[1].l );
    if ( var[code].task ) {
	if (token[1].s[0] != '%') dnetx_define_task ( *var[code].ptr );
    }

    return 1;
}

#ifndef SCRIPTSERVER
static int authenticator_rule ( int tcount, string *token, int arg1, int code,
	char errmsg[256] )
{
    /*
     * First argument is authenticator image name, followed by optional
     * cache size (default 50) and timeout (default 300 seconds).
     */
    int status, i, size, timeout;

    http_authenticator_image = malloc ( token[1].l + 1 );
    tu_strnzcpy ( http_authenticator_image, token[1].s, token[1].l );

    size = 50;
    timeout = 300;		/* set default values */

    if ( tcount > 2 ) {
	size = atoi ( token[2].s );
    }
    if ( tcount > 3 ) {
	char temp[256];		/* work space for parse_timelimit */

	timeout = parse_timelimit ( token[3].s, temp );
    }
    authcache_initialize ( size, timeout );

    return 1;
}
#endif

static int catchall ( int tcount, string *token, int arg1, int code,
	char errmsg[256] )
{
    char temp[512];
    int status, i, flags, enabled;
    /*
     * catchall for miscellaneous rules.
     */
    status = 1;
    switch ( code ) {
	case 0:		/* The null rule */
	    break;

	case 1:		/* welcome rule */
	    if ( (index_file_alloc == 0) || (token[1].l > 0) ) {
		add_index_file ( token[1].s );
		if ( token[1].l == 0 ) {
		    free ( http_index_filename[0] );
		    http_index_filename[0] = (char *) 0;
		    tlog_putlog (1,"Disabled search for WELCOME files.!/" );
		}
	    }
	    break;

	case 2:		/* PORT rule */
	    for ( i = 1; (i < tcount) && (i < 3); i++ ) {
		tu_strnzcpy ( temp, token[i].s, token[i].l >= sizeof(temp) ? 
			sizeof(temp)-1 : token[i].l );
		http_set_port_attributes( i-1, temp );
	    }
	    break; 

	case 3:		/* DNSlookup, arg index is flag value (off, on) */
	    http_dns_enable = arg1;
	    break;

	case 4:		/*  set user directory path */
	    http_define_ident ( "/~*", IDENT_MAP_RULE_USERMAP, token[1].s );
	    break;

	case 5:		/* set directory browse inhibit */
	    http_dir_access = arg1;	/* on, selecive, off --> 0, 1, 2 */
	    if ( (arg1 == 1) && (token[2].l > 0) ) {
		/* override filename that flags browsable directories */
		http_dir_access_file = malloc ( token[2].l+1 );
		tu_strnzcpy ( http_dir_access_file,token[2].s,token[2].l);
	    }
	    break;

#ifndef SCRIPTSERVER
	case 6:		/* set alternate names for localhost block */
	    http_set_multihome_alias ( token[1].s, token[1].l );
	    break;

	case 7:		/* manage rule */
	    tu_strupcase ( temp, token[1].s );
	    status = http_configure_manage_port ( temp,
		token[2].s, token[3].s, token[4].s );
	    break;

	case 8:	/* define host counter */
	    if ( arg1 == 0 ) {			/* CLIENTS */
		http_enable_active_counters();
		if ( http_counters ) http_counters->flags = counter_flags;
	    } else if ( arg1 == 2 ) {		/* FLAGS */
		http_zero_counters ( 0 );		/* inits counters */
		if ( tcount >= 3 ) {
		    counter_flags = atoi ( token[2].s );
		    if ( http_counters ) http_counters->flags = counter_flags;
		} else {
		    tu_strcpy ( errmsg, "missing flags value" );
		    status = 20;
		}
	    } else if ( tcount >= 4 ) {		/* HOSTCLASS */
	        http_define_host_counter ( token[2].s, token[3].s );
	    } else {
		tu_strcpy ( errmsg, "missing hostclass arguments" );
		status = 20;
	    }
	    break;

	case 9:			/* fileflags */
	    tfc_set_tf_options ( arg1, &enabled );
	    status = 1;
	    if ( enabled != arg1 ) tlog_putlog ( 0,
		"Not all file options set: !SL of !SL", enabled, arg1 );
	    break;
#endif

	default:
	    tu_strcpy ( errmsg, "inconsistent command table" );
	    status = 0;
    }
    return status;
}
/*
 * The follow rule routines only apply to the web server, not scriptservers.
 */
#ifndef SCRIPTSERVER
static int localaddress_rule ( int tcount, string *token, int arg1_ndx, 
	int code, char errmsg[256] )
{
    int status;
    char temp[512];
    /*
     *  handle localaddress rule, number of arguments dicates action to take.
     */
    status = 1;
    if ( tcount > 1 ) {
	/*
	 * We are either CNAME hostname or nnn.nnn.nnn.nnn hostname
	 */
	tu_strnzcpy ( temp, token[1].s, sizeof(temp)-1 );
	if ( temp[0] != '@' ) tu_strupcase ( temp, temp );
	if ( (tcount > 2) && (0==tu_strncmp ( temp,"CNAME", 6 )) ) {
	    /*
	     * Synthesize fake address on lopback interface.
	     */
	    http_cname_count++;
	    sprintf(temp,"127.0.%d.%d", (http_cname_count+1)/256,
		255&(http_cname_count+1) );
	    tlog_putlog(1,"Creating CNAME-based virt. host (addr=!AZ)!/",
		temp );
	} else {
	    /*
	     * Call TCP layer to set listen address.
	     */
	    http_local_ip_addresses++;
	    status = ts_set_local_addr(temp);
	    if ( status==1 ) tlog_putlog ( 1, (temp[0] == '@') ?
		"TCP driver option '!AZ'!/" :
		"Set local ip address to '!AZ'!/", temp );
	    else sprintf(errmsg,"Error setting local ip address '%s'", temp );
	}
	/*
	 * If hostname present, go into multihomed mode.
	 */
	if ( tcount > 2 ) {
	    http_multihomed = 1;
	    http_define_multihome ( token[2].s, token[2].l, temp );
	    acc_local_log = 1;
	}
    } else if ( http_multihomed ) {
	/*  End of localaddress block */
	http_define_ident ( "*", IDENT_MAP_RULE_LOCALADDRESS, "" );
	acc_local_log = 0;
    }
    return status;
}

static int dynamap_rule ( int tcount, string *token, int arg1_ndx,
    int code, char errmsg[256] )
{
    /* 
     * Dynamically defined usermap, build value string as
     * '(token[3])token[2]' to define ident.
     */
    int length;
    char temp[512];

    temp[0] = '(';
    if ( token[3].s[0] == '(' ) {
	tu_strnzcpy ( &temp[1], &token[3].s[1], sizeof(temp)-1 );
    } else {
	tu_strnzcpy ( &temp[1], token[3].s, sizeof(temp)-1 );
    }
    length = tu_strlen ( temp );
    if ( temp[length-1] != ')' ) temp[length++] = ')';

    tu_strnzcpy ( &temp[length], token[2].s, sizeof(temp)-length-1 );
    http_define_ident ( token[1].s, IDENT_MAP_RULE_USERMAP, temp ); 

    return 1;
}

static int service_rule ( int tcount, string *token, int arg1_ndx, int code,
	char errmsg[256] )
{
    int status;
    status = http_load_dynamic_service ( token, tcount, errmsg );
    return status;
}

static int timelimit_rule ( int tcount, string *token, int arg1, int code,
	char errmsg[256] )
{
    int status, limit;
    char temp[256];
    static int *limptr[] = { 
	&http_request_time_limit, &http_response_time_limit, 
	&http_script_time_limit1, &http_script_time_limit2,
	&http_keepalive_time, &http_keepalive_time };

    limit = parse_timelimit ( token[2].s, temp );
    if ( arg1 < 6 ) *limptr[arg1] = limit;

    if ( (arg1 == 4) || (arg1 == 5) ) {		/* extra args for keepalive*/
	if ( tcount > 3 ) {
	    limit = atoi(token[3].s);
	    if ( limit > 1 ) http_keepalive_limit = limit;
	}
	if ( (tcount > 4) && (token[4].l < sizeof(temp)) ) {
	    tu_strupcase ( temp, token[4].s );
	    if ( tu_strncmp ( temp, "SCAVENGE", 9 ) == 0 ) {
		tlog_putlog(1,"Using 'scavenge' mode for keep-alives!/");
		http_keepalive_time = (-http_keepalive_time);
	    }
	}
    } if ( arg1 == 6 ) {		/* scriptreuse */
	int climit;
	if ( tcount > 3 ) {
	    climit = atoi ( token[3].s );
	} else climit = 0;
	dnetx_set_saved_limits ( 0, climit, limit );
    }
    return 1;
}

static int filecache_rule ( int tcount, string *token, int arg1, int code,
	char errmsg[256] )
{
    int status, limit;
    char temp[256];

    switch ( arg1 ) {
	case 0:		/* refresh */
	    limit = parse_timelimit ( token[2].s, temp );
	    if ( tcount < 4 ) {
		http_cache_params[0] = limit;
	    } else {
		/* Make translation rule for pattern in token[3] */
		tu_strint ( limit, temp );
		http_define_ident ( token[3].s, IDENT_MAP_RULE_REFRESH, temp );
	    }
	    break;
	case 1:		/* size */
	case 2:		/* limit */
	case 3:		/* maxrec */
	    http_cache_params[arg1] = atoi ( token[2].s );
	    break;

	case 4:		/* exclude */
	    http_define_ident ( token[2].s, IDENT_MAP_RULE_REFRESH, "" );
	    break;

    }
    return 1;
}

static int fileexpire_rule ( int tcount, string *token, int arg1, int code,
	char errmsg[256] )
{
    int status, limit, base;
    char temp[256], *offset;
    tu_strnzcpy ( temp, token[2].s, sizeof(temp)-1 );
    tu_strupcase ( temp, temp );
    offset = token[2].s;
    if ( tu_strncmp ( temp, "CDT+", 4 ) == 0 ) {
	base = 1;
	offset = &offset[4];
    } else if ( tu_strncmp ( temp, "RDT+", 4 ) == 0 ) {
	base = 2;
	offset = &offset[4];
    } else if ( tu_strncmp ( temp, "EDT", 3 ) == 0 ) {
	base = 3;
	offset = "0";			/* ignore offset */
    } else {
	/* Default to RDT+ behaviour */
	base = 2;
    }
    limit = parse_timelimit ( offset, temp );
    sprintf ( temp, "X%d=%d", base, limit );
    /* printf("expire rule off: '%s', b: %d, l: %d\n", offset, base, limit );*/
    http_define_ident ( token[1].s, IDENT_MAP_RULE_REFRESH, temp );
    return 1;
}

static int errorpage_rule ( int tcount, string *token, int arg1_ndx, int code,
	char errmsg[256] )
{
    int status;
    status = http_define_error_page ( token[1].s, token[2].s,
	tcount > 3 ? token[3].s : "" );
    if ( status == 0 ) sprintf ( errmsg,
		"bad ErrorPage option: '%s'", token[1].s );
    return 1;
}
#endif
/***********************************************************************/
/*
 * Initialize rules and load rules database from file.
 */
int http_read_rules ( char *rule_file )
{
    FILE *rf[20];
    string token[8], uber_token[64];
    char line[512], tline[512], temp[300], fname[256];
    char *dir_script;
    int length, count, colon, id_count, lnum[20], pass_count, sp;
    int lcnt, t_count, http_get_presentation();
    int ext_alloc;
    int t_pos, i;
    info_ptr rule;
    /*
     * Initialize global variables.
     */
    http_search_script = NULL;
    http_authenticator_image = NULL;
    http_dns_enable = 0;
    http_index_filename = default_index_file;
    http_dir_access = 0;		/* Unrestricted access */
    http_dir_access_file = ".www_browsable";
    http_request_time_limit = 0;
    http_response_time_limit = 0;
    http_script_time_limit1 = 0;
    http_script_time_limit2 = 0;
    http_keepalive_limit = 0;
    http_keepalive_time = 10;
    http_log_extension = (int *) 0;
    ext_alloc = 0;
    http_local_ip_addresses = 0;
    ext_alloc = 0;
    acc_log_count = 1;			/* base for log index. */
    acc_local_log = 0;			/* true if inside localaddress block*/
    new_rf = (FILE *) 0;
    http_manage_port = 0;		/* No management host */
    http_manage_host = (char *) 0;	/* Default management host  */
    http_gateway_script = (char *) 0;	/* No proxy gateway */
    http_gateway_log = -1;		/* use accesslog by default */
    http_cache_params[0] = 0;		/* default to immediate refresh */
    http_cache_params[1] = 1000000;	/* 1 meg, ~50 files */
    http_cache_params[2] = 30000;	/* 30K limit on cached files */
    http_cache_params[3] = 0;		/* default maxrec */
#ifndef SCRIPTSERVER
    http_cname_count = 0;
    http_init_multihome();
#endif
    /*
     * See if rule file specified and attempt to open it.
     */
    if ( !(*rule_file) ) {
	tlog_putlog ( 0, "No rule file specified, abort.!/" );
	return 20;
    }
    rf[0] = fopen ( rule_file, "r" VMS_FOPEN_OPT );
    if ( !rf[0] ) {
	tlog_putlog ( 0, "Error opening rule file '!AZ'!/", rule_file );
	return  (vaxc$errno);
    }
    if ( http_log_level > 1 ) tlog_putlog ( 0,
		"Loading configuration/rules file !AZ!/", rule_file );
    /*
     * Process the rule file.
     */
    id_count = pass_count = 0;
    suffix_count = 0;
    sp = lnum[0] = 0;
    while (read_nested ( line, sizeof(line)-1, rf, &sp, &lcnt )) {
	/*
	 * Copy line to work area and parse into tokens, getting line length
	 * as side-effect.   We use work area so we can upcase portions for 
	 * caseless-compares yet still show original line if needed.
	 */
	lnum[sp]+=lcnt;			/* track position in file */
	for (length = 0; line[length]; length++) tline[length] = line[length];
	tline[length] = '\0';
	if ( length > 0 ) t_count = http_parse_elements(63, tline, uber_token);
	else t_count = 0;
	/*
	 * Check if first token is pre-processor rule and expand.
	 */
	if ( t_count > 0 ) {
	    tu_strupcase ( uber_token[0].s, uber_token[0].s );
	    if ( http_log_level > 14 ) tlog_putlog ( 15,
		"Processing rule '!AZ', tokens: !SL!/", uber_token[0].s,
	 	t_count );
	    while ( (t_count > 0) && (uber_token[0].s[0] == '.') ) {
		/* Rescan line */
		t_count = preprocess_rule ( t_count, 63, uber_token );
		if ( t_count < 0 ) tlog_putlog ( 0,
			"Error in line !SL of rule file !AZ: !AZ!/",
			lnum[sp], sp ? fgetname(rf[sp],fname) : rule_file,
			line );
	    }
	}
	/*
	 * Break token list into separate sequences of tokens that
	 * represent individual rules and parse.
	 */
	for ( t_pos=0; 
		partition_tlist(uber_token,t_count,&t_pos, token,7,&count); ) {
	    char errmsg[256]; int status;
	    if ( count <= 0 ) continue;		/* ignore blank lines */

	    status = process_command ( command_table, token, count, errmsg );
	    if ( new_rf ) {
		    /* New file, push onto stack */
		    if ( sp > 18 ) {
			fclose ( new_rf );
			tu_strcpy ( errmsg,
			    "Nesting level to deep to include file" );
			status = 0;
		    } else {
			rf[++sp] = new_rf;
			lnum[sp] = 0;
			preprocess_rule ( count, 7, token );
		    }
		    new_rf = (FILE *) 0;
	    }
	    if ( (status&1) == 0 ) {
		tlog_putlog(0,"Error in line !SL of rule file !AZ: !AZ!/",
			lnum[sp], sp ? fgetname(rf[sp],fname) : rule_file,
			errmsg );
		tlog_putlog(0,"(!AZ)!/", line);
	    }
	}
    }
    /*
     * Rundown remaining files and verify rule list has at least 1 pass rule.
     */
    while ( sp >= 0 ) fclose ( rf[sp--] );
    for ( rule = http_rule_list; rule; rule = rule->next ) {
	switch ( rule->code ) {
	    case IDENT_MAP_RULE_USERMAP:
	    case IDENT_MAP_RULE_PASS:
	    case IDENT_MAP_RULE_EXEC:
		pass_count++;
	    default:
		id_count++;
		break;
	}
    }
    if ( (http_log_level > 1) || (pass_count==0) ) tlog_putlog ( 0,
		"Rule file has !SL suffix def!%S., !SL translation rule!%S!/",
		suffix_count, id_count );
    if ( pass_count == 0 ) {
	    tlog_putlog ( 0, "Rule file contains no 'pass' actions, !AZ!/",
		"all requests would be _ruled_ out." );
	    return 2160;
    }
#ifndef SCRIPTSERVER
    /*
     * Enable interval counters if selected by flags.
     */
    if ( http_counters ) {
	if ( http_counters->flags & COUNTER_FLAG_INTERVAL ) {
	    http_icb_init ( 60, http_ports[0] );
	    tlog_putlog ( 1, "Initialized interval counters\n" );
	}
    }
#endif
    /*
     * Force http_dir_access to 'on' if a presentation rule for
     * text/file-directory defined to ensure presentation script will get
     * opportunity to act on directories without trailing slash.
     */
    if ( http_get_presentation ( "text/file-directory", &dir_script ) ) {
	tlog_putlog (3, "Using external directory handler: !AZ!/", dir_script);
	if ( http_dir_access != 0 ) {
	    tlog_putlog(0, "Warning, DirAccess rule ignored !AZ!/",
		"due to presence of alternate directory handler" );
	}
	http_dir_access = 0;
    }
    /*
     * Provide default rules if not found in rule file.
     */
    if ( suffix_count == 0 ) {
	tlog_putlog ( 0, "Adding default suffix rules!/" );
	http_define_suffix ( ".gif", "image/gif", "BINARY", "1.0" );
	http_define_suffix ( ".txt", "text/plain", "8BIT", "0.5" );
	http_define_suffix ( ".com", "text/plain", "8BIT", "0.5" );
	http_define_suffix ( ".html", "text/html", "8BIT", "1.0" );
    }
#ifndef SCRIPTSERVER
    /*
     * Now that all rules loaded, translate any error page defs.
     */
    http_translate_error_pages ();
#endif
    return 1;
}
/*****************************************************************************/
/* Handle preprocessor operations, rewriting element list.
 * Directives:
 *    .DEFINE symbol elem1 [elem2 [elem3...]]
 *    .EXPAND [string|$symbol] ...
 *    .IGNORE directive [directive [directive...]]
 *    .ITERATE [string|$dummyarg] ...
 *     .NEXT arg ...
 *	INCLUDE		! push operations, note no leading period
 *
 * A negative count forces a pop operations.
 *
 * Function value returned is new size of resulting list or -1 for error.
 */
static int preprocess_rule ( int count, int limit, string *elem )
{
    struct keydef {
	struct keydef *next;
	int count;
        string elem[1];
    } *sym;
    struct keysave {
	struct keysave *next;
	struct keydef *symlist, *ignore_list;
    } *env;
    static struct keysave *keystack = (struct keysave *) 0;
    static struct keydef *symlist = (struct keydef *) 0;
    static struct keydef *ignore_list = (struct keydef *) 0;
    static work_limit = 0, pattern_count = 0;
    static string *work_elem, *pattern;
    int i, j, k;

    if ( limit > work_limit ) {
	if ( http_log_level > 14 ) tlog_putlog(14,
		"Expanding pre-processor work list to !SL!/", limit );
	if ( work_limit == 0 ) work_elem = malloc(sizeof(string)*limit);
	else work_elem = realloc(work_elem,sizeof(string)*limit);
	if ( work_limit == 0 ) pattern = malloc(sizeof(string)*limit);
	else pattern = realloc(pattern,sizeof(string)*limit);
	work_limit = limit;
    }
    /*
     * Check for stack operations. 
     */
    if ( count < 0 ) {
	/*
	 * Restore next symlist and ignore_list saved on keystack.
	 */
	if ( !keystack ) return -1;
	symlist = keystack->symlist;
	ignore_list = keystack->ignore_list;
	env = keystack;
	keystack = keystack->next;
	free ( env );
	return 0;
    } else if ( tu_strncmp ( elem[0].s, "INCLUDE", 8) == 0 ) {
	/*
	 * Save current symlist and ignore lists and reset to empty.
	 */
	env = (struct keysave *) malloc ( sizeof(struct keysave) );
	if ( !env ) return -1;
	env->next = keystack;
	keystack = env;
	env->symlist = symlist;
	env->ignore_list = ignore_list;
	
	symlist = ignore_list = (struct keydef *) 0;
	return 0;
    }
    /*
     * Process standard directives.
     */
    if ( (count > 1 ) && (tu_strncmp(elem[0].s,".DEFINE", 8) == 0) ) {
	/*
	 * make duplicate of elem list and add to symbol table.
	 */
	tu_strupcase ( elem[1].s, elem[1].s );
	sym = (struct keydef *) malloc ( sizeof(struct keydef) +
		sizeof(string)*count-2 );
	sym->next = symlist;
	symlist = sym;
	sym->count = count-1;

	for ( i = 0; i < count-1; i++ ) {
	    sym->elem[i].l = elem[i+1].l;
	    sym->elem[i].s = (char *) malloc ( elem[i+1].l + 1 );
	    tu_strcpy ( sym->elem[i].s, elem[i+1].s );
	}
	return 0;
    } else if ( tu_strncmp(elem[0].s,".EXPAND",8) == 0 ) {
	/*
	 * Copy elements to temporary work array.
	 *   i - input index, j - output index.
	 */
	for ( i =1, j = 0; i < count; i++ ) {
	    if ( elem[i].s[0] == '$' ) {
		/*
		 * String following $ is symbol name to match.
		 */
		tu_strupcase(elem[i].s, elem[i].s);
		for ( sym = symlist; sym; sym = sym->next ) {
		    if ( tu_strncmp(sym->elem[0].s, &elem[i].s[1],
				sym->elem[0].l) == 0 ) {
			if ( '\0' == elem[i].s[1+sym->elem[0].l] ) break;
		    }
		}
		if ( !sym ) {
		    tlog_putlog(0, "Undefined symbol in expansion: !AZ!/",
			elem[i].s );
		    return -1;		/* error */
		}
		/*
		 * append to output, null definition terminates expansion
		 */
		if ( sym->count < 1 ) break;
		if ( sym->elem[1].l <= 0 ) break;
		for ( k = 1; k < sym->count; k++ ) {
		    work_elem[j++] = sym->elem[k];
		}
	    } else {
		/* Copy element with no expansion */
		work_elem[j++] = elem[i];
	    }
	}
	/*
	 * Copy result back, re-upcase first token.
	 */
	for ( i = 0; i < j; i++ ) elem[i] = work_elem[i];
	if ( j > 0 ) tu_strupcase ( elem[0].s, elem[0].s );
	for ( i = j; i < limit; i++ ) elem[j].l = 0;

	if ( http_log_level > 14 ) {
	   tlog_putlog(14,"Pre-processor expansion:" );
	    for ( i = 0; i < j; i++ ) tlog_putlog(14," '!AZ'", elem[i].s );
	    tlog_putlog(14,"!/");
	}
	return j;
    } else if ( tu_strncmp(elem[0].s,".NEXT",8) == 0 ) {
	/*
	 * Build work array drawing each element from pattern.
	 */
	for ( j = 0, i = 1; j < pattern_count; j++ ) {
	    if ( pattern[j].s[0] == '$' ) {
		/* Truncate pattern if null element */
		if ( i >= count ) break;
		work_elem[j] = elem[i++];
	    } else {
		work_elem[j] = pattern[j];
	    }
	}
	/*
	 * Copy result back, re-upcase first token.
	 */
	for ( i = 0; i < j; i++ ) elem[i] = work_elem[i];
	if ( j > 0 ) tu_strupcase ( elem[0].s, elem[0].s );
	for ( i = j; i < limit; i++ ) elem[j].l = 0;

	if ( http_log_level > 14 ) {
	   tlog_putlog(14,"Pre-processor iteration:" );
	    for ( i = 0; i < j; i++ ) tlog_putlog(14," '!AZ'", elem[i].s );
	    tlog_putlog(14,"!/");
	}
	return j;
    } else if ( tu_strncmp(elem[0].s,".ITERATE",8) == 0 ) {
	/*
	 * Save elements in pattern buffer.
	 */
	pattern_count = count - 1;
	for ( i = 0; i < pattern_count; i++ ) {
	    pattern[i].l = elem[i+1].l;
	    pattern[i].s = (char *) malloc ( elem[i+1].l + 1 );
	    tu_strcpy ( pattern[i].s, elem[i+1].s );
	}
	return 0;
    } else if ( tu_strncmp(elem[0].s,".IGNORE",8) == 0 ) {
	/*
	 * Append symbols to ignore_list.
	 */
	tu_strupcase ( elem[1].s, elem[1].s );
	sym = (struct keydef *) malloc ( sizeof(struct keydef) +
		sizeof(string)*count-1 );
	sym->next = ignore_list;
	ignore_list = sym;
	sym->count = count-1;

	for ( i = 0; i < count-1; i++ ) {
	    sym->elem[i].l = elem[i+1].l;
	    sym->elem[i].s = (char *) malloc ( elem[i+1].l + 1 );
	    tu_strupcase ( sym->elem[i].s, elem[i+1].s );
	}
	return 0;
    } else {
	/*
	 * See if directive was on one of the ignore lists and return 0 if
	 * found, which effectively ignores the line.
	 */
	for ( sym = ignore_list; sym; sym = sym->next ) {
	    for ( i = 0; i < sym->count; i++ ) {
		if ( 0 == tu_strncmp ( elem[0].s, sym->elem[i].s,
			sym->elem[i].l+1 ) ) return 0;
	    }
	}
	return -1;		/* unknown directive */
    }
    return count;
}
