/*
 *  httplog.c - An Apache logfile rollover program
 *
 *  Copyright (c) 2001,2002 Eli Sand
 *
 *  This source file is covered by the Free Software License (FSL), as published
 *  by Eli Sand.  A copy of the Free Software License (FSL) should have been
 *  included with this file, if not, please contact the author of this software.
 *
 */

#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <netdb.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "defines.h"

#ifdef USE_ZLIB
# include <sys/wait.h>
# include <zlib.h>
#endif

/* maximum characters allowed in path */
#ifndef PATH_MAX
# define PATH_MAX	512
#endif

/* maximum characters allowed from stdin */
#ifndef BUFSIZ
# define BUFSIZ		8192
#endif

#define min(x, y)	((x) < (y)) ? (x) : (y)

#ifdef USE_ZLIB
 int gzip(char *);
#endif
int mkdirs(char *);
int eprintf(const char *, const char *, ...);
int parsetags(char *, size_t, const char *);
void sighandler(int);
char *getlocalfqdn(void);


int main(int argc, char *argv[]) {
	int curopt = 0;
	time_t epoch = time(NULL);
	unsigned long bufsize = BUFSIZ;

	FILE *fd = NULL;
	char *buffer = NULL;

	char input[BUFSIZ + 1] = {'\0'};
	char tmplog[PATH_MAX + 1] = {'\0'};
	char oldlog[PATH_MAX + 1] = {'\0'};
	char curlog[PATH_MAX + 1] = {'\0'};
	char symlnk[PATH_MAX + 1] = {'\0'};

	struct tm *timeinfo;
	struct group  *gid = NULL;
	struct passwd *uid = NULL;
	struct sigaction sigact_hup;
	struct sigaction sigact_term;

#ifdef USE_ZLIB
	int zlib = 0;
	struct sigaction sigact_chld;

	/* trap SIGCHLD so we can get rid of any zombie processes */
	sigact_chld.sa_handler = (void *)sighandler;
	sigemptyset(&sigact_chld.sa_mask);
	sigaction(SIGCHLD, &sigact_chld, NULL);
#endif

	/* trap SIGHUP so we can flush our logfile */
	sigact_hup.sa_handler = (void *)sighandler;
	sigemptyset(&sigact_hup.sa_mask);
	sigaction(SIGHUP, &sigact_hup, NULL);

	/* trap SIGTERM so we can flush our logfile and exit gracefully */
	sigact_term.sa_handler = (void *)sighandler;
	sigemptyset(&sigact_term.sa_mask);
	sigaction(SIGTERM, &sigact_term, NULL);

	while ((curopt = getopt(argc, argv, "Vvzu:g:s:b:h?")) != EOF) {
		switch (curopt) {
			case 'V':
			case 'v': {
				eprintf("notice", "\b\b version %s", VERSION);
				break;
			}
			case 'z': {
				/* enable zlib compression for logfiles */
#ifdef USE_ZLIB
				zlib = 1;
#else
				eprintf("error", "zlib compression was not compiled into httplog");
				exit(1);
#endif
				break;
			}
			case 'u': {
				/* find the user id number for the specified user */
				if ((uid = getpwnam(optarg)) == NULL) {
					eprintf("error", "unable to find user id number for `%s'", optarg);
					exit(1);
				}
				break;
			}
			case 'g': {
				/* find the group id number for the specified group */
				if ((gid = getgrnam(optarg)) == NULL) {
					eprintf("error", "unable to find group id number for `%s'", optarg);
					exit(1);
				}
				break;
			}
			case 's': {
				/* enable symlink creation */
				strncpy(symlnk, optarg, PATH_MAX);
				break;
			}
			case 'b': {
				/* find out the maximum amount of ram to malloc */
				if (atoi(optarg) < BUFSIZ) {
					eprintf("error", "buffer size can be no less than `%d'", BUFSIZ);
					exit(1);
				}
				bufsize = atoi(optarg);
				break;
			}
			case 'h':
			case '?':
			default : {
				fprintf(stderr, "usage: %s logfile [-z] [-u user] [-g group] [-s symlink] [-b buffer_size]\n", argv[0]);
				fprintf(stderr, "\tFor example, in your Apache httpd.conf file, insert this line:\n");
				fprintf(stderr, "\tCustomLog \"|/path/to/%s /path/to/logfiles/ex%%Y%%m%%d.log\" combined\n", argv[0]);
				exit(1);
			}
		}
	}
	if ((argc - optind) < 1) {
		fprintf(stderr, "usage: %s logfile [-z] [-u user] [-g group] [-s symlink] [-b buffer_size]\n", argv[0]);
		fprintf(stderr, "\tFor example, in your Apache httpd.conf file, insert this line:\n");
		fprintf(stderr, "\tCustomLog \"|/path/to/%s /path/to/logfiles/ex%%Y%%m%%d.log\" combined\n", argv[0]);
		exit(1);
	}

	/* change the current group id being used by the program */
	if (gid != NULL) {
		if (setgid(gid->gr_gid)) {
#ifdef USE_DEBUG
			eprintf("debug", strerror(errno));
#endif
			eprintf("error", "unable to set group id to `%s'", optarg);
			exit(1);
		}
	}

	/* change the current user id being used by the program */
	if (uid != NULL) {
		if (setuid(uid->pw_uid)) {
#ifdef USE_DEBUG
			eprintf("debug", strerror(errno));
#endif
			eprintf("error", "unable to set user id to `%s'", optarg);
			exit(1);
		}
	}

	/* if we get here, print out a notice to stderr saying we're up and running */
	eprintf("notice", "\b\b/%s configured -- resuming normal operations", VERSION);

	/* parse any special & tags in the filename template */
	parsetags(tmplog, PATH_MAX, argv[optind]);

	/* read standard input */
	/* if we didn't catch EOF from stdin, push text to file, and loop */
	/* test stdin for EOF so that when we catch a SIG, we don't up and die from fgets() returning NULL */
	while (!feof(stdin)) {
		if (fgets(input, BUFSIZ, stdin)) {

			/* parse filename given on command line for strftime formatting */
			if (!(epoch = time(NULL)) || !(timeinfo = localtime(&epoch))) {
#ifdef USE_DEBUG
				eprintf("debug", strerror(errno));
#endif
				eprintf("error", "unable to get local time");
				exit(1);
			}
			strftime(curlog, PATH_MAX, tmplog, timeinfo);

			/* compare the last filename to the current one to see if we need to open a new logfile */
			if (strcmp(oldlog, curlog)) {

				if (fd) {

					/* close the file and free up memory */
					fclose(fd);
					free(buffer);

					/* reset values to ensure data integrety */
					fd = NULL;
					buffer = NULL;

#ifdef USE_ZLIB
					/* if compression is enabled, fork off a child process for compression */
					if (zlib) {
						switch (fork()) {
							case  0: {
								exit(gzip(oldlog));
								break;
							}
							case -1: {
#ifdef USE_DEBUG
								eprintf("debug", strerror(errno));
#endif
								eprintf("error", "unable to fork compression stage for logfile `%s'", curlog);
								break;
							}
						}
					}
#endif
				}

				/* create path to the logfile if it doesn't exist already */
				mkdirs(curlog);

				/* try to append to the evaluated filename, if it doesn't exist, create it */
				if ((fd = fopen(curlog, "a")) == NULL) {
#ifdef USE_DEBUG
					eprintf("debug", strerror(errno));
#endif
					eprintf("error", "unable to open logfile `%s'", curlog);
					exit(1);
				}

				/* allocate some memory for our own logfile buffer */
				if ((buffer = malloc(bufsize + 1)) == NULL) {
#ifdef USE_DEBUG
					eprintf("debug", strerror(errno));
#endif
					eprintf("error", "unable to allocate memory for logfile `%s'", curlog);
					fclose(fd);
					exit(1);
				}
				memset(buffer, '\0', bufsize + 1);

				/* set up our own logfile buffer type based on flush delay setting */
				if (setvbuf(fd, buffer, (bufsize > BUFSIZ) ? _IOFBF : _IOLBF, bufsize) != 0) {
#ifdef USE_DEBUG
					eprintf("debug", strerror(errno));
#endif
					eprintf("error", "unable to initialize buffer for logfile `%s'", curlog);
					fclose(fd);
					free(buffer);
					exit(1);
				}

				/* make a symlink to the current filename */
				if (strlen(symlnk) > 0) {
					unlink(symlnk);
					mkdirs(symlnk);
					if (symlink(curlog, symlnk) != 0) {
#ifdef USE_DEBUG
						eprintf("debug", strerror(errno));
#endif
						eprintf("error", "unable to create symlink `%s'", symlnk);
					}
				}

				/* update the filename for the next loop comparison */
				strncpy(oldlog, curlog, PATH_MAX);
			}

			/* push the input to the logfile */
			if (fputs(input, fd) == EOF) {
#ifdef USE_DEBUG
				eprintf("debug", strerror(errno));
#endif
				eprintf("error", "unable to append to logfile `%s'", curlog);
				fclose(fd);
				free(buffer);
				exit(1);
			}
		}
	}

	/* close any open file descriptors and free up the ram used by the buffer */
	if (fd) {
		fclose(fd);
		free(buffer);
	}

	return(0);
}

