/** ****************************************************************
 * C/C++ Source: pushmain.c
 *
 * Main process file for the pushfiles process, a very lightweight
 * HTTP PUT client to send the listed files out to some remote PUT handler
 * making a record of the errors.
 * 
 * this process is called by a ruby script that sets up FIFO pipes
 * and then attaches this process to the pipe, letting these
 * exit when the queue is exhausted.  The program stays alive for 5
 * minutes awaiting new input on stdin, then exits if none happens.
 * 
 * the incoming pipe stream can also contain authentication  information
 * as "# AUTH: user:pw" but this is discouraged as it gets sent on every 
 * PUT and is not really secure -- a far better method is to enable the 
 * bcast/.netrc code and setup file with entries like
 *
 *  machine myhost.mydomain.com
 *   login userlogin
 *   password secretword
 *
 * Created: Mon Feb 28 19:07:48 EST 2005
 *
 * @author:  Gary Lawrence Murphy <gary@xmlteam.com>
 * Copyright:  2005 XMLTeam Solutions Inc (www.xmlteam.com)
 * @version: $Id: main.c,v 1.4 2005/06/07 14:08:34 garym Exp $
 ****************************************************************
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <syslog.h>

#include <curl/curl.h>

#undef TRUE
#define TRUE 1

#ifndef DEBUG
#define DEBUG 1
#endif

#define TIMEOUT 300 /* persist for 5 min awaiting input */

CURL *curl;
char *target_url;

/* current file data: */
#define MAX_PATHLEN 2048
char filename_buffer[MAX_PATHLEN] = ""; /* allow for a really long path */
char *filename = (char *) filename_buffer;

#define CHUNKSIZE 0x10000
FILE *fileptr;
size_t bytes_in = 0;
char* mem_buffer = NULL;

/* log timestamp */
char tstamp[20];
time_t   now;

size_t set_tstamp(void) {
  time( &now );
  return strftime( tstamp, 20, "%Y-%m-%d %H:%M:%S", localtime( &now ));
}

/* abort only after 5 min inactivity */
void ALRMhandler( int sig ) {
  exit(EXIT_SUCCESS);
}

void atexit_callback(void) {
  set_tstamp();
  if (filename[0]) { /* in the midst of a file */
	    fputs( tstamp, stdout );
	    fputs( " FAIL: exit aborts ", stdout );
	    fputs( filename, stdout );
	    fputs( "\n", stdout );
	    fflush( stdout );
  }
  /* curl manual says "easy_cleanup" is to be done at the end of each
     "session"; that's taken to be a list of files across a connection
     kept alive, but maybe that's too generous?
  */
  curl_easy_cleanup(curl);

  curl_global_cleanup();

  fputs( tstamp, stderr );
  fputs( " DEBUG EXIT: ", stderr );
  fputs( target_url, stderr );
  fputs( "\n", stderr );
  fflush( stderr );

}

/* implemented here to ensure that the read buffer cannot contain the
   sporadic PUT headers seen in the received data stream.  The buffer
   is re-allocated fresh for every PUT transaction to guarantee the
   bug does not creep in here.
*/

size_t read_callback(void *ptr, size_t size, size_t nmemb, void *stream)
{
  size_t bytes_out = size * nmemb;

  if (bytes_out > bytes_in) { /* only send what we have so far */
    bytes_out = bytes_in;
  }

  if (bytes_out > 0) {
    memcpy( ptr, stream, bytes_out );
    memcpy( stream, ((char *)stream)+bytes_out, bytes_in - bytes_out );
    bytes_in -= bytes_out;
    if (bytes_in <= 0) {
      bytes_in = fread( mem_buffer, 1, CHUNKSIZE, fileptr );
      if (bytes_in <= 0 && ! feof(fileptr)) {
		set_tstamp();
		fprintf( stderr, "%s ERROR: read error %d\n", tstamp, ferror(fileptr));
		fflush( stderr );
      }
    }
  }

  return bytes_out;
}

CURLcode curl_putfile( CURL *curl, char *fname )
{
  CURLcode res;
  size_t file_size = 0;

  set_tstamp();
  /* get the file size of the current local file */
  fileptr = fopen( fname, "rb");
  fseek(fileptr, 0, SEEK_END);
  file_size = ftell(fileptr);
  fseek(fileptr, 0, SEEK_SET); 
	
  /* reject zero files */
  if (file_size <= 0)
    {
      fputs( tstamp, stderr );
      fputs( " ERROR: ", stderr );
      fputs( fname, stderr );
      fputs( " is zero length\n", stderr );
      return 0;
    }

  if (!fileptr) 
    {
      fputs( tstamp, stderr );
      fputs( " ERROR: ", stderr );
      fputs( fname, stderr );
      fputs( " is unreadable\n", stderr );
      return 0;
    }

  if (DEBUG) {
    set_tstamp();
    fprintf( stderr, "%s DEBUG: Sending %d bytes\n", tstamp, file_size );
    fflush( stderr );
  }
  /* load up the initial buffer */

  bytes_in = fread(mem_buffer,1,CHUNKSIZE,fileptr);
  if (bytes_in <= 0 && ! feof(fileptr)) {
	set_tstamp();
	fprintf( stderr, "%s ERROR: read error %d\n", tstamp, ferror(fileptr));
	fflush( stderr );
  }
		
  /* set the file and CONTENT_LENGTH */
  curl_easy_setopt(curl, CURLOPT_INFILESIZE, file_size);

  /* PUT the file */
  res = curl_easy_perform(curl);

  fclose(fileptr); /* close the local file */

  return res;
}

