/*
 * DECnet object for gatewaying script requests to run under user script
 * directories.  The http server connects to this object when requests of
 * the form /htbin~username/script/... occur.  The gateway receives the
 * request, parses out the username and makes a new connection to WWWEXEC
 * object (default scriptserver object) as that user (via VMS 6.2's persona 
 * services).
 *
 * Upon successful connection, the gateway relays the scriptserver protocol
 * messages between the http server and the new script.  The relay is
 * transparent with the following exceptions:
 *     <DNETMANAGE>	Not supported.
 *     <DNETPATH>	Adjusted to include the username.
 *     <DNETBINDIR>	Synthesized by appending the real <DNETBINDIR> real
 *			from the server with user's home directory.
 *
 * The script directory WWWEXEC will search will be a sub-directory of the
 * user's home directory (not thier web directory, e.g. users:[smith.wwwbin]).
 * Since the script process runs as the user, no special protections are needed
 * on this directory.
 *
 * This gateway provides a framework for future refinements to the user
 * scripting environment.  Possible areas to research are timeand/or
 * data transfer and/or per-user concurrency limits.
 *
 * Usage:
 *    $ run USER_SCRIPT
 *
 * HTTP server configuration, 2 options:
 *    exec /htbin~* 0::"0=WWWUEXEC"wwwbin
 *    exec /capbin/* 0::"WWWCEXEC="disk:[cap-dir]
 *
 * DECnet proxy configuration:
 *     add/proxy node::* *  /default	! lets user proxy to themselves.
 *
 * Required logical names:
 *    USER_SCRIPT_ACCESS	Multi-valued logical name, each equivalence name
 *				is of form NODE::USERNAME, where node is the
 *				nodename of the server and username is the
 *				username of the HTTP server account.
 *
 * Required environment variables:
 *    USER_SCRIPT_LIMIT		Maximum number of concurrent threads allowed.
 *    USER_SCRIPT_OBJECT	Decnet object (e.g. WWWUEXEC) to declare, must
 *				match object name for 'exec /htbin~*' rule
 *				used by server.
 *    USER_SCRIPTSERVER		Decnet specification for scriptserver object,
 *				(required if captive mode).  If non-captive,
 *				default is 0::"0=WWWEXEC".
 *
 * Required privileges:
 *    OPER
 *    SYSNAM
 *    SYSPRV
 *
 * Author:   David Jones
 * Date:     6-MAY-1997
 * Revised:  7-MAY-1997		Fixed bug in handling invalid users.  Add
 *				extended error explanation to abort_client().
 * Revised:  28-JUL-1998	Fix bug in terminator detection logic.
 * Revised:  12-JUL-2000	Enhanced with 'captive' mode.
 * Revised:  20-JUL-2000	Fix errant putlog call and broken cache
 * 				logic.
 * Revised:  22-FEB-2001	Fix bug in pcache mutual exclusion.
 * Revised:   8-MAR_2001	Fix bugs in persona cache flush and LRU
 *				tracking (references to invalid pcache slots).
 */
#include "pthread_1c_np.h"
#include <stdio.h>
#ifdef __DECC
/* Work around problems with fsync() not being ANSI standard */
#ifndef fsync
#define fsync decc$fsync
int decc$fsync(int fd);
#endif
#endif
#include <stdlib.h>
#include <impdef.h>
#include <descrip.h>
#include "tutil.h"
#include "tserver_decnet.h"
#include "decnet_access.h"
int tlog_putlog(int,char *, ...);
int tlog_init(char *);

#define TID task_id(pthread_self())
int task_id();

typedef struct { char *s; int l; } string;

static int new_client 
	( ts_client_ctx ctx, struct ncbdef *ncb, int ndx, int available );

/*
 * Define list of opcodes.
 */
static enum opcodes { DNET_HDR, DNET_ARG, DNET_ARG2, DNET_INPUT,
	DNET_TEXT, DNET_RAW, DNET_RQURL, DNET_CGI, DNET_HOST, DNET_ID,
	DNET_BINDIR, DNET_PATH, DNET_XLATE, DNET_XLATEV, DNET_SENTINEL, 
	DNET_INVCACHE, DNET_RECMODE, DNET_ID2, DNET_MANAGE } dummy;

