/*									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 <pthread.h>

#include <sys/time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#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 /<filename> HTTP/1.0

       If cmd matches this format, we extract <filename> 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;
}