/* gs-parse.c
 *
 * part of the getstats program
 */


#include <stdio.h>
#include <string.h>
#include <sys/stat.h>			/* for stat */
#include <time.h>			/* for strptime */
#include "compat.h"			/* for bzero */
#include "getstats.h"
#include "gs-parse.h"


extern char	logcopy[];		/* current input line, for error messages */
extern int	debug, filesopen, failshort, faildate,
		failmin, failhost, failprefix, failreq, countbytes;
static char	*gunzipbin = GUNZIPBIN;
int		truncline=0, truncreq=0, truncref=0;




/* read a line up to '\n', ignoring NULLs
 * control characters are translated to ^X format
 * returns TRUE on EOF
 *  to facilitate merging
 */
int readaline(logline, bufsize, fp)
char logline[];
int bufsize;
FILE *fp;
{
    int c, ip = 0;
    do {
	c = getc(fp);

	if ((c=='\n') || (c==EOF))	/* do not echo newline or EOF */
	    ;
	else if (c & 0200)		/* translate garbage to '!' */
	    logline[ip++] = '!';
	else if (c == 0177) {		/* translate <DEL> to "^?" */
	    logline[ip++] = '^';
	    logline[ip++] = '?';
	} else if (c == '\0')		/* ignore NULL */
	    ;
	else if (c < ' ') {		/* translate control characters */
	    logline[ip++] = '^'; 	/* to "^X" */
	    logline[ip++] = c + 'A' - 1;
	} else				/* normal char, keep it */
	    logline[ip++] = c;
    } while (c != '\n' &&
	     c != EOF &&
	     ip < bufsize-1);
    logline[ip] = '\0';
    if (ip >= bufsize-1)
	truncline++;
    return (c == EOF);
} /* readaline */



void EraseLogr(r)
logr *r;
{
    *r->address = '\0';
    *r->ident   = '\0';
    *r->user    = '\0';
    *r->date    = '\0';
    *r->rprefix = '\0';
    *r->request = '\0';
    *r->rsuffix = '\0';
    *r->status  = '\0';
    *r->nbytstr = '\0';
    *r->referer = '\0';
    *r->vhost   = '\0';
    *r->client  = '\0';

    r->nbytes   = 0.0;
    *r->shortdate = '\0';
    *r->filtyp    = '\0';
    r->datesecs = BIGDATE;
    r->localreq = FALSE;
    r->html     = FALSE;
    r->virthost = NULL;
    return;
}



/* parse access log record in the old NCSA format (before version 1.3)
 * if the file has been reconciled against the error log then each record
 *  will contain status information: (ok), (not found), (permission denied).
 *
 * returns TRUE if record format is valid
 */
int ParseAccNCSA(logline, r, err)
char *logline;
logr *r;
FILE *err;
{
    int i, j;
    char *c;

    EraseLogr(r);
    if (strlen(logline) < DATELEN)
	return 0;
    for (i = 0;
	 logline[i] != ' ' &&
	 logline[i] != '[' &&
	 logline[i] != '\t' &&
	 i < ADDRLEN;
	 i++)
	r->address[i] = logline[i];
    r->address[i] = '\0';

    while (logline[i] != '[' &&		/* skip to '[' */
	   logline[i] != '\0')
	i++;
    if (logline[i] == '\0')		/* line has no '[' */
	return 0;
    else				/* skip past '[' */
	i++;

    for (j = 0;
	 logline[i] != ']' &&
	 logline[i] != '\0' &&
	 j <= DATELEN; )
	r->date[j++] = logline[i++];
    r->date[j] = '\0';
    if (logline[i] == '\0')		/* line has no ']' */
	return 0;

    c = valid_http_req(logline+i, r);	/* find GET, HEAD, POST */
    if (!c) {				/* else fail */
	failprefix++;
	return 0;
    }

		/* parse file path: read to first space or '?' or '"' */
    for (i = 0;
	 *c != ' ' &&
	 *c != '?' &&
	 *c != '"' &&
	 *c != '\0';
	 c++)
	    r->request[i++] = *c;

    r->request[i] = '\0';
    if (r->request[0] == '\0') {
	failreq++;
	return 0;
    }
    if (*c == '"')
	  strcat(r->request, "<quoted-string>");

    while (*c != ' ' &&				/* skip to ' ' */
	   *c != '\0')
	c++;
    if (*c == '\0')				/* nothing after request */
	return 1;
    else					/* skip past ' ' */
	c++;

    if (*c != '(')
	return 1;
    c++;					/* skip past '(' */
    for (j = 0;
	 *c != ')' &&
	 *c != '\0' &&
	 j <= REQUESTLEN; )
	r->status[j++] = *c++;
    r->status[j] = '\0';

    return 1;
} /* ParseAccNCSA */



