/* -*- 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 - 2009 Novell, Inc.
 * Copyright (C) 2008 - 2011 Red Hat, Inc.
 */

#include <errno.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <dbus/dbus-glib.h>
#include <nm-setting.h>
#include <nm-setting-ip4-config.h>
#include <nm-setting-ip6-config.h>
#include <nm-setting-vpn.h>
#include <nm-setting-connection.h>
#include <nm-setting-wired.h>
#include <nm-setting-wireless.h>
#include <nm-setting-bluetooth.h>
#include <nm-setting-8021x.h>
#include <arpa/inet.h>
#include <netinet/ether.h>
#include <string.h>
#include <ctype.h>

#include "nm-dbus-glib-types.h"
#include "nm-system-config-interface.h"
#include "reader.h"
#include "common.h"

static gboolean
read_array_of_uint (GKeyFile *file,
                    NMSetting *setting,
                    const char *key)
{
	GArray *array = NULL;
	gsize length;
	int i;
	gint *tmp;

	tmp = g_key_file_get_integer_list (file, nm_setting_get_name (setting), key, &length, NULL);
	array = g_array_sized_new (FALSE, FALSE, sizeof (guint32), length);
	for (i = 0; i < length; i++)
		g_array_append_val (array, tmp[i]);

	if (array) {
		g_object_set (setting, key, array, NULL);
		g_array_free (array, TRUE);
	}

	return TRUE;
}

static gboolean
get_one_int (const char *str, guint32 max_val, const char *key_name, guint32 *out)
{
	long tmp;

	errno = 0;
	tmp = strtol (str, NULL, 10);
	if (errno || (tmp < 0) || (tmp > max_val)) {
		g_warning ("%s: ignoring invalid IP %s item '%s'", __func__, key_name, str);
		return FALSE;
	}

	*out = (guint32) tmp;
	return TRUE;
}

static void
free_one_ip4_address (gpointer data, gpointer user_data)
{
	g_array_free ((GArray *) data, TRUE);
}

static GPtrArray *
read_ip4_addresses (GKeyFile *file,
			    const char *setting_name,
			    const char *key)
{
	GPtrArray *addresses;
	int i = 0;

	addresses = g_ptr_array_sized_new (3);

	/* Look for individual addresses */
	while (i++ < 1000) {
		gchar **tmp, **iter;
		char *key_name;
		gsize length = 0;
		int ret;
		GArray *address;
		guint32 empty = 0;
		int j;

		key_name = g_strdup_printf ("%s%d", key, i);
		tmp = g_key_file_get_string_list (file, setting_name, key_name, &length, NULL);
		g_free (key_name);

		if (!tmp || !length)
			break; /* all done */

		if ((length < 2) || (length > 3)) {
			g_warning ("%s: ignoring invalid IPv4 address item '%s'", __func__, key_name);
			goto next;
		}

		/* convert the string array into IP addresses */
		address = g_array_sized_new (FALSE, TRUE, sizeof (guint32), 3);
		for (iter = tmp, j = 0; *iter; iter++, j++) {
			struct in_addr addr;

			if (j == 1) {
				guint32 prefix = 0;

				/* prefix */
				if (!get_one_int (*iter, 32, key_name, &prefix)) {
					g_array_free (address, TRUE);
					goto next;
				}

				g_array_append_val (address, prefix);
			} else {
				/* address and gateway */
				ret = inet_pton (AF_INET, *iter, &addr);
				if (ret <= 0) {
					g_warning ("%s: ignoring invalid IPv4 %s element '%s'", __func__, key_name, *iter);
					g_array_free (address, TRUE);
					goto next;
				}
				g_array_append_val (address, addr.s_addr);
			}
		}

		/* fill in blank gateway if not specified */
		if (address->len == 2)
			g_array_append_val (address, empty);

		g_ptr_array_add (addresses, address);

next:
		g_strfreev (tmp);
	}

	if (addresses->len < 1) {
		g_ptr_array_free (addresses, TRUE);
		addresses = NULL;
	}

	return addresses;
}

static void
ip4_addr_parser (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path)
{
	GPtrArray *addresses;
	const char *setting_name = nm_setting_get_name (setting);

	addresses = read_ip4_addresses (keyfile, setting_name, key);

	/* Work around for previous syntax */
	if (!addresses && !strcmp (key, NM_SETTING_IP4_CONFIG_ADDRESSES))
		addresses = read_ip4_addresses (keyfile, setting_name, "address");

	if (addresses) {
		g_object_set (setting, key, addresses, NULL);
		g_ptr_array_foreach (addresses, free_one_ip4_address, NULL);
		g_ptr_array_free (addresses, TRUE);
	}
}

static void
free_one_ip4_route (gpointer data, gpointer user_data)
{
	g_array_free ((GArray *) data, TRUE);
}

