/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2008 Novell, Inc.
 * Copyright (C) 2008 - 2015 Red Hat, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nms-keyfile-writer.h"

#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

#include "libnm-core-intern/nm-keyfile-internal.h"

#include "nms-keyfile-utils.h"
#include "nms-keyfile-reader.h"

#include "libnm-glib-aux/nm-io-utils.h"

/*****************************************************************************/

typedef struct {
    const char *keyfile_dir;
} WriteInfo;

static void
cert_writer(NMConnection                     *connection,
            GKeyFile                         *file,
            NMSetting8021x                   *setting,
            const NMSetting8021xSchemeVtable *vtable,
            WriteInfo                        *info,
            GError                          **error)
{
    const char            *setting_name = nm_setting_get_name(NM_SETTING(setting));
    NMSetting8021xCKScheme scheme;
    NMSetting8021xCKFormat format;
    const char            *path = NULL, *ext = "pem";

    scheme = vtable->scheme_func(setting);
    if (scheme == NM_SETTING_802_1X_CK_SCHEME_PATH) {
        char       *tmp           = NULL;
        const char *accepted_path = NULL;

        path = vtable->path_func(setting);
        g_assert(path);

        if (g_str_has_prefix(path, info->keyfile_dir)) {
            const char *p = path + strlen(info->keyfile_dir);

            /* If the path is rooted in the keyfile directory, just use a
             * relative path instead of an absolute one.
             */
            if (*p == '/') {
                while (*p == '/')
                    p++;
                if (p[0]) {
                    /* If @p looks like an integer list, the following detection will fail too and
                     * we will file:// qualify the path below. We thus avoid writing a path string
                     * that would be interpreted as legacy binary format by reader. */
                    tmp = nm_keyfile_detect_unqualified_path_scheme(info->keyfile_dir,
                                                                    p,
                                                                    -1,
                                                                    FALSE,
                                                                    NULL);
                    if (tmp) {
                        nm_clear_g_free(&tmp);
                        accepted_path = p;
                    }
                }
            }
        }
        if (!accepted_path) {
            /* What we are about to write, must also be understood by the reader.
             * Otherwise, add a file:// prefix */
            tmp =
                nm_keyfile_detect_unqualified_path_scheme(info->keyfile_dir, path, -1, FALSE, NULL);
            if (tmp) {
                nm_clear_g_free(&tmp);
                accepted_path = path;
            }
        }

        if (!accepted_path)
            accepted_path = tmp = g_strconcat(NM_KEYFILE_CERT_SCHEME_PREFIX_PATH, path, NULL);
        nm_keyfile_plugin_kf_set_string(file, setting_name, vtable->setting_key, accepted_path);
        g_free(tmp);
    } else if (scheme == NM_SETTING_802_1X_CK_SCHEME_PKCS11) {
        nm_keyfile_plugin_kf_set_string(file,
                                        setting_name,
                                        vtable->setting_key,
                                        vtable->uri_func(setting));
    } else if (scheme == NM_SETTING_802_1X_CK_SCHEME_BLOB) {
        GBytes       *blob;
        const guint8 *blob_data;
        gsize         blob_len;
        gboolean      success;
        GError       *local = NULL;
        char         *new_path;

        blob = vtable->blob_func(setting);
        g_assert(blob);
        blob_data = g_bytes_get_data(blob, &blob_len);

        if (vtable->format_func) {
            /* Get the extension for a private key */
            format = vtable->format_func(setting);
            if (format == NM_SETTING_802_1X_CK_FORMAT_PKCS12)
                ext = "p12";
        } else {
            /* DER or PEM format certificate? */
            if (blob_len > 2 && blob_data[0] == 0x30 && blob_data[1] == 0x82)
                ext = "der";
        }

        /* Write the raw data out to the standard file so that we can use paths
         * from now on instead of pushing around the certificate data.
         */
        new_path = g_strdup_printf("%s/%s-%s.%s",
                                   info->keyfile_dir,
                                   nm_connection_get_uuid(connection),
                                   vtable->file_suffix,
                                   ext);

        /* FIXME(keyfile-parse-in-memory): writer must not access/write to the file system before
         * being sure that the entire profile can be written and all circumstances are good to
         * proceed. That means, while writing we must only collect the blobs in-memory, and write
         * them all in the end together (or not at all). */
        success = nm_utils_file_set_contents(new_path,
                                             (const char *) blob_data,
                                             blob_len,
                                             0600,
                                             NULL,
                                             NULL,
                                             &local);
        if (success) {
            /* Write the path value to the keyfile.
             * We know, that basename(new_path) starts with a UUID, hence no conflict with "data:;base64,"  */
            nm_keyfile_plugin_kf_set_string(file,
                                            setting_name,
                                            vtable->setting_key,
                                            strrchr(new_path, '/') + 1);
        } else {
            nm_log_warn(LOGD_SETTINGS,
                        "keyfile: %s.%s: failed to write certificate to file %s: %s",
                        setting_name,
                        vtable->setting_key,
                        new_path,
                        local->message);
            g_error_free(local);
        }
        g_free(new_path);
    } else {
        /* scheme_func() returns UNKNOWN in all other cases. The only valid case
         * where a scheme is allowed to be UNKNOWN, is unsetting the value. In this
         * case, we don't expect the writer to be called, because the default value
         * will not be serialized.
         * The only other reason for the scheme to be UNKNOWN is an invalid cert.
         * But our connection verifies, so that cannot happen either. */
        g_return_if_reached();
    }
}

