/* --------------------------------------- Copyright 1996, The Meme Factory Inc. The Text of this program may be freely distributed so long as this copyright notice is included in it's entirety and any changes from the original text of the program are clearly noted. The inclusion of a diff between the original and the modified version with the distribution satisfies this requirement. This program may be freely used by non profit organizations and individuals personal use, all others are licensed to use this program upon payment of $10 U.S. to: The Meme Factory, 5512 S. Woodlawn Ave., Chicago, IL 60637. The user assumes all liability and risk associated with the use of this program. --------------------------------------------- Comments and corrections may be addressed to the author Karl O. Pinc, . C cgi program to display animation. Arguments: The first is an environment variable, the remainder are supplied on the command line separated by plus signs. All arguments must be supplied. Directory The frames are in. As supplied by the caller is appended to the path of the called cgi program in the invoking URL. This path is translated by the server in the same way it translates all paths in URLs, so usually this will be relative to the DocumentRoot HTTPD server configuration parameter although ~ may be able to be used to reference relative to a user's home directory. Referenced in this code through the PATH_TRANSLATED environment variable, which supplies relative to the default directory of the cgi process. Starting Frame number. Ending Frame number. Prefix Which appears before the frame number in each filename. May not contain the ../ sequence of characters. Suffix Which appears after the frame number in each filename. May not contain the ../ sequence of characters. Delay After display of each frame, In microseconds. The actual accuracy of this is the same as that of the setitimer(2) signal. At the time of this writing it is accurate to 10 milliseconds. At least this long of an interval will occur between the output of the beginning of each frame. FrameMType Mime type/subtype of the frames. Final Path to final frame (relative to Directory argument.) Must be a relative pathname (no leading /.) Null string when there is no final frame. May not contain the ../ sequence of characters. FinalMType Mime type/subtype of the final frame. Uses server push. Errors result in termination of the animation and generation of a message. I was careful to encode mime stuff with carriage return/line feed as per RFC1341. I was careful to check for buffer overrun and consequent security holes when calling sprintf to substitute a caller supplied string into a fixed length buffer. Example: The pieces are: http:/cgi-bin/pushprog Run the pushprog cgi program on the current machine. /~rima/tc/gifs Directory: /~rima/tc/gifs ? Start of command line arguments. 1 Starting: 1 (with 1st frame) + Space (occurs between arguments) (Will be left out from now on.) 10 Ending: 10 (Display through 10th frame) Prefix: none .gif Suffix: .gif (Files are named N.gif, N is frame #) 2500000 Delay: 2.5 seconds between frames image/gif FrameMType: image/gif (Frames are gif images) 1.gif Final: 1.gif (Redisplay /~rima/tc/gifs/1.gif as last frame) image/gif FinalMType: image/gif (Last frame is a gif too.) Bugs: We could allow the "../" substring in Prefix, Suffix, and Final, so long as we check that we don't back up past DOCUMENT_ROOT. Relies on the server to deliver the frames in a timely manner. Apparently, this does not always happen. The trouble seems to be that the server waits for some buffer to fill. Perhaps this can be avoided in the NCSA HTTPD server by compiling it with CGI_SSI_HACK defined. Does something weird when outputting a file containing the MULTIPART_DELIMITER string. Doesn't check for error on munmap call due to lack of documented return code. Failure on write of mime multipart header or delimiter may not produce a clean error message. The error messages don't do squat when used in a type html clause because the browser is expecting a image and I'm returning ascii text (although with the correct mime type.) */ #include #include #include #include #include #include #include #include #include #include /* Forward function declarations. */ void send_frame(int fvar, char * mime_type) ; void frame_error(char * mesg) ; void legal_path(char * path, char * argname) ; void server_error(char * mesg) ; void delay_till_alarm(int microseconds) ; void alarm_handler(int signal) ; /* The largest string we can handle. */ #define MULTIPART_DELIMITER "--ThisRandomString" #define CONTENT_TYPE "Content-type: %s\r\n\r\n" /* blank line follows header */ #define MAX_SSIZE 500 char error_mesg[MAX_SSIZE] ; char buf[MAX_SSIZE] ; int buf_len ; /* number of chars in previous */ char rep_multipart_delimiter[sizeof(MULTIPART_DELIMITER) + 10] ; int rmd_len ; /* number of chars in previous */ char final_multipart_delimiter[sizeof(MULTIPART_DELIMITER) + 10] ; int fmd_len ; /* number of chars in previous */ int alarm_received = 1 ; /* A boolean, used in the timer system. */ /* Initialize so there's no pause before the first frame. */ int main(int argc, char ** argv) { #define ARGS_EXPECTED 9 char * directory ; int starting ; int ending ; char * prefix ; char * suffix ; int delay ; char * frame_mtype ; char * final ; char * final_mtype ; char filepath[MAX_SSIZE] ; struct itimerval timerval ; FILE * stdout_stream ; int step ; int display_frames ; int frame_no ; int fd ; char * final_path ; int directory_size ; char * filepath_format = "%s/%s%d%s" ; #define MIME_HEADER "Content-type: multipart/x-mixed-replace;boundary=ThisRandomString\r\n" /* Initialize globals. */ rmd_len = sprintf(rep_multipart_delimiter, "\r\n%s\r\n", MULTIPART_DELIMITER) ; fmd_len = sprintf(final_multipart_delimiter, "\r\n%s--\r\n", MULTIPART_DELIMITER) ; /* Get all the arguments. */ if ((directory = getenv("PATH_TRANSLATED")) == NULL) server_error("No PATH_TRANSLATED.") ; directory_size = strlen(directory) ; if (argc != ARGS_EXPECTED) { sprintf(error_mesg, "Wrong number of arguments to cgi program. \ Expected %d, got: %d", ARGS_EXPECTED, argc) ; server_error(error_mesg) ; } starting = atoi(argv[1]) ; ending = atoi(argv[2]) ; prefix = argv[3] ; suffix = argv[4] ; delay = atoi(argv[5]) ; frame_mtype = argv[6] ; final = argv[7] ; final_mtype = argv[8] ; /* Check the arguments. */ /* The supplied paths cannot reference "../" */ legal_path(prefix, "Prefix") ; legal_path(suffix, "Suffix") ; legal_path(final, "Final") ; /* See that the supplied prefix and suffix won't overflow our buffers. */ if (strlen(filepath_format) + directory_size + strlen(prefix) + 10 + /* for decimal representation of frame_no */ strlen(suffix) > sizeof(filepath) - 1 /* for trailing \0 */ ) server_error("The filepath variable has overrun it's storage because the prefix and suffix are too long.") ; /* Check the length of the mime content type. (For send_frame.) */ if (strlen(CONTENT_TYPE) - 2 + /* for the %s */ strlen(frame_mtype) > sizeof(buf) - 1 /* for trailing \0 */ ) server_error("Buffer overrun, mime content type too long") ; /* See that the timer is not being used. */ if (getitimer(ITIMER_REAL, &timerval) || timerval.it_value.tv_sec || timerval.it_value.tv_usec) server_error("Real time timer already in use.") ; /* Get a stream pointer for stdout so we can flush it. */ stdout_stream = fdopen(STDOUT_FILENO, "a" ) ; if (stdout_stream == NULL) server_error("Unable to associate a stream with stdout.") ; if (strlen(final) != 0) { /* Make the Final argument relative to PATH_TRANSLATED, instead of cgi-bin. Construct the full pathname. (final_path) */ int final_path_size ; int final_offset ; /* Allocate memory for the full pathname. */ final_path_size = directory_size + strlen(final) ; if (directory[directory_size] == '/') final_path_size += 1 ; final_path = (char *) malloc(final_path_size + 1) ; /* Construct full pathname. */ strcpy(final_path, directory) ; final_offset = directory_size ; if (directory[directory_size] != '/') { /* We need to add a / to delimit the end of the PATH_TRANSLATED. */ final_path[directory_size] = '/' ; final_offset += 1 ; } strcpy(final_path + final_offset, final) ; } /* Output the mime multipart header. */ if (write(STDOUT_FILENO, MIME_HEADER, strlen(MIME_HEADER)) == -1) /* This may produce trash if the header is partially output. */ server_error("Cannot output mime multipart header.") ; /* Start the frame display process with a multipart delimiter. */ if (write(STDOUT_FILENO, rep_multipart_delimiter, rmd_len) == -1) frame_error("Cannot write multipart delimiter") ; /* Go through each frame, displaying it. */ display_frames = abs(ending - starting) + 1 ; if ( starting <= ending ) step = 1 ; else step = -1 ; frame_no = starting ; while(1) { /* Open the file. */ sprintf(filepath, filepath_format, directory, prefix, frame_no, suffix ) ; fd = open(filepath, O_RDONLY) ; if (fd == -1) { sprintf(error_mesg, "Unable to open the graphic file for frame: %d", frame_no ) ; frame_error(error_mesg) ; } /* Wait for the alarm, the reset it. */ delay_till_alarm (delay) ; /* Output the file. */ send_frame(fd, frame_mtype) ; /* Close the file. */ if (close(fd) == -1) { sprintf(error_mesg, "Unable to close the graphic file for frame: %d", frame_no ); frame_error(error_mesg) ; } /* Loop exit test. */ if (display_frames <= 1) break ; /* Output the mime-multipart delimiter to finish the frame. */ if (write(STDOUT_FILENO, rep_multipart_delimiter, rmd_len) == -1) frame_error("Cannot write multipart delimiter") ; /* Make sure that the output is sent. */ if (fflush(stdout_stream)) frame_error("Cannot flush stdout.") ; /* Prepare for next iteration. */ --display_frames ; frame_no = frame_no + step ; } ; if (strlen(final) != 0) { /* There is a final frame. */ /* Output the mime-multipart delimiter to finish the previous frame. */ if (write(STDOUT_FILENO, rep_multipart_delimiter, rmd_len) == -1) frame_error("Cannot write multipart delimiter") ; /* Open the last frame. */ fd = open(final_path, O_RDONLY) ; if (fd == -1) frame_error("Unable to open the final frame.") ; /* Clean up after what we did to setup the final frame. */ free(final_path) ; /* Wait for the alarm, then reset it. */ delay_till_alarm(delay) ; /* Output the last frame. */ send_frame(fd, final_mtype) ; } /* Finish frame outputting entirely. */ if (write(STDOUT_FILENO, final_multipart_delimiter, fmd_len) == -1) server_error("Cannot write final multipart delimiter.") ; /* Make sure that the output is sent. */ if (fflush(stdout_stream)) frame_error("Cannot flush stdout.") ; /* Cleanup The following is cleaned up by the OS when we exit. Close the file holding the final frame. Turn off real time timer. Reset signal handler to the default value. */ exit(EXIT_SUCCESS) ; } ; void send_frame(int fvar, char * frame_mtype) { /* Sends a frame from the disk, the disk file is memory mapped. Input: fvar The file variable for the file. frame_mtype The mime type/subtype of the frame. */ struct stat file_status ; caddr_t memory_map ; /* Memory map the file. */ if (fstat(fvar, &file_status)) frame_error("send_frame: fstat called failed.") ; memory_map = mmap(NULL, file_status.st_size, PROT_READ, MAP_PRIVATE | MAP_FILE, fvar, 0) ; if (memory_map == (caddr_t) -1) frame_error("send_frame: mmap call failed.") ; /* Build a mime content type header in the buf global. */ buf_len = sprintf(buf, CONTENT_TYPE, frame_mtype) ; /* Output the content type of the frame. */ if (write(STDOUT_FILENO, buf, buf_len) == -1) { /* Cannot call frame error because we may have already sent the content type. */ write(STDOUT_FILENO, final_multipart_delimiter, fmd_len) ; exit(-1) ; } /* Output the contents of the file. */ if(write(STDOUT_FILENO, (char *) memory_map, file_status.st_size) == -1) { /* Can't call frame error because we have already sent out the Mime content type. Just finish sending the frame and stop with an error exit. */ write(STDOUT_FILENO, final_multipart_delimiter, fmd_len) ; exit(-1) ; } munmap(memory_map, file_status.st_size) ; return ; } ; void legal_path(char * path, char * argname) { /* Check to be sure that the path does not contain anything that would back it out of SERVER_ROOT. Right now this just checks for a ../ substring. */ if (strstr(path, "../") != NULL) { char * error_string ; error_string = "The '%s' argument's value (%s) contains the illegal '../' substring" ; if (strlen(error_string) - 2 - 2 + strlen(argname) + strlen(path) > sizeof(error_mesg) - 1) /* The error message is too big. */ sprintf(error_mesg, "The '%s' argument's value contains the illegal '../' substring.", argname) ; else sprintf(error_mesg, error_string, argname, path) ; server_error(error_mesg) ; } return ; } ; void frame_error(char * mesg) { /* Handle a fatal error while outputting frames. */ printf("Content_type: text/html\r\n\r\n") ; printf("

