// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2017 Intel Corporation
 */

#include "nm-default.h"

#include "nm-iwd-manager.h"

#include <net/if.h>

#include "nm-logging.h"
#include "nm-core-internal.h"
#include "nm-manager.h"
#include "nm-device-iwd.h"
#include "nm-wifi-utils.h"
#include "nm-glib-aux/nm-random-utils.h"
#include "settings/nm-settings.h"

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

typedef struct {
	const char *name;
	NMIwdNetworkSecurity security;
	char buf[0];
} KnownNetworkId;

typedef struct {
	GDBusProxy *known_network;
	NMSettingsConnection *mirror_connection;
} KnownNetworkData;

typedef struct {
	NMManager *manager;
	NMSettings *settings;
	GCancellable *cancellable;
	gboolean running;
	GDBusObjectManager *object_manager;
	guint agent_id;
	char *agent_path;
	GHashTable *known_networks;
} NMIwdManagerPrivate;

struct _NMIwdManager {
	GObject parent;
	NMIwdManagerPrivate _priv;
};

struct _NMIwdManagerClass {
	GObjectClass parent;
};

G_DEFINE_TYPE (NMIwdManager, nm_iwd_manager, G_TYPE_OBJECT)

#define NM_IWD_MANAGER_GET_PRIVATE(self) _NM_GET_PRIVATE (self, NMIwdManager, NM_IS_IWD_MANAGER)

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

#define _NMLOG_PREFIX_NAME                "iwd-manager"
#define _NMLOG_DOMAIN                     LOGD_WIFI