static gboolean
_handler_write(NMConnection         *connection,
               GKeyFile             *keyfile,
               NMKeyfileHandlerType  type,
               NMKeyfileHandlerData *type_data,
               void                 *user_data)
{
    if (type == NM_KEYFILE_HANDLER_TYPE_WRITE_CERT) {
        cert_writer(connection,
                    keyfile,
                    NM_SETTING_802_1X(type_data->cur_setting),
                    type_data->write_cert.vtable,
                    user_data,
                    type_data->p_error);
        return TRUE;
    }
    return FALSE;
}

static gboolean
_internal_write_connection(NMConnection                   *connection,
                           gboolean                        is_nm_generated,
                           gboolean                        is_volatile,
                           gboolean                        is_external,
                           const char                     *shadowed_storage,
                           gboolean                        shadowed_owned,
                           const char                     *keyfile_dir,
                           const char                     *profile_dir,
                           gboolean                        with_extension,
                           uid_t                           owner_uid,
                           pid_t                           owner_grp,
                           const char                     *existing_path,
                           gboolean                        existing_path_read_only,
                           NMTernary                       force_rename,
                           NMSKeyfileWriterAllowFilenameCb allow_filename_cb,
                           gpointer                        allow_filename_user_data,
                           char                          **out_path,
                           NMConnection                  **out_reread,
                           gboolean                       *out_reread_same,
                           GError                        **error)
{
    nm_auto_unref_keyfile GKeyFile *kf_file        = NULL;
    gs_free char                   *kf_content_buf = NULL;
    gsize                           kf_content_len;
    gs_free char                   *path = NULL;
    const char                     *id;
    WriteInfo                       info      = {0};
    gs_free_error GError           *local_err = NULL;
    int                             errsv;
    gboolean                        rename;
    gboolean                        rename_follow;
    int                             i_path;
    gs_unref_object NMConnection   *reread      = NULL;
    gboolean                        reread_same = FALSE;

    g_return_val_if_fail(!out_path || !*out_path, FALSE);
    g_return_val_if_fail(keyfile_dir && keyfile_dir[0] == '/', FALSE);

    nm_assert(_nm_connection_verify(connection, NULL) == NM_SETTING_VERIFY_SUCCESS);

    nm_assert(!shadowed_owned || shadowed_storage);

    rename = existing_path_read_only
             || (existing_path && !nm_utils_file_is_in_path(existing_path, keyfile_dir))
             || force_rename == NM_TERNARY_TRUE;

    /* Follow the connection.id upon change. */
    rename_follow = !rename && existing_path && force_rename == NM_TERNARY_DEFAULT;

    id = nm_connection_get_id(connection);
    nm_assert(id && *id);

    info.keyfile_dir = keyfile_dir;

    kf_file =
        nm_keyfile_write(connection, NM_KEYFILE_HANDLER_FLAGS_NONE, _handler_write, &info, error);
    if (!kf_file)
        return FALSE;

    if (is_nm_generated) {
        g_key_file_set_boolean(kf_file,
                               NM_KEYFILE_GROUP_NMMETA,
                               NM_KEYFILE_KEY_NMMETA_NM_GENERATED,
                               TRUE);
    }

    if (is_volatile) {
        g_key_file_set_boolean(kf_file,
                               NM_KEYFILE_GROUP_NMMETA,
                               NM_KEYFILE_KEY_NMMETA_VOLATILE,
                               TRUE);
    }

    if (is_external) {
        g_key_file_set_boolean(kf_file,
                               NM_KEYFILE_GROUP_NMMETA,
                               NM_KEYFILE_KEY_NMMETA_EXTERNAL,
                               TRUE);
    }

    if (shadowed_storage) {
        g_key_file_set_string(kf_file,
                              NM_KEYFILE_GROUP_NMMETA,
                              NM_KEYFILE_KEY_NMMETA_SHADOWED_STORAGE,
                              shadowed_storage);
    }

    if (shadowed_owned) {
        g_key_file_set_boolean(kf_file,
                               NM_KEYFILE_GROUP_NMMETA,
                               NM_KEYFILE_KEY_NMMETA_SHADOWED_OWNED,
                               TRUE);
    }

    kf_content_buf = g_key_file_to_data(kf_file, &kf_content_len, error);
    if (!kf_content_buf)
        return FALSE;

    if (!g_file_test(keyfile_dir, G_FILE_TEST_IS_DIR))
        (void) g_mkdir_with_parents(keyfile_dir, 0755);

    for (i_path = -2; i_path < 10000; i_path++) {
        gs_free char *path_candidate = NULL;
        gboolean      is_existing_path;

        if (i_path == -2) {
            if (!existing_path || rename || rename_follow)
                continue;
            path_candidate = g_strdup(existing_path);
        } else if (i_path == -1) {
            gs_free char *filename_escaped = NULL;

            filename_escaped = nm_keyfile_utils_create_filename(id, with_extension);
            path_candidate   = g_build_filename(keyfile_dir, filename_escaped, NULL);
        } else {
            gs_free char *filename_escaped = NULL;
            gs_free char *filename         = NULL;

            if (i_path == 0)
                filename = g_strdup_printf("%s-%s", id, nm_connection_get_uuid(connection));
            else
                filename =
                    g_strdup_printf("%s-%s-%d", id, nm_connection_get_uuid(connection), i_path);

            filename_escaped = nm_keyfile_utils_create_filename(filename, with_extension);

            path_candidate = g_strdup_printf("%s/%s", keyfile_dir, filename_escaped);
        }

        is_existing_path = existing_path && nm_streq(existing_path, path_candidate);

        if (is_existing_path && rename)
            continue;

        if (allow_filename_cb && !allow_filename_cb(path_candidate, allow_filename_user_data))
            continue;

        if (!is_existing_path) {
            if (g_file_test(path_candidate, G_FILE_TEST_EXISTS))
                continue;
        }

        path = g_steal_pointer(&path_candidate);
        break;
    }

    if (!path) {
        gs_free char *ss = NULL;

        /* this really should not happen, we tried hard to find an unused name... bail out. */
        g_set_error(error,
                    NM_SETTINGS_ERROR,
                    NM_SETTINGS_ERROR_FAILED,
                    "could not find suitable keyfile file name (%s already used)",
                    ss = ({
                        gs_free char *filename_escaped = NULL;

                        filename_escaped = nm_keyfile_utils_create_filename(id, with_extension);
                        g_build_filename(keyfile_dir, filename_escaped, NULL);
                    }));

        return FALSE;
    }

    if (out_reread || out_reread_same) {
        gs_free_error GError *reread_error = NULL;

        reread =
            nms_keyfile_reader_from_keyfile(kf_file, path, NULL, profile_dir, FALSE, &reread_error);

        if (!reread || !nm_connection_normalize(reread, NULL, NULL, &reread_error)) {
            nm_log_err(
                LOGD_SETTINGS,
                "BUG: the profile cannot be stored in keyfile format without becoming unusable: %s",
                reread_error->message);
            g_set_error(error,
                        NM_SETTINGS_ERROR,
                        NM_SETTINGS_ERROR_FAILED,
                        "keyfile writer produces an invalid connection: %s",
                        reread_error->message);
            nm_assert_not_reached();
            return FALSE;
        }

        if (out_reread_same) {
            reread_same =
                !!nm_connection_compare(reread, connection, NM_SETTING_COMPARE_FLAG_EXACT);

            nm_assert(reread_same
                      == nm_connection_compare(connection, reread, NM_SETTING_COMPARE_FLAG_EXACT));
            nm_assert(reread_same == ({
                          gs_unref_hashtable GHashTable *_settings = NULL;

                          (nm_connection_diff(reread,
                                              connection,
                                              NM_SETTING_COMPARE_FLAG_EXACT,
                                              &_settings)
                           && !_settings);
                      }));
        }
    }

    nm_utils_file_set_contents(path, kf_content_buf, kf_content_len, 0600, NULL, NULL, &local_err);
    if (local_err) {
        g_set_error(error,
                    NM_SETTINGS_ERROR,
                    NM_SETTINGS_ERROR_FAILED,
                    "error writing to file '%s': %s",
                    path,
                    local_err->message);
        return FALSE;
    }

    if (chown(path, owner_uid, owner_grp) < 0) {
        errsv = errno;
        g_set_error(error,
                    NM_SETTINGS_ERROR,
                    NM_SETTINGS_ERROR_FAILED,
                    "error chowning '%s': %s (%d)",
                    path,
                    nm_strerror_native(errsv),
                    errsv);
        unlink(path);
        return FALSE;
    }

    /* In case of updating the connection and changing the file path,
     * we need to remove the old one, not to end up with two connections.
     */
    if (existing_path && !existing_path_read_only && !nm_streq(path, existing_path))
        unlink(existing_path);

    NM_SET_OUT(out_reread, g_steal_pointer(&reread));
    NM_SET_OUT(out_reread_same, reread_same);
    NM_SET_OUT(out_path, g_steal_pointer(&path));

    return TRUE;
}

