/*
 * Sample authenticator with extensions for access control lists and
 * MD5.
 *
 * The setup file simply defines a list of usernames/hosts and passwords.
 * If a password of '*' is specified, the corresponding username is checked
 * against the SYSUAF file.  If the username specification includes
 * a host address (e.g. SMITH@128.146.235.*), the password applies to only
 * the matching hosts.  A null username with a host address will accept
 * any connect from that host with no password check.
 *
 * Summary:
 *  <tag> tag-args...		   -> Special configuration info:
 *					<realm> string
 *  @host    *	   	           -> Host address must match
 *  username[@host] password       -> must match both
 *  *[@host]        password       -> any username, must match password
 *  username[@host] *              -> must match username, password from SYSUAF
 *  *[@host]        *              -> any username, password from SYSUAF
 *
 * If host restrictions would deny access regardless of username/password
 * given, the authentication fails with a 403 status so client doesn't waste
 * it's time prompting user for username/password.
 *
 * MACRO symbols for compile-time constants:
 *    DNS_ENABLE	Enable remote address translation via external
 *		  	lookup_host() function (authhost.c).
 *    UAF_HTBL_SIZE	Size of hash table for UAF cache must be power of 2.
 *			Default is 256.
 *    UAF_CACHE_LIMIT	Absolute limit of number of cache blocks allocated.
 *			Default = 10000
 *    UAF_CACHE_TIMEOUT	Timeout on uaf cache entries, specified as delta time.
 *			default = "0 01:00:00" (1 hour).
 *
 * Author:      David Jones
 * Date:        29-MAY-1994
 * Revised:      5-FEB-1995	Fixes for DECC.  Support host address.
 * Revised:	 8-FEB-1995	Add caching of UAI information.
 *				Also check flags for disuser's account.
 * Revised:	13-FEB-1995	Bugfix from CEL, make context var. static.
 * Revised:	23-FEB-1995	Add auth_callback prototype for notosimple.
 * Revised:	7-SEP-1995	Report SYSPRV status in startup message.
 * Revised:	26-NOV-1996	Added Dave Smith's ACL enhancements.
 * Revised:	13-FEB-1997	Incorporated fixes to MD5 code from
 *				Martin Winter (martin.winter@dm.krinfo.ch).
 *				Cleaned up structure of nososimple().
 *				Modified setup to preserve username/password
 *				case when <digest> tag present in file.
 * Revised:	16-AUG-1997	Modified to call $SCAN_INTRUSION service
 *				if compiled on version 6.1 or higher and
 *				image installed with SECURITY privilege.
 * Revised:	12-SEP-1997	Fix bug in 'with-document' processing, add
 *				MD5_ALLOW_WD_MISSING configuration option.
 * Revised:	26-SEP-1997	Alloc optional port restriction on host mask.
 * Revised:	9-NOV-2001	Use file_access and file cache routines.
 *				(Bump version to 1.1).
 */
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <descrip.h>
#include "ctype_np.h"
#include <uaidef.h>
#include <prvdef.h>
#include "authlib.h"
#include "authacl.h"
#include "file_access.h"
#include "file_cache.h"
#include "md5.h"		/* Eric Young's MD5 digester */
typedef struct { int l; char *s; } string;

#define MAXLINE 1024

static struct AUTHINFO {
   char user[MAXLINE];
   char pass[MAXLINE];
   char *host;			/* Host restrictions in username */
   struct AUTHINFO *next;
   int port;			/* if non-zero, port restriction */
} *uhead, *utail, *ufree;
static struct {
    char setup_file[256];		/* Name of setup file */
    char realm[256];			/* realm (signature) for file */
    char digest[256];			/* Parameters for digest authenticate */
    char acl[ACLMAXBIN];		/* binary ACL for file */
    int  acllen;			/* ACL length */
    int  wd_missing;			/* Protect file with document missing? */
} suf;					/* current [s]et[u]p [f]ile */

/*
 * Define structures for caching sysuaf data.
 */
#ifndef UAF_HTBL_SIZE
#define UAF_HTBL_SIZE 256
#endif
#ifndef UAF_CACHE_LIMIT
#define UAF_CACHE_LIMIT 10000
#endif
#ifndef UAF_CACHE_TIMEOUT
#define UAF_CACHE_TIMEOUT "0 01:00:00"
#endif
static struct UAFINFO {
    struct UAFINFO *next;
    long expiration[2];
    char username[40];
    char password[40];
} *uaf_hashtbl[UAF_HTBL_SIZE], *uaf_free;
static int cache_allocated;
static $DESCRIPTOR(cache_timeout_str, UAF_CACHE_TIMEOUT);
static long cache_timeout[2];
static long priv_mask[2];
static long prev_privs[2];
static long *uai_context;
static int allow_wd_missing;	/* if true, missing with-document prot file,
				 * permits access. */

/* prototypes for forward references */
int hash_realm(char *s);
int sys$setprv(), sys$getuai(), sys$hash_password();
int sys$bintim(), sys$gettim(), lib$subx();
int check_vms_password(char *username, char *password, unsigned char *rem_addr);
int http_parse_elements ( int limit, char *line, string *elem );
static int expand_uaf_free ( int alloc_size );
auth_callback notsosimple;		/* typedef auth_callback in authlib.h*/
static int check_digest ( char *fields[8], char *method, char *uri, 
	unsigned char *remote_address, char *correct_password, 
	char **response, char *log_message );
static void digest_challenge ( char *, int, char *, char * );
static char *digest_md5 ( char *result,	/* 32 char array to receive result */
	char *fmt, ... );		/* variable number of arguments. */

/*
 * Main routine.
 */