#define _NMLOG(level, ...) \
	G_STMT_START { \
		if (nm_logging_enabled (level, _NMLOG_DOMAIN)) { \
			char __prefix[32]; \
			\
			if (self) \
				g_snprintf (__prefix, sizeof (__prefix), "%s[%p]", ""_NMLOG_PREFIX_NAME"", (self)); \
			else \
				g_strlcpy (__prefix, _NMLOG_PREFIX_NAME, sizeof (__prefix)); \
			_nm_log ((level), (_NMLOG_DOMAIN), 0, NULL, NULL, \
			          "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
			          __prefix _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
		} \
	} G_STMT_END

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

static void mirror_8021x_connection_take_and_delete (NMSettingsConnection *sett_conn);

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

static const char *
get_variant_string_or_null (GVariant *v)
{
	if (!v)
		return NULL;

	if (   !g_variant_is_of_type (v, G_VARIANT_TYPE_STRING)
	    && !g_variant_is_of_type (v, G_VARIANT_TYPE_OBJECT_PATH))
		return NULL;

	return g_variant_get_string (v, NULL);
}

static const char *
get_property_string_or_null (GDBusProxy *proxy, const char *property)
{
	gs_unref_variant GVariant *value = NULL;

	if (!proxy || !property)
		return NULL;

	value = g_dbus_proxy_get_cached_property (proxy, property);

	return get_variant_string_or_null (value);
}

static void
agent_dbus_method_cb (GDBusConnection *connection,
                      const char *sender, const char *object_path,
                      const char *interface_name, const char *method_name,
                      GVariant *parameters,
                      GDBusMethodInvocation *invocation,
                      gpointer user_data)
{
	NMIwdManager *self = user_data;
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	const char *network_path, *device_path, *ifname;
	gs_unref_object GDBusInterface *network = NULL, *device_obj = NULL;
	int ifindex;
	NMDevice *device;
	gs_free char *name_owner = NULL;
	int errsv;

	/* Be paranoid and check the sender address */
	name_owner = g_dbus_object_manager_client_get_name_owner (G_DBUS_OBJECT_MANAGER_CLIENT (priv->object_manager));
	if (!nm_streq0 (name_owner, sender))
		goto return_error;

	if (!strcmp (method_name, "RequestUserPassword"))
		g_variant_get (parameters, "(&os)", &network_path, NULL);
	else
		g_variant_get (parameters, "(&o)", &network_path);

	network = g_dbus_object_manager_get_interface (priv->object_manager,
	                                               network_path,
	                                               NM_IWD_NETWORK_INTERFACE);
	if (!network) {
		_LOGE ("unable to find the network object");
		return;
	}


	device_path = get_property_string_or_null (G_DBUS_PROXY (network), "Device");
	if (!device_path) {
		_LOGD ("agent-request: device not cached for network %s in IWD Agent request",
		       network_path);
		goto return_error;
	}

	device_obj = g_dbus_object_manager_get_interface (priv->object_manager,
	                                                  device_path,
	                                                  NM_IWD_DEVICE_INTERFACE);

	ifname = get_property_string_or_null (G_DBUS_PROXY (device_obj), "Name");
	if (!ifname) {
		_LOGD ("agent-request: name not cached for device %s in IWD Agent request",
		       device_path);
		goto return_error;
	}

	ifindex = if_nametoindex (ifname);
	if (!ifindex) {
		errsv = errno;
		_LOGD ("agent-request: if_nametoindex failed for Name %s for Device at %s: %i",
		       ifname, device_path, errsv);
		goto return_error;
	}

	device = nm_manager_get_device_by_ifindex (priv->manager, ifindex);
	if (!NM_IS_DEVICE_IWD (device)) {
		_LOGD ("agent-request: IWD device named %s is not a Wifi device in IWD Agent request",
		       ifname);
		goto return_error;
	}

	if (nm_device_iwd_agent_query (NM_DEVICE_IWD (device), invocation))
		return;

	_LOGD ("agent-request: device %s did not handle the IWD Agent request", ifname);

return_error:
	/* IWD doesn't look at the specific error */
	g_dbus_method_invocation_return_error_literal (invocation, NM_DEVICE_ERROR,
	                                               NM_DEVICE_ERROR_INVALID_CONNECTION,
	                                               "Secrets not available for this connection");
}

static const GDBusInterfaceInfo iwd_agent_iface_info = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT (
	"net.connman.iwd.Agent",
	.methods = NM_DEFINE_GDBUS_METHOD_INFOS (
		NM_DEFINE_GDBUS_METHOD_INFO (
			"RequestPassphrase",
			.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
				NM_DEFINE_GDBUS_ARG_INFO ("network", "o"),
			),
			.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
				NM_DEFINE_GDBUS_ARG_INFO ("passphrase", "s"),
			),
		),
		NM_DEFINE_GDBUS_METHOD_INFO (
			"RequestPrivateKeyPassphrase",
			.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
				NM_DEFINE_GDBUS_ARG_INFO ("network", "o"),
			),
			.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
				NM_DEFINE_GDBUS_ARG_INFO ("passphrase", "s"),
			),
		),
		NM_DEFINE_GDBUS_METHOD_INFO (
			"RequestUserNameAndPassword",
			.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
				NM_DEFINE_GDBUS_ARG_INFO ("network", "o"),
			),
			.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
				NM_DEFINE_GDBUS_ARG_INFO ("user", "s"),
				NM_DEFINE_GDBUS_ARG_INFO ("password", "s"),
			),
		),
		NM_DEFINE_GDBUS_METHOD_INFO (
			"RequestUserPassword",
			.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
				NM_DEFINE_GDBUS_ARG_INFO ("network", "o"),
				NM_DEFINE_GDBUS_ARG_INFO ("user", "s"),
			),
			.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
				NM_DEFINE_GDBUS_ARG_INFO ("password", "s"),
			),
		),
	),
);

static guint
iwd_agent_export (GDBusConnection *connection, gpointer user_data,
                  char **agent_path, GError **error)
{
	static const GDBusInterfaceVTable vtable = {
		.method_call = agent_dbus_method_cb,
	};
	char path[50];
	unsigned int rnd;
	guint id;

	nm_utils_random_bytes (&rnd, sizeof (rnd));

	nm_sprintf_buf (path, "/agent/%u", rnd);

	id = g_dbus_connection_register_object (connection, path,
	                                        NM_UNCONST_PTR (GDBusInterfaceInfo, &iwd_agent_iface_info),
	                                        &vtable, user_data, NULL, error);

	if (id)
		*agent_path = g_strdup (path);
	return id;
}

static void
register_agent (NMIwdManager *self)
{
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	GDBusInterface *agent_manager;

	agent_manager = g_dbus_object_manager_get_interface (priv->object_manager,
	                                                     "/net/connman/iwd",
	                                                     NM_IWD_AGENT_MANAGER_INTERFACE);

	if (!agent_manager) {
		/* IWD prior to 1.0 dated 30 October, 2019 has the agent manager on a
		 * different path. */
		agent_manager = g_dbus_object_manager_get_interface (priv->object_manager,
		                                                     "/",
		                                                     NM_IWD_AGENT_MANAGER_INTERFACE);
	}

	if (!agent_manager) {
		_LOGE ("unable to register the IWD Agent: PSK/8021x Wi-Fi networks may not work");
		return;
	}

	/* Register our agent */
	g_dbus_proxy_call (G_DBUS_PROXY (agent_manager),
	                   "RegisterAgent",
	                   g_variant_new ("(o)", priv->agent_path),
	                   G_DBUS_CALL_FLAGS_NONE, -1,
	                   NULL, NULL, NULL);

	g_object_unref (agent_manager);
}

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