static struct { enum opcodes opc; 	/* opcode */
		int l; 			/* Length of tag name (s) */
		char *s; 		/* Tag name */
		int in;			/* Additional input arguments */
		int out; } 		/* expected output arguments */
    tag_list[] = {
	{ DNET_HDR, 9, "<DNETHDR>", 0, -1 }, 	/* Send header */
	{ DNET_ARG, 9, "<DNETARG>", 0, 1 }, 	/* Send search arg */
	{ DNET_ARG2, 10, "<DNETARG2>", 0, 1 },	/* Send trunc. search arg */
	{ DNET_INPUT, 11, "<DNETINPUT>", 0, 1 },/* Send client data */
	{ DNET_TEXT, 10, "<DNETTEXT>", -1, 0 },	/* Read text response */
	{ DNET_RAW, 9, "<DNETRAW>", -1, 0 }, 	/* Read HTTP response */
	{ DNET_RQURL, 11, "<DNETRQURL>", 0, 1 },/* Send original URL */
	{ DNET_CGI, 9, "<DNETCGI>", -1, 0 }, 	/* Read 'CGI' response */
	{ DNET_HOST, 10,"<DNETHOST>", 0, 1 },	/* Send server host */
	{ DNET_ID, 8,"<DNETID>", 0, 1 },	/* Send connection info */
	{ DNET_BINDIR, 12,"<DNETBINDIR>", 0, 0 },/* Send htbin/exec directory */
	{ DNET_PATH, 10, "<DNETPATH>", 0, 0 },	/* Send htbin/exec prefix */
        { DNET_XLATE, 11, "<DNETXLATE>", 1, 1 },/* Translate URL by rules file*/
        { DNET_XLATEV, 12, "<DNETXLATEV>", 1, 1 },/* Translate URL by rules file*/
	{ DNET_INVCACHE, 14, "<DNETINVCACHE>", 0, 0 },
        { DNET_RECMODE, 13, "<DNETRECMODE>", 0, 0 },
	{ DNET_ID2, 9, "<DNETID2>", 0, 1 },	/* extended ID2 */
	{ DNET_SENTINEL, -1, "Sentinel", -1, 0 }
    };
/*
 * Task specification for scriptserver task run by user netserver processes.
 */
static char *scriptserver_task;
/******************************************************************************/
/* Define routines for assuming new persona.
 */
#define PCACHE_SIZE 20
static struct persona_block {
    char username[36];
    long persona;			/* VMS context pointer */
    unsigned int last_access;		/* for LRU replacement */
} pcache[PCACHE_SIZE];
int pcache_waiters;
int pcache_busy;
unsigned int pcache_sequence;
long original_persona, persona_flags;
int SYS$PERSONA_CREATE(), SYS$PERSONA_ASSUME(), SYS$PERSONA_DELETE();
static pthread_mutex_t pcache_lock;
static pthread_cond_t pcache_ready;