/* we break each record into 7 required fields,
 *  and 4 optional fields: vhost, referer, client, ???
 */
#define MINFIELDS	7
#define NPACFIELDS	MINFIELDS+4


/* parse access log record in standard format
 *
 * returns 0 if invalid record format
 *         1    valid record
 */
int ParseAccSTD(logline, r, err)
char *logline;
logr *r;
FILE *err;
{
    char usrfield[ADDRLEN] = "";
    int nfields, i;
    char *c;
    char *field[NPACFIELDS] = { '\0' };
    long datesecs;

    EraseLogr(r);
    if (strlen(logline) < DATELEN) {	/* invalid rec: line too short */
	failshort++;
	return 0;
    }
    strcpy(logcopy, logline);		/* for error messages */
    c = logline;
    /* create an array of pointers to individual log record fields */
    for (nfields = 0; nfields < NPACFIELDS && *c; nfields++) {
      int done = FALSE;
      char stopchar = ' ';

      if ((*c=='"') && (*(c+1)=='(')) {	/* "(field)" */
	stopchar = ')';
	c+=2;
	field[nfields] = c;
      } else if (*c=='"') {		/* "field" */
	stopchar = '"';
	c++;
	field[nfields] = c;
      } else if (*c == '[') {		/* [field] */
	stopchar = ']';
	c++;
	field[nfields] = c;
      } else if (nfields==2 && !strncmp(c, " [", 2)) {
		/* handle missing authuser: "host.name -  [18/Oct..." */
	strcpy(usrfield, "-");
	c++;
	field[nfields] = usrfield;
	done = TRUE;
      } else if (nfields==2) {
	/* handle authuser with embedded space:
	 *   "host.name - Harry Smith [18/Oct..."
	 */
	char *odate = strstr(c, " [");
	if (!odate) {
	  faildate++;
	  return 0;
	}
	*odate++ = '\0';
	if (strchr(c, ' '))		/* bogus authuser contains a space */
	  strcpy(usrfield, "-");	/* ignore it */
	else
	  sprintf(usrfield, "%.*s", (sizeof usrfield)-1, c);
	c = odate;
	field[nfields] = usrfield;
	done = TRUE;
      } else
	field[nfields] = c;

      while (!done) {			/* parse one field */
	char *d;
	if (*c == '\0')
	    done = TRUE;
	else if ((nfields==4) &&	/* looking at req field */
		 (stopchar=='"') &&
		 (*c == '"')) {		/* found a quote */
	    char *h = strstr(c, " HTTP/");
	    if (h) {			/* not the final quote */
		c = h+6;		/* point to final quote instead */
		h = strchr(c, '"');
		if (h) c = h;
	    }
	    *c++ = '\0';
	    if (*c == ' ') *c++ = '\0';
	    done = TRUE;
	} else if ((nfields>=MINFIELDS) && /* ref, client, or vhost */
		 (stopchar=='"') &&
		 (*c == '"')) {		/* found a quote */
	    char *h = strstr(c, "\" \""); /* find the next field */
	    if (!h) {			/* this is the last field */
		c = c + strlen(c) - 1;	/* point to end of record */
		if (*c=='"')
		    *c++ = '\0';
		else
		    fprintf(err, "should end with '\"':\n%s\n\n", logcopy);
	    } else {
		*h = '\0';
		c = h+2;
	    }
	    done = TRUE;
	} else if (*c == stopchar) {
	    if (stopchar==')') {
		if (*(c+1)!='"') {
		    c++;
		    continue;
		} else
		    *c++ = '\0';	/* "(field)" */
	    }
	    *c++ = '\0';		/* end of this field */
	    done = TRUE;
		/* handle missing authuser: "host.name -  [18/Oct..." */
	    if ((nfields!=1) && (*c==' '))
		*c++ = '\0';
	} else
	    c++;
      } /* while not done */
    }
    if (nfields < MINFIELDS) {		/* must have all required fields */
	failmin++;
	return 0;
    }
    if (strlen(field[0]) >= (sizeof r->address)-1) {
	failhost++;			/* host name too long */
	return 0;
    }
    strcpy(r->address, field[0]);	/* good host name */

    sprintf(r->ident, "%.*s", (sizeof r->ident)-1, field[1]);
    sprintf(r->user,  "%.*s", (sizeof r->user)-1,  field[2]);
    if (strlen(field[3]) >= sizeof r->date) {
	faildate++;			/* date too long */
	return 0;
    }
    strcpy(r->date, field[3]);		/* date and time */

    if (!strcmp(field[4], "  "))	/* kludge */
	strcpy(field[4], "-");

    if (strcmp(field[4], "-")) {
      /* find method name (GET, HEAD, POST) and copy it to r->rprefix */
      if ((c=valid_http_req(field[4], r))=='\0') { 
	failprefix++;			/* protocol missing */
	fprintf(err, "invalid method:\n%s\n\n", logcopy);
	return 0;
      }
      for (i = 0;			/* parse file path */
		*c && (i < REQUESTLEN);	c++) {
	if ((*c==' ') &&		/* this must be " HTTP/1.x" */
	    !strchr(c+1, ' '))		/*  yes, final space */
		break;
	r->request[i++] = *c;
      }
      r->request[i] = '\0';
      if (r->request[0] == '\0') {
	failreq++;			/* req is missing */
	return 0;
      }
      if (i == REQUESTLEN)		/* req too long, truncate */
	truncreq++;

      /* convert multiple slashes to single slash, and convert %7E to ~ */
      unescape_url(r->request);

      if (c=strstr(c, "HTTP"))
	sprintf(r->rsuffix,  "%.*s", (sizeof r->rsuffix)-1, c);
    }
    sprintf(r->status,  "%.*s",      (sizeof r->status)-1,  field[5]);
    sprintf(r->nbytstr, "%.*s",      (sizeof r->nbytstr)-1, field[6]);
    for (i = MINFIELDS; i < nfields; i++) {
	if (!strncmp(field[i], "ref ", 4)) {
	    if (strlen(field[i]+4) > (sizeof r->referer)-1)
		truncref++;		/* ref too long, truncate */
	    sprintf(r->referer, "%.*s", (sizeof r->referer)-1, field[i]+4);
	} else if (!strncmp(field[i], "vhost ", 6))
	    sprintf(r->vhost, "%.*s",   (sizeof r->vhost)-1, field[i]+6);
	else if (!strncmp(field[i], "client ", 7))
	    sprintf(r->client, "%.*s",  (sizeof r->client)-1, field[i]+7);
	else
	    fprintf(err, "unexpected field: %s\n%s\n\n", field[i], logcopy);
    }
    r->virthost = NULL;
    return 1;
} /* ParseAccSTD */