static KnownNetworkId *
known_network_id_new (const char *name, NMIwdNetworkSecurity security)
{
	KnownNetworkId *id;
	gsize strsize = strlen (name) + 1;

	id = g_malloc (sizeof (KnownNetworkId) + strsize);
	id->name = id->buf;
	id->security = security;
	memcpy (id->buf, name, strsize);

	return id;
}

static guint
known_network_id_hash (KnownNetworkId *id)
{
	NMHashState h;

	nm_hash_init (&h, 1947951703u);
	nm_hash_update_val (&h, id->security);
	nm_hash_update_str (&h, id->name);
	return nm_hash_complete (&h);
}

static gboolean
known_network_id_equal (KnownNetworkId *a, KnownNetworkId *b)
{
	return    a->security == b->security
	       && nm_streq (a->name, b->name);
}

static void
known_network_data_free (KnownNetworkData *network)
{
	if (!network)
		return;

	g_object_unref (network->known_network);
	mirror_8021x_connection_take_and_delete (network->mirror_connection);
	g_slice_free (KnownNetworkData, network);
}

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

static void
set_device_dbus_object (NMIwdManager *self, GDBusProxy *proxy,
                        GDBusObject *object)
{
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	const char *ifname;
	int ifindex;
	NMDevice *device;
	int errsv;

	ifname = get_property_string_or_null (proxy, "Name");
	if (!ifname) {
		_LOGE ("Name not cached for Device at %s",
		       g_dbus_proxy_get_object_path (proxy));
		return;
	}

	ifindex = if_nametoindex (ifname);

	if (!ifindex) {
		errsv = errno;
		_LOGE ("if_nametoindex failed for Name %s for Device at %s: %i",
		       ifname, g_dbus_proxy_get_object_path (proxy), errsv);
		return;
	}

	device = nm_manager_get_device_by_ifindex (priv->manager, ifindex);
	if (!NM_IS_DEVICE_IWD (device)) {
		_LOGE ("IWD device named %s is not a Wifi device", ifname);
		return;
	}

	nm_device_iwd_set_dbus_object (NM_DEVICE_IWD (device), object);
}

/* Look up an existing NMSettingsConnection for a WPA2-Enterprise network
 * that has been preprovisioned with an IWD config file, or create a new
 * in-memory connection object so that NM autoconnect mechanism and the
 * clients know this networks needs no additional EAP configuration from
 * the user.
 */
