/*
    Copyright (C) 2009  Zdenek Prikryl (zprikryl@redhat.com)
    Copyright (C) 2009  RedHat 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 <fnmatch.h>
#include <glib.h>
#include "libabrt.h"
#include "rpm.h"

#define GPG_CONF "gpg_keys.conf"

/**
    "python3.4, python3.5, python3.6, python3.7, perl, perl5.16.2"
  * The regexes should cover interpreters with basename:
  * Python:
  *   python
  *   python2
  *   python3
  *   python2.7
  *   python3.8
  *   platform-python
  *   platform-python3
  *   platform-python3.8
  *
  * Perl:
  *   perl
  *   perl5.30.1
  *
  * PHP:
  *   php
  *   php-cgi
  *
  * R:
  *   R
  *
  * tcl:
  *   tclsh
  *   tclsh8.6
  **/
#define DEFAULT_INTERPRETERS_REGEX \
    "^(perl ([[:digit:]][.][[:digit:]]+[.][[:digit:]])? |" \
    "php (-cgi)? |" \
    "(platform-)? python ([[:digit:]]([.][[:digit:]])?)? |" \
    "R |" \
    "tclsh ([[:digit:]][.][[:digit:]])?)$"

static bool   settings_bOpenGPGCheck = false;
static GList *settings_setOpenGPGPublicKeys = NULL;
static GList *settings_setBlackListedPkgs = NULL;
static GList *settings_setBlackListedPaths = NULL;
static bool   settings_bProcessUnpackaged = false;
static GList *settings_Interpreters = NULL;

static void ParseCommon(map_string_t *settings, const char *conf_filename)
{
    const char *value;

    value = get_map_string_item_or_NULL(settings, "OpenGPGCheck");
    if (value)
    {
        settings_bOpenGPGCheck = string_to_bool(value);
        remove_map_string_item(settings, "OpenGPGCheck");
    }

    value = get_map_string_item_or_NULL(settings, "BlackList");
    if (value)
    {
        settings_setBlackListedPkgs = parse_list(value);
        remove_map_string_item(settings, "BlackList");
    }

    value = get_map_string_item_or_NULL(settings, "BlackListedPaths");
    if (value)
    {
        settings_setBlackListedPaths = parse_list(value);
        remove_map_string_item(settings, "BlackListedPaths");
    }

    value = get_map_string_item_or_NULL(settings, "ProcessUnpackaged");
    if (value)
    {
        settings_bProcessUnpackaged = string_to_bool(value);
        remove_map_string_item(settings, "ProcessUnpackaged");
    }

    value = get_map_string_item_or_NULL(settings, "Interpreters");
    if (value)
    {
        settings_Interpreters = parse_list(value);
        remove_map_string_item(settings, "Interpreters");
    }

    map_string_iter_t iter;
    const char *name;
    /*char *value; - already declared */
    init_map_string_iter(&iter, settings);
    while (next_map_string_iter(&iter, &name, &value))
    {
        error_msg("Unrecognized variable '%s' in '%s'", name, conf_filename);
    }
}

static void load_gpg_keys(void)
{
    map_string_t *settings = new_map_string();
    if (!load_abrt_conf_file(GPG_CONF, settings))
    {
        error_msg("Can't load '%s'", GPG_CONF);
        return;
    }

    const char *gpg_keys_dir = get_map_string_item_or_NULL(settings, "GPGKeysDir");
    if (gpg_keys_dir != NULL && strcmp(gpg_keys_dir, "") != 0)
    {
        log_debug("Reading gpg keys from '%s'", gpg_keys_dir);
        GHashTable *done_set = g_hash_table_new(g_str_hash, g_str_equal);
        GList *gpg_files = get_file_list(gpg_keys_dir, NULL /* we don't care about the file ext */);
        for (GList *iter = gpg_files; iter; iter = g_list_next(iter))
        {
            const char *key_path = fo_get_fullpath((file_obj_t *)iter->data);

            if (g_hash_table_contains(done_set, key_path))
                continue;

            g_hash_table_insert(done_set, (gpointer)key_path, NULL);
            log_debug("Loading gpg key '%s'", key_path);
            settings_setOpenGPGPublicKeys = g_list_append(settings_setOpenGPGPublicKeys, xstrdup(key_path));
        }

        g_list_free_full(gpg_files, (GDestroyNotify)free_file_obj);
        g_hash_table_destroy(done_set);
    }
}