#ifdef USE_ZLIB
int gzip(char *filename) {
	FILE    *fd = NULL;
	gzFile gzfd = NULL;
	char buffer[BUFSIZ + 1];
	char gzfilename[PATH_MAX + 1];

	/* zero out the buffer */
	memset(buffer, '\0', BUFSIZ + 1);

	/* add ".gz" to the end of the file */
	if ((strlen(filename) + 3) < PATH_MAX)
		sprintf(gzfilename, "%s.gz", filename);
	else {
		eprintf("error", "pathname for compressed file `%s.gz' is too long", filename);
		return(1);
	}

	/* try to open the file for reading */
	if ((fd = fopen(filename, "r")) == NULL) {
#ifdef USE_DEBUG
		eprintf("debug", strerror(errno));
#endif
		eprintf("error", "unable to open file `%s' for compression stage", filename);
		return(1);
	}

	/* try to create the compressed file at maximum compression */
	if ((gzfd = gzopen(gzfilename, "wb9")) == NULL) {
#ifdef USE_DEBUG
		eprintf("debug", strerror(errno));
#endif
		eprintf("error", "unable to open compressed file `%s' for compression stage", gzfilename);
		fclose(fd);
		return(1);
	}

	/* read from the uncompressed file until we hit EOF */
	while (fread(buffer, sizeof(char), BUFSIZ, fd) > 0) {

		/* output and compress the line we read to the compressed file */
		if (!gzwrite(gzfd, buffer, BUFSIZ)) {
			eprintf("error", "unable to output compressed data to compressed file `%s'", gzfilename);
			gzclose(gzfd);
			fclose(fd);
			return(1);
		}
	}

	/* test to see if there were any errors while reading in data */
	if (ferror(fd)) {
		eprintf("error", "unable to read data from file `%s'", filename);
		gzclose(gzfd);
		fclose(fd);
		return(1);
	}

	/* close both files */
	gzclose(gzfd);
	fclose(fd);

	/* delete the original uncompressed file */
	if (unlink(filename) == -1) {
#ifdef USE_DEBUG
		eprintf("debug", strerror(errno));
#endif
		eprintf("notice", "unable to delete file `%s' after compression stage", filename);
	}

	return(0);
}
#endif