int main ( int argc, char **argv )
{
    int status, i;
    int cache_size, cache_maxitem, cache_refresh;
    char log_message[120];
    char *envval;
    /*
     * turn off any initial SYSPRV
     */

    priv_mask[0] = PRV$M_SYSPRV; priv_mask[1] = 0;
    (void) sys$setprv ( 0, priv_mask, 0, prev_privs );
    /*
     * Initialize data structures needed to perform authentication.
     * Using a linked list, null out the head and tail pointers.
     */

     uhead = utail = ufree = NULL;
     suf.setup_file[0] = '\0';
     suf.digest[0] = '\0';
     cache_allocated = 0;
     uai_context = auth_getuai_context();
     for ( i = 0; i < UAF_HTBL_SIZE; i++ ) 
	uaf_hashtbl[i] = (struct UAFINFO *) 0;
     uaf_free = (struct UAFINFO *) 0;
     expand_uaf_free(40);
     status = sys$bintim ( &cache_timeout_str, cache_timeout );
     if ( 0 == (status&1) ) { fprintf(stderr,"Bad cache timeout specified\n");
	exit ( status );
     }
    allow_wd_missing = 0;
    envval = getenv ( "MD5_ALLOW_WD_MISSING" );
    if ( envval ) allow_wd_missing = atoi ( envval );
    /*
     * Initialize the file cache for saving setup file info.
     */
    tf_initialize ( (char *) 0 );

    cache_refresh = 300;		/* 5 minutes */
    envval = getenv ( "MD5_CACHE_REFRESH" );
    if ( envval ) cache_refresh = atoi ( envval );
    cache_size = 10000000;		/* 1 megabyte */
    envval = getenv ( "MD5_CACHE_SIZE" );
    if ( envval ) cache_size = atoi ( envval );
    cache_maxitem = 10000;		/* Max per item */
    envval = getenv ( "MD5_CACHE_MAXITEM" );
    if ( envval ) cache_maxitem = atoi ( envval );

    status = tfc_init ( cache_refresh, cache_size, cache_maxitem );
    /*
     * Process server requests.  The auth_server routine connects to the
     * the HTTP server that created us and effecitively makes the authenticate
     * function (notsosimple()) a remote procedure call for the server.
     * The third argument is text to be placed in the server's log file.
     */
    sprintf ( log_message,
	"MD5 Authenticator, V 1.1T1, initialized successfully, %s SYSPRV%s",
	(PRV$M_SYSPRV&prev_privs[0]) ? "has" : "does not have",
	"\r\nMD5 digest code by Eric Young" );

    status = auth_server ( argc, argv, log_message, 1, notsosimple );
    /*
     * Getting to this point means an error occurred since auth_server
     * loops to continuously get authentication requests.  Most likely the
     * the HTTP server exitted so we should run down as well.  Perform any
     * necessary cleanup and exit.
     */
    exit ( status );
}
/************************************************************************/
/*
 * Read protection setup file and store in global variable.
 */