static GPtrArray *
read_ip4_routes (GKeyFile *file,
			 const char *setting_name,
			 const char *key)
{
	GPtrArray *routes;
	int i = 0;

	routes = g_ptr_array_sized_new (3);

	/* Look for individual routes */
	while (i++ < 1000) {
		gchar **tmp, **iter;
		char *key_name;
		gsize length = 0;
		int ret;
		GArray *route;
		int j;

		key_name = g_strdup_printf ("%s%d", key, i);
		tmp = g_key_file_get_string_list (file, setting_name, key_name, &length, NULL);
		g_free (key_name);

		if (!tmp || !length)
			break; /* all done */

		if (length != 4) {
			g_warning ("%s: ignoring invalid IPv4 route item '%s'", __func__, key_name);
			goto next;
		}

		/* convert the string array into IP addresses */
		route = g_array_sized_new (FALSE, TRUE, sizeof (guint32), 4);
		for (iter = tmp, j = 0; *iter; iter++, j++) {
			struct in_addr addr;

			if (j == 1) {
				guint32 prefix = 0;

				/* prefix */
				if (!get_one_int (*iter, 32, key_name, &prefix)) {
					g_array_free (route, TRUE);
					goto next;
				}

				g_array_append_val (route, prefix);
			} else if (j == 3) {
				guint32 metric = 0;

				/* metric */
				if (!get_one_int (*iter, G_MAXUINT32, key_name, &metric)) {
					g_array_free (route, TRUE);
					goto next;
				}

				g_array_append_val (route, metric);
			} else {
				/* address and next hop */
				ret = inet_pton (AF_INET, *iter, &addr);
				if (ret <= 0) {
					g_warning ("%s: ignoring invalid IPv4 %s element '%s'", __func__, key_name, *iter);
					g_array_free (route, TRUE);
					goto next;
				}
				g_array_append_val (route, addr.s_addr);
			}
		}
		g_ptr_array_add (routes, route);

next:
		g_strfreev (tmp);
	}

	if (routes->len < 1) {
		g_ptr_array_free (routes, TRUE);
		routes = NULL;
	}

	return routes;
}

static void
ip4_route_parser (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path)
{
	GPtrArray *routes;
	const char *setting_name = nm_setting_get_name (setting);

	routes = read_ip4_routes (keyfile, setting_name, key);
	if (routes) {
		g_object_set (setting, key, routes, NULL);
		g_ptr_array_foreach (routes, free_one_ip4_route, NULL);
		g_ptr_array_free (routes, TRUE);
	}
}

static void
ip4_dns_parser (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path)
{
	const char *setting_name = nm_setting_get_name (setting);
	GArray *array = NULL;
	gsize length;
	char **list, **iter;
	int ret;

	list = g_key_file_get_string_list (keyfile, setting_name, key, &length, NULL);
	if (!list || !g_strv_length (list))
		return;

	array = g_array_sized_new (FALSE, FALSE, sizeof (guint32), length);
	for (iter = list; *iter; iter++) {
		struct in_addr addr;

		ret = inet_pton (AF_INET, *iter, &addr);
		if (ret <= 0) {
			g_warning ("%s: ignoring invalid DNS server address '%s'", __func__, *iter);
			continue;
		}

		g_array_append_val (array, addr.s_addr);
	}
	g_strfreev (list);

	if (array) {
		g_object_set (setting, key, array, NULL);
		g_array_free (array, TRUE);
	}
}

static void
free_one_ip6_address (gpointer data, gpointer user_data)
{
	g_value_array_free ((GValueArray *) data);
}

static char *
split_prefix (char *addr)
{
	char *slash;

	g_return_val_if_fail (addr != NULL, NULL);

	/* Find the prefix and split the string */
	slash = strchr (addr, '/');
	if (slash && slash > addr) {
		slash++;
		*(slash - 1) = '\0';
	}

	return slash;
}

static char *
split_gw (char *str)
{
	char *comma;

	g_return_val_if_fail (str != NULL, NULL);

	/* Find the prefix and split the string */
	comma = strchr (str, ',');
	if (comma && comma > str) {
		comma++;
		*(comma - 1) = '\0';
		return comma;
	}
	return NULL;
}

