/*
 * Minimal HTTP server.
 * Author:  David Jones
 */
#include "pthread_1c_np.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "ctype_np.h"
#ifndef __GNUC__
#include <unixlib.h>
#endif

#include "tserver_tcp.h"	/* TCP/IP port listener */
#include "tutil.h"
#include "mini_server.h"

#define MAX_LOG_LINE 512

#define METHOD 0
#define URL 1
#define PROTOCOL 2
#define RESPONSE_LINE_LIMIT 100
#define HTTP_DEFAULT_CLIENT_LIMIT 16
#define DEFAULT_CACHE_SIZE 0
#define DEFAULT_LOG_LEVEL 1

/* forward references */
static int read_request_header ( mini_string *request, tu_stream inbound,
	char *buffer, int bufsize, int *lines );

static int mini_session ( void *ctx, 
	short port, unsigned char *rem_address, int ndx, int available );

struct port_param *load_configuration ( struct mini_server_parameters  * );

/*
 * Global (program-wide) variables.
 */
char * http_default_host;		/* Host name for re-directs */
char http_server_version[] = "mini.1";
/*
 * Define module-wide static globals.
 */
static pthread_once_t module_setup = PTHREAD_ONCE_INIT;
static pthread_mutex_t port_data;
static pthread_key_t active_port;
struct port_param {
    struct port_param *next;
    int ref_count;
    struct mini_server_parameters arg;
    pthread_attr_t client_attr;
    int (*callback) ( struct mini_request * );
};
static struct port_param *active_ports, *free_ports;

/**************************************************************************/
/* Decrement reference count on block.
 */
static void port_rundown ( void *pinfo_vp )
{
    struct port_param *pinfo;

    pinfo = (struct port_param *) pinfo_vp;
    pthread_mutex_lock ( &port_data );
    pinfo->ref_count--;
    if ( pinfo->ref_count <= 0 ) {
	pinfo->next = free_ports;
	free_ports = pinfo;
    }
    pthread_mutex_unlock ( &port_data );
}
/**************************************************************************/
/*
 * One-init for module.
 */
static logger_dispatch ( int level, char *fmt, ... )
{
    return 1;
}
static void module_init()
{
    INITIALIZE_MUTEX ( &port_data );
    CREATE_KEY ( &active_port, port_rundown );

    active_ports = (struct port_param *) 0;
    free_ports = (struct port_param *) 0;
    /*
     * Init log file callbacks.
     */
    ts_set_logging ( logger_dispatch );
}
/**************************************************************************/
/* Main program. 
 */
int mini_server_start ( 
	struct mini_server_parameters *param,
	int (*request_handler) ( struct mini_request * ),
	pthread_t *listen_thread )
{
    int status;
    pthread_attr_t client_attr;
    struct port_param *port;
    /*
     * Do one-time initialization.
     */
    pthread_once ( &module_setup, module_init );
    /*
     * allocate port structure and initalize its thread attributes.
     */
    port = load_configuration(param);
    if ( !port ) return 20;		/* bad parameter */

    INITIALIZE_THREAD_ATTR ( &port->client_attr );
    pthread_attr_setinheritsched ( &port->client_attr, PTHREAD_EXPLICIT_SCHED );
    pthread_attr_setstacksize ( 
	&port->client_attr, param->client_thread_stacksize );
    port->callback = request_handler;
    /*
     * Start listening on TCP port.
     */
    status = ts_declare_tcp_port 
	(param->port_number, port->arg.client_limit, &port->client_attr, 
	listen_thread, mini_session);

    return status;
}
/*
 * Allocate and initialize new port structure.
 */
struct port_param *load_configuration( struct mini_server_parameters *param )
{
    int status, cache_bytes, cache_refresh, cache_limit;
    struct port_param *pinfo;
    char *envval;
    /*
     * Allocate pinfo structure.
     */
    pthread_mutex_lock ( &port_data );
    if ( free_ports ) {
	pinfo = free_ports;
	free_ports = pinfo->next;
    } else {
	LOCK_C_RTL
	pinfo = (struct port_param *) malloc ( sizeof(struct port_param) );
	UNLOCK_C_RTL
    }
    if ( pinfo ) {
	pinfo->ref_count = 1;
	pinfo->next = active_ports;
	active_ports = pinfo;
    }
    pthread_mutex_unlock ( &port_data );
    if ( !pinfo ) return pinfo;
    PTHREAD_SETSPECIFIC(active_port,pinfo);
    /*
     * copy parameters.
     */
    pinfo->arg = *param;
    /*
     * Set host name global based on environment variable if rule file didn't
     * set it.
     */
    envval = getenv ( "HTTP_DEFAULT_HOST" );
    if ( (http_default_host == NULL) && envval ) {
	http_default_host = malloc ( strlen ( envval ) + 1 );
	strcpy ( http_default_host, envval );
    }
    return pinfo;
}

