/* This dynamically loaded MST script provides an interface to FASTCGI
 * applications. Use the following rules in the configuration file to load 
 * this module and bind an exec path to it:
 *
 *    ThreadPool fcgi  stack=60000 q_flag=1 limit=10
 *    Service testsrv pool=fcgi dynamic=(fastcgi,fastcgi_mst) info=configfile
 *    Exec /$fastcgi/* %fastcgi:
 *
 * The configfile provides a mapping of virutal 'scripts' to TCP/IP application
 * ports.  Each line in the file is either a comment (#) or has the following 
 * format:
 *
 *    role name [+]host:port [command]
 *    e.g. "responder test localhost:2301 "
 *
 * Where:
 *    role		FastCGI role name, must be 'responder'
 *    name		If virtual script name (element after /$fastcgi).
 *    [+]host:port	Host and port to make connections 2.  The optional
 *			plus indicates duplicates are selected round-robin.
 *    command		VMS command to invoke to start application.
 *
 * In addition, the following environment variables control operations:
 *    FCGI_RESPONDER_FLAGS	Integer value.  1-keep connections.
 *
 * Author: David Jones
 * Date:   20-JUN-1997
 * Revised: 22-JUN-1997		Keep connections open.
 */
#include "pthread_1c_np.h"
#include "mst_share.h"
#include "tutil.h"
#include "fastcgi_mux.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef VMS
#include <descrip.h>
#endif
/*
 * Define data structures for store configuration file contents.
 */
static int load_config ( FILE *cfg, char *fname );
struct app_info {
    struct app_info *next;
    char *host;
    int port;
};
struct virtual_script {
    struct virtual_script *next;
    char *name;
    int app_count;			/* number of app_info defs */
    int rr_flag;			/* round-robin flag */
    int waiter_count;
    pthread_mutex_t lock;
    pthread_cond_t app_available;
    struct app_info *first;
};
static struct virtual_script *vscript_list;
static struct {
    unsigned char roleb1, roleb0, flags, reserved[5];
} responder_request = { 0, FCGI_RESPONDER, FCGI_KEEP_CONN, 0, 0, 0, 0, 0  };
static int send_params ( fcgi_handle fcgi, struct mstshr_envbuf *env );
static int send_input ( fcgi_handle fcgi, mst_link_t link, int content_length );
/***************************************************************************/
/* Every dynamically loaded service must have an INIT routine, which is
 * called once during the processing of the Service rule to initialize
 * static structures needed by the MST.  By default the init routine
 * name is the start routine + the string _init, and 'init=name' clause
 * on the service rule will override the default.
 *
 * Arguments:
 *    mst_linkage vector  Structure containing addresses of essential
 *			items wee need from main program, such as
 *			address of putlog() routine.
 *
 *    char *info	Administrator provided argument (info=).
 *
 *    char *errmsg	Error text for log file when error status returned.
 */
int fastcgi_init ( mst_linkage vector, char *info, char *errmsg )
{
    FILE *cfg;
    char * envval;
    int status;
   /*
    * Setup mst_share globals.
    */
   if ( (1&mstshr_init ( vector, info, errmsg )) == 0 ) return 0;
   /*
    * Initialize the layer responsible for multiplexing data streams.
    */
   status = fcgi_init_mux();
   tu_strcpy ( errmsg, "FastCGI scriptserver sucessfully initialized" );
   /*
    * 'info' argument is name of configuration file.  read and build
    * table of virtual script names.  Spawn any application processes as well.
    * Note that initialization is done single threaded.
    */
    tlog_putlog(1,"fastcgi confifile name: '!AZ'!/", info );
    cfg = fopen ( info, "r", "dna=www_system:.conf" );
    if ( cfg ) {
	status = load_config ( cfg, info );
	fclose ( cfg );
	if ( (status&1) == 0 ) tu_strcpy ( errmsg,
		"FastCGI scriptserver configuration failure" );
    } else {
	sprintf(errmsg, "FastCGI scriptserver failed to open '%s'", info );
	return 0;
    }
    /*
     * Set responder flags.
     */
    envval = getenv ( "FCGI_RESPONDER_FLAGS" );
    if ( envval ) responder_request.flags = atoi ( envval );
    /*
     * Return success status.
     */
    return status;
}
/***************************************************************************/
/* Main routine to handle client requests.  To the server, this routine
 * must behave like a DECnet scriptserver task (e.g. WWWEXEC) only messages 
 * are transferred via mst_read() and mst_write() rather than $QIO's to a 
 * logical link.
 *
 * Arguments:
 *    mst_link_t link	Connection structure used by mst_read(), mst_write().
 *
 *    char *service	Service name (for logging purposes).
 *
 *    char *info	Script-directory argument from 'exec' rule that
 *			triggered the MST (exec /path/* %service:info).
 *
 *    int ndx		Service thread index, all services sharing same pool
 *			share the same thread index numbers.
 *
 *    int avail		Number of remaining threads in pool assigned to service.
 */