static GPtrArray *
read_ip6_addresses (GKeyFile *file,
                    const char *setting_name,
                    const char *key)
{
	GPtrArray *addresses;
	struct in6_addr addr, gw;
	guint32 prefix;
	int i = 0;

	addresses = g_ptr_array_sized_new (3);

	/* Look for individual addresses */
	while (i++ < 1000) {
		char *tmp, *key_name, *str_prefix, *str_gw;
		int ret;
		GValueArray *values;
		GByteArray *address;
		GByteArray *gateway;
		GValue value = { 0 };

		key_name = g_strdup_printf ("%s%d", key, i);
		tmp = g_key_file_get_string (file, setting_name, key_name, NULL);
		g_free (key_name);

		if (!tmp)
			break; /* all done */

		/* convert the string array into IPv6 addresses */
		values = g_value_array_new (2); /* NMIP6Address has 2 items */

		/* Split the address and prefix */
		str_prefix = split_prefix (tmp);

		/* address */
		ret = inet_pton (AF_INET6, tmp, &addr);
		if (ret <= 0) {
			g_warning ("%s: ignoring invalid IPv6 %s element '%s'", __func__, key_name, tmp);
			g_value_array_free (values);
			goto next;
		}

		address = g_byte_array_new ();
		g_byte_array_append (address, (guint8 *) addr.s6_addr, 16);
		g_value_init (&value, DBUS_TYPE_G_UCHAR_ARRAY);
		g_value_take_boxed (&value, address);
		g_value_array_append (values, &value);
		g_value_unset (&value);

		/* prefix */
		prefix = 0;
		if (str_prefix) {
			if (!get_one_int (str_prefix, 128, key_name, &prefix)) {
				g_value_array_free (values);
				goto next;
			}
		} else {
			/* Missing prefix defaults to /64 */
			prefix = 64;
		}

		g_value_init (&value, G_TYPE_UINT);
		g_value_set_uint (&value, prefix);
		g_value_array_append (values, &value);
		g_value_unset (&value);

		/* Gateway (optional) */
		str_gw = split_gw (str_prefix);
		if (str_gw) {
			ret = inet_pton (AF_INET6, str_gw, &gw);
			if (ret <= 0) {
				g_warning ("%s: ignoring invalid IPv6 %s gateway '%s'", __func__, key_name, tmp);
				g_value_array_free (values);
				goto next;
			}

			if (!IN6_IS_ADDR_UNSPECIFIED (&gw)) {
				gateway = g_byte_array_new ();
				g_byte_array_append (gateway, (guint8 *) gw.s6_addr, 16);
				g_value_init (&value, DBUS_TYPE_G_UCHAR_ARRAY);
				g_value_take_boxed (&value, gateway);
				g_value_array_append (values, &value);
				g_value_unset (&value);
			}
		}

		g_ptr_array_add (addresses, values);

next:
		g_free (tmp);
	}

	if (addresses->len < 1) {
		g_ptr_array_free (addresses, TRUE);
		addresses = NULL;
	}

	return addresses;
}

static void
ip6_addr_parser (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path)
{
	GPtrArray *addresses;
	const char *setting_name = nm_setting_get_name (setting);

	addresses = read_ip6_addresses (keyfile, setting_name, key);
	if (addresses) {
		g_object_set (setting, key, addresses, NULL);
		g_ptr_array_foreach (addresses, free_one_ip6_address, NULL);
		g_ptr_array_free (addresses, TRUE);
	}
}

static void
free_one_ip6_route (gpointer data, gpointer user_data)
{
	g_value_array_free ((GValueArray *) data);
}

static GPtrArray *
read_ip6_routes (GKeyFile *file,
                 const char *setting_name,
                 const char *key)
{
	GPtrArray *routes;
	struct in6_addr addr;
	guint32 prefix, metric;
	int i = 0;

	routes = g_ptr_array_sized_new (3);

	/* Look for individual routes */
	while (i++ < 1000) {
		gchar **tmp;
		char *key_name, *str_prefix;
		gsize length = 0;
		int ret;
		GValueArray *values;
		GByteArray *address;
		GValue value = { 0 };

		key_name = g_strdup_printf ("%s%d", key, i);
		tmp = g_key_file_get_string_list (file, setting_name, key_name, &length, NULL);
		g_free (key_name);

		if (!tmp || !length)
			break; /* all done */

		if (length != 3) {
			g_warning ("%s: ignoring invalid IPv6 address item '%s'", __func__, key_name);
			goto next;
		}

		/* convert the string array into IPv6 routes */
		values = g_value_array_new (4); /* NMIP6Route has 4 items */

		/* Split the route and prefix */
		str_prefix = split_prefix (tmp[0]);

		/* destination address */
		ret = inet_pton (AF_INET6, tmp[0], &addr);
		if (ret <= 0) {
			g_warning ("%s: ignoring invalid IPv6 %s element '%s'", __func__, key_name, tmp[0]);
			g_value_array_free (values);
			goto next;
		}
		address = g_byte_array_new ();
		g_byte_array_append (address, (guint8 *) addr.s6_addr, 16);
		g_value_init (&value, DBUS_TYPE_G_UCHAR_ARRAY);
		g_value_take_boxed (&value, address);
		g_value_array_append (values, &value);
		g_value_unset (&value);

		/* prefix */
		prefix = 0;
		if (str_prefix) {
			if (!get_one_int (str_prefix, 128, key_name, &prefix)) {
				g_value_array_free (values);
				goto next;
			}
		} else {
			/* default to 64 if unspecified */
			prefix = 64;
		}
		g_value_init (&value, G_TYPE_UINT);
		g_value_set_uint (&value, prefix);
		g_value_array_append (values, &value);
		g_value_unset (&value);

		/* next hop address */
		ret = inet_pton (AF_INET6, tmp[1], &addr);
		if (ret <= 0) {
			g_warning ("%s: ignoring invalid IPv6 %s element '%s'", __func__, key_name, tmp[1]);
			g_value_array_free (values);
			goto next;
		}
		address = g_byte_array_new ();
		g_byte_array_append (address, (guint8 *) addr.s6_addr, 16);
		g_value_init (&value, DBUS_TYPE_G_UCHAR_ARRAY);
		g_value_take_boxed (&value, address);
		g_value_array_append (values, &value);
		g_value_unset (&value);

		/* metric */
		metric = 0;
		if (!get_one_int (tmp[2], G_MAXUINT32, key_name, &metric)) {
			g_value_array_free (values);
			goto next;
		}
		g_value_init (&value, G_TYPE_UINT);
		g_value_set_uint (&value, metric);
		g_value_array_append (values, &value);
		g_value_unset (&value);

		g_ptr_array_add (routes, values);

next:
		g_strfreev (tmp);
	}

	if (routes->len < 1) {
		g_ptr_array_free (routes, TRUE);
		routes = NULL;
	}

	return routes;
}