/***********************************************************************/
/* Main routine for handling http server connection.  This function is called 
 * as the thread init routine for tcp-ip server threads.
 */
static int mini_session ( void *ctx, 	/* TCP connection context */
	short port, 			/* Port connection received on */
	unsigned char *rem_address, 	/* Address/port of client */
	int ndx,			/* Thread index */
	int available )			/* # of contexts left on  free list */
{
    int length, status, seg_len;
    int i, lines, ts_tcp_write(), ts_tcp_read(), rem_port;
    mini_string reqline[128];
    struct port_param *pinfo;
    char reqbuf[8192];
    char *opcode, *method, *url, *protocol, *port_attributes, log_prefix[32];
    char *ts_tcp_remote_host();
    struct tu_streambuf inbound;
    struct tu_textbuf response;
    struct mini_request request;
    /*
     * Locate parameter block in global list based upon port number.
     */
    pthread_mutex_lock ( &port_data );
    for ( pinfo = active_ports; pinfo; pinfo = pinfo->next ) {
	if ( pinfo->arg.port_number == port ) {
	    pinfo->ref_count++;
	    break;
	}
    }
    pthread_mutex_unlock ( &port_data );
    if ( !pinfo ) return 20;	/* bugcheck, shouldn't happen */
    PTHREAD_SETSPECIFIC(active_port,pinfo);
    /*
     * Make prefix string for log entries so we can follow interleaved
     * entries in log file.  Log the connection and time.
     */
    tu_strcpy ( log_prefix, "TCP-" );
    tu_strint( (int)((unsigned) port), &log_prefix[4] );
    i = 4 + tu_strlen(&log_prefix[4]); log_prefix[i++] = '/';
    tu_strint(ndx, &log_prefix[i] );

    if ( pinfo->arg.log_level > 0 ) {
        rem_port = rem_address[2];
	(*pinfo->arg.logger) ( 1, 
		"!AZ connect from !UB.!UB.!UB.!UB:!UW, !%D (!SL)!/", 
		log_prefix, 
		rem_address[4], rem_address[5], rem_address[6], rem_address[7], 
		rem_port*256 + rem_address[3], 0, available );
    }
    /*
     * Initialize the input stream.
     */
    status = tu_init_stream ( ctx, ts_tcp_read, &inbound );
    /*
     * Set timeout and read first line.
     */
    ts_tcp_set_time_limit ( ctx, pinfo->arg.request_time_limit );
    do {
        /*
         * Read first line of request from network client and parse into
         * separate strings.
         */
        do {
            status = tu_read_line ( &inbound, reqbuf, sizeof(reqbuf)-21, &length );
	    if ( (status&1) == 0 ) {
	        /* Read error */
		length = 0;
	        i = mini_parse_elements ( 3, "{EOF}", reqline );
	        break;
	    }
        } while ( length == 0 );

        reqbuf[length++] = '\0';
        i = mini_parse_elements ( 3, reqbuf, reqline );
        reqline[3].l = 0;
        /*
         * Validate command and fetch extended request info.
         */
        if ( !read_request_header ( reqline, &inbound, &reqbuf[length],
		sizeof(reqbuf)-length, &lines ) ) break;

	if ( reqline[3].l < 0 )  {
	    /*
	     * Problem with request, return reqline[3].s as error message 
	     */
	    status = ts_tcp_write(ctx, reqline[3].s, tu_strlen (reqline[3].s));
	    break;
	}
	/*
	 * Build request structure and call user routine.
	 */
	request.local_port = port;
	request.tcp_ctx = ctx;
	request.remote_port = (rem_address[2]*256) + rem_address[3];
	request.remote_address = &rem_address[4];
	request.method = reqline[0].s;
	request.path = reqline[1].s;
	request.protocol = reqline[2].s;
	request.header = &reqline[3];
	request.header_overrun.l = inbound.filled - inbound.used;
	request.header_overrun.s = &inbound.data[inbound.used];

        ts_tcp_set_time_limit ( ctx, pinfo->arg.response_time_limit );
	status = (*pinfo->callback) ( &request );

    } while ( port == 0 );
    /*
     * Processing done, cleanup.
     */
    ts_tcp_close ( ctx );

    if ( pinfo->arg.log_level > 0 ) {
	(*pinfo->arg.logger) ( 1,
	"!AZ connection closed with status: !SL at !%T!/",
	log_prefix, status, 0 );
    }
    return status;
}
/****************************************************************************/
/* Read lines from client and build string descriptor for each line, storing
 * text in supplied reqbuf, until a zero-length line is read.  The record 
 * delimiter (lf or crlf) is not part of the string.  Terminate the request
 * array with a zero-length line.
 *
 * Lines are loaded starting at position 3.
 */