void convtoshortdate(date, shortdate)
char *date;
char *shortdate;
{
    struct tm thistime;
    char *datfmt;

    bzero((char *) &thistime, sizeof thistime);
    if (TRUE)				/* standard format */
	datfmt = "%d/%b/%Y:%T";		/* "31/Jan/1995:01:02:03 -0500" */
    else				/* old NCSA format */
	datfmt = "%a %b %d %T %Y";	/* "Sun Jan 31 01:02:03 1995" */
    strptime(date, datfmt, &thistime);
				/* write date in the format "01/31/1995" */
    strftime(shortdate, SHORTDATELEN, "%m/%d/%Y", &thistime);

    return;
} /* convtoshortdate */



int monthdays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

/* use strptime to parse the date string
 * set datesecs to the number of seconds since 1/1/BASEYEAR
 *
 * doing it this way is faster than calling timelocal()
 */
void convtoyearsecs(r, datfmt)
logr *r;
char *datfmt;
{
    int i;
    long ndays;
    struct tm thistime;

    *r->shortdate = '\0';
    r->datesecs = BIGDATE;
    if (*r->date == '\0')
	return;
    bzero((char *) &thistime, sizeof thistime);
    strptime(r->date, datfmt, &thistime);
				/* write date in the format "01/31/1995" */
    strftime(r->shortdate, sizeof r->shortdate, "%m/%d/%Y", &thistime);

    /* r->datesecs=timelocal(&thistime); <-- this is too slow!!! */

    ndays = 0;			/* count the days since Jan 1, BASEYEAR */
    for (i = BASEYEAR; i < 1900+thistime.tm_year; i++) {
	if (IS_LEAP(i))
	    ndays++;
	ndays += 365;
    }
    for (i = 0; i < thistime.tm_mon; i++) {
	if (i == 1 && IS_LEAP(1900+thistime.tm_year))
	    ndays++;
	ndays += monthdays[i];
    }
    ndays += thistime.tm_mday;
    r->datesecs = (long) ndays * SECSPERDAY +
		  thistime.tm_hour * SECSPERHOUR +
		  thistime.tm_min  * SECSPERMIN +
		  thistime.tm_sec;
    return;
} /* convtoyearsecs */



