2 * Copyright (c) 2009-2010 Todd C. Miller <Todd.Miller@courtesan.com>
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 #include <sys/types.h>
20 #include <sys/param.h>
24 #include <sys/ioctl.h>
25 #ifdef HAVE_SYS_SELECT_H
26 #include <sys/select.h>
27 #endif /* HAVE_SYS_SELECT_H */
36 #endif /* STDC_HEADERS */
38 # if defined(HAVE_MEMORY_H) && !defined(STDC_HEADERS)
42 #endif /* HAVE_STRING_H */
45 #endif /* HAVE_STRINGS_H */
48 #endif /* HAVE_UNISTD_H */
49 #if TIME_WITH_SYS_TIME
53 # include <emul/timespec.h>
61 # define NAMLEN(dirent) strlen((dirent)->d_name)
63 # define dirent direct
64 # define NAMLEN(dirent) (dirent)->d_namlen
65 # ifdef HAVE_SYS_NDIR_H
66 # include <sys/ndir.h>
68 # ifdef HAVE_SYS_DIR_H
86 #include <pathnames.h>
94 # define LINE_MAX 2048
97 /* Must match the defines in iolog.c */
100 #define IOFD_STDERR 2
102 #define IOFD_TTYOUT 4
103 #define IOFD_TIMING 5
106 /* Bitmap of iofds to be replayed */
107 unsigned int replay_filter = (1 << IOFD_STDOUT) | (1 << IOFD_STDERR) |
126 * Info present in the I/O log file
139 * Handle expressions like:
140 * ( user millert or user root ) and tty console and command /bin/sh
143 struct search_node *next;
148 #define ST_RUNASUSER 5
149 #define ST_RUNASGROUP 6
150 #define ST_FROMDATE 7
168 struct search_node *expr;
173 #define STACK_NODE_SIZE 32
174 static struct search_node *node_stack[32];
175 static int stack_top;
177 static const char *session_dir = _PATH_SUDO_IO_LOGDIR;
179 static union io_fd io_fds[IOFD_MAX];
180 static const char *io_fnames[IOFD_MAX] = {
189 extern time_t get_date __P((char *));
190 extern char *get_timestr __P((time_t, int));
191 extern int term_raw __P((int, int));
192 extern int term_restore __P((int, int));
193 extern void zero_bytes __P((volatile void *, size_t));
194 void cleanup __P((int));
196 static int list_sessions __P((int, char **, const char *, const char *, const char *));
197 static int parse_expr __P((struct search_node **, char **));
198 static void check_input __P((int, double *));
199 static void delay __P((double));
200 static void usage __P((void));
201 static void *open_io_fd __P((char *pathbuf, int len, const char *suffix));
202 static int parse_timing __P((const char *buf, const char *decimal, int *idx, double *seconds, size_t *nbytes));
205 # define REGEX_T regex_t
207 # define REGEX_T char
210 #define VALID_ID(s) (isalnum((unsigned char)(s)[0]) && \
211 isalnum((unsigned char)(s)[1]) && isalnum((unsigned char)(s)[2]) && \
212 isalnum((unsigned char)(s)[3]) && isalnum((unsigned char)(s)[4]) && \
213 isalnum((unsigned char)(s)[5]) && (s)[6] == '\0')
220 int ch, idx, plen, nready, interactive = 0, listonly = 0;
221 const char *id, *user = NULL, *pattern = NULL, *tty = NULL, *decimal = ".";
222 char path[PATH_MAX], buf[LINE_MAX], *cp, *ep;
223 double seconds, to_wait, speed = 1.0, max_wait = 0;
227 size_t len, nbytes, nread, off;
233 #ifdef HAVE_SETLOCALE
234 setlocale(LC_ALL, "");
235 decimal = localeconv()->decimal_point;
238 while ((ch = getopt(argc, argv, "d:f:lm:s:V")) != -1) {
241 session_dir = optarg;
244 /* Set the replay filter. */
246 for (cp = strtok(optarg, ","); cp; cp = strtok(NULL, ",")) {
247 if (strcmp(cp, "stdout") == 0)
248 SET(replay_filter, 1 << IOFD_STDOUT);
249 else if (strcmp(cp, "stderr") == 0)
250 SET(replay_filter, 1 << IOFD_STDERR);
251 else if (strcmp(cp, "ttyout") == 0)
252 SET(replay_filter, 1 << IOFD_TTYOUT);
254 errorx(1, "invalid filter option: %s", optarg);
262 max_wait = strtod(optarg, &ep);
263 if (*ep != '\0' || errno != 0)
264 errorx(1, "invalid max wait: %s", optarg);
268 speed = strtod(optarg, &ep);
269 if (*ep != '\0' || errno != 0)
270 errorx(1, "invalid speed factor: %s", optarg);
273 (void) printf("%s version %s\n", getprogname(), PACKAGE_VERSION);
285 exit(list_sessions(argc, argv, pattern, user, tty));
290 /* 6 digit ID in base 36, e.g. 01G712AB */
293 errorx(1, "invalid ID %s", id);
295 plen = snprintf(path, sizeof(path), "%s/%.2s/%.2s/%.2s/timing",
296 session_dir, id, &id[2], &id[4]);
297 if (plen <= 0 || plen >= sizeof(path))
298 errorx(1, "%s/%.2s/%.2s/%.2s/%.2s/timing: %s", session_dir,
299 id, &id[2], &id[4], strerror(ENAMETOOLONG));
302 /* Open files for replay, applying replay filter for the -f flag. */
303 for (idx = 0; idx < IOFD_MAX; idx++) {
304 if (ISSET(replay_filter, 1 << idx) || idx == IOFD_TIMING) {
305 io_fds[idx].v = open_io_fd(path, plen, io_fnames[idx]);
306 if (io_fds[idx].v == NULL)
307 error(1, "unable to open %s", path);
313 strlcat(path, "/log", sizeof(path));
314 lfile = fopen(path, "r");
316 error(1, "unable to open %s", path);
319 getline(&cp, &len, lfile); /* log */
320 getline(&cp, &len, lfile); /* cwd */
321 getline(&cp, &len, lfile); /* command */
322 printf("Replaying sudo session: %s", cp);
327 zero_bytes(&sa, sizeof(sa));
328 sigemptyset(&sa.sa_mask);
329 sa.sa_flags = SA_RESETHAND;
330 sa.sa_handler = cleanup;
331 (void) sigaction(SIGINT, &sa, NULL);
332 (void) sigaction(SIGKILL, &sa, NULL);
333 (void) sigaction(SIGTERM, &sa, NULL);
334 (void) sigaction(SIGHUP, &sa, NULL);
335 sa.sa_flags = SA_RESTART;
336 sa.sa_handler = SIG_IGN;
337 (void) sigaction(SIGTSTP, &sa, NULL);
338 (void) sigaction(SIGQUIT, &sa, NULL);
340 /* XXX - read user input from /dev/tty and set STDOUT to raw if not a pipe */
341 /* Set stdin to raw mode if it is a tty */
342 interactive = isatty(STDIN_FILENO);
344 ch = fcntl(STDIN_FILENO, F_GETFL, 0);
346 (void) fcntl(STDIN_FILENO, F_SETFL, ch | O_NONBLOCK);
347 if (!term_raw(STDIN_FILENO, 1))
348 error(1, "cannot set tty to raw mode");
350 fdsw = (fd_set *)emalloc2(howmany(STDOUT_FILENO + 1, NFDBITS),
354 * Timing file consists of line of the format: "%f %d\n"
357 while (gzgets(io_fds[IOFD_TIMING].g, buf, sizeof(buf)) != NULL) {
359 while (fgets(buf, sizeof(buf), io_fds[IOFD_TIMING].f) != NULL) {
361 if (!parse_timing(buf, decimal, &idx, &seconds, &nbytes))
362 errorx(1, "invalid timing file line: %s", buf);
365 check_input(STDIN_FILENO, &speed);
367 /* Adjust delay using speed factor and clamp to max_wait */
368 to_wait = seconds / speed;
369 if (max_wait && to_wait > max_wait)
373 /* Even if we are not relaying, we still have to delay. */
374 if (io_fds[idx].v == NULL)
377 /* All output is sent to stdout. */
378 while (nbytes != 0) {
379 if (nbytes > sizeof(buf))
384 nread = gzread(io_fds[idx].g, buf, len);
386 nread = fread(buf, 1, len, io_fds[idx].f);
391 /* no stdio, must be unbuffered */
392 nwritten = write(STDOUT_FILENO, buf + off, nread - off);
393 if (nwritten == -1) {
396 if (errno == EAGAIN) {
397 FD_SET(STDOUT_FILENO, fdsw);
399 nready = select(STDOUT_FILENO + 1, NULL, fdsw, NULL, NULL);
400 } while (nready == -1 && errno == EINTR);
404 error(1, "writing to standard output");
407 } while (nread > off);
410 term_restore(STDIN_FILENO, 1);
418 struct timespec ts, rts;
422 * Typical max resolution is 1/HZ but we can't portably check that.
423 * If the interval is small enough, just ignore it.
429 rts.tv_nsec = (secs - (double) rts.tv_sec) * 1000000000.0;
431 memcpy(&ts, &rts, sizeof(ts));
432 rval = nanosleep(&ts, &rts);
433 } while (rval == -1 && errno == EINTR);
435 error(1, "nanosleep: tv_sec %ld, tv_nsec %ld", ts.tv_sec, ts.tv_nsec);
439 open_io_fd(path, len, suffix)
445 strlcat(path, suffix, PATH_MAX);
448 return gzopen(path, "r");
450 return fopen(path, "r");
455 * Build expression list from search args
458 parse_expr(headp, argv)
459 struct search_node **headp;
462 struct search_node *sn, *newsn;
463 char or = 0, not = 0, type, **av;
466 for (av = argv; *av; av++) {
468 case 'a': /* and (ignore) */
469 if (strncmp(*av, "and", strlen(*av)) != 0)
473 if (strncmp(*av, "or", strlen(*av)) != 0)
477 case '!': /* negate */
478 if (av[0][1] != '\0')
482 case 'c': /* command */
483 if (av[0][1] == '\0')
484 errorx(1, "ambiguous expression \"%s\"", *av);
485 if (strncmp(*av, "cwd", strlen(*av)) == 0)
487 else if (strncmp(*av, "command", strlen(*av)) == 0)
492 case 'f': /* from date */
493 if (strncmp(*av, "fromdate", strlen(*av)) != 0)
497 case 'g': /* runas group */
498 if (strncmp(*av, "group", strlen(*av)) != 0)
500 type = ST_RUNASGROUP;
502 case 'r': /* runas user */
503 if (strncmp(*av, "runas", strlen(*av)) != 0)
507 case 't': /* tty or to date */
508 if (av[0][1] == '\0')
509 errorx(1, "ambiguous expression \"%s\"", *av);
510 if (strncmp(*av, "todate", strlen(*av)) == 0)
512 else if (strncmp(*av, "tty", strlen(*av)) == 0)
518 if (strncmp(*av, "user", strlen(*av)) != 0)
522 case '(': /* start sub-expression */
523 if (av[0][1] != '\0')
525 if (stack_top + 1 == STACK_NODE_SIZE) {
526 errorx(1, "too many parenthesized expressions, max %d",
529 node_stack[stack_top++] = sn;
532 case ')': /* end sub-expression */
533 if (av[0][1] != '\0')
537 errorx(1, "unmatched ')' in expression");
538 if (node_stack[stack_top])
539 sn->next = node_stack[stack_top]->next;
540 return(av - argv + 1);
543 errorx(1, "unknown search term \"%s\"", *av);
547 /* Allocate new search node */
548 newsn = emalloc(sizeof(*newsn));
552 newsn->negated = not;
553 if (type == ST_EXPR) {
554 av += parse_expr(&newsn->u.expr, av + 1);
557 errorx(1, "%s requires an argument", av[-1]);
559 if (type == ST_PATTERN) {
560 if (regcomp(&newsn->u.cmdre, *av, REG_EXTENDED|REG_NOSUB) != 0)
561 errorx(1, "invalid regex: %s", *av);
564 if (type == ST_TODATE || type == ST_FROMDATE) {
565 newsn->u.tstamp = get_date(*av);
566 if (newsn->u.tstamp == -1)
567 errorx(1, "could not parse date \"%s\"", *av);
572 not = or = 0; /* reset state */
580 errorx(1, "unmatched '(' in expression");
582 errorx(1, "illegal trailing \"or\"");
584 errorx(1, "illegal trailing \"!\"");
590 match_expr(head, log)
591 struct search_node *head;
592 struct log_info *log;
594 struct search_node *sn;
597 for (sn = head; sn; sn = sn->next) {
598 /* If we have no match, skip ahead to the next OR entry. */
599 if (!matched && !sn->or)
604 matched = match_expr(sn->u.expr, log);
607 matched = strcmp(sn->u.cwd, log->cwd) == 0;
610 matched = strcmp(sn->u.tty, log->tty) == 0;
613 matched = strcmp(sn->u.runas_group, log->runas_group) == 0;
616 matched = strcmp(sn->u.runas_user, log->runas_user) == 0;
619 matched = strcmp(sn->u.user, log->user) == 0;
623 rc = regexec(&sn->u.cmdre, log->cmd, 0, NULL, 0);
624 if (rc && rc != REG_NOMATCH) {
626 regerror(rc, &sn->u.cmdre, buf, sizeof(buf));
627 errorx(1, "%s", buf);
629 matched = rc == REG_NOMATCH ? 0 : 1;
631 matched = strstr(log.cmd, sn->u.pattern) != NULL;
635 matched = log->tstamp >= sn->u.tstamp;
638 matched = log->tstamp <= sn->u.tstamp;
648 list_session_dir(pathbuf, re, user, tty)
657 char *buf = NULL, *cmd = NULL, *cwd = NULL, idstr[7], *cp;
659 size_t bufsize = 0, cwdsize = 0, cmdsize = 0, plen;
661 plen = strlen(pathbuf);
662 d = opendir(pathbuf);
663 if (d == NULL && errno != ENOTDIR) {
664 warning("cannot opendir %s", pathbuf);
667 while ((dp = readdir(d)) != NULL) {
668 if (NAMLEN(dp) != 2 || !isalnum((unsigned char)dp->d_name[0]) ||
669 !isalnum((unsigned char)dp->d_name[1]))
672 /* open log file, print id and command */
673 pathbuf[plen + 0] = '/';
674 pathbuf[plen + 1] = dp->d_name[0];
675 pathbuf[plen + 2] = dp->d_name[1];
676 pathbuf[plen + 3] = '/';
677 pathbuf[plen + 4] = 'l';
678 pathbuf[plen + 5] = 'o';
679 pathbuf[plen + 6] = 'g';
680 pathbuf[plen + 7] = '\0';
681 fp = fopen(pathbuf, "r");
683 warning("unable to open %s", pathbuf);
688 * ID file has three lines:
691 * 3) command with args
693 if (getline(&buf, &bufsize, fp) == -1 ||
694 getline(&cwd, &cwdsize, fp) == -1 ||
695 getline(&cmd, &cmdsize, fp) == -1) {
701 /* crack the log line: timestamp:user:runas_user:runas_group:tty */
702 buf[strcspn(buf, "\n")] = '\0';
703 if ((li.tstamp = atoi(buf)) == 0)
706 if ((cp = strchr(buf, ':')) == NULL)
711 if ((cp = strchr(cp, ':')) == NULL)
716 if ((cp = strchr(cp, ':')) == NULL)
721 if ((cp = strchr(cp, ':')) == NULL)
726 cwd[strcspn(cwd, "\n")] = '\0';
729 cmd[strcspn(cmd, "\n")] = '\0';
732 /* Match on search expression if there is one. */
733 if (search_expr && !match_expr(search_expr, &li))
736 /* Convert from /var/log/sudo-sessions/00/00/01 to 000001 */
737 idstr[0] = pathbuf[plen - 5];
738 idstr[1] = pathbuf[plen - 4];
739 idstr[2] = pathbuf[plen - 2];
740 idstr[3] = pathbuf[plen - 1];
741 idstr[4] = pathbuf[plen + 1];
742 idstr[5] = pathbuf[plen + 2];
744 printf("%s : %s : TTY=%s ; CWD=%s ; USER=%s ; ",
745 get_timestr(li.tstamp, 1), li.user, li.tty, li.cwd, li.runas_user);
747 printf("GROUP=%s ; ", li.runas_group);
748 printf("TSID=%s ; COMMAND=%s\n", idstr, li.cmd);
754 list_sessions(argc, argv, pattern, user, tty)
762 struct dirent *dp1, *dp2;
763 REGEX_T rebuf, *re = NULL;
765 char pathbuf[PATH_MAX];
767 /* Parse search expression if present */
768 parse_expr(&search_expr, argv);
770 d1 = opendir(session_dir);
772 error(1, "unable to open %s", session_dir);
778 if (regcomp(re, pattern, REG_EXTENDED|REG_NOSUB) != 0)
779 errorx(1, "invalid regex: %s", pattern);
782 re = (char *) pattern;
783 #endif /* HAVE_REGCOMP */
785 sdlen = strlcpy(pathbuf, session_dir, sizeof(pathbuf));
786 if (sdlen + sizeof("/00/00/00/log") >= sizeof(pathbuf)) {
787 errno = ENAMETOOLONG;
788 error(1, "%s/00/00/00/log", session_dir);
792 * Three levels of directory, e.g. 00/00/00 .. ZZ/ZZ/ZZ
793 * We do a depth-first traversal.
795 while ((dp1 = readdir(d1)) != NULL) {
796 if (NAMLEN(dp1) != 2 || !isalnum((unsigned char)dp1->d_name[0]) ||
797 !isalnum((unsigned char)dp1->d_name[1]))
800 pathbuf[sdlen + 0] = '/';
801 pathbuf[sdlen + 1] = dp1->d_name[0];
802 pathbuf[sdlen + 2] = dp1->d_name[1];
803 pathbuf[sdlen + 3] = '\0';
804 d2 = opendir(pathbuf);
808 while ((dp2 = readdir(d2)) != NULL) {
809 if (NAMLEN(dp2) != 2 || !isalnum((unsigned char)dp2->d_name[0]) ||
810 !isalnum((unsigned char)dp2->d_name[1]))
813 pathbuf[sdlen + 3] = '/';
814 pathbuf[sdlen + 4] = dp2->d_name[0];
815 pathbuf[sdlen + 5] = dp2->d_name[1];
816 pathbuf[sdlen + 6] = '\0';
817 list_session_dir(pathbuf, re, user, tty);
826 * Check input for ' ', '<', '>'
830 check_input(ttyfd, speed)
835 int nready, paused = 0;
840 fdsr = (fd_set *)emalloc2(howmany(ttyfd + 1, NFDBITS), sizeof(fd_mask));
847 nready = select(ttyfd + 1, fdsr, NULL, NULL, paused ? NULL : &tv);
850 n = read(ttyfd, &ch, 1);
873 * Parse a timing line, which is formatted as:
874 * index sleep_time num_bytes
875 * Where index is IOFD_*, sleep_time is the number of seconds to sleep
876 * before writing the data and num_bytes is the number of bytes to output.
877 * Returns 1 on success and 0 on failure.
880 parse_timing(buf, decimal, idx, seconds, nbytes)
893 ul = strtoul(buf, &ep, 10);
897 for (cp = ep + 1; isspace((unsigned char) *cp); cp++)
901 * Parse number of seconds. Sudo logs timing data in the C locale
902 * but this may not match the current locale so we cannot use strtod().
903 * Furthermore, sudo < 1.7.4 logged with the user's locale so we need
904 * to be able to parse those logs too.
907 l = strtol(cp, &ep, 10);
908 if ((errno == ERANGE && (l == LONG_MAX || l == LONG_MIN)) ||
909 l < 0 || l > INT_MAX ||
910 (*ep != '.' && strncmp(ep, decimal, strlen(decimal)) != 0)) {
913 *seconds = (double)l;
914 cp = ep + (*ep == '.' ? 1 : strlen(decimal));
916 while (isdigit((unsigned char) *cp)) {
917 fract += (*cp - '0') / d;
922 while (isspace((unsigned char) *cp))
926 ul = strtoul(cp, &ep, 10);
927 if (errno == ERANGE && ul == ULONG_MAX)
929 *nbytes = (size_t)ul;
940 "usage: %s [-d directory] [-m max_wait] [-s speed_factor] ID\n",
943 "usage: %s [-d directory] -l [search expression]\n",
949 * Cleanup hook for error()/errorx()
955 term_restore(STDIN_FILENO, 0);
957 kill(getpid(), signo);