/*
** logres.c
**
** convert IP addresses to host names in httpd access logs.
**
** keeps all IP addresses (found and not found) in a hash table.
** writes the hash table to disk file when finished.
*/

#include <stdio.h>
#include <string.h>
#include <ctype.h>			/* for isdigit */
#include <sys/types.h>			/* for lookuphost */
#include <sys/socket.h>			/* for lookuphost */
#include <netdb.h>			/* for lookuphost */
#include <netinet/in.h>			/* for in_addr */
#include <sys/stat.h>			/* for stat */
#include <sys/time.h>			/* for timeval, used by resource.h */
#include <sys/resource.h>		/* for setrlimit */
#include <time.h>
#include "logres.h"

#define hash(addr)	(addr        + (addr >> 8) + \
			(addr >> 16) + (addr >> 24)) % HASHSIZE

					/* fork() requires this to be static */
static char gunzipbin[DOMAINLEN] = "/usr/local/bin/gunzip";
char *hashfilnam="/www/data/httpd/pl/iptable.bin";
char *ohashfilnam="/www/data/httpd/pl/iptable.hash";

static struct IPtable *iptable[HASHSIZE] = {0};
int nhashed=0,
    truncated=0;

char *infilnam=NULL, *outfilnam=NULL, *cmdfilnam=NULL;
off_t hashfilsiz=-1;			/* size of hash file */
uid_t hashfilown=-1,			/* uid that owns hash file */
      myuid=-1;				/* uid of this process */


int main(argc, argv)
int argc;
char **argv;
{
    char *word[NCMDFIELDS+1];
    char *msg;

/*    do_args(argc, argv);  */
    myuid = getuid();
    set_ulimit();			/* no limit to memory size */
    DoLoadCommand();			/* load hash table */
    DoDumpCommand();			/* dump hash table */
    return(0);
} /* main */



int do_args(argc, argv)
int argc;
char **argv;
{
    extern int optind;
    extern char *optarg;
    int c;

    while ((c = getopt(argc, argv, "i:o:c:")) != EOF)
      switch(c) {
	case 'i':
	  infilnam = optarg;		/* get input file name */
	  break;
	case 'o':
	  outfilnam = optarg;		/* get output file name */
	  break;
	case 'c':
	  cmdfilnam = optarg;		/* get command file name */
	  break;
	case '?':
	  usage();
	  break;
	}
    if (optind < argc)			/* command options not valid */
	usage();

    /* check for required arguments */
    if (((infilnam==NULL) || (outfilnam==NULL)) && (cmdfilnam==NULL))
	usage();
    return;
}


