// SPDX-License-Identifier: GPL-2.0+
/*
 * Dan Williams <dcbw@redhat.com>
 * Sjoerd Simons <sjoerd.simons@collabora.co.uk>
 * Daniel Drake <dsd@laptop.org>
 * Copyright (C) 2005 - 2014 Red Hat, Inc.
 * Copyright (C) 2008 Collabora Ltd.
 * Copyright (C) 2009 One Laptop per Child
 */

#include "nm-default.h"

#include "nm-device-olpc-mesh.h"

#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <sys/ioctl.h>

#include "devices/nm-device.h"
#include "nm-device-wifi.h"
#include "devices/nm-device-private.h"
#include "nm-utils.h"
#include "NetworkManagerUtils.h"
#include "nm-act-request.h"
#include "nm-setting-connection.h"
#include "nm-setting-olpc-mesh.h"
#include "nm-manager.h"
#include "platform/nm-platform.h"

#include "devices/nm-device-logging.h"
_LOG_DECLARE_SELF(NMDeviceOlpcMesh);

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

NM_GOBJECT_PROPERTIES_DEFINE (NMDeviceOlpcMesh,
	PROP_COMPANION,
	PROP_ACTIVE_CHANNEL,
);

typedef struct {
	NMDevice *companion;
	NMManager *manager;
	bool stage1_waiting:1;
} NMDeviceOlpcMeshPrivate;

struct _NMDeviceOlpcMesh {
	NMDevice parent;
	NMDeviceOlpcMeshPrivate _priv;
};

struct _NMDeviceOlpcMeshClass {
	NMDeviceClass parent;
};

G_DEFINE_TYPE (NMDeviceOlpcMesh, nm_device_olpc_mesh, NM_TYPE_DEVICE)

#define NM_DEVICE_OLPC_MESH_GET_PRIVATE(self) _NM_GET_PRIVATE (self, NMDeviceOlpcMesh, NM_IS_DEVICE_OLPC_MESH, NMDevice)

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

static gboolean
get_autoconnect_allowed (NMDevice *device)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (device);
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);

	/* We can't even connect if we don't have a companion yet. */
	if (!priv->companion)
		return FALSE;

	/* We must not attempt to autoconnect when the companion is connected or
	 * connecting, * because we'd tear down its connection. */
	if (nm_device_get_state (priv->companion) > NM_DEVICE_STATE_DISCONNECTED)
		return FALSE;

	return TRUE;
}

#define DEFAULT_SSID "olpc-mesh"

static gboolean
complete_connection (NMDevice *device,
                     NMConnection *connection,
                     const char *specific_object,
                     NMConnection *const*existing_connections,
                     GError **error)
{
	NMSettingOlpcMesh *s_mesh;

	s_mesh = nm_connection_get_setting_olpc_mesh (connection);
	if (!s_mesh) {
		s_mesh = (NMSettingOlpcMesh *) nm_setting_olpc_mesh_new ();
		nm_connection_add_setting (connection, NM_SETTING (s_mesh));
	}

	if (!nm_setting_olpc_mesh_get_ssid (s_mesh)) {
		gs_unref_bytes GBytes *ssid = NULL;

		ssid = g_bytes_new_static (DEFAULT_SSID, NM_STRLEN (DEFAULT_SSID));
		g_object_set (G_OBJECT (s_mesh), NM_SETTING_OLPC_MESH_SSID, ssid, NULL);
	}

	if (!nm_setting_olpc_mesh_get_dhcp_anycast_address (s_mesh)) {
		const char *anycast = "c0:27:c0:27:c0:27";

		g_object_set (G_OBJECT (s_mesh), NM_SETTING_OLPC_MESH_DHCP_ANYCAST_ADDRESS, anycast, NULL);

	}

	nm_utils_complete_generic (nm_device_get_platform (device),
	                           connection,
	                           NM_SETTING_OLPC_MESH_SETTING_NAME,
	                           existing_connections,
	                           NULL,
	                           _("Mesh"),
	                           NULL,
	                           NULL,
	                           FALSE); /* No IPv6 by default */

	return TRUE;
}

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

static NMActStageReturn
act_stage1_prepare (NMDevice *device, NMDeviceStateReason *out_failure_reason)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (device);
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);

	/* disconnect companion device, if it is connected */
	if (nm_device_get_act_request (NM_DEVICE (priv->companion))) {
		_LOGI (LOGD_OLPC, "disconnecting companion device %s",
		       nm_device_get_iface (priv->companion));
		/* FIXME: VPN stuff here is a bug; but we can't really change API now... */
		nm_device_state_changed (NM_DEVICE (priv->companion),
		                         NM_DEVICE_STATE_DISCONNECTED,
		                         NM_DEVICE_STATE_REASON_USER_REQUESTED);
		_LOGI (LOGD_OLPC, "companion %s disconnected",
		       nm_device_get_iface (priv->companion));
	}

	/* wait with continuing configuration until the companion device is done scanning */
	if (nm_device_wifi_get_scanning (NM_DEVICE_WIFI (priv->companion))) {
		priv->stage1_waiting = TRUE;
		return NM_ACT_STAGE_RETURN_POSTPONE;
	}

	priv->stage1_waiting = FALSE;
	return NM_ACT_STAGE_RETURN_SUCCESS;
}

