/*
 * The http_cgi_execute routine handles the interaction of the HTTP server with
 * a scriptserver object that has selected CGI mode (tag <DNETCGI>).
 *
 * int http_cgi_execute ( session_ctx scb, io_chan link, int text_mode,
 *		string *iobuf, char **end_tag );
 *
 * scb fields referenced:
 *    scb->acc->port_attr	For port's KA capability.
 *    scb->acc->local_address
 *    scb->acc->user
 *    scb->rshhdr		Storage for HTTP response header
 *    scb->log_prefix		Logging information.
 *    scb->keepalive_count
 *    scb->keepalive_limit
 *    scb->keepalive_pending
 *    scb->cnx
 *    scb->data_bytes		If keeplive_pending set and end_tag non-null, 
 *				receives number of expected output bytes.
 *    scb->request		HTTP request and headers sent by client
 *    scb->inbound		Input stream for request content.
 *
 * Author:  David Jones
 * Date:   14-MAR-1998		Split from script_execute module.
 * Revised:18-MAR-1998		Maintain keepalive if script includes
 *				content-length header.
 * Revised:28-MAR-1998		Allow script to override server: and mime-
 *				version headers, weed out connection headers.
 * Revsied: 5-DEC-1998		Hack for text_mode==2: if buffer is exactly
 *				buffersize-2, treat as a streamed read.
 * Revised: 8-APR-2000		Make more robust check for missing content-type.
 * Revised: 14-MAY-2000		Error errorpage support.
 * Revised: 9-DEC-2000		Fix erroneous reference to uninitialized
 *				stsline variable.
 */
#include <stdio.h>
#include <stdlib.h>
#include "session.h"
#include "tserver_tcp.h"
#include "decnet_searchlist.h"
#include "errorpage.h"
#define VMS_EOF 2162

int http_log_level;			/* global variable */
extern char http_server_version[];	/* Global variable, server version */
int http_crlf_newline;			/* 0-LF newline, 1- CRLF newline */
int tlog_putlog(int, char *, ...);
int http_dns_enable;		/* GLobal variable, name lookup enable */
int http_send_error 		/* prototype for send_error function */
	( session_ctx scb, char *msg, char *buf );
int http_add_response(), http_send_response_header(), http_translate_ident();
int http_invalidate_doc_cache(), http_check_protection();
int http_script_execute();

struct io_channel_def {
    void *dptr;				/* handle for I/O routine */
    int (*read)();			/* read function */
    int (*write)();
    int (*close)();
    int (*set_time_limit)();
    int (*format_err)();
};
typedef struct io_channel_def *io_chan;
int http_script_link_close ( io_chan );
typedef int ifunc();

/**************************************************************************/
/* Special read routines for reading CGI response header lines.  If the
 * end-of-response tag (</DNETCGI>) is read, replace status with EOF
 * so that tu_readline will abort read (otherwise readline would try
 * to read more data since tag does not include a linefeed).
 *
 * In record mode (text mode) read_cgi_recmode_header is used to read the
 * header lines, it appends a newline to every record.
 */
static int read_cgi_header 
	( io_chan link, char *buffer, int bufsize, int *read )
{
    int status;
    status = (*(link->read))( link->dptr, buffer, bufsize, read );
    if ( (status&1) == 0 ) return status;
    if ( (*read == 10) && (*buffer == '<') ) {
	if ( 0 == tu_strncmp ( buffer, "</DNETCGI>", 10 ) ) status = VMS_EOF;
    }
    return status;
}
static int read_cgi_recmode_header 
	( io_chan link, char *buffer, int bufsize, int *read )
{
    int status;
    if ( bufsize <= 2 ) return read_cgi_header ( link, buffer, bufsize, read );
    status = (*(link->read))( link->dptr, buffer, bufsize-2, read );
    if ( (status&1) == 0 ) return status;
    if ( (*read == 10) && (*buffer == '<') ) {
	if ( 0 == tu_strncmp ( buffer, "</DNETCGI>", 10 ) ) status = VMS_EOF;
    }
    buffer[*read] = '\r'; 
    *read += 1;
    buffer[*read] = '\n';	/* Add implied CR/LF */
    *read += 1;
    return status;
}
static int read_cgi_recmode2_header 
	( io_chan link, char *buffer, int bufsize, int *read )
{
    int status;
    if ( bufsize <= 2 ) return read_cgi_header ( link, buffer, bufsize, read );
    status = (*(link->read))( link->dptr, buffer, bufsize-2, read );
    if ( (status&1) == 0 ) return status;
    if ( (*read == 10) && (*buffer == '<') ) {
	if ( 0 == tu_strncmp ( buffer, "</DNETCGI>", 10 ) ) status = VMS_EOF;
    }
    if ( *read == (bufsize-2) ) return status;	/* buffer full */
    buffer[*read] = '\r'; 
    *read += 1;
    buffer[*read] = '\n';	/* Add implied CR/LF */
    *read += 1;
    return status;
}
/*
 * Convert decimal text string to unsigned integer.
 */
