/* * last(1) from sysvinit project, merged into util-linux in Aug 2013. * * Copyright (C) 1991-2004 Miquel van Smoorenburg. * Copyright (C) 2013 Ondrej Oprala * Karel Zak * * Re-implementation of the 'last' command, this time for Linux. Yes I know * there is BSD last, but I just felt like writing this. No thanks :-). Also, * this version gives lots more info (especially with -x) * * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "c.h" #include "nls.h" #include "optutils.h" #include "pathnames.h" #include "xalloc.h" #include "closestream.h" #include "carefulputc.h" #include "strutils.h" #include "timeutils.h" #include "monotonic.h" #include "fileutils.h" #ifdef FUZZ_TARGET #include "fuzz.h" #endif #ifndef SHUTDOWN_TIME # define SHUTDOWN_TIME 254 #endif #ifndef LAST_LOGIN_LEN # define LAST_LOGIN_LEN 8 #endif #ifndef LAST_DOMAIN_LEN # define LAST_DOMAIN_LEN 16 #endif #ifndef LAST_TIMESTAMP_LEN # define LAST_TIMESTAMP_LEN 32 #endif #define UCHUNKSIZE 16384 /* How much we read at once. */ struct last_control { unsigned int lastb :1, /* Is this command 'lastb' */ extended :1, /* Lots of info */ showhost :1, /* Show hostname */ altlist :1, /* Hostname at the end */ usedns :1, /* Use DNS to lookup the hostname */ useip :1; /* Print IP address in number format */ unsigned int name_len; /* Number of login name characters to print */ unsigned int domain_len; /* Number of domain name characters to print */ unsigned int maxrecs; /* Maximum number of records to list */ char **show; /* Match search list */ struct timeval boot_time; /* system boot time */ time_t since; /* at what time to start displaying the file */ time_t until; /* at what time to stop displaying the file */ time_t present; /* who where present at time_t */ unsigned int time_fmt; /* time format */ }; /* Double linked list of struct utmp's */ struct utmplist { struct utmpx ut; struct utmplist *next; struct utmplist *prev; }; /* Types of listing */ enum { R_CRASH = 1, /* No logout record, system boot in between */ R_DOWN, /* System brought down in decent way */ R_NORMAL, /* Normal */ R_NOW, /* Still logged in */ R_REBOOT, /* Reboot record. */ R_PHANTOM, /* No logout record but session is stale. */ R_TIMECHANGE /* NEW_TIME or OLD_TIME */ }; enum { LAST_TIMEFTM_NONE = 0, LAST_TIMEFTM_SHORT, LAST_TIMEFTM_CTIME, LAST_TIMEFTM_ISO8601, LAST_TIMEFTM_HHMM, /* non-public */ }; struct last_timefmt { const char *name; int in_len; /* log-in */ int in_fmt; int out_len; /* log-out */ int out_fmt; }; static struct last_timefmt timefmts[] = { [LAST_TIMEFTM_NONE] = { .name = "notime" }, [LAST_TIMEFTM_SHORT] = { .name = "short", .in_len = 16, .out_len = 7, .in_fmt = LAST_TIMEFTM_CTIME, .out_fmt = LAST_TIMEFTM_HHMM }, [LAST_TIMEFTM_CTIME] = { .name = "full", .in_len = 24, .out_len = 26, .in_fmt = LAST_TIMEFTM_CTIME, .out_fmt = LAST_TIMEFTM_CTIME }, [LAST_TIMEFTM_ISO8601] = { .name = "iso", .in_len = 25, .out_len = 27, .in_fmt = LAST_TIMEFTM_ISO8601, .out_fmt = LAST_TIMEFTM_ISO8601 } }; /* Global variables */ static unsigned int recsdone; /* Number of records listed */ static time_t lastdate; /* Last date we've seen */ static time_t currentdate; /* date when we started processing the file */ #ifndef FUZZ_TARGET /* --time-format=option parser */ static int which_time_format(const char *s) { size_t i; for (i = 0; i < ARRAY_SIZE(timefmts); i++) { if (strcmp(timefmts[i].name, s) == 0) return i; } errx(EXIT_FAILURE, _("unknown time format: %s"), s); } #endif /* * Read one utmp entry, return in new format. * Automatically reposition file pointer. */ static int uread(FILE *fp, struct utmpx *u, int *quit, const char *filename) { static int utsize; static char buf[UCHUNKSIZE]; char tmp[1024]; static off_t fpos; static int bpos; off_t o; if (quit == NULL && u != NULL) { /* * Normal read. */ return fread(u, sizeof(struct utmpx), 1, fp); } if (u == NULL) { /* * Initialize and position. */ utsize = sizeof(struct utmpx); fseeko(fp, 0, SEEK_END); fpos = ftello(fp); if (fpos == 0) return 0; o = ((fpos - 1) / UCHUNKSIZE) * UCHUNKSIZE; if (fseeko(fp, o, SEEK_SET) < 0) { warn(_("seek on %s failed"), filename); return 0; } bpos = (int)(fpos - o); if (fread(buf, bpos, 1, fp) != 1) { warn(_("cannot read %s"), filename); return 0; } fpos = o; return 1; } /* * Read one struct. From the buffer if possible. */ bpos -= utsize; if (bpos >= 0) { memcpy(u, buf + bpos, sizeof(struct utmpx)); return 1; } /* * Oops we went "below" the buffer. We should be able to * seek back UCHUNKSIZE bytes. */ fpos -= UCHUNKSIZE; if (fpos < 0) return 0; /* * Copy whatever is left in the buffer. */ memcpy(tmp + (-bpos), buf, utsize + bpos); if (fseeko(fp, fpos, SEEK_SET) < 0) { warn(_("seek on %s failed"), filename); return 0; } /* * Read another UCHUNKSIZE bytes. */ if (fread(buf, UCHUNKSIZE, 1, fp) != 1) { warn(_("cannot read %s"), filename); return 0; } /* * The end of the UCHUNKSIZE byte buffer should be the first * few bytes of the current struct utmp. */ memcpy(tmp, buf + UCHUNKSIZE + bpos, -bpos); bpos += UCHUNKSIZE; memcpy(u, tmp, sizeof(struct utmpx)); return 1; } #ifndef FUZZ_TARGET /* * Print a short date. */ static char *showdate(void) { static char s[CTIME_BUFSIZ]; ctime_r(&lastdate, s); s[16] = 0; return s; } /* * SIGINT handler */ static void int_handler(int sig __attribute__((unused))) { errx(EXIT_FAILURE, _("Interrupted %s"), showdate()); } /* * SIGQUIT handler */ static void quit_handler(int sig __attribute__((unused))) { warnx(_("Interrupted %s"), showdate()); signal(SIGQUIT, quit_handler); } #endif /* * Lookup a host with DNS. */ static int dns_lookup(char *result, int size, int useip, int32_t *a) { struct sockaddr_in sin; struct sockaddr_in6 sin6; struct sockaddr *sa; int salen, flags; int mapped = 0; flags = useip ? NI_NUMERICHOST : 0; /* * IPv4 or IPv6 ? * 1. If last 3 4bytes are 0, must be IPv4 * 2. If IPv6 in IPv4, handle as IPv4 * 3. Anything else is IPv6 * * Ugly. */ if (a[0] == 0 && a[1] == 0 && a[2] == (int32_t)htonl (0xffff)) mapped = 1; if (mapped || (a[1] == 0 && a[2] == 0 && a[3] == 0)) { /* IPv4 */ sin.sin_family = AF_INET; sin.sin_port = 0; sin.sin_addr.s_addr = mapped ? a[3] : a[0]; sa = (struct sockaddr *)&sin; salen = sizeof(sin); } else { /* IPv6 */ memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; sin6.sin6_port = 0; memcpy(sin6.sin6_addr.s6_addr, a, 16); sa = (struct sockaddr *)&sin6; salen = sizeof(sin6); } return getnameinfo(sa, salen, result, size, NULL, 0, flags); } static int time_formatter(int fmt, char *dst, size_t dlen, time_t *when) { int ret = 0; switch (fmt) { case LAST_TIMEFTM_NONE: *dst = 0; break; case LAST_TIMEFTM_HHMM: { struct tm tm; localtime_r(when, &tm); if (!snprintf(dst, dlen, "%02d:%02d", tm.tm_hour, tm.tm_min)) ret = -1; break; } case LAST_TIMEFTM_CTIME: { char buf[CTIME_BUFSIZ]; ctime_r(when, buf); snprintf(dst, dlen, "%s", buf); ret = rtrim_whitespace((unsigned char *) dst); break; } case LAST_TIMEFTM_ISO8601: ret = strtime_iso(when, ISO_TIMESTAMP_T, dst, dlen); break; default: abort(); } return ret; } /* * Remove trailing spaces from a string. */ static void trim_trailing_spaces(char *s) { char *p; for (p = s; *p; ++p) continue; while (p > s && isspace(*--p)) continue; if (p > s) ++p; *p++ = '\n'; *p = '\0'; } /* * Show one line of information on screen */ static int list(const struct last_control *ctl, struct utmpx *p, time_t logout_time, int what) { time_t secs, utmp_time; char logintime[LAST_TIMESTAMP_LEN]; char logouttime[LAST_TIMESTAMP_LEN]; char length[LAST_TIMESTAMP_LEN]; char final[512]; char utline[sizeof(p->ut_line) + 1]; char domain[256]; char *s; int mins, hours, days; int r, len; struct last_timefmt *fmt; /* * uucp and ftp have special-type entries */ mem2strcpy(utline, p->ut_line, sizeof(p->ut_line), sizeof(utline)); if (strncmp(utline, "ftp", 3) == 0 && isdigit(utline[3])) utline[3] = 0; if (strncmp(utline, "uucp", 4) == 0 && isdigit(utline[4])) utline[4] = 0; /* * Is this something we want to show? */ if (ctl->show) { char **walk; for (walk = ctl->show; *walk; walk++) { if (strncmp(p->ut_user, *walk, sizeof(p->ut_user)) == 0 || strcmp(utline, *walk) == 0 || (strncmp(utline, "tty", 3) == 0 && strcmp(utline + 3, *walk) == 0)) break; } if (*walk == NULL) return 0; } /* * Calculate times */ fmt = &timefmts[ctl->time_fmt]; utmp_time = p->ut_tv.tv_sec; if (ctl->present) { if (ctl->present < utmp_time) return 0; if (0 < logout_time && logout_time < ctl->present) return 0; } /* log-in time */ if (time_formatter(fmt->in_fmt, logintime, sizeof(logintime), &utmp_time) < 0) errx(EXIT_FAILURE, _("preallocation size exceeded")); /* log-out time */ secs = logout_time - utmp_time; /* Under strange circumstances, secs < 0 can happen */ mins = (secs / 60) % 60; hours = (secs / 3600) % 24; days = secs / 86400; strcpy(logouttime, "- "); if (time_formatter(fmt->out_fmt, logouttime + 2, sizeof(logouttime) - 2, &logout_time) < 0) errx(EXIT_FAILURE, _("preallocation size exceeded")); if (logout_time == currentdate) { if (ctl->time_fmt > LAST_TIMEFTM_SHORT) { snprintf(logouttime, sizeof(logouttime), " still running"); length[0] = 0; } else { snprintf(logouttime, sizeof(logouttime), " still"); snprintf(length, sizeof(length), "running"); } } else if (days) { snprintf(length, sizeof(length), "(%d+%02d:%02d)", days, abs(hours), abs(mins)); /* hours and mins always shown as positive (w/o minus sign!) even if secs < 0 */ } else if (hours) { snprintf(length, sizeof(length), " (%02d:%02d)", hours, abs(mins)); /* mins always shown as positive (w/o minus sign!) even if secs < 0 */ } else if (secs >= 0) { snprintf(length, sizeof(length), " (%02d:%02d)", hours, mins); } else { snprintf(length, sizeof(length), " (-00:%02d)", abs(mins)); /* mins always shown as positive (w/o minus sign!) even if secs < 0 */ } switch(what) { case R_CRASH: snprintf(logouttime, sizeof(logouttime), "- crash"); break; case R_DOWN: snprintf(logouttime, sizeof(logouttime), "- down "); break; case R_NOW: if (ctl->time_fmt > LAST_TIMEFTM_SHORT) { snprintf(logouttime, sizeof(logouttime), " still logged in"); length[0] = 0; } else { snprintf(logouttime, sizeof(logouttime), " still"); snprintf(length, sizeof(length), "logged in"); } break; case R_PHANTOM: if (ctl->time_fmt > LAST_TIMEFTM_SHORT) { snprintf(logouttime, sizeof(logouttime), " gone - no logout"); length[0] = 0; } else if (ctl->time_fmt == LAST_TIMEFTM_SHORT) { snprintf(logouttime, sizeof(logouttime), " gone"); snprintf(length, sizeof(length), "- no logout"); } else { logouttime[0] = 0; snprintf(length, sizeof(length), "no logout"); } break; case R_TIMECHANGE: logouttime[0] = 0; length[0] = 0; break; case R_NORMAL: case R_REBOOT: break; default: abort(); } /* * Look up host with DNS if needed. */ r = -1; if (ctl->usedns || ctl->useip) r = dns_lookup(domain, sizeof(domain), ctl->useip, (int32_t*)p->ut_addr_v6); if (r < 0) mem2strcpy(domain, p->ut_host, sizeof(p->ut_host), sizeof(domain)); if (ctl->showhost) { if (!ctl->altlist) { len = snprintf(final, sizeof(final), "%-8.*s %-12.12s %-16.*s %-*.*s %-*.*s %s\n", ctl->name_len, p->ut_user, utline, ctl->domain_len, domain, fmt->in_len, fmt->in_len, logintime, fmt->out_len, fmt->out_len, logouttime, length); } else { len = snprintf(final, sizeof(final), "%-8.*s %-12.12s %-*.*s %-*.*s %-12.12s %s\n", ctl->name_len, p->ut_user, utline, fmt->in_len, fmt->in_len, logintime, fmt->out_len, fmt->out_len, logouttime, length, domain); } } else len = snprintf(final, sizeof(final), "%-8.*s %-12.12s %-*.*s %-*.*s %s\n", ctl->name_len, p->ut_user, utline, fmt->in_len, fmt->in_len, logintime, fmt->out_len, fmt->out_len, logouttime, length); #if defined(__GLIBC__) # if (__GLIBC__ == 2) && (__GLIBC_MINOR__ == 0) final[sizeof(final)-1] = '\0'; # endif #endif trim_trailing_spaces(final); /* * Print out "final" string safely. */ for (s = final; *s; s++) fputc_careful(*s, stdout, '*'); if (len < 0 || (size_t)len >= sizeof(final)) putchar('\n'); recsdone++; if (ctl->maxrecs && ctl->maxrecs <= recsdone) return 1; return 0; } #ifndef FUZZ_TARGET static void __attribute__((__noreturn__)) usage(const struct last_control *ctl) { FILE *out = stdout; fputs(USAGE_HEADER, out); fprintf(out, _( " %s [options] [...] [...]\n"), program_invocation_short_name); fputs(USAGE_SEPARATOR, out); fputs(_("Show a listing of last logged in users.\n"), out); fputs(USAGE_OPTIONS, out); fputs(_(" - how many lines to show\n"), out); fputs(_(" -a, --hostlast display hostnames in the last column\n"), out); fputs(_(" -d, --dns translate the IP number back into a hostname\n"), out); fprintf(out, _(" -f, --file use a specific file instead of %s\n"), ctl->lastb ? _PATH_BTMP : _PATH_WTMP); fputs(_(" -F, --fulltimes print full login and logout times and dates\n"), out); fputs(_(" -i, --ip display IP numbers in numbers-and-dots notation\n"), out); fputs(_(" -n, --limit how many lines to show\n"), out); fputs(_(" -R, --nohostname don't display the hostname field\n"), out); fputs(_(" -s, --since