/*
 * This program is run as a presentation script to implement the byterange
 * extension to the HTTP protocol.  This script only works on binary files
 * (files for which seek-to-byte operations are valid).
 *
 * To install, place the executable in the www_system directory
 * and add a line of the form "presentation {content-type} byterange",
 * where {content-type} is a content-type defined by a suffix rule
 * (e.g. application/pdf).
 *
 * The program assumes that cgilib returns the content-type that triggered
 * the presentation script in the SCRIPT_PATH variable and that script_name
 * is the translated path of the target file.
 *
 * Author:	David Jones
 * Date:	28-DEC-1995
 * Revised:	 2-MAY-1996	Support multiple ranges.
 * Revsied:	12-FEB-1997	Fix bug in open call.
 * Revised:	4-JUL-2001	Change 'range:' to 'content-range'.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unixio.h>
#include <file.h>
#include <stat.h>
#include <ctype.h>
#include <string.h>
#include <signal.h>

#include "cgilib.h"
#include "scriptlib.h"

static int is_since ( stat_t *, char * ); int cgi_show_env();
char *tf_format_time ( unsigned long bintim, char *buffer );
struct byte_range { int start, finish; };
static parse_ranges ( char *spec, int size, struct byte_range *range );
/***************************************************************************/
/* Main program, arguments:
 *    argv[0]  method
 *    argv[1]  translated path
 *    argv[2]  protocol
 */