static int read_request_header ( mini_string *request, tu_stream inbound,
	char *reqbuf, int reqbuf_size, int *line_count ) {
    char *protocol;
    int lines;

    protocol = request[PROTOCOL].s;
    if ( request[PROTOCOL].l == 0 ) {
	/*
	 * Assume simple request.
         */
	if ( request[3].l == -9 ) {
        } else if ( tu_strncmp ( request[METHOD].s, "GET", 4 ) ) {
	    request[3].l = -1;		/* indicate error */
	    request[3].s = "599 Invalid request";	/* detail */
	} else {
	    request[3].l = 0;
	    request[3].s = "";	/* Make null header line */
	}
	lines = 4;

    } else if ( (tu_strncmp ( protocol, "HTTP/", 5 ) == 0) ||
		(tu_strncmp ( protocol, "HTRQ/", 5 ) == 0) ) {
	/*
         * Protocols other than 1 are followed by header lines.
	 * Read lines until null line read.
         */
	int length, seg_len, status;
	length = 0;
	for ( lines = seg_len = 3; seg_len > 0; lines++ ) {
	    /*
	     * make sure we have enough room to read more.
	     */
	    if ( (lines >= 126) || (length >= reqbuf_size-2) ) {
		request[lines].l = 0; 
		request[lines].s = &reqbuf[length];
		break;
	    }
	    status = tu_read_line ( inbound, &reqbuf[length], 
			reqbuf_size-length-1, &seg_len );
    	    if ( (status&1) == 0 ) {
		seg_len = 0;
		request[3].l == -9;	/* flag read error */
		*line_count = lines;
		return 0;
	    }

	    request[lines].l = seg_len;
	    request[lines].s = &reqbuf[length];
	    length += seg_len+1;
	}
	if ( (inbound->state != 0) && (seg_len == 0) ) {
	    /*
	     * Special hack to get around case where client sends CR without
	     * accompanying LF in a timely fashion for a password-protected
	     * page (client sees FIN on connection prior to sending LF, so
	     * connection is 'broken'.  If internal state of inbound stream
	     * is expecting an LF, reset state and attempt to read last line
	     * (zero length) again to force the LF to be read.
	     */
	    inbound->state = 0;		/* UGLY, members should be private */
	    status = tu_read_line ( inbound, &reqbuf[length],
		reqbuf_size-length-1, &seg_len );
	    /*
	     * All bets off if we don't get a null line.
	     */
	    if ( (status&1) == 0 ) return 0;	/* read error */
	    if ( seg_len != 0 ) return 0;	/* non-null line */
	}
	if ( request[lines].l > 0 ) request[++lines].l = 0;
    } else {
	/*
	 * Unknown protocol in 3rd element.
	 */
	request[3].l = -1;
	request[3].s = "599 protocol error in request";
    }
    *line_count = lines;
    return 1;
}
/**************************************************************************/
/* Append text followed by CRLF to response buffer.   If standard flag is true,
 *  append default headers to buffer. If standard flag is 2, use html content.
 */
int mini_add_response ( tu_text rsp, char *text, int standard )
{
    int status;
    status = tu_add_text ( rsp, text, 512 );
    status = tu_add_text ( rsp, "\r\n", 3 );
    if ( standard ) {
	status = tu_add_text ( rsp, "MIME-version: 1.0\r\n", 80 );
	status = tu_add_text ( rsp, "Server: OSU/", 80 );
	status = tu_add_text ( rsp, http_server_version, 80 );
	status = tu_add_text ( rsp, (standard == 2) ? 
		"\r\nContent-type: text/html\r\n" : 
		"\r\nContent-type: text/plain\r\n", 80 );
	status = tu_add_text (rsp, "Content-transfer-encoding: 8bit\r\n", 80);
    }
    return status;
}
/***************************************************************************/
/* Send contents of response header buffer to client.  If protocol is 1.1,
 * include date: header.
 */
