/* * 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. */ #include #include #include #include #include #include #include #include #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("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\nRange: 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; }