Updated ChangeLog.
[ashd.git] / src / accesslog.c
1 /*
2     ashd - A Sane HTTP Daemon
3     Copyright (C) 2008  Fredrik Tolf <fredrik@dolda2000.com>
4
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14
15     You should have received a copy of the GNU General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <stdlib.h>
20 #include <stdio.h>
21 #include <unistd.h>
22 #include <string.h>
23 #include <errno.h>
24 #include <sys/poll.h>
25 #include <time.h>
26 #include <sys/time.h>
27 #include <signal.h>
28 #include <fcntl.h>
29 #include <sys/stat.h>
30
31 #ifdef HAVE_CONFIG_H
32 #include <config.h>
33 #endif
34 #include <utils.h>
35 #include <log.h>
36 #include <req.h>
37 #include <proc.h>
38
39 #define DEFFORMAT "%{%Y-%m-%d %H:%M:%S}t %m %u %A \"%G\""
40
41 static int ch;
42 static char *outname = NULL;
43 static FILE *out;
44 static int flush = 1, locklog = 1;
45 static char *format;
46 static struct timeval now;
47 static volatile int reopen = 0;
48
49 static void qputs(char *s, FILE *o)
50 {
51     for(; *s; s++) {
52         if(*s == '\"') {
53             fputs("\\\"", o);
54         } else if(*s == '\\') {
55             fputs("\\\\", o);
56         } else if(*s == '\n') {
57             fputs("\\n", o);
58         } else if(*s == '\t') {
59             fputs("\\t", o);
60         } else if((*s < 32) || (*s >= 128)) {
61             fprintf(o, "\\x%02x", *s);
62         } else {
63             fputc(*s, o);
64         }
65     }
66 }
67
68 static void logitem(struct hthead *req, char o, char *d)
69 {
70     char *h, *p;
71     char buf[1024];
72     
73     switch(o) {
74     case '%':
75         putc('%', out);
76         break;
77     case 'h':
78         if((h = getheader(req, d)) == NULL) {
79             putc('-', out);
80         } else {
81             qputs(h, out);
82         }
83         break;
84     case 'u':
85         qputs(req->url, out);
86         break;
87     case 'U':
88         strcpy(buf, req->url);
89         if((p = strchr(buf, '?')) != NULL)
90             *p = 0;
91         qputs(buf, out);
92         break;
93     case 'm':
94         qputs(req->method, out);
95         break;
96     case 'r':
97         qputs(req->rest, out);
98         break;
99     case 'v':
100         qputs(req->ver, out);
101         break;
102     case 't':
103         if(!*d)
104             d = "%a, %d %b %Y %H:%M:%S %z";
105         strftime(buf, sizeof(buf), d, localtime(&now.tv_sec));
106         qputs(buf, out);
107         break;
108     case 'T':
109         if(!*d)
110             d = "%a, %d %b %Y %H:%M:%S %z";
111         strftime(buf, sizeof(buf), d, gmtime(&now.tv_sec));
112         qputs(buf, out);
113         break;
114     case 's':
115         fprintf(out, "%06i", (int)now.tv_usec);
116         break;
117     case 'A':
118         logitem(req, 'h', "X-Ash-Address");
119         break;
120     case 'H':
121         logitem(req, 'h', "Host");
122         break;
123     case 'R':
124         logitem(req, 'h', "Referer");
125         break;
126     case 'G':
127         logitem(req, 'h', "User-Agent");
128         break;
129     }
130 }
131
132 static void logreq(struct hthead *req)
133 {
134     char *p, *p2;
135     char d[strlen(format)];
136     char o;
137     
138     p = format;
139     while(*p) {
140         if(*p == '%') {
141             p++;
142             if(*p == '{') {
143                 p++;
144                 if((p2 = strchr(p, '}')) == NULL)
145                     continue;
146                 memcpy(d, p, p2 - p);
147                 d[p2 - p] = 0;
148                 p = p2 + 1;
149             } else {
150                 d[0] = 0;
151             }
152             o = *p++;
153             if(o == 0)
154                 break;
155             logitem(req, o, d);
156         } else {
157             fputc(*p++, out);
158         }
159     }
160     fputc('\n', out);
161     if(flush)
162         fflush(out);
163 }
164
165 static void serve(struct hthead *req, int fd)
166 {
167     gettimeofday(&now, NULL);
168     if(sendreq(ch, req, fd)) {
169         flog(LOG_ERR, "accesslog: could not pass request to child: %s", strerror(errno));
170         exit(1);
171     }
172     logreq(req);
173 }
174
175 static void sighandler(int sig)
176 {
177     if(sig == SIGHUP)
178         reopen = 1;
179 }
180
181 static int lockfile(FILE *file)
182 {
183     struct flock ld;
184     
185     memset(&ld, 0, sizeof(ld));
186     ld.l_type = F_WRLCK;
187     ld.l_whence = SEEK_SET;
188     ld.l_start = 0;
189     ld.l_len = 0;
190     return(fcntl(fileno(file), F_SETLK, &ld));
191 }
192
193 static void fetchpid(char *filename)
194 {
195     int fd, ret;
196     struct flock ld;
197     
198     if((fd = open(filename, O_WRONLY)) < 0) {
199         fprintf(stderr, "accesslog: %s: %s\n", filename, strerror(errno));
200         exit(1);
201     }
202     memset(&ld, 0, sizeof(ld));
203     ld.l_type = F_WRLCK;
204     ld.l_whence = SEEK_SET;
205     ld.l_start = 0;
206     ld.l_len = 0;
207     ret = fcntl(fd, F_GETLK, &ld);
208     close(fd);
209     if(ret) {
210         fprintf(stderr, "accesslog: %s: %s\n", filename, strerror(errno));
211         exit(1);
212     }
213     if(ld.l_type == F_UNLCK) {
214         fprintf(stderr, "accesslog: %s: not locked\n", filename);
215         exit(1);
216     }
217     printf("%i\n", (int)ld.l_pid);
218 }
219
220 static void reopenlog(void)
221 {
222     FILE *new;
223     struct stat olds, news;
224     
225     if(outname == NULL) {
226         flog(LOG_WARNING, "accesslog: received SIGHUP but logging to stdout, so ignoring");
227         return;
228     }
229     if(locklog) {
230         if(fstat(fileno(out), &olds)) {
231             flog(LOG_ERR, "accesslog: could not stat current logfile(?!): %s", strerror(errno));
232             return;
233         }
234         if(!stat(outname, &news)) {
235             if((olds.st_dev == news.st_dev) && (olds.st_ino == news.st_ino)) {
236                 /*
237                  * This needs to be ignored, because if the same logfile
238                  * is opened and then closed, the lock is lost. To quote
239                  * the Linux fcntl(2) manpage: "This is bad." No kidding.
240                  *
241                  * Technically, there is a race condition here when the
242                  * file has been stat'ed but not yet opened, where the old
243                  * log file, having been previously renamed, changes name
244                  * back to the name accesslog knows and is thus reopened
245                  * regardlessly, but I think that might fit under the
246                  * idiom "pathological case". It should, at least, not be
247                  * a security problem.
248                  */
249                 flog(LOG_INFO, "accesslog: received SIGHUP, but logfile has not changed, so ignoring");
250                 return;
251             }
252         }
253     }
254     if((new = fopen(outname, "a")) == NULL) {
255         flog(LOG_WARNING, "accesslog: could not reopen log file `%s' on SIGHUP: %s", outname, strerror(errno));
256         return;
257     }
258     fcntl(fileno(new), F_SETFD, FD_CLOEXEC);
259     if(locklog) {
260         if(lockfile(new)) {
261             if((errno == EAGAIN) || (errno == EACCES)) {
262                 flog(LOG_ERR, "accesslog: logfile is already locked; reverting to current log", strerror(errno));
263                 fclose(new);
264                 return;
265             } else {
266                 flog(LOG_WARNING, "accesslog: could not lock logfile, so no lock will be held: %s", strerror(errno));
267             }
268         }
269     }
270     fclose(out);
271     out = new;
272 }
273
274 static void usage(FILE *out)
275 {
276     fprintf(out, "usage: accesslog [-hFaL] [-f FORMAT] [-p PIDFILE] OUTFILE CHILD [ARGS...]\n");
277     fprintf(out, "       accesslog -P LOGFILE\n");
278 }
279
280 int main(int argc, char **argv)
281 {
282     int c, ret;
283     struct hthead *req;
284     int fd;
285     struct pollfd pfd[2];
286     char *pidfile;
287     FILE *pidout;
288     
289     pidfile = NULL;
290     while((c = getopt(argc, argv, "+hFaLf:p:P:")) >= 0) {
291         switch(c) {
292         case 'h':
293             usage(stdout);
294             exit(0);
295         case 'F':
296             flush = 0;
297             break;
298         case 'L':
299             locklog = 0;
300             break;
301         case 'f':
302             format = optarg;
303             break;
304         case 'P':
305             fetchpid(optarg);
306             exit(0);
307         case 'p':
308             pidfile = optarg;
309             break;
310         case 'a':
311             format = "%A - - [%{%d/%b/%Y:%H:%M:%S %z}t] \"%m %u %v\" - - \"%R\" \"%G\"";
312             break;
313         default:
314             usage(stderr);
315             exit(1);
316         }
317     }
318     if(argc - optind < 2) {
319         usage(stderr);
320         exit(1);
321     }
322     if(format == NULL)
323         format = DEFFORMAT;
324     if(!strcmp(argv[optind], "-"))
325         outname = NULL;
326     else
327         outname = argv[optind];
328     if(outname == NULL) {
329         out = stdout;
330         locklog = 0;
331     } else {
332         if((out = fopen(argv[optind], "a")) == NULL) {
333             flog(LOG_ERR, "accesslog: could not open %s for logging: %s", argv[optind], strerror(errno));
334             exit(1);
335         }
336         fcntl(fileno(out), F_SETFD, FD_CLOEXEC);
337     }
338     if(locklog) {
339         if(lockfile(out)) {
340             if((errno == EAGAIN) || (errno == EACCES)) {
341                 flog(LOG_ERR, "accesslog: logfile is already locked", strerror(errno));
342                 exit(1);
343             } else {
344                 flog(LOG_WARNING, "accesslog: could not lock logfile: %s", strerror(errno));
345             }
346         }
347     }
348     if((ch = stdmkchild(argv + optind + 1, NULL, NULL)) < 0) {
349         flog(LOG_ERR, "accesslog: could not fork child: %s", strerror(errno));
350         exit(1);
351     }
352     signal(SIGHUP, sighandler);
353     if(pidfile) {
354         if(!strcmp(pidfile, "-")) {
355             if(!outname) {
356                 flog(LOG_ERR, "accesslog: cannot derive PID file name without an output file");
357                 exit(1);
358             }
359             pidfile = sprintf2("%s.pid", outname);
360         }
361         if((pidout = fopen(pidfile, "w")) == NULL) {
362             flog(LOG_ERR, "accesslog: could not open PID file %s for writing: %s", pidfile);
363             exit(1);
364         }
365         fprintf(pidout, "%i\n", (int)getpid());
366         fclose(pidout);
367     }
368     while(1) {
369         if(reopen) {
370             reopenlog();
371             reopen = 0;
372         }
373         memset(pfd, 0, sizeof(pfd));
374         pfd[0].fd = 0;
375         pfd[0].events = POLLIN;
376         pfd[1].fd = ch;
377         pfd[1].events = POLLHUP;
378         if((ret = poll(pfd, 2, -1)) < 0) {
379             if(errno != EINTR) {
380                 flog(LOG_ERR, "accesslog: error in poll: %s", strerror(errno));
381                 exit(1);
382             }
383         }
384         if(pfd[0].revents) {
385             if((fd = recvreq(0, &req)) < 0) {
386                 if(errno == 0)
387                     break;
388                 flog(LOG_ERR, "accesslog: error in recvreq: %s", strerror(errno));
389                 exit(1);
390             }
391             serve(req, fd);
392             freehthead(req);
393             close(fd);
394         }
395         if(pfd[1].revents & POLLHUP)
396             break;
397     }
398     if(pidfile != NULL)
399         unlink(pidfile);
400     return(0);
401 }