static int become_persona ( int ndx, char *username )
{
    char compare_string[36];
    int oldest_entry, status, i, match;
    /*
     * Do caseless compares.
     */
    tu_strupcase ( compare_string, username );
    /*
     * Get lock, once we have it, leave pcache_busy at 1.  We always return
     * with this lock held, regardless of completion status (caller must
     * always call revert_persona to release lock).
     */
    pthread_mutex_lock ( &pcache_lock );
    while ( pcache_busy ) {
	tlog_putlog(0,"!SL: Waiting for pcache !%T!/", ndx, 0 );
	pcache_waiters++;
	pthread_cond_wait ( &pcache_ready, &pcache_lock );
	--pcache_waiters;
    }
    pcache_busy = 1;
    pthread_mutex_unlock ( &pcache_lock );
    /*
     * Search cache for existing match, remember oldest entry.
     */
    for ( match = -1, i = oldest_entry = 0; i < PCACHE_SIZE; i++ ) {
	if ( pcache[i].last_access == 0 ) {
	    /* Entry never used, make it the 'oldest' */
	    oldest_entry = i; break;
	}
	else if ( tu_strncmp (compare_string, pcache[i].username, 39) == 0 ) {
	    match = i; break;
	} else if ( pcache[i].last_access < pcache[oldest_entry].last_access) {
	    oldest_entry = i;
	}
    }
    if ( match == -1 ) {
	/*
	 * No match, make the 'match' the least recently used persona slot
	 * and make a new persona.
	 */
	struct dsc$descriptor username_dx;
	match = oldest_entry;
	if ( pcache[match].last_access != 0 ) {
	    /*
	     * delete previous use of entry.
	     */
	    status = SYS$PERSONA_DELETE ( &pcache[match].persona );
	    tlog_putlog(0,"!SL: reusing persona[!SL] (!AZ), delete: !SL!/", 
		ndx, match, pcache[match].username, status );
	}
	/*
	 * Create new persona.
	 */
	pcache[match].last_access = 0;
	tu_strnzcpy ( pcache[match].username, compare_string, 
		sizeof(pcache[match].username)-1 );
        username_dx.dsc$a_pointer = compare_string;
        username_dx.dsc$w_length = tu_strlen ( pcache[match].username );
	status = SYS$PERSONA_CREATE (&pcache[match].persona, &username_dx, 0);
	tlog_putlog(0,"!SL: Created persona[!SL] (!AZ), status: !SL!/",
		ndx, match, compare_string,status );
	if ( (status&1) == 0 ) return status;
    }
    /*
     * Update sequence.
     */
    status = SYS$PERSONA_ASSUME ( &pcache[match].persona, persona_flags );
    tlog_putlog(0,"!SL: assumed persona[!SL] (!AZ), status: !SL!/", 
		ndx, match, compare_string,status );
    pcache_sequence++;
    if ( pcache_sequence == 0 ) pcache_sequence = 1;
    pcache[match].last_access = pcache_sequence;

    return status;
}

static int revert_persona ( )
{
    int status;
    /*
     * Change persona back to original.
     */
    status = SYS$PERSONA_ASSUME ( &original_persona, persona_flags );
    /*
     * Release lock.
     */
    pthread_mutex_lock ( &pcache_lock );
    pcache_busy = 0;
    if ( pcache_waiters > 0 ) pthread_cond_signal ( &pcache_ready );
    pthread_mutex_unlock ( &pcache_lock );
    return status;
}
/******************************************************************************/
/* Main entry point.
 */
