diff options
Diffstat (limited to 'src/faillog.c')
-rw-r--r-- | src/faillog.c | 720 |
1 files changed, 720 insertions, 0 deletions
diff --git a/src/faillog.c b/src/faillog.c new file mode 100644 index 0000000..0f94836 --- /dev/null +++ b/src/faillog.c @@ -0,0 +1,720 @@ +/* + * SPDX-FileCopyrightText: 1989 - 1993, Julianne Frances Haugh + * SPDX-FileCopyrightText: 1996 - 2000, Marek Michałkiewicz + * SPDX-FileCopyrightText: 2002 - 2006, Tomasz Kłoczko + * SPDX-FileCopyrightText: 2007 - 2011, Nicolas François + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include <config.h> + +#ident "$Id$" + +#include <getopt.h> +#include <pwd.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <assert.h> +#include "defines.h" +#include "faillog.h" +#include "prototypes.h" +/*@-exitarg@*/ +#include "exitcodes.h" +#include "shadowlog.h" + +/* local function prototypes */ +static /*@noreturn@*/void usage (int status); +static void print_one (/*@null@*/const struct passwd *pw, bool force); +static void set_locktime (long locktime); +static bool set_locktime_one (uid_t uid, long locktime); +static void setmax (short max); +static bool setmax_one (uid_t uid, short max); +static void print (void); +static bool reset_one (uid_t uid); +static void reset (void); + +/* + * Global variables + */ +const char *Prog; /* Program name */ +static FILE *fail; /* failure file stream */ +static time_t seconds; /* that number of days in seconds */ +static unsigned long umin; /* if uflg and has_umin, only display users with uid >= umin */ +static bool has_umin = false; +static unsigned long umax; /* if uflg and has_umax, only display users with uid <= umax */ +static bool has_umax = false; +static bool errors = false; + +static bool aflg = false; /* set if all users are to be printed always */ +static bool uflg = false; /* set if user is a valid user id */ +static bool tflg = false; /* print is restricted to most recent days */ +static bool lflg = false; /* set the locktime */ +static bool mflg = false; /* set maximum failed login counters */ +static bool rflg = false; /* reset the counters of login failures */ + +static struct stat statbuf; /* fstat buffer for file size */ + +#define NOW (time((time_t *) 0)) + +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -a, --all display faillog records for all users\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -l, --lock-secs SEC after failed login lock account for SEC seconds\n"), usageout); + (void) fputs (_(" -m, --maximum MAX set maximum failed login counters to MAX\n"), usageout); + (void) fputs (_(" -r, --reset reset the counters of login failures\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -t, --time DAYS display faillog records more recent than DAYS\n"), usageout); + (void) fputs (_(" -u, --user LOGIN/RANGE display faillog record or maintains failure\n" + " counters and limits (if used with -r, -m,\n" + " or -l) only for the specified LOGIN(s)\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +static void print_one (/*@null@*/const struct passwd *pw, bool force) +{ + static bool once = false; + struct tm *tm; + off_t offset; + struct faillog fl; + time_t now; + char *cp; + char ptime[80]; + + if (NULL == pw) { + return; + } + + offset = (off_t) pw->pw_uid * sizeof (fl); + if (offset + sizeof (fl) <= statbuf.st_size) { + /* fseeko errors are not really relevant for us. */ + int err = fseeko (fail, offset, SEEK_SET); + assert (0 == err); + /* faillog is a sparse file. Even if no entries were + * entered for this user, which should be able to get the + * empty entry in this case. + */ + if (fread ((char *) &fl, sizeof (fl), 1, fail) != 1) { + fprintf (stderr, + _("%s: Failed to get the entry for UID %lu\n"), + Prog, (unsigned long int)pw->pw_uid); + return; + } + } else { + /* Outsize of the faillog file. + * Behave as if there were a missing entry (same behavior + * as if we were reading an non existing entry in the + * sparse faillog file). + */ + memzero (&fl, sizeof (fl)); + } + + /* Nothing to report */ + if (!force && (0 == fl.fail_time)) { + return; + } + + (void) time(&now); + + /* Filter out entries that do not match with the -t option */ + if (tflg && ((now - fl.fail_time) > seconds)) { + return; + } + + /* Print the header only once */ + if (!once) { + puts (_("Login Failures Maximum Latest On\n")); + once = true; + } + + tm = localtime (&fl.fail_time); + if (!tm) { + fprintf (stderr, "Cannot read time from faillog.\n"); + return; + } + strftime (ptime, sizeof (ptime), "%D %H:%M:%S %z", tm); + cp = ptime; + + printf ("%-9s %5d %5d ", + pw->pw_name, fl.fail_cnt, fl.fail_max); + printf ("%s %s", cp, fl.fail_line); + if (0 != fl.fail_locktime) { + if ( ((fl.fail_time + fl.fail_locktime) > now) + && (0 != fl.fail_cnt)) { + printf (_(" [%lus left]"), + (unsigned long) fl.fail_time + fl.fail_locktime - now); + } else { + printf (_(" [%lds lock]"), + fl.fail_locktime); + } + } + putchar ('\n'); +} + +static void print (void) +{ + if (uflg && has_umin && has_umax && (umin==umax)) { + print_one (getpwuid ((uid_t)umin), true); + } else { + /* We only print records for existing users. + * Loop based on the user database instead of reading the + * whole file. We will have to query the database anyway + * so except for very small ranges and large user + * database, this should not be a performance issue. + */ + struct passwd *pwent; + + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ( uflg + && ( (has_umin && (pwent->pw_uid < (uid_t)umin)) + || (has_umax && (pwent->pw_uid > (uid_t)umax)))) { + continue; + } + print_one (pwent, aflg); + } + endpwent (); + } +} + +/* + * reset_one - Reset the fail count for one user + * + * This returns a boolean indicating if an error occurred. + */ +static bool reset_one (uid_t uid) +{ + off_t offset; + struct faillog fl; + + offset = (off_t) uid * sizeof (fl); + if (offset + sizeof (fl) <= statbuf.st_size) { + /* fseeko errors are not really relevant for us. */ + int err = fseeko (fail, offset, SEEK_SET); + assert (0 == err); + /* faillog is a sparse file. Even if no entries were + * entered for this user, which should be able to get the + * empty entry in this case. + */ + if (fread ((char *) &fl, sizeof (fl), 1, fail) != 1) { + fprintf (stderr, + _("%s: Failed to get the entry for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; + } + } else { + /* Outsize of the faillog file. + * Behave as if there were a missing entry (same behavior + * as if we were reading an non existing entry in the + * sparse faillog file). + */ + memzero (&fl, sizeof (fl)); + } + + if (0 == fl.fail_cnt) { + /* If the count is already null, do not write in the file. + * This avoids writing 0 when no entries were present for + * the user. + */ + return false; + } + + fl.fail_cnt = 0; + + if ( (fseeko (fail, offset, SEEK_SET) == 0) + && (fwrite ((char *) &fl, sizeof (fl), 1, fail) == 1)) { + (void) fflush (fail); + return false; + } + + fprintf (stderr, + _("%s: Failed to reset fail count for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; +} + +static void reset (void) +{ + if (uflg && has_umin && has_umax && (umin==umax)) { + if (reset_one ((uid_t)umin)) { + errors = true; + } + } else { + /* There is no need to reset outside of the faillog + * database. + */ + uid_t uidmax = statbuf.st_size / sizeof (struct faillog); + if (uidmax > 1) { + uidmax--; + } + if (has_umax && (uid_t)umax < uidmax) { + uidmax = (uid_t)umax; + } + + /* Reset all entries in the specified range. + * Non existing entries will not be touched. + */ + if (aflg) { + /* Entries for non existing users are also reset. + */ + uid_t uid = 0; + + /* Make sure we stay in the umin-umax range if specified */ + if (has_umin) { + uid = (uid_t)umin; + } + + while (uid <= uidmax) { + if (reset_one (uid)) { + errors = true; + } + uid++; + } + } else { + /* Only reset records for existing users. + */ + struct passwd *pwent; + + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ( uflg + && ( (has_umin && (pwent->pw_uid < (uid_t)umin)) + || (pwent->pw_uid > (uid_t)uidmax))) { + continue; + } + if (reset_one (pwent->pw_uid)) { + errors = true; + } + } + endpwent (); + } + } +} + +/* + * setmax_one - Set the maximum failed login counter for one user + * + * This returns a boolean indicating if an error occurred. + */ +static bool setmax_one (uid_t uid, short max) +{ + off_t offset; + struct faillog fl; + + offset = (off_t) uid * sizeof (fl); + if (offset + sizeof (fl) <= statbuf.st_size) { + /* fseeko errors are not really relevant for us. */ + int err = fseeko (fail, offset, SEEK_SET); + assert (0 == err); + /* faillog is a sparse file. Even if no entries were + * entered for this user, which should be able to get the + * empty entry in this case. + */ + if (fread ((char *) &fl, sizeof (fl), 1, fail) != 1) { + fprintf (stderr, + _("%s: Failed to get the entry for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; + } + } else { + /* Outsize of the faillog file. + * Behave as if there were a missing entry (same behavior + * as if we were reading an non existing entry in the + * sparse faillog file). + */ + memzero (&fl, sizeof (fl)); + } + + if (max == fl.fail_max) { + /* If the max is already set to the right value, do not + * write in the file. + * This avoids writing 0 when no entries were present for + * the user and the max argument is 0. + */ + return false; + } + + fl.fail_max = max; + + if ( (fseeko (fail, offset, SEEK_SET) == 0) + && (fwrite ((char *) &fl, sizeof (fl), 1, fail) == 1)) { + (void) fflush (fail); + return false; + } + + fprintf (stderr, + _("%s: Failed to set max for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; +} + +static void setmax (short max) +{ + if (uflg && has_umin && has_umax && (umin==umax)) { + if (setmax_one ((uid_t)umin, max)) { + errors = true; + } + } else { + /* Set max for entries in the specified range. + * If max is unchanged for an entry, the entry is not touched. + * If max is null, and no entries exist for this user, no + * entries will be created. + */ + if (aflg) { + /* Entries for non existing user are also taken into + * account (in order to define policy for future users). + */ + uid_t uid = 0; + /* The default umax value is based on the size of the + * faillog database. + */ + uid_t uidmax = statbuf.st_size / sizeof (struct faillog); + if (uidmax > 1) { + uidmax--; + } + + /* Make sure we stay in the umin-umax range if specified */ + if (has_umin) { + uid = (uid_t)umin; + } + if (has_umax) { + uidmax = (uid_t)umax; + } + + while (uid <= uidmax) { + if (setmax_one (uid, max)) { + errors = true; + } + uid++; + } + } else { + /* Only change records for existing users. + */ + struct passwd *pwent; + + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ( uflg + && ( (has_umin && (pwent->pw_uid < (uid_t)umin)) + || (has_umax && (pwent->pw_uid > (uid_t)umax)))) { + continue; + } + if (setmax_one (pwent->pw_uid, max)) { + errors = true; + } + } + endpwent (); + } + } +} + +/* + * set_locktime_one - Set the locktime for one user + * + * This returns a boolean indicating if an error occurred. + */ +static bool set_locktime_one (uid_t uid, long locktime) +{ + off_t offset; + struct faillog fl; + + offset = (off_t) uid * sizeof (fl); + if (offset + sizeof (fl) <= statbuf.st_size) { + /* fseeko errors are not really relevant for us. */ + int err = fseeko (fail, offset, SEEK_SET); + assert (0 == err); + /* faillog is a sparse file. Even if no entries were + * entered for this user, which should be able to get the + * empty entry in this case. + */ + if (fread ((char *) &fl, sizeof (fl), 1, fail) != 1) { + fprintf (stderr, + _("%s: Failed to get the entry for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; + } + } else { + /* Outsize of the faillog file. + * Behave as if there were a missing entry (same behavior + * as if we were reading an non existing entry in the + * sparse faillog file). + */ + memzero (&fl, sizeof (fl)); + } + + if (locktime == fl.fail_locktime) { + /* If the locktime is already set to the right value, do not + * write in the file. + * This avoids writing 0 when no entries were present for + * the user and the locktime argument is 0. + */ + return false; + } + + fl.fail_locktime = locktime; + + if ( (fseeko (fail, offset, SEEK_SET) == 0) + && (fwrite ((char *) &fl, sizeof (fl), 1, fail) == 1)) { + (void) fflush (fail); + return false; + } + + fprintf (stderr, + _("%s: Failed to set locktime for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; +} + +static void set_locktime (long locktime) +{ + if (uflg && has_umin && has_umax && (umin==umax)) { + if (set_locktime_one ((uid_t)umin, locktime)) { + errors = true; + } + } else { + /* Set locktime for entries in the specified range. + * If locktime is unchanged for an entry, the entry is not touched. + * If locktime is null, and no entries exist for this user, no + * entries will be created. + */ + if (aflg) { + /* Entries for non existing user are also taken into + * account (in order to define policy for future users). + */ + uid_t uid = 0; + /* The default umax value is based on the size of the + * faillog database. + */ + uid_t uidmax = statbuf.st_size / sizeof (struct faillog); + if (uidmax > 1) { + uidmax--; + } + + /* Make sure we stay in the umin-umax range if specified */ + if (has_umin) { + uid = (uid_t)umin; + } + if (has_umax) { + uidmax = (uid_t)umax; + } + + while (uid <= uidmax) { + if (set_locktime_one (uid, locktime)) { + errors = true; + } + uid++; + } + } else { + /* Only change records for existing users. + */ + struct passwd *pwent; + + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ( uflg + && ( (has_umin && (pwent->pw_uid < (uid_t)umin)) + || (has_umax && (pwent->pw_uid > (uid_t)umax)))) { + continue; + } + if (set_locktime_one (pwent->pw_uid, locktime)) { + errors = true; + } + } + endpwent (); + } + } +} + +int main (int argc, char **argv) +{ + long fail_locktime = 0; + short fail_max = 0; // initialize to silence compiler warning + long days = 0; + + /* + * Get the program name. The program name is used as a prefix to + * most error messages. + */ + Prog = Basename (argv[0]); + log_set_progname(Prog); + log_set_logfd(stderr); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + { + int c; + static struct option long_options[] = { + {"all", no_argument, NULL, 'a'}, + {"help", no_argument, NULL, 'h'}, + {"lock-secs", required_argument, NULL, 'l'}, + {"maximum", required_argument, NULL, 'm'}, + {"reset", no_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, + {"time", required_argument, NULL, 't'}, + {"user", required_argument, NULL, 'u'}, + {NULL, 0, NULL, '\0'} + }; + while ((c = getopt_long (argc, argv, "ahl:m:rR:t:u:", + long_options, NULL)) != -1) { + switch (c) { + case 'a': + aflg = true; + break; + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'l': + if (getlong (optarg, &fail_locktime) == 0) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + lflg = true; + break; + case 'm': + { + long int lmax; + if ( (getlong (optarg, &lmax) == 0) + || ((long int)(short) lmax != lmax)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + fail_max = (short) lmax; + mflg = true; + break; + } + case 'r': + rflg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 't': + if (getlong (optarg, &days) == 0) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + seconds = (time_t) days * DAY; + tflg = true; + break; + case 'u': + { + /* + * The user can be: + * - a login name + * - numerical + * - a numerical login ID + * - a range (-x, x-, x-y) + */ + struct passwd *pwent; + + uflg = true; + /* local, no need for xgetpwnam */ + pwent = getpwnam (optarg); + if (NULL != pwent) { + umin = (unsigned long) pwent->pw_uid; + has_umin = true; + umax = umin; + has_umax = true; + } else { + if (getrange (optarg, + &umin, &has_umin, + &umax, &has_umax) == 0) { + fprintf (stderr, + _("%s: Unknown user or range: %s\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + } + + break; + } + default: + usage (E_USAGE); + } + } + if (argc > optind) { + fprintf (stderr, + _("%s: unexpected argument: %s\n"), + Prog, argv[optind]); + usage (EXIT_FAILURE); + } + } + + if (tflg && (lflg || mflg || rflg)) { + usage (E_USAGE); + } + + /* Open the faillog database */ + if (lflg || mflg || rflg) { + fail = fopen (FAILLOG_FILE, "r+"); + } else { + fail = fopen (FAILLOG_FILE, "r"); + } + if (NULL == fail) { + fprintf (stderr, + _("%s: Cannot open %s: %s\n"), + Prog, FAILLOG_FILE, strerror (errno)); + exit (E_NOPERM); + } + + /* Get the size of the faillog */ + if (fstat (fileno (fail), &statbuf) != 0) { + fprintf (stderr, + _("%s: Cannot get the size of %s: %s\n"), + Prog, FAILLOG_FILE, strerror (errno)); + exit (E_NOPERM); + } + + if (lflg) { + set_locktime (fail_locktime); + } + + if (mflg) { + setmax (fail_max); + } + + if (rflg) { + reset (); + } + + if (!(lflg || mflg || rflg)) { + print (); + } + + if (lflg || mflg || rflg) { + if ( (ferror (fail) != 0) + || (fflush (fail) != 0) + || (fsync (fileno (fail)) != 0) + || (fclose (fail) != 0)) { + fprintf (stderr, + _("%s: Failed to write %s: %s\n"), + Prog, FAILLOG_FILE, strerror (errno)); + (void) fclose (fail); + errors = true; + } + } else { + (void) fclose (fail); + } + + exit (errors ? E_NOPERM : E_SUCCESS); +} + |