static int load_conf(const char *conf_filename)
{
    map_string_t *settings = new_map_string();
    if (conf_filename != NULL)
    {
        if (!load_conf_file(conf_filename, settings, false))
            error_msg("Can't open '%s'", conf_filename);
    }
    else
    {
        conf_filename = "abrt-action-save-package-data.conf";
        if (!load_abrt_conf_file(conf_filename, settings))
            error_msg("Can't load '%s'", conf_filename);
    }

    ParseCommon(settings, conf_filename);
    free_map_string(settings);

    load_gpg_keys();

    return 0;
}

/**
 * Returns the first full path argument in the command line or NULL.
 * Skips options (params of the form "-XXX").
 * Returns malloc'ed string.
 * NB: the cmdline is delimited by (single, not multiple) spaces, not tabs!
 * "abc def\t123" means there are two params: "abc", "def\t123".
 * "abc  def" means there are three params: "abc", "", "def".
 */
static char *get_argv1_if_full_path(const char* cmdline)
{
    const char *argv1 = strchr(cmdline, ' ');
    while (argv1 != NULL)
    {
        /* we found space in cmdline, so it might contain
         * path to some script like:
         * /usr/bin/python [-XXX] /usr/bin/system-control-network
         */
        argv1++; /* skip the space */
        if (*argv1 != '-')
            break;
        /* looks like -XXX in "perl -XXX /usr/bin/script.pl", skipping */
        argv1 = strchr(argv1, ' ');
    }

    /* if the string following the space doesn't start
     * with '/', it is not a full path to script
     * and we can't use it to determine the package name
     */
    if (argv1 == NULL || *argv1 != '/')
        return NULL;

    /* good, it has "/foo/bar" form, return it */
    int len = strchrnul(argv1, ' ') - argv1;
    return xstrndup(argv1, len);
}

static bool is_path_blacklisted(const char *path)
{
    GList *li;
    for (li = settings_setBlackListedPaths; li != NULL; li = g_list_next(li))
    {
        if (fnmatch((char*)li->data, path, /*flags:*/ 0) == 0)
        {
            return true;
        }
    }
    return false;
}

static struct pkg_envra *get_script_name(const char *cmdline, char **executable, const char *chroot)
{
// TODO: we don't verify that python executable is not modified
// or that python package is properly signed
// (see CheckFingerprint/CheckHash below)
    /* Try to find package for the script by looking at argv[1].
     * This will work only if the cmdline contains the whole path.
     * Example: python /usr/bin/system-control-network
     */
    struct pkg_envra *script_pkg = NULL;
    char *script_name = get_argv1_if_full_path(cmdline);
    if (script_name)
    {
        script_pkg = rpm_get_package_nvr(script_name, chroot);
        if (script_pkg)
        {
            /* There is a well-formed script name in argv[1],
             * and it does belong to some package.
             * Replace executable
             * with data pertaining to the script.
             */
            *executable = script_name;
        }
    }

    return script_pkg;
}