int main ( int argc, char **argv )
{
    int client_limit, i, status, *final_status, pthread_create();
    char *object, *arg;
    pthread_attr_t attr;
    pthread_t listener, writer, clock;
    /*
     * Get environment variables.
     */
    arg = getenv ( "USER_SCRIPT_OBJECT" );
    if ( arg ) {
	object = malloc ( tu_strlen(arg) + 1 );
	tu_strcpy ( object, arg );
    } else {
	fprintf(stderr, 
		"USER_SCRIPT_OBJECT environment variable not defined\n");
	object = (char *) 0;
    }
    if ( !object ) exit ( 1 );
    arg = getenv ( "USER_SCRIPT_LIMIT" );
    if ( arg ) {
	client_limit = atoi ( arg );
    } client_limit = 32;
    scriptserver_task = (char *) 0;
    arg = getenv ( "USER_SCRIPTSERVER" );
    if ( arg ) {
	scriptserver_task = malloc ( tu_strlen(arg) + 1 );
	if ( scriptserver_task ) tu_strcpy ( scriptserver_task, arg );
    }
    /*
     * Initialize global variables.
     */
    if ( argc > 1 ) 
        tlog_init ( argv[1] );
    else
        tlog_init ( "sys$output" );
    status = ts_set_access ( "USER_SCRIPT_ACCESS" );
    if ( (status&1) == 0 ) {
	fprintf(stderr,"Error setting access list\n");
	exit ( status );
    }
    tlog_putlog(0,"User script 0.2 started at !%D, objectname: '!AZ'!/", 0,
	object );
    status = dnet_initialize ( );
    if ( (status&1) == 0 ) {
	fprintf(stderr,"Error initializing decnet client access\n");
	exit ( status );
    }
    /*
     * Initialize persona information.
     */
    INITIALIZE_MUTEX ( &pcache_lock );
    INITIALIZE_CONDITION ( &pcache_ready );
    original_persona = 1;			/* thats what the book says */
    persona_flags = IMP$M_ASSUME_SECURITY;	/* changes username */
    pcache_waiters = pcache_busy = pcache_sequence = 0;
    for ( i = 0; i < PCACHE_SIZE; i++ ) {
	pcache[i].username[0] = '\0';
	pcache[i].persona = 0;
	pcache[i].last_access = 0;
    }
    /*
     * Create threads:
     *   Listener thread listens for client connects, creating new client
     *   threads.
     */
    INITIALIZE_THREAD_ATTR ( &attr );
    pthread_attr_setstacksize ( &attr, 140000 );

    status = ts_declare_decnet_object 
		( object, client_limit, &attr, &listener, new_client );
    if ( (status&1) == 0 ) {
	tlog_putlog ( 0, "Error declaring DECnet object: !SL!/", status );
	fprintf(stderr, "Error declaring object %s: %d\n", object, status );
	exit(status);
    }
    /*
     * Wait for listener thread exit.
     */
    pthread_join ( listener, (void *)  &final_status );

    exit ( *final_status );
}
/*****************************************************************************/
/* The fixup_bindir helper function reads the <DNETBINDIR> response from
 * the server and uses it to construct a user-specific binddir from the
 * translation of the user's home directory.
 */
static int fixup_bindir ( ts_client_ctx ctx, void *script_cnx, 
	char *username,  char io_buf[4096] )
{
    int status, i, j, state, subdir_len, length;
    char subdir[40], user_bindir[256];
    /*
     * Read response from server and save as subdirectory.
     */
    status = ts_decnet_read ( ctx, io_buf, 4096, &subdir_len );
    if ( (status&1) == 0 ) return status;
    if ( subdir_len > 39 ) return 20;
    tu_strnzcpy ( subdir, io_buf, subdir_len );
    /*
     * Get user's home directory by asking server to translate /~username
     * and converting back to VMS file specification.
     * (Another way would be to call GETUAI outselve to it).
     */
    status = ts_decnet_write ( ctx, "<DNETXLATE>", 11 );
    if ( (status&1) == 0 ) return status;
    io_buf[0] = '/'; io_buf[1] = '~';
    length = tu_strlen ( username );
    tu_strncpy ( &io_buf[2], username, length );
    status = ts_decnet_write ( ctx, io_buf, length+2 );
    if ( (status&1) == 0 ) return status;
    status = ts_decnet_read ( ctx, io_buf, 4096, &length );
    if ( (status&1) == 0 ) return status;
    if ( length == 0 ) {
	/*
	 * error, return original server's response.
	 */
	tu_strcpy ( user_bindir, subdir );
	j = length;
    } else if ( io_buf[0] == '/' ) {
	/*
	 * Replace /device/dir1/...dir_m/dir_n with 
	 *  device:[dir1...dir_m.subdir]
	 */
	for ( j = 0, i = 1; i < length && io_buf[i] != '/'; i++ ) {
	    user_bindir[j++] = io_buf[i];
	    if ( j > 254 ) break;
	}
	user_bindir[j++] = ':'; i++;
	if ( (j < 254) && (i < length) ) {
	    /*
	     * Assume last directory in path is user's directory.
	     */
	    int last_dot = 0;
	    user_bindir[j++] = '[';
	    while ( (j<254) && (i < length) ) {
		user_bindir[j] = io_buf[i++];
		if ( user_bindir[j] == '/' ) { 
		    user_bindir[j] = '.'; last_dot = j;
		}
		j++;
	    }
	    if ( last_dot > 0 ) {
		/* overwrite last subdirectory */
		j = last_dot + 1;
		tu_strcpy ( &user_bindir[j], subdir );
		j += subdir_len;
	    }
	    user_bindir[j++] = ']';
	}
    } else {
	/*
	 * Unexpected response to tranlsation (first char not '/').
	 */
	tu_strcpy ( user_bindir, subdir );
	j = length;
    }
    /*
     * Send final reconstructed directory to script.
     */
    status = dnet_write ( script_cnx, user_bindir, j, &length );
    return status;
}
/*****************************************************************************/
/* Compose standard error response.
 */
