/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/*
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 * Copyright 2007 - 2008 Novell, Inc.
 * Copyright 2007 - 2008 Red Hat, Inc.
 */

#include "nm-default.h"

#include "nm-vpn-plugin.h"

#include <signal.h>

#include "nm-vpn-enum-types.h"
#include "nm-utils.h"
#include "nm-connection.h"
#include "nm-dbus-glib-types.h"
#include "nm-setting-private.h"

static gboolean impl_vpn_plugin_connect    (NMVPNPlugin *plugin,
                                            GHashTable *connection,
                                            GError **error);

static gboolean impl_vpn_plugin_connect_interactive (NMVPNPlugin *plugin,
                                                     GHashTable *connection,
                                                     GHashTable *details,
                                                     GError **error);

static gboolean impl_vpn_plugin_need_secrets (NMVPNPlugin *plugin,
                                              GHashTable *connection,
                                              char **service_name,
                                              GError **err);

static gboolean impl_vpn_plugin_new_secrets (NMVPNPlugin *plugin,
                                             GHashTable *connection,
                                             GError **err);

static gboolean impl_vpn_plugin_disconnect (NMVPNPlugin *plugin,
                                            GError **err);

static gboolean impl_vpn_plugin_set_config (NMVPNPlugin *plugin,
                                            GHashTable *config,
                                            GError **err);

static gboolean impl_vpn_plugin_set_ip4_config (NMVPNPlugin *plugin,
                                                GHashTable *config,
                                                GError **err);

static gboolean impl_vpn_plugin_set_ip6_config (NMVPNPlugin *plugin,
                                                GHashTable *config,
                                                GError **err);

static gboolean impl_vpn_plugin_set_failure (NMVPNPlugin *plugin,
                                             char *reason,
                                             GError **err);

#include "nm-vpn-plugin-glue.h"

#define NM_VPN_PLUGIN_QUIT_TIMER    180

G_DEFINE_ABSTRACT_TYPE (NMVPNPlugin, nm_vpn_plugin, G_TYPE_OBJECT)

typedef struct {
	NMVPNServiceState state;

	/* DBUS-y stuff */
	DBusGConnection *connection;
	char *dbus_service_name;

	/* Temporary stuff */
	guint connect_timer;
	guint quit_timer;
	guint fail_stop_id;
	gboolean interactive;

	gboolean got_config;
	gboolean has_ip4, got_ip4;
	gboolean has_ip6, got_ip6;

	/* Config stuff copied from config to ip4config */
	GValue banner, tundev, gateway, mtu;
} NMVPNPluginPrivate;

#define NM_VPN_PLUGIN_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_VPN_PLUGIN, NMVPNPluginPrivate))

enum {
	STATE_CHANGED,
	CONFIG,
	IP4_CONFIG,
	IP6_CONFIG,
	LOGIN_BANNER,
	FAILURE,
	QUIT,
	SECRETS_REQUIRED,

	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

enum {
	PROP_0,
	PROP_DBUS_SERVICE_NAME,
	PROP_STATE,

	LAST_PROP
};

static GSList *active_plugins = NULL;


GQuark
nm_vpn_plugin_error_quark (void)
{
	static GQuark quark = 0;

	if (!quark)
		quark = g_quark_from_static_string ("nm_vpn_plugin_error");

	return quark;
}


static void
nm_vpn_plugin_set_connection (NMVPNPlugin *plugin,
                              DBusGConnection *connection)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);

	if (priv->connection)
		dbus_g_connection_unref (priv->connection);

	priv->connection = connection;
}

DBusGConnection *
nm_vpn_plugin_get_connection (NMVPNPlugin *plugin)
{
	DBusGConnection *connection;

	g_return_val_if_fail (NM_IS_VPN_PLUGIN (plugin), NULL);

	connection = NM_VPN_PLUGIN_GET_PRIVATE (plugin)->connection;

	if (connection)
		dbus_g_connection_ref (connection);

	return connection;
}