int main ( int argc, char **argv )
{
    char *source_file, *content_type, *if_modified, *unless_modified;
    char *range_spec;
    int status, src, range_count;
    stat_t src_stat;
    char last_modified[100];		/* for header */
    char buffer[4096];
    struct byte_range range[256];
    /*
     * Setup CGI environment and verify that we were invoked as a presentation
     * script (script path can't start with '/'), which lets us retrieve
     * the target content type (script_path).
     */
    status = cgi_init ( argc, argv );
    if ( (status&1) == 0 ) exit ( status );

    content_type = cgi_info ( "SCRIPT_PATH" );
    if ( !content_type ) content_type = "/";
    if ( *content_type  == '/' ) {
	cgi_printf("content-type: text/plain\nstatus: 400 bad script\n\n");
	cgi_printf("Invalid invocation of byterange script\n");
	exit(1);
    }
#ifdef DEBUG
cgi_show_env ( printf );
#endif
    /*
     * Attempt to open file and get its attributes (date,size).  When invoked 
     * as a presentation script variable script_name is the target file.
     */
    source_file = cgi_info ( "SCRIPT_NAME" );
    if ( !source_file ) {
	cgi_printf("content-type: text/plain\nstatus: 500 internal error\n\n");
	cgi_printf("CGI error: missing script_name variable\n");
	exit(1);
    }
    src = open ( source_file, O_RDONLY, 0777, "ctx=stm" );
    if ( src < 0 ) {
	cgi_printf("content-type: text/plain\nstatus: 404 open error\n\n");
	cgi_printf("Error opening file\n");
	exit(1);
    }
    if ( fstat ( src, &src_stat ) != 0 ) {
	cgi_printf("content-type: text/plain\nstatus: 500 stat error\n\n");
	cgi_printf("Error getting file attributes\n");
	exit(1);
    }
    tf_format_time ( src_stat.st_mtime, last_modified );
    /*
     * Process HEAD requests.
     */
    if ( strcmp(argv[1],"HEAD") == 0 ) {
	close (src);
	cgi_printf("content-type: %s\nAccept-ranges: bytes\n", content_type );
	cgi_printf("Allow-ranges: bytes\nLast-Modified: %s\n", last_modified );
	cgi_printf("Content-Length: %d\n\n", src_stat.st_size );
	return 1;
    } else if ( strcmp(argv[1],"GET") != 0 ) {
	cgi_printf("content-type: text/plain\nstatus: 501 unknown method\n\n");
	cgi_printf("Method '%s' is unknown/not supported\n", argv[1]);
	return 1;
    }
    /*
     * Process if-modified-since.
     */
    if_modified = cgi_info ( "HTTP_IF_MODIFIED_SINCE" );
    if ( if_modified ) if ( !is_since ( &src_stat, if_modified ) ) {
	/* Modified date on file is after that specified in if_modified header*/
	close ( src );
	cgi_printf("content-type: text/plain\nstatus: 304 cached copy OK\n" );
	cgi_printf("Last-Modified: %s\n\n", last_modified );
	exit(1);
    }
    /*
     * Check for range header and inhibit it (nullify) if unless-modified
     * test passe.
     */
    range_spec = cgi_info ( "HTTP_RANGE" );
    unless_modified = cgi_info ( "HTTP_UNLESS_MODIFIED_SINCE" );
    if ( unless_modified ) if ( is_since ( &src_stat, unless_modified ) ) {
	/*
	 * File modified after date specified in unless-modified header,
         * force range to value that forces whole file to be returned.
	 */
	printf("If-modified earlier, ignoring range '%s'\n", range_spec );
	range_spec = (char *) 0;
    }
    /*
     * Construct range to return.
     */
    range_count = parse_ranges ( range_spec, src_stat.st_size, range );
    printf("Range '%s' parsed into %d range%s\n", range_spec ? range_spec : "",
	 range_count, (range_count == 1) ? "" : "s" );
    if ( range_count <= 0 ) {
	/*
	 * Syntax error, make single range.
	 */
	range[0].start = 0;
	range[0].finish = src_stat.st_size-1;
	range_count = 1;
    }
    /*
     * return requested range(s).
     */
    if ( range_count == 1 ) {
	int i, segment, start, finish;
	/*
	 * Return single range, return whole thing as code 200 or
	 * sub-part code 206.
	 */
	cgi_printf("content-type: %s\nAccept-ranges: bytes\n", content_type );
	cgi_printf("Allow-ranges: bytes\n");
	start = range[0].start;
	finish = range[0].finish;
	if ( (start != 0) || (finish != (src_stat.st_size-1)) )  {
	    /*
	     * Change status to indicate partial data being returned.
	     */
	    cgi_printf("status: 206 returning range\n");
	    cgi_printf("Content-Range: bytes %d-%d/%d\n", 
			start, finish, src_stat.st_size );
	}
	if ( start > 0 ) lseek ( src,  start, 0 );
	/*
	 * return the data.  Use scriptlib to write raw bytes directly
	 * to the network channel (use large buffers for efficiency).
	 */
	cgi_printf("Last-Modified: %s\n", last_modified );
	cgi_printf("Content-Length: %d\n\n", finish - start + 1 );
	for ( i = start; i <=  finish; i += segment ) {
	    segment = finish - i + 1;
	    if ( segment > sizeof(buffer) ) segment = sizeof(buffer);
	    segment = read ( src, buffer, segment );
	    if ( segment <= 0 ) break;
	    status = net_link_write ( buffer, segment );
	    if ( (status&1) == 0 ) break;
	}
	exit(status);
    } else { /* ( range_count > 0 ) */
	/*
	 * return multipart content type, build multipart header.
	 */
	int i, j, segment, start, finish;
	static char *boundary_tag = "multipart-boundary";

	cgi_printf("content-type: multipart/x-byteranges; boundary=%s\n",
		boundary_tag );
	cgi_printf("status: 206 segmented content\n");
	cgi_printf("Allow-ranges: bytes\n");
	cgi_printf("Accept-ranges: bytes\nLast-Modified: %s\n", 
		last_modified );
	start = 0;
	finish = src_stat.st_size-1;
	/*
	 * Send each requested range.
	 */
	for ( j = 0; j < range_count; j++ ) {
	    /*
	     * Make sub-part header for specified range.
	     */
	    start = range[j].start;
	    finish = range[j].finish;
	    printf("range[%d]: %d-%d\n", j, start, finish );
	    cgi_printf("\n--%s\n", boundary_tag );
	    cgi_printf("Content-type: %s\nContent-Range: bytes %d-%d/%d\n\n",
		content_type, start, finish, src_stat.st_size );
	    /*
	     * Extract data and send in sizeof(buffer) sized chunks.
	     */
	    lseek ( src,  start, 0 );
	    for ( i = start; i <= finish; i += segment ) {
		segment = finish - i + 1;
	        if ( segment > sizeof(buffer) ) segment = sizeof(buffer);
		segment = read ( src, buffer, segment );
		/* printf("    segment bytes read: %d\n", segment ); */
		if ( segment <= 0 ) break;	/* read error */
		status = net_link_write ( buffer, segment );
		if ( (status&1) == 0 ) return status;	/* write error */
	    }
	}
	cgi_printf("\n--%s--\n", boundary_tag );	/* final boundary */
	exit(status);
    }
}
/*************************************************************************/
/*
 * define function that returns true iff modified time in stat structure
 * is after time specified by ascii test_date string.
 */