/* parse the request method, save it in r->rprefix
 * returns byte pointer if the method was found
 * returns NULL otherwise
 */
char *valid_http_req(request, r)
char *request;
logr *r;
{
    char *sp = strchr(request, ' ');
    int plen;
    int maxsize = sizeof r->rprefix;

    if (!sp) return '\0';		/* error, no method name found */
    plen = sp - request;
    if (plen > 29) return '\0';		/* error, method name too long */

    sprintf(r->rprefix, "%.*s",
		(plen>maxsize) ? maxsize : plen,
		request);
    return sp+1;
}



/* use ^G to represent CTRL/G
 */
void AddCaret(url)
char *url;
{
    char temp[REQUESTLEN*2];
    register int x, y, anychanges = FALSE;

    for (x=0,y=0; url[y]; x++,y++)
        if (url[y] >= ' ')
	    temp[x] = url[y];
	else {
	    temp[x++] = '^';		/* found a control character */
	    temp[x] = url[y] + 'A' - 1;	/* make it printable */
	    anychanges = TRUE;
	}
    temp[x] = '\0';
    if (anychanges) {
	if (strlen(temp) > REQUESTLEN-1)
	    fprintf(stderr, "AddCaret req too long: %s\n", url);
	else
	    strcpy(url, temp);
    }
}



/* unescape_url
 *
 * convert multiple slashes to single slash, and convert escape codes
 * from %7E format to regular ASCII, then convert control characters
 * to ^X format
 */
void unescape_url(url)
     char *url;
{
    register int x, y, slash;

    for (x=0,y=0,slash=FALSE; url[y]; y++) {
	if (url[y] == '/') {		/* found a slash */
	    if (!slash) {		/*  by itself */
		url[x++] = url[y];	/* keep the slash */
		slash = TRUE;
	    }				/* ignore second slash */
	} else if (url[y] == '%' &&	/* found a %7E escape sequence */
		 isxdigit(url[y+1]) &&
		 isxdigit(url[y+2])) {
	    url[x++] = x2c(&url[y+1]);
	    y+=2;
	    slash = FALSE;
	} else {
	    url[x++] = url[y];
	    slash = FALSE;
	}
    }
    url[x] = '\0';
}



char x2c(what)
char *what;
{
    register int digit;

    digit  = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
    digit *= 16;
    digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));

    if ((digit < ' ') ||
	(digit >= 0177))	/* translate garbage to '!' */
	return '!';
    else
	return digit;
}



#define MAXARGS 4
/* fork a process for input using gunzip for decompression
 *
 * returns -1 on error
 *          0 on success
 */
int read_uncompressed(zip, infilnam)
FILE **zip;
char *infilnam;
{
    char *a[MAXARGS];
    int pipe0[2], pipe1[2], pipe2[2];
    FILE *fp1, *fp2;
    struct stat s;

    if (!infilnam || !(*infilnam)) {
	printf("read_uncompressed requires infilnam\n");
	return -1;
    }
    if (stat(gunzipbin, &s) != 0) {
	fprintf(stderr, "read_uncompressed can't execute %s ", gunzipbin);
	perror("because");
	return -1;
    }
    if (stat(infilnam, &s) != 0) {
	fprintf(stderr, "read_uncompressed can't open %s ", infilnam);
	perror("because");
	return -1;
    }
    a[0] = gunzipbin;
    a[1] = e_strdup("-c");
    a[2] = e_strdup(infilnam);
    a[3] = '\0';
    pipe(pipe0);
    pipe(pipe1);
    pipe(pipe2);
    switch(fork()) {
	case -1:
	    perror("fork() failed");
	    exit(3);
	case 0:
	    close(pipe0[1]);
	    close(pipe1[0]);
	    close(pipe2[0]);
	    dup2(pipe0[0], 0);		/* stdin */
	    dup2(pipe1[1], 1);		/* stdout */
	    dup2(pipe2[1], 2);		/* stderr */
	    execv(gunzipbin, a);
	    perror("execv() failed");
	    exit(3);
	default:
	    close(pipe0[0]);
	    close(pipe1[1]);
	    close(pipe2[1]);
	    *zip = fdopen(pipe1[0], "r");	/* read from his stdout */
	    fp1  = fdopen(pipe0[1], "w");	/* write to his stdin */
	    fp2  = fdopen(pipe2[0], "r");	/* read from his stderr */
	    if (debug)
		fprintf(stderr, "%d files, %s is open by gunzip\n",
				++filesopen, infilnam);

	    return 0;				/* success */
    }
} /* read_uncompressed */
