// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2008 - 2011 Red Hat, Inc.
 */

#include "nm-default.h"

#include "nm-dispatcher-utils.h"

#include "nm-dbus-interface.h"
#include "nm-connection.h"
#include "nm-setting-ip4-config.h"
#include "nm-setting-ip6-config.h"
#include "nm-setting-connection.h"

#include "nm-libnm-core-aux/nm-dispatcher-api.h"
#include "nm-utils.h"

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

static gboolean
_is_valid_key (const char *line, gssize len)
{
	gsize i, l;
	char ch;

	if (!line)
		return FALSE;

	if (len < 0)
		len = strlen (line);

	if (len == 0)
		return FALSE;

	ch = line[0];
	if (   !(ch >= 'A' && ch <= 'Z')
	    && !NM_IN_SET (ch, '_'))
		return FALSE;

	l = (gsize) len;

	for (i = 1; i < l; i++) {
		ch = line[i];

		if (   !(ch >= 'A' && ch <= 'Z')
		    && !(ch >= '0' && ch <= '9')
		    && !NM_IN_SET (ch, '_'))
			return FALSE;
	}

	return TRUE;
}

static gboolean
_is_valid_line (const char *line)
{
	const char *d;

	if (!line)
		return FALSE;

	d = strchr (line, '=');
	if (!d || d == line)
		return FALSE;

	return _is_valid_key (line, d - line);
}

static char *
_sanitize_var_name (const char *key)
{
	char *sanitized;

	nm_assert (key);

	if (!key[0])
		return NULL;

	sanitized = g_ascii_strup (key, -1);
	if (!NM_STRCHAR_ALL (sanitized, ch,    (ch >= 'A' && ch <= 'Z')
	                                    || (ch >= '0' && ch <= '9')
	                                    || NM_IN_SET (ch, '_'))) {
		g_free (sanitized);
		return NULL;
	}

	nm_assert (_is_valid_key (sanitized, -1));
	return sanitized;
}

static void
_items_add_str_take (GPtrArray *items, char *line)
{
	nm_assert (items);
	nm_assert (_is_valid_line (line));

	g_ptr_array_add (items, line);
}

static void
_items_add_str (GPtrArray *items, const char *line)
{
	_items_add_str_take (items, g_strdup (line));
}

static void
_items_add_key (GPtrArray *items, const char *prefix, const char *key, const char *value)
{
	nm_assert (items);
	nm_assert (_is_valid_key (key, -1));
	nm_assert (value);

	_items_add_str_take (items, g_strconcat (prefix ?: "", key, "=", value, NULL));
}

static void
_items_add_key0 (GPtrArray *items, const char *prefix, const char *key, const char *value)
{
	nm_assert (items);
	nm_assert (_is_valid_key (key, -1));

	if (!value) {
		/* for convenience, allow NULL values to indicate to skip the line. */
		return;
	}

	_items_add_str_take (items, g_strconcat (prefix ?: "", key, "=", value, NULL));
}

G_GNUC_PRINTF (2, 3)
static void
_items_add_printf (GPtrArray *items, const char *fmt, ...)
{
	va_list ap;
	char *line;

	nm_assert (items);
	nm_assert (fmt);

	va_start (ap, fmt);
	line = g_strdup_vprintf (fmt, ap);
	va_end (ap);
	_items_add_str_take (items, line);
}

static void
_items_add_strv (GPtrArray *items, const char *prefix, const char *key, const char *const*values)
{
	gboolean has;
	guint i;
	GString *str;

	nm_assert (items);
	nm_assert (_is_valid_key (key, -1));

	if (!values || !values[0]) {
		/* Only add an item if the list of @values is not empty */
		return;
	}

	str = g_string_new (NULL);

	if (prefix)
		g_string_append (str, prefix);
	g_string_append (str, key);
	g_string_append_c (str, '=');

	has = FALSE;
	for (i = 0; values[i]; i++) {
		if (!values[i][0])
			continue;
		if (has)
			g_string_append_c (str, ' ');
		else
			has = TRUE;
		g_string_append (str, values[i]);
	}

	_items_add_str_take (items, g_string_free (str, FALSE));
}

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