NMVPNServiceState
nm_vpn_plugin_get_state (NMVPNPlugin *plugin)
{
	g_return_val_if_fail (NM_IS_VPN_PLUGIN (plugin), NM_VPN_SERVICE_STATE_UNKNOWN);

	return NM_VPN_PLUGIN_GET_PRIVATE (plugin)->state;
}

void
nm_vpn_plugin_set_state (NMVPNPlugin *plugin,
                         NMVPNServiceState state)
{
	NMVPNPluginPrivate *priv;

	g_return_if_fail (NM_IS_VPN_PLUGIN (plugin));

	priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);
	if (priv->state != state) {
		priv->state = state;
		g_signal_emit (plugin, signals[STATE_CHANGED], 0, state);
	}
}

void
nm_vpn_plugin_set_login_banner (NMVPNPlugin *plugin,
                                const char *banner)
{
	g_return_if_fail (NM_IS_VPN_PLUGIN (plugin));
	g_return_if_fail (banner != NULL);

	g_signal_emit (plugin, signals[LOGIN_BANNER], 0, banner);
}

void
nm_vpn_plugin_failure (NMVPNPlugin *plugin,
                       NMVPNPluginFailure reason)
{
	g_return_if_fail (NM_IS_VPN_PLUGIN (plugin));

	g_signal_emit (plugin, signals[FAILURE], 0, reason);
}

gboolean
nm_vpn_plugin_disconnect (NMVPNPlugin *plugin, GError **err)
{
	gboolean ret = FALSE;
	NMVPNServiceState state;

	g_return_val_if_fail (NM_IS_VPN_PLUGIN (plugin), FALSE);

	state = nm_vpn_plugin_get_state (plugin);
	switch (state) {
	case NM_VPN_SERVICE_STATE_STOPPING:
		g_set_error (err,
		             NM_VPN_PLUGIN_ERROR,
		             NM_VPN_PLUGIN_ERROR_STOPPING_IN_PROGRESS,
		             "%s",
		             "Could not process the request because the VPN connection is already being stopped.");
		break;
	case NM_VPN_SERVICE_STATE_STOPPED:
		g_set_error (err,
		             NM_VPN_PLUGIN_ERROR,
		             NM_VPN_PLUGIN_ERROR_ALREADY_STOPPED,
		             "%s",
		             "Could not process the request because no VPN connection was active.");
		break;
	case NM_VPN_SERVICE_STATE_STARTING:
	case NM_VPN_SERVICE_STATE_STARTED:
		nm_vpn_plugin_set_state (plugin, NM_VPN_SERVICE_STATE_STOPPING);
		ret = NM_VPN_PLUGIN_GET_CLASS (plugin)->disconnect (plugin, err);
		nm_vpn_plugin_set_state (plugin, NM_VPN_SERVICE_STATE_STOPPED);
		break;
	case NM_VPN_SERVICE_STATE_INIT:
		ret = TRUE;
		break;

	default:
		g_warning ("Unhandled VPN service state %d", state);
		g_assert_not_reached ();
		break;
	}

	return ret;
}

static void
nm_vpn_plugin_emit_quit (NMVPNPlugin *plugin)
{
	g_signal_emit (plugin, signals[QUIT], 0);
}

static gboolean
connect_timer_expired (gpointer data)
{
	NMVPNPlugin *plugin = NM_VPN_PLUGIN (data);
	GError *err = NULL;

	NM_VPN_PLUGIN_GET_PRIVATE (plugin)->connect_timer = 0;
	g_message ("Connect timer expired, disconnecting.");
	nm_vpn_plugin_disconnect (plugin, &err);
	if (err) {
		g_warning ("Disconnect failed: %s", err->message);
		g_error_free (err);
	}

	return G_SOURCE_REMOVE;
}

static gboolean
quit_timer_expired (gpointer data)
{
	NMVPNPlugin *self = NM_VPN_PLUGIN (data);

	NM_VPN_PLUGIN_GET_PRIVATE (self)->quit_timer = 0;
	nm_vpn_plugin_emit_quit (self);
	return G_SOURCE_REMOVE;
}