void usage()
{
  fputs( "usage: cat files.lst | pushfiles <URL>\n", stderr );
}

int main(int argc, char **argv)
{
  char errmsg_buf[ CURL_ERROR_SIZE ];
  char *errmsg = (char *) errmsg_buf;
  
  if(argc < 2)
  {
    usage();
    return 1;
  }

  signal(SIGALRM, ALRMhandler);
  atexit(atexit_callback);

  target_url = argv[1];

  /* In windows, this will init the winsock stuff */
  curl_global_init(CURL_GLOBAL_ALL);

  /* get a curl handle */
  curl = curl_easy_init();
  if (!curl) return 1;

  curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, 150);

  /* timeout is set short because the remote end on a windows client
	 was hanging for several seconds as if it was awaiting more input;
	 just to "make it work" our chief prog ordered a "hurl and forget"
	 policy. There is no difference in the header-inserted behaviour
	 whether this was a 60 second or a 1 second timeout, the only
	 change is more timeout errors in the logs; docs still arrive
	 corrupted.
  */
  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1);

  curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);

  /* debugging */
  curl_easy_setopt(curl,CURLOPT_VERBOSE, DEBUG);
  curl_easy_setopt(curl,CURLOPT_ERRORBUFFER, errmsg);

  int retrying=0;

  alarm(TIMEOUT);
  memset( filename, 0, sizeof filename_buffer );

  while (1) 
  {
    if  ( fgets( filename, sizeof filename_buffer, stdin ) ) 
    {
      /* chop cr from filename */
      filename[ strlen(filename) - 1 ] = 0;
      if (filename[0] && (filename[0] != '#')) {
		CURLcode res = 0;
		set_tstamp();
		if (DEBUG) {
		  fputs( tstamp, stderr );
		  fputs( " DEBUG: PUT ", stderr );
		  fputs( filename, stderr );
		  fputs( "\n", stderr );
		  fflush( stderr );
		}
		/* set up transfer buffer */
		mem_buffer = (char *) malloc(CHUNKSIZE);
		curl_easy_setopt(curl, CURLOPT_READDATA, mem_buffer);

		/* these two were originally OUTSIDE the send loop in case
		   they had side-effects necessary for each transaction; 
		   moving them here doesn't affect the bug */

		curl_easy_setopt(curl,CURLOPT_URL, target_url);
		curl_easy_setopt(curl, CURLOPT_UPLOAD, TRUE) ;

		switch (res = curl_putfile( curl, filename )) /*  see curl/curl.h */
		  {
		  case CURLE_COULDNT_RESOLVE_HOST:
		  case CURLE_COULDNT_CONNECT:
		  case CURLE_PARTIAL_FILE:
		  case CURLE_SEND_ERROR:
		  case CURLE_RECV_ERROR:
		  case CURLE_FAILED_INIT:
		  case CURLE_WRITE_ERROR:
		  case CURLE_READ_ERROR:
			set_tstamp();
			fprintf( stderr, "%s ERROR: %d: %s\n", tstamp, res, errmsg );
			fflush( stderr );

		  default: /* retry disabled. see xts-irc 2005-05-26 21:00-22:00 */
		  case CURLE_OK: 
			alarm(300); /* 5 min since last success */
			if (DEBUG) {
			  set_tstamp();
			  fputs( tstamp, stderr );
			  fputs( " SENT: ", stderr );
			  fputs( filename, stderr );
			  fputs( "\n", stderr );
			  fflush( stderr );
			}
			/* make tripple sure there's no corrupt data in the buffer */
			memset( filename, 0, sizeof filename_buffer );
			free( mem_buffer );
			mem_buffer = NULL;

			break;
		  } /* switch */

      } else { /* special command line */
		char *auth = strstr( filename, "AUTH" );
		if (auth) {
		  auth = strchr( auth, ':' );
		  if (auth && auth[1]) {
			auth++; /* skip the colon */
			while (auth[0] == ' ') auth++; /* skip spaces */
			curl_easy_setopt(curl, CURLOPT_USERPWD, auth);
			/*
			  curl_easy_setopt(curl, CURLOPT_NETRC_FILE, 
			  ".netrc");
			*/
		  }
		} /* if (auth) */
      } /* else filename[0] */
    } /* if stdin */
  }  
    return 0;
}

/**  2005 by XMLTeam Solutions Inc - info@xmlteam.com */
