/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2009-2020 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 <stdio.h>
#include <stdlib.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include <compat/stdbool.h>
#endif /* HAVE_STDBOOL_H */
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#include <sudo_compat.h>
#include <sudo_debug.h>
#include <sudo_eventlog.h>
#include <sudo_fatal.h>
#include <sudo_gettext.h>
#include <sudo_json.h>
#include <sudo_iolog.h>
#include <sudo_util.h>

struct eventlog *
iolog_parse_loginfo(int dfd, const char *iolog_dir)
{
    struct eventlog *evlog = NULL;
    FILE *fp = NULL;
    int fd = -1;
    int tmpfd = -1;
    bool ok, legacy = false;
    debug_decl(iolog_parse_loginfo, SUDO_DEBUG_UTIL);

    if (dfd == -1) {
	if ((tmpfd = open(iolog_dir, O_RDONLY|O_DIRECTORY)) == -1) {
	    sudo_warn("%s", iolog_dir);
	    goto bad;
	}
	dfd = tmpfd;
    }
    if ((fd = openat(dfd, "log.json", O_RDONLY, 0)) == -1) {
	fd = openat(dfd, "log", O_RDONLY, 0);
	legacy = true;
    }
    if (tmpfd != -1)
	close(tmpfd);
    if (fd == -1 || (fp = fdopen(fd, "r")) == NULL) {
	sudo_warn("%s/log", iolog_dir);
	goto bad;
    }
    fd = -1;

    if ((evlog = calloc(1, sizeof(*evlog))) == NULL) {
	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	goto bad;
    }
    evlog->runuid = (uid_t)-1;
    evlog->rungid = (gid_t)-1;
    evlog->exit_value = -1;

    ok = legacy ? iolog_parse_loginfo_legacy(fp, iolog_dir, evlog) :
	iolog_parse_loginfo_json(fp, iolog_dir, evlog);
    if (ok) {
	fclose(fp);
	debug_return_ptr(evlog);
    }

bad:
    if (fd != -1)
	close(fd);
    if (fp != NULL)
	fclose(fp);
    eventlog_free(evlog);
    debug_return_ptr(NULL);
}

/*
 * Write the legacy I/O log file that contains the user and command info.
 * This file is not compressed.
 */
static bool
iolog_write_info_file_legacy(int dfd, struct eventlog *evlog)
{
    char * const *av;
    FILE *fp;
    int error, fd;
    debug_decl(iolog_info_write_log, SUDO_DEBUG_UTIL);

    fd = iolog_openat(dfd, "log", O_CREAT|O_TRUNC|O_WRONLY);
    if (fd == -1 || (fp = fdopen(fd, "w")) == NULL) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
	    "unable to %sopen %s/log", fd == -1 ? "" : "fd", evlog->iolog_path);
	if (fd != -1)
	    close(fd);
	debug_return_bool(false);
    }
    if (fchown(fd, iolog_get_uid(), iolog_get_gid()) != 0) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
	    "%s: unable to fchown %d:%d %s/log", __func__,
	    (int)iolog_get_uid(), (int)iolog_get_gid(), evlog->iolog_path);
    }

    fprintf(fp, "%lld:%s:%s:%s:%s:%d:%d\n%s\n",
	(long long)evlog->event_time.tv_sec,
	evlog->submituser ? evlog->submituser : "unknown",
	evlog->runuser ? evlog->runuser : RUNAS_DEFAULT,
	evlog->rungroup ? evlog->rungroup : "",
	evlog->ttyname ? evlog->ttyname : "unknown",
	evlog->lines, evlog->columns,
	evlog->cwd ? evlog->cwd : "unknown");
    fputs(evlog->command ? evlog->command : "unknown", fp);
    for (av = evlog->runargv + 1; *av != NULL; av++) {
	fputc(' ', fp);
	fputs(*av, fp);
    }
    fputc('\n', fp);
    fflush(fp);
    if ((error = ferror(fp))) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
	    "unable to write to I/O log file %s/log", evlog->iolog_path);
    }
    fclose(fp);

    debug_return_bool(!error);
}

/*
 * Write the "log.json" file that contains the user and command info.
 * This file is not compressed.
 */
static bool
iolog_write_info_file_json(int dfd, struct eventlog *evlog)
{
    struct json_container jsonc;
    struct json_value json_value;
    bool ret = false;
    FILE *fp = NULL;
    int fd = -1;
    debug_decl(iolog_write_info_file_json, SUDO_DEBUG_UTIL);

    if (!sudo_json_init(&jsonc, 4, false, false, false))
	debug_return_bool(false);

    /* Timestamp */
    if (!sudo_json_open_object(&jsonc, "timestamp"))
	goto oom;

    json_value.type = JSON_NUMBER;
    json_value.u.number = evlog->event_time.tv_sec;
    if (!sudo_json_add_value(&jsonc, "seconds", &json_value))
        goto oom;

    json_value.type = JSON_NUMBER;
    json_value.u.number = evlog->event_time.tv_nsec;
    if (!sudo_json_add_value(&jsonc, "nanoseconds", &json_value))
        goto oom;

    if (!sudo_json_close_object(&jsonc))
	goto oom;

    if (!eventlog_store_json(&jsonc, evlog))
	goto done;

    fd = iolog_openat(dfd, "log.json", O_CREAT|O_TRUNC|O_WRONLY);
    if (fd == -1 || (fp = fdopen(fd, "w")) == NULL) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
	    "unable to %sopen %s/log.json", fd == -1 ? "" : "fd",
	    evlog->iolog_path);
        goto done;
    }
    if (fchown(fd, iolog_get_uid(), iolog_get_gid()) != 0) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
	    "%s: unable to fchown %d:%d %s/log.json", __func__,
	    (int)iolog_get_uid(), (int)iolog_get_gid(), evlog->iolog_path);
    }
    fd = -1;

    fprintf(fp, "{%s\n}\n", sudo_json_get_buf(&jsonc));
    fflush(fp);
    if (ferror(fp)) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
            "unable to write to I/O log file %s/log.json", evlog->iolog_path);
	goto done;
    }

    ret = true;
    goto done;

oom:
    sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
done:
    sudo_json_free(&jsonc);
    if (fp != NULL)
	fclose(fp);
    if (fd != -1)
	close(fd);

    debug_return_bool(ret);
}

/*
 * Write the I/O log and log.json files that contain user and command info.
 * These files are not compressed.
 */
bool
iolog_write_info_file(int dfd, struct eventlog *evlog)
{
    debug_decl(iolog_write_info_file, SUDO_DEBUG_UTIL);

    if (!iolog_write_info_file_legacy(dfd, evlog))
	debug_return_bool(false);
    if (!iolog_write_info_file_json(dfd, evlog))
	debug_return_bool(false);

    debug_return_bool(true);
}