int fastcgi ( mst_link_t link, char *service, char *info, int ndx, int avail )
{
    int i, status, length, app_port, alloc_fails;
    fcgi_handle fcgi;
    char *vscript, *apphost, line[512];
    struct mstshr_envbuf env;
    struct virtual_script *vsb;
    struct app_info *aib;
    /*
     * Log that we began execution
     */
    if ( http_log_level > 0 ) tlog_putlog ( 1, 
	"Service!AZ/!SL connected, info: '!AZ', pool remaining: !SL!/", 
	service, ndx, info, avail );
    /*
     * Setup cgi environment (reads prologue as a consequence).
     */
    status = mstshr_cgi_symbols ( link, info, &env );
    if ( (status &1) == 0 ) return 0;
    /*
     * Convert script name (SCRIPT_NAME-SCRIPT_PATH) to script definition.
     */
    vscript = mstshr_getenv ( "SCRIPT_NAME", &env ) +
	tu_strlen ( mstshr_getenv ( "SCRIPT_PATH", &env ) );
    for ( vsb = vscript_list; vsb; vsb = vsb->next ) {
	if ( !vsb->name ) continue;
	if ( tu_strncmp ( vsb->name, vscript, 200 ) == 0 ) break;
    }

    if ( !vsb ) {
	status = mst_write ( link, "<DNETTEXT>", 10, &length );
        if ( (status &1) == 0 ) return 0;
	status = mst_write (link, "404 virtual script not found", 29, &length);
	status = mst_write ( link, 
		"Virtual script name not in configuration file.",
		 46, &length );
        if ( (status &1) == 0 ) return 0;
        status = mst_write ( link, "</DNETTEXT>", 11, &length );
	mst_close ( link );
	return status;
    }
    /*
     * Connect to application.
     */
    pthread_mutex_lock ( &vsb->lock );
    aib = vsb->first;
    if ( vsb->rr_flag ) vsb->first = vsb->first->next;
    pthread_mutex_unlock ( &vsb->lock );
    line[0] = '\0';
    for ( alloc_fails = i = 0; i < vsb->app_count; i++ ) {
	status = fcgi_connect ( aib->host, aib->port, &fcgi, line );
        if ( http_log_level > 4 ) tlog_putlog ( 5, 
	"Service!AZ/!SL connect attempt to host !AZ, port !SL, status=!SL!/",
	 service, ndx, aib->host, aib->port, status );

	if ( status&1 ) break;
	if ( status == 2 ) alloc_fails++;
	aib = aib->next;
	if ( alloc_fails == vsb->app_count ) {
	    /*
	     * Wait for current app to rundown and restart search.
	     */
	    tlog_putlog(5,"Service!AZ/!SL waiting for available app, !%D!/",
		service, ndx, 0 );
	    pthread_mutex_lock ( &vsb->lock );
	    vsb->waiter_count++;
	    pthread_cond_wait ( &vsb->app_available, &vsb->lock );
	    vsb->waiter_count--;
	    aib = vsb->first;
	    pthread_mutex_unlock ( &vsb->lock );
	    tlog_putlog(5,"Service!AZ/!SL restarting scan at !%T, app_count: !SL!/",
		service, ndx, 0, vsb->app_count );
	    alloc_fails = 0;
	    i = -1;			/* account for increment for stmt */
	}
    }
    if ( (status&1) == 0 ) {
	status = mst_write ( link, "<DNETTEXT>", 10, &length );
        if ( (status &1) == 0 ) return 0;
	status = mst_write ( link, "400 connect error", 17, &length );
	status = mst_write ( link, 
		"Error connecting to FASTCGI application. reason:\n",
		 49, &length );
        if ( (status &1) == 0 ) return 0;
        status = mst_write ( link, line, tu_strlen(line), &length );
        status = mst_write ( link, "</DNETTEXT>", 11, &length );
	mst_close ( link );
	return status;
    }
    /*
     * Send params stream.
     */
    status = fcgi_send ( fcgi, FCGI_BEGIN_REQUEST, &responder_request,
	sizeof(responder_request) );

    status = send_params ( fcgi, &env );
    if ( (status&1) ) {
        /*
         * Send input stream.
         */
	char *cl_str;
	int content_length;
	content_length = 0;
	cl_str = mstshr_getenv ( "HTTP_CONTENT_LENGTH", &env );
	if ( cl_str ) {
	    /* Decode content length string */
	    while ( *cl_str >= '0' && *cl_str <= '9' ) {
	       content_length = (content_length*10) + (*cl_str - '0');
		if ( content_length > 400000000 ) break;
	    }
	}
	status = send_input ( fcgi, link, content_length );
    }
    if ( status&1 ) {
	char *msg;  int msgtype, msgsize;
	/*
	 * Enter CGI mode and return output.
	 */
	status = mst_write ( link, "<DNETCGI>", 9, &length );
	status = mst_write ( link, 
		"Status: 200 FastCGI output follows\r\n", 36, &length );

	for ( msgtype = FCGI_BEGIN_REQUEST; ( status&1 ); )  {
	    status = fcgi_recv ( fcgi, &msgtype, &msg, &msgsize );
	    if ( http_log_level > 10 ) tlog_putlog ( 11,
		"Data message from app status: !SL, type: !SL, size: !SL (!XL)!/",
		status, msgtype, msgsize, msg );
	    if ( (status&1) == 0 ) break;
	    if ( msgtype == FCGI_END_REQUEST ) break;

	    switch ( msgtype ) {
		case FCGI_STDOUT:
		    status = mst_write ( link, msg, msgsize, &length );
		    break;
		case FCGI_STDERR:
		    tlog_putlog ( 0, "Service!AZ/!SL app error: !AF!/",
			service, ndx, msgsize, msg );
		    status = 1;
		    break;
		default:
		    status = tlog_putlog ( 0, 
			"Service!AZ/!SL Unexpect message type recieved: !SL!/",
			service, ndx, msgtype );
		    
		    break;		
	    }
	}
	status = mst_write ( link, "</DNETCGI>", 10, &length );
	if ( msgtype == FCGI_END_REQUEST ) {
	    status = fcgi_disconnect ( fcgi, responder_request.flags );
	    if ( http_log_level > 4 ) tlog_putlog ( 5,
		"Service!AZ/!SL disconnected status: !SL!/",
		service, ndx, status);
	    pthread_mutex_lock ( &vsb->lock );
	    if ( vsb->waiter_count > 0 ) pthread_cond_signal ( &vsb->app_available );
	    pthread_mutex_unlock ( &vsb->lock );
	}

    } else {
	/*
	 * Return error message.
	 */
        status = mst_write ( link, "<DNETTEXT>", 10, &length );
        if ( (status &1) == 0 ) return 0;

        status = mst_write ( link, "500 Error in FASTCGI mech", 25, &length );
        if ( (status &1) == 0 ) return 0;
        /*
         * Cleanly shutdown connection and return.
         */
	status = fcgi_disconnect ( fcgi, 0 );
        status = mst_write ( link, "</DNETTEXT>", 11, &length );
        if ( (status &1) == 0 ) return 0;
    }

    mst_close ( link );
    
    return 1;
}