static int load_setup ( char *setup_file, char *log_message, char *ident )
{
     int i, j, length, count, preserve_case, tfc_status;
     struct AUTHINFO *u;
     /* FILE *fp; */
     void *fp;
     char line[MAXLINE];
     string token[10];
     char aclent[256];
     int setup_wd;  /* flag indicating setup file is with document */
     char wd_file[768]; /* path of 'with document' setup file */
     char *input_file;
     struct tfc_context tfc;
     /*
      * Construct actual filename to be opened.  Setup-file names that
      * begin with '.' are 'with document' setup files.
      */
    input_file = setup_file;

    setup_wd = FALSE;
    preserve_case = 0;
    if ( *setup_file == '.' )	{	/* file with document? */
	/*
	 * Construct setup file name using directory of current ident.
	 */
	setup_wd = TRUE;
	for ( i = 0; i < sizeof ( wd_file ); i++ ) {
	    wd_file[i] = ident[i];
	    if ( !wd_file[i] || (wd_file[i] == '*') ) break;
	}
	for (i = i-1;  (i > 0) && (wd_file[i] != '/');  i--);
	if ( (i + strlen(setup_file) + 1) < sizeof(wd_file ) ) {
	    strcpy (&wd_file[i+1], setup_file);
	} else {
	    strcpy ( wd_file, "0:file.too.long" );
	}
	input_file = wd_file;
    }
     /*
      * Attempt to open cached copy of file (type=1 (text)).
      */
     tfc_status = tfc_cached_open ( 1, input_file, &fp, line, &tfc );
     if ( tfc_status == 0 ) {
	sprintf(log_message,"unable to open setup file %s: %s",setup_file,
		line );
	if ( setup_wd && allow_wd_missing ) {
	    /*
	     * Make a dummy setup-file parse result if 'with document' setup 
	     * files are allowed to be missing.
	     */
	    suf.wd_missing = TRUE;
	    suf.realm[0] = '\0';
	    suf.acllen = 0;
	    if ( utail != NULL ) utail->next = ufree;
	    ufree = uhead;
	    uhead = utail = NULL;
	    strcpy (suf.setup_file, input_file);	/* save failure */
	    return 1;
	} else {
	    return 0;
	}
     }
     /*
      * Check if setup already loaded, otherwise try to open it.
      * Cache can be disabled by defining DEBUGAUTH when debugging 
      * authenticator or .PROT files.
      */
#ifndef DEBUGAUTH
     if ( (tfc_status==1) && (0 == strcmp ( input_file, suf.setup_file )) ) {
	tfc_rundown_context ( &tfc, 0 );
	return 1;
     }
#endif
     /*
      * Free old UAF info blocks and init suf structure.
      */
     if ( utail != NULL ) {
	utail->next = ufree;
     }
     ufree = uhead;
     uhead = utail = NULL;
     suf.setup_file[0] = '\0';		/* mark as invalid */
     suf.digest[0] = '\0';
     suf.realm[0] = '\0';
     suf.acllen = 0;
     suf.wd_missing = 0;
     /*
      * Read and parse suffix file, set file access to use just LF as
      * line delimiter.
      */
     if ( tfc_status == 2 ) tf_set_crlf_newline ( fp, 0 );
     for ( ; ; ) {
	if ( tfc_status == 1 ) {
	    /* Take next line from cache */
	    char *rec;
	    if ( 1 != tfc_get ( &tfc, (void **) &rec, &length ) ) break;
	    strncpy ( line, rec, length );
	    line[length] = '\0';
	} else {
	    /*
	     * New entry, read line and add to cache.
	     */
	    length = tf_getline ( fp, line, sizeof(line), 1 );
	    if ( length < 0 ) {
		tfc_rundown_context ( &tfc, 1 );
		sprintf ( log_message, "Error reading %s\n", setup_file );
		return 0;
	    } else if ( length == 0 ) {
		break;
	    }
	    if ( line[length-1] == '\n' ) length--;
	    line[length] = '\0';
	    for (j=0; (j<length); j++) {
                if ((line[j] == '#') || (line[j] == '\n')) {
                    line[j] = '\0';
                    break;
                }
            }
	    length = j;
	    tfc_put ( &tfc, line, length );
	}
	/*
	 * Break line into tokens and add to setup file structure.
	 */
        if (length > 0) {
            count = http_parse_elements(9,line,token);
	    if ( count == 1 ) if ( token[0].s[0] == '@' ) count = 2;
            if ( count > 1 ) {
		/*
		 * Check for special control tags.
		 */
		if ( (token[0].s[0] == '<') && 
			(token[0].s[token[0].l-1] == '>') ) {
		    for ( j = 0; token[0].s[j]; j++ ) 
			token[0].s[j] = UPPER_CASE(token[0].s[j]);
		    if ( strcmp ( token[0].s, "<REALM>" ) == 0 ) {
			/* Concatenate rest of tokens as realm name */
			for ( j=0, i = 1; i < count; i++ ) {
			    if ( j ) suf.realm[j++] = ' ';
			    if ( (token[i].l + j) >= sizeof(suf.realm) ) break;
			    strcpy ( &suf.realm[j], token[i].s );
			    j = j + token[i].l;
			}
			suf.realm[j] = '\0';
		    }
		    else if ( strcmp ( token[0].s, "<ACL>" ) == 0 ) {
			/* Concatenate rest of tokens as ACE */
			for ( j=0, i = 1; i < count; i++ ) {
			    if ( j ) aclent[j++] = ' ';
			    if ( (token[i].l + j) >= sizeof(aclent) ) break;
			    strcpy ( &aclent[j], token[i].s );
			    j = j + token[i].l;
			}
			aclent[j] = '\0';
			if ( !authacl_parse_acl ( aclent, 
						  suf.acl, 
						  &suf.acllen,
						  log_message ) ) {
			    if ( fp ) tf_close ( fp );
			    tfc_rundown_context ( &tfc, 1 );
			    return 0;
			}
		    }
		    else if ( strcmp ( token[0].s, "<DIGEST>" ) == 0 ) {
			/* Concatenate rest of tokens as digest arguments */
			for ( j=0, i = 1; i < count; i++ ) {
			    if ( j ) suf.digest[j++] = ' ';
			    if ((token[i].l + j) >= sizeof(suf.digest)) break;
			    strcpy ( &suf.digest[j], token[i].s );
			    j = j + token[i].l;
			}
			suf.digest[j] = '\0';
			/* all following names are case sensitive */
			preserve_case = 1;	
		    } else if ( strcmp ( token[0].s, "<CACHE>" ) == 0 ) {
			/*
			 * Set the cache expiration time for this file's
			 * cache entry.  A time of 0 means each access checks
			 * the file's modification date against cached entry.
			 */
			if ( tfc_status == 2 ) tfc_update_expiration (
				&tfc,  atoi(token[1].s) );
		    }
		    continue;
		}
		/*
		 * Allocate block.
		 */
		if ( ufree == NULL ) {
                    u = (struct AUTHINFO *)malloc(sizeof(struct AUTHINFO));
		    if ( u == NULL ) {
			if ( fp ) tf_close ( fp );
			tfc_rundown_context ( &tfc, 1 );
			return 0;
		    };
 		} else {
		    u = ufree;			/* Get from lookaside list */
		    ufree = u->next;
  		}
		/*
		 * Copy tokens to authoinfo structure.
		 */
		u->host = NULL;
                for (i=0; i<token[0].l; i++) {
		    (u->user)[i] = (preserve_case && (u->host==NULL) ) ?
			(token[0].s)[i] : UPPER_CASE((token[0].s)[i]);
		    if ( (u->user)[i] == '@' ) {
			/* Terminate username and make remainder host string */
		 	(u->user)[i] = '\0';
			u->host = &(u->user)[i+1];
		    }
  		}
                (u->user)[token[0].l] = '\0';
		u->port = 0;
		if ( u->host ) {
		    for ( i = 0; (u->host)[i]; i++ ) if ((u->host)[i] == ':') {
			(u->host)[i] = '\0';
			u->port = atoi ( &(u->host)[i+1] );
			break;
		    }
		}
                for (i=0; i<token[1].l; i++) 
			(u->pass)[i] = preserve_case ?
			(token[1].s)[i] : UPPER_CASE((token[1].s)[i]);
                (u->pass)[token[1].l] = '\0';
		/*
		 * Add to linked list of authinfo structures.
		 */
                u->next = NULL;
		if ( utail == NULL ) uhead = u;
		else utail->next = u;
		utail = u;
            }
        }
     }
     /*
     * Close file/cache entry and save name.
     */
     if ( fp ) tf_close ( fp );
     tfc_rundown_context ( &tfc, 0 );
     strcpy ( suf.setup_file, input_file );
     return 1;
}
/************************************************************************/
/* Dummy routine for returning hostname for address.  To replace, compile
 * module DNS_ENABLE defined.
 */