int mkdirs(char *pathname) {
	int fd;
	char *tmpdir;
	char *curdir = pathname;

	/* if there's no directories to create, return to main */
	if (!(tmpdir = strchr(pathname, '/')))
		return(0);

	/* save a link to our current working directory */
	fd = open(".", O_RDONLY);

	/* for each directory in the path, create it if it doesn't exist */
	while ((tmpdir = strchr(curdir, '/'))) {

		/* isolate the next directory to create */
		*tmpdir = '\0';

		/* attempt to create the required directory, and cd into it */
		if (curdir == pathname)
			chdir("/");
		else {
			mkdir(curdir, 0755);
			chdir(curdir);
		}

		/* reset pathname to it's original state */
		*tmpdir = '/';
		curdir = tmpdir + 1;
	}

	/* change back to the original base directory */
	fchdir(fd);
	close(fd);

	return(0);
}

int eprintf(const char *type, const char *format, ...) {
	va_list ap;
	char errmsg[BUFSIZ + 1];

	/* get the current date and time */
	time_t  epoch = time(NULL);
	char *curtime = ctime(&epoch);

	/* cut off the '\n' at the end of ctime()'s output */
	curtime[strlen(curtime) - 1] = '\0';

	va_start(ap, format);
	vsprintf(errmsg, format, ap);
	va_end(ap);

	return(fprintf(stderr, "[%s] [%s] httplog: %s\n", curtime, type, errmsg));
}

