/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2018-2023 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <sys/types.h>
#if defined(MAJOR_IN_MKDEV)
# include <sys/mkdev.h>
#elif defined(MAJOR_IN_SYSMACROS)
# include <sys/sysmacros.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <time.h>
#include <unistd.h>

#include <sudoers.h>
#include <timestamp.h>

struct timestamp_entry_common {
    unsigned short version;	/* version number */
    unsigned short size;	/* entry size */
    unsigned short type;	/* TS_GLOBAL, TS_TTY, TS_PPID */
    unsigned short flags;	/* TS_DISABLED, TS_ANYUID */
};

union timestamp_entry_storage {
    struct timestamp_entry_common common;
    struct timestamp_entry_v1 v1;
    struct timestamp_entry v2;
};

sudo_dso_public int main(int argc, char *argv[]);

static void dump_entry(struct timestamp_entry *entry, off_t pos);
static bool valid_entry(union timestamp_entry_storage *u, off_t pos);
static bool convert_entry(union timestamp_entry_storage *record, struct timespec *off);
sudo_noreturn static void usage(void);

/*
 * tsdump: a simple utility to dump the contents of a time stamp file.
 * Unlock sudo, does not perform any locking of the time stamp file.
 */

int
main(int argc, char *argv[])
{
    int ch, fd;
    const char *user = NULL;
    char *fname = NULL;
    union timestamp_entry_storage cur;
    struct timespec now, timediff;
    debug_decl(main, SUDOERS_DEBUG_MAIN);

#if defined(SUDO_DEVEL) && defined(__OpenBSD__)
    malloc_options = "S";
#endif

    initprogname(argc > 0 ? argv[0] : "tsdump");

    bindtextdomain("sudoers", LOCALEDIR);
    textdomain("sudoers");

    /* Initialize the debug subsystem. */
    if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1)
	return EXIT_FAILURE;
    if (!sudoers_debug_register(getprogname(), sudo_conf_debug_files(getprogname())))
	return EXIT_FAILURE;

    while ((ch = getopt(argc, argv, "f:u:")) != -1) {
	switch (ch) {
	    case 'f':
		fname = optarg;
		break;
	    case 'u':
		user = optarg;
		break;
	    default:
		usage();
	}
    }
    argc -= optind;
    argv += optind;

    if (fname != NULL && user != NULL) {
	sudo_warnx("the -f and -u flags are mutually exclusive");
	usage();
    }

    /* Calculate the difference between real time and mono time. */
    if (sudo_gettime_real(&now) == -1)
	sudo_fatal("unable to get current time");
    if (sudo_gettime_mono(&timediff) == -1)
	sudo_fatal("unable to read the clock");
    sudo_timespecsub(&now, &timediff, &timediff);

    if (fname == NULL) {
	uid_t uid;
	int len;

	if (user == NULL) {
	    uid = geteuid();
	} else {
	    struct passwd *pw = getpwnam(user);
	    if (pw == NULL)
		sudo_fatalx(U_("unknown user %s"), user);
	    uid = pw->pw_uid;
	}
	len = asprintf(&fname, "%s/%u", _PATH_SUDO_TIMEDIR, (unsigned int)uid);
	if (len == -1)
	    sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    }

    fd = open(fname, O_RDONLY);
    if (fd == -1)
	sudo_fatal(U_("unable to open %s"), fname);

    for (;;) {
	off_t pos = lseek(fd, 0, SEEK_CUR);
	ssize_t nread;
	bool valid;

	if ((nread = read(fd, &cur, sizeof(cur))) == 0)
	    break;
	if (nread < 0)
	    sudo_fatal(U_("unable to read %s"), fname);

	valid = valid_entry(&cur, pos);
	if (cur.common.size != 0 && cur.common.size != sizeof(cur)) {
	    off_t offset = (off_t)cur.common.size - (off_t)sizeof(cur);
	    if (lseek(fd, offset, SEEK_CUR) == -1)
		sudo_fatal("unable to seek %d bytes", (int)offset);
	}
	if (valid) {
	    /* Convert entry to latest version as needed. */
	    if (!convert_entry(&cur, &timediff))
		continue;
	    dump_entry(&cur.v2, pos);
	}
    }

    return 0;
}

static bool
valid_entry(union timestamp_entry_storage *u, off_t pos)
{
    struct timestamp_entry *entry = (struct timestamp_entry *)u;
    debug_decl(valid_entry, SUDOERS_DEBUG_UTIL);

    switch (entry->version) {
    case 1:
	if (entry->size != sizeof(struct timestamp_entry_v1)) {
	    sudo_warn("wrong sized v1 record @ %lld, got %hu, expected %zu",
		(long long)pos, entry->size, sizeof(struct timestamp_entry_v1));
	    debug_return_bool(false);
	}
	break;
    case 2:
	if (entry->size != sizeof(struct timestamp_entry)) {
	    sudo_warn("wrong sized v2 record @ %lld, got %hu, expected %zu",
		(long long)pos, entry->size, sizeof(struct timestamp_entry));
	    debug_return_bool(false);
	}
	break;
    default:
	sudo_warn("unknown time stamp entry version %d @ %lld",
	    (int)entry->version, (long long)pos);
	debug_return_bool(false);
	break;
    }
    debug_return_bool(true);
}