static void
ip6_route_parser (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path)
{
	GPtrArray *routes;
	const char *setting_name = nm_setting_get_name (setting);

	routes = read_ip6_routes (keyfile, setting_name, key);

	if (routes) {
		g_object_set (setting, key, routes, NULL);
		g_ptr_array_foreach (routes, free_one_ip6_route, NULL);
		g_ptr_array_free (routes, TRUE);
	}
}

static void
free_one_ip6_dns (gpointer data, gpointer user_data)
{
	g_byte_array_free ((GByteArray *) data, TRUE);
}

static void
ip6_dns_parser (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path)
{
	const char *setting_name = nm_setting_get_name (setting);
	GPtrArray *array = NULL;
	gsize length;
	char **list, **iter;
	int ret;

	list = g_key_file_get_string_list (keyfile, setting_name, key, &length, NULL);
	if (!list || !g_strv_length (list))
		return;

	array = g_ptr_array_sized_new (length);
	for (iter = list; *iter; iter++) {
		GByteArray *byte_array;
		struct in6_addr addr;

		ret = inet_pton (AF_INET6, *iter, &addr);
		if (ret <= 0) {
			g_warning ("%s: ignoring invalid DNS server IPv6 address '%s'", __func__, *iter);
			continue;
		}
		byte_array = g_byte_array_new ();
		g_byte_array_append (byte_array, (guint8 *) addr.s6_addr, 16);

		g_ptr_array_add (array, byte_array);
	}
	g_strfreev (list);

	if (array) {
		g_object_set (setting, key, array, NULL);
		g_ptr_array_foreach (array, free_one_ip6_dns, NULL);
		g_ptr_array_free (array, TRUE);
	}
}

static void
mac_address_parser (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path)
{
	const char *setting_name = nm_setting_get_name (setting);
	struct ether_addr *eth;
	char *tmp_string = NULL, *p;
	gint *tmp_list;
	GByteArray *array = NULL;
	gsize length;
	int i;

	p = tmp_string = g_key_file_get_string (keyfile, setting_name, key, NULL);
	if (tmp_string) {
		/* Look for enough ':' characters to signify a MAC address */
		i = 0;
		while (*p) {
			if (*p == ':')
				i++;
			p++;
		}
		if (i == 5) {
			/* parse as a MAC address */
			eth = ether_aton (tmp_string);
			if (eth) {
				g_free (tmp_string);
				array = g_byte_array_sized_new (ETH_ALEN);
				g_byte_array_append (array, eth->ether_addr_octet, ETH_ALEN);
				goto done;
			}
		}
	}
	g_free (tmp_string);

	/* Old format; list of ints */
	tmp_list = g_key_file_get_integer_list (keyfile, setting_name, key, &length, NULL);
	array = g_byte_array_sized_new (length);
	for (i = 0; i < length; i++) {
		int val = tmp_list[i];
		unsigned char v = (unsigned char) (val & 0xFF);

		if (val < 0 || val > 255) {
			g_warning ("%s: %s / %s ignoring invalid byte element '%d' (not "
			           " between 0 and 255 inclusive)", __func__, setting_name,
			           key, val);
		} else
			g_byte_array_append (array, (const unsigned char *) &v, sizeof (v));
	}
	g_free (tmp_list);

done:
	if (array->len == ETH_ALEN) {
		g_object_set (setting, key, array, NULL);
	} else {
		g_warning ("%s: ignoring invalid MAC address for %s / %s",
		           __func__, setting_name, key);
	}
	g_byte_array_free (array, TRUE);
}

static void
read_hash_of_string (GKeyFile *file, NMSetting *setting, const char *key)
{
	char **keys, **iter;
	char *value;
	const char *setting_name = nm_setting_get_name (setting);

	keys = g_key_file_get_keys (file, setting_name, NULL, NULL);
	if (!keys || !*keys)
		return;

	for (iter = keys; *iter; iter++) {
		value = g_key_file_get_string (file, setting_name, *iter, NULL);
		if (!value)
			continue;

		if (NM_IS_SETTING_VPN (setting)) {
			if (strcmp (*iter, NM_SETTING_VPN_SERVICE_TYPE))
				nm_setting_vpn_add_data_item (NM_SETTING_VPN (setting), *iter, value);
		}
		g_free (value);
	}
	g_strfreev (keys);
}