static NMSettingsConnection *
mirror_8021x_connection (NMIwdManager *self,
                         const char *name,
                         gboolean create_new)
{
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	NMSettingsConnection *const*iter;
	gs_unref_object NMConnection *connection = NULL;
	NMSettingsConnection *settings_connection = NULL;
	char uuid[37];
	NMSetting *setting;
	GError *error = NULL;
	gs_unref_bytes GBytes *new_ssid = NULL;

	for (iter = nm_settings_get_connections (priv->settings, NULL); *iter; iter++) {
		NMSettingsConnection *sett_conn = *iter;
		NMConnection *conn = nm_settings_connection_get_connection (sett_conn);
		NMIwdNetworkSecurity security;
		gs_free char *ssid_name = NULL;
		NMSettingWireless *s_wifi;
		NMSetting8021x *s_8021x;
		gboolean external = FALSE;
		guint i;

		security = nm_wifi_connection_get_iwd_security (conn, NULL);
		if (security != NM_IWD_NETWORK_SECURITY_8021X)
			continue;

		s_wifi = nm_connection_get_setting_wireless (conn);
		if (!s_wifi)
			continue;

		ssid_name = _nm_utils_ssid_to_utf8 (nm_setting_wireless_get_ssid (s_wifi));

		if (!nm_streq (ssid_name, name))
			continue;

		s_8021x = nm_connection_get_setting_802_1x (conn);
		for (i = 0; i < nm_setting_802_1x_get_num_eap_methods (s_8021x); i++) {
			if (nm_streq (nm_setting_802_1x_get_eap_method (s_8021x, i), "external")) {
				external = TRUE;
				break;
			}
		}

		/* Prefer returning connections for EAP method "external" */
		if (!settings_connection || external)
			settings_connection = sett_conn;
	}

	/* If we already have an NMSettingsConnection matching this
	 * KnownNetwork, whether it's saved or an in-memory connection
	 * potentially created by ourselves then we have nothing left to
	 * do here.
	 */
	if (settings_connection || !create_new)
		return settings_connection;

	connection = nm_simple_connection_new ();

	setting = NM_SETTING (g_object_new (NM_TYPE_SETTING_CONNECTION,
	                                    NM_SETTING_CONNECTION_TYPE, NM_SETTING_WIRELESS_SETTING_NAME,
	                                    NM_SETTING_CONNECTION_ID, name,
	                                    NM_SETTING_CONNECTION_UUID, nm_utils_uuid_generate_buf (uuid),
	                                    NULL));
	nm_connection_add_setting (connection, setting);

	new_ssid = g_bytes_new (name, strlen (name));
	setting = NM_SETTING (g_object_new (NM_TYPE_SETTING_WIRELESS,
	                                    NM_SETTING_WIRELESS_SSID, new_ssid,
	                                    NM_SETTING_WIRELESS_MODE, NM_SETTING_WIRELESS_MODE_INFRA,
	                                    NULL));
	nm_connection_add_setting (connection, setting);

	setting = NM_SETTING (g_object_new (NM_TYPE_SETTING_WIRELESS_SECURITY,
	                                    NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open",
	                                    NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-eap",
	                                    NULL));
	nm_connection_add_setting (connection, setting);

	/* "password" and "private-key-password" may be requested by the IWD agent
	 * from NM and IWD will implement a specific secret cache policy so by
	 * default respect that policy and don't save copies of those secrets in
	 * NM settings.  The saved values can not be used anyway because of our
	 * use of NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW.
	 */
	setting = NM_SETTING (g_object_new (NM_TYPE_SETTING_802_1X,
	                                    NM_SETTING_802_1X_PASSWORD_FLAGS, NM_SETTING_SECRET_FLAG_NOT_SAVED,
	                                    NM_SETTING_802_1X_PRIVATE_KEY_PASSWORD_FLAGS, NM_SETTING_SECRET_FLAG_NOT_SAVED,
	                                    NULL));
	nm_setting_802_1x_add_eap_method (NM_SETTING_802_1X (setting), "external");
	nm_connection_add_setting (connection, setting);

	if (!nm_connection_normalize (connection, NULL, NULL, NULL))
		return NULL;

	if (!nm_settings_add_connection (priv->settings,
	                                 connection,
	                                 NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
	                                 NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
	                                 NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED,
	                                 &settings_connection,
	                                 &error)) {
		_LOGW ("failed to add a mirror NMConnection for IWD's Known Network '%s': %s",
		       name, error->message);
		g_error_free (error);
		return NULL;
	}

	return settings_connection;
}

static void
mirror_8021x_connection_take_and_delete (NMSettingsConnection *sett_conn)
{
	NMSettingsConnectionIntFlags flags;

	if (!sett_conn)
		return;

	flags = nm_settings_connection_get_flags (sett_conn);

	/* If connection has not been saved since we created it
	 * in interface_added it too can be removed now. */
	if (NM_FLAGS_HAS (flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED))
		nm_settings_connection_delete (sett_conn, FALSE);

	g_object_unref (sett_conn);
}