static int is_since ( stat_t *src, char *test_date )
{
    int i, j;
    unsigned long test_time, tf_decode_time();
    char compressed[100];
    /*
     * remove spaces from time string.
     */
    for ( i = j = 0; (i < 99) && test_date[i]; i++ ) {
	if ( !isspace ( test_date[i] ) ) compressed[j++] = test_date[i];
    }
    compressed[j] = '\0';

    test_time = tf_decode_time ( compressed );
    return (src->st_mtime > test_time) ? 1 : 0;
}
/*************************************************************************/
/* Parse byte range string and decode into integer pairs, returning number
 * of pairs parsed as function value.  Syntax errors will cause a zero value 
 * to be returned (resulting in whole file being returned).
 *
 * Range format:  bytes=[[s1]-[e1][,[s2]-[e2][,...]]]
 *
 * If start offset is omitted, assume 0; if end offset omitted, assume last
 * byte in file.
 */
static int parse_ranges ( 
	char *spec, 			/* Range specifier */
	int size, 			/* File size */
	struct byte_range *range )	/* receives start/end pairs */
{
    int i,j, start, finish, count;
    char  numbuf[16];

    if ( !spec ) return 0;		/* range spec is null */

    start = -1;
    for ( count = j = i = 0; ; i++ ) {
	/*
	 * Scan for delimiters in string and take action.
	 */
	if ( spec[i] == '=' ) {
	     if ( strncmp ( &spec[j], "bytes=", 6 ) != 0 ) return 0;
	     j = i + 1;
	} else if ( spec[i] == '-' ) {
	    if ( start >= 0 ) return 0;		/* more than 1 hyphen */
	    if ( i - j > 10 ) return 0;		/* too big */
	    if ( i - j > 0 ) {
		strncpy ( numbuf, &spec[j], i-j );
		numbuf[i-j] = '\0';
		sscanf ( numbuf, "%d", &start );
		if ( start < 0 ) return 0;		/* decode error */
	    } else start = 0;			/* -nnn form */
	    j = i + 1;
	} else if ( spec[i] == ',' || spec[i] == '\0' ) {
	    /*
	     * Bytes spec[j..i-1] have a range specification.
	     */
	    if ( count >= 256 ) return 0;	/* too many ranges */
	    if ( start < 0 ) return 0;		/* missing hyphen */
	    if ( i - j > 10 ) return 0;		/* too big */
	    if ( i - j > 0 ) {
		strncpy ( numbuf, &spec[j], i-j );
		numbuf[i-j] = '\0';
		sscanf ( numbuf, "%d", &finish );
		if ( finish < 0 ) return 0;		/* decode error */
	    } else finish = size-1;			/* -nnn form */
	    if ( finish < start ) return 0;
	    /*
	     * valid range was specified, add to list.
	     */
	    range[count].start = start;
	    range[count].finish = (finish >= size) ? size-1 : finish;
	    count++;

	    if ( !spec[i] ) break;
	    start = -1;
	    j = i + 1;
	}
    }
    return count;
}
