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