static void
construct_proxy_items (GPtrArray *items, GVariant *proxy_config, const char *prefix)
{
	GVariant *variant;

	nm_assert (items);

	if (!proxy_config)
		return;

	variant = g_variant_lookup_value (proxy_config, "pac-url", G_VARIANT_TYPE_STRING);
	if (variant) {
		_items_add_key (items, prefix, "PROXY_PAC_URL",
		                g_variant_get_string (variant, NULL));
		g_variant_unref (variant);
	}

	variant = g_variant_lookup_value (proxy_config, "pac-script", G_VARIANT_TYPE_STRING);
	if (variant) {
		_items_add_key (items, prefix, "PROXY_PAC_SCRIPT",
		                g_variant_get_string (variant, NULL));
		g_variant_unref (variant);
	}
}

static void
construct_ip_items (GPtrArray *items, int addr_family, GVariant *ip_config, const char *prefix)
{
	GVariant *val;
	guint i;
	guint nroutes = 0;
	char four_or_six;

	if (!ip_config)
		return;

	if (!prefix)
		prefix = "";

	four_or_six = nm_utils_addr_family_to_char (addr_family);

	val = g_variant_lookup_value (ip_config,
	                              "addresses",
	                                addr_family == AF_INET
	                              ? G_VARIANT_TYPE ("aau")
	                              : G_VARIANT_TYPE ("a(ayuay)"));
	if (val) {
		gs_unref_ptrarray GPtrArray *addresses = NULL;
		gs_free char *gateway_free = NULL;
		const char *gateway;

		if (addr_family == AF_INET)
			addresses = nm_utils_ip4_addresses_from_variant (val, &gateway_free);
		else
			addresses = nm_utils_ip6_addresses_from_variant (val, &gateway_free);

		gateway = gateway_free ?: "0.0.0.0";

		if (addresses && addresses->len) {
			for (i = 0; i < addresses->len; i++) {
				NMIPAddress *addr = addresses->pdata[i];

				_items_add_printf (items,
				                   "%sIP%c_ADDRESS_%d=%s/%d %s",
				                   prefix,
				                   four_or_six,
				                   i,
				                   nm_ip_address_get_address (addr),
				                   nm_ip_address_get_prefix (addr),
				                   gateway);
			}

			_items_add_printf (items,
			                   "%sIP%c_NUM_ADDRESSES=%u",
			                   prefix,
			                   four_or_six,
			                   addresses->len);
		}

		_items_add_key (items,
		                prefix,
		                  addr_family == AF_INET
		                ? "IP4_GATEWAY"
		                : "IP6_GATEWAY",
		                gateway);

		g_variant_unref (val);
	}

	val = g_variant_lookup_value (ip_config,
	                              "nameservers",
	                                addr_family == AF_INET
	                              ? G_VARIANT_TYPE ("au")
	                              : G_VARIANT_TYPE ("aay"));
	if (val) {
		gs_strfreev char **v = NULL;

		if (addr_family == AF_INET)
			v = nm_utils_ip4_dns_from_variant (val);
		else
			v = nm_utils_ip6_dns_from_variant (val);
		_items_add_strv (items,
		                 prefix,
		                   addr_family == AF_INET
		                 ? "IP4_NAMESERVERS"
		                 : "IP6_NAMESERVERS",
		                 NM_CAST_STRV_CC (v));
		g_variant_unref (val);
	}

	val = g_variant_lookup_value (ip_config, "domains", G_VARIANT_TYPE_STRING_ARRAY);
	if (val) {
		gs_free const char **v = NULL;

		v = g_variant_get_strv (val, NULL);
		_items_add_strv (items, prefix,
		                   addr_family == AF_INET
		                 ? "IP4_DOMAINS"
		                 : "IP6_DOMAINS",
		                 v);
		g_variant_unref (val);
	}


	if (addr_family == AF_INET) {
		val = g_variant_lookup_value (ip_config, "wins-servers", G_VARIANT_TYPE ("au"));
		if (val) {
			gs_strfreev char **v = NULL;

			v = nm_utils_ip4_dns_from_variant (val);
			_items_add_strv (items, prefix, "IP4_WINS_SERVERS", NM_CAST_STRV_CC (v));
			g_variant_unref (val);
		}
	}

	val = g_variant_lookup_value (ip_config,
	                              "routes",
	                                addr_family == AF_INET
	                              ? G_VARIANT_TYPE ("aau")
	                              : G_VARIANT_TYPE ("a(ayuayu)"));
	if (val) {
		gs_unref_ptrarray GPtrArray *routes = NULL;

		if (addr_family == AF_INET)
			routes = nm_utils_ip4_routes_from_variant (val);
		else
			routes = nm_utils_ip6_routes_from_variant (val);

		if (   routes
		    && routes->len > 0) {
			const char *const DEFAULT_GW = addr_family == AF_INET ? "0.0.0.0" : "::";

			nroutes = routes->len;

			for (i = 0; i < routes->len; i++) {
				NMIPRoute *route = routes->pdata[i];

				_items_add_printf (items,
				                   "%sIP%c_ROUTE_%u=%s/%d %s %u",
				                   prefix,
				                   four_or_six,
				                   i,
				                   nm_ip_route_get_dest (route),
				                   nm_ip_route_get_prefix (route),
				                   nm_ip_route_get_next_hop (route) ?: DEFAULT_GW,
				                   (guint) NM_MAX ((gint64) 0, nm_ip_route_get_metric (route)));
			}
		}

		g_variant_unref (val);
	}
	if (nroutes > 0 || addr_family == AF_INET) {
		/* we also set IP4_NUM_ROUTES=0, but don't do so for addresses and IPv6 routes.
		 * Historic reasons. */
		_items_add_printf (items, "%sIP%c_NUM_ROUTES=%u", prefix, four_or_six, nroutes);
	}
}

