/* virt-log
 * Copyright (C) 2010-2023 Red Hat Inc.
 *
 * 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 <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <fcntl.h>
#include <unistd.h>
#include <getopt.h>
#include <errno.h>
#include <error.h>
#include <locale.h>
#include <limits.h>
#include <assert.h>
#include <libintl.h>
#include <syslog.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>

#include "getprogname.h"

#include "guestfs.h"
#include "structs-cleanups.h"
#include "options.h"
#include "display-options.h"

/* Currently open libguestfs handle. */
guestfs_h *g;

int read_only = 1;
int verbose = 0;
int keys_from_stdin = 0;
int echo_keys = 0;
const char *libvirt_uri = NULL;
int inspector = 1;
int in_guestfish = 0;
int in_virt_rescue = 0;

#define JOURNAL_DIR "/var/log/journal"

static int do_log (void);
static int do_log_journal (void);
static int do_log_text_file (const char *filename);
static int do_log_windows_evtx (void);

static void __attribute__((noreturn))
usage (int status)
{
  if (status != EXIT_SUCCESS)
    fprintf (stderr, _("Try ‘%s --help’ for more information.\n"),
             getprogname ());
  else {
    printf (_("%s: display log files in a virtual machine\n"
              "Copyright (C) 2010-2023 Red Hat Inc.\n"
              "Usage:\n"
              "  %s [--options] -d domname\n"
              "  %s [--options] -a disk.img [-a disk.img ...]\n"
              "Options:\n"
              "  -a|--add image       Add image\n"
              "  --blocksize[=512|4096]\n"
              "                       Set sector size of the disk for -a option\n"
              "  -c|--connect uri     Specify libvirt URI for -d option\n"
              "  -d|--domain guest    Add disks from libvirt guest\n"
              "  --echo-keys          Don't turn off echo for passphrases\n"
              "  --format[=raw|..]    Force disk format for -a option\n"
              "  --help               Display brief help\n"
              "  --key selector       Specify a LUKS key\n"
              "  --keys-from-stdin    Read passphrases from stdin\n"
              "  -v|--verbose         Verbose messages\n"
              "  -V|--version         Display version and exit\n"
              "  -x                   Trace libguestfs API calls\n"
              "For more information, see the manpage %s(1).\n"),
            getprogname (), getprogname (),
            getprogname (), getprogname ());
  }
  exit (status);
}