static int load_config ( FILE *cfg, char *fname )
{
    char line[2048];
    char *tmp, *role, *name, *host_port, *command;
    int status, line_num, rr_flag;
    struct virtual_script *vsb, *prev_vsb, dummy;
    struct app_info *aib;

    vscript_list = (struct virtual_script *) 0;
    /*
     * Read every line of configuration file, track the line number.
     */
    for ( line_num = 1; fgets ( line, sizeof(line), cfg ); line_num++ ) {
	/*
	 * Parse tokens from string: role name host command
	 */
	tmp = strchr ( line, '\n' );
	if ( tmp ) *tmp = '\0';
	else { tlog_putlog(0,"Line too long!/"); return 0; }
	role = strtok ( line, " \t" );
	if ( !role ) continue;		/* blank line */
	if ( *role == '#' ) continue;	/* comment line */
	if ( strcmp ( role, "responder" ) != 0 ) {
	    tlog_putlog ( 0, 
		"Unsupported role (!AZ) in FastCGI config file, line !SL!/",
		role, line_num );
	    continue;
	}
	name = strtok ( (char *) 0, " \t" );
	if ( !name ) {
	    tlog_putlog ( 0, 
		"Syntax error in FastCGI config file, line !SL!/",
		role, line_num );
	    continue;
	}
	host_port = strtok ( (char *) 0, " \t" );
	if ( !host_port ) {
	    tlog_putlog ( 0, 
		"Syntax error in FastCGI config file, line !SL!/",
		role, line_num );
	    continue;
	}
	command = strtok ( (char *) 0, "" );
	if ( !command ) command = "";
	tlog_putlog ( 5, "!SL: '!AZ' '!AZ' '!AZ' '!AZ'!/", line_num,
		role, name, host_port, command );
	/*
	 * Make application info block.
	 */
	aib = (struct app_info *) malloc ( sizeof(struct app_info) );
	aib->next = (struct app_info *) 0;
	aib->host = malloc ( strlen(host_port)+1 );
	rr_flag = 0;
	strcpy ( aib->host, host_port );
	if ( aib->host[0] == '+' ) { aib->host++; rr_flag = 1; }
	tmp = strchr ( aib->host, ':' );
	if ( !tmp ) aib->port = 2001;
	else {
	    *tmp++ = '\0';
	    aib->port = atoi ( tmp );
	}
	/*
	 * Search for virtual script block that matches name.
	 */
	dummy.next = vscript_list;
	prev_vsb = &dummy;
	for ( vsb = prev_vsb->next; vsb; vsb = vsb->next ) {
	    if ( strcmp ( vsb->name, name ) == 0 ) break;
	    prev_vsb = vsb;
	}
	if ( !vsb ) {
	    vsb = (struct virtual_script *) malloc ( 
		sizeof(struct virtual_script) );
	    if ( !vsb ) return 0;
	    vsb->next = (struct virtual_script *) 0;
	    prev_vsb->next = vsb;
	    vsb->name = malloc ( strlen(name)+1 );
	    strcpy ( vsb->name, name );
	    vsb->app_count = vsb->rr_flag = vsb->waiter_count = 0;
	    INITIALIZE_MUTEX ( &vsb->lock );
	    SET_MUTEX_NAME(&vsb->lock,"OSU fastCGI vsb lock")
	    INITIALIZE_CONDITION ( &vsb->app_available );
	    SET_COND_NAME(&vsb->app_available,"OSU fastCGI vsb avail.")
	}
	if ( !vscript_list ) vscript_list = dummy.next;
	/*
	 * Add app info to script block.
	 */
	vsb->rr_flag = rr_flag;
	if ( vsb->app_count == 0 ) {
	    vsb->first = aib;
	    aib->next = vsb->first;
	} else {
	    /* Place app info block at end of (circular) list. */
	    aib->next = vsb->first;
	    while ( vsb->first->next != aib->next ) vsb->first =
			vsb->first->next;
	    vsb->first->next = aib;
	    vsb->first = aib->next;
	}
	vsb->app_count++;
	/*
	 * Execute startup command for app if specified.
	 */
	if ( *command ) {
#ifdef VMS
	    int LIB$SPAWN (), flags;
	    static $DESCRIPTOR(cmd_dx,"");

	    tlog_putlog ( 1, "FastCGI: $ !AZ!/", command );
	    cmd_dx.dsc$w_length = strlen ( command );
	    cmd_dx.dsc$a_pointer = command;
	    flags = 1;		/* no wait */
	    status = LIB$SPAWN ( &cmd_dx, 0, 0, &flags );
#endif
	}
    }
    return 1;
}
/*****************************************************************************/
/*  Convert CGI environment array to namevalue pairs and send to
 * fcgi application as PARAMS stream.
 */