static void
interface_added (GDBusObjectManager *object_manager, GDBusObject *object,
                 GDBusInterface *interface, gpointer user_data)
{
	NMIwdManager *self = user_data;
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	GDBusProxy *proxy;
	const char *iface_name;

	if (!priv->running)
		return;

	g_return_if_fail (G_IS_DBUS_PROXY (interface));

	proxy = G_DBUS_PROXY (interface);
	iface_name = g_dbus_proxy_get_interface_name (proxy);

	if (nm_streq (iface_name, NM_IWD_DEVICE_INTERFACE)) {
		set_device_dbus_object (self, proxy, object);
		return;
	}

	if (nm_streq (iface_name, NM_IWD_KNOWN_NETWORK_INTERFACE)) {
		KnownNetworkId *id;
		KnownNetworkData *data;
		NMIwdNetworkSecurity security;
		const char *type_str, *name;
		NMSettingsConnection *sett_conn = NULL;

		type_str = get_property_string_or_null (proxy, "Type");
		name = get_property_string_or_null (proxy, "Name");
		if (!type_str || !name)
			return;

		if (nm_streq (type_str, "open"))
			security = NM_IWD_NETWORK_SECURITY_NONE;
		else if (nm_streq (type_str, "psk"))
			security = NM_IWD_NETWORK_SECURITY_PSK;
		else if (nm_streq (type_str, "8021x"))
			security = NM_IWD_NETWORK_SECURITY_8021X;
		else
			return;

		id = known_network_id_new (name, security);

		data = g_hash_table_lookup (priv->known_networks, id);
		if (data) {
			_LOGW ("DBus error: KnownNetwork already exists ('%s', %s)",
			       name, type_str);
			g_free (id);
			nm_g_object_ref_set (&data->known_network, proxy);
		} else {
			data = g_slice_new0 (KnownNetworkData);
			data->known_network = g_object_ref (proxy);
			g_hash_table_insert (priv->known_networks, id, data);
		}

		if (security == NM_IWD_NETWORK_SECURITY_8021X) {
			sett_conn = mirror_8021x_connection (self, name, TRUE);

			if (   sett_conn
			    && sett_conn != data->mirror_connection) {
				NMSettingsConnection *sett_conn_old = data->mirror_connection;

				data->mirror_connection = nm_g_object_ref (sett_conn);
				mirror_8021x_connection_take_and_delete (sett_conn_old);
			}
		} else
			mirror_8021x_connection_take_and_delete (g_steal_pointer (&data->mirror_connection));

		return;
	}
}

static void
interface_removed (GDBusObjectManager *object_manager, GDBusObject *object,
                   GDBusInterface *interface, gpointer user_data)
{
	NMIwdManager *self = user_data;
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	GDBusProxy *proxy;
	const char *iface_name;

	g_return_if_fail (G_IS_DBUS_PROXY (interface));

	proxy = G_DBUS_PROXY (interface);
	iface_name = g_dbus_proxy_get_interface_name (proxy);

	if (nm_streq (iface_name, NM_IWD_DEVICE_INTERFACE)) {
		set_device_dbus_object (self, proxy, NULL);
		return;
	}

	if (nm_streq (iface_name, NM_IWD_KNOWN_NETWORK_INTERFACE)) {
		KnownNetworkId id;
		const char *type_str;

		type_str = get_property_string_or_null (proxy, "Type");
		id.name = get_property_string_or_null (proxy, "Name");
		if (!type_str || !id.name)
			return;

		if (nm_streq (type_str, "open"))
			id.security = NM_IWD_NETWORK_SECURITY_NONE;
		else if (nm_streq (type_str, "psk"))
			id.security = NM_IWD_NETWORK_SECURITY_PSK;
		else if (nm_streq (type_str, "8021x"))
			id.security = NM_IWD_NETWORK_SECURITY_8021X;
		else
			return;

		g_hash_table_remove (priv->known_networks, &id);
		return;
	}
}