static int decode_int ( char *string )
{
    int value;
    for ( value = 0; *string; string++ ) {
	if ( *string == ' ' || *string == '\t' ) continue;
	if ( *string >= '0' && *string <= '9' ) {
	    value = (value * 10) + ((*string) - '0');
	    if ( value >= 2000000000 ) break;
	} else break;
    }
    return value;
}
/*
 * Generate URL relative to current port number and protocol.
 */
static int reflex_url ( session_ctx scb, char *path, char *buffer, 
	int bufsize )
{
    char *scheme;
    int explicit_port, i, l_len, h_len;
    /*
     * Determine protocol used by port and if explicit port number needed.
     */
    scheme = scb->acc->port_attr;
    if ( *scheme == '+' ) scheme++;		/* ka capability */
    if ( *scheme == ':' ) {scheme++; explicit_port = 1;} else explicit_port=0;
    /*
     * Verify scheme://host[:port] will fit in buffer and build.
     */
    i = tu_strlen ( scheme );
    l_len = tu_strlen ( path );
    h_len = tu_strlen ( scb->acc->local_address );
    if ( (i + l_len + h_len + (explicit_port*12)) > bufsize ) return 0;

    tu_strcpy ( buffer, scheme );
    tu_strcpy ( &buffer[i], "://" ); i += 3;
    tu_strcpy ( &buffer[i], scb->acc->local_address ); i += h_len;
    if ( explicit_port ) {
	/*
	 * Append :nnn to host string.
	 */
	int status, local_port, remote_port, remote_addr;
	status = ts_tcp_info ( &local_port, &remote_port, 
		(unsigned int *) &remote_addr );
	buffer[i++] = ':';
	tu_strint ( local_port, &buffer[i] );
	i = tu_strlen ( buffer );
    }
    tu_strnzcpy ( &buffer[i], path, bufsize-i-1 );	/* truncates */
    return 1;
}
/**************************************************************************/
/*
 * Handle processing of CGI mode script execution.  Read header lines from
 * from script and take action.  Note that CGI input is a stream, records
 * are delimited by <CR><LF> or <LF>.
 */