static GByteArray *
get_uchar_array (GKeyFile *keyfile,
                 const char *setting_name,
                 const char *key)
{
	GByteArray *array = NULL;
	char *tmp_string;
	gint *tmp_list;
	gsize length;
	int i;

	/* New format: just a string
	 * Old format: integer list; e.g. 11;25;38
	 */
	tmp_string = g_key_file_get_string (keyfile, setting_name, key, NULL);
	if (tmp_string) {
		gboolean new_format = FALSE;
		GRegex *regex;
		GMatchInfo *match_info;
		const char *pattern = "^[[:space:]]*[[:digit:]]{1,3}[[:space:]]*(;[[:space:]]*[[:digit:]]{1,3}[[:space:]]*)*(;[[:space:]]*)?$";

		regex = g_regex_new (pattern, 0, 0, NULL);
		g_regex_match (regex, tmp_string, 0, &match_info);
		if (!g_match_info_matches (match_info))
			new_format = TRUE;
		g_match_info_free (match_info);
		g_regex_unref (regex);

		if (new_format) {
			array = g_byte_array_sized_new (strlen (tmp_string));
			g_byte_array_append (array, (guint8 *) tmp_string, strlen (tmp_string));
		}
		g_free (tmp_string);
	}

	if (!array) {
		/* Old format; list of ints */
		tmp_list = g_key_file_get_integer_list (keyfile, setting_name, key, &length, NULL);
		array = g_byte_array_sized_new (length);
		for (i = 0; i < length; i++) {
			int val = tmp_list[i];
			unsigned char v = (unsigned char) (val & 0xFF);

			if (val < 0 || val > 255) {
				g_warning ("%s: %s / %s ignoring invalid byte element '%d' (not "
					       " between 0 and 255 inclusive)", __func__, setting_name,
					       key, val);
			} else
				g_byte_array_append (array, (const unsigned char *) &v, sizeof (v));
		}
		g_free (tmp_list);
	}

	if (array->len == 0) {
		g_byte_array_free (array, TRUE);
		array = NULL;
	}
	return array;
}

static void
ssid_parser (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path)
{
	const char *setting_name = nm_setting_get_name (setting);
	GByteArray *array;

	array = get_uchar_array (keyfile, setting_name, key);
	if (array) {
		g_object_set (setting, key, array, NULL);
		g_byte_array_free (array, TRUE);
	} else {
		g_warning ("%s: ignoring invalid SSID for %s / %s",
		           __func__, setting_name, key);
	}
}

static char *
get_cert_path (const char *keyfile_path, GByteArray *cert_path)
{
	const char *base;
	char *p = NULL, *path, *dirname, *tmp;

	g_return_val_if_fail (keyfile_path != NULL, NULL);
	g_return_val_if_fail (cert_path != NULL, NULL);

	base = path = g_malloc0 (cert_path->len + 1);
	memcpy (path, cert_path->data, cert_path->len);

	if (path[0] == '/')
		return path;

	p = strrchr (path, '/');
	if (p)
		base = p + 1;

	dirname = g_path_get_dirname (keyfile_path);
	tmp = g_build_path ("/", dirname, base, NULL);
	g_free (dirname);
	g_free (path);
	return tmp;
}

#define SCHEME_PATH "file://"

static const char *certext[] = { ".pem", ".cert", ".crt", ".cer", ".p12", ".der", ".key" };

static gboolean
has_cert_ext (GByteArray *array)
{
	int i;

	for (i = 0; i < G_N_ELEMENTS (certext); i++) {
		guint32 extlen = strlen (certext[i]);

		if (array->len <= extlen)
			continue;
		if (memcmp (&array->data[array->len - extlen], certext[i], extlen) == 0)
			return TRUE;
	}
	return FALSE;
}

static void
cert_parser (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path)
{
	const char *setting_name = nm_setting_get_name (setting);
	GByteArray *array;
	gboolean success = FALSE;

	array = get_uchar_array (keyfile, setting_name, key);
	if (array) {
		/* Value could be either:
		 * 1) the raw key/cert data as a blob
		 * 2) a path scheme (ie, starts with "file://")
		 * 3) a plain path
		 */
		if (   (array->len > strlen (SCHEME_PATH))
		    && g_str_has_prefix ((const char *) array->data, SCHEME_PATH)
		    && (array->data[array->len - 1] == '\0')) {
			/* It's the PATH scheme, can just set plain data */
			g_object_set (setting, key, array, NULL);
			success = TRUE;
		} else if (   (array->len < 500)
		           && g_utf8_validate ((const char *) array->data, array->len, NULL)) {
			GByteArray *val;
			char *path;
			gboolean exists;

			/* Might be a bare path without the file:// prefix; in that case
			 * if it's an absolute path, use that, otherwise treat it as a
			 * relative path to the current directory.
			 */

			path = get_cert_path (keyfile_path, array);
			exists = g_file_test (path, G_FILE_TEST_EXISTS);
			if (   exists
			    || memchr (array->data, '/', array->len)
			    || has_cert_ext (array)) {
				/* Construct the proper value as required for the PATH scheme */
				val = g_byte_array_sized_new (strlen (SCHEME_PATH) + array->len + 1);
				g_byte_array_append (val, (const guint8 *) SCHEME_PATH, strlen (SCHEME_PATH));
				g_byte_array_append (val, (const guint8 *) path, strlen (path));
				g_byte_array_append (val, (const guint8 *) "\0", 1);
				g_object_set (setting, key, val, NULL);
				g_byte_array_free (val, TRUE);
				success = TRUE;

				/* Warn if the certificate didn't exist */
				if (exists == FALSE) {
					PLUGIN_WARN (KEYFILE_PLUGIN_NAME, "   certificate or key %s does not exist", path);
				}
			}
			g_free (path);
		}

		if (!success) {
			/* Assume it's a simple blob value of the certificate or private key's data */
			g_object_set (setting, key, array, NULL);
		}

		g_byte_array_free (array, TRUE);
	} else {
		g_warning ("%s: ignoring invalid key/cert value for %s / %s",
		           __func__, setting_name, key);
	}
}

