/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* NetworkManager system settings service - keyfile plugin
 *
 * 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.
 *
 * Copyright (C) 2008 Novell, Inc.
 * Copyright (C) 2008 - 2015 Red Hat, Inc.
 */

#include "nm-default.h"

#include "nms-keyfile-writer.h"

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

#include "nm-keyfile-internal.h"

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

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

typedef struct {
	const char *keyfile_dir;
} WriteInfo;

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

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

		path = cert_data->vtable->path_func (cert_data->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) {
						g_clear_pointer (&tmp, g_free);
						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) {
				g_clear_pointer (&tmp, g_free);
				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, cert_data->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, cert_data->vtable->setting_key,
		                                 cert_data->vtable->uri_func (cert_data->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 = cert_data->vtable->blob_func (cert_data->setting);
		g_assert (blob);
		blob_data = g_bytes_get_data (blob, &blob_len);

		if (cert_data->vtable->format_func) {
			/* Get the extension for a private key */
			format = cert_data->vtable->format_func (cert_data->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),
		                            cert_data->vtable->file_suffix, ext);

		success = nm_utils_file_set_contents (new_path, (const gchar *) blob_data,
		                                      blob_len, 0600, &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, cert_data->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, cert_data->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,
                NMKeyfileWriteType type,
                void *type_data,
                void *user_data,
                GError **error)
{
	if (type == NM_KEYFILE_WRITE_TYPE_CERT) {
		cert_writer (connection, keyfile,
		             (NMKeyfileWriteTypeDataCert *) type_data,
		             (WriteInfo *) user_data, error);
		return TRUE;
	}
	return FALSE;
}

static gboolean
_internal_write_connection (NMConnection *connection,
                            const char *keyfile_dir,
                            uid_t owner_uid,
                            pid_t owner_grp,
                            const char *existing_path,
                            gboolean force_rename,
                            char **out_path,
                            NMConnection **out_reread,
                            gboolean *out_reread_same,
                            GError **error)
{
	gs_unref_keyfile GKeyFile *key_file = NULL;
	gs_free char *data = NULL;
	gsize len;
	gs_free char *path = NULL;
	const char *id;
	WriteInfo info = { 0 };
	GError *local_err = NULL;
	int errsv;

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

	switch (_nm_connection_verify (connection, error)) {
	case NM_SETTING_VERIFY_NORMALIZABLE:
		nm_assert_not_reached ();
		/* fall-through */
	case NM_SETTING_VERIFY_SUCCESS:
		break;
	default:
		g_return_val_if_reached (FALSE);
	}

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

	info.keyfile_dir = keyfile_dir;

	key_file = nm_keyfile_write (connection, _handler_write, &info, error);
	if (!key_file)
		return FALSE;
	data = g_key_file_to_data (key_file, &len, error);
	if (!data)
		return FALSE;

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

	/* If we have existing file path, use it. Else generate one from
	 * connection's ID.
	 */
	if (existing_path != NULL && !force_rename) {
		path = g_strdup (existing_path);
	} else {
		char *filename_escaped = nms_keyfile_utils_escape_filename (id);

		path = g_build_filename (keyfile_dir, filename_escaped, NULL);
		g_free (filename_escaped);
	}

	/* If a file with this path already exists (but isn't the existing path
	 * of the connection) then we need another name.  Multiple connections
	 * can have the same ID (ie if two connections with the same ID are visible
	 * to different users) but of course can't have the same path.  Yeah,
	 * there's a race here, but there's not a lot we can do about it, and
	 * we shouldn't get more than one connection with the same UUID either.
	 */
	if (g_strcmp0 (path, existing_path) != 0 && g_file_test (path, G_FILE_TEST_EXISTS)) {
		guint i;
		gboolean name_found = FALSE;

		/* A keyfile with this connection's ID already exists. Pick another name. */
		for (i = 0; i < 100; i++) {
			char *filename, *filename_escaped;

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

			filename_escaped = nms_keyfile_utils_escape_filename (filename);

			g_free (path);
			path = g_strdup_printf ("%s/%s", keyfile_dir, filename_escaped);
			g_free (filename);
			g_free (filename_escaped);
			if (g_strcmp0 (path, existing_path) == 0 || !g_file_test (path, G_FILE_TEST_EXISTS)) {
				name_found = TRUE;
				break;
			}
		}
		if (!name_found) {
			if (existing_path == 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)", path);
				return FALSE;
			}
			/* Both our preferred path based on connection id and id-uuid are taken.
			 * Fallback to @existing_path */
			g_free (path);
			path = g_strdup (existing_path);
		}
	}

	/* 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 != NULL && strcmp (path, existing_path) != 0)
		unlink (existing_path);

	nm_utils_file_set_contents (path, data, len, 0600, &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);
		g_error_free (local_err);
		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, g_strerror (errsv), errsv);
		unlink (path);
		return FALSE;
	}

	if (out_path && g_strcmp0 (existing_path, path)) {
		*out_path = path;  /* pass path out to caller */
		path = NULL;
	}

	if (out_reread || out_reread_same)
	{
		gs_unref_object NMConnection *reread = NULL;
		gboolean reread_same = FALSE;

		reread = nms_keyfile_reader_from_keyfile (key_file, path, FALSE, NULL);

		nm_assert (NM_IS_CONNECTION (reread));

		if (   reread
		    && !nm_connection_normalize (reread, NULL, NULL, NULL)) {
			nm_assert_not_reached ();
			g_clear_object (&reread);
		}

		if (reread && 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_SET_OUT (out_reread, g_steal_pointer (&reread));
		NM_SET_OUT (out_reread_same, reread_same);
	}

	return TRUE;
}

gboolean
nms_keyfile_writer_connection (NMConnection *connection,
                               const char *existing_path,
                               gboolean force_rename,
                               char **out_path,
                               NMConnection **out_reread,
                               gboolean *out_reread_same,
                               GError **error)
{
	return _internal_write_connection (connection,
	                                   nms_keyfile_utils_get_path (),
	                                   0, 0,
	                                   existing_path,
	                                   force_rename,
	                                   out_path,
	                                   out_reread,
	                                   out_reread_same,
	                                   error);
}

gboolean
nms_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,
	                                   keyfile_dir,
	                                   owner_uid, owner_grp,
	                                   NULL,
	                                   FALSE,
	                                   out_path,
	                                   out_reread,
	                                   out_reread_same,
	                                   error);
}