#ifndef DNS_ENABLE
void lookup_hostname ( unsigned char *address, char *remote_host )
{
    strcpy ( remote_host, "-1.-1.-1.-1" );
}
#endif
/************************************************************************/
/* Compare host string with IP address, return 1 if it matches. 
 */
static int check_host ( char *host, unsigned char *address )
{
    static char octet_str[256][4];
    static int octet_str_init = 0;

    if ( ( *host >= '0' ) && ( *host <= '9' ) ) {
	/*
	 * Numeric host address.  Compare octet by octet, host string can
	 * substitute * for an octet.
	 */
        char number[16], *d, *octet;
        int i, j, len;
	if ( !octet_str_init ) {
	    /* First time, init numeric strings */
	    for ( i = 0; i < 256; i++ ) sprintf ( octet_str[i], "%d", i );
	    octet_str_init = 1;
	}

        for ( d = host, i = 0; i < 4; i++ ) {
	    if ( (*d == '*') && ((d[1]=='.')||(d[1]=='\0')) ) {
		d += 2;
	    } else {
		octet = octet_str[address[i]];
		for ( len = 0; d[len] && (d[len] != '.'); len++ ) {
		    if ( d[len] != octet[len] ) break;		/* mismatch */
		}

		if ( octet[len] != '\0' ) break;
		else if ( i < 3 ) {
		    if ( d[len] != '.' ) break;		/* wrong delimiter */
		} else {
		    if ( d[len] != '\0' ) break;
		}
		d += len + 1;
	    }
	}
	if ( i == 4 ) return 1;	/* we have a match, 4 fields matched */

    } else {
	/*
	 * Compare domain name.
	 */
	char *d;
        int i, j, k, len, rhost_len;
	char remote_host[512];
        void lookup_hostname();

	lookup_hostname ( address, remote_host );
	rhost_len = strlen ( remote_host );
	remote_host[rhost_len++] = '.';

	/* Break hostname into separate labels */
	d = host;
	for (j=i=0; i <= rhost_len; i++ ) if (remote_host[i]=='.') {
	    /* 
	     * String goes from j to i, ending in period.  Determine
	     * length (k) of next label in pattern string (d)
	     */
	    for ( k=0; d[k] && (d[k]!='.'); k++ );
	    len = i - j;
	    if ( (k != 1) || (d[0] != '*') ) {

	        if ( k != len) break;	/* mismatch */
	        if ( strncmp(d,&remote_host[j],len) ) break;
	    }
	    d += k; if ( *d ) d++;
	    j = i+1;
	}
	/* We pass if If all labels matched and no more value */
	if ( (i > rhost_len) && !*d ) return 1;
    }
    return 0;
}
/************************************************************************/
/*
 * access checking routine.
 *  check against username/remote_address pairs stored in linked list
 *  and return password if it is necessary for the final access check
 *  several cases:
 *
 *  @host    *	   	           -> Host address must match
 *  username[@host] password       -> must match both
 *  *[@host]        password       -> any username, must match password
 *  username[@host] *              -> must match username, password from SYSUAF
 *  *[@host]        *              -> any username, password from SYSUAF
 *
 *  Return values:
 *	1		Access granted.
 *	0		No match, username required.
 *	-1		No match, setup file specified no username checks
 *			for remote (or all) host so authorization will not help.
 *      -2              Access ok, if password matches returned password
 *      -3              Access ok, if VMS-password check succeeds
 */
static int check_access ( char *username, unsigned char *remote_address,
	char *password, int max_pwd, int local_port )
{
    struct AUTHINFO *u;
    int user_check;

    user_check = -1;
    for ( u = uhead; u != NULL; u = u->next ) {
	if ( u->host ) {
	    /* Check host and port , go to next autinfo if check fails. */
	    if ( u->port ) if ( u->port != local_port ) continue;
	    if ( !check_host ( u->host, remote_address ) ) continue;

	    /* Host passed, Accept uncondionally if username null. */
	    if ( u->user[0] == '\0' ) return 1;
	}
	user_check = 0;
	/* fprintf(stderr,"checking user '%s'@'%s', password '%s'\n", u->user,
	    u->host ? u->host : "*", u->pass ); */
        if (strcmp(u->user,"*") == 0) {
            if (strcmp(u->pass,"*") == 0) {
                return -3;
            } else {
                strncpy(password,u->pass,max_pwd-1);
		password[max_pwd-1] = '\0';
                return -2;
            }
        } else if (strcmp(u->user,username) == 0) {
            if (strcmp(u->pass,"*") == 0) {
                return -3;
            } else {
                strncpy(password,u->pass,max_pwd-1);
		password[max_pwd-1] = '\0';
                return -2;
            }
        }
    }
    return user_check;
}
/************************************************************************/
/*
 * password checking routine.
 *  check against username/password pairs stored in linked list
 *  several cases:
 *
 *  @host    *	   	           -> Host address must match
 *  username[@host] password       -> must match both
 *  *[@host]        password       -> any username, must match password
 *  username[@host] *              -> must match username, password from SYSUAF
 *  *[@host]        *              -> any username, password from SYSUAF
 *
 *  Return values:
 *	1		Access granted.
 *	0		No match, username required.
 *	-1		No match, setup file specified no username checks
 *			for remote (or all) host so authorization will not help.
 */
