/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "defines.h" #ifdef USE_ZLIB # include # include #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); }