1 /* $OpenBSD: newsyslog.c,v 1.115 2024/10/30 09:16:24 jan Exp $ */
2
3 /*
4 * Copyright (c) 1999, 2002, 2003 Todd C. Miller <millert@openbsd.org>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 *
18 * Sponsored in part by the Defense Advanced Research Projects
19 * Agency (DARPA) and Air Force Research Laboratory, Air Force
20 * Materiel Command, USAF, under agreement number F39502-99-1-0512.
21 */
22
23 /*
24 * Copyright (c) 1997, Jason Downs. All rights reserved.
25 *
26 * Redistribution and use in source and binary forms, with or without
27 * modification, are permitted provided that the following conditions
28 * are met:
29 * 1. Redistributions of source code must retain the above copyright
30 * notice, this list of conditions and the following disclaimer.
31 * 2. Redistributions in binary form must reproduce the above copyright
32 * notice, this list of conditions and the following disclaimer in the
33 * documentation and/or other materials provided with the distribution.
34 *
35 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS
36 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
37 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT,
39 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
40 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
41 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
42 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
43 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
44 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
45 * SUCH DAMAGE.
46 */
47
48 /*
49 * This file contains changes from the Open Software Foundation.
50 */
51
52 /*
53 * Copyright 1988, 1989 by the Massachusetts Institute of Technology
54 *
55 * Permission to use, copy, modify, and distribute this software
56 * and its documentation for any purpose and without fee is
57 * hereby granted, provided that the above copyright notice
58 * appear in all copies and that both that copyright notice and
59 * this permission notice appear in supporting documentation,
60 * and that the names of M.I.T. and the M.I.T. S.I.P.B. not be
61 * used in advertising or publicity pertaining to distribution
62 * of the software without specific, written prior permission.
63 * M.I.T. and the M.I.T. S.I.P.B. make no representations about
64 * the suitability of this software for any purpose. It is
65 * provided "as is" without express or implied warranty.
66 */
67
68 /*
69 * newsyslog - roll over selected logs at the appropriate time,
70 * keeping the specified number of backup files around.
71 *
72 */
73
74 #define CONF "/etc/newsyslog.conf"
75 #define PIDFILE "/var/run/syslog.pid"
76 #define COMPRESS "/usr/bin/gzip"
77 #define COMPRESS_POSTFIX ".gz"
78 #define STATS_DIR "/var/run"
79 #define SENDMAIL "/usr/sbin/sendmail"
80
81 #include <sys/param.h> /* DEV_BSIZE */
82 #include <sys/queue.h>
83 #include <sys/stat.h>
84 #include <sys/time.h>
85 #include <sys/wait.h>
86
87 #include <ctype.h>
88 #include <err.h>
89 #include <errno.h>
90 #include <fcntl.h>
91 #include <grp.h>
92 #include <limits.h>
93 #include <pwd.h>
94 #include <signal.h>
95 #include <stdio.h>
96 #include <stdlib.h>
97 #include <string.h>
98 #include <time.h>
99 #include <unistd.h>
100
101 #define CE_ROTATED 0x01 /* Log file has been rotated */
102 #define CE_COMPACT 0x02 /* Compact the archived log files */
103 #define CE_BINARY 0x04 /* Logfile is in binary, don't add */
104 /* status messages */
105 #define CE_MONITOR 0x08 /* Monitor for changes */
106 #define CE_FOLLOW 0x10 /* Follow symbolic links */
107 #define CE_TRIMAT 0x20 /* Trim at a specific time */
108
109 #define MIN_PID 2 /* Don't touch pids lower than this */
110 #define MIN_SIZE 256 /* Don't rotate if smaller (in bytes) */
111
112 #define DPRINTF(x) do { if (verbose) printf x ; } while (0)
113
114 struct conf_entry {
115 char *log; /* Name of the log */
116 char *logbase; /* Basename of the log */
117 char *backdir; /* Directory in which to store backups */
118 uid_t uid; /* Owner of log */
119 gid_t gid; /* Group of log */
120 int numlogs; /* Number of logs to keep */
121 off_t size; /* Size cutoff to trigger trimming the log */
122 int hours; /* Hours between log trimming */
123 time_t trim_at; /* Specific time at which to do trimming */
124 mode_t permissions; /* File permissions on the log */
125 int signal; /* Signal to send (defaults to SIGHUP) */
126 int flags; /* Flags (CE_COMPACT & CE_BINARY) */
127 char *whom; /* Whom to notify if logfile changes */
128 char *pidfile; /* Path to file containing pid to signal */
129 char *runcmd; /* Command to run instead of sending a signal */
130 TAILQ_ENTRY(conf_entry) next;
131 };
132 TAILQ_HEAD(entrylist, conf_entry);
133
134 struct pidinfo {
135 char *file;
136 int signal;
137 };
138
139 int verbose = 0; /* Print out what's going on */
140 int needroot = 1; /* Root privs are necessary */
141 int noaction = 0; /* Don't do anything, just show it */
142 int monitormode = 0; /* Don't do monitoring by default */
143 int force = 0; /* Force the logs to be rotated */
144 char *conf = CONF; /* Configuration file to use */
145 time_t timenow;
146 char hostname[HOST_NAME_MAX+1]; /* Hostname */
147 char daytime[33]; /* timenow in human readable form */
148 char *arcdir; /* Dir to put archives in (if it exists) */
149
150 char *lstat_log(char *, size_t, int);
151 char *missing_field(char *, char *, int);
152 char *sob(char *);
153 char *son(char *);
154 int age_old_log(struct conf_entry *);
155 int domonitor(struct conf_entry *);
156 int isnumberstr(char *);
157 int log_trim(char *);
158 int movefile(char *, char *, uid_t, gid_t, mode_t);
159 int stat_suffix(char *, size_t, char *, struct stat *,
160 int (*)(const char *, struct stat *));
161 off_t sizefile(struct stat *);
162 int parse_file(struct entrylist *, int *);
163 time_t parse8601(char *);
164 time_t parseDWM(char *);
165 void child_killer(int);
166 void compress_log(struct conf_entry *);
167 void do_entry(struct conf_entry *);
168 void dotrim(struct conf_entry *);
169 void rotate(struct conf_entry *, const char *);
170 void parse_args(int, char **);
171 void run_command(char *);
172 void send_signal(char *, int);
173 void usage(void);
174
175 int
main(int argc,char ** argv)176 main(int argc, char **argv)
177 {
178 struct entrylist config, runlist;
179 struct conf_entry *p, *q, *tmp;
180 struct pidinfo *pidlist, *pl;
181 int status, listlen, ret;
182 char **av;
183
184 parse_args(argc, argv);
185 argc -= optind;
186 argv += optind;
187
188 if (needroot && getuid() && geteuid())
189 errx(1, "You must be root.");
190
191 TAILQ_INIT(&config);
192 TAILQ_INIT(&runlist);
193
194 /* Keep passwd and group files open for faster lookups. */
195 setpassent(1);
196 setgroupent(1);
197
198 ret = parse_file(&config, &listlen);
199 if (argc == 0)
200 TAILQ_CONCAT(&runlist, &config, next);
201 else {
202 /* Only rotate specified files. */
203 listlen = 0;
204 for (av = argv; *av; av++) {
205 TAILQ_FOREACH_SAFE(q, &config, next, tmp)
206 if (strcmp(*av, q->log) == 0) {
207 TAILQ_REMOVE(&config, q, next);
208 TAILQ_INSERT_TAIL(&runlist, q, next);
209 listlen++;
210 break;
211 }
212 if (q == NULL)
213 warnx("%s: %s not found", conf, *av);
214 }
215 if (TAILQ_EMPTY(&runlist))
216 errx(1, "%s: no specified log files", conf);
217 }
218
219 pidlist = calloc(listlen + 1, sizeof(struct pidinfo));
220 if (pidlist == NULL)
221 err(1, NULL);
222
223 signal(SIGCHLD, child_killer);
224
225 /* Step 1, rotate all log files */
226 TAILQ_FOREACH(q, &runlist, next)
227 do_entry(q);
228
229 /* Step 2, make a list of unique pid files */
230 pl = pidlist;
231 TAILQ_FOREACH(q, &runlist, next) {
232 if (q->flags & CE_ROTATED) {
233 struct pidinfo *pltmp;
234
235 for (pltmp = pidlist; pltmp < pl; pltmp++) {
236 if ((q->pidfile && pltmp->file &&
237 strcmp(pltmp->file, q->pidfile) == 0 &&
238 pltmp->signal == q->signal) ||
239 (q->runcmd && pltmp->file &&
240 strcmp(q->runcmd, pltmp->file) == 0))
241 break;
242 }
243 if (pltmp == pl) { /* unique entry */
244 if (q->runcmd) {
245 pl->file = q->runcmd;
246 pl->signal = -1;
247 } else {
248 pl->file = q->pidfile;
249 pl->signal = q->signal;
250 }
251 pl++;
252 }
253 }
254 }
255
256 /* Step 3, send a signal or run a command */
257 for (pl--; pl >= pidlist; pl--) {
258 if (pl->file != NULL) {
259 if (pl->signal == -1)
260 run_command(pl->file);
261 else
262 send_signal(pl->file, pl->signal);
263 }
264 }
265 if (!noaction)
266 sleep(5);
267
268 /* Step 4, compress the log.0 file if configured to do so and free */
269 TAILQ_FOREACH(p, &runlist, next) {
270 if ((p->flags & CE_COMPACT) && (p->flags & CE_ROTATED) &&
271 p->numlogs > 0)
272 compress_log(p);
273 }
274
275 /* Wait for children to finish, then exit */
276 while (waitpid(-1, &status, 0) != -1)
277 ;
278 return (ret);
279 }
280
281 void
do_entry(struct conf_entry * ent)282 do_entry(struct conf_entry *ent)
283 {
284 struct stat sb;
285 int modhours;
286 off_t size;
287 int oversized;
288
289 if (lstat(ent->log, &sb) != 0)
290 return;
291 if (!S_ISREG(sb.st_mode) &&
292 (!S_ISLNK(sb.st_mode) || !(ent->flags & CE_FOLLOW))) {
293 DPRINTF(("--> not a regular file, skipping\n"));
294 return;
295 }
296 if (S_ISLNK(sb.st_mode) && stat(ent->log, &sb) != 0) {
297 DPRINTF(("--> link target does not exist, skipping\n"));
298 return;
299 }
300 if (ent->uid == (uid_t)-1)
301 ent->uid = sb.st_uid;
302 if (ent->gid == (gid_t)-1)
303 ent->gid = sb.st_gid;
304
305 DPRINTF(("%s <%d%s%s%s%s>: ", ent->log, ent->numlogs,
306 (ent->flags & CE_COMPACT) ? "Z" : "",
307 (ent->flags & CE_BINARY) ? "B" : "",
308 (ent->flags & CE_FOLLOW) ? "F" : "",
309 (ent->flags & CE_MONITOR) && monitormode ? "M" : ""));
310 size = sizefile(&sb);
311 oversized = (ent->size > 0 && size >= ent->size);
312 modhours = age_old_log(ent);
313 if (ent->flags & CE_TRIMAT && !force && !oversized) {
314 if (timenow < ent->trim_at ||
315 difftime(timenow, ent->trim_at) >= 60 * 60) {
316 DPRINTF(("--> will trim at %s",
317 ctime(&ent->trim_at)));
318 return;
319 } else if (ent->hours <= 0) {
320 DPRINTF(("--> time is up\n"));
321 }
322 }
323 if (ent->size > 0)
324 DPRINTF(("size (KB): %.2f [%d] ", size / 1024.0,
325 (int)(ent->size / 1024)));
326 if (ent->hours > 0)
327 DPRINTF(("age (hr): %d [%d] ", modhours, ent->hours));
328 if (monitormode && (ent->flags & CE_MONITOR) && domonitor(ent))
329 DPRINTF(("--> monitored\n"));
330 else if (!monitormode &&
331 (force || oversized ||
332 (ent->hours <= 0 && (ent->flags & CE_TRIMAT)) ||
333 (ent->hours > 0 && (modhours >= ent->hours || modhours < 0)
334 && ((ent->flags & CE_BINARY) || size >= MIN_SIZE)))) {
335 DPRINTF(("--> trimming log....\n"));
336 if (noaction && !verbose)
337 printf("%s <%d%s%s%s>\n", ent->log,
338 ent->numlogs,
339 (ent->flags & CE_COMPACT) ? "Z" : "",
340 (ent->flags & CE_BINARY) ? "B" : "",
341 (ent->flags & CE_FOLLOW) ? "F" : "");
342 dotrim(ent);
343 ent->flags |= CE_ROTATED;
344 } else
345 DPRINTF(("--> skipping\n"));
346 }
347
348 /* Run the specified command */
349 void
run_command(char * cmd)350 run_command(char *cmd)
351 {
352 if (noaction)
353 (void)printf("run %s\n", cmd);
354 else
355 system(cmd);
356 }
357
358 /* Send a signal to the pid specified by pidfile */
359 void
send_signal(char * pidfile,int signal)360 send_signal(char *pidfile, int signal)
361 {
362 char line[BUFSIZ], *ep, *err;
363 pid_t pid;
364 long lval;
365 FILE *f;
366
367 if ((f = fopen(pidfile, "r")) == NULL) {
368 warn("can't open %s", pidfile);
369 return;
370 }
371
372 pid = 0;
373 errno = 0;
374 err = NULL;
375 if (fgets(line, sizeof(line), f)) {
376 lval = strtol(line, &ep, 10);
377 if (line[0] == '\0' || (*ep != '\0' && *ep != '\n'))
378 err = "invalid number in";
379 else if (lval < 0 || (errno == ERANGE && lval == LONG_MAX))
380 err = "out of range number in";
381 else if (lval == 0)
382 err = "no number in";
383 else if (lval < MIN_PID)
384 err = "preposterous process number in";
385 else
386 pid = (pid_t)lval;
387 } else {
388 if (errno == 0)
389 err = "empty";
390 else
391 err = "error reading";
392 }
393 (void)fclose(f);
394
395 if (err)
396 warnx("%s pid file: %s", err, pidfile);
397 else if (noaction)
398 (void)printf("kill -%s %ld\n", sys_signame[signal], (long)pid);
399 else if (kill(pid, signal))
400 warnx("warning - could not send SIG%s to PID from pid file %s",
401 sys_signame[signal], pidfile);
402 }
403
404 void
parse_args(int argc,char ** argv)405 parse_args(int argc, char **argv)
406 {
407 struct timeval now;
408 struct tm *tm;
409 size_t l;
410 char *p;
411 int ch;
412
413 gettimeofday(&now, NULL);
414 timenow = now.tv_sec;
415 tm = gmtime(&now.tv_sec);
416 l = strftime(daytime, sizeof(daytime), "%FT%T", tm);
417 snprintf(daytime + l, sizeof(daytime) - l, ".%03ldZ",
418 now.tv_usec / 1000);
419
420 /* Let's get our hostname */
421 (void)gethostname(hostname, sizeof(hostname));
422
423 /* Truncate domain */
424 if ((p = strchr(hostname, '.')) != NULL)
425 *p = '\0';
426
427 while ((ch = getopt(argc, argv, "Fmnrva:f:")) != -1) {
428 switch (ch) {
429 case 'a':
430 arcdir = optarg;
431 break;
432 case 'n':
433 noaction = 1; /* This implies needroot as off */
434 /* fall through */
435 case 'r':
436 needroot = 0;
437 break;
438 case 'v':
439 verbose = 1;
440 break;
441 case 'f':
442 conf = optarg;
443 break;
444 case 'm':
445 monitormode = 1;
446 break;
447 case 'F':
448 force = 1;
449 break;
450 default:
451 usage();
452 }
453 }
454 if (monitormode && force)
455 errx(1, "cannot specify both -m and -F flags");
456 }
457
458 void
usage(void)459 usage(void)
460 {
461 extern const char *__progname;
462
463 (void)fprintf(stderr, "usage: %s [-Fmnrv] [-a directory] "
464 "[-f config_file] [log ...]\n", __progname);
465 exit(1);
466 }
467
468 /*
469 * Parse a configuration file and build a linked list of all the logs
470 * to process
471 */
472 int
parse_file(struct entrylist * list,int * nentries)473 parse_file(struct entrylist *list, int *nentries)
474 {
475 char line[BUFSIZ], *parse, *q, *errline, *group, *tmp, *ep;
476 struct conf_entry *working;
477 struct stat sb;
478 int lineno = 0;
479 int ret = 0;
480 FILE *f;
481 long l;
482
483 if (strcmp(conf, "-") == 0)
484 f = stdin;
485 else if ((f = fopen(conf, "r")) == NULL)
486 err(1, "can't open %s", conf);
487
488 *nentries = 0;
489
490 nextline:
491 while (fgets(line, sizeof(line), f) != NULL) {
492 lineno++;
493 tmp = sob(line);
494 if (*tmp == '\0' || *tmp == '#')
495 continue;
496 errline = strdup(tmp);
497 if (errline == NULL)
498 err(1, NULL);
499 working = calloc(1, sizeof(*working));
500 if (working == NULL)
501 err(1, NULL);
502
503 q = parse = missing_field(sob(line), errline, lineno);
504 *(parse = son(line)) = '\0';
505 working->log = strdup(q);
506 if (working->log == NULL)
507 err(1, NULL);
508
509 if ((working->logbase = strrchr(working->log, '/')) != NULL)
510 working->logbase++;
511
512 q = parse = missing_field(sob(++parse), errline, lineno);
513 *(parse = son(parse)) = '\0';
514 if ((group = strchr(q, ':')) != NULL ||
515 (group = strrchr(q, '.')) != NULL) {
516 *group++ = '\0';
517 if (*q == '\0') {
518 working->uid = (uid_t)-1;
519 } else if (isnumberstr(q)) {
520 working->uid = atoi(q);
521 } else if (uid_from_user(q, &working->uid) == -1) {
522 warnx("%s:%d: unknown user %s --> skipping",
523 conf, lineno, q);
524 ret = 1;
525 goto nextline;
526 }
527
528 q = group;
529 if (*q == '\0') {
530 working->gid = (gid_t)-1;
531 } else if (isnumberstr(q)) {
532 working->gid = atoi(q);
533 } else if (gid_from_group(q, &working->gid) == -1) {
534 warnx("%s:%d: unknown group %s --> skipping",
535 conf, lineno, q);
536 ret = 1;
537 goto nextline;
538 }
539
540 q = parse = missing_field(sob(++parse), errline, lineno);
541 *(parse = son(parse)) = '\0';
542 } else {
543 working->uid = (uid_t)-1;
544 working->gid = (gid_t)-1;
545 }
546
547 l = strtol(q, &ep, 8);
548 if (*ep != '\0' || l < 0 || l > ALLPERMS) {
549 warnx("%s:%d: bad permissions: %s --> skipping", conf,
550 lineno, q);
551 ret = 1;
552 goto nextline;
553 }
554 working->permissions = (mode_t)l;
555
556 q = parse = missing_field(sob(++parse), errline, lineno);
557 *(parse = son(parse)) = '\0';
558 l = strtol(q, &ep, 10);
559 if (*ep != '\0' || l < 0 || l >= INT_MAX) {
560 warnx("%s:%d: bad number: %s --> skipping", conf,
561 lineno, q);
562 ret = 1;
563 goto nextline;
564 }
565 working->numlogs = (int)l;
566
567 q = parse = missing_field(sob(++parse), errline, lineno);
568 *(parse = son(parse)) = '\0';
569 if (isdigit((unsigned char)*q))
570 working->size = atoi(q) * 1024;
571 else
572 working->size = -1;
573
574 working->flags = 0;
575 q = parse = missing_field(sob(++parse), errline, lineno);
576 *(parse = son(parse)) = '\0';
577 l = strtol(q, &ep, 10);
578 if (l < 0 || l >= INT_MAX) {
579 warnx("%s:%d: interval out of range: %s --> skipping",
580 conf, lineno, q);
581 ret = 1;
582 goto nextline;
583 }
584 working->hours = (int)l;
585 switch (*ep) {
586 case '\0':
587 break;
588 case '@':
589 working->trim_at = parse8601(ep + 1);
590 if (working->trim_at == (time_t) - 1) {
591 warnx("%s:%d: bad time: %s --> skipping", conf,
592 lineno, q);
593 ret = 1;
594 goto nextline;
595 }
596 working->flags |= CE_TRIMAT;
597 break;
598 case '$':
599 working->trim_at = parseDWM(ep + 1);
600 if (working->trim_at == (time_t) - 1) {
601 warnx("%s:%d: bad time: %s --> skipping", conf,
602 lineno, q);
603 ret = 1;
604 goto nextline;
605 }
606 working->flags |= CE_TRIMAT;
607 break;
608 case '*':
609 if (q == ep)
610 break;
611 /* FALLTHROUGH */
612 default:
613 warnx("%s:%d: bad interval/at: %s --> skipping", conf,
614 lineno, q);
615 ret = 1;
616 goto nextline;
617 }
618
619 q = sob(++parse); /* Optional field */
620 if (*q == 'Z' || *q == 'z' || *q == 'B' || *q == 'b' ||
621 *q == 'M' || *q == 'm' || *q == 'F' || *q == 'f') {
622 *(parse = son(q)) = '\0';
623 while (*q) {
624 switch (*q) {
625 case 'Z':
626 case 'z':
627 working->flags |= CE_COMPACT;
628 break;
629 case 'B':
630 case 'b':
631 working->flags |= CE_BINARY;
632 break;
633 case 'M':
634 case 'm':
635 working->flags |= CE_MONITOR;
636 break;
637 case 'F':
638 case 'f':
639 working->flags |= CE_FOLLOW;
640 break;
641 default:
642 warnx("%s:%d: illegal flag: `%c'"
643 " --> skipping",
644 conf, lineno, *q);
645 ret = 1;
646 goto nextline;
647 }
648 q++;
649 }
650 } else
651 parse--; /* no flags so undo */
652
653 working->pidfile = PIDFILE;
654 working->signal = SIGHUP;
655 working->runcmd = NULL;
656 working->whom = NULL;
657 for (;;) {
658 q = parse = sob(++parse); /* Optional field */
659 if (q == NULL || *q == '\0')
660 break;
661 if (*q == '/') {
662 *(parse = son(parse)) = '\0';
663 if (strlen(q) >= PATH_MAX) {
664 warnx("%s:%d: pathname too long: %s"
665 " --> skipping",
666 conf, lineno, q);
667 ret = 1;
668 goto nextline;
669 }
670 working->pidfile = strdup(q);
671 if (working->pidfile == NULL)
672 err(1, NULL);
673 } else if (*q == '"' && (tmp = strchr(q + 1, '"'))) {
674 *(parse = tmp) = '\0';
675 if (*++q != '\0') {
676 working->runcmd = strdup(q);
677 if (working->runcmd == NULL)
678 err(1, NULL);
679 }
680 working->pidfile = NULL;
681 working->signal = -1;
682 } else if (strncmp(q, "SIG", 3) == 0) {
683 int i;
684
685 *(parse = son(parse)) = '\0';
686 for (i = 1; i < NSIG; i++) {
687 if (!strcmp(sys_signame[i], q + 3)) {
688 working->signal = i;
689 break;
690 }
691 }
692 if (i == NSIG) {
693 warnx("%s:%d: unknown signal: %s"
694 " --> skipping",
695 conf, lineno, q);
696 ret = 1;
697 goto nextline;
698 }
699 } else if (working->flags & CE_MONITOR) {
700 *(parse = son(parse)) = '\0';
701 working->whom = strdup(q);
702 if (working->whom == NULL)
703 err(1, NULL);
704 } else {
705 warnx("%s:%d: unrecognized field: %s"
706 " --> skipping",
707 conf, lineno, q);
708 ret = 1;
709 goto nextline;
710 }
711 }
712 free(errline);
713
714 if ((working->flags & CE_MONITOR) && working->whom == NULL) {
715 warnx("%s:%d: missing monitor notification field"
716 " --> skipping",
717 conf, lineno);
718 ret = 1;
719 goto nextline;
720 }
721
722 /* If there is an arcdir, set working->backdir. */
723 if (arcdir != NULL && working->logbase != NULL) {
724 if (*arcdir == '/') {
725 /* Fully qualified arcdir */
726 working->backdir = arcdir;
727 } else {
728 /* arcdir is relative to log's parent dir */
729 *(working->logbase - 1) = '\0';
730 if ((asprintf(&working->backdir, "%s/%s",
731 working->log, arcdir)) == -1)
732 err(1, NULL);
733 *(working->logbase - 1) = '/';
734 }
735 /* Ignore arcdir if it doesn't exist. */
736 if (stat(working->backdir, &sb) != 0 ||
737 !S_ISDIR(sb.st_mode)) {
738 if (working->backdir != arcdir)
739 free(working->backdir);
740 working->backdir = NULL;
741 }
742 } else
743 working->backdir = NULL;
744
745 /* Make sure we can't oflow PATH_MAX */
746 if (working->backdir != NULL) {
747 if (snprintf(line, sizeof(line), "%s/%s.%d%s",
748 working->backdir, working->logbase,
749 working->numlogs, COMPRESS_POSTFIX) >= PATH_MAX) {
750 warnx("%s:%d: pathname too long: %s"
751 " --> skipping", conf, lineno, q);
752 ret = 1;
753 goto nextline;
754 }
755 } else {
756 if (snprintf(line, sizeof(line), "%s.%d%s",
757 working->log, working->numlogs, COMPRESS_POSTFIX)
758 >= PATH_MAX) {
759 warnx("%s:%d: pathname too long: %s"
760 " --> skipping", conf, lineno,
761 working->log);
762 ret = 1;
763 goto nextline;
764 }
765 }
766 TAILQ_INSERT_TAIL(list, working, next);
767 (*nentries)++;
768 }
769 (void)fclose(f);
770 return (ret);
771 }
772
773 char *
missing_field(char * p,char * errline,int lineno)774 missing_field(char *p, char *errline, int lineno)
775 {
776 if (p == NULL || *p == '\0') {
777 warnx("%s:%d: missing field", conf, lineno);
778 fputs(errline, stderr);
779 exit(1);
780 }
781 return (p);
782 }
783
784 void
rotate(struct conf_entry * ent,const char * oldlog)785 rotate(struct conf_entry *ent, const char *oldlog)
786 {
787 char file1[PATH_MAX], file2[PATH_MAX], *suffix;
788 int numdays = ent->numlogs - 1;
789 int done = 0;
790
791 /* Remove old logs */
792 do {
793 (void)snprintf(file1, sizeof(file1), "%s.%d", oldlog, numdays);
794 (void)snprintf(file2, sizeof(file2), "%s.%d%s", oldlog,
795 numdays, COMPRESS_POSTFIX);
796 if (noaction) {
797 printf("\trm -f %s %s\n", file1, file2);
798 done = access(file1, 0) && access(file2, 0);
799 } else {
800 done = unlink(file1) && unlink(file2);
801 }
802 numdays++;
803 } while (done == 0);
804
805 /* Move down log files */
806 for (numdays = ent->numlogs - 2; numdays >= 0; numdays--) {
807 /*
808 * If both the compressed archive and the non-compressed archive
809 * exist, we decide which to rotate based on the CE_COMPACT flag
810 */
811 (void)snprintf(file1, sizeof(file1), "%s.%d", oldlog, numdays);
812 suffix = lstat_log(file1, sizeof(file1), ent->flags);
813 if (suffix == NULL)
814 continue;
815 (void)snprintf(file2, sizeof(file2), "%s.%d%s", oldlog,
816 numdays + 1, suffix);
817
818 if (noaction) {
819 printf("\tmv %s %s\n", file1, file2);
820 printf("\tchmod %o %s\n", ent->permissions, file2);
821 printf("\tchown %u:%u %s\n", ent->uid, ent->gid, file2);
822 } else {
823 if (rename(file1, file2))
824 warn("can't mv %s to %s", file1, file2);
825 if (chmod(file2, ent->permissions))
826 warn("can't chmod %s", file2);
827 if (chown(file2, ent->uid, ent->gid))
828 warn("can't chown %s", file2);
829 }
830 }
831 }
832
833 void
dotrim(struct conf_entry * ent)834 dotrim(struct conf_entry *ent)
835 {
836 char file1[PATH_MAX], file2[PATH_MAX], oldlog[PATH_MAX];
837 int fd;
838
839 /* Is there a separate backup dir? */
840 if (ent->backdir != NULL)
841 snprintf(oldlog, sizeof(oldlog), "%s/%s", ent->backdir,
842 ent->logbase);
843 else
844 strlcpy(oldlog, ent->log, sizeof(oldlog));
845
846 if (ent->numlogs > 0)
847 rotate(ent, oldlog);
848 if (!noaction && !(ent->flags & CE_BINARY))
849 (void)log_trim(ent->log);
850
851 (void)snprintf(file2, sizeof(file2), "%s.XXXXXXXXXX", ent->log);
852 if (noaction) {
853 printf("\tmktemp %s\n", file2);
854 } else {
855 if ((fd = mkstemp(file2)) == -1)
856 err(1, "can't start '%s' log", file2);
857 if (fchmod(fd, ent->permissions))
858 err(1, "can't chmod '%s' log file", file2);
859 if (fchown(fd, ent->uid, ent->gid))
860 err(1, "can't chown '%s' log file", file2);
861 (void)close(fd);
862 /* Add status message */
863 if (!(ent->flags & CE_BINARY) && log_trim(file2))
864 err(1, "can't add status message to log '%s'", file2);
865 }
866
867 if (ent->numlogs == 0) {
868 if (noaction)
869 printf("\trm %s\n", ent->log);
870 else if (unlink(ent->log))
871 warn("can't rm %s", ent->log);
872 } else {
873 (void)snprintf(file1, sizeof(file1), "%s.0", oldlog);
874 if (noaction) {
875 printf("\tmv %s to %s\n", ent->log, file1);
876 printf("\tchmod %o %s\n", ent->permissions, file1);
877 printf("\tchown %u:%u %s\n", ent->uid, ent->gid, file1);
878 } else if (movefile(ent->log, file1, ent->uid, ent->gid,
879 ent->permissions))
880 warn("can't mv %s to %s", ent->log, file1);
881 }
882
883 /* Now move the new log file into place */
884 if (noaction)
885 printf("\tmv %s to %s\n", file2, ent->log);
886 else if (rename(file2, ent->log))
887 warn("can't mv %s to %s", file2, ent->log);
888 }
889
890 /* Log the fact that the logs were turned over */
891 int
log_trim(char * log)892 log_trim(char *log)
893 {
894 FILE *f;
895
896 if ((f = fopen(log, "a")) == NULL)
897 return (-1);
898 (void)fprintf(f, "%s %s newsyslog[%ld]: logfile turned over\n",
899 daytime, hostname, (long)getpid());
900 if (fclose(f) == EOF)
901 err(1, "log_trim: fclose");
902 return (0);
903 }
904
905 /* Fork off compress or gzip to compress the old log file */
906 void
compress_log(struct conf_entry * ent)907 compress_log(struct conf_entry *ent)
908 {
909 char *base, tmp[PATH_MAX];
910 pid_t pid;
911
912 if (ent->backdir != NULL)
913 snprintf(tmp, sizeof(tmp), "%s/%s.0", ent->backdir,
914 ent->logbase);
915 else
916 snprintf(tmp, sizeof(tmp), "%s.0", ent->log);
917
918 if ((base = strrchr(COMPRESS, '/')) == NULL)
919 base = COMPRESS;
920 else
921 base++;
922 if (noaction) {
923 printf("%s %s\n", base, tmp);
924 return;
925 }
926 pid = fork();
927 if (pid == -1) {
928 err(1, "fork");
929 } else if (pid == 0) {
930 (void)execl(COMPRESS, base, "-f", tmp, (char *)NULL);
931 warn(COMPRESS);
932 _exit(1);
933 }
934 }
935
936 /* Return size in bytes of a file */
937 off_t
sizefile(struct stat * sb)938 sizefile(struct stat *sb)
939 {
940 /* For sparse files, return the size based on number of blocks used. */
941 if (sb->st_size / DEV_BSIZE > sb->st_blocks)
942 return (sb->st_blocks * DEV_BSIZE);
943 else
944 return (sb->st_size);
945 }
946
947 /* Return the age (in hours) of old log file (file.0), or -1 if none */
948 int
age_old_log(struct conf_entry * ent)949 age_old_log(struct conf_entry *ent)
950 {
951 char file[PATH_MAX];
952 struct stat sb;
953
954 if (ent->backdir != NULL)
955 (void)snprintf(file, sizeof(file), "%s/%s.0", ent->backdir,
956 ent->logbase);
957 else
958 (void)snprintf(file, sizeof(file), "%s.0", ent->log);
959 if (ent->flags & CE_COMPACT) {
960 if (stat_suffix(file, sizeof(file), COMPRESS_POSTFIX, &sb,
961 stat) < 0 && stat(file, &sb) == -1)
962 return (-1);
963 } else {
964 if (stat(file, &sb) == -1 && stat_suffix(file, sizeof(file),
965 COMPRESS_POSTFIX, &sb, stat) < 0)
966 return (-1);
967 }
968 return ((int)(timenow - sb.st_mtime + 1800) / 3600);
969 }
970
971 /* Skip Over Blanks */
972 char *
sob(char * p)973 sob(char *p)
974 {
975 if (p == NULL)
976 return(p);
977 while (isspace((unsigned char)*p))
978 p++;
979 return (p);
980 }
981
982 /* Skip Over Non-Blanks */
983 char *
son(char * p)984 son(char *p)
985 {
986 while (p && *p && !isspace((unsigned char)*p))
987 p++;
988 return (p);
989 }
990
991 /* Check if string is actually a number */
992 int
isnumberstr(char * string)993 isnumberstr(char *string)
994 {
995 while (*string) {
996 if (!isdigit((unsigned char)*string++))
997 return (0);
998 }
999 return (1);
1000 }
1001
1002 int
domonitor(struct conf_entry * ent)1003 domonitor(struct conf_entry *ent)
1004 {
1005 char fname[PATH_MAX], *flog, *p, *rb = NULL;
1006 struct stat sb, tsb;
1007 off_t osize;
1008 FILE *fp;
1009 int rd;
1010
1011 if (stat(ent->log, &sb) == -1)
1012 return (0);
1013
1014 if (noaction) {
1015 if (!verbose)
1016 printf("%s: monitored\n", ent->log);
1017 return (1);
1018 }
1019
1020 flog = strdup(ent->log);
1021 if (flog == NULL)
1022 err(1, NULL);
1023
1024 for (p = flog; *p != '\0'; p++) {
1025 if (*p == '/')
1026 *p = '_';
1027 }
1028 snprintf(fname, sizeof(fname), "%s/newsyslog.%s.size",
1029 STATS_DIR, flog);
1030
1031 /* ..if it doesn't exist, simply record the current size. */
1032 if ((sb.st_size == 0) || stat(fname, &tsb) == -1)
1033 goto update;
1034
1035 fp = fopen(fname, "r");
1036 if (fp == NULL) {
1037 warn("%s", fname);
1038 goto cleanup;
1039 }
1040 if (fscanf(fp, "%lld\n", &osize) != 1) {
1041 fclose(fp);
1042 goto update;
1043 }
1044
1045 fclose(fp);
1046
1047 /* If the file is smaller, mark the entire thing as changed. */
1048 if (sb.st_size < osize)
1049 osize = 0;
1050
1051 /* Now see if current size is larger. */
1052 if (sb.st_size > osize) {
1053 rb = malloc(sb.st_size - osize);
1054 if (rb == NULL)
1055 err(1, NULL);
1056
1057 /* Open logfile, seek. */
1058 fp = fopen(ent->log, "r");
1059 if (fp == NULL) {
1060 warn("%s", ent->log);
1061 goto cleanup;
1062 }
1063 fseek(fp, osize, SEEK_SET);
1064 rd = fread(rb, 1, sb.st_size - osize, fp);
1065 if (rd < 1) {
1066 warn("fread");
1067 fclose(fp);
1068 goto cleanup;
1069 }
1070 fclose(fp);
1071
1072 /* Send message. */
1073 fp = popen(SENDMAIL " -t", "w");
1074 if (fp == NULL) {
1075 warn("popen");
1076 goto cleanup;
1077 }
1078 fprintf(fp, "Auto-Submitted: auto-generated\n");
1079 fprintf(fp, "To: %s\nSubject: LOGFILE NOTIFICATION: %s\n\n\n",
1080 ent->whom, ent->log);
1081 fwrite(rb, 1, rd, fp);
1082 fputs("\n\n", fp);
1083
1084 pclose(fp);
1085 }
1086 update:
1087 /* Reopen for writing and update file. */
1088 fp = fopen(fname, "w");
1089 if (fp == NULL) {
1090 warn("%s", fname);
1091 goto cleanup;
1092 }
1093 fprintf(fp, "%lld\n", (long long)sb.st_size);
1094 fclose(fp);
1095
1096 cleanup:
1097 free(flog);
1098 free(rb);
1099 return (1);
1100 }
1101
1102 void
child_killer(int signo)1103 child_killer(int signo)
1104 {
1105 int save_errno = errno;
1106 int status;
1107
1108 while (waitpid(-1, &status, WNOHANG) > 0)
1109 ;
1110 errno = save_errno;
1111 }
1112
1113 int
stat_suffix(char * file,size_t size,char * suffix,struct stat * sp,int (* func)(const char *,struct stat *))1114 stat_suffix(char *file, size_t size, char *suffix, struct stat *sp,
1115 int (*func)(const char *, struct stat *))
1116 {
1117 size_t n;
1118
1119 n = strlcat(file, suffix, size);
1120 if (n < size && func(file, sp) == 0)
1121 return (0);
1122 file[n - strlen(suffix)] = '\0';
1123 return (-1);
1124 }
1125
1126 /*
1127 * lstat() a log, possibly appending a suffix; order is based on flags.
1128 * Returns the suffix appended (may be empty string) or NULL if no file.
1129 */
1130 char *
lstat_log(char * file,size_t size,int flags)1131 lstat_log(char *file, size_t size, int flags)
1132 {
1133 struct stat sb;
1134
1135 if (flags & CE_COMPACT) {
1136 if (stat_suffix(file, size, COMPRESS_POSTFIX, &sb, lstat) == 0)
1137 return (COMPRESS_POSTFIX);
1138 if (lstat(file, &sb) == 0)
1139 return ("");
1140 } else {
1141 if (lstat(file, &sb) == 0)
1142 return ("");
1143 if (stat_suffix(file, size, COMPRESS_POSTFIX, &sb, lstat) == 0)
1144 return (COMPRESS_POSTFIX);
1145
1146 }
1147 return (NULL);
1148 }
1149
1150 /*
1151 * Parse a limited subset of ISO 8601. The specific format is as follows:
1152 *
1153 * [CC[YY[MM[DD]]]][THH[MM[SS]]] (where `T' is the literal letter)
1154 *
1155 * We don't accept a timezone specification; missing fields (including timezone)
1156 * are defaulted to the current date but time zero.
1157 */
1158 time_t
parse8601(char * s)1159 parse8601(char *s)
1160 {
1161 struct tm tm, *tmp;
1162 char *t;
1163 long l;
1164
1165 tmp = localtime(&timenow);
1166 tm = *tmp;
1167
1168 tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
1169
1170 l = strtol(s, &t, 10);
1171 if (l < 0 || l >= INT_MAX || (*t != '\0' && *t != 'T'))
1172 return (-1);
1173
1174 /*
1175 * Now t points either to the end of the string (if no time was
1176 * provided) or to the letter `T' which separates date and time in
1177 * ISO 8601. The pointer arithmetic is the same for either case.
1178 */
1179 switch (t - s) {
1180 case 8:
1181 tm.tm_year = ((l / 1000000) - 19) * 100;
1182 l = l % 1000000;
1183 case 6:
1184 tm.tm_year -= tm.tm_year % 100;
1185 tm.tm_year += l / 10000;
1186 l = l % 10000;
1187 case 4:
1188 tm.tm_mon = (l / 100) - 1;
1189 l = l % 100;
1190 case 2:
1191 tm.tm_mday = l;
1192 case 0:
1193 break;
1194 default:
1195 return (-1);
1196 }
1197
1198 /* sanity check */
1199 if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 12 ||
1200 tm.tm_mday < 1 || tm.tm_mday > 31)
1201 return (-1);
1202
1203 if (*t != '\0') {
1204 s = ++t;
1205 l = strtol(s, &t, 10);
1206 if (l < 0 || l >= INT_MAX ||
1207 (*t != '\0' && !isspace((unsigned char)*t)))
1208 return (-1);
1209
1210 switch (t - s) {
1211 case 6:
1212 tm.tm_sec = l % 100;
1213 l /= 100;
1214 case 4:
1215 tm.tm_min = l % 100;
1216 l /= 100;
1217 case 2:
1218 tm.tm_hour = l;
1219 case 0:
1220 break;
1221 default:
1222 return (-1);
1223 }
1224
1225 /* sanity check */
1226 if (tm.tm_sec < 0 || tm.tm_sec > 60 || tm.tm_min < 0 ||
1227 tm.tm_min > 59 || tm.tm_hour < 0 || tm.tm_hour > 23)
1228 return (-1);
1229 }
1230 return (mktime(&tm));
1231 }
1232
1233 /*-
1234 * Parse a cyclic time specification, the format is as follows:
1235 *
1236 * [Dhh] or [Wd[Dhh]] or [Mdd[Dhh]]
1237 *
1238 * to rotate a logfile cyclic at
1239 *
1240 * - every day (D) within a specific hour (hh) (hh = 0...23)
1241 * - once a week (W) at a specific day (d) OR (d = 0..6, 0 = Sunday)
1242 * - once a month (M) at a specific day (d) (d = 1..31,l|L)
1243 *
1244 * We don't accept a timezone specification; missing fields
1245 * are defaulted to the current date but time zero.
1246 */
1247 time_t
parseDWM(char * s)1248 parseDWM(char *s)
1249 {
1250 static int mtab[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1251 int WMseen = 0, Dseen = 0, nd;
1252 struct tm tm, *tmp;
1253 char *t;
1254 long l;
1255
1256 tmp = localtime(&timenow);
1257 tm = *tmp;
1258
1259 /* set no. of days per month */
1260
1261 nd = mtab[tm.tm_mon];
1262
1263 if (tm.tm_mon == 1) {
1264 if (((tm.tm_year + 1900) % 4 == 0) &&
1265 ((tm.tm_year + 1900) % 100 != 0) &&
1266 ((tm.tm_year + 1900) % 400 == 0)) {
1267 nd++; /* leap year, 29 days in february */
1268 }
1269 }
1270 tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
1271
1272 for (;;) {
1273 switch (*s) {
1274 case 'D':
1275 if (Dseen)
1276 return (-1);
1277 Dseen++;
1278 s++;
1279 l = strtol(s, &t, 10);
1280 if (l < 0 || l > 23)
1281 return (-1);
1282 tm.tm_hour = l;
1283 break;
1284
1285 case 'W':
1286 if (WMseen)
1287 return (-1);
1288 WMseen++;
1289 s++;
1290 l = strtol(s, &t, 10);
1291 if (l < 0 || l > 6)
1292 return (-1);
1293 if (l != tm.tm_wday) {
1294 int save;
1295
1296 if (l < tm.tm_wday) {
1297 save = 6 - tm.tm_wday;
1298 save += (l + 1);
1299 } else {
1300 save = l - tm.tm_wday;
1301 }
1302
1303 tm.tm_mday += save;
1304
1305 if (tm.tm_mday > nd) {
1306 tm.tm_mon++;
1307 tm.tm_mday = tm.tm_mday - nd;
1308 }
1309 }
1310 break;
1311
1312 case 'M':
1313 if (WMseen)
1314 return (-1);
1315 WMseen++;
1316 s++;
1317 if (tolower((unsigned char)*s) == 'l') {
1318 tm.tm_mday = nd;
1319 s++;
1320 t = s;
1321 } else {
1322 l = strtol(s, &t, 10);
1323 if (l < 1 || l > 31)
1324 return (-1);
1325
1326 if (l > nd)
1327 return (-1);
1328 if (l < tm.tm_mday)
1329 tm.tm_mon++;
1330 tm.tm_mday = l;
1331 }
1332 break;
1333
1334 default:
1335 return (-1);
1336 break;
1337 }
1338
1339 if (*t == '\0' || isspace((unsigned char)*t))
1340 break;
1341 else
1342 s = t;
1343 }
1344 return (mktime(&tm));
1345 }
1346
1347 /*
1348 * Move a file using rename(2) if possible and copying if not.
1349 */
1350 int
movefile(char * from,char * to,uid_t owner_uid,gid_t group_gid,mode_t perm)1351 movefile(char *from, char *to, uid_t owner_uid, gid_t group_gid, mode_t perm)
1352 {
1353 FILE *src, *dst;
1354 int i;
1355
1356 /* try rename(2) first */
1357 if (rename(from, to) == 0) {
1358 if (chmod(to, perm))
1359 warn("can't chmod %s", to);
1360 if (chown(to, owner_uid, group_gid))
1361 warn("can't chown %s", to);
1362 return (0);
1363 } else if (errno != EXDEV)
1364 return (-1);
1365
1366 /* different filesystem, have to copy the file */
1367 if ((src = fopen(from, "r")) == NULL)
1368 err(1, "can't fopen %s for reading", from);
1369 if ((dst = fopen(to, "w")) == NULL)
1370 err(1, "can't fopen %s for writing", to);
1371 if (fchmod(fileno(dst), perm))
1372 err(1, "can't fchmod %s", to);
1373 if (fchown(fileno(dst), owner_uid, group_gid))
1374 err(1, "can't fchown %s", to);
1375
1376 while ((i = getc(src)) != EOF) {
1377 if ((putc(i, dst)) == EOF)
1378 err(1, "error writing to %s", to);
1379 }
1380
1381 if (ferror(src))
1382 err(1, "error reading from %s", from);
1383 if ((fclose(src)) != 0)
1384 err(1, "can't fclose %s", from);
1385 if ((fclose(dst)) != 0)
1386 err(1, "can't fclose %s", to);
1387 if ((unlink(from)) != 0)
1388 err(1, "can't unlink %s", from);
1389
1390 return (0);
1391 }
1392