int usage(void)
{
    fprintf(stderr,
"Usage:
     logres -c cmdfil
or   logres -i infil -o outfil
");
    exit(3);
}



/* add this IP address and domain name to the hash table
 * call this routine after you know it's not in the hash table
 */
void addip(hashval, ipnum, domain)
unsigned int hashval;
u_long ipnum;
char *domain;
{
    struct IPtable *ip;
    ip = (struct IPtable *) emalloc(sizeof(*ip));
    ip->ipaddr = ipnum;
    if (domain)
	ip->domain = e_strdup(domain);
    else
	ip->domain = NULL;
    ip->next = iptable[hashval];
    iptable[hashval] = ip;
    nhashed++;
    return;
}



/* lookup this IP address in the hash table
 * returns a pointer to the domain name, or NULL
 */
char *find_domain(hashval, ipnum)
unsigned int hashval;
u_long ipnum;
{
    struct IPtable *ip;
    for (ip = iptable[hashval]; ip != NULL; ip = ip->next)
	if (ipnum == ip->ipaddr)
	    return ip->domain;
    return NULL;
}



/* write the hash table to disk, overwriting the old hash file
 * returns  -1   nothing to write, hash table empty
 *          -2   can't overwrite hash file
 *           n   number of entries in the largest bucket
 */
int dump_hash_table(hasfilnam)
char *hasfilnam;
{
    FILE *has;
    int bucket, bigbucket=0, nwritten=0;
    struct IPtable *ip;

    if (!nhashed)
	return -1;			/* hash table is empty */
    if ((has = fopen(hasfilnam, "w")) == NULL) {
	fprintf(stderr, "can't overwrite hash file %s ", hasfilnam);
	perror("because");
	return -2;
    }
    /* write IP address and domain name pairs
	ignore IP addresses that were not found by named */
    for (bucket = 0; bucket < HASHSIZE; bucket++) {
	int e=0;			/* count entries in this bucket */
	for (ip = iptable[bucket]; ip != NULL; ip = ip->next) {
	  e++;
	  if (ip->domain) {			/* 4-byte IP address */
	    fwrite(ip, 1, sizeof(ip->ipaddr), has);
	    fputs(ip->domain, has);		/* domain name */
	    putc('\n', has);			/* newline */
	    nwritten++;
	  }
	}
	if (e > bigbucket) bigbucket = e;	/* size of biggest bucket */
    }
    fclose(has);
    fprintf(stderr, "%d out of %d hash table entries written to %s\n",
			nwritten, nhashed, hasfilnam);
    return bigbucket;
} /* dump_hash_table */



/* read IP,host pairs from the disk file, add them to hash table */
void load_old_hash_table(hasfilnam)
char *hasfilnam;
{
    FILE *has;
    int xhashed = nhashed;			/* entries before loading */
    char ipaddr[DOMAINLEN];
    char *domain, *p;

    if ((has = fopen(hasfilnam, "r")) == NULL) { /* print error msg */
	fprintf(stderr, "can't open hash file %s ", hasfilnam);
	perror("because");
	return;
    }
    if (xhashed)
	fprintf(stderr, "hash table contains %d entries before loading\n",
		xhashed);
    while (fgets(ipaddr, DOMAINLEN, has)) {
	struct in_addr ipnumb;
	unsigned int hashval;
	makelower(ipaddr);			/* cvt to lower case */

	/* find the TAB that separates the fields */
	for (domain = ipaddr; (*domain != '\t') && (*domain); domain++)
	    ;
	if (*domain == '\0') {
	    fprintf(stderr, "invalid hash record: %s", ipaddr);
	    continue;
	}
	/* change TAB to a NULL, domain points after the NULL */
	*domain++ = '\0';

	/* remove trailing newline */
	for (p = domain; (*p != '\n') && (*p); p++)
	    ;
	*p = '\0';

	ipnumb.s_addr = inet_addr(ipaddr);	/* separate octets */
	if (ipnumb.s_addr != 0xffffffffu) {	/* numeric address */
	    hashval = hash(ipnumb.s_addr);	/* bucket number */
	    addip(hashval, ipnumb.s_addr, domain); /* add hash table entry */
	}
    }
    fclose(has);
    fprintf(stderr, "%d hash table entries loaded from %s\n",
		nhashed-xhashed, hasfilnam);
    if (xhashed)
	fprintf(stderr, "hash table now contains %d entries\n", nhashed);
    return;
} /* load_old_hash_table */



/* read IP,host pairs from the disk file, add them to hash table */
void load_hash_table(hasfilnam)
char *hasfilnam;
{
    FILE *has;
    int xhashed = nhashed;			/* entries before loading */
    int nrecs = 0;				/* records in hash file */
    u_long ipnum;				/* IP address in 4 bytes */
    char domain[DOMAINLEN];

    if ((has = fopen(hasfilnam, "r")) == NULL) { /* print error msg */
	fprintf(stderr, "can't open hash file %s ", hasfilnam);
	perror("because");
	return;
    }
    if (xhashed)
	fprintf(stderr, "hash table contains %d entries before loading\n",
		xhashed);
    while (fread(&ipnum, 1, sizeof(ipnum), has)) { /* read 4 byte IP addr */
	unsigned int hashval;
	nrecs++;
	if (!fgets(domain, DOMAINLEN, has)) {	/* read domain name */
	    fprintf(stderr, "hash record %d is invalid\n", nrecs);
	    exit(5);
	}
	hashval = hash(ipnum);			/* bucket number */
	addip(hashval, ipnum, domain);		/* add hash table entry */
    }
    fclose(has);
    fprintf(stderr, "%d hash table entries loaded from %s\n",
		nhashed-xhashed, hasfilnam);
    if (xhashed)
	fprintf(stderr, "hash table now contains %d entries\n", nhashed);
    return;
} /* load_hash_table */



char *e_strdup(string)
char *string;
{
    char *p = (char *) malloc(strlen(string) + 1);
    if (p == NULL) {
	fprintf(stderr, "not enough memory! %d entries hashed\n", nhashed);
	exit(5);
    }
    strcpy(p, string);
    return p;
}



void *emalloc(i)
int i;
{
    void *p  = (void *) malloc(i);
    if (p == NULL) {
	fprintf(stderr, "not enough memory! %d entries hashed\n", nhashed);
	exit(5);
    }
    return p;
}



/* use DNS to find the host name that corresponds to this IP address */
char *lookuphost(ipnum)
u_long ipnum;
{
    struct hostent *host;
    host = gethostbyaddr((char *) &ipnum, sizeof(ipnum), AF_INET);
    if (host && host->h_name) {
	makelower(host->h_name);		/* cvt to lower case */
	return(host->h_name);			/* success */
    }
    return(0);					/* failed */
}



void makelower(string)
char *string;
{
    while (*string)
	*string++ = tolower(*string);
}



/* read a line up to '\n', ignoring NULLs
 * control characters are translated to ^X format
 * returns TRUE on EOF
 */
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 (ip >= bufsize-1)	/* ignore excess characters */
	    ;
	else if (c & 0200)		/* translate garbage to '!' */
	    logline[ip++] = '!';
	else if (c == 0177) {		/* translate <DEL> to "^?" */
	    logline[ip++] = '^';
	    logline[ip++] = '?';
	} else if (c == NULL)		/* 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);
    logline[ip] = NULL;
    if (ip >= bufsize-1)
	truncated++;
    return (c == EOF);
} /* readaline */



/* 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;
{
    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;
}



/* parse and execute each line of the command file
 *
 */
void readcmdfile(cmdfilnam)
char *cmdfilnam;
{
    char cmdline[CMDLEN];
    char *cmd_result = NULL;
    FILE *cmd;

    if (strcmp(cmdfilnam, "-") == 0)
	cmd = stdin;
    else if ((cmd = fopen(cmdfilnam, "r")) == NULL) {
	fprintf(stderr, "Can't open command file %s ", cmdfilnam);
	perror("because");
	exit(3);
    }
    while (fgets(cmdline, CMDLEN, cmd)) { /* read a line */
	int i, nwords;
	char *word[NCMDFIELDS+1],
	     *c, *command;

	c = strchr(cmdline, '\n');	/* erase the trailing newline */
	if (c) *c = '\0';
	if (*cmdline != '#') {		/* ignore comments */
	    getwords(cmdline, word, &nwords);
	    command = word[0];
	    if (!strcmp(command, "resolve"))
		DoResCommand(word, nwords);
	    else
		fprintf(stderr, "invalid command: %s\n", cmdline);
	} /* ignore comments */
    } /* read a line */
    if (strcmp(cmdfilnam, "-"))
	fclose(cmd);
    return;
} /* readcmdfile */



/* getwords
 *
 * create an array of pointers to individual command line fields
 * if any field begins with a doublequote, strip the quotes and
 * allow it to contain spaces.  any fields not surrounded by quotes
 * are converted to lower case.
 */
void getwords(buffer, word, nwords)
char *buffer;
char *word[];
int *nwords;
{
    static char bcopy[MAXLINE];
    char *c = bcopy;
    int i;
   
    strcpy(bcopy, buffer);
    for (i = 0; i <= NCMDFIELDS && *c; i++) {
	int done;
	char stopchar = ' ';
	if (*c == '"') {
	    stopchar = '"';
	    c++;
	}
	word[i] = c;			/* begin */
	done = FALSE;
	while (!done) {			/* parse one field */
	    char *d;
	    if (*c == NULL)
		  done = TRUE;
	    else if (*c == stopchar) {
		*c++ = NULL;		/* end */
		done = TRUE;
		while (*c == ' ')
		      *c++ = NULL;
	    } else
		  c++;
	} /* while not done */
	if (stopchar != '"')		/* if not a quoted field */
	    makelower(word[i]);		/* cvt to lower case */
    } /* for */
    *nwords = i;
} /* getwords */



/* parse and execute the LOAD command */
void DoLoadCommand(void)
{
    struct stat s;

    prtime();				/* print current time */
    fprintf(stderr, "load from %s\n", ohashfilnam);
    if (stat(ohashfilnam, &s) != 0) {
	fprintf(stderr, "can't open hash file %s ", ohashfilnam);
	perror("because");
	return;
    }
    hashfilsiz = s.st_size;		/* remember file size */
    hashfilown = s.st_uid;		/* remember the owner */
    load_old_hash_table(ohashfilnam);	/* load into memory */
    return;
} /* DoLoadCommand */



/* parse and execute the RESOLVE command */
void DoResCommand(word, nwords)
char *word[];
int nwords;
{
    char buf[MAXLINE];
    FILE *in, *out;
    char *infilnam  = word[1],
	 *outfilnam = word[2];
    int ip_found=0, ip_not_found=0, found_in_hash=0;
    int nrecs=0;

    prtime();				/* print current time */
    fprintf(stderr, "resolve to %s\n", outfilnam);
    if (nwords != 3) {
	fprintf(stderr, "wrong number of arguments");
	return;
    }
    if (strstr(infilnam, ".gz")) {	/* pipe through gunzip */
	if (read_uncompressed(&in, infilnam) < 0)
	    return;			/* error */
    } else if ((in = fopen(infilnam, "r")) == NULL) {
	fprintf(stderr, "can't open %s ", infilnam);
	perror("because");
	return;
    }
    if ((out = fopen(outfilnam, "w")) == NULL) {
	fprintf(stderr, "can't create %s ", outfilnam);
	perror("because");
	return;
    }
    while (!readaline(buf, MAXLINE, in)) {
	char *inrec = buf;
	char adomain[DOMAINLEN];
	char *thisdomain;
	int k = 0;
	unsigned int hashval;
	u_long ipnum;

	nrecs++;
	while (*inrec && (*inrec != ' ') &&
		(k < DOMAINLEN-1)) {		/* copy IP address */
	  adomain[k++] = *inrec++;
	}
	adomain[k] = '\0';
	unescape_url(inrec);			/* convert %7E to ~ */

	ipnum = inet_addr(adomain);		/* separate octets */
	if (ipnum != 0xffffffffu) {		/* numeric address */
	    hashval = hash(ipnum);		/* bucket number */
						/* look in hash table */
	    if (thisdomain = find_domain(hashval, ipnum)) {
		found_in_hash++;
		strcpy(adomain, thisdomain);
	    } else if (thisdomain = lookuphost(ipnum)) { /* ask DNS */
		ip_found++;
		addip(hashval, ipnum, thisdomain); /* insert in hash table */
		strcpy(adomain, thisdomain);
	    } else {				/* not registered with DNS */
		ip_not_found++;
		addip(hashval, ipnum, NULL);	/* insert IP address only */
	    }
	} /* numeric */
	fprintf(out, "%s%s\n", adomain, inrec);
    } /* while not EOF */
    fclose(in);
    fclose(out);
    fprintf(stderr, "%d records read from %s\n", nrecs, infilnam);
    if (truncated)
      fprintf(stderr, "%d records truncated (longer than %d bytes)\n",
			truncated, MAXLINE);
    fprintf(stderr, "%6d/%6d/%6d DNS lookups succeeded/failed/saved\n",
			ip_found, ip_not_found, found_in_hash);

    DoDumpCommand();
    return;
} /* DoResCommand */



/* dump the contents of the hash table to disk */
void DoDumpCommand(void)
{
    struct stat s;
    int bigbucket;


    if (myuid != hashfilown)		/* existing hash file owned by me? */
	return;				/* no, return */
			/* yes, overwrite it with a new (bigger) version */
    prtime();				/* print current time */
    fprintf(stderr, "dump to %s\n", hashfilnam);
    if (stat(hashfilnam, &s) != 0)
	fprintf(stderr, "creating a new %s\n", hashfilnam);
    else if ((hashfilsiz != -1) && (hashfilsiz != s.st_size)) {
	fprintf(stderr, "%s has changed, not overwriting\n", hashfilnam);
	return;
    }
    if ((bigbucket=dump_hash_table(hashfilnam)) < 0)
	return;
    
    if (stat(hashfilnam, &s) != 0) {
	fprintf(stderr, "can't stat new hash file %s ", hashfilnam);
	perror("because");
    } else
	hashfilsiz = s.st_size;		/* remember new size */
    fprintf(stderr, "  # hash table stats\n");
    fprintf(stderr, "  # %d buckets, %d entries, %.1f entries per bucket\n",
		HASHSIZE, nhashed, 1.0 * nhashed / HASHSIZE);
    fprintf(stderr, "  # the most crowded bucket has %d ", bigbucket);
    if (bigbucket == 1)
	fprintf(stderr, "entry\n");
    else
	fprintf(stderr, "entries\n");
    return;
} /* DoDumpCommand */



void prtime(void)
{
    char *datfmt = "%d/%b/%Y %T";         /* "31/Jan/1995 01:02:03 -0500" */
    char date[90];
    time_t tp;

    time(&tp);
    if (TRUE)
	strftime(date, 90, datfmt, localtime(&tp));
					/* 05/Jan/1997 01:16:36 */
    else
	strcpy(date, asctime(localtime(&tp)));
					/* Sun Jan  5 01:15:28 1997 */
    fprintf(stderr, "   %s ", date);
    return;
}



void set_ulimit(void)			/* no limit to memory size */
{
    struct rlimit rs;

    if (getrlimit(RLIMIT_DATA, &rs) != 0) perror("getrlimit");
    rs.rlim_cur = RLIM_INFINITY;	/* ulimit -d unlimited */
    if (setrlimit(RLIMIT_DATA, &rs) != 0) perror("setrlimit");

    if (getrlimit(RLIMIT_STACK, &rs) != 0) perror("getrlimit");
    rs.rlim_cur = rs.rlim_max = RLIM_INFINITY; /* ulimit -m unlimited */
    if (setrlimit(RLIMIT_STACK, &rs) != 0) perror("setrlimit");
    return;
}



#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)) {
	fprintf(stderr, "read_uncompressed requires infilnam\n");
	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] = NULL;
    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 0
	    while (fgets(inbuf, MAXLINE+2, *zip)) {
		fprintf(stderr, "unzip: %s", inbuf);
	    }
	    while (fgets(inbuf, MAXLINE+2, fp2)) {
		fprintf(stderr, "unzip err: %s", inbuf);
	    }
	    fprintf(fp1, "q\n");
	    fflush(fp1);
#endif

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