static void
construct_device_dhcp_items (GPtrArray *items, int addr_family, GVariant *dhcp_config)
{
	GVariantIter iter;
	const char *key;
	GVariant *val;
	char four_or_six;
	gboolean found_unknown_245 = FALSE;
	gs_unref_variant GVariant *private_245_val = NULL;

	if (!dhcp_config)
		return;

	if (!g_variant_is_of_type (dhcp_config, G_VARIANT_TYPE_VARDICT))
		return;

	four_or_six = nm_utils_addr_family_to_char (addr_family);

	g_variant_iter_init (&iter, dhcp_config);
	while (g_variant_iter_next (&iter, "{&sv}", &key, &val)) {
		if (g_variant_is_of_type (val, G_VARIANT_TYPE_STRING)) {
			gs_free char *ucased = NULL;

			ucased = _sanitize_var_name (key);
			if (ucased) {
				_items_add_printf (items,
				                   "DHCP%c_%s=%s",
				                   four_or_six,
				                   ucased,
				                   g_variant_get_string (val, NULL));

				/* MS Azure sends the server endpoint in the dhcp private
				 * option 245. cloud-init searches the Azure server endpoint
				 * value looking for the standard dhclient label used for
				 * that option, which is "unknown_245".
				 * The 11-dhclient script shipped with Fedora and RHEL dhcp
				 * package converts our dispatcher environment vars to the
				 * dhclient ones (new_<some_option>) and calls dhclient hook
				 * scripts.
				 * Let's make cloud-init happy and let's duplicate the dhcp
				 * option 245 with the legacy name of the default dhclient
				 * label also when using the internal client.
				 * Note however that the dhclient plugin will have unknown_
				 * labels represented as ascii string when possible, falling
				 * back to hex string otherwise.
				 * private_ labels instead are always in hex string format.
				 * This shouldn't affect the MS Azure server endpoint value,
				 * as it usually belongs to the 240.0.0.0/4 network and so
				 * is always represented as an hex string. Moreover, cloudinit
				 * code checks just for an hex value in unknown_245.
				 */
				if (addr_family == AF_INET) {
					if (nm_streq (key, "private_245"))
						private_245_val = g_variant_ref (val);
					else if (nm_streq (key, "unknown_245"))
						found_unknown_245 = true;
				}
			}
		}
		g_variant_unref (val);
	}

	if (private_245_val != NULL && !found_unknown_245) {
		_items_add_printf (items,
		                   "DHCP4_UNKNOWN_245=%s",
		                   g_variant_get_string (private_245_val, NULL));
	}
}

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