gboolean
nms_keyfile_writer_connection(NMConnection                   *connection,
                              gboolean                        is_nm_generated,
                              gboolean                        is_volatile,
                              gboolean                        is_external,
                              const char                     *shadowed_storage,
                              gboolean                        shadowed_owned,
                              const char                     *keyfile_dir,
                              const char                     *profile_dir,
                              const char                     *existing_path,
                              gboolean                        existing_path_read_only,
                              NMTernary                       force_rename,
                              NMSKeyfileWriterAllowFilenameCb allow_filename_cb,
                              gpointer                        allow_filename_user_data,
                              char                          **out_path,
                              NMConnection                  **out_reread,
                              gboolean                       *out_reread_same,
                              GError                        **error)
{
    return _internal_write_connection(connection,
                                      is_nm_generated,
                                      is_volatile,
                                      is_external,
                                      shadowed_storage,
                                      shadowed_owned,
                                      keyfile_dir,
                                      profile_dir,
                                      TRUE,
                                      nm_utils_get_nm_uid(),
                                      nm_utils_get_nm_gid(),
                                      existing_path,
                                      existing_path_read_only,
                                      force_rename,
                                      allow_filename_cb,
                                      allow_filename_user_data,
                                      out_path,
                                      out_reread,
                                      out_reread_same,
                                      error);
}

gboolean
nmtst_keyfile_writer_test_connection(NMConnection  *connection,
                                     const char    *keyfile_dir,
                                     uid_t          owner_uid,
                                     pid_t          owner_grp,
                                     char         **out_path,
                                     NMConnection **out_reread,
                                     gboolean      *out_reread_same,
                                     GError       **error)
{
    return _internal_write_connection(connection,
                                      FALSE,
                                      FALSE,
                                      FALSE,
                                      NULL,
                                      FALSE,
                                      keyfile_dir,
                                      keyfile_dir,
                                      FALSE,
                                      owner_uid,
                                      owner_grp,
                                      NULL,
                                      FALSE,
                                      FALSE,
                                      NULL,
                                      NULL,
                                      out_path,
                                      out_reread,
                                      out_reread_same,
                                      error);
}