static void
schedule_quit_timer (NMVPNPlugin *self)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (self);

	nm_clear_g_source (&priv->quit_timer);
	priv->quit_timer = g_timeout_add_seconds (NM_VPN_PLUGIN_QUIT_TIMER,
	                                          quit_timer_expired,
	                                          self);
}

static gboolean
fail_stop (gpointer data)
{
	NMVPNPlugin *self = NM_VPN_PLUGIN (data);

	NM_VPN_PLUGIN_GET_PRIVATE (self)->fail_stop_id = 0;
	nm_vpn_plugin_set_state (self, NM_VPN_SERVICE_STATE_STOPPED);
	return G_SOURCE_REMOVE;
}

static void
schedule_fail_stop (NMVPNPlugin *plugin, guint timeout_secs)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);

	nm_clear_g_source (&priv->fail_stop_id);
	if (timeout_secs)
		priv->fail_stop_id = g_timeout_add_seconds (timeout_secs, fail_stop, plugin);
	else
		priv->fail_stop_id = g_idle_add (fail_stop, plugin);
}

static void
_g_value_set (GValue *dst, GValue *src)
{
	if (src) {
		GType type = G_VALUE_TYPE (src);

		if (G_IS_VALUE (dst))
			g_value_unset (dst);
		g_value_init (dst, type);
		g_value_copy (src, dst);
	} else if (G_IS_VALUE (dst))
		g_value_unset (dst);
}

void
nm_vpn_plugin_set_config (NMVPNPlugin *plugin,
                          GHashTable *config)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);
	GValue *val;

	g_return_if_fail (NM_IS_VPN_PLUGIN (plugin));
	g_return_if_fail (config != NULL);

	priv->got_config = TRUE;

	val = g_hash_table_lookup (config, NM_VPN_PLUGIN_CONFIG_HAS_IP4);
	if (val && g_value_get_boolean (val))
		priv->has_ip4 = TRUE;
	val = g_hash_table_lookup (config, NM_VPN_PLUGIN_CONFIG_HAS_IP6);
	if (val && g_value_get_boolean (val))
		priv->has_ip6 = TRUE;

	/* Record the items that need to also be inserted into the
	 * ip4config, for compatibility with older daemons.
	 */
	_g_value_set (&priv->banner, g_hash_table_lookup (config, NM_VPN_PLUGIN_CONFIG_BANNER));
	_g_value_set (&priv->tundev, g_hash_table_lookup (config, NM_VPN_PLUGIN_CONFIG_TUNDEV));
	_g_value_set (&priv->gateway, g_hash_table_lookup (config, NM_VPN_PLUGIN_CONFIG_EXT_GATEWAY));
	_g_value_set (&priv->mtu, g_hash_table_lookup (config, NM_VPN_PLUGIN_CONFIG_MTU));

	g_signal_emit (plugin, signals[CONFIG], 0, config);
}

void
nm_vpn_plugin_set_ip4_config (NMVPNPlugin *plugin,
                              GHashTable *ip4_config)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);
	GHashTable *combined_config;
	GHashTableIter iter;
	gpointer key, value;

	g_return_if_fail (NM_IS_VPN_PLUGIN (plugin));
	g_return_if_fail (ip4_config != NULL);

	priv->got_ip4 = TRUE;

	/* Old plugins won't send the "config" signal and thus can't send
	 * NM_VPN_PLUGIN_CONFIG_HAS_IP4 either.  But since they don't support IPv6,
	 * we can safely assume that, if we don't receive a "config" signal but do
	 * receive an "ip4-config" signal, the old plugin supports IPv4.
	 */
	if (!priv->got_config)
		priv->has_ip4 = TRUE;

	/* Older NetworkManager daemons expect all config info to be in
	 * the ip4 config, so they won't even notice the "config" signal
	 * being emitted. So just copy all of that data into the ip4
	 * config too.
	 */
	combined_config = g_hash_table_new (g_str_hash, g_str_equal);
	g_hash_table_iter_init (&iter, ip4_config);
	while (g_hash_table_iter_next (&iter, &key, &value))
		g_hash_table_insert (combined_config, key, value);

	if (G_VALUE_TYPE (&priv->banner) != G_TYPE_INVALID)
		g_hash_table_insert (combined_config, NM_VPN_PLUGIN_IP4_CONFIG_BANNER, &priv->banner);
	if (G_VALUE_TYPE (&priv->tundev) != G_TYPE_INVALID)
		g_hash_table_insert (combined_config, NM_VPN_PLUGIN_IP4_CONFIG_TUNDEV, &priv->tundev);
	if (G_VALUE_TYPE (&priv->gateway) != G_TYPE_INVALID)
		g_hash_table_insert (combined_config, NM_VPN_PLUGIN_IP4_CONFIG_EXT_GATEWAY, &priv->gateway);
	if (G_VALUE_TYPE (&priv->mtu) != G_TYPE_INVALID)
		g_hash_table_insert (combined_config, NM_VPN_PLUGIN_IP4_CONFIG_MTU, &priv->mtu);

	g_signal_emit (plugin, signals[IP4_CONFIG], 0, combined_config);
	g_hash_table_destroy (combined_config);

	if (   priv->has_ip4 == priv->got_ip4
	    && priv->has_ip6 == priv->got_ip6)
		nm_vpn_plugin_set_state (plugin, NM_VPN_SERVICE_STATE_STARTED);
}