typedef struct {
	const char *setting_name;
	const char *key;
	gboolean check_for_key;
	void (*parser) (NMSetting *setting, const char *key, GKeyFile *keyfile, const char *keyfile_path);
} KeyParser;

/* A table of keys that require further parsing/conversion because they are
 * stored in a format that can't be automatically read using the key's type.
 * i.e. IPv4 addresses, which are stored in NetworkManager as guint32, but are
 * stored in keyfiles as strings, eg "10.1.1.2" or IPv6 addresses stored 
 * in struct in6_addr internally, but as string in keyfiles.
 */
static KeyParser key_parsers[] = {
	{ NM_SETTING_IP4_CONFIG_SETTING_NAME,
	  NM_SETTING_IP4_CONFIG_ADDRESSES,
	  FALSE,
	  ip4_addr_parser },
	{ NM_SETTING_IP6_CONFIG_SETTING_NAME,
	  NM_SETTING_IP6_CONFIG_ADDRESSES,
	  FALSE,
	  ip6_addr_parser },
	{ NM_SETTING_IP4_CONFIG_SETTING_NAME,
	  NM_SETTING_IP4_CONFIG_ROUTES,
	  FALSE,
	  ip4_route_parser },
	{ NM_SETTING_IP6_CONFIG_SETTING_NAME,
	  NM_SETTING_IP6_CONFIG_ROUTES,
	  FALSE,
	  ip6_route_parser },
	{ NM_SETTING_IP4_CONFIG_SETTING_NAME,
	  NM_SETTING_IP4_CONFIG_DNS,
	  FALSE,
	  ip4_dns_parser },
	{ NM_SETTING_IP6_CONFIG_SETTING_NAME,
	  NM_SETTING_IP6_CONFIG_DNS,
	  FALSE,
	  ip6_dns_parser },
	{ NM_SETTING_WIRED_SETTING_NAME,
	  NM_SETTING_WIRED_MAC_ADDRESS,
	  TRUE,
	  mac_address_parser },
	{ NM_SETTING_WIRED_SETTING_NAME,
	  NM_SETTING_WIRED_CLONED_MAC_ADDRESS,
	  TRUE,
	  mac_address_parser },
	{ NM_SETTING_WIRELESS_SETTING_NAME,
	  NM_SETTING_WIRELESS_MAC_ADDRESS,
	  TRUE,
	  mac_address_parser },
	{ NM_SETTING_WIRELESS_SETTING_NAME,
	  NM_SETTING_WIRELESS_CLONED_MAC_ADDRESS,
	  TRUE,
	  mac_address_parser },
	{ NM_SETTING_WIRELESS_SETTING_NAME,
	  NM_SETTING_WIRELESS_BSSID,
	  TRUE,
	  mac_address_parser },
	{ NM_SETTING_BLUETOOTH_SETTING_NAME,
	  NM_SETTING_BLUETOOTH_BDADDR,
	  TRUE,
	  mac_address_parser },
	{ NM_SETTING_WIRELESS_SETTING_NAME,
	  NM_SETTING_WIRELESS_SSID,
	  TRUE,
	  ssid_parser },
	{ NM_SETTING_802_1X_SETTING_NAME,
	  NM_SETTING_802_1X_CA_CERT,
	  TRUE,
	  cert_parser },
	{ NM_SETTING_802_1X_SETTING_NAME,
	  NM_SETTING_802_1X_CLIENT_CERT,
	  TRUE,
	  cert_parser },
	{ NM_SETTING_802_1X_SETTING_NAME,
	  NM_SETTING_802_1X_PRIVATE_KEY,
	  TRUE,
	  cert_parser },
	{ NM_SETTING_802_1X_SETTING_NAME,
	  NM_SETTING_802_1X_PHASE2_CA_CERT,
	  TRUE,
	  cert_parser },
	{ NM_SETTING_802_1X_SETTING_NAME,
	  NM_SETTING_802_1X_PHASE2_CLIENT_CERT,
	  TRUE,
	  cert_parser },
	{ NM_SETTING_802_1X_SETTING_NAME,
	  NM_SETTING_802_1X_PHASE2_PRIVATE_KEY,
	  TRUE,
	  cert_parser },
	{ NULL, NULL, FALSE }
};