static int SavePackageDescriptionToDebugDump(const char *dump_dir_name, const char *chroot)
{
    struct dump_dir *dd = dd_opendir(dump_dir_name, /*flags:*/ 0);
    if (!dd)
        return 1;

    char *type = dd_load_text(dd, FILENAME_TYPE);
    bool kernel_oops = !strcmp(type, "Kerneloops") || !strcmp(type, "vmcore");
    free(type);

    char *cmdline = NULL;
    char *executable = NULL;
    char *rootdir = NULL;
    char *package_short_name = NULL;
    char *fingerprint = NULL;
    struct pkg_envra *pkg_name = NULL;
    char *component = NULL;
    char *kernel = NULL;
    int error = 1;
    /* note: "goto ret" statements below free all the above variables,
     * but they don't dd_close(dd) */

    if (kernel_oops)
    {
        kernel = dd_load_text(dd, FILENAME_KERNEL);
        if (!kernel)
        {
            log_warning("File 'kernel' containing kernel version not "
                "found in current directory");
            goto ret;
        }
        /* Trim trailing white-spaces. */
        strchrnul(kernel, ' ')[0] = '\0';

        log_info("Looking for kernel package");
        executable = xasprintf("/boot/vmlinuz-%s", kernel);
    }
    else
    {
        cmdline = dd_load_text_ext(dd, FILENAME_CMDLINE, DD_FAIL_QUIETLY_ENOENT);
        executable = dd_load_text(dd, FILENAME_EXECUTABLE);
    }


    /* Do not implicitly query rpm database in process's root dir, if
     * ExploreChroots is disabled. */
    if (g_settings_explorechroots && chroot == NULL)
        chroot = rootdir = dd_load_text_ext(dd, FILENAME_ROOTDIR,
                               DD_FAIL_QUIETLY_ENOENT | DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE);

    /* Close dd while we query package database. It can take some time,
     * don't want to keep dd locked longer than necessary */
    dd_close(dd);
    dd = NULL;

    /* The check for kernel_oops is there because it could be an unexpected
     * behaviour. If one wants to ignore kernel oops, she/he should disable
     * the corresponding services. */
    if (!kernel_oops && is_path_blacklisted(executable))
    {
        log_warning("Blacklisted executable '%s'", executable);
        goto ret; /* return 1 (failure) */
    }

    pkg_name = rpm_get_package_nvr(executable, chroot);
    if (!pkg_name)
    {
        if (settings_bProcessUnpackaged)
        {
            log_info("Crash in unpackaged executable '%s', "
                      "proceeding without packaging information", executable);
            goto ret0; /* no error */
        }
        if (kernel_oops)
            log_warning("Can't find kernel package corresponding to '%s'", kernel);
        else
            log_warning("Executable '%s' doesn't belong to any package"
                " and ProcessUnpackaged is set to 'no'", executable);
        goto ret; /* return 1 (failure) */
    }

    if (kernel_oops)
        goto skip_interpreter;

    /* Check well-known interpreter names */
    const char *basename = strrchr(executable, '/');
    if (basename)
        basename++;
    else
        basename = executable;

    /* if basename is known interpreter, we want to blame the running script
     * not the interpreter
     */
    if (g_regex_match_simple(DEFAULT_INTERPRETERS_REGEX, basename, G_REGEX_EXTENDED, /*MatchFlags*/0) ||
        g_list_find_custom(settings_Interpreters, basename, (GCompareFunc)g_strcmp0))
    {
        struct pkg_envra *script_pkg = get_script_name(cmdline, &executable, chroot);
        /* executable may have changed, check it again */
        if (is_path_blacklisted(executable))
        {
            log_warning("Blacklisted executable '%s'", executable);
            goto ret; /* return 1 (failure) */
        }
        if (!script_pkg)
        {
            /* Script name is not absolute, or it doesn't
             * belong to any installed package.
             */
            if (!settings_bProcessUnpackaged)
            {
                log_warning("Interpreter crashed, but no packaged script detected: '%s'", cmdline);
                goto ret; /* return 1 (failure) */
            }

            /* Unpackaged script, but the settings says we want to keep it.
             * BZ plugin wont allow to report this anyway, because component
             * is missing, so there is no reason to mark it as not_reportable.
             * Someone might want to use abrt to report it using ftp.
             */
            goto ret0;
        }

        free_pkg_envra(pkg_name);
        pkg_name = script_pkg;
    }

skip_interpreter:
    package_short_name = xasprintf("%s", pkg_name->p_name);
    log_info("Package:'%s' short:'%s'", pkg_name->p_nvr, package_short_name);

    /* The check for kernel_oops is there because it could be an unexpected
     * behaviour. If one wants to ignore kernel oops, she/he should disable
     * the corresponding services. */
    if (!kernel_oops && g_list_find_custom(settings_setBlackListedPkgs, package_short_name, (GCompareFunc)g_strcmp0))
    {
        log_warning("Blacklisted package '%s'", package_short_name);
        goto ret; /* return 1 (failure) */
    }

    fingerprint = rpm_get_fingerprint(package_short_name);
    if (!(fingerprint != NULL && rpm_fingerprint_is_imported(fingerprint))
         && settings_bOpenGPGCheck)
    {
        log_warning("Package '%s' isn't signed with proper key", package_short_name);
        goto ret; /* return 1 (failure) */
        /* We used to also check the integrity of the executable here:
         *  if (!CheckHash(package_short_name.c_str(), executable)) BOOM();
         * Checking the MD5 sum requires to run prelink to "un-prelink" the
         * binaries - this is considered potential security risk so we don't
         * do it now, until we find some non-intrusive way.
         */
    }

    component = rpm_get_component(executable, chroot);

    dd = dd_opendir(dump_dir_name, /*flags:*/ 0);
    if (!dd)
        goto ret; /* return 1 (failure) */

    if (pkg_name)
    {
        dd_save_text(dd, FILENAME_PACKAGE, pkg_name->p_nvr);
        dd_save_text(dd, FILENAME_PKG_EPOCH, pkg_name->p_epoch);
        dd_save_text(dd, FILENAME_PKG_NAME, pkg_name->p_name);
        dd_save_text(dd, FILENAME_PKG_VERSION, pkg_name->p_version);
        dd_save_text(dd, FILENAME_PKG_RELEASE, pkg_name->p_release);
        dd_save_text(dd, FILENAME_PKG_ARCH, pkg_name->p_arch);
        dd_save_text(dd, FILENAME_PKG_VENDOR, pkg_name->p_vendor);

        if (fingerprint)
        {
            /* 16 character + 3 spaces + 1 '\0' + 2 Bytes for errors :) */
            char key_fingerprint[22] = {0};

            /* The condition is just a defense against errors */
            for (size_t i = 0, j = 0; j < sizeof(key_fingerprint) - 2; )
            {
                key_fingerprint[j++] = toupper(fingerprint[i++]);

                if (fingerprint[i] == '\0')
                    break;

                if (!(i & (0x3)))
                    key_fingerprint[j++] = ' ';
            }

            dd_save_text(dd, FILENAME_PKG_FINGERPRINT, key_fingerprint);
        }
    }

    if (component)
        dd_save_text(dd, FILENAME_COMPONENT, component);

 ret0:
    error = 0;
 ret:
    if (dd)
        dd_close(dd);

    free(cmdline);
    free(executable);
    free(rootdir);
    free(package_short_name);
    free_pkg_envra(pkg_name);
    free(component);
    free(fingerprint);

    return error;
}