void
nm_vpn_plugin_set_ip6_config (NMVPNPlugin *plugin,
                              GHashTable *ip6_config)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);

	g_return_if_fail (NM_IS_VPN_PLUGIN (plugin));
	g_return_if_fail (ip6_config != NULL);

	priv->got_ip6 = TRUE;
	g_signal_emit (plugin, signals[IP6_CONFIG], 0, ip6_config);

	if (   priv->has_ip4 == priv->got_ip4
	    && priv->has_ip6 == priv->got_ip6)
		nm_vpn_plugin_set_state (plugin, NM_VPN_SERVICE_STATE_STARTED);
}

static void
connect_timer_start (NMVPNPlugin *plugin)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);

	nm_clear_g_source (&priv->connect_timer);
	priv->connect_timer = g_timeout_add_seconds (60, connect_timer_expired, plugin);
}

static gboolean
_connect_generic (NMVPNPlugin *plugin,
                  GHashTable *properties,
                  GHashTable *details,
                  GError **error)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);
	NMVPNPluginClass *vpn_class = NM_VPN_PLUGIN_GET_CLASS (plugin);
	NMConnection *connection;
	gboolean success = FALSE;
	GError *local = NULL;
	guint fail_stop_timeout = 0;

	if (priv->state != NM_VPN_SERVICE_STATE_STOPPED &&
	    priv->state != NM_VPN_SERVICE_STATE_INIT) {
		g_set_error (error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_WRONG_STATE,
		             "Could not start connection: wrong plugin state %d",
		             priv->state);
		return FALSE;
	}

	connection = _nm_connection_new_from_hash (properties);

	priv->interactive = FALSE;
	if (details && !vpn_class->connect_interactive) {
		g_set_error_literal (error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_INTERACTIVE_NOT_SUPPORTED,
		                     "Plugin does not implement ConnectInteractive()");
		return FALSE;
	}

	nm_clear_g_source (&priv->fail_stop_id);

	if (details) {
		priv->interactive = TRUE;
		success = vpn_class->connect_interactive (plugin, connection, details, &local);
		if (g_error_matches (local, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_INTERACTIVE_NOT_SUPPORTED)) {
			/* Give NetworkManager a bit of time to fall back to Connect() */
			fail_stop_timeout = 5;
		}
		g_propagate_error (error, local);
	} else
		success = vpn_class->connect (plugin, connection, error);

	if (success) {
		nm_vpn_plugin_set_state (plugin, NM_VPN_SERVICE_STATE_STARTING);

		/* Add a timer to make sure we do not wait indefinitely for the successful connect. */
		connect_timer_start (plugin);
	} else {
		/* Stop the plugin from an idle handler so that the Connect
		 * method return gets sent before the STOP StateChanged signal.
		 */
		schedule_fail_stop (plugin, fail_stop_timeout);
	}

	g_object_unref (connection);
	return success;
}