int parsetags(char *output, size_t max, const char *input) {
	char *outptr = output;
	const char *curptr = NULL;
	const char *oldptr = input;

	/* search input for special tags */
	while ((curptr = strchr(oldptr, '%')) != NULL) {

		/* copy everything from last tag to current tag */
		strncpy(outptr, oldptr, min(max - strlen(output), curptr - oldptr));

		/* update output pointer */
		outptr += strlen(outptr);

		/* parse current tag */
		switch ((char)*(curptr + 1)) {
			case '1': {
				char *ptr;
				char *fqdn;

				/* output host name */
				if ((fqdn = getlocalfqdn()) != NULL) {
					if ((ptr = strchr(fqdn, '.')) != NULL)
						*ptr = '\0';
					strncpy(outptr, fqdn, min(max - strlen(output), strlen(fqdn)));
				}
				else
					strncpy(outptr, "none", min(max - strlen(output), 4 * sizeof(char)));
				break;
			}
			case '2': {
				char *ptr;
				char *fqdn;

				/* output domain name */
				if ((fqdn = getlocalfqdn()) != NULL) {
					if ((ptr = strchr(fqdn, '.')) != NULL)
						fqdn = ptr + 1;
					strncpy(outptr, fqdn, min(max - strlen(output), strlen(fqdn)));
				}
				else
					strncpy(outptr, "none", min(max - strlen(output), 4 * sizeof(char)));
				break;
			}
			case '3': {
				char *fqdn;

				/* output full domain name */
				if ((fqdn = getlocalfqdn()) != NULL)
					strncpy(outptr, fqdn, min(max - strlen(output), strlen(fqdn)));
				else
					strncpy(outptr, "none", min(max - strlen(output), 4 * sizeof(char)));
				break;
			}
			default: {
				/* copy the unknown tag to the output */
				strncpy(outptr, curptr, min(max - strlen(output), 2 * sizeof(char)));
			}
		}

		/* update tag pointers */
		oldptr = curptr + 2;

		/* update output pointer */
		outptr += strlen(outptr);
	}

	/* copy everything left over from input */
	strncpy(outptr, oldptr, min(max - strlen(output), strlen(oldptr)));

	return(strlen(output));
}

void sighandler(int signal) {

	/* handle the appropriate signal properly */
	switch (signal) {
		case SIGHUP: {
			/* flush all data for all open streams */
			eprintf("notice", "SIGHUP received.  Flushing buffers");
			fflush(NULL);
			break;
		}
		case SIGTERM: {
			/* flush all data for all open streams */
			eprintf("notice", "SIGTERM received.  Flushing buffers and exiting");
			fflush(NULL);
			exit(0);
			break;
		}
#ifdef USE_ZLIB
		case SIGCHLD: {
			/* free up resources tied up by any zombie child process */
			waitpid(-1, NULL, WNOHANG);
			break;
		}
#endif
	}

	return;
}

char *getlocalfqdn(void) {
	char hostname[256];
	struct hostent *hostinfo;

	gethostname(hostname, sizeof(hostname));
	if ((hostinfo = gethostbyname(hostname)) != NULL)
		return(hostinfo->h_name);
	else
		return(NULL);
}


syntax highlighted by Code2HTML, v. 0.9.1