int main(int argc, char **argv)
{
    /* I18n */
    setlocale(LC_ALL, "");
#if ENABLE_NLS
    bindtextdomain(PACKAGE, LOCALEDIR);
    textdomain(PACKAGE);
#endif

    abrt_init(argv);

    const char *dump_dir_name = ".";
    const char *conf_filename = NULL;
    const char *chroot = NULL;

    /* Can't keep these strings/structs static: _() doesn't support that */
    const char *program_usage_string = _(
        "& [-v] [-c CONFFILE] [-r CHROOT] -d DIR\n"
        "\n"
        "Query package database and save package and component name"
    );
    enum {
        OPT_v = 1 << 0,
        OPT_d = 1 << 1,
        OPT_c = 1 << 2,
        OPT_r = 1 << 2,
    };
    /* Keep enum above and order of options below in sync! */
    struct options program_options[] = {
        OPT__VERBOSE(&g_verbose),
        OPT_STRING('d', NULL, &dump_dir_name, "DIR"     , _("Problem directory")),
        OPT_STRING('c', NULL, &conf_filename, "CONFFILE", _("Configuration file")),
        OPT_STRING('r', "chroot", &chroot,    "CHROOT"  , _("Use this directory as RPM root")),
        OPT_END()
    };
    /*unsigned opts =*/ parse_opts(argc, argv, program_options, program_usage_string);

    export_abrt_envvars(0);

    log_notice("Loading settings");
    if (load_conf(conf_filename) != 0)
        return 1; /* syntax error (logged already by load_conf) */

    log_notice("Initializing rpm library");
    rpm_init();

    GList *li;
    for (li = settings_setOpenGPGPublicKeys; li != NULL; li = g_list_next(li))
    {
        log_notice("Loading GPG key '%s'", (char*)li->data);
        rpm_load_gpgkey((char*)li->data);
    }

    int r = SavePackageDescriptionToDebugDump(dump_dir_name, chroot);

    /* Close RPM database */
    rpm_destroy();

    return r;
}