static gboolean
impl_vpn_plugin_connect (NMVPNPlugin *plugin,
                         GHashTable *connection,
                         GError **error)
{
	return _connect_generic (plugin, connection, NULL, error);
}

static gboolean
impl_vpn_plugin_connect_interactive (NMVPNPlugin *plugin,
                                     GHashTable *connection,
                                     GHashTable *details,
                                     GError **error)
{
	return _connect_generic (plugin, connection, details, error);
}

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

static gboolean
impl_vpn_plugin_need_secrets (NMVPNPlugin *plugin,
                              GHashTable *properties,
                              char **setting_name,
                              GError **err)
{
	gboolean ret = FALSE;
	NMConnection *connection;
	char *sn = NULL;
	GError *ns_err = NULL;
	gboolean needed = FALSE;

	g_return_val_if_fail (NM_IS_VPN_PLUGIN (plugin), FALSE);
	g_return_val_if_fail (properties != NULL, FALSE);

	connection = _nm_connection_new_from_hash (properties);

	if (!NM_VPN_PLUGIN_GET_CLASS (plugin)->need_secrets) {
		*setting_name = "";
		ret = TRUE;
		goto out;
	}

	needed = NM_VPN_PLUGIN_GET_CLASS (plugin)->need_secrets (plugin, connection, &sn, &ns_err);
	if (ns_err) {
		*err = g_error_copy (ns_err);
		g_error_free (ns_err);
		goto out;
	}

	ret = TRUE;
	if (needed) {
		/* Push back the quit timer so the VPN plugin doesn't quit in the
		 * middle of asking the user for secrets.
		 */
		schedule_quit_timer (plugin);

		g_assert (sn);
		*setting_name = g_strdup (sn);
	} else {
		/* No secrets required */
		*setting_name = g_strdup ("");
	}

out:
	return ret;
}

static gboolean
impl_vpn_plugin_new_secrets (NMVPNPlugin *plugin,
                             GHashTable *properties,
                             GError **error)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);
	NMConnection *connection;
	gboolean success;

	if (priv->state != NM_VPN_SERVICE_STATE_STARTING) {
		g_set_error (error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_WRONG_STATE,
		             "Could not accept new secrets: wrong plugin state %d",
		             priv->state);
		return FALSE;
	}

	connection = _nm_connection_new_from_hash (properties);

	if (!NM_VPN_PLUGIN_GET_CLASS (plugin)->new_secrets) {
		g_set_error_literal (error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_INTERACTIVE_NOT_SUPPORTED,
		                     "Could not accept new secrets: plugin cannot process interactive secrets");
		g_object_unref (connection);
		return FALSE;
	}

	success = NM_VPN_PLUGIN_GET_CLASS (plugin)->new_secrets (plugin, connection, error);
	if (success) {
		/* Add a timer to make sure we do not wait indefinitely for the successful connect. */
		connect_timer_start (plugin);
	} else {
		/* Stop the plugin from and idle handler so that the NewSecrets
		 * method return gets sent before the STOP StateChanged signal.
		 */
		schedule_fail_stop (plugin, 0);
	}

	g_object_unref (connection);
	return success;
}

/**
 * nm_vpn_plugin_secrets_required:
 * @plugin: the #NMVPNPlugin
 * @message: an information message about why secrets are required, if any
 * @hints: VPN specific secret names for required new secrets
 *
 * Called by VPN plugin implementations to signal to NetworkManager that secrets
 * are required during the connection process.  This signal may be used to
 * request new secrets when the secrets originally provided by NetworkManager
 * are insufficient, or the VPN process indicates that it needs additional
 * information to complete the request.
 *
 * Since: 0.9.10
 */