static void
connection_removed (NMSettings *settings,
                    NMSettingsConnection *sett_conn,
                    gpointer user_data)
{
	NMIwdManager *self = user_data;
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	NMConnection *conn = nm_settings_connection_get_connection (sett_conn);
	NMSettingWireless *s_wireless;
	gboolean mapped;
	KnownNetworkData *data;
	KnownNetworkId id;

	id.security = nm_wifi_connection_get_iwd_security (conn, &mapped);
	if (!mapped)
		return;

	s_wireless = nm_connection_get_setting_wireless (conn);
	id.name = _nm_utils_ssid_to_utf8 (nm_setting_wireless_get_ssid (s_wireless));
	data = g_hash_table_lookup (priv->known_networks, &id);
	g_free ((char *) id.name);
	if (!data)
		return;

	if (id.security == NM_IWD_NETWORK_SECURITY_8021X) {
		NMSettingsConnection *new_mirror_conn;

		if (data->mirror_connection != sett_conn)
			return;

		g_clear_object (&data->mirror_connection);

		/* Don't call Forget for an 8021x network until there's no
		 * longer *any* matching NMSettingsConnection (debatable)
		 */
		new_mirror_conn = mirror_8021x_connection (self, id.name, FALSE);
		if (new_mirror_conn) {
			data->mirror_connection = g_object_ref (new_mirror_conn);
			return;
		}
	}

	if (!priv->running)
		return;

	g_dbus_proxy_call (data->known_network, "Forget",
	                   NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}

static gboolean
_om_has_name_owner (GDBusObjectManager *object_manager)
{
	gs_free char *name_owner = NULL;

	nm_assert (G_IS_DBUS_OBJECT_MANAGER_CLIENT (object_manager));

	name_owner = g_dbus_object_manager_client_get_name_owner (G_DBUS_OBJECT_MANAGER_CLIENT (object_manager));
	return !!name_owner;
}

static void
object_added (NMIwdManager *self, GDBusObject *object)
{
	GList *interfaces, *iter;

	interfaces = g_dbus_object_get_interfaces (object);

	for (iter = interfaces; iter; iter = iter->next) {
		GDBusInterface *interface = G_DBUS_INTERFACE (iter->data);

		interface_added (NULL, object, interface, self);
	}

	g_list_free_full (interfaces, g_object_unref);
}

static void
release_object_manager (NMIwdManager *self)
{
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);

	if (!priv->object_manager)
		return;

	g_signal_handlers_disconnect_by_data (priv->object_manager, self);

	if (priv->agent_id) {
		GDBusConnection *agent_connection;
		GDBusObjectManagerClient *omc = G_DBUS_OBJECT_MANAGER_CLIENT (priv->object_manager);

		agent_connection = g_dbus_object_manager_client_get_connection (omc);

		/* We're is called when we're shutting down (i.e. our DBus connection
		 * is being closed, and IWD will detect this) or IWD was stopped so
		 * in either case calling UnregisterAgent will not do anything.
		 */
		g_dbus_connection_unregister_object (agent_connection, priv->agent_id);
		priv->agent_id = 0;
		nm_clear_g_free (&priv->agent_path);
	}

	g_clear_object (&priv->object_manager);
}

static void prepare_object_manager (NMIwdManager *self);

static void
name_owner_changed (GObject *object, GParamSpec *pspec, gpointer user_data)
{
	NMIwdManager *self = user_data;
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	GDBusObjectManager *object_manager = G_DBUS_OBJECT_MANAGER (object);

	nm_assert (object_manager == priv->object_manager);

	if (_om_has_name_owner (object_manager)) {
		release_object_manager (self);
		prepare_object_manager (self);
	} else {
		const CList *tmp_lst;
		NMDevice *device;

		if (!priv->running)
			return;

		priv->running = false;

		nm_manager_for_each_device (priv->manager, device, tmp_lst) {
			if (NM_IS_DEVICE_IWD (device)) {
				nm_device_iwd_set_dbus_object (NM_DEVICE_IWD (device),
				                               NULL);
			}
		}
	}
}

static void
device_added (NMManager *manager, NMDevice *device, gpointer user_data)
{
	NMIwdManager *self = user_data;
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	GList *objects, *iter;

	if (!NM_IS_DEVICE_IWD (device))
		return;

	if (!priv->running)
		return;

	objects = g_dbus_object_manager_get_objects (priv->object_manager);
	for (iter = objects; iter; iter = iter->next) {
		GDBusObject *object = G_DBUS_OBJECT (iter->data);
		gs_unref_object GDBusInterface *interface = NULL;
		const char *obj_ifname;

		interface = g_dbus_object_get_interface (object,
		                                         NM_IWD_DEVICE_INTERFACE);
		obj_ifname = get_property_string_or_null ((GDBusProxy *) interface, "Name");

		if (!obj_ifname || strcmp (nm_device_get_iface (device), obj_ifname))
			continue;

		nm_device_iwd_set_dbus_object (NM_DEVICE_IWD (device), object);
		break;
	}

	g_list_free_full (objects, g_object_unref);
}