static struct {
    char *cue;			/* sts_text */
    char *detail;
} extended_err[] = {
{ "Requested script (", "\n\
The requested script was either improperly specified in the URL or the\n\
script or its directory was inaccessible by the script process" },

{ "%SYSTEM-F-NOSUCHOBJ", "\n\
The scriptserver failed to create a script process because the DECnet\n\
mechanism it uses could not find a WWWEXEC object on the target node.\n\
The fault could be in the object definition in the network configuration;\n\
or the user's login directory is missing WWWEXEC.COM; or a system-wide WWWEXE\n\
logical points to a file is inaccessible to username the script runs under." },

{"%SYSTEM-F-LINKEXIT", "\n\
This error indicates a configuration problem with the server, causing the\n\
script process to die unexpectedly before acknowleging the network connection.\n\
Check the netserver.log file in the login directory for clues." },

{"Invalid username specified", "\n\
The string in the script path between the tilda (~) and the first slash (/)\n\
character specifies the username that is to run the script.  This username\n\
is not a valid user for running scripts on the target node, please check\n\
that the URL was specified correctly.  If the URL does not contain a tilda\n\
in the first path element, then the server is internally mapping this URL\n\
to the user script and you must see the server administrator to fix the\n\
problem."},

{"Captive scripts must ", "\n\
The URL used to invoke the target script must be authenticated with a valid\n\
username/password.  Check the server configuration to ensure the script\n\
path is marked as protected."},

{"Captive scriptserver not ", "\n\
The user_scriptserver parameter, which specifies the DECnet task\n\
for WWWEXEC, was not defined.  Captive mode operation requires this be\n\
defined."},

{"Bad tag from script: '", "\n\
The script sent invalid data to the net_link channel.  The initial dialog\n\
between the script and the server must consist of special protocol tags of\n\
the form <DNETxxx> where 'xxx' if one of: HDR, ARG, ARG2, INPUT, TEXT, RAW,\n\
RQURL, CGI, HOST, ID, ID2, BINDIR, PATH, XLATE, XLATEV, INVCACHE, or RECMODE.\n\
\nCGI scripts must first send a <DNETCGI> tag prior to sending the regular CGI\n\
response (\"content-type: ...\").  The cgilib function cgi_init() performs\n\
the necessary setup so that the script can begin sending the CGI response\n\
with cgi_printf()." },

{"Script closed connection", "\n\
A read error occured during the initial dialog between the script and the\n\
server, usually due to a fatal error in the script"},


{ "", "" }		/* end marker */
};
static void abort_client ( ts_client_ctx ctx, char *sts_line, char *sts_text )
{
    int status, length;
    char temp[80];
    /*
     * Place into text mode and send specified text.
     */
    status = ts_decnet_write ( ctx, "<DNETTEXT>", 10 );
    status = ts_decnet_write ( ctx, sts_line, tu_strlen(sts_line) );
    status = ts_decnet_write ( ctx, sts_text, tu_strlen(sts_text) );
    /*
     * Look for extended detail.
     */
    if ( (status&1) ) {
	int i, length;
	for ( i = 0; extended_err[i].cue[0]; i++ ) {
	    length = tu_strlen ( extended_err[i].cue );
	    if ( 0 == tu_strncmp (extended_err[i].cue, sts_text, length) ) {
		status = ts_decnet_write ( ctx, extended_err[i].detail,
			tu_strlen ( extended_err[i].detail ) );
		break;
	    }
	}
    }
    /*
     * Send closing tag and do dummy read to stall until server closes
     * connection first.
     */
    status = ts_decnet_write ( ctx, "</DNETTEXT>", 11 );
    status = ts_decnet_read ( ctx, temp, sizeof(temp)-1, &length );
}
/*****************************************************************************/
static int new_client ( ts_client_ctx ctx, 
	struct ncbdef *ncb, 
	int ndx, 
	int available )
{
    int status, length, opcode, i, j, script_user_len, captive_mode;
    char taskname[256], remote_node[256], remote_user[64];
    char source[64];
    string prologue[4];
    char *url, *bp;
    void *script_cnx;
    char prolog_buf[1104], script_path[256], script_dir[256], script_user[16];
    char cmd_buf[4096];

    ts_decnet_info ( taskname, remote_node, remote_user );

    tlog_putlog ( 0, "!SL: Connect to !AZ at !%D from !AZ@!AZ!/",
	ndx, taskname, 0, remote_user, remote_node );

    tu_strcpy ( source, remote_node );
    tu_strcpy ( &source[tu_strlen(source)], remote_user );
    /*                   0      1        2        3
     * Read prologue (module, method, protocol, ident) sent by HTTP server.
     */
    for ( i = 0, bp = prolog_buf; i < 4; i++ ) {
	status = ts_decnet_read ( ctx, bp, 255, &length );
	if ( (status&1) == 1 ) {
	    prologue[i].l = length;
	    prologue[i].s = bp;
	    bp[length++] = '\0';	/* safety first, terminate string */
	    bp = &bp[length];
	} else {
	    tlog_putlog ( 0, "!%D Error reading prologue: !SL!/", 0,
			status );
	    return status;
	}
    }
    url = prologue[3].s;
    /*
     * Query server for script path (DNETPATH) and validate.
     */
    status = ts_decnet_write ( ctx, "<DNETPATH>", 10 );
    if ( (status&1) != 1 ) return status;

    status = ts_decnet_read ( ctx, script_path, 255, &length );
    if ( (status&1) != 1 ) return status;
    for ( i = 0; i < length; i++ ) if ( script_path[i] != url[i] ) {
	/*
	 * Consistency check failed, first part of url should match script path.
	 */
	abort_client (ctx,"500 consitency check failed",
		"Internal path consistency failure" );
	return 20;
    }
    script_path[length] = '\0';
    /*
     * Get username to run script under.
     */
    if ( (length>0) && (script_path[length-1] == '/') ) {
	/*
	 * Server's exec rule is of form 'exec /htbin/* ...', use captive mode.
	 * Username for script obtained via web server authentication.
	 */
	int i, delim_count;

	captive_mode = 1;
	status = ts_decnet_write ( ctx, "<DNETID>", 8 );
	if ( (status&1) == 0 ) return status;

	status = ts_decnet_read ( ctx, cmd_buf, sizeof(cmd_buf)-1, &length );
	if ( (status&1) == 0 ) return status;
	cmd_buf[length] = '\0';

	script_user[0] = '\0';
	for ( i = delim_count = 0; i < length; i++ ) if ( cmd_buf[i] == ' ' ) {
	    delim_count++;		/* count number of spaces. */
	    if ( delim_count >= 5 ) {
		/*
		 * Usename is all chars following 5th space in response.
	 	 */
		length = tu_strlen ( &cmd_buf[i+1] );
		if ( length >= sizeof(script_user) ) { 	/* name too long */
		    abort_client (ctx,"400 invalid user(too long)", 
			"Invalid user" );
		    return 20;
		}
		tu_strcpy ( script_user, &cmd_buf[i+1] );
		break;
	    }
	}
	if ( !script_user[0] ) {
	    abort_client ( ctx, "500 captive script not authenticated",
		"Captive scripts must be authenticated" );
	    return 20;
	}
    } else {
	/*
	 * Server's exec rule is of form 'exec /htbin~* ...', run script in 
	 * user's personal bin directory.  Username is use is parsed from
	 * request URL (portion between end of script_path and first '/').
	 */
	captive_mode = 0;
	for ( i = 0; url[i+length]; i++ ) {
	    if ( i >= sizeof(script_user) ) {	/* name too long */
	        abort_client (ctx,"400 invalid user(too long)", "Invalid user" );
	        return 20;
	    } else if ( url[i+length] == '/' ) {
		script_user[i] = '\0';
		break;
	    } else {
		script_user[i] = url[i+length];
	    }
	}
	script_user_len = i;
    }
    /*
     * Assume persona of target user and make connection.  Become_persona
     * takes a global lock while active so only one connection can be in
     * progress at a time (lock is released by revert_persona).
     */
    status = become_persona ( ndx, script_user );
    if ( status&1 ) {
	/*
	 * Become successful, try connect.  Only allow default task
         * if in non-captive mode.
	 */
	char *task;
	task = scriptserver_task;
	if ( !task ) {
	    if ( captive_mode ) {
		revert_persona();
		abort_client ( ctx, "500 configuration error",
			"Captive scriptserver not defined" );
	    	return 20;
	    }
	    task = "0::\"0=WWWEXEC\"";		/* fallback to original */
	}

	status = dnet_connect ( task, &script_cnx );
	revert_persona();
	if ( (status&1) == 0 ) {
	    dnet_format_error ( status, cmd_buf, 255 );
	    abort_client ( ctx, "400 User script startup failure",
		 cmd_buf );
	    return status;
	}
    } else {
	/*
	 * Failed to become user.
	 */
	revert_persona();
        tlog_putlog(0,"!SL: Failed to become user !AZ, status: !SL!/", ndx, 
		script_user, status);
	abort_client ( ctx, "400 Invalid user", 
	     "Invalid username specified in script directory" );
	return status;
    }
    /*
     * Send the prolog to the user script process.
     */
    for ( i = 0; i < 4; i++ ) {
	status = dnet_write ( script_cnx, prologue[i].s,
		prologue[i].l, &length );
	if ( (status&1) != 1 || length != prologue[i].l ) {
	    /* Error sending data */
	    return status;
	}
    }
    /*
     * Main loop, relay scriptserver protocol between use script and web server.
     */
    dnet_set_time_limit ( script_cnx, 120 );		/* 2 minutes */
    while ( status&1 ) {
	/*
	 * Read command from remote script.
	 */
	status = dnet_read ( script_cnx, cmd_buf, sizeof(cmd_buf), &length );
	if ( (status&1) == 0 ) {
	    abort_client ( ctx, "500 Script abort", 
		"Script closed connection" );
	    break;
	}
	/*
	 * Lookup command string and convert to opcode number.
         */
	for ( opcode = 0; tag_list[opcode].l > 0; opcode++ )
	    if ( (length == tag_list[opcode].l) && 
		(0 == tu_strncmp(cmd_buf,tag_list[opcode].s,length)) ) break;
	if ( tag_list[opcode].l <= 0 ) {
	
	     /*
	      * Compose error message in iobuffer.
	      */
	     if ( length < 60 ) cmd_buf[length] = '\0';
	     tu_strnzcpy ( &cmd_buf[100], cmd_buf, 60 );  /* save first 60 */
	     cmd_buf[59] = '\0';
	     tu_strupcase ( cmd_buf, cmd_buf );
	     if ( 0 == tu_strncmp ( cmd_buf, "CONTENT-TYPE:", 13 ) ) {
		/*
		 * Special case, mistakenly assumed CGI.
		 */
	     }
	     tu_strcpy ( cmd_buf, "Bad tag from script: '" );
	     tu_strcpy ( &cmd_buf[22], &cmd_buf[100] );
	     length = tu_strlen ( cmd_buf );
	     tu_strcpy ( &cmd_buf[length], "'" );
	     abort_client ( ctx, "500 Bad tag from script", cmd_buf );
	     break;
	}
	status = ts_decnet_write ( ctx, cmd_buf, length );
	if ( (status&1) == 0 ) break;
	/*
	 * Take special actions for DNETPATH and DNETBINDIR to redirect
	 * to the users home directories when using per-user mode (non-captive).
	 */
	if ( captive_mode ) {
	    /* 
	     * Do straight passthrough for the special cases instead.
	     */
	    if ( (opcode == DNET_PATH) || (opcode == DNET_BINDIR) ) {
		status = ts_decnet_read ( ctx, cmd_buf, sizeof(cmd_buf), 
			&length );
		if ( (status&1) == 0 ) break;
		status = dnet_write ( script_cnx, cmd_buf, length, &length );
		if ( (status&1) == 0 ) break;
	    }
	} else if ( opcode == DNET_PATH ) {
	    /*
	     * Read server's response and append 'username/'.
	     */
	    status = ts_decnet_read ( ctx, cmd_buf, sizeof(cmd_buf), &length );
	    tu_strncpy ( &cmd_buf[length], script_user,  
		    sizeof(cmd_buf)-length-script_user_len );
	    length += script_user_len;
	    cmd_buf[length++] = '/';
	    status = dnet_write ( script_cnx, cmd_buf, length, &length );
	    if ( (status&1) == 0 ) break;
	} else if ( opcode == DNET_BINDIR ) {
	    /*
	     * Use actual bindir argument to construct a user-specific
	     * bin directory.
	     */
	    status = fixup_bindir (ctx, script_cnx, script_user, cmd_buf);
	    if ( (status&1) == 0 ) break;
	}
	/*
	 * Read additional arguments and send to server, a minus 1 is
	 * a terminal.
	 */
	for ( i = 0; i < tag_list[opcode].in; i++ ) {
	    status = dnet_read ( script_cnx, cmd_buf, sizeof(cmd_buf), 
			&length );
	    if ( (status&1) == 0 ) break;
	    status = ts_decnet_write ( ctx, cmd_buf, length );
	    if ( (status&1) == 0 ) break;
	}
	if ( tag_list[opcode].in < 0 ) {
	   /*
	    * Relay output until terminal seen.
	    */
	   char terminal[40];
	   int term_len;
	   term_len = length + 1;
	   tu_strnzcpy ( &terminal[2], &cmd_buf[1], sizeof(terminal)-2 );
	   terminal[0] = '<'; terminal[1] = '/';
	    dnet_set_time_limit ( script_cnx, 600 );
	   for ( ; ; ) {
		status = dnet_read ( script_cnx, cmd_buf, sizeof(cmd_buf), 
			&length );
		if ( (status&1) == 0 ) break;
		status = ts_decnet_write ( ctx, cmd_buf, length );
	        if ( (status&1) == 0 ) break;
		if ( length == term_len )  if ( 0 == 
				tu_strncmp(terminal, cmd_buf, term_len) ) {
		    /* do final read after sending terminator so the
		     * server is one to sever connection (ensures all data
		     * received)
		     */
		    ts_decnet_read ( ctx, cmd_buf, sizeof(cmd_buf),
			&length );
		    break;
		}
	    }
	    break;	/* all done */
	}
	/*
	 * Read the response from the server, a negative out length,
	 * means relay until zero length record (e.g. DNETHDR).
	 */
	for ( i = 0; i < tag_list[opcode].out; i++ ) {
	    status = ts_decnet_read ( ctx, cmd_buf,
		sizeof(cmd_buf), &length );
	    if ( (status&1) == 0 ) break;
	    status = dnet_write ( script_cnx, cmd_buf, length, &length );
	    if ( (status&1) == 0 ) break;
	}
	while ( tag_list[opcode].out < 0 ) {
	    status = ts_decnet_read ( ctx, cmd_buf,
		sizeof(cmd_buf), &length );
	    if ( (status&1) == 0 ) break;
	    status = dnet_write ( script_cnx, cmd_buf, length, &length );
	    if ( (status&1) == 0 || (length == 0) ) break;
	}
    }
    /*
     * Cleanup and log activity.
     */
    dnet_disconnect ( script_cnx );
    ts_decnet_close ( ctx );
    return status;
}
int task_id(long addr, long tid ) { return (tid&0x0ffff); }
