Annotation of embedaddon/sudo/plugins/sudoers/sudoreplay.c, revision 1.1.1.3
1.1 misho 1: /*
1.1.1.2 misho 2: * Copyright (c) 2009-2012 Todd C. Miller <Todd.Miller@courtesan.com>
1.1 misho 3: *
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.
7: *
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.
15: */
16:
17: #include <config.h>
18:
19: #include <sys/types.h>
20: #include <sys/param.h>
1.1.1.3 ! misho 21: #include <sys/uio.h>
1.1 misho 22: #ifdef HAVE_SYS_SYSMACROS_H
23: # include <sys/sysmacros.h>
24: #endif
25: #include <sys/stat.h>
26: #include <sys/time.h>
27: #include <sys/wait.h>
28: #include <sys/ioctl.h>
29: #ifdef HAVE_SYS_SELECT_H
30: #include <sys/select.h>
31: #endif /* HAVE_SYS_SELECT_H */
32: #include <stdio.h>
33: #ifdef STDC_HEADERS
34: # include <stdlib.h>
35: # include <stddef.h>
36: #else
37: # ifdef HAVE_STDLIB_H
38: # include <stdlib.h>
39: # endif
40: #endif /* STDC_HEADERS */
41: #ifdef HAVE_STRING_H
42: # if defined(HAVE_MEMORY_H) && !defined(STDC_HEADERS)
43: # include <memory.h>
44: # endif
45: # include <string.h>
46: #endif /* HAVE_STRING_H */
47: #ifdef HAVE_STRINGS_H
48: # include <strings.h>
49: #endif /* HAVE_STRINGS_H */
50: #ifdef HAVE_UNISTD_H
51: # include <unistd.h>
52: #endif /* HAVE_UNISTD_H */
53: #if TIME_WITH_SYS_TIME
54: # include <time.h>
55: #endif
1.1.1.2 misho 56: #ifndef HAVE_STRUCT_TIMESPEC
1.1 misho 57: # include "compat/timespec.h"
58: #endif
59: #include <ctype.h>
60: #include <errno.h>
61: #include <limits.h>
62: #include <fcntl.h>
63: #ifdef HAVE_DIRENT_H
64: # include <dirent.h>
65: # define NAMLEN(dirent) strlen((dirent)->d_name)
66: #else
67: # define dirent direct
68: # define NAMLEN(dirent) (dirent)->d_namlen
69: # ifdef HAVE_SYS_NDIR_H
70: # include <sys/ndir.h>
71: # endif
72: # ifdef HAVE_SYS_DIR_H
73: # include <sys/dir.h>
74: # endif
75: # ifdef HAVE_NDIR_H
76: # include <ndir.h>
77: # endif
78: #endif
79: #ifdef HAVE_REGCOMP
80: # include <regex.h>
81: #endif
82: #ifdef HAVE_ZLIB_H
83: # include <zlib.h>
84: #endif
85: #ifdef HAVE_SETLOCALE
86: # include <locale.h>
87: #endif
88: #include <signal.h>
1.1.1.2 misho 89: #ifdef HAVE_STDBOOL_H
90: # include <stdbool.h>
91: #else
92: # include "compat/stdbool.h"
93: #endif /* HAVE_STDBOOL_H */
1.1 misho 94:
95: #include <pathnames.h>
96:
97: #include "missing.h"
98: #include "alloc.h"
99: #include "error.h"
100: #include "gettext.h"
1.1.1.2 misho 101: #include "sudo_plugin.h"
102: #include "sudo_conf.h"
103: #include "sudo_debug.h"
1.1 misho 104:
105: #ifndef LINE_MAX
106: # define LINE_MAX 2048
107: #endif
108:
109: /* Must match the defines in iolog.c */
110: #define IOFD_STDIN 0
111: #define IOFD_STDOUT 1
112: #define IOFD_STDERR 2
113: #define IOFD_TTYIN 3
114: #define IOFD_TTYOUT 4
115: #define IOFD_TIMING 5
116: #define IOFD_MAX 6
117:
118: /* Bitmap of iofds to be replayed */
119: unsigned int replay_filter = (1 << IOFD_STDOUT) | (1 << IOFD_STDERR) |
120: (1 << IOFD_TTYOUT);
121:
122: /* For getopt(3) */
123: extern char *optarg;
124: extern int optind;
125:
126: union io_fd {
127: FILE *f;
128: #ifdef HAVE_ZLIB_H
129: gzFile g;
130: #endif
131: void *v;
132: };
133:
134: /*
135: * Info present in the I/O log file
136: */
137: struct log_info {
138: char *cwd;
139: char *user;
140: char *runas_user;
141: char *runas_group;
142: char *tty;
143: char *cmd;
144: time_t tstamp;
1.1.1.2 misho 145: int rows;
146: int cols;
1.1 misho 147: };
148:
149: /*
150: * Handle expressions like:
151: * ( user millert or user root ) and tty console and command /bin/sh
152: */
153: struct search_node {
154: struct search_node *next;
155: #define ST_EXPR 1
156: #define ST_TTY 2
157: #define ST_USER 3
158: #define ST_PATTERN 4
159: #define ST_RUNASUSER 5
160: #define ST_RUNASGROUP 6
161: #define ST_FROMDATE 7
162: #define ST_TODATE 8
163: #define ST_CWD 9
164: char type;
165: char negated;
166: char or;
167: char pad;
168: union {
169: #ifdef HAVE_REGCOMP
170: regex_t cmdre;
171: #endif
172: time_t tstamp;
173: char *cwd;
174: char *tty;
175: char *user;
176: char *pattern;
177: char *runas_group;
178: char *runas_user;
179: struct search_node *expr;
180: void *ptr;
181: } u;
182: } *search_expr;
183:
1.1.1.2 misho 184: sudo_conv_t sudo_conv; /* NULL in non-plugin */
185:
1.1 misho 186: #define STACK_NODE_SIZE 32
187: static struct search_node *node_stack[32];
188: static int stack_top;
189:
190: static const char *session_dir = _PATH_SUDO_IO_LOGDIR;
191:
192: static union io_fd io_fds[IOFD_MAX];
193: static const char *io_fnames[IOFD_MAX] = {
194: "/stdin",
195: "/stdout",
196: "/stderr",
197: "/ttyin",
198: "/ttyout",
199: "/timing"
200: };
201:
202: extern time_t get_date(char *);
203: extern char *get_timestr(time_t, int);
204: extern int term_raw(int, int);
205: extern int term_restore(int, int);
1.1.1.2 misho 206: extern void get_ttysize(int *rowp, int *colp);
1.1 misho 207: void cleanup(int);
208:
209: static int list_sessions(int, char **, const char *, const char *, const char *);
210: static int parse_expr(struct search_node **, char **);
211: static void check_input(int, double *);
212: static void delay(double);
213: static void help(void) __attribute__((__noreturn__));
214: static void usage(int);
215: static int open_io_fd(char *pathbuf, int len, const char *suffix, union io_fd *fdp);
216: static int parse_timing(const char *buf, const char *decimal, int *idx, double *seconds, size_t *nbytes);
1.1.1.2 misho 217: static struct log_info *parse_logfile(char *logfile);
218: static void free_log_info(struct log_info *li);
1.1.1.3 ! misho 219: static size_t atomic_writev(int fd, struct iovec *iov, int iovcnt);
1.1 misho 220:
221: #ifdef HAVE_REGCOMP
222: # define REGEX_T regex_t
223: #else
224: # define REGEX_T char
225: #endif
226:
227: #define VALID_ID(s) (isalnum((unsigned char)(s)[0]) && \
228: isalnum((unsigned char)(s)[1]) && isalnum((unsigned char)(s)[2]) && \
229: isalnum((unsigned char)(s)[3]) && isalnum((unsigned char)(s)[4]) && \
230: isalnum((unsigned char)(s)[5]) && (s)[6] == '\0')
231:
232: #define IS_IDLOG(s) ( \
233: isalnum((unsigned char)(s)[0]) && isalnum((unsigned char)(s)[1]) && \
234: (s)[2] == '/' && \
235: isalnum((unsigned char)(s)[3]) && isalnum((unsigned char)(s)[4]) && \
236: (s)[5] == '/' && \
237: isalnum((unsigned char)(s)[6]) && isalnum((unsigned char)(s)[7]) && \
238: (s)[8] == '/' && (s)[9] == 'l' && (s)[10] == 'o' && (s)[11] == 'g' && \
1.1.1.2 misho 239: (s)[12] == '\0')
1.1 misho 240:
241: int
242: main(int argc, char *argv[])
243: {
1.1.1.3 ! misho 244: int ch, idx, plen, exitcode = 0, rows = 0, cols = 0;
! 245: bool interactive = false, listonly = false, need_nlcr = false;
1.1 misho 246: const char *id, *user = NULL, *pattern = NULL, *tty = NULL, *decimal = ".";
247: char path[PATH_MAX], buf[LINE_MAX], *cp, *ep;
248: double seconds, to_wait, speed = 1.0, max_wait = 0;
249: sigaction_t sa;
1.1.1.3 ! misho 250: size_t len, nbytes, nread;
1.1.1.2 misho 251: struct log_info *li;
1.1.1.3 ! misho 252: struct iovec *iov = NULL;
! 253: int iovcnt = 0, iovmax = 0;
1.1.1.2 misho 254: debug_decl(main, SUDO_DEBUG_MAIN)
255:
256: #if defined(SUDO_DEVEL) && defined(__OpenBSD__)
257: {
258: extern char *malloc_options;
259: malloc_options = "AFGJPR";
260: }
261: #endif
1.1 misho 262:
263: #if !defined(HAVE_GETPROGNAME) && !defined(HAVE___PROGNAME)
264: setprogname(argc > 0 ? argv[0] : "sudoreplay");
265: #endif
266:
267: #ifdef HAVE_SETLOCALE
268: setlocale(LC_ALL, "");
269: decimal = localeconv()->decimal_point;
270: #endif
271: bindtextdomain("sudoers", LOCALEDIR); /* XXX - should have sudoreplay domain */
272: textdomain("sudoers");
273:
1.1.1.2 misho 274: /* Read sudo.conf. */
275: sudo_conf_read();
276:
1.1 misho 277: while ((ch = getopt(argc, argv, "d:f:hlm:s:V")) != -1) {
278: switch(ch) {
279: case 'd':
280: session_dir = optarg;
281: break;
282: case 'f':
283: /* Set the replay filter. */
284: replay_filter = 0;
285: for (cp = strtok(optarg, ","); cp; cp = strtok(NULL, ",")) {
286: if (strcmp(cp, "stdout") == 0)
287: SET(replay_filter, 1 << IOFD_STDOUT);
288: else if (strcmp(cp, "stderr") == 0)
289: SET(replay_filter, 1 << IOFD_STDERR);
290: else if (strcmp(cp, "ttyout") == 0)
291: SET(replay_filter, 1 << IOFD_TTYOUT);
292: else
293: errorx(1, _("invalid filter option: %s"), optarg);
294: }
295: break;
296: case 'h':
297: help();
298: /* NOTREACHED */
299: case 'l':
1.1.1.2 misho 300: listonly = true;
1.1 misho 301: break;
302: case 'm':
303: errno = 0;
304: max_wait = strtod(optarg, &ep);
305: if (*ep != '\0' || errno != 0)
306: errorx(1, _("invalid max wait: %s"), optarg);
307: break;
308: case 's':
309: errno = 0;
310: speed = strtod(optarg, &ep);
311: if (*ep != '\0' || errno != 0)
312: errorx(1, _("invalid speed factor: %s"), optarg);
313: break;
314: case 'V':
315: (void) printf(_("%s version %s\n"), getprogname(), PACKAGE_VERSION);
1.1.1.2 misho 316: goto done;
1.1 misho 317: default:
318: usage(1);
319: /* NOTREACHED */
320: }
321:
322: }
323: argc -= optind;
324: argv += optind;
325:
1.1.1.2 misho 326: if (listonly) {
327: exitcode = list_sessions(argc, argv, pattern, user, tty);
328: goto done;
329: }
1.1 misho 330:
331: if (argc != 1)
332: usage(1);
333:
334: /* 6 digit ID in base 36, e.g. 01G712AB or free-form name */
335: id = argv[0];
336: if (VALID_ID(id)) {
337: plen = snprintf(path, sizeof(path), "%s/%.2s/%.2s/%.2s/timing",
338: session_dir, id, &id[2], &id[4]);
339: if (plen <= 0 || plen >= sizeof(path))
340: errorx(1, _("%s/%.2s/%.2s/%.2s/timing: %s"), session_dir,
341: id, &id[2], &id[4], strerror(ENAMETOOLONG));
342: } else {
343: plen = snprintf(path, sizeof(path), "%s/%s/timing",
344: session_dir, id);
345: if (plen <= 0 || plen >= sizeof(path))
346: errorx(1, _("%s/%s/timing: %s"), session_dir,
347: id, strerror(ENAMETOOLONG));
348: }
349: plen -= 7;
350:
351: /* Open files for replay, applying replay filter for the -f flag. */
352: for (idx = 0; idx < IOFD_MAX; idx++) {
353: if (ISSET(replay_filter, 1 << idx) || idx == IOFD_TIMING) {
354: if (open_io_fd(path, plen, io_fnames[idx], &io_fds[idx]) == -1)
355: error(1, _("unable to open %s"), path);
356: }
357: }
358:
1.1.1.2 misho 359: /* Parse log file. */
1.1 misho 360: path[plen] = '\0';
361: strlcat(path, "/log", sizeof(path));
1.1.1.2 misho 362: if ((li = parse_logfile(path)) == NULL)
363: exit(1);
364: printf(_("Replaying sudo session: %s\n"), li->cmd);
365:
366: /* Make sure the terminal is large enough. */
367: get_ttysize(&rows, &cols);
368: if (li->rows != 0 && li->cols != 0) {
369: if (li->rows > rows) {
370: printf(_("Warning: your terminal is too small to properly replay the log.\n"));
371: printf(_("Log geometry is %d x %d, your terminal's geometry is %d x %d."), li->rows, li->cols, rows, cols);
372: }
373: }
374:
375: /* Done with parsed log file. */
376: free_log_info(li);
377: li = NULL;
1.1 misho 378:
379: fflush(stdout);
1.1.1.2 misho 380: memset(&sa, 0, sizeof(sa));
1.1 misho 381: sigemptyset(&sa.sa_mask);
382: sa.sa_flags = SA_RESETHAND;
383: sa.sa_handler = cleanup;
384: (void) sigaction(SIGINT, &sa, NULL);
385: (void) sigaction(SIGKILL, &sa, NULL);
386: (void) sigaction(SIGTERM, &sa, NULL);
387: (void) sigaction(SIGHUP, &sa, NULL);
388: sa.sa_flags = SA_RESTART;
389: sa.sa_handler = SIG_IGN;
390: (void) sigaction(SIGTSTP, &sa, NULL);
391: (void) sigaction(SIGQUIT, &sa, NULL);
392:
393: /* XXX - read user input from /dev/tty and set STDOUT to raw if not a pipe */
394: /* Set stdin to raw mode if it is a tty */
395: interactive = isatty(STDIN_FILENO);
396: if (interactive) {
397: ch = fcntl(STDIN_FILENO, F_GETFL, 0);
398: if (ch != -1)
399: (void) fcntl(STDIN_FILENO, F_SETFL, ch | O_NONBLOCK);
400: if (!term_raw(STDIN_FILENO, 1))
401: error(1, _("unable to set tty to raw mode"));
1.1.1.3 ! misho 402: iovcnt = 0;
! 403: iovmax = 32;
! 404: iov = ecalloc(iovmax, sizeof(*iov));
1.1 misho 405: }
406:
407: /*
408: * Timing file consists of line of the format: "%f %d\n"
409: */
410: #ifdef HAVE_ZLIB_H
411: while (gzgets(io_fds[IOFD_TIMING].g, buf, sizeof(buf)) != NULL) {
412: #else
413: while (fgets(buf, sizeof(buf), io_fds[IOFD_TIMING].f) != NULL) {
414: #endif
1.1.1.3 ! misho 415: char last_char = '\0';
! 416:
1.1 misho 417: if (!parse_timing(buf, decimal, &idx, &seconds, &nbytes))
418: errorx(1, _("invalid timing file line: %s"), buf);
419:
420: if (interactive)
421: check_input(STDIN_FILENO, &speed);
422:
423: /* Adjust delay using speed factor and clamp to max_wait */
424: to_wait = seconds / speed;
425: if (max_wait && to_wait > max_wait)
426: to_wait = max_wait;
427: delay(to_wait);
428:
429: /* Even if we are not relaying, we still have to delay. */
430: if (io_fds[idx].v == NULL)
431: continue;
432:
1.1.1.3 ! misho 433: /* Check whether we need to convert newline to CR LF pairs. */
! 434: if (interactive)
! 435: need_nlcr = (idx == IOFD_STDOUT || idx == IOFD_STDERR);
! 436:
1.1 misho 437: /* All output is sent to stdout. */
438: while (nbytes != 0) {
439: if (nbytes > sizeof(buf))
440: len = sizeof(buf);
441: else
442: len = nbytes;
443: #ifdef HAVE_ZLIB_H
444: nread = gzread(io_fds[idx].g, buf, len);
445: #else
446: nread = fread(buf, 1, len, io_fds[idx].f);
447: #endif
448: nbytes -= nread;
1.1.1.3 ! misho 449:
! 450: /* Convert newline to carriage return + linefeed if needed. */
! 451: if (need_nlcr) {
! 452: size_t remainder = nread;
! 453: size_t linelen;
! 454: iovcnt = 0;
! 455: cp = buf;
! 456: ep = cp - 1;
! 457: /* Handle a "\r\n" pair that spans a buffer. */
! 458: if (last_char == '\r' && buf[0] == '\n') {
! 459: ep++;
! 460: remainder--;
! 461: }
! 462: while ((ep = memchr(ep + 1, '\n', remainder)) != NULL) {
! 463: /* Is there already a carriage return? */
! 464: if (cp != ep && ep[-1] == '\r') {
! 465: remainder = (size_t)(&buf[nread - 1] - ep);
! 466: continue;
1.1 misho 467: }
1.1.1.3 ! misho 468:
! 469: /* Store the line in iov followed by \r\n pair. */
! 470: if (iovcnt + 3 > iovmax) {
! 471: iovmax <<= 1;
! 472: iov = erealloc3(iov, iovmax, sizeof(*iov));
! 473: }
! 474: linelen = (size_t)(ep - cp) + 1;
! 475: iov[iovcnt].iov_base = cp;
! 476: iov[iovcnt].iov_len = linelen - 1; /* not including \n */
! 477: iovcnt++;
! 478: iov[iovcnt].iov_base = "\r\n";
! 479: iov[iovcnt].iov_len = 2;
! 480: iovcnt++;
! 481: cp = ep + 1;
! 482: remainder -= linelen;
! 483: }
! 484: if (cp - buf != nread) {
! 485: /*
! 486: * Partial line without a linefeed or multiple lines
! 487: * with \r\n pairs.
! 488: */
! 489: iov[iovcnt].iov_base = cp;
! 490: iov[iovcnt].iov_len = nread - (cp - buf);
! 491: iovcnt++;
1.1 misho 492: }
1.1.1.3 ! misho 493: last_char = buf[nread - 1]; /* stash last char of old buffer */
! 494: } else {
! 495: /* No conversion needed. */
! 496: iov[0].iov_base = buf;
! 497: iov[0].iov_len = nread;
! 498: iovcnt = 1;
! 499: }
! 500: if (atomic_writev(STDOUT_FILENO, iov, iovcnt) == -1)
! 501: error(1, _("writing to standard output"));
1.1 misho 502: }
503: }
504: term_restore(STDIN_FILENO, 1);
1.1.1.2 misho 505: done:
506: sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, exitcode);
507: exit(exitcode);
1.1 misho 508: }
509:
510: static void
511: delay(double secs)
512: {
513: struct timespec ts, rts;
514: int rval;
515:
516: /*
517: * Typical max resolution is 1/HZ but we can't portably check that.
518: * If the interval is small enough, just ignore it.
519: */
520: if (secs < 0.0001)
521: return;
522:
523: rts.tv_sec = secs;
524: rts.tv_nsec = (secs - (double) rts.tv_sec) * 1000000000.0;
525: do {
526: memcpy(&ts, &rts, sizeof(ts));
527: rval = nanosleep(&ts, &rts);
528: } while (rval == -1 && errno == EINTR);
529: if (rval == -1) {
1.1.1.2 misho 530: error2(1, _("nanosleep: tv_sec %ld, tv_nsec %ld"),
1.1 misho 531: (long)ts.tv_sec, (long)ts.tv_nsec);
532: }
533: }
534:
535: static int
536: open_io_fd(char *path, int len, const char *suffix, union io_fd *fdp)
537: {
1.1.1.3 ! misho 538: debug_decl(open_io_fd, SUDO_DEBUG_UTIL)
! 539:
1.1 misho 540: path[len] = '\0';
541: strlcat(path, suffix, PATH_MAX);
542:
543: #ifdef HAVE_ZLIB_H
544: fdp->g = gzopen(path, "r");
545: #else
546: fdp->f = fopen(path, "r");
547: #endif
1.1.1.2 misho 548: debug_return_int(fdp->v ? 0 : -1);
1.1 misho 549: }
550:
551: /*
1.1.1.3 ! misho 552: * Call writev(), restarting as needed and handling EAGAIN since
! 553: * fd may be in non-blocking mode.
! 554: */
! 555: static size_t
! 556: atomic_writev(int fd, struct iovec *iov, int iovcnt)
! 557: {
! 558: ssize_t n, nwritten = 0;
! 559: size_t count, remainder, nbytes = 0;
! 560: int i;
! 561: debug_decl(atomic_writev, SUDO_DEBUG_UTIL)
! 562:
! 563: for (i = 0; i < iovcnt; i++)
! 564: nbytes += iov[i].iov_len;
! 565:
! 566: for (;;) {
! 567: n = writev(STDOUT_FILENO, iov, iovcnt);
! 568: if (n > 0) {
! 569: nwritten += n;
! 570: remainder = nbytes - nwritten;
! 571: if (remainder == 0)
! 572: break;
! 573: /* short writev, adjust iov and do the rest. */
! 574: count = 0;
! 575: i = iovcnt;
! 576: while (i--) {
! 577: count += iov[i].iov_len;
! 578: if (count == remainder) {
! 579: iov += i;
! 580: iovcnt -= i;
! 581: break;
! 582: }
! 583: if (count > remainder) {
! 584: size_t off = (count - remainder);
! 585: /* XXX - side effect prevents iov from being const */
! 586: iov[i].iov_base = (char *)iov[i].iov_base + off;
! 587: iov[i].iov_len -= off;
! 588: iov += i;
! 589: iovcnt -= i;
! 590: break;
! 591: }
! 592: }
! 593: continue;
! 594: }
! 595: if (n == 0 || errno == EAGAIN) {
! 596: int nready;
! 597: fd_set fdsw;
! 598: FD_ZERO(&fdsw);
! 599: FD_SET(STDOUT_FILENO, &fdsw);
! 600: do {
! 601: nready = select(STDOUT_FILENO + 1, NULL, &fdsw, NULL, NULL);
! 602: } while (nready == -1 && errno == EINTR);
! 603: if (nready == 1)
! 604: continue;
! 605: }
! 606: if (errno == EINTR)
! 607: continue;
! 608: nwritten = -1;
! 609: break;
! 610: }
! 611: debug_return_size_t(nwritten);
! 612: }
! 613:
! 614: /*
1.1 misho 615: * Build expression list from search args
616: */
617: static int
618: parse_expr(struct search_node **headp, char *argv[])
619: {
620: struct search_node *sn, *newsn;
621: char or = 0, not = 0, type, **av;
1.1.1.2 misho 622: debug_decl(parse_expr, SUDO_DEBUG_UTIL)
1.1 misho 623:
624: sn = *headp;
625: for (av = argv; *av; av++) {
626: switch (av[0][0]) {
627: case 'a': /* and (ignore) */
628: if (strncmp(*av, "and", strlen(*av)) != 0)
629: goto bad;
630: continue;
631: case 'o': /* or */
632: if (strncmp(*av, "or", strlen(*av)) != 0)
633: goto bad;
634: or = 1;
635: continue;
636: case '!': /* negate */
637: if (av[0][1] != '\0')
638: goto bad;
639: not = 1;
640: continue;
641: case 'c': /* command */
642: if (av[0][1] == '\0')
643: errorx(1, _("ambiguous expression \"%s\""), *av);
644: if (strncmp(*av, "cwd", strlen(*av)) == 0)
645: type = ST_CWD;
646: else if (strncmp(*av, "command", strlen(*av)) == 0)
647: type = ST_PATTERN;
648: else
649: goto bad;
650: break;
651: case 'f': /* from date */
652: if (strncmp(*av, "fromdate", strlen(*av)) != 0)
653: goto bad;
654: type = ST_FROMDATE;
655: break;
656: case 'g': /* runas group */
657: if (strncmp(*av, "group", strlen(*av)) != 0)
658: goto bad;
659: type = ST_RUNASGROUP;
660: break;
661: case 'r': /* runas user */
662: if (strncmp(*av, "runas", strlen(*av)) != 0)
663: goto bad;
664: type = ST_RUNASUSER;
665: break;
666: case 't': /* tty or to date */
667: if (av[0][1] == '\0')
668: errorx(1, _("ambiguous expression \"%s\""), *av);
669: if (strncmp(*av, "todate", strlen(*av)) == 0)
670: type = ST_TODATE;
671: else if (strncmp(*av, "tty", strlen(*av)) == 0)
672: type = ST_TTY;
673: else
674: goto bad;
675: break;
676: case 'u': /* user */
677: if (strncmp(*av, "user", strlen(*av)) != 0)
678: goto bad;
679: type = ST_USER;
680: break;
681: case '(': /* start sub-expression */
682: if (av[0][1] != '\0')
683: goto bad;
684: if (stack_top + 1 == STACK_NODE_SIZE) {
685: errorx(1, _("too many parenthesized expressions, max %d"),
686: STACK_NODE_SIZE);
687: }
688: node_stack[stack_top++] = sn;
689: type = ST_EXPR;
690: break;
691: case ')': /* end sub-expression */
692: if (av[0][1] != '\0')
693: goto bad;
694: /* pop */
695: if (--stack_top < 0)
696: errorx(1, _("unmatched ')' in expression"));
697: if (node_stack[stack_top])
698: sn->next = node_stack[stack_top]->next;
1.1.1.2 misho 699: debug_return_int(av - argv + 1);
1.1 misho 700: bad:
701: default:
702: errorx(1, _("unknown search term \"%s\""), *av);
703: /* NOTREACHED */
704: }
705:
706: /* Allocate new search node */
1.1.1.2 misho 707: newsn = ecalloc(1, sizeof(*newsn));
1.1 misho 708: newsn->type = type;
709: newsn->or = or;
710: newsn->negated = not;
1.1.1.2 misho 711: /* newsn->next = NULL; */
1.1 misho 712: if (type == ST_EXPR) {
713: av += parse_expr(&newsn->u.expr, av + 1);
714: } else {
715: if (*(++av) == NULL)
716: errorx(1, _("%s requires an argument"), av[-1]);
717: #ifdef HAVE_REGCOMP
718: if (type == ST_PATTERN) {
719: if (regcomp(&newsn->u.cmdre, *av, REG_EXTENDED|REG_NOSUB) != 0)
720: errorx(1, _("invalid regular expression: %s"), *av);
721: } else
722: #endif
723: if (type == ST_TODATE || type == ST_FROMDATE) {
724: newsn->u.tstamp = get_date(*av);
725: if (newsn->u.tstamp == -1)
726: errorx(1, _("could not parse date \"%s\""), *av);
727: } else {
728: newsn->u.ptr = *av;
729: }
730: }
731: not = or = 0; /* reset state */
732: if (sn)
733: sn->next = newsn;
734: else
735: *headp = newsn;
736: sn = newsn;
737: }
738: if (stack_top)
739: errorx(1, _("unmatched '(' in expression"));
740: if (or)
741: errorx(1, _("illegal trailing \"or\""));
742: if (not)
743: errorx(1, _("illegal trailing \"!\""));
744:
1.1.1.2 misho 745: debug_return_int(av - argv);
1.1 misho 746: }
747:
1.1.1.2 misho 748: static bool
1.1 misho 749: match_expr(struct search_node *head, struct log_info *log)
750: {
751: struct search_node *sn;
1.1.1.2 misho 752: bool matched = true;
753: int rc;
754: debug_decl(match_expr, SUDO_DEBUG_UTIL)
1.1 misho 755:
756: for (sn = head; sn; sn = sn->next) {
757: /* If we have no match, skip ahead to the next OR entry. */
758: if (!matched && !sn->or)
759: continue;
760:
761: switch (sn->type) {
762: case ST_EXPR:
763: matched = match_expr(sn->u.expr, log);
764: break;
765: case ST_CWD:
766: matched = strcmp(sn->u.cwd, log->cwd) == 0;
767: break;
768: case ST_TTY:
769: matched = strcmp(sn->u.tty, log->tty) == 0;
770: break;
771: case ST_RUNASGROUP:
772: matched = strcmp(sn->u.runas_group, log->runas_group) == 0;
773: break;
774: case ST_RUNASUSER:
775: matched = strcmp(sn->u.runas_user, log->runas_user) == 0;
776: break;
777: case ST_USER:
778: matched = strcmp(sn->u.user, log->user) == 0;
779: break;
780: case ST_PATTERN:
781: #ifdef HAVE_REGCOMP
782: rc = regexec(&sn->u.cmdre, log->cmd, 0, NULL, 0);
783: if (rc && rc != REG_NOMATCH) {
784: char buf[BUFSIZ];
785: regerror(rc, &sn->u.cmdre, buf, sizeof(buf));
786: errorx(1, "%s", buf);
787: }
788: matched = rc == REG_NOMATCH ? 0 : 1;
789: #else
790: matched = strstr(log.cmd, sn->u.pattern) != NULL;
791: #endif
792: break;
793: case ST_FROMDATE:
794: matched = log->tstamp >= sn->u.tstamp;
795: break;
796: case ST_TODATE:
797: matched = log->tstamp <= sn->u.tstamp;
798: break;
799: }
800: if (sn->negated)
801: matched = !matched;
802: }
1.1.1.2 misho 803: debug_return_bool(matched);
1.1 misho 804: }
805:
1.1.1.2 misho 806: static struct log_info *
807: parse_logfile(char *logfile)
1.1 misho 808: {
809: FILE *fp;
1.1.1.2 misho 810: char *buf = NULL, *cp, *ep;
1.1 misho 811: size_t bufsize = 0, cwdsize = 0, cmdsize = 0;
1.1.1.2 misho 812: struct log_info *li = NULL;
813: debug_decl(list_session, SUDO_DEBUG_UTIL)
1.1 misho 814:
815: fp = fopen(logfile, "r");
816: if (fp == NULL) {
817: warning(_("unable to open %s"), logfile);
1.1.1.2 misho 818: goto bad;
1.1 misho 819: }
820:
821: /*
822: * ID file has three lines:
823: * 1) a log info line
824: * 2) cwd
825: * 3) command with args
826: */
1.1.1.2 misho 827: li = ecalloc(1, sizeof(*li));
1.1 misho 828: if (getline(&buf, &bufsize, fp) == -1 ||
1.1.1.2 misho 829: getline(&li->cwd, &cwdsize, fp) == -1 ||
830: getline(&li->cmd, &cmdsize, fp) == -1) {
831: goto bad;
1.1 misho 832: }
833:
1.1.1.2 misho 834: /* Strip the newline from the cwd and command. */
835: li->cwd[strcspn(li->cwd, "\n")] = '\0';
836: li->cmd[strcspn(li->cmd, "\n")] = '\0';
837:
838: /*
839: * Crack the log line (rows and cols not present in old versions).
840: * timestamp:user:runas_user:runas_group:tty:rows:cols
841: */
1.1 misho 842: buf[strcspn(buf, "\n")] = '\0';
843:
1.1.1.2 misho 844: /* timestamp */
845: if ((ep = strchr(buf, ':')) == NULL)
846: goto bad;
847: if ((li->tstamp = atoi(buf)) == 0)
848: goto bad;
1.1 misho 849:
1.1.1.2 misho 850: /* user */
851: cp = ep + 1;
852: if ((ep = strchr(cp, ':')) == NULL)
853: goto bad;
854: li->user = estrndup(cp, (size_t)(ep - cp));
1.1 misho 855:
1.1.1.2 misho 856: /* runas user */
857: cp = ep + 1;
858: if ((ep = strchr(cp, ':')) == NULL)
859: goto bad;
860: li->runas_user = estrndup(cp, (size_t)(ep - cp));
1.1 misho 861:
1.1.1.2 misho 862: /* runas group */
863: cp = ep + 1;
864: if ((ep = strchr(cp, ':')) == NULL)
865: goto bad;
866: if (cp != ep)
867: li->runas_group = estrndup(cp, (size_t)(ep - cp));
1.1 misho 868:
1.1.1.2 misho 869: /* tty, followed by optional rows + columns */
870: cp = ep + 1;
871: if ((ep = strchr(cp, ':')) == NULL) {
872: li->tty = estrdup(cp);
873: } else {
874: li->tty = estrndup(cp, (size_t)(ep - cp));
875: cp = ep + 1;
876: li->rows = atoi(cp);
877: if ((ep = strchr(cp, ':')) != NULL) {
878: cp = ep + 1;
879: li->cols = atoi(cp);
880: }
881: }
882: fclose(fp);
883: efree(buf);
884: debug_return_ptr(li);
885:
886: bad:
1.1.1.3 ! misho 887: if (fp != NULL)
! 888: fclose(fp);
1.1.1.2 misho 889: efree(buf);
890: free_log_info(li);
891: debug_return_ptr(NULL);
892: }
893:
894: static void
895: free_log_info(struct log_info *li)
896: {
897: if (li != NULL) {
898: efree(li->cwd);
899: efree(li->user);
900: efree(li->runas_user);
901: efree(li->runas_group);
902: efree(li->tty);
903: efree(li->cmd);
904: efree(li);
905: }
906: }
907:
908: static int
909: list_session(char *logfile, REGEX_T *re, const char *user, const char *tty)
910: {
911: char idbuf[7], *idstr, *cp;
912: struct log_info *li;
913: int rval = -1;
914: debug_decl(list_session, SUDO_DEBUG_UTIL)
1.1 misho 915:
1.1.1.2 misho 916: if ((li = parse_logfile(logfile)) == NULL)
917: goto done;
1.1 misho 918:
919: /* Match on search expression if there is one. */
1.1.1.2 misho 920: if (search_expr && !match_expr(search_expr, li))
1.1 misho 921: goto done;
922:
923: /* Convert from /var/log/sudo-sessions/00/00/01/log to 000001 */
924: cp = logfile + strlen(session_dir) + 1;
925: if (IS_IDLOG(cp)) {
1.1.1.2 misho 926: idbuf[0] = cp[0];
927: idbuf[1] = cp[1];
928: idbuf[2] = cp[3];
929: idbuf[3] = cp[4];
930: idbuf[4] = cp[6];
931: idbuf[5] = cp[7];
1.1 misho 932: idbuf[6] = '\0';
933: idstr = idbuf;
934: } else {
935: /* Not an id, just use the iolog_file portion. */
936: cp[strlen(cp) - 4] = '\0';
937: idstr = cp;
938: }
1.1.1.2 misho 939: /* XXX - print rows + cols? */
1.1 misho 940: printf("%s : %s : TTY=%s ; CWD=%s ; USER=%s ; ",
1.1.1.2 misho 941: get_timestr(li->tstamp, 1), li->user, li->tty, li->cwd, li->runas_user);
942: if (li->runas_group)
943: printf("GROUP=%s ; ", li->runas_group);
944: printf("TSID=%s ; COMMAND=%s\n", idstr, li->cmd);
1.1 misho 945:
946: rval = 0;
947:
948: done:
1.1.1.2 misho 949: free_log_info(li);
950: debug_return_int(rval);
1.1 misho 951: }
952:
953: static int
1.1.1.2 misho 954: session_compare(const void *v1, const void *v2)
955: {
956: const char *s1 = *(const char **)v1;
957: const char *s2 = *(const char **)v2;
958: return strcmp(s1, s2);
959: }
960:
961: /* XXX - always returns 0, calls error() on failure */
962: static int
1.1 misho 963: find_sessions(const char *dir, REGEX_T *re, const char *user, const char *tty)
964: {
965: DIR *d;
966: struct dirent *dp;
967: struct stat sb;
1.1.1.2 misho 968: size_t sdlen, sessions_len = 0, sessions_size = 36*36;
969: int i, len;
970: char pathbuf[PATH_MAX], **sessions = NULL;
1.1.1.3 ! misho 971: #ifdef HAVE_STRUCT_DIRENT_D_TYPE
! 972: bool checked_type = true;
! 973: #else
! 974: const bool checked_type = false;
! 975: #endif
1.1.1.2 misho 976: debug_decl(find_sessions, SUDO_DEBUG_UTIL)
1.1 misho 977:
978: d = opendir(dir);
979: if (d == NULL)
980: error(1, _("unable to open %s"), dir);
981:
982: /* XXX - would be faster to chdir and use relative names */
983: sdlen = strlcpy(pathbuf, dir, sizeof(pathbuf));
984: if (sdlen + 1 >= sizeof(pathbuf)) {
985: errno = ENAMETOOLONG;
986: error(1, "%s/", dir);
987: }
988: pathbuf[sdlen++] = '/';
989: pathbuf[sdlen] = '\0';
1.1.1.2 misho 990:
991: /* Store potential session dirs for sorting. */
992: sessions = emalloc2(sessions_size, sizeof(char *));
1.1 misho 993: while ((dp = readdir(d)) != NULL) {
994: /* Skip "." and ".." */
995: if (dp->d_name[0] == '.' && (dp->d_name[1] == '\0' ||
996: (dp->d_name[1] == '.' && dp->d_name[2] == '\0')))
997: continue;
1.1.1.2 misho 998: #ifdef HAVE_STRUCT_DIRENT_D_TYPE
1.1.1.3 ! misho 999: if (checked_type) {
! 1000: if (dp->d_type != DT_DIR) {
! 1001: /* Not all file systems support d_type. */
! 1002: if (dp->d_type != DT_UNKNOWN)
! 1003: continue;
! 1004: checked_type = false;
! 1005: }
! 1006: }
1.1.1.2 misho 1007: #endif
1008:
1009: /* Add name to session list. */
1010: if (sessions_len + 1 > sessions_size) {
1011: sessions_size <<= 1;
1012: sessions = erealloc3(sessions, sessions_size, sizeof(char *));
1013: }
1014: sessions[sessions_len++] = estrdup(dp->d_name);
1015: }
1016: closedir(d);
1.1 misho 1017:
1.1.1.2 misho 1018: /* Sort and list the sessions. */
1019: qsort(sessions, sessions_len, sizeof(char *), session_compare);
1020: for (i = 0; i < sessions_len; i++) {
1.1 misho 1021: len = snprintf(&pathbuf[sdlen], sizeof(pathbuf) - sdlen,
1.1.1.2 misho 1022: "%s/log", sessions[i]);
1.1 misho 1023: if (len <= 0 || len >= sizeof(pathbuf) - sdlen) {
1024: errno = ENAMETOOLONG;
1.1.1.2 misho 1025: error(1, "%s/%s/log", dir, sessions[i]);
1.1 misho 1026: }
1.1.1.2 misho 1027: efree(sessions[i]);
1.1 misho 1028:
1029: /* Check for dir with a log file. */
1030: if (lstat(pathbuf, &sb) == 0 && S_ISREG(sb.st_mode)) {
1031: list_session(pathbuf, re, user, tty);
1032: } else {
1033: /* Strip off "/log" and recurse if a dir. */
1034: pathbuf[sdlen + len - 4] = '\0';
1.1.1.3 ! misho 1035: if (checked_type || (lstat(pathbuf, &sb) == 0 && S_ISDIR(sb.st_mode)))
1.1 misho 1036: find_sessions(pathbuf, re, user, tty);
1037: }
1038: }
1.1.1.2 misho 1039: efree(sessions);
1.1 misho 1040:
1.1.1.2 misho 1041: debug_return_int(0);
1.1 misho 1042: }
1043:
1.1.1.2 misho 1044: /* XXX - always returns 0, calls error() on failure */
1.1 misho 1045: static int
1046: list_sessions(int argc, char **argv, const char *pattern, const char *user,
1047: const char *tty)
1048: {
1049: REGEX_T rebuf, *re = NULL;
1.1.1.2 misho 1050: debug_decl(list_sessions, SUDO_DEBUG_UTIL)
1.1 misho 1051:
1052: /* Parse search expression if present */
1053: parse_expr(&search_expr, argv);
1054:
1055: #ifdef HAVE_REGCOMP
1056: /* optional regex */
1057: if (pattern) {
1058: re = &rebuf;
1059: if (regcomp(re, pattern, REG_EXTENDED|REG_NOSUB) != 0)
1060: errorx(1, _("invalid regex: %s"), pattern);
1061: }
1062: #else
1063: re = (char *) pattern;
1064: #endif /* HAVE_REGCOMP */
1065:
1.1.1.2 misho 1066: debug_return_int(find_sessions(session_dir, re, user, tty));
1.1 misho 1067: }
1068:
1069: /*
1070: * Check input for ' ', '<', '>'
1071: * pause, slow, fast
1072: */
1073: static void
1074: check_input(int ttyfd, double *speed)
1075: {
1076: fd_set *fdsr;
1077: int nready, paused = 0;
1078: struct timeval tv;
1079: char ch;
1080: ssize_t n;
1.1.1.2 misho 1081: debug_decl(check_input, SUDO_DEBUG_UTIL)
1.1 misho 1082:
1.1.1.2 misho 1083: fdsr = ecalloc(howmany(ttyfd + 1, NFDBITS), sizeof(fd_mask));
1.1 misho 1084: for (;;) {
1085: FD_SET(ttyfd, fdsr);
1086: tv.tv_sec = 0;
1087: tv.tv_usec = 0;
1088:
1089: nready = select(ttyfd + 1, fdsr, NULL, NULL, paused ? NULL : &tv);
1090: if (nready != 1)
1091: break;
1092: n = read(ttyfd, &ch, 1);
1093: if (n == 1) {
1094: if (paused) {
1095: paused = 0;
1096: continue;
1097: }
1098: switch (ch) {
1099: case ' ':
1100: paused = 1;
1101: break;
1102: case '<':
1103: *speed /= 2;
1104: break;
1105: case '>':
1106: *speed *= 2;
1107: break;
1108: }
1109: }
1110: }
1111: free(fdsr);
1.1.1.2 misho 1112: debug_return;
1.1 misho 1113: }
1114:
1115: /*
1116: * Parse a timing line, which is formatted as:
1117: * index sleep_time num_bytes
1118: * Where index is IOFD_*, sleep_time is the number of seconds to sleep
1119: * before writing the data and num_bytes is the number of bytes to output.
1120: * Returns 1 on success and 0 on failure.
1121: */
1122: static int
1123: parse_timing(buf, decimal, idx, seconds, nbytes)
1124: const char *buf;
1125: const char *decimal;
1126: int *idx;
1127: double *seconds;
1128: size_t *nbytes;
1129: {
1130: unsigned long ul;
1131: long l;
1132: double d, fract = 0;
1133: char *cp, *ep;
1.1.1.2 misho 1134: debug_decl(parse_timing, SUDO_DEBUG_UTIL)
1.1 misho 1135:
1136: /* Parse index */
1137: ul = strtoul(buf, &ep, 10);
1138: if (ul > IOFD_MAX)
1139: goto bad;
1140: *idx = (int)ul;
1141: for (cp = ep + 1; isspace((unsigned char) *cp); cp++)
1142: continue;
1143:
1144: /*
1145: * Parse number of seconds. Sudo logs timing data in the C locale
1146: * but this may not match the current locale so we cannot use strtod().
1147: * Furthermore, sudo < 1.7.4 logged with the user's locale so we need
1148: * to be able to parse those logs too.
1149: */
1150: errno = 0;
1151: l = strtol(cp, &ep, 10);
1152: if ((errno == ERANGE && (l == LONG_MAX || l == LONG_MIN)) ||
1153: l < 0 || l > INT_MAX ||
1154: (*ep != '.' && strncmp(ep, decimal, strlen(decimal)) != 0)) {
1155: goto bad;
1156: }
1157: *seconds = (double)l;
1158: cp = ep + (*ep == '.' ? 1 : strlen(decimal));
1159: d = 10.0;
1160: while (isdigit((unsigned char) *cp)) {
1161: fract += (*cp - '0') / d;
1162: d *= 10;
1163: cp++;
1164: }
1165: *seconds += fract;
1166: while (isspace((unsigned char) *cp))
1167: cp++;
1168:
1169: errno = 0;
1170: ul = strtoul(cp, &ep, 10);
1171: if (errno == ERANGE && ul == ULONG_MAX)
1172: goto bad;
1173: *nbytes = (size_t)ul;
1174:
1.1.1.2 misho 1175: debug_return_int(1);
1.1 misho 1176: bad:
1.1.1.2 misho 1177: debug_return_int(0);
1.1 misho 1178: }
1179:
1180: static void
1181: usage(int fatal)
1182: {
1183: fprintf(fatal ? stderr : stdout,
1184: _("usage: %s [-h] [-d directory] [-m max_wait] [-s speed_factor] ID\n"),
1185: getprogname());
1186: fprintf(fatal ? stderr : stdout,
1187: _("usage: %s [-h] [-d directory] -l [search expression]\n"),
1188: getprogname());
1189: if (fatal)
1190: exit(1);
1191: }
1192:
1193: static void
1194: help(void)
1195: {
1196: (void) printf(_("%s - replay sudo session logs\n\n"), getprogname());
1197: usage(0);
1198: (void) puts(_("\nOptions:\n"
1199: " -d directory specify directory for session logs\n"
1200: " -f filter specify which I/O type to display\n"
1201: " -h display help message and exit\n"
1202: " -l [expression] list available session IDs that match expression\n"
1203: " -m max_wait max number of seconds to wait between events\n"
1204: " -s speed_factor speed up or slow down output\n"
1205: " -V display version information and exit"));
1206: exit(0);
1207: }
1208:
1209: /*
1210: * Cleanup hook for error()/errorx()
1211: */
1212: void
1213: cleanup(int signo)
1214: {
1215: term_restore(STDIN_FILENO, 0);
1216: if (signo)
1217: kill(getpid(), signo);
1218: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>