static gboolean
_mesh_set_channel (NMDeviceOlpcMesh *self, guint32 channel)
{
	NMPlatform *platform;
	int ifindex = nm_device_get_ifindex (NM_DEVICE (self));
	guint32 old_channel;

	platform = nm_device_get_platform (NM_DEVICE (self));
	old_channel = nm_platform_mesh_get_channel (platform, ifindex);

	if (channel == 0)
		channel = old_channel;

	/* We want to call this even if the channel number is the same,
	 * because that actually starts the mesh with the configured mesh ID. */
	if (!nm_platform_mesh_set_channel (platform, ifindex, channel))
		return FALSE;

	if (old_channel != channel)
		_notify (self, PROP_ACTIVE_CHANNEL);

	return TRUE;
}

static NMActStageReturn
act_stage2_config (NMDevice *device, NMDeviceStateReason *out_failure_reason)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (device);
	NMSettingOlpcMesh *s_mesh;
	GBytes *ssid;
	const char *anycast_addr;
	gboolean success;

	s_mesh = nm_device_get_applied_setting (device, NM_TYPE_SETTING_OLPC_MESH);
	g_return_val_if_fail (s_mesh, NM_ACT_STAGE_RETURN_FAILURE);

	ssid = nm_setting_olpc_mesh_get_ssid (s_mesh);

	nm_device_take_down (NM_DEVICE (self), TRUE);
	success = nm_platform_mesh_set_ssid (nm_device_get_platform (device),
	                                     nm_device_get_ifindex (device),
	                                     g_bytes_get_data (ssid, NULL),
	                                     g_bytes_get_size (ssid));
	nm_device_bring_up (NM_DEVICE (self), TRUE, NULL);
	if (!success) {
		_LOGW (LOGD_WIFI, "Unable to set the mesh ID");
		return NM_ACT_STAGE_RETURN_FAILURE;
	}

	anycast_addr = nm_setting_olpc_mesh_get_dhcp_anycast_address (s_mesh);
	nm_device_set_dhcp_anycast_address (device, anycast_addr);

	if (!_mesh_set_channel (self, nm_setting_olpc_mesh_get_channel (s_mesh))) {
		_LOGW (LOGD_WIFI, "Unable to set the mesh channel");
		return NM_ACT_STAGE_RETURN_FAILURE;
	}


	return NM_ACT_STAGE_RETURN_SUCCESS;
}

static gboolean
is_available (NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (device);

	if (!NM_DEVICE_OLPC_MESH_GET_PRIVATE (self)->companion) {
		_LOGD (LOGD_WIFI, "not available because companion not found");
		return FALSE;
	}

	return TRUE;
}

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

static void
companion_cleanup (NMDeviceOlpcMesh *self)
{
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);

	if (priv->companion) {
		nm_device_wifi_scanning_prohibited_track (NM_DEVICE_WIFI (priv->companion),
		                                          self,
		                                          FALSE);
		g_signal_handlers_disconnect_by_data (priv->companion, self);
		g_clear_object (&priv->companion);
	}
	_notify (self, PROP_COMPANION);
}

static void
companion_notify_cb (NMDeviceWifi *companion, GParamSpec *pspec, gpointer user_data)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (user_data);
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);

	nm_assert (NM_IS_DEVICE_WIFI (companion));
	nm_assert (priv->companion == (gpointer) companion);

	if (!priv->stage1_waiting)
		return;

	if (!nm_device_wifi_get_scanning (NM_DEVICE_WIFI (companion))) {
		priv->stage1_waiting = FALSE;
		nm_device_activate_schedule_stage1_device_prepare (NM_DEVICE (self), FALSE);
	}
}