int mini_send_response_header ( void *ctx, tu_text rsp)
{
    int status, length;
    char *buffer;

    buffer = rsp->s;
    if ( (rsp->l > 9) && (0 == tu_strncmp(rsp->s,"HTTP/1.", 7)) ) {
	/*
	 * Add date header.
	 */
#ifdef DO_DATE
	time_t now;
	char date_header[60];
	/*if ( 0 == futc_test_cache ( now, date_header, sizeof(date_header),
		&header_cache ) ) {
	     tu_strcpy ( date_header, "Date: " );
	    tf_format_time ( futc_current_time(&now), &date_header[6] );
	}
	tu_add_text ( rsp, date_header, sizeof(date_header) ); */
#else
	tu_add_text ( rsp, "\r\n", 3 );		/* add null line */
#endif
	length = rsp->l;
    } else if ( *buffer == '3' ) {  /* force full re-direct headers */
	/*
	 * Response buffer is old protocol, skip the version field and
	 * truncate after first line.
	 */
	/* buffer = &buffer[9]; */
	for ( length = 0; 
		buffer[length] && (buffer[length] != '\n'); length++);
	if ( buffer[length] == '\n' ) length++;
	buffer = &buffer[length];
	for ( ; buffer[length] && (buffer[length] != '\n'); length++);
	if ( buffer[length] == '\n' ) length++;
    } else {
	/*
	 * Don't send anything.
	 */
	length = 0;
    }
    if ( length > 0 ) status = ts_tcp_write ( ctx, buffer, length );
    else status = 1;
    return status;
}
/***********************************************************************/
/* 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 mini_parse_elements (
	int limit, 		    /* Max number of element to delimit */
	char *line,		    /* input line to parse */
	mini_string *elem )	    /* Output array. */
{
    int tcnt, in_token, length, i;
    char *ptr;
    /* Delimit up to three tokens */
    for ( in_token = tcnt = length = 0, ptr = line; *ptr; ptr++ ) {
	if ( in_token ) {
	    if ( isspace ( *ptr ) ) {
		/* End current token */
		*ptr = '\0';  /* terminate string */
		elem[tcnt++].l = length;
		in_token = 0;
	    }
	    else length++;
	} else {
	    if ( !isspace ( *ptr ) ) {
		/* start next token */
		if ( tcnt >= limit ) {
		    /* more tokens than expected */
		    tcnt++;
		    break;
		}
		elem[tcnt].s = ptr;
		in_token = 1;
		length = 1;
	    }
        }
    }
    /*
     * Make final adjust to element count and make remaining elment null.
     */
    if ( in_token ) { elem[tcnt++].l = length; }
    for ( i = tcnt; i < limit; i++ ) { elem[i].l = 0; elem[i].s = ptr; }
    return tcnt;
}
/***************************************************************************/
int mini_parse_url 
	( char *url, 			/* locator to parse */
	char *info,			/* Scratch area for result pts*/
	char **service,			/* Protocol (e.g. http) indicator */
	char **node,			/* Node name. */
	char **ident,			/* File specification. */
	char **arg )			/* Search argument */
	
{
    int i, state;
    char *last_slash, *p, c, arg_c;
    /*
     * Copy contents of url into info area.
     */
    *service = *node = *ident = *arg = last_slash = "";

    for ( state = i = 0; (info[i] = url[i]) != 0; ) {
	c = info[i];
	switch ( state ) {
	    case 0:
		if ( c == ':' ) {
		    info[i] = '\0';	/* terminate string */
		    *service = info;
		    state = 1;
		}
	    case 1:
		if ( c == '/' ) {
		    *ident = last_slash = &info[i];
		    state = 2;
		}
		break;
	    case 2:
		state = 4;
		if ( c == '/' ) {	/* 2 slashes in a row */
		    *node = *ident;
		    state = 3;
		}
		else if ( (c == '#') || (c == '?') ) {
		    arg_c = c;
		    info[i] = '\0';
		    *arg = &info[i+1];
		    state = 5;
		}
		break;
	    case 3:			/* find end of host spec */
		if ( c == '/' ) {
		    state = 4;
		    *ident = last_slash = &info[i];
		    for ( p = *node; p < *ident; p++ ) p[0] = p[1];
		    info[i-1] = '\0';	/* Terminate host string */
		}
		break;
	    case 4:			/* Find end of filename */
		if ( c == '/' ) last_slash = &info[i];
		else if ( (c == '#') || (c == '?') ) {
		    arg_c = c;
		    info[i] = '\0';
		    *arg = &info[i+1];
		    state = 5;
		}
	    case 5:
		break;
        }
	i++;
    }
    /*
     * Insert arg delimiter back into string.
     */
    if ( **arg ) {
	char tmp;
	for ( p = *arg; arg_c; p++ ) { tmp = *p; *p = arg_c; arg_c = tmp; }
	*p = '\0';
    }
    return 1;
}
/*
 * Scan ident and return pointer to suffix porition of filename, including
 * the period.
 * Return "/" if no filename is present and "" if no extension.
 */
char *mini_url_suffix ( char *ident )
{
    char *p, *suffix;
    suffix = "";
    for ( p = ident; *p; p++ )
	if ( *p == '.' ) suffix = p;
	else if ( *p == '/' ) suffix = "";
    /*
     * Check if last thing on line is "/".
     */
    if ( (*suffix == '\0') && (p > ident) )
	if ( *--p == '/' ) suffix = p;

    return suffix;
}