static void
got_object_manager (GObject *object, GAsyncResult *result, gpointer user_data)
{
	NMIwdManager *self = user_data;
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	GError *error = NULL;
	GDBusObjectManager *object_manager;
	GDBusConnection *connection;

	object_manager = g_dbus_object_manager_client_new_for_bus_finish (result, &error);
	if (object_manager == NULL) {
		_LOGE ("failed to acquire IWD Object Manager: Wi-Fi will not be available (%s)",
		       error->message);
		g_clear_error (&error);
		return;
	}

	priv->object_manager = object_manager;

	g_signal_connect (priv->object_manager, "notify::name-owner",
	                  G_CALLBACK (name_owner_changed), self);

	nm_assert (G_IS_DBUS_OBJECT_MANAGER_CLIENT (object_manager));

	connection = g_dbus_object_manager_client_get_connection (G_DBUS_OBJECT_MANAGER_CLIENT (object_manager));

	priv->agent_id = iwd_agent_export (connection,
	                                   self,
	                                   &priv->agent_path,
	                                   &error);
	if (!priv->agent_id) {
		_LOGE ("failed to export the IWD Agent: PSK/8021x Wi-Fi networks may not work: %s",
		       error->message);
		g_clear_error (&error);
	}

	if (_om_has_name_owner (object_manager)) {
		GList *objects, *iter;

		priv->running = true;

		g_signal_connect (priv->object_manager, "interface-added",
		                  G_CALLBACK (interface_added), self);
		g_signal_connect (priv->object_manager, "interface-removed",
		                  G_CALLBACK (interface_removed), self);

		g_hash_table_remove_all (priv->known_networks);

		objects = g_dbus_object_manager_get_objects (object_manager);
		for (iter = objects; iter; iter = iter->next)
			object_added (self, G_DBUS_OBJECT (iter->data));

		g_list_free_full (objects, g_object_unref);

		if (priv->agent_id)
			register_agent (self);
	}
}

static void
prepare_object_manager (NMIwdManager *self)
{
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);

	g_dbus_object_manager_client_new_for_bus (NM_IWD_BUS_TYPE,
	                                          G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
	                                          NM_IWD_SERVICE, "/",
	                                          NULL, NULL, NULL,
	                                          priv->cancellable,
	                                          got_object_manager, self);
}

gboolean
nm_iwd_manager_is_known_network (NMIwdManager *self, const char *name,
                                 NMIwdNetworkSecurity security)
{
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	KnownNetworkId kn_id = { name, security };

	return g_hash_table_contains (priv->known_networks, &kn_id);
}

GDBusProxy *
nm_iwd_manager_get_dbus_interface (NMIwdManager *self, const char *path,
                                   const char *name)
{
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);
	GDBusInterface *interface;

	if (!priv->object_manager)
		return NULL;

	interface = g_dbus_object_manager_get_interface (priv->object_manager, path, name);

	return interface ? G_DBUS_PROXY (interface) : NULL;
}

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

NM_DEFINE_SINGLETON_GETTER (NMIwdManager, nm_iwd_manager_get,
                            NM_TYPE_IWD_MANAGER);

static void
nm_iwd_manager_init (NMIwdManager *self)
{
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);

	priv->manager = g_object_ref (NM_MANAGER_GET);
	g_signal_connect (priv->manager, NM_MANAGER_DEVICE_ADDED,
	                  G_CALLBACK (device_added), self);

	priv->settings = g_object_ref (NM_SETTINGS_GET);
	g_signal_connect (priv->settings, NM_SETTINGS_SIGNAL_CONNECTION_REMOVED,
	                  G_CALLBACK (connection_removed), self);

	priv->cancellable = g_cancellable_new ();

	priv->known_networks = g_hash_table_new_full ((GHashFunc) known_network_id_hash,
	                                              (GEqualFunc) known_network_id_equal,
	                                              g_free,
	                                              (GDestroyNotify) known_network_data_free);

	prepare_object_manager (self);
}

static void
dispose (GObject *object)
{
	NMIwdManager *self = (NMIwdManager *) object;
	NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE (self);

	release_object_manager (self);

	nm_clear_g_cancellable (&priv->cancellable);

	if (priv->settings) {
		g_signal_handlers_disconnect_by_data (priv->settings, self);
		g_clear_object (&priv->settings);
	}

	/* This may trigger mirror connection removals so it happens
	 * after the g_signal_handlers_disconnect_by_data above.
	 */
	nm_clear_pointer (&priv->known_networks, g_hash_table_destroy);

	if (priv->manager) {
		g_signal_handlers_disconnect_by_data (priv->manager, self);
		g_clear_object (&priv->manager);
	}

	G_OBJECT_CLASS (nm_iwd_manager_parent_class)->dispose (object);
}

static void
nm_iwd_manager_class_init (NMIwdManagerClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->dispose = dispose;
}