/* disconnect from mesh if someone starts using the companion */
static void
companion_state_changed_cb (NMDeviceWifi *companion,
                            NMDeviceState state,
                            NMDeviceState old_state,
                            NMDeviceStateReason reason,
                            gpointer user_data)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (user_data);
	NMDeviceState self_state = nm_device_get_state (NM_DEVICE (self));

	if (   old_state > NM_DEVICE_STATE_DISCONNECTED
	    && state <= NM_DEVICE_STATE_DISCONNECTED) {
		nm_device_emit_recheck_auto_activate (NM_DEVICE (self));
	}

	if (   self_state < NM_DEVICE_STATE_PREPARE
	    || self_state > NM_DEVICE_STATE_ACTIVATED
	    || state < NM_DEVICE_STATE_PREPARE
	    || state > NM_DEVICE_STATE_ACTIVATED)
		return;

	_LOGD (LOGD_OLPC, "disconnecting mesh due to companion connectivity");
	/* FIXME: VPN stuff here is a bug; but we can't really change API now... */
	nm_device_state_changed (NM_DEVICE (self),
	                         NM_DEVICE_STATE_DISCONNECTED,
	                         NM_DEVICE_STATE_REASON_USER_REQUESTED);
}

static gboolean
companion_autoconnect_allowed_cb (NMDeviceWifi *companion, gpointer user_data)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (user_data);
	NMDeviceState state = nm_device_get_state (NM_DEVICE (self));

	/* Don't allow the companion to autoconnect while a mesh connection is
	 * active */
	return (state < NM_DEVICE_STATE_PREPARE) || (state > NM_DEVICE_STATE_ACTIVATED);
}

static gboolean
check_companion (NMDeviceOlpcMesh *self, NMDevice *other)
{
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);
	const char *my_addr, *their_addr;

	if (!NM_IS_DEVICE_WIFI (other))
		return FALSE;

	my_addr = nm_device_get_hw_address (NM_DEVICE (self));
	their_addr = nm_device_get_hw_address (other);
	if (!nm_utils_hwaddr_matches (my_addr, -1, their_addr, -1))
		return FALSE;

	nm_assert (priv->companion == NULL);
	priv->companion = g_object_ref (other);

	_LOGI (LOGD_OLPC, "found companion Wi-Fi device %s",
	       nm_device_get_iface (other));

	g_signal_connect (G_OBJECT (other), NM_DEVICE_STATE_CHANGED,
	                  G_CALLBACK (companion_state_changed_cb), self);

	g_signal_connect (G_OBJECT (other), "notify::" NM_DEVICE_WIFI_SCANNING,
	                  G_CALLBACK (companion_notify_cb), self);

	g_signal_connect (G_OBJECT (other), NM_DEVICE_AUTOCONNECT_ALLOWED,
	                  G_CALLBACK (companion_autoconnect_allowed_cb), self);

	_notify (self, PROP_COMPANION);

	return TRUE;
}

static void
device_added_cb (NMManager *manager, NMDevice *other, gpointer user_data)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (user_data);
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);

	if (!priv->companion && check_companion (self, other)) {
		nm_device_queue_recheck_available (NM_DEVICE (self),
		                                   NM_DEVICE_STATE_REASON_NONE,
		                                   NM_DEVICE_STATE_REASON_NONE);
		nm_device_remove_pending_action (NM_DEVICE (self), NM_PENDING_ACTION_WAITING_FOR_COMPANION, FALSE);
	}
}

static void
device_removed_cb (NMManager *manager, NMDevice *other, gpointer user_data)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (user_data);

	if (other == NM_DEVICE_OLPC_MESH_GET_PRIVATE (self)->companion)
		companion_cleanup (self);
}

static void
find_companion (NMDeviceOlpcMesh *self)
{
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);
	const CList *tmp_lst;
	NMDevice *candidate;

	if (priv->companion)
		return;

	nm_device_add_pending_action (NM_DEVICE (self), NM_PENDING_ACTION_WAITING_FOR_COMPANION, TRUE);

	/* Try to find the companion if it's already known to the NMManager */
	nm_manager_for_each_device (priv->manager, candidate, tmp_lst) {
		if (check_companion (self, candidate)) {
			nm_device_queue_recheck_available (NM_DEVICE (self),
			                                   NM_DEVICE_STATE_REASON_NONE,
			                                   NM_DEVICE_STATE_REASON_NONE);
			nm_device_remove_pending_action (NM_DEVICE (self), NM_PENDING_ACTION_WAITING_FOR_COMPANION, TRUE);
			break;
		}
	}
}

static void
state_changed (NMDevice *device,
               NMDeviceState new_state,
               NMDeviceState old_state,
               NMDeviceStateReason reason)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (device);
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);

	if (new_state == NM_DEVICE_STATE_UNAVAILABLE)
		find_companion (self);

	if (priv->companion) {
		gboolean temporarily_prohibited = FALSE;

		if (   new_state >= NM_DEVICE_STATE_PREPARE
		    && new_state <= NM_DEVICE_STATE_IP_CONFIG) {
			/* Don't allow the companion to scan while configuring the mesh interface */
			temporarily_prohibited = TRUE;
		}
		nm_device_wifi_scanning_prohibited_track (NM_DEVICE_WIFI (priv->companion),
		                                          self,
		                                          temporarily_prohibited);
	}
}