static int check_password ( char *username, char *password, 
	unsigned char *remote_address, int local_port )
{
    struct AUTHINFO *u;
    int user_check;

    user_check = -1;
    for ( u = uhead; u != NULL; u = u->next ) {
	if ( u->host ) {
	    /* Check host, go to next autinfo if check fails. */
	    if ( u->port ) if ( u->port != local_port ) continue;
	    if ( !check_host ( u->host, remote_address ) ) continue;

	    /* Host passed, Accept uncondionally if username null. */
	    if ( u->user[0] == '\0' ) return 1;
	}
	user_check = 0;
	/* fprintf(stderr,"checking user '%s'@'%s', password '%s'\n", u->user,
	    u->host ? u->host : "*", u->pass ); */
        if (strcmp(u->user,"*") == 0) {
            if (strcmp(u->pass,"*") == 0) {
                if (check_vms_password(username, password,remote_address)) return 1;
            } else {
                if (strcmp(u->pass,password) == 0) return 1;
            }
        } else if (strcmp(u->user,username) == 0) {
            if (strcmp(u->pass,"*") == 0) {
                if (check_vms_password(username,password,remote_address)) return 1;
            } else {
                if (strcmp(u->pass,password) == 0) return 1;
            }
        }
    }
    return user_check;
}


/****************************************************************************/
/* Do garbage collection (move expired cached entries to free list), if
 * free list after GC is empty, allocate more cache blocks for it.
 */
static int expand_uaf_free ( int alloc_size ) {
    int i, j, expired;
    long delta[2], cur_time[2];
    struct UAFINFO *cb, *prev_cb;
    /*
     * Check each hash table chain for expired entries.
     */
    sys$gettim ( cur_time );
    for ( i = 0; i < UAF_HTBL_SIZE; i++ ) if ( uaf_hashtbl[i] ) {
	prev_cb = (struct UAFINFO *) 0;
	for ( cb = uaf_hashtbl[i]; cb; ) {
	    /* 
	     * See if cur_time is larger (after) cache block's expiration.
	     */
	    lib$subx ( cb->expiration, cur_time, delta );
	    expired = (delta[1] < 0);
	    if ( expired ) {
		/*
		 * Remove block from hash chain and add to free list.
		 */
		/* fprintf(stderr, "Removing expired entry for cache, %s\n", cb->username); */
		for ( j = 0; cb->password[j]; j++ ) cb->password[j] = '\0';
		if ( prev_cb ) prev_cb->next = cb->next;
		else uaf_hashtbl[i] = cb->next;
		cb->next = uaf_free;
		uaf_free = cb;

		cb = (prev_cb) ? prev_cb->next : uaf_hashtbl[i];
	    } else {
		prev_cb = cb;
	        cb = cb->next;
	    }
	}
    }
    /*
     * Expand free list by alloc_size blocks if still emtpy and alloc_size > 0.
     * Limit total number of cache blocks allocated to UAF_CACHE_LIMIT.
     */
    if ( uaf_free ) return 1;
    if ( alloc_size > (UAF_CACHE_LIMIT - cache_allocated) )
	alloc_size = UAF_CACHE_LIMIT - cache_allocated;
    if ( alloc_size <= 0 ) return 0;
    /*
     * Allocate all in 1 chunk and divide up into linked list.
     */
    cb = uaf_free;
    uaf_free = malloc ( sizeof(struct UAFINFO) * alloc_size );
    if ( !uaf_free ) return 0;
    cache_allocated += alloc_size;
    for ( i = 0; i < alloc_size-1; i++ ) uaf_free[i].next = &uaf_free[i+1];
    uaf_free[i].next = cb;
    return 1;
}

/*
 * Define wrapper function for calling $scan_intrusion system service.
 * Lack of SECURITY privilege disables use of $scan_intrustion.
 */
#ifdef HAS_SCAN_INTRUSION
#include <ciadef>
#include <jpidef>
#include <secsrvdef>
#ifdef PRV$M_SECURITY
#undef PRV$M_SECURITY
#endif
#define PRV$M_SECURITY 64
#ifdef VAXC
globalref int secsrv$_intruder;
#else
extern int secsrv$_intruder;
#endif

static int scan_intrusion ( int login_status, int username_valid,
	struct dsc$descriptor_s *username, unsigned char *remote_addr )
{
    int status, sys$scan_intrusion(), flags;
    $DESCRIPTOR(authenticator_user,"MD5_AUTHENTICATOR");
    $DESCRIPTOR(remote_node_dx, "");
    char remote_node[20];
    /*
     * Check that process has SECURITY privilege.
     */
    if ( (PRV$M_SECURITY&prev_privs[1]) == 0 ) return login_status;
    /*
     * Register activity. Encode remote address as node name and
     * use MD5_AUTHENTICATOR as source user.
     */
    sprintf(remote_node,"%d.%d.%d.%d", remote_addr[0], remote_addr[1],
	remote_addr[2], remote_addr[3] );
    remote_node_dx.dsc$w_length = strlen ( remote_node );
    remote_node_dx.dsc$a_pointer = remote_node;
    flags = (username_valid) ? CIA$M_REAL_USERNAME : 0;

    status = sys$scan_intrusion ( login_status, username, JPI$K_NETWORK,
	0, &remote_node_dx, &authenticator_user, 0, 0, 0, 0, flags );
    /*
     * Fail request if intruder detected, otherwise return original
     * login status.
     */
    if ( status == (int) &secsrv$_intruder ) return 0;

    return (1&login_status);
}
#else
/*
 * Skip using SYS$SCAN_INTRUSION completely (VMS 5.x)
 */
#define scan_intrusion(a,b,c,d) (a)
#endif