typedef struct {
	GKeyFile *keyfile;
	const char *keyfile_path;
} ReadInfo;

static void
read_one_setting_value (NMSetting *setting,
                        const char *key,
                        const GValue *value,
                        GParamFlags flags,
                        gpointer user_data)
{
	ReadInfo *info = user_data;
	const char *setting_name;
	GType type;
	GError *err = NULL;
	gboolean check_for_key = TRUE;
	KeyParser *parser = &key_parsers[0];

	/* Property is not writable */
	if (!(flags & G_PARAM_WRITABLE))
		return;

	/* Setting name gets picked up from the keyfile's section name instead */
	if (!strcmp (key, NM_SETTING_NAME))
		return;

	/* Don't read the NMSettingConnection object's 'read-only' property */
	if (   NM_IS_SETTING_CONNECTION (setting)
	    && !strcmp (key, NM_SETTING_CONNECTION_READ_ONLY))
		return;

	setting_name = nm_setting_get_name (setting);

	/* Look through the list of handlers for non-standard format key values */
	while (parser->setting_name) {
		if (!strcmp (parser->setting_name, setting_name) && !strcmp (parser->key, key)) {
			check_for_key = parser->check_for_key;
			break;
		}
		parser++;
	}

	/* VPN properties don't have the exact key name */
	if (NM_IS_SETTING_VPN (setting))
		check_for_key = FALSE;

	/* Check for the exact key in the GKeyFile if required.  Most setting
	 * properties map 1:1 to a key in the GKeyFile, but for those properties
	 * like IP addresses and routes where more than one value is actually
	 * encoded by the setting property, this won't be true.
	 */
	if (check_for_key && !g_key_file_has_key (info->keyfile, setting_name, key, &err)) {
		/* Key doesn't exist or an error ocurred, thus nothing to do. */
		if (err) {
			g_warning ("Error loading setting '%s' value: %s", setting_name, err->message);
			g_error_free (err);
		}
		return;
	}

	/* If there's a custom parser for this key, handle that before the generic
	 * parsers below.
	 */
	if (parser && parser->setting_name) {
		(*parser->parser) (setting, key, info->keyfile, info->keyfile_path);
		return;
	}

	type = G_VALUE_TYPE (value);

	if (type == G_TYPE_STRING) {
		char *str_val;

		str_val = g_key_file_get_string (info->keyfile, setting_name, key, NULL);
		g_object_set (setting, key, str_val, NULL);
		g_free (str_val);
	} else if (type == G_TYPE_UINT) {
		int int_val;

		int_val = g_key_file_get_integer (info->keyfile, setting_name, key, NULL);
		if (int_val < 0)
			g_warning ("Casting negative value (%i) to uint", int_val);
		g_object_set (setting, key, int_val, NULL);
	} else if (type == G_TYPE_INT) {
		int int_val;

		int_val = g_key_file_get_integer (info->keyfile, setting_name, key, NULL);
		g_object_set (setting, key, int_val, NULL);
	} else if (type == G_TYPE_BOOLEAN) {
		gboolean bool_val;

		bool_val = g_key_file_get_boolean (info->keyfile, setting_name, key, NULL);
		g_object_set (setting, key, bool_val, NULL);
	} else if (type == G_TYPE_CHAR) {
		int int_val;

		int_val = g_key_file_get_integer (info->keyfile, setting_name, key, NULL);
		if (int_val < G_MININT8 || int_val > G_MAXINT8)
			g_warning ("Casting value (%i) to char", int_val);

		g_object_set (setting, key, int_val, NULL);
	} else if (type == G_TYPE_UINT64) {
		char *tmp_str;
		guint64 uint_val;

		tmp_str = g_key_file_get_value (info->keyfile, setting_name, key, NULL);
		uint_val = g_ascii_strtoull (tmp_str, NULL, 10);
		g_free (tmp_str);
		g_object_set (setting, key, uint_val, NULL);
 	} else if (type == DBUS_TYPE_G_UCHAR_ARRAY) {
		gint *tmp;
		GByteArray *array;
		gsize length;
		int i;

		tmp = g_key_file_get_integer_list (info->keyfile, setting_name, key, &length, NULL);

		array = g_byte_array_sized_new (length);
		for (i = 0; i < length; i++) {
			int val = tmp[i];
			unsigned char v = (unsigned char) (val & 0xFF);

			if (val < 0 || val > 255) {
				g_warning ("%s: %s / %s ignoring invalid byte element '%d' (not "
				           " between 0 and 255 inclusive)", __func__, setting_name,
				           key, val);
			} else
				g_byte_array_append (array, (const unsigned char *) &v, sizeof (v));
		}

		g_object_set (setting, key, array, NULL);
		g_byte_array_free (array, TRUE);
		g_free (tmp);
 	} else if (type == DBUS_TYPE_G_LIST_OF_STRING) {
		gchar **sa;
		gsize length;
		int i;
		GSList *list = NULL;

		sa = g_key_file_get_string_list (info->keyfile, setting_name, key, &length, NULL);
		for (i = 0; i < length; i++)
			list = g_slist_prepend (list, sa[i]);

		list = g_slist_reverse (list);
		g_object_set (setting, key, list, NULL);

		g_slist_free (list);
		g_strfreev (sa);
	} else if (type == DBUS_TYPE_G_MAP_OF_STRING) {
		read_hash_of_string (info->keyfile, setting, key);
	} else if (type == DBUS_TYPE_G_UINT_ARRAY) {
		if (!read_array_of_uint (info->keyfile, setting, key)) {
			g_warning ("Unhandled setting property type (read): '%s/%s' : '%s'",
					 setting_name, key, G_VALUE_TYPE_NAME (value));
		}
	} else {
		g_warning ("Unhandled setting property type (read): '%s/%s' : '%s'",
				 setting_name, key, G_VALUE_TYPE_NAME (value));
	}
}

