/* tab:8 * * push_thr_code.c - per-connection child thread code for HTTP push server * * "Copyright (c) 1999 by Steven S. Lumetta." * * Permission to use, copy, modify, and distribute this software and its * documentation for any purpose, without fee, and without written agreement is * hereby granted, provided that the above copyright notice and the following * two paragraphs appear in all copies of this software. * * IN NO EVENT SHALL THE AUTHOR OR THE UNIVERSITY OF ILLINOIS BE LIABLE TO * ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL * DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, * EVEN IF THE AUTHOR AND/OR THE UNIVERSITY OF ILLINOIS HAS BEEN ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * * THE AUTHOR AND THE UNIVERSITY OF ILLINOIS SPECIFICALLY DISCLAIM ANY * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND NEITHER THE AUTHOR NOR * THE UNIVERSITY OF ILLINOIS HAS NO OBLIGATION TO PROVIDE MAINTENANCE, * SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS." * * Author: Steve Lumetta * Version: 2 * Creation Date: Wed Feb 17 22:27:03 1999 * Filename: push_thr_code.c * History: * SL 2 Wed Feb 28 11:09:28 2001 * Fixed comments on child thread cleanups. * SL 1 Wed Feb 17 22:27:03 1999 * First written. */ #ident "$Id$" /* NOTE: pthread.h must be included before errno.h for correct errno location definition. Alternatively, define _REENTRANT as a compiler flag. */ #include #include #include #include #include #include #include #include #include #include #include "local_assert.h" #include "exit_codes.h" #include "read_line.h" #include "pushy.h" #define CHECK_PERIOD 1 /* check for push every 1 second */ #define MAX_REQ_LEN 1000 /* limit GET request line to 1000 bytes */ #define MAX_FILE_SIZE 65536 /* limit file pushed to 64kB */ /* unknown file response strings */ #define MSG_BAD_START \ "HTTP/1.0 404 Not Found\nContent-type: text/html\nContent-length: %d\n\n" #define MSG_BAD_URL "HTTP/1.0 404 Not Found" #define MSG_BAD_URL_LEN 22 /* server push strings */ #define BORDER "---never appears in document---" #define BORDER_LEN 31 #define MSG_PUSH_HEADER \ "HTTP/1.0 200 OK\n\ Content-type: multipart/x-mixed-replace;boundary=" \ BORDER "\n\n" BORDER "\n" #define MSG_PUSH_HEADER_LEN (68 + 2 * BORDER_LEN) #define MSG_PUSH_START "Content-type:text/html\n\n" #define MSG_PUSH_START_LEN 24 #define MSG_PUSH_END "\n\n" BORDER "\n" #define MSG_PUSH_END_LEN (3 + BORDER_LEN) /* Wait up to a full CHECK_PERIOD for data to appear from the client (on descriptor fd). If data appear, return 1. Otherwise, return 0. */ static int client_has_data (int fd); /* Parse an HTTP get command, strip the initial "/" from the file name, and store it in fname. fname must have enough space to store all of cmd. If successful, return 1. Otherwise, return 0. */ static int parse_get_cmd (char* cmd, char* fname); /* Deallocate a thread_info structure. */ static void release_thread_info (thread_info_t* info); /* Attempt to read a request from the client. If a request is available, parse it as a GET command and prepare to send a copy of the requested file. */ static int read_client_request (thread_info_t* info); /* Read the filename from a valid HTTP GET request block and store it in the thread-specific data structure. Process the full request block (by discarding all other lines). */ static int read_file_name (thread_info_t* info); /* Send an update of a modified file to the client if necessary. */ static int send_file_to_client (thread_info_t* info); ASSERT_STRING; /* Static copy of file name for assertions. */ void client_thread (thread_info_t* info) { /* Check argument. */ ASSERT (info != NULL); /* Free the thread info block whenever the thread terminates. Note that pushing this cleanup function races with external termination. If external termination wins, the memory is never released. However, by default, external cancellation is deferred until a thread reaches a cancellation point, thus ensuring that the child thread wins the race. You can, of course, make cancellation asynchronous, in which case you have to handle the situation by a) having the parent thread (which allocated the memory) deallocate the memory at some point in the future; or b) synchronously hand off cleanup duty from parent to child after the child has pushed the cleanup. */ pthread_cleanup_push ((void (*)(void*))release_thread_info, info); /* Loop between waiting for a request and sending a new copy of the current file of interest. */ while (read_client_request (info) == 0 && send_file_to_client (info) == 0); /* Defer cancellations to avoid re-entering deallocation routine (release_thread_info) in the middle, then pop (and execute) the deallocation routine.*/ pthread_setcanceltype (PTHREAD_CANCEL_DEFERRED, NULL); pthread_cleanup_pop (1); } static int client_has_data (int fd) { fd_set read_set; struct timeval timeout; /* Check argument. */ ASSERT (fd > 0); /* Set timeout for select. */ timeout.tv_sec = CHECK_PERIOD; timeout.tv_usec = 0; /* Set read mask for select. */ FD_ZERO (&read_set); FD_SET (fd, &read_set); /* Call select. Possible return values are {-1, 0, 1}. */ if (select (fd + 1, &read_set, NULL, NULL, &timeout) < 1) { /* We can't check errno in a thread--assume nothing bad has happened. */ return 0; } /* Select returned 1 file descriptor ready for reading. */ return 1; } /* WARNING: fname must have enough space to store cmd. */ static int parse_get_cmd (char* cmd, char* fname) { char* find; char* last; /* GET command has the format: GET / HTTP/1.0 If cmd matches this format, we extract into fname. */ /* Check arguments. */ ASSERT (cmd != NULL && fname != NULL); /* Validate the first two pieces. */ if ((find = strtok_r (cmd, " ", &last)) == NULL || strcmp (find, "GET") != 0 || (find = strtok_r (NULL, " ", &last)) == NULL || find[0] != '/') return 0; /* Copy the potential file name, droping the initial /. */ (void)strcpy (fname, find + 1); /* Validate the last piece and the end-of-line. */ if ((find = strtok_r (NULL, " ", &last)) == NULL || strncmp (find, "HTTP/1.", 7) != 0 || (find = strtok_r (NULL, " ", &last)) != NULL) return 0; /* Return success. */ return 1; } static int read_client_request (thread_info_t* info) { /* Check argument. */ ASSERT (info != NULL); ASSERT (info->fd > 0); /* If the client sends data within the timeout, interpret it as an HTTP GET request and fill in the file name. If the data cannot be interpreted as such (or if the client closes the connection), we close the connection. */ if (client_has_data (info->fd)) { if (!read_file_name (info)) return -1; /* Mark the file to be resent, but do not override the 0 marker, which indicates that we have yet to send a server push header. */ if (info->last_sent != (time_t)0) info->last_sent = (time_t)1; } /* If no data are available within the timeout, or if a filename was correctly received, return success. */ return 0; } static int read_file_name (thread_info_t* info) { char buf[MAX_REQ_LEN + 1]; /* buffer for reading lines */ char possible[MAX_REQ_LEN + 1]; /* buffer for possible new file name */ char* new; /* temporary copy pointer */ /* Check argument. */ ASSERT (info != NULL); ASSERT (info->fd > 0); /* Try to read one line and parse it as an HTTP GET command. */ if (read_line (info->fd, buf, MAX_REQ_LEN) != NULL && parse_get_cmd (buf, possible)) { /* If successful, read lines and discard them until we find a blank line, which marks the end of the HTTP request. */ while (read_line (info->fd, buf, MAX_REQ_LEN) != NULL) { if (buf[0] == 0 && (new = malloc (strlen (possible) + 1)) != NULL) { /* Copy the new filename into the thread-specific data structure. */ strcpy (new, possible); if (info->fname != NULL) free (info->fname); info->fname = new; return 1; } } } return 0; } static void release_thread_info (thread_info_t* info) { /* Check argument. */ ASSERT (info != NULL); ASSERT (info->fd > 0); /* Close the socket, either in stream or file descriptor form. */ if (close (info->fd) == -1) fputs ("close socket error\n", stderr); /* Free the file name of interest, if any. */ if (info->fname != NULL) free (info->fname); /* Free the thread information structure itself. */ free (info); } static int send_file_to_client (thread_info_t* info) { char outbuf[1000]; /* output data */ char filbuf[MAX_FILE_SIZE]; /* file data */ struct stat buf; /* file statistics (timestamp) */ int fil_fd; /* descriptor for file */ int cnt; /* count of printed characters */ /* Check argument. */ ASSERT (info != NULL); ASSERT (info->fd > 0); /* If no request has been received, return. */ if (info->fname == NULL) return 0; /* Read file modification timestamp and check size. If we cannot check the timestamp or if the file is too large, refuse to cooperate. */ if (stat (info->fname, &buf) == 0 && buf.st_size <= MAX_FILE_SIZE) { /* If the file was sent more recently than it was modified, we are done. */ if (buf.st_mtime <= info->last_sent) return 0; /* Open the file. */ if ((fil_fd = open (info->fname, O_RDONLY)) >= 0) { /* Read the data into memory. */ if (read (fil_fd, filbuf, buf.st_size) == buf.st_size) { /* If this is the first file being pushed to this client, start with a server push header. */ if (info->last_sent == (time_t)0) write (info->fd, MSG_PUSH_HEADER, MSG_PUSH_HEADER_LEN); /* Mark the start of the data block. */ write (info->fd, MSG_PUSH_START, strlen (MSG_PUSH_START)); /* Write the data. */ write (info->fd, filbuf, buf.st_size); /* Mark the end of the data block. */ write (info->fd, MSG_PUSH_END, strlen (MSG_PUSH_END)); /* Update our timestamp. */ info->last_sent = time (NULL); /* Close the file; if all goes smoothly, report success. */ if (close (fil_fd) == 0) return 0; } else (void)close (fil_fd); } } /* No such file exists (or unreadable). */ cnt = sprintf (outbuf, MSG_BAD_START, MSG_BAD_URL_LEN); write (info->fd, outbuf, cnt); write (info->fd, MSG_BAD_URL, MSG_BAD_URL_LEN); /* Report failure. */ return -1; }