int check_vms_password(char *username, char *password, unsigned char
		*remote_address)
{
    int iss, hndx;
    unsigned long hash1[2], hash0[2];
    long cur_time[2], delta[2];
    static long context = -1;
    $DESCRIPTOR(d_user,"");
    $DESCRIPTOR(d_pass,"");
    unsigned char algorithm;
    unsigned short salt;
    unsigned long flags, len0, len1, len2;

    struct item_list_3 {
        unsigned short il3$w_buflen;
        unsigned short il3$w_code;
        void *         il3$a_buffer;
        unsigned long *il3$a_retlen;
    };

    static struct item_list_3 uai_items[]  = {
        {  8, UAI$_PWD, NULL, NULL},    /* &hash0[0], &len0}, */
        {  2, UAI$_SALT, NULL, NULL},   /* &salt,     &len1}, */
        {  1, UAI$_ENCRYPT, NULL, NULL},/* &algorithm,&len2}, */
	{  4, UAI$_FLAGS, NULL, NULL},
        {  0, 0, NULL, NULL}
    };
    struct UAFINFO *cb;
    /*
     * Check cache for matching entry.
     */
    if ( username[0] == '\0' ) return 0;		/* null username */
    hndx = ((UAF_HTBL_SIZE-1)&hash_realm(username));
    /* fprintf(stderr,"search cache, hash ndx = %d (%d)\n", hndx, uaf_hashtbl[hndx]);*/
    sys$gettim ( cur_time );
    for ( cb = uaf_hashtbl[hndx]; cb; cb = cb->next ) {
	if ( 0 == strcmp ( username, cb->username ) ) {
	    /*
	     * Cache block found for this username stale and re-validate if so. 
	     */
	    lib$subx ( cb->expiration, cur_time, delta );
	    if ( delta[1] < 0 ) break;
	    /* 
	     * Test password against cached copy
	     */
	    if ( 0 != strcmp ( password, cb->password ) ) break;
	    /* Accept */
	    return 1;
	}
    }
    /*
     * Build arguments for $getuai.
     */
    uai_items[0].il3$a_buffer = &hash0[0];
    uai_items[0].il3$a_retlen = &len0;
    uai_items[1].il3$a_buffer = &salt;
    uai_items[1].il3$a_retlen = &len1;
    uai_items[2].il3$a_buffer = &algorithm;
    uai_items[2].il3$a_retlen = &len2;
    uai_items[3].il3$a_buffer = &flags;

    d_user.dsc$a_pointer = username;
    d_user.dsc$w_length  = strlen(username);
    d_pass.dsc$a_pointer = password;
    d_pass.dsc$w_length  = strlen(password);
    /*
     * Enable SYSPRV temporarily, get UAF items and disable again.
     */
    iss = sys$setprv ( 1, priv_mask, 0, 0 );
    if (!(iss&1)) return 0;

    iss = sys$getuai(0,uai_context,&d_user,uai_items,0,0,0);

    (void) sys$setprv ( 0, priv_mask, 0, 0 );
    if ( 0 == (iss&1) ) {
	/* Register failure with intrusion database */
	iss = scan_intrusion ( iss, 0, &d_user, remote_address );
    }
    /*
     * Hash the password for comparison with the UAF entry.
     */
    if ((iss&1)) {
	iss = sys$hash_password(&d_pass,algorithm,salt,&d_user,&hash1[0]);
    }
    if (!(iss&1)) {
	return 0;
    }
    /*
     * At this point, we have hashed the user's password and have
     * his SYSUAF entry.  Compare them.
     */
    if ( ((flags&UAI$M_DISACNT) == 0 ) && 
		(hash0[0] == hash1[0]) && (hash0[1]==hash1[1]) ) {
	/*
	 * Verify user was not flagged an intruder.
	 */
	iss = scan_intrusion ( 1, 1, &d_user, remote_address );
	if ( 0 == (iss&1) ) return 0;
	/* 
	 * Password validated, save it in cache block 
	 */
	if ( !cb ) {
	    if ( !uaf_free ) if ( !expand_uaf_free(100) ) return 1;
	    cb = uaf_free;
	    uaf_free = cb->next;
	    cb->next = uaf_hashtbl[hndx];
	    uaf_hashtbl[hndx] = cb;
	}
	/*
	 * Load/reload cache block with latest expriation and password.
	 */
	lib$subx ( cur_time, cache_timeout, cb->expiration );
	strncpy ( cb->username, username, 39 );
	strncpy ( cb->password, password, 39 );		/* pick up latest */
	cb->username[39] = '\0';
	cb->password[39] = '\0';
	return 1;
    } else {
	if ( cb ) {
	    /*
	     * Flush expired entries to get rid of the stale 1.
	     */
	    lib$subx ( cb->expiration, cur_time, delta );
	    if ( delta[1] < 0 ) expand_uaf_free ( 0 );
	}
	/*
	 * Tell VMS about failure.
	 */
	iss = scan_intrusion ( 0, 0, &d_user, remote_address );
    }
    return 0;
}

/*************************************************************************/
/* Authenticator callback routine for auth_server().  This routine is
 * called to process an authentication request sent by the server.
 *
 * This routine determines whether access to the requested object (ident)
 * with the specified method is to be granted, validating the authorization
 * information against the protections defined by the setup file.
 *
 * return values:
 *      0       Access to the object is denied.
 *      1       Access to the object is permitted.
 *
 * When access is denied, this routine sets the response argument to point
 * to a statically allocated string containing the HTTP error response headers
 * to send to the client, sans the HTTP version (i.e. "403 Access denied\r\n").
 * The newlines are included.  If a 401 status is returned, the response header
 * includes the WWW-authenticate headers for the authorization schemes that
 * this routine understands.
 */