int
main (int argc, char *argv[])
{
  setlocale (LC_ALL, "");
  bindtextdomain (PACKAGE, LOCALEBASEDIR);
  textdomain (PACKAGE);

  enum { HELP_OPTION = CHAR_MAX + 1 };

  static const char options[] = "a:c:d:vVx";
  static const struct option long_options[] = {
    { "add", 1, 0, 'a' },
    { "blocksize", 2, 0, 0 },
    { "connect", 1, 0, 'c' },
    { "domain", 1, 0, 'd' },
    { "echo-keys", 0, 0, 0 },
    { "format", 2, 0, 0 },
    { "help", 0, 0, HELP_OPTION },
    { "key", 1, 0, 0 },
    { "keys-from-stdin", 0, 0, 0 },
    { "long-options", 0, 0, 0 },
    { "short-options", 0, 0, 0 },
    { "verbose", 0, 0, 'v' },
    { "version", 0, 0, 'V' },
    { 0, 0, 0, 0 }
  };
  struct drv *drvs = NULL;
  const char *format = NULL;
  bool format_consumed = true;
  int blocksize = 0;
  bool blocksize_consumed = true;
  int c;
  int r;
  int option_index;
  struct key_store *ks = NULL;

  g = guestfs_create ();
  if (g == NULL)
    error (EXIT_FAILURE, errno, "guestfs_create");

  for (;;) {
    c = getopt_long (argc, argv, options, long_options, &option_index);
    if (c == -1) break;

    switch (c) {
    case 0:			/* options which are long only */
      if (STREQ (long_options[option_index].name, "long-options"))
        display_long_options (long_options);
      else if (STREQ (long_options[option_index].name, "short-options"))
        display_short_options (options);
      else if (STREQ (long_options[option_index].name, "keys-from-stdin")) {
        keys_from_stdin = 1;
      } else if (STREQ (long_options[option_index].name, "echo-keys")) {
        echo_keys = 1;
      } else if (STREQ (long_options[option_index].name, "format")) {
        OPTION_format;
      } else if (STREQ (long_options[option_index].name, "blocksize")) {
        OPTION_blocksize;
      } else if (STREQ (long_options[option_index].name, "key")) {
        OPTION_key;
      } else
        error (EXIT_FAILURE, 0,
               _("unknown long option: %s (%d)"),
               long_options[option_index].name, option_index);
      break;

    case 'a':
      OPTION_a;
      break;

    case 'c':
      OPTION_c;
      break;

    case 'd':
      OPTION_d;
      break;

    case 'v':
      OPTION_v;
      break;

    case 'V':
      OPTION_V;
      break;

    case 'x':
      OPTION_x;
      break;

    case HELP_OPTION:
      usage (EXIT_SUCCESS);

    default:
      usage (EXIT_FAILURE);
    }
  }

  /* These are really constants, but they have to be variables for the
   * options parsing code.  Assert here that they have known-good
   * values.
   */
  assert (read_only == 1);
  assert (inspector == 1);

  /* User must not specify more arguments on the command line. */
  if (optind != argc) {
    fprintf (stderr, _("%s: error: extra argument ‘%s’ on command line.\n"
             "Make sure to specify the argument for --format "
             "like '--format=%s'.\n"),
             getprogname (), argv[optind], argv[optind]);
    usage (EXIT_FAILURE);
  }

  CHECK_OPTION_format_consumed;
  CHECK_OPTION_blocksize_consumed;

  /* User must have specified some drives. */
  if (drvs == NULL) {
    fprintf (stderr, _("%s: error: you must specify at least one -a or -d option.\n"),
             getprogname ());
    usage (EXIT_FAILURE);
  }

  /* Add drives, inspect and mount.  Note that inspector is always true,
   * and there is no -m option.
   */
  add_drives (drvs);

  if (key_store_requires_network (ks) && guestfs_set_network (g, 1) == -1)
    exit (EXIT_FAILURE);

  if (guestfs_launch (g) == -1)
    exit (EXIT_FAILURE);

  inspect_mount ();

  /* Free up data structures, no longer needed after this point. */
  free_drives (drvs);
  free_key_store (ks);

  r = do_log ();

  guestfs_close (g);

  exit (r == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
}

static int
do_log (void)
{
  CLEANUP_FREE_STRING_LIST char **roots = NULL;
  char *root;
  CLEANUP_FREE char *type = NULL;
  CLEANUP_FREE_STRING_LIST char **journal_files = NULL;

  /* Get root mountpoint.  fish/inspect.c guarantees the assertions
   * below.
   */
  roots = guestfs_inspect_get_roots (g);
  assert (roots);
  assert (roots[0] != NULL);
  assert (roots[1] == NULL);
  root = roots[0];

  type = guestfs_inspect_get_type (g, root);
  if (!type)
    return -1;

  /* Windows needs special handling. */
  if (STREQ (type, "windows")) {
    if (guestfs_inspect_get_major_version (g, root) >= 6)
      return do_log_windows_evtx ();

    fprintf (stderr,
             _("%s: Windows Event Log for pre-Vista guests is not supported.\n"),
             getprogname ());
    return -1;
  }

  /* systemd journal? */
  guestfs_push_error_handler (g, NULL, NULL);
  journal_files = guestfs_ls (g, JOURNAL_DIR);
  guestfs_pop_error_handler (g);
  if (STREQ (type, "linux") &&
      journal_files != NULL && journal_files[0] != NULL)
    return do_log_journal ();

  /* Regular /var/log text files with different names. */
  if (STRNEQ (type, "windows")) {
    const char *logfiles[] = { "/var/log/syslog", "/var/log/messages", NULL };
    size_t i;

    for (i = 0; logfiles[i] != NULL; ++i) {
      if (guestfs_is_file_opts (g, logfiles[i],
                                GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1,
                                -1) == 1)
        return do_log_text_file (logfiles[i]);
    }
  }

  /* Otherwise, there are no log files.  Hmm, is this right? XXX */
  return 0;
}

/* Find the value of the named field from the list of attributes.  If
 * not found, returns NULL (not an error).  If found, returns a
 * pointer to the field, and the length of the field.  NOTE: The field
 * is NOT \0-terminated, so you have to print it using "%.*s".
 *
 * There may be multiple fields with the same name.  In this case, the
 * function returns the first entry.
 */
static const char *
get_journal_field (const struct guestfs_xattr_list *xattrs, const char *name,
                   size_t *len_rtn)
{
  uint32_t i;

  for (i = 0; i < xattrs->len; ++i) {
    if (STREQ (name, xattrs->val[i].attrname)) {
      *len_rtn = xattrs->val[i].attrval_len;
      return xattrs->val[i].attrval;
    }
  }

  return NULL;                  /* not found */
}

static const char *const log_level_table[] = {
  [LOG_EMERG] = "emerg",
  [LOG_ALERT] = "alert",
  [LOG_CRIT] = "crit",
  [LOG_ERR] = "err",
  [LOG_WARNING] = "warning",
  [LOG_NOTICE] = "notice",
  [LOG_INFO] = "info",
  [LOG_DEBUG] = "debug"
};

static int
do_log_journal (void)
{
  int r;
  unsigned errors = 0;

  if (guestfs_journal_open (g, JOURNAL_DIR) == -1)
    return -1;

  while ((r = guestfs_journal_next (g)) > 0) {
    CLEANUP_FREE_XATTR_LIST struct guestfs_xattr_list *xattrs = NULL;
    const char *priority_str, *identifier, *comm, *pid, *message;
    size_t priority_len, identifier_len, comm_len = 0, pid_len, message_len;
    int priority = LOG_INFO;
    int64_t ts;

    /* The question is what fields to display.  We should probably
     * make this configurable, but for now use the "short" format from
     * journalctl.  (XXX)
     */

    xattrs = guestfs_journal_get (g);
    if (xattrs == NULL)
      return -1;

    ts = guestfs_journal_get_realtime_usec (g); /* error checked below */

    priority_str = get_journal_field (xattrs, "PRIORITY", &priority_len);
    //hostname = get_journal_field (xattrs, "_HOSTNAME", &hostname_len);
    identifier = get_journal_field (xattrs, "SYSLOG_IDENTIFIER",
                                    &identifier_len);
    comm = get_journal_field (xattrs, "_COMM", &comm_len);
    pid = get_journal_field (xattrs, "_PID", &pid_len);
    message = get_journal_field (xattrs, "MESSAGE", &message_len);

    /* Timestamp. */
    if (ts >= 0) {
      char buf[64];
      time_t t = ts / 1000000;
      struct tm tm;

      if (strftime (buf, sizeof buf, "%b %d %H:%M:%S",
                    localtime_r (&t, &tm)) <= 0) {
        fprintf (stderr, _("%s: could not format journal entry timestamp\n"),
                 getprogname ());
        errors++;
        continue;
      }
      fputs (buf, stdout);
    }

    /* Hostname. */
    /* We don't print this because it is assumed each line from the
     * guest will have the same hostname.  (XXX)
     */
    //if (hostname)
    //  printf (" %.*s", (int) hostname_len, hostname);

    /* Identifier. */
    if (identifier)
      printf (" %.*s", (int) identifier_len, identifier);
    else if (comm)
      printf (" %.*s", (int) comm_len, comm);

    /* PID */
    if (pid)
      printf ("[%.*s]", (int) pid_len, pid);

    /* Log level. */
    if (priority_str && *priority_str >= '0' && *priority_str <= '7')
      priority = *priority_str - '0';

    printf (" %s:", log_level_table[priority]);

    /* Message. */
    if (message)
      printf (" %.*s", (int) message_len, message);

    printf ("\n");
  }
  if (r == -1)                  /* error from guestfs_journal_next */
    return -1;

  if (guestfs_journal_close (g) == -1)
    return -1;

  return errors > 0 ? -1 : 0;
}

static int
do_log_text_file (const char *filename)
{
  return guestfs_download (g, filename, "/dev/stdout");
}

/* For Windows >= Vista, if evtxdump.py is installed then we can
 * use it to dump the System.evtx log.
 */
static int
do_log_windows_evtx (void)
{
  CLEANUP_FREE char *filename = NULL;
  CLEANUP_FREE char *tmpdir = guestfs_get_tmpdir (g);
  CLEANUP_UNLINK_FREE char *localfile = NULL;
  CLEANUP_FREE char *cmd = NULL;
  char dev_fd[64];
  int fd, status;

  if (system ("evtxdump.py -h >/dev/null 2>&1") != 0) {
    fprintf (stderr, _("%s: you need to install ‘evtxdump.py’ (from the python-evtx package)\n"
                       "in order to parse Windows Event Logs.  If you cannot install this, then\n"
                       "use virt-copy-out(1) to copy the contents of /Windows/System32/winevt/Logs\n"
                       "from this guest, and examine in a binary file viewer.\n"),
             getprogname ());
    return -1;
  }

  /* Check if System.evtx exists.  XXX Allow the filename to be
   * configurable, since there are many logs.
   */
  filename = guestfs_case_sensitive_path (g, "/Windows/System32/winevt/Logs/System.evtx");
  if (filename == NULL)
    return -1;

  /* Note that guestfs_case_sensitive_path does NOT check for existence. */
  if (guestfs_is_file_opts (g, filename,
                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1,
                            -1) <= 0) {
    fprintf (stderr, _("%s: Windows Event Log file (%s) not found\n"),
             getprogname (), filename);
    return -1;
  }

  /* Download the file to a temporary.  Python-evtx wants to mmap
   * the file so we cannot use a pipe.
   */
  if (asprintf (&localfile, "%s/virtlogXXXXXX", tmpdir) == -1) {
    perror ("asprintf");
    return -1;
  }
  if ((fd = mkstemp (localfile)) == -1) {
    perror ("mkstemp");
    return -1;
  }

  snprintf (dev_fd, sizeof dev_fd, "/dev/fd/%d", fd);

  if (guestfs_download (g, filename, dev_fd) == -1)
    return -1;
  close (fd);

  /* This should be safe as long as $TMPDIR is not set to something wild. */
  if (asprintf (&cmd, "evtxdump.py '%s'", localfile) == -1) {
    perror ("asprintf");
    return -1;
  }

  status = system (cmd);
  if (status) {
    char buf[256];
    fprintf (stderr, "%s: %s\n",
             getprogname (),
             guestfs_int_exit_status_to_string (status, "evtxdump.py",
                                                buf, sizeof buf));
    return -1;
  }

  return 0;
}