void
nm_vpn_plugin_secrets_required (NMVPNPlugin *plugin,
                                const char *message,
                                const char **hints)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);

	/* Plugin must be able to accept the new secrets if it calls this method */
	g_return_if_fail (NM_VPN_PLUGIN_GET_CLASS (plugin)->new_secrets);

	/* Plugin cannot call this method if NetworkManager didn't originally call
	 * ConnectInteractive().
	 */
	g_return_if_fail (priv->interactive == TRUE);

	/* Cancel the connect timer since secrets might take a while.  It'll
	 * get restarted when the secrets come back via NewSecrets().
	 */
	nm_clear_g_source (&priv->connect_timer);

	g_signal_emit (plugin, signals[SECRETS_REQUIRED], 0, message, hints);
}

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

static gboolean
impl_vpn_plugin_disconnect (NMVPNPlugin *plugin,
                            GError **err)
{
	return nm_vpn_plugin_disconnect (plugin, err);
}

static gboolean
impl_vpn_plugin_set_config (NMVPNPlugin *plugin,
                            GHashTable *config,
                            GError **err)
{
	nm_vpn_plugin_set_config (plugin, config);

	return TRUE;
}

static gboolean
impl_vpn_plugin_set_ip4_config (NMVPNPlugin *plugin,
                                GHashTable *config,
                                GError **err)
{
	nm_vpn_plugin_set_ip4_config (plugin, config);

	return TRUE;
}

static gboolean
impl_vpn_plugin_set_ip6_config (NMVPNPlugin *plugin,
                                GHashTable *config,
                                GError **err)
{
	nm_vpn_plugin_set_ip6_config (plugin, config);

	return TRUE;
}

static gboolean
impl_vpn_plugin_set_failure (NMVPNPlugin *plugin,
                             char *reason,
                             GError **err)
{
	nm_vpn_plugin_failure (plugin, NM_VPN_PLUGIN_FAILURE_BAD_IP_CONFIG);

	return TRUE;
}

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

static void
sigterm_handler (int signum)
{
	g_slist_foreach (active_plugins, (GFunc) nm_vpn_plugin_emit_quit, NULL);
}

static void
setup_unix_signal_handler (void)
{
	struct sigaction action;
	sigset_t block_mask;

	action.sa_handler = sigterm_handler;
	sigemptyset (&block_mask);
	action.sa_mask = block_mask;
	action.sa_flags = 0;
	sigaction (SIGINT, &action, NULL);
	sigaction (SIGTERM, &action, NULL);
}

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

static void
one_plugin_destroyed (gpointer data,
                      GObject *object)
{
	active_plugins = g_slist_remove (active_plugins, object);
}

static void
nm_vpn_plugin_init (NMVPNPlugin *plugin)
{
	active_plugins = g_slist_append (active_plugins, plugin);
	g_object_weak_ref (G_OBJECT (plugin),
	                   one_plugin_destroyed,
	                   NULL);
}

static GObject *
constructor (GType type,
             guint n_construct_params,
             GObjectConstructParam *construct_params)
{
	GObject *object;
	NMVPNPlugin *plugin;
	NMVPNPluginPrivate *priv;
	DBusGConnection *connection;
	DBusGProxy *proxy;
	guint request_name_result;
	GError *err = NULL;

	object = G_OBJECT_CLASS (nm_vpn_plugin_parent_class)->constructor (type,
	                                                                   n_construct_params,
	                                                                   construct_params);
	if (!object)
		return NULL;

	priv = NM_VPN_PLUGIN_GET_PRIVATE (object);
	if (!priv->dbus_service_name)
		goto err;

	connection = dbus_g_bus_get (DBUS_BUS_SYSTEM, &err);
	if (!connection)
		goto err;

	proxy = dbus_g_proxy_new_for_name (connection,
	                                   DBUS_SERVICE_DBUS,
	                                   DBUS_PATH_DBUS,
	                                   DBUS_INTERFACE_DBUS);

	if (!dbus_g_proxy_call (proxy, "RequestName", &err,
	                        G_TYPE_STRING, priv->dbus_service_name,
	                        G_TYPE_UINT, 0,
	                        G_TYPE_INVALID,
	                        G_TYPE_UINT, &request_name_result,
	                        G_TYPE_INVALID)) {
		g_object_unref (proxy);
		goto err;
	}

	g_object_unref (proxy);

	dbus_g_connection_register_g_object (connection,
	                                     NM_VPN_DBUS_PLUGIN_PATH,
	                                     object);

	plugin = NM_VPN_PLUGIN (object);

	nm_vpn_plugin_set_connection (plugin, connection);
	nm_vpn_plugin_set_state (plugin, NM_VPN_SERVICE_STATE_INIT);

	return object;

 err:
	if (err) {
		g_warning ("Failed to initialize VPN plugin: %s", err->message);
		g_error_free (err);
	}

	if (object)
		g_object_unref (object);

	return NULL;
}