int notsosimple (
        int local_port,                 /* Local port # of TCP/IP connection */
        int remote_port,                /* Remote TCP/IP port of client */
        unsigned char remote_address[4],/* Remote binary IP address of client*/
        char *method,                   /* Requested method (GET, PUT, etc); */
        char *setup_file,               /* Protection setup file name */
        char *ident,                    /* Ident portion of translated URL */
        char *authorization,            /* Auth. headers included in request */
        char **response,                /* HTTP error response headers */
        char **log )                    /* Optional log message */
{
    int status, allowed, auth_type, i;
    char basic_username[40], correct_password[40], password[40];
    /*
     * Note that response and log buffers must be statically allocated.
     * Fail and succeed are pointers to string constants (implicitly static),
     * if you make them char arrays be to all make them static storage class.
     */
    static char log_message[200];
    static char fail[200];
    char *username, *fields[8];
    char *succeed = "200 access to protected file granted\r\n";
    char *setup_failure = "500 failed to open protection db file\r\n";
    char *host_failure = "403 Forbidden, authorization will not help\r\n";
    char http_uri[256], field_store[512];
    /*
     * Read the protection setup file that defines
     * the protections for the object.  If this operation fails, we
     * return failure (0) with a "500" error status line.
     */
     log_message[0] = 0;
     if ( !load_setup ( setup_file, log_message, ident ) ) {
        *response = setup_failure;
        *log = log_message;
        return 0;
     }
    /*
     * Set default response to the "unauthorized" error response headers string
     */
    *response = fail;
    *log = log_message;
    /*
     * determine authentication available in request: 0-none, 1-md5, 2-basic
     * Set username to that indicated in the request.
     */
    auth_type = 0;
    username = "";
    password[0] = correct_password[0] = '\0';
    if ( suf.digest[0] ) {
        /*
         * Load file has MD5 flag, look for digest authentication
         */
	status = auth_digest_authorization ( authorization,
		http_uri, fields, field_store );
	if ( status&1 ) { auth_type = 1; username = fields[0]; }
	if ( !username ) username = "";
    }
    if ( auth_type == 0 ) {
	/* 
	 * Look for basic authorization header 
	 */
	if ( auth_basic_authorization ( authorization, 
	    basic_username, sizeof(basic_username), 
	    password, sizeof(password), 1 ) ) {
	    auth_type = 2;
	    username = basic_username;
	}
    }
    /*
     * Do access check against the protection setup file or allow access
     * if wd_missing flag set (no with-document).
     */
    if ( suf.wd_missing ) allowed = 1;
    else allowed = check_access ( username, remote_address, correct_password,
	sizeof(correct_password), local_port );
#ifdef DEBUG
    fprintf(stdout,"Check access result: %d, auth_type: %d\n", allowed,auth_type);
#endif
    if ( allowed == -2 ) {
	/*
	 * Password in request must match password supplied in setup file.
	 */
	if ( auth_type == 1 ) {
	    /* 
	     * MD5 authentication 
	     */
	    allowed = check_digest ( fields, method, http_uri, remote_address,
		correct_password, response, log_message );

	} else if ( auth_type == 2 ) {  
	    /* 
	     * basic authorization 
	     */
	    for ( i = 0; correct_password[i]; i++ )
		correct_password[i] = UPPER_CASE(correct_password[i]);
	    if ( 0 == strcmp ( password, correct_password ) ) allowed = 1;
	    else allowed = 0;

	} else allowed = 0; 
    } else if ( allowed == -3 ) {
	/*
	 * authentication password must match user's SYSUAF password (basic
	 * authentication only)
	 */
	if ( auth_type == 2 ) {
	    allowed = check_vms_password ( username, password, remote_address );
	} else allowed = 0;
    }
    /*
     * At this point, allowed flag should be have 3 possible values:
     *    1 - Access provisionally granted.
     *    0 - Access denied, retry with different username/pass may work.
     *   -1 - Access denied for reason other than invalid  username/pwd.
     *
     * If access granted and acl present, verify against the ACL.
     */
    if ( (allowed == 1) && (suf.acllen != 0) ) {
	allowed = authacl_check_acl ( username, method, suf.acl, suf.acllen,
		0,  (unsigned long *) 0 );
    }
    /*
     * Generate a message for the server's log file.
     * The server checks if the log line begins with "[username]" and
     * makes 'username' the local user in the access.log file.
     */
    if ( allowed < 0 && *username) {
	/*
	 * Make last chance check using null username and log as default access.
	 */
	allowed = check_password ( "", "", remote_address, local_port );
	sprintf(log_message,"Default access authentication %s",
		(allowed==1) ? "succeeded" : "failed" );
    } else {
        sprintf(log_message,"[%s]%sAuthentication for user='%s' pass='%s' %s.",
            allowed ? username : "", (auth_type==1) ? "MD5-" : "", username, 
            "[CENSORED]", allowed ? "succeeded" : "failed" );
    }
    /*
     * Set the HTTP response line to send back to the client (currently fail).
     * Don't override if a previous step explicitly set response line.
     */
    if ( allowed < 0 ) {
	/* Host checks failed, send 403 status. */
	*response = host_failure;
	allowed = 0;

    } else if ( allowed == 1 ) {
	/* Access was granted */
	if ( *response == fail ) *response = succeed;

    } else if ( *response == fail ) {
	/* 
	 * Generate authorization failure status line with authenticate header
	 * Synthesize realm name if none in setup file.
	 */
	if ( suf.realm[0] == '\0' )
		sprintf ( suf.realm, "CEL%08X",	hash_realm(suf.setup_file) );

	strcpy ( fail, "401 Unauthorized\r\n" );
	if ( suf.digest[0] == '\0' ) {
	    /* No digest tag in setup file, use basic */
	    sprintf ( &fail[18], "WWW-Authenticate: Basic realm=\"%s\"\r\n",
		suf.realm );
	} else {
	    /* Create www-authenticate header for digest method */
	    digest_challenge ( &fail[18], sizeof(fail)-18,
		suf.realm, http_uri );
	}
    }
    return allowed;
}

/* following swiped from rule_file.c ... we don't want the rest of the
 * baggage though.
 */

/*
 * 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 limit 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;
}


/*
    just a simple minded hash of ascii -> 32 bit integer
    so that we can (hopefully) tell one setup file from another
    and assign them unique realms without giving away too much
    information about where the setup files actually live.
*/