Animation Server Error

") ; printf("This server encountered an error while attempting to display frames:") ; printf("%s", mesg) ; printf(final_multipart_delimiter) ; exit(-1) ; } void server_error(char * mesg) { /* Output a message and stop. */ printf("Content-type: text/html\r\n\r\n"); printf("

Animation Server Error

"); printf("This server encountered an error:

"); printf("%s", mesg); exit(-1); } void delay_till_alarm(int waitfor) { /* Wait until the alarm has gone off. Then reset it to go off in waitfor microseconds. Input: waitfor The number of micro seconds to wait before the alarm should go off again. */ static struct itimerval timer_control = { { /* it_interval */ 0, /* tv_sec */ 0}, /* tv_usec */ { /* it_value */ 0, /* tv_sec */ 0}} ; /* tv_usec */ static struct sigaction handler_control = { &alarm_handler, /* sa_handler */ 0, /* sa_mask */ SA_NOMASK & SA_RESTART & SA_ONESHOT} ; /* sa_flags */ /* sa_restorer */ if (waitfor == 0) return ; if (! alarm_received) pause() ; /* The alarm has gone off, turn it back on. */ timer_control.it_value.tv_usec = waitfor ; if(setitimer(ITIMER_REAL, &timer_control, NULL)) frame_error("delay_till_alarm: failed called to setitimer.") ; /* Establish a handler for the alarm. */ if (sigaction(SIGALRM, &handler_control, NULL )) frame_error("delay_till_alarm: failed call to sigaction.") ; alarm_received = 0 ; return ; } void alarm_handler(int signal) { /* Remember whether the alarm went off. */ alarm_received = 1 ; return ; }