static const char *
type2string(int type)
{
    static char name[64];
    debug_decl(type2string, SUDOERS_DEBUG_UTIL);

    switch (type) {
    case TS_LOCKEXCL:
	debug_return_const_str("TS_LOCKEXCL");
    case TS_GLOBAL:
	debug_return_const_str("TS_GLOBAL");
    case TS_TTY:
	debug_return_const_str("TS_TTY");
    case TS_PPID:
	debug_return_const_str("TS_PPID");
    }
    (void)snprintf(name, sizeof(name), "UNKNOWN (0x%x)", type);
    debug_return_const_str(name);
}

static void
print_flags(unsigned int flags)
{
    bool first = true;
    debug_decl(print_flags, SUDOERS_DEBUG_UTIL);

    printf("flags: ");
    if (ISSET(flags, TS_DISABLED)) {
	printf("%sTS_DISABLED", first ? "" : ", ");
	CLR(flags, TS_DISABLED);
	first = false;
    }
    if (ISSET(flags, TS_ANYUID)) {
	/* TS_ANYUID should never appear on disk. */
	printf("%sTS_ANYUID", first ? "" : ", ");
	CLR(flags, TS_ANYUID);
	first = false;
    }
    if (flags != 0)
	printf("%s0x%x", first ? "" : ", ", flags);
    putchar('\n');

    debug_return;
}

/*
 * Convert an older entry to current.
 * Also adjusts time stamps on Linux to be wallclock time.
 */
static bool
convert_entry(union timestamp_entry_storage *record, struct timespec *off)
{
    union timestamp_entry_storage orig;
    debug_decl(convert_entry, SUDOERS_DEBUG_UTIL);

    if (record->common.version != TS_VERSION) {
	if (record->common.version != 1) {
	    sudo_warnx("unexpected record version %hu", record->common.version);
	    debug_return_bool(false);
	}

	/* The first four fields are the same regardless of version. */
	memcpy(&orig, record, sizeof(union timestamp_entry_storage));
	record->v2.auth_uid = orig.v1.auth_uid;
	record->v2.sid = orig.v1.sid;
	sudo_timespecclear(&record->v2.start_time);
	record->v2.ts = orig.v1.ts;
	if (record->common.type == TS_TTY)
	    record->v2.u.ttydev = orig.v1.u.ttydev;
	else if (record->common.type == TS_PPID)
	    record->v2.u.ppid = orig.v1.u.ppid;
	else
	    memset(&record->v2.u, 0, sizeof(record->v2.u));
    }

    /* On Linux, start time is relative to boot time, adjust to real time. */
#ifdef __linux__
    if (sudo_timespecisset(&record->v2.start_time))
	sudo_timespecadd(&record->v2.start_time, off, &record->v2.start_time);
#endif

    /* Adjust time stamp from mono time to real time. */
    if (sudo_timespecisset(&record->v2.ts))
	sudo_timespecadd(&record->v2.ts, off, &record->v2.ts);

    debug_return_bool(true);
}

static void
dump_entry(struct timestamp_entry *entry, off_t pos)
{
    debug_decl(dump_entry, SUDOERS_DEBUG_UTIL);

    printf("position: %lld\n", (long long)pos);
    printf("version: %hu\n", entry->version);
    printf("size: %hu\n", entry->size);
    printf("type: %s\n", type2string(entry->type));
    print_flags(entry->flags);
    printf("auth uid: %u\n", (unsigned int)entry->auth_uid);
    printf("session ID: %d\n", (int)entry->sid);
    if (sudo_timespecisset(&entry->start_time))
	printf("start time: %s", ctime(&entry->start_time.tv_sec));
    if (sudo_timespecisset(&entry->ts))
	printf("time stamp: %s", ctime(&entry->ts.tv_sec));
    if (entry->type == TS_TTY) {
	char ttypath[PATH_MAX];
	if (sudo_ttyname_dev(entry->u.ttydev, ttypath, sizeof(ttypath)) == NULL) {
	    printf("terminal: %u, %u (0x%x)\n",
		(unsigned int)major(entry->u.ttydev),
		(unsigned int)minor(entry->u.ttydev),
		(unsigned int)entry->u.ttydev);
	} else {
	    printf("terminal: %s (0x%x)\n", ttypath,
		(unsigned int)entry->u.ttydev);
	}
    } else if (entry->type == TS_PPID) {
	printf("parent pid: %d\n", (int)entry->u.ppid);
    }
    putchar('\n');

    debug_return;
}

sudo_noreturn static void
usage(void)
{
    fprintf(stderr, "usage: %s [-f timestamp_file] | [-u username]\n",
	getprogname());
    exit(EXIT_FAILURE);
}