int http_cgi_execute 
	( session_ctx scb,		/* session control block */
	  io_chan link,			/* Decnet connection to script task */
	  int text_mode,		/* true if implied CRLF */
	  string *iobuf,		/* Scratch data buffer for I/O */
	  char **end_tag )
{
    int status, length, i, lines, mode, tcnt, bufsize, http_parse_elements();
    int http_send_document(), http_translate_ident();
    int sts_offset, has_content_length, content_length, need_server_label;
    int need_mime_label;
    struct acc_info_struct acc, *orig_acc;
    struct tu_streambuf input, *orig_input;
    char *buffer, label[80], location[1024], content[256], stsline[256];
    /*
     * Use utility routine to parse CGI header data returned by script.
     */
    *end_tag = (char *) 0;			/* No transfer_output */
    has_content_length = 0;			/* content-length: not seen */
    bufsize = iobuf->l; buffer = iobuf->s;
    tu_init_stream ( link, text_mode ? 
	((text_mode==2) ? read_cgi_recmode2_header : read_cgi_recmode_header) 
	: read_cgi_header, &input );
    stsline[0] = '\0';
    sts_offset = scb->rsphdr->l;
    need_server_label = 1;
    need_mime_label = 1;
			       /*     12345678901234567890123456789012345 */
    http_add_response ( scb->rsphdr, "200 CGI script output data follows.",
		0 );
    content[0] = '\0';

    for ( mode=0; ; ) {
	/*
	 * Read next header line, end loop when null line read.
	 */
        status = tu_read_line ( &input, buffer, bufsize-1, &length );
	if ( (status&1) == 0 ) tlog_putlog ( 0,
	    "!AZ CGI readline error: !SL!/", scb->log_prefix, status );
	if ( (status&1) == 0 ) return status;
	buffer[length] = '\0';		/* Ensure we have terminated string */
	if ( http_log_level > 4 )
	    tlog_putlog ( 4, "!AZ CGI header: '!AZ' (!SL)!/",
		scb->log_prefix, buffer, length );
	if ( length == 0 ) break;	/* header terminator */
	/*
	 * parse first element and make sure there are at least 2.
	 */
	for ( i = 0; (i < length) && (i < sizeof(label)); i++ ) {
	    label[i] = buffer[i];
	    if ( label[i] == ':' ) break;
        }
	if ( (i >= length) || (i >= sizeof(label)) ) {
	    status = http_send_error ( scb,
		"500 protocol error in CGI script", "Invalid header seen" );
	    return status;
	}
	label[i+1] = '\0';
	for ( i++; (buffer[i] == ' ') || (buffer[i] == '\t'); i++ );
	if ( buffer[i] == '\0' ) { 	/* missing argument */
	    status = http_send_error ( scb,
		"500 protocol error in CGI script", "Missing header value" );
	    return status;
	}
	/*
	 * Interpret label to see if it is CGI directive.
	 */
	tu_strupcase ( label, label );
	if ( tu_strncmp ( label, "LOCATION:", 9 ) == 0 ) {
	    /*
	     * Save next element in location array.
	     */
	    tu_strnzcpy ( location, &buffer[i], sizeof(location)-1 );
	    mode = 1;
	    /*
	     * If script included an explicit status line with redirct code,
	     * then just treat as normal extra header.
	     */
	    if ( stsline[0] == '3' && stsline[1] == '0' &&
		(stsline[2] == '1' || stsline[2] == '2') ) {
		mode = 3;
	       status = http_add_response ( scb->rsphdr, buffer, 0 );
	    }
	} else if ( tu_strncmp ( label, "CONTENT-TYPE:", 13) == 0 ) {
	    /*
	     * Save next element in content header line.
	     */
	    tu_strcpy ( content, "Content-type: " );
	    tu_strnzcpy ( &content[14], &buffer[i], sizeof(content)-14 );
	    if ( mode < 2 ) mode = 2;
	} else if ( tu_strncmp ( label, "STATUS:", 13) == 0 ) {
	    /*
	     * Override status line to return.
	     */
	    tu_strnzcpy ( stsline, &buffer[i], sizeof(stsline)-1 );
	} else if ( tu_strncmp ( label, "CONTENT-LENGTH:", 15) == 0 ) {
	    /*
	     * Note that content-length present and decode.
	     */
	    has_content_length = 1;
	    content_length = decode_int ( &buffer[15] );
	    mode = 3;			/* flag that rsphdr has extra stuff */
	    status = http_add_response ( scb->rsphdr, buffer, 0 );
	} else if ( tu_strncmp ( label, "DATE:", 6 ) == 0 ) {
	    /* Suppress passing on date: header, added by send_response */
	} else if ( tu_strncmp ( label, "CONNECTION:", 12 ) != 0 ) {
	    /*
	     * Additional headers after a content-type header are saved,
	     * except for "connection:" headers, which server must coordinate.
	     */
	    mode = 3;
	    status = http_add_response ( scb->rsphdr, buffer, 0 );
	    /*
	     * Check if header label is one we normally generate and set
	     * flag to suppress generating it.
	     */
	    if ( tu_strncmp ( label, "SERVER:", 8 ) == 0 ) {
	        need_server_label = 0;
	    } else if ( tu_strncmp ( label, "MIME-VERSION:", 15 ) == 0 ) {
	        need_mime_label = 0;
	    }
	}
    }
    /*
     * Getting to this point means we have a valid header.  Continue
     * processing based upon mode (which type of header).
     */
    if ( http_log_level > 4 ) tlog_putlog ( 4, 
	"!AZ CGI mode: !SL,  stm buf: !SL/!SL!/", scb->log_prefix,
		 mode, input.used, input.filled );
    if ( (mode == 0) || ((mode>=2) && !content[0]) ) {
	tu_strcpy ( stsline, "5003 Script protocol error, no content-type" );
	tu_strcpy ( content,"content-type: text/plain" );
	scb->rsphdr->l = sts_offset;	/* reset */
	mode = 2;
    }
    if ( (stsline[0]) && (stsline[3] != ' ') && (stsline[3]) ) {
	/*
	 * 4th position in status line is not a space, assume caller is
	 * using a 4-digit status code to invoke a error page where 4th
	 * digit is code indicating which error page.  Convert the extra 
	 * digit to a numeric code and remove from the status line.
	 */
	int i, code;
	code = stsline[3] - '0';
	for ( i = 4; stsline[i]; i++ ) stsline[i-1] = stsline[i];
	stsline[i-1] = '\0';
	if ( http_log_level > 4 ) tlog_putlog ( 4, 
	   "!AZ CGI errorpage request, code !SL!/", 
		scb->log_prefix, code );
	if ( (code < 0) || (code > 9) ) code = 3;	/* protocol error */
	/*
	 * See if an error page defined for this code and redirect to the
	 * error page if so, otherwise ignore the error page request.
	 */
	if ( http_epage.def[code].type != 0 ) {
	    char ecode[8];
	    tu_strnzcpy ( ecode, stsline, 4 );
	    tu_strnzcpy ( location, scb->request[1].s, 255 );
	    if ( scb->request[1].l < 255 ) location[scb->request[1].l] = '\0';
	    scb->rsphdr->l = sts_offset;	/* reset the header position*/
	    status = http_process_error ( scb, &http_epage.def[code], ecode,
		location, &location[256], sizeof(location)-256, "", iobuf );
	    return status;
	}
    }
    if ( mode >= 2 ) {
	/*
	 * Script will return data of indicated contents type.  Build
	 * header with indicated content type and send to client.
	 */
	if ( (mode == 2) && stsline[0] ) {
	    /* overwrite full HTTP status line. */
	    scb->rsphdr->l = sts_offset;
    	    status = http_add_response ( scb->rsphdr, stsline, 0 );
	} else if ( stsline[0] ) {
	    char *stsptr = &scb->rsphdr->s[sts_offset];
	    /* Truncate or pad stsline contents into 35 character slot. */
	    for (i=0; (i < 35) && stsline[i]; i++) stsptr[i] = stsline[i];
	    while ( i < 35 ) stsptr[i++] = ' ';
	}
	if ( need_mime_label )	status = http_add_response ( 
		scb->rsphdr, "MIME-version: 1.0", 0 );
	if ( need_server_label ) {
	    tu_strcpy ( stsline, "Server: OSU/" );
	    tu_strcpy ( &stsline[12], http_server_version );
    	    status = http_add_response ( scb->rsphdr, stsline, 0 );
        }
	status = http_add_response ( scb->rsphdr, content, 0 );
	if ( has_content_length && 
		(scb->keepalive_count < scb->keepalive_limit) ) {
	    http_add_response ( scb->rsphdr, "Connection: Keep-Alive", 0 );
	    scb->keepalive_pending = 1;
	    scb->data_bytes = content_length;	/* number expected */
	}
	status = http_send_response_header ( scb );
	if ( (status&1) == 0 ) 
	    tlog_putlog ( 5, "!AZ Error sending CGI header to client: !SL!/",
			scb->log_prefix, status);
	if ( (status&1) == 0 ) return status;
	/*
	 * Dump anything left in stream buffer to client.
	 */
	if ( input.used && (input.used < input.filled) ) {
	    status = tu_read_raw ( &input, buffer, bufsize, &length );
	    if ( (status&1) == 1 ) status = ts_tcp_write 
			( scb->cnx, buffer, length );
	    else if ( status == VMS_EOF ) return 1;

	    if ( (status&1) == 0 ) {
	        tlog_putlog ( 0, "!AZ Error flushing readline buffer: !SL !SL!/",
			scb->log_prefix, status, bufsize);
	        input.used = input.filled; 
	        length = 0; 
	    }
	    scb->data_bytes += length;
	}
        /*
	 * Transfer rest of task's output in raw mode.
	 */
	*end_tag = "</DNETCGI>";
	return status;
    }
    if ( mode == 1 ) {
	/*
	 * Script is supplying a relocation.  Determine if it is relative
	 * or absolute.  Location is absolute if it contains a scheme (colon),
	 * a node (//), or a tag (#).
	 */
	int i, http_parse_url();
	char *scheme, *node, *ident, *arg, url_store[1024];

	scb->rsphdr->l = sts_offset;		 /* reset output */
	status = http_parse_url ( location, url_store, &scheme, &node,
		&ident, &arg );
	/*printf("url parse: '%s' '%s' '%s' '%s'\n", scheme, node,ident,arg);*/
	if ( *scheme || *node || (*arg == '#') ) {
	    /*
	     * Send redirection to be handled by the client.
	     */
	    if ( !stsline[0] ) tu_strcpy ( stsline, "302 OK, CGI redirect" );
    	    status = http_add_response ( scb->rsphdr, stsline, 0 );
	    tu_strcpy ( buffer, "Location: " );
	    if ( tu_strncmp ( location, "*://*:*", 7 ) == 0 ) {
		/* Special hack, substitute current information */
		status = reflex_url (scb, &location[7], &buffer[10], bufsize);
		if ( (status&1) == 0 ) return status;
	    } else {
	       tu_strcpy ( &buffer[10], location );
	    } 
	    status = http_add_response ( scb->rsphdr, buffer, 1 );
	    status = http_send_response_header ( scb );
	    if ( (status&1) == 0 ) return status;
	    scb->data_bytes += scb->rsphdr->l;
            /*
	     * Transfer rest of task's output in raw mode.
	     */
	    *end_tag = "</DNETCGI>";
	    return status;
	} else {
	    /*
	     * Relative file, recursively call send-document.
	     */
	    string orig_ident;
	    char munged_ident[512];
            /*
	     * Discard rest of data (up to 10K) sent by script and close 
	     * connection to free up bytlm for script we are chaining to.
	     * (Also frees DECnet resources).
	     */
	    for ( i = 0; i < 10000; i+= length ) {
		status = (*link->read)(link->dptr, iobuf->s,
			iobuf->l > 4096 ? 4096 : iobuf->l, &length );
		if ( (1&status) == 0 ) break;
		if ( (length == 10) && ( 0 ==
		    tu_strncmp(iobuf->s,"</DNETCGI>",10)) ) break;
	    }
	    if ( link->close ) http_script_link_close ( link );
	    link->close = (ifunc *) 0;
	    /*
	     * Translate ident by rules file, mainly to do access check.
	     */
	    if ( http_log_level > 4 ) tlog_putlog ( 5,
		"!AZ CGI Recursively fetching relative ident: !AZ!/", 
		scb->log_prefix, ident );

	    acc.uic = 0;
	    acc.cache_allowed = 1;
	    acc.prot_file = "";
	    acc.rem_user[0] = '\0';
	    acc.retention = 0;
	    acc.fopt.etype = 0;
	    tu_strncpy(acc.user,scb->acc->user, sizeof(acc.user)-1);
	    acc.local_address = scb->acc->local_address;
	    acc.port_attr = scb->acc->port_attr;
            status = http_translate_ident ( ident, munged_ident, 
			sizeof(munged_ident), &acc );
	    if ( !status ) {
	        /*
	         * Error status means ident matched a 'fail' record in the
	         * the rule database.
	         */
	        status = http_send_error ( scb, "403 Forbidden",
		    "Access to object is _ruled_ out." );
	        return status;
	    } else if ( status == 2 ) {
		/*
		 * Rule file redirect.
		 */
		tu_strcpy ( stsline, "302 OK, CGI recurred redirect" );
		status = http_add_response ( scb->rsphdr, stsline, 0 );
		tu_strcpy ( buffer, "Location: " );
		tu_strnzcpy ( &buffer[10], ident, bufsize-11 );
		status = http_add_response ( scb->rsphdr, buffer, 1 );
		status = http_send_response_header ( scb );
		if ( (status&1) == 0 ) return status;
		scb->data_bytes += scb->rsphdr->l;
		return status;
	    }
	    /*
	     * Save original request ident and re-call either send_document
	     * or script_execute with the re-directed one.
	     */
	    orig_ident = scb->request[1];
	    orig_input = scb->inbound;
	    orig_acc = scb->acc;
	    scb->inbound = &input;
	    scb->request[1].l = tu_strlen(ident); scb->request[1].s = ident;
	    scb->rsphdr->l = sts_offset;
	    scb->acc = &acc;
	    if ( status == 3 ) {
		status = http_script_execute
			( scb, "HTBIN", munged_ident, arg, iobuf );
	    } else {
		status = http_send_document 
			( scb, ident, munged_ident, arg, iobuf );
	    }
	    scb->request[1] = orig_ident;
	    scb->inbound = orig_input;
	    scb->acc = orig_acc;
	    
	    return status;
	}
    }
    return 1;
}