static guint32
get_dhcp_timeout_for_device (NMDevice *device, int addr_family)
{
	/* shorter timeout for mesh connectivity */
	return 20;
}

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

static void
get_property (GObject *object, guint prop_id,
              GValue *value, GParamSpec *pspec)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (object);
	NMDevice *device = NM_DEVICE (self);
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);

	switch (prop_id) {
	case PROP_COMPANION:
		nm_dbus_utils_g_value_set_object_path (value, priv->companion);
		break;
	case PROP_ACTIVE_CHANNEL:
		g_value_set_uint (value, nm_platform_mesh_get_channel (nm_device_get_platform (device), nm_device_get_ifindex (device)));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

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

static void
nm_device_olpc_mesh_init (NMDeviceOlpcMesh * self)
{
}

static void
constructed (GObject *object)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (object);
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);

	G_OBJECT_CLASS (nm_device_olpc_mesh_parent_class)->constructed (object);

	priv->manager = g_object_ref (NM_MANAGER_GET);

	g_signal_connect (priv->manager, NM_MANAGER_DEVICE_ADDED, G_CALLBACK (device_added_cb), self);
	g_signal_connect (priv->manager, NM_MANAGER_DEVICE_REMOVED, G_CALLBACK (device_removed_cb), self);
}

NMDevice *
nm_device_olpc_mesh_new (const char *iface)
{
	return (NMDevice *) g_object_new (NM_TYPE_DEVICE_OLPC_MESH,
	                                  NM_DEVICE_IFACE, iface,
	                                  NM_DEVICE_TYPE_DESC, "802.11 OLPC Mesh",
	                                  NM_DEVICE_DEVICE_TYPE, NM_DEVICE_TYPE_OLPC_MESH,
	                                  NM_DEVICE_LINK_TYPE, NM_LINK_TYPE_OLPC_MESH,
	                                  NULL);
}

static void
dispose (GObject *object)
{
	NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH (object);
	NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE (self);

	companion_cleanup (self);

	if (priv->manager) {
		g_signal_handlers_disconnect_by_func (priv->manager, G_CALLBACK (device_added_cb), self);
		g_signal_handlers_disconnect_by_func (priv->manager, G_CALLBACK (device_removed_cb), self);
		g_clear_object (&priv->manager);
	}

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

static const NMDBusInterfaceInfoExtended interface_info_device_olpc_mesh = {
	.parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT (
		NM_DBUS_INTERFACE_DEVICE_OLPC_MESH,
		.signals = NM_DEFINE_GDBUS_SIGNAL_INFOS (
			&nm_signal_info_property_changed_legacy,
		),
		.properties = NM_DEFINE_GDBUS_PROPERTY_INFOS (
			NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L ("HwAddress",     "s", NM_DEVICE_HW_ADDRESS),
			NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L ("Companion",     "o", NM_DEVICE_OLPC_MESH_COMPANION),
			NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L ("ActiveChannel", "u", NM_DEVICE_OLPC_MESH_ACTIVE_CHANNEL),
		),
	),
	.legacy_property_changed = TRUE,
};

static void
nm_device_olpc_mesh_class_init (NMDeviceOlpcMeshClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS (klass);
	NMDeviceClass *device_class = NM_DEVICE_CLASS (klass);

	object_class->constructed = constructed;
	object_class->get_property = get_property;
	object_class->dispose = dispose;

	dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS (&interface_info_device_olpc_mesh);

	device_class->connection_type_supported = NM_SETTING_OLPC_MESH_SETTING_NAME;
	device_class->connection_type_check_compatible = NM_SETTING_OLPC_MESH_SETTING_NAME;
	device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES (NM_LINK_TYPE_OLPC_MESH);

	device_class->get_autoconnect_allowed = get_autoconnect_allowed;
	device_class->complete_connection = complete_connection;
	device_class->is_available = is_available;
	device_class->act_stage1_prepare = act_stage1_prepare;
	device_class->act_stage2_config = act_stage2_config;
	device_class->state_changed = state_changed;
	device_class->get_dhcp_timeout_for_device = get_dhcp_timeout_for_device;

	obj_properties[PROP_COMPANION] =
	     g_param_spec_string (NM_DEVICE_OLPC_MESH_COMPANION, "", "",
	                          NULL,
	                          G_PARAM_READABLE |
	                          G_PARAM_STATIC_STRINGS);

	obj_properties[PROP_ACTIVE_CHANNEL] =
	     g_param_spec_uint (NM_DEVICE_OLPC_MESH_ACTIVE_CHANNEL, "", "",
	                        0, G_MAXUINT32, 0,
	                        G_PARAM_READABLE |
	                        G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties);
}