char **
nm_dispatcher_utils_construct_envp (const char *action,
                                    GVariant *connection_dict,
                                    GVariant *connection_props,
                                    GVariant *device_props,
                                    GVariant *device_proxy_props,
                                    GVariant *device_ip4_props,
                                    GVariant *device_ip6_props,
                                    GVariant *device_dhcp4_props,
                                    GVariant *device_dhcp6_props,
                                    const char *connectivity_state,
                                    const char *vpn_ip_iface,
                                    GVariant *vpn_proxy_props,
                                    GVariant *vpn_ip4_props,
                                    GVariant *vpn_ip6_props,
                                    char **out_iface,
                                    const char **out_error_message)
{
	const char *iface = NULL;
	const char *ip_iface = NULL;
	const char *uuid = NULL;
	const char *id = NULL;
	const char *path = NULL;
	const char *filename = NULL;
	gboolean external;
	NMDeviceState dev_state = NM_DEVICE_STATE_UNKNOWN;
	GVariant *variant;
	gs_unref_ptrarray GPtrArray *items = NULL;
	const char *error_message_backup;

	if (!out_error_message)
		out_error_message = &error_message_backup;

	g_return_val_if_fail (action != NULL, NULL);
	g_return_val_if_fail (out_iface != NULL, NULL);
	g_return_val_if_fail (*out_iface == NULL, NULL);

	items = g_ptr_array_new_with_free_func (g_free);

	/* Hostname and connectivity changes don't require a device nor contain a connection */
	if (NM_IN_STRSET (action, NMD_ACTION_HOSTNAME,
	                          NMD_ACTION_CONNECTIVITY_CHANGE))
		goto done;

	/* Connection properties */
	if (g_variant_lookup (connection_props, NMD_CONNECTION_PROPS_PATH, "&o", &path))
		_items_add_key (items, NULL, "CONNECTION_DBUS_PATH", path);

	if (g_variant_lookup (connection_props, NMD_CONNECTION_PROPS_EXTERNAL, "b", &external) && external)
		_items_add_str (items, "CONNECTION_EXTERNAL=1");

	if (g_variant_lookup (connection_props, NMD_CONNECTION_PROPS_FILENAME, "&s", &filename))
		_items_add_key (items, NULL, "CONNECTION_FILENAME", filename);

	/* Canonicalize the VPN interface name; "" is used when passing it through
	 * D-Bus so make sure that's fixed up here.
	 */
	if (vpn_ip_iface && !vpn_ip_iface[0])
		vpn_ip_iface = NULL;

	if (!g_variant_lookup (device_props, NMD_DEVICE_PROPS_INTERFACE, "&s", &iface)) {
		*out_error_message = "Missing or invalid required value " NMD_DEVICE_PROPS_INTERFACE "!";
		return NULL;
	}
	if (!*iface)
		iface = NULL;

	variant = g_variant_lookup_value (device_props, NMD_DEVICE_PROPS_IP_INTERFACE, NULL);
	if (variant) {
		if (!g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING)) {
			*out_error_message = "Invalid value " NMD_DEVICE_PROPS_IP_INTERFACE "!";
			return NULL;
		}
		g_variant_unref (variant);
		(void) g_variant_lookup (device_props, NMD_DEVICE_PROPS_IP_INTERFACE, "&s", &ip_iface);
	}

	if (!g_variant_lookup (device_props, NMD_DEVICE_PROPS_TYPE, "u", NULL)) {
		*out_error_message = "Missing or invalid required value " NMD_DEVICE_PROPS_TYPE "!";
		return NULL;
	}

	variant = g_variant_lookup_value (device_props, NMD_DEVICE_PROPS_STATE, G_VARIANT_TYPE_UINT32);
	if (!variant) {
		*out_error_message = "Missing or invalid required value " NMD_DEVICE_PROPS_STATE "!";
		return NULL;
	}
	dev_state = g_variant_get_uint32 (variant);
	g_variant_unref (variant);

	if (!g_variant_lookup (device_props, NMD_DEVICE_PROPS_PATH, "o", NULL)) {
		*out_error_message = "Missing or invalid required value " NMD_DEVICE_PROPS_PATH "!";
		return NULL;
	}

	{
		gs_unref_variant GVariant *con_setting = NULL;

		con_setting = g_variant_lookup_value (connection_dict, NM_SETTING_CONNECTION_SETTING_NAME, NM_VARIANT_TYPE_SETTING);
		if (!con_setting) {
			*out_error_message = "Failed to read connection setting";
			return NULL;
		}

		if (!g_variant_lookup (con_setting, NM_SETTING_CONNECTION_UUID, "&s", &uuid)) {
			*out_error_message = "Connection hash did not contain the UUID";
			return NULL;
		}

		if (!g_variant_lookup (con_setting, NM_SETTING_CONNECTION_ID, "&s", &id)) {
			*out_error_message = "Connection hash did not contain the ID";
			return NULL;
		}

		_items_add_key0 (items, NULL, "CONNECTION_UUID", uuid);
		_items_add_key0 (items, NULL, "CONNECTION_ID", id);
		_items_add_key0 (items, NULL, "DEVICE_IFACE", iface);
		_items_add_key0 (items, NULL, "DEVICE_IP_IFACE", ip_iface);
	}

	/* Device items aren't valid if the device isn't activated */
	if (   iface
	    && dev_state == NM_DEVICE_STATE_ACTIVATED) {
		construct_proxy_items (items, device_proxy_props, NULL);
		construct_ip_items (items, AF_INET, device_ip4_props, NULL);
		construct_ip_items (items, AF_INET6, device_ip6_props, NULL);
		construct_device_dhcp_items (items, AF_INET, device_dhcp4_props);
		construct_device_dhcp_items (items, AF_INET6, device_dhcp6_props);
	}

	if (vpn_ip_iface) {
		_items_add_key (items, NULL, "VPN_IP_IFACE", vpn_ip_iface);
		construct_proxy_items (items, vpn_proxy_props, "VPN_");
		construct_ip_items (items, AF_INET, vpn_ip4_props, "VPN_");
		construct_ip_items (items, AF_INET6, vpn_ip6_props, "VPN_");
	}

	/* Backwards compat: 'iface' is set in this order:
	 * 1) VPN interface name
	 * 2) Device IP interface name
	 * 3) Device interface anme
	 */
	if (vpn_ip_iface)
		*out_iface = g_strdup (vpn_ip_iface);
	else if (ip_iface)
		*out_iface = g_strdup (ip_iface);
	else
		*out_iface = g_strdup (iface);

done:
	/* The connectivity_state value will only be meaningful for 'connectivity-change' events
	 * (otherwise it will be "UNKNOWN"), so we only set the environment variable in those cases.
	 */
	if (!NM_IN_STRSET (connectivity_state, NULL, "UNKNOWN"))
		_items_add_key (items, NULL, "CONNECTIVITY_STATE", connectivity_state);

	_items_add_key0 (items, NULL, "PATH", g_getenv ("PATH"));

	_items_add_key (items, NULL, "NM_DISPATCHER_ACTION", action);

	*out_error_message = NULL;
	g_ptr_array_add (items, NULL);
	return (char **) g_ptr_array_free (g_steal_pointer (&items), FALSE);
}