int hash_realm(char *s)
{
    char c;
    int i = 0;

    while (c = *s++) {
        i = ((i>>24)&0x3F) ^ c ^ (i<<6);
    }
    return i;
}
/*************************************************************************/
/*  Perform digest authentication.
 *
 * Input:
 *    fields	 	array of arguments parsed from authorzation: header.
 *    uri		URL path parsed from HTTP request line.
 *    remote_address	Binary IP address of remote host.
 *
 * Output:
 *    response		Receives pointer to static array holding HTTP response
 *			line.
 *    log_message	Receives text to be added to server's trace log.
 *
 * Return value:
 *      1		Success.
 *	0		Authentication failed due to bad password.
 *     -1		Authentication permanently failed.
 */
int check_digest (  
	char *fields[8], 	/* fields from authenticate header */
	char *method, 		/* method from HTTP request */
	char *uri, 		/* URI specified in HTTP request */
	unsigned char *remote_address,	/* IP address of remote client */
	char *correct_password, 	/* valid password from setup file */
	char **response, char *log_message )   /* Return HTTP status line */
{
    static char http_status[512];
    int status;
    char result[33], pwd_hash[33], uri_hash[33];
    /*
     * lookup or compute hash for username:realm:password.
     */
#ifdef DEBUG
    fprintf(stdout,"supplied fields: '%s' '%s' '%s' '%s' '%s'\n",
	fields[0], fields[1], fields[2], fields[3], fields[4] );
#endif
    digest_md5(pwd_hash, "%s:%s:%s", fields[0] ? fields[0] : "", suf.realm, 
	correct_password );                   
    /*
     * Compute response using our secret.
     */
    digest_md5(result,"%m:%s:%m", pwd_hash, fields[2],
	digest_md5(uri_hash, "%s:%s", method, fields[3]) );
#ifdef DEBUG
    fprintf(stdout,"request method: '%s', URI: '%s', realm: '%s', result hash: '%s'\n",
    method, uri, suf.realm, result );
#endif
    if ( strncmp(result,fields[4],32) == 0 ) {
	/* Password check passed, now verify nonce and uri match. */
        /* compare URI without leading "/" */
	if ( strcmp(&uri[1],fields[3]) != 0 ) return 0;
	strcpy ( http_status, "200 OK" );
	return 1;
    } else {
	return 0;
    }
    return 0;
}
/*************************************************************************/
/* Generate www-authenticate header for MD5 digest authentication style.
 * Output buffer must be null-terminated and include ending carriage control.
 */
void digest_challenge ( char *header, int hdr_size, char *realm, char *uri )
{
    char nonce_val[33];
    nonce_val[32] = '\0';
    sprintf ( header,
	"WWW-Authenticate: digest realm=\"%s\", %s%s%s%s\r\n",
	realm, "nonce=\"", digest_md5(nonce_val, "%s", uri ),
	"\", algorithm=\"MD5\", stale=\"false\"", " opaque=\"aabc\"" );
}
/*************************************************************************/
/* Return 32 character hex encoding of formatted string.  Construction
 * of string to digest is controlled by fmt string which contains conversion
 * codes introduced by a percent sign (e.g. %s),  Codes are:
 *
 *    % - Insert % in data stream.
 *    s - Take next argument as pointer to zero-terminated ascii string.
 *    m - take next argument as pointer to 32 raw data bytes (another MD5 output.
 *    q - take next 2 arguments as a byte count (int) followed by
 *        pointer to count number of raw data bytes.
 */
static char *digest_md5 ( char *result,	/* 32 char array to receive result */
	char *fmt,			/* format control string */
	... )				/* variable number of arguments. */
{
    int i, j, length;
    va_list ap;				/* argument pointer */
    char *arg;
    MD5_CTX c;
    unsigned char digest[MD5_BLOCK];

    MD5_Init ( &c );
    va_start(ap, fmt);			/* point to first variable argument */
    /*
     * Scan for format string and build MD5 checksum from the components.
     */
    for ( i = j = 0; fmt[i]; i++ ) if ( fmt[i] == '%' ) {
	i++;
	if ( !fmt[i] ) break;		/* dangling percent is illegal. */
	if ( fmt[i] == '%' ) {
	    MD5_Update(&c, (unsigned char *) &fmt[j], i-j );
	} else if ( fmt[i] == 's' ) {
	    if ((i-j) > 1) MD5_Update ( &c, (unsigned char *) &fmt[j], i-j-1 );
	    arg = va_arg(ap, char *);
	    length = strlen ( arg );
	    MD5_Update ( &c, (unsigned char *) arg, length );
	} else if ( fmt[i] == 'm' ) {
	    /*
	     * String points to 32 character hash.
	     */
	    if ((i-j) > 1) MD5_Update ( &c, (unsigned char *) &fmt[j], i-j-1 );
	    arg = va_arg(ap, char *);
	    MD5_Update ( &c, (unsigned char *) arg, 32 );
	} else if ( fmt[i] == 'q' ) {
	    /*
	     * String points to counted string.
	     */
	    int bcount;
	    if ((i-j) > 1) MD5_Update ( &c, (unsigned char *) &fmt[j], i-j-1 );
	    bcount = va_arg(ap, int);
	    arg = va_arg(ap, char *);
	    MD5_Update ( &c, (unsigned char *) arg, bcount );
	}
	j = i+1;			/* start of next segment */
    }

    if ( j < i ) MD5_Update ( &c, (unsigned char *) &fmt[j], i - j );
    MD5_Final ( digest, &c );
    /*
     * Convert final 128 bit digest to hex result.
     */
    for ( i = j = 0; i < MD5_BLOCK; i++ ) {
	int nibble;
	nibble = (digest[i]>>4)&15;
	if ( nibble < 10 ) result[j++] = '0' + nibble;
	else result[j++] = 'a' + (nibble-10);
	nibble = (digest[i])&15;
	if ( nibble < 10 ) result[j++] = '0' + nibble;
	else result[j++] = 'a' + (nibble-10);
    }
    return result;    
}