static NMSetting *
read_setting (GKeyFile *file, const char *keyfile_path, const char *setting_name)
{
	NMSetting *setting;
	ReadInfo info = { file, keyfile_path };

	setting = nm_connection_create_setting (setting_name);
	if (setting)
		nm_setting_enumerate_values (setting, read_one_setting_value, &info);
	else
		g_warning ("Invalid setting name '%s'", setting_name);

	return setting;
}

static void
read_vpn_secrets (GKeyFile *file, NMSettingVPN *s_vpn)
{
	char **keys, **iter;

	keys = g_key_file_get_keys (file, VPN_SECRETS_GROUP, NULL, NULL);
	for (iter = keys; *iter; iter++) {
		char *secret;

		secret = g_key_file_get_string (file, VPN_SECRETS_GROUP, *iter, NULL);
		if (secret) {
			nm_setting_vpn_add_secret (s_vpn, *iter, secret);
			g_free (secret);
		}
	}
	g_strfreev (keys);
}

NMConnection *
nm_keyfile_plugin_connection_from_file (const char *filename, GError **error)
{
	GKeyFile *key_file;
	struct stat statbuf;
	gboolean bad_owner, bad_permissions;
	NMConnection *connection = NULL;
	NMSettingConnection *s_con;
	NMSetting *setting;
	gchar **groups;
	gsize length;
	int i;
	gboolean vpn_secrets = FALSE;
	const char *ctype;
	GError *verify_error = NULL;

	if (stat (filename, &statbuf) != 0 || !S_ISREG (statbuf.st_mode)) {
		g_set_error_literal (error, KEYFILE_PLUGIN_ERROR, 0,
		                     "File did not exist or was not a regular file");
		return NULL;
	}

	bad_owner = getuid () != statbuf.st_uid;
	bad_permissions = statbuf.st_mode & 0077;

	if (bad_owner || bad_permissions) {
		g_set_error (error, KEYFILE_PLUGIN_ERROR, 0,
		             "File permissions (%o) or owner (%d) were insecure",
		             statbuf.st_mode, statbuf.st_uid);
		return NULL;
	}

	key_file = g_key_file_new ();
	if (!g_key_file_load_from_file (key_file, filename, G_KEY_FILE_NONE, error))
		goto out;

	connection = nm_connection_new ();

	groups = g_key_file_get_groups (key_file, &length);
	for (i = 0; i < length; i++) {
		/* Only read out secrets when needed */
		if (!strcmp (groups[i], VPN_SECRETS_GROUP)) {
			vpn_secrets = TRUE;
			continue;
		}

		setting = read_setting (key_file, filename, groups[i]);
		if (setting)
			nm_connection_add_setting (connection, setting);
	}

	/* Make sure that we have the base device type setting even if
	 * the keyfile didn't include it, which can happen when the base
	 * device type setting is all default values (like ethernet).
	 */
	s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION);
	if (s_con) {
		ctype = nm_setting_connection_get_connection_type (s_con);
		setting = nm_connection_get_setting_by_name (connection, ctype);
		if (ctype) {
			if (!setting && !strcmp (ctype, NM_SETTING_WIRED_SETTING_NAME))
				nm_connection_add_setting (connection, nm_setting_wired_new ());
		}
	}

	/* Handle vpn secrets after the 'vpn' setting was read */
	if (vpn_secrets) {
		NMSettingVPN *s_vpn;

		s_vpn = (NMSettingVPN *) nm_connection_get_setting (connection, NM_TYPE_SETTING_VPN);
		if (s_vpn)
			read_vpn_secrets (key_file, s_vpn);
	}

	g_strfreev (groups);

	/* Verify the connection */
	if (!nm_connection_verify (connection, &verify_error)) {
		g_set_error (error, KEYFILE_PLUGIN_ERROR, 0,
			         "invalid or missing connection property '%s'",
			         (verify_error && verify_error->message) ? verify_error->message : "(unknown)");
		g_clear_error (&verify_error);
		g_object_unref (connection);
		connection = NULL;
	}

out:
	g_key_file_free (key_file);
	return connection;
}