static int make_nv_header ( int nlen, int vlen, unsigned char *hdr )
{
    int off;
    off = 0;
    /* Encode lengths using either 1 byte or 4 bytes (bigendian). */
    if ( nlen < 128 ) {
	hdr[off++] = nlen;
    } else {
	hdr[off++] = (nlen>>24) | 128;
	hdr[off++] = (nlen>>16) & 255;
	hdr[off++] = (nlen>>8) & 255;
	hdr[off++] = nlen & 255;
    }
    if ( vlen < 128 ) {
	hdr[off++] = vlen;
    } else {
	hdr[off++] = (vlen>>24) | 128;
	hdr[off++] = (vlen>>16) & 255;
	hdr[off++] = (vlen>>8) & 255;
	hdr[off++] = vlen & 255;
    }
    return off;			/* number of bytes used */
}

static int append_params ( fcgi_handle fcgi, char *buffer,
int bufsize, int *boff, char *data, int dsize )
{
   int status, i, off, segment;

    off = *boff;
    while ( dsize + off > bufsize ) {
	segment = bufsize - off;
	for ( i = 0; i < dsize; i++ ) buffer[off++] = data[i];
	status = fcgi_send ( fcgi, FCGI_PARAMS, buffer, segment );
	off = 0;
	data += segment;
	dsize = dsize - segment;
    }
    for ( i = 0; i < dsize; i++ ) buffer[off++] = data[i];
    *boff = off;
    status = 1;
    return status;
}
/*
 * Send MST's environment array as a FCGI_PARAMS stream of name-value pairs.
 * Skip first 4 items (simulated PATH, HOME, TERM, USER).
 */