static void
set_property (GObject *object, guint prop_id,
              const GValue *value, GParamSpec *pspec)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (object);

	switch (prop_id) {
	case PROP_DBUS_SERVICE_NAME:
		/* Construct-only */
		priv->dbus_service_name = g_strdup (g_value_get_string (value));
		break;
	case PROP_STATE:
		nm_vpn_plugin_set_state (NM_VPN_PLUGIN (object),
		                         (NMVPNServiceState) g_value_get_uint (value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
get_property (GObject *object, guint prop_id,
              GValue *value, GParamSpec *pspec)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (object);

	switch (prop_id) {
	case PROP_DBUS_SERVICE_NAME:
		g_value_set_string (value, priv->dbus_service_name);
		break;
	case PROP_STATE:
		g_value_set_uint (value, nm_vpn_plugin_get_state (NM_VPN_PLUGIN (object)));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
dispose (GObject *object)
{
	NMVPNPlugin *plugin = NM_VPN_PLUGIN (object);
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);
	NMVPNServiceState state;
	GError *err = NULL;

	nm_clear_g_source (&priv->fail_stop_id);
	nm_clear_g_source (&priv->quit_timer);
	nm_clear_g_source (&priv->connect_timer);

	state = nm_vpn_plugin_get_state (plugin);

	if (state == NM_VPN_SERVICE_STATE_STARTED ||
	    state == NM_VPN_SERVICE_STATE_STARTING)
		nm_vpn_plugin_disconnect (plugin, &err);

	if (err) {
		g_warning ("Error disconnecting VPN connection: %s", err->message);
		g_error_free (err);
	}

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

static void
finalize (GObject *object)
{
	NMVPNPlugin *plugin = NM_VPN_PLUGIN (object);
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);

	nm_vpn_plugin_set_connection (plugin, NULL);
	g_free (priv->dbus_service_name);

	if (G_IS_VALUE (&priv->banner))
		g_value_unset (&priv->banner);
	if (G_IS_VALUE (&priv->tundev))
		g_value_unset (&priv->tundev);
	if (G_IS_VALUE (&priv->gateway))
		g_value_unset (&priv->gateway);
	if (G_IS_VALUE (&priv->mtu))
		g_value_unset (&priv->mtu);

	G_OBJECT_CLASS (nm_vpn_plugin_parent_class)->finalize (object);
}

static void
state_changed (NMVPNPlugin *plugin, NMVPNServiceState state)
{
	NMVPNPluginPrivate *priv = NM_VPN_PLUGIN_GET_PRIVATE (plugin);

	switch (state) {
	case NM_VPN_SERVICE_STATE_STARTING:
		nm_clear_g_source (&priv->quit_timer);
		nm_clear_g_source (&priv->fail_stop_id);
		break;
	case NM_VPN_SERVICE_STATE_STOPPED:
		schedule_quit_timer (plugin);
		break;
	default:
		/* Clean up all timers we might have set up. */
		nm_clear_g_source (&priv->connect_timer);
		nm_clear_g_source (&priv->quit_timer);
		nm_clear_g_source (&priv->fail_stop_id);
		break;
	}
}

static void
nm_vpn_plugin_class_init (NMVPNPluginClass *plugin_class)
{
	GObjectClass *object_class = G_OBJECT_CLASS (plugin_class);

	g_type_class_add_private (object_class, sizeof (NMVPNPluginPrivate));

	dbus_g_object_type_install_info (G_TYPE_FROM_CLASS (plugin_class),
	                                 &dbus_glib_nm_vpn_plugin_object_info);

	/* virtual methods */
	object_class->constructor  = constructor;
	object_class->set_property = set_property;
	object_class->get_property = get_property;
	object_class->dispose      = dispose;
	object_class->finalize     = finalize;

	plugin_class->state_changed = state_changed;

	/* properties */

	/**
	 * NMVPNPlugin:service-name:
	 *
	 * The D-Bus service name of this plugin.
	 */
	g_object_class_install_property
		(object_class, PROP_DBUS_SERVICE_NAME,
		 g_param_spec_string (NM_VPN_PLUGIN_DBUS_SERVICE_NAME, "", "",
		                      NULL,
		                      G_PARAM_READWRITE |
		                      G_PARAM_CONSTRUCT_ONLY |
		                      G_PARAM_STATIC_STRINGS));

	/**
	 * NMVPNPlugin:state:
	 *
	 * The state of the plugin.
	 */
	g_object_class_install_property
		(object_class, PROP_STATE,
		 g_param_spec_uint (NM_VPN_PLUGIN_STATE, "", "",
		                    NM_VPN_SERVICE_STATE_UNKNOWN,
		                    NM_VPN_SERVICE_STATE_STOPPED,
		                    NM_VPN_SERVICE_STATE_INIT,
		                    G_PARAM_READWRITE |
		                    G_PARAM_STATIC_STRINGS));

	/* signals */
	signals[STATE_CHANGED] =
		g_signal_new ("state-changed",
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              G_STRUCT_OFFSET (NMVPNPluginClass, state_changed),
		              NULL, NULL,
		              g_cclosure_marshal_VOID__UINT,
		              G_TYPE_NONE, 1,
		              G_TYPE_UINT);

	signals[SECRETS_REQUIRED] =
		g_signal_new ("secrets-required",
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              0, NULL, NULL,
		              NULL,
		              G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRV);

	signals[CONFIG] =
		g_signal_new ("config",
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              G_STRUCT_OFFSET (NMVPNPluginClass, config),
		              NULL, NULL,
		              g_cclosure_marshal_VOID__BOXED,
		              G_TYPE_NONE, 1,
		              DBUS_TYPE_G_MAP_OF_VARIANT);

	signals[IP4_CONFIG] =
		g_signal_new ("ip4-config",
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              G_STRUCT_OFFSET (NMVPNPluginClass, ip4_config),
		              NULL, NULL,
		              g_cclosure_marshal_VOID__BOXED,
		              G_TYPE_NONE, 1,
		              DBUS_TYPE_G_MAP_OF_VARIANT);

	signals[IP6_CONFIG] =
		g_signal_new ("ip6-config",
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              G_STRUCT_OFFSET (NMVPNPluginClass, ip6_config),
		              NULL, NULL,
		              g_cclosure_marshal_VOID__BOXED,
		              G_TYPE_NONE, 1,
		              DBUS_TYPE_G_MAP_OF_VARIANT);

	signals[LOGIN_BANNER] =
		g_signal_new ("login-banner",
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              G_STRUCT_OFFSET (NMVPNPluginClass, login_banner),
		              NULL, NULL,
		              g_cclosure_marshal_VOID__STRING,
		              G_TYPE_NONE, 1,
		              G_TYPE_STRING);

	signals[FAILURE] =
		g_signal_new ("failure",
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              G_STRUCT_OFFSET (NMVPNPluginClass, failure),
		              NULL, NULL,
		              g_cclosure_marshal_VOID__UINT,
		              G_TYPE_NONE, 1,
		              G_TYPE_UINT);

	signals[QUIT] =
		g_signal_new ("quit",
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              G_STRUCT_OFFSET (NMVPNPluginClass, quit),
		              NULL, NULL,
		              g_cclosure_marshal_VOID__VOID,
		              G_TYPE_NONE, 0,
		              G_TYPE_NONE);

	dbus_g_error_domain_register (NM_VPN_PLUGIN_ERROR,
	                              NM_DBUS_VPN_ERROR_PREFIX,
	                              NM_TYPE_VPN_PLUGIN_ERROR);

	setup_unix_signal_handler ();
}