static int send_params ( fcgi_handle fcgi, struct mstshr_envbuf *env )
{
    int status, i, nlen, vlen, boff, hlen;
    char *p, buffer[4096], header[8];

    for ( boff = 0, i = 4; i < env->count; i++ ) {
	/*
	 * env string is form 'name=value', determine string lengths, don't 
	 * include the '='
         */
	nlen = vlen = 0;
	p = env->ptr[i];
	for ( p = env->ptr[i]; *p != '='; p++ ) nlen++;
	for ( p++; *p; p++ ) vlen++;
	/*
	 * Construct namevalue header.
	 */
	if ( boff+8 > sizeof(buffer) ) {
	    hlen = make_nv_header ( nlen, vlen, (unsigned char *) 
			&buffer[boff] );
	    boff += hlen;
	} else {
	    hlen = make_nv_header ( nlen, vlen, (unsigned char *) header );

	    status = append_params ( fcgi, buffer, sizeof(buffer), &boff,
		header, hlen );
        if ( (status &1) == 0 ) break;
	}
	status = append_params ( fcgi, buffer, sizeof(buffer), &boff,
		env->ptr[i], nlen );
        if ( (status &1) == 0 ) break;
	status = append_params ( fcgi, buffer, sizeof(buffer), &boff,
		&env->ptr[i][nlen+1], vlen );
        if ( (status &1) == 0 ) break;
    }    
    if ( (status&1) && boff > 0 ) {
	status = fcgi_send ( fcgi, FCGI_PARAMS, buffer, boff );
    }
    if ( (status&1) ) {
	/*
	 * Terminate stream with a zero-length record.
	 */
	status = fcgi_send ( fcgi, FCGI_PARAMS, "", 0 );
    }
    return status;
}
/*****************************************************************************/
/* Send request content as STDIN stream.
 */
static int send_input ( fcgi_handle fcgi, mst_link_t link, int content_length )
{
    int status, length;
    char buffer[256];

    while ( content_length > 0 ) {
	status = mst_write ( link, "<DNETINPUT>", 11, &length );
	if ( (status&1) == 0 ) return status;
	status = mst_read ( link, buffer, sizeof(buffer), &length );
	if ( (status&1) == 0 ) return status;
	if ( length > content_length ) length = content_length;
	content_length = content_length - length;

	status = fcgi_send  ( fcgi, FCGI_STDIN, buffer, length );
	if ( (status&1) == 0 ) return status;
    }
    status = fcgi_send ( fcgi, FCGI_STDIN, "", 0 );	/* end stream */
    return status;
}
