/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2014  Intel Corporation. All rights reserved.
 *  Copyright (C) 2011-2014  BMW Car IT GmbH.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>

#include <gdbus.h>

#include "connman.h"

static DBusConnection *connection;
static GHashTable *session_hash;
static GHashTable *service_hash;
static struct connman_session *ecall_session;
static uint32_t session_mark = 256;

struct session_info {
	struct connman_session_config config;
	enum connman_session_state state;
};

struct connman_session {
	char *owner;
	char *session_path;
	char *notify_path;
	guint notify_watch;

	bool active;
	bool append_all;
	struct session_info *info;
	struct session_info *info_last;
	struct connman_service *service;
	struct connman_service *service_last;
	struct connman_session_config *policy_config;
	GSList *user_allowed_bearers;
	char *user_allowed_interface;

	bool ecall;

	enum connman_session_id_type id_type;
	struct firewall_context *fw;
	uint32_t mark;
	int index;
	char *addr;
	char *gateway;
	unsigned char prefixlen;
	bool policy_routing;
	bool snat_enabled;
};

struct connman_service_info {
	struct connman_service *service;
	GSList *sessions;
};

struct fw_snat {
	GSList *sessions;
	int id;
	int index;
	char *addr;
	struct firewall_context *fw;
};

GSList *fw_snat_list;

static struct connman_session_policy *policy;
static void session_activate(struct connman_session *session);
static void session_deactivate(struct connman_session *session);
static void update_session_state(struct connman_session *session);

static void cleanup_service(gpointer data)
{
	struct connman_service_info *info = data;

	g_slist_free(info->sessions);
	g_free(info);
}

static const char *state2string(enum connman_session_state state)
{
	switch (state) {
	case CONNMAN_SESSION_STATE_DISCONNECTED:
		return "disconnected";
	case CONNMAN_SESSION_STATE_CONNECTED:
		return "connected";
	case CONNMAN_SESSION_STATE_ONLINE:
		return "online";
	}

	return NULL;
}

static const char *type2string(enum connman_session_type type)
{
	switch (type) {
	case CONNMAN_SESSION_TYPE_UNKNOWN:
		return "";
	case CONNMAN_SESSION_TYPE_ANY:
		return "any";
	case CONNMAN_SESSION_TYPE_LOCAL:
		return "local";
	case CONNMAN_SESSION_TYPE_INTERNET:
		return "internet";
	}

	return NULL;
}

enum connman_session_roaming_policy connman_session_parse_roaming_policy(const char *policy)
{
	if (g_strcmp0(policy, "default") == 0)
		return CONNMAN_SESSION_ROAMING_POLICY_DEFAULT;
	else if (g_strcmp0(policy, "always") == 0)
		return CONNMAN_SESSION_ROAMING_POLICY_ALWAYS;
	else if (g_strcmp0(policy, "forbidden") == 0)
		return CONNMAN_SESSION_ROAMING_POLICY_FORBIDDEN;
	else if (g_strcmp0(policy, "national") == 0)
		return CONNMAN_SESSION_ROAMING_POLICY_NATIONAL;
	else if (g_strcmp0(policy, "international") == 0)
		return CONNMAN_SESSION_ROAMING_POLICY_INTERNATIONAL;
	else
		return CONNMAN_SESSION_ROAMING_POLICY_UNKNOWN;
}

enum connman_session_type connman_session_parse_connection_type(const char *type)
{
	if (g_strcmp0(type, "any") == 0)
		return CONNMAN_SESSION_TYPE_ANY;
	if (g_strcmp0(type, "local") == 0)
		return CONNMAN_SESSION_TYPE_LOCAL;
	else if (g_strcmp0(type, "internet") == 0)
		return CONNMAN_SESSION_TYPE_INTERNET;

	return CONNMAN_SESSION_TYPE_UNKNOWN;
}

static int bearer2service(const char *bearer, enum connman_service_type *type)
{
	if (g_strcmp0(bearer, "ethernet") == 0)
		*type = CONNMAN_SERVICE_TYPE_ETHERNET;
	else if (g_strcmp0(bearer, "wifi") == 0)
		*type = CONNMAN_SERVICE_TYPE_WIFI;
	else if (g_strcmp0(bearer, "gadget") == 0)
		*type = CONNMAN_SERVICE_TYPE_GADGET;
	else if (g_strcmp0(bearer, "bluetooth") == 0)
		*type = CONNMAN_SERVICE_TYPE_BLUETOOTH;
	else if (g_strcmp0(bearer, "cellular") == 0)
		*type = CONNMAN_SERVICE_TYPE_CELLULAR;
	else if (g_strcmp0(bearer, "vpn") == 0)
		*type = CONNMAN_SERVICE_TYPE_VPN;
	else
		return -EINVAL;

	return 0;
}

static char *service2bearer(enum connman_service_type type)
{
	switch (type) {
	case CONNMAN_SERVICE_TYPE_ETHERNET:
		return "ethernet";
	case CONNMAN_SERVICE_TYPE_GADGET:
		return "gadget";
	case CONNMAN_SERVICE_TYPE_WIFI:
		return "wifi";
	case CONNMAN_SERVICE_TYPE_BLUETOOTH:
		return "bluetooth";
	case CONNMAN_SERVICE_TYPE_CELLULAR:
		return "cellular";
	case CONNMAN_SERVICE_TYPE_VPN:
		return "vpn";
	case CONNMAN_SERVICE_TYPE_SYSTEM:
	case CONNMAN_SERVICE_TYPE_GPS:
	case CONNMAN_SERVICE_TYPE_P2P:
	case CONNMAN_SERVICE_TYPE_UNKNOWN:
		return "";
	}

	return "";
}

static struct fw_snat *fw_snat_lookup(int index, const char *addr)
{
	struct fw_snat *fw_snat;
	GSList *list;

	for (list = fw_snat_list; list; list = list->next) {
		fw_snat = list->data;

		if (fw_snat->index == index) {
			if (g_strcmp0(addr, fw_snat->addr) != 0)
				continue;
			return fw_snat;
		}
	}
	return NULL;
}

static int fw_snat_create(struct connman_session *session,
				int index, const char *ifname, const char *addr)
{
	struct fw_snat *fw_snat;
	int err;

	fw_snat = g_new0(struct fw_snat, 1);

	fw_snat->fw = __connman_firewall_create();
	fw_snat->index = index;
	fw_snat->addr = g_strdup(addr);

	fw_snat->id = __connman_firewall_enable_snat(fw_snat->fw,
						index, ifname, addr);
	if (fw_snat->id < 0) {
		err = fw_snat->id;
		goto err;
	}

	fw_snat_list = g_slist_prepend(fw_snat_list, fw_snat);
	fw_snat->sessions = g_slist_prepend(fw_snat->sessions, session);

	return 0;
err:
	__connman_firewall_destroy(fw_snat->fw);
	g_free(fw_snat->addr);
	g_free(fw_snat);
	return err;
}

static void fw_snat_ref(struct connman_session *session,
				struct fw_snat *fw_snat)
{
	if (g_slist_find(fw_snat->sessions, session))
		return;
	fw_snat->sessions = g_slist_prepend(fw_snat->sessions, session);
}

static void fw_snat_unref(struct connman_session *session,
				struct fw_snat *fw_snat)
{
	fw_snat->sessions = g_slist_remove(fw_snat->sessions, session);
	if (fw_snat->sessions)
		return;

	fw_snat_list = g_slist_remove(fw_snat_list, fw_snat);

	__connman_firewall_disable_snat(fw_snat->fw);
	__connman_firewall_destroy(fw_snat->fw);
	g_free(fw_snat);
}

static int init_firewall_session(struct connman_session *session)
{
	struct firewall_context *fw;
	int err;
	struct connman_ipconfig *ipconfig = NULL;
	const char *addr = NULL;

	if (session->policy_config->id_type == CONNMAN_SESSION_ID_TYPE_UNKNOWN &&
			!session->info->config.source_ip_rule)
		return 0;

	DBG("");

	if (session->info->config.source_ip_rule) {
		ipconfig = __connman_service_get_ip4config(session->service);
		if (session->policy_config->id_type == CONNMAN_SESSION_ID_TYPE_UNKNOWN && !ipconfig)
			return 0;
	}

	fw = __connman_firewall_create();
	if (!fw)
		return -ENOMEM;

	if (session->info->config.source_ip_rule && ipconfig) {
		addr = __connman_ipconfig_get_local(ipconfig);
	}

	err =__connman_firewall_enable_marking(fw,
					session->policy_config->id_type,
					session->policy_config->id,
					addr, session->mark);
	if (err < 0) {
		__connman_firewall_destroy(fw);
		return err;
	}
	session->id_type = session->policy_config->id_type;
	session->fw = fw;

	return 0;
}

static void cleanup_firewall_session(struct connman_session *session)
{
	if (!session->fw)
		return;

	__connman_firewall_disable_marking(session->fw);
	__connman_firewall_disable_snat(session->fw);
	__connman_firewall_destroy(session->fw);

	session->fw = NULL;
}

static int init_routing_table(struct connman_session *session)
{
	int err;

	if (session->policy_config->id_type == CONNMAN_SESSION_ID_TYPE_UNKNOWN &&
			!session->info->config.source_ip_rule)
		return 0;

	if (!session->service)
		return 0;

	DBG("");

	err = __connman_inet_add_fwmark_rule(session->mark,
						AF_INET, session->mark);
	if (err < 0)
		return err;

	err = __connman_inet_add_fwmark_rule(session->mark,
						AF_INET6, session->mark);
	if (err < 0)
		__connman_inet_del_fwmark_rule(session->mark,
						AF_INET, session->mark);
	session->policy_routing = true;

	return err;
}

static void del_default_route(struct connman_session *session)
{
	if (!session->gateway)
		return;

	DBG("index %d routing table %d default gateway %s/%u",
		session->index, session->mark, session->gateway, session->prefixlen);

	__connman_inet_del_subnet_from_table(session->mark,
		session->index, session->gateway, session->prefixlen);

	__connman_inet_del_default_from_table(session->mark,
					session->index, session->gateway);
	g_free(session->gateway);
	session->gateway = NULL;
	session->prefixlen = 0;
	session->index = -1;
}

static void add_default_route(struct connman_session *session)
{
	struct connman_ipconfig *ipconfig;
	int err;
	struct in_addr addr = { INADDR_ANY };

	if (!session->service)
		return;

	ipconfig = __connman_service_get_ip4config(session->service);
	session->index = __connman_ipconfig_get_index(ipconfig);
	session->gateway = g_strdup(__connman_ipconfig_get_gateway(ipconfig));

	if (!session->gateway)
		session->gateway = g_strdup(inet_ntoa(addr));

	session->prefixlen = __connman_ipconfig_get_prefixlen(ipconfig);

	DBG("index %d routing table %d default gateway %s/%u",
		session->index, session->mark, session->gateway, session->prefixlen);

	err = __connman_inet_add_default_to_table(session->mark,
					session->index, session->gateway);
	if (err < 0)
		DBG("session %p %s", session, strerror(-err));

	err = __connman_inet_add_subnet_to_table(session->mark,
					session->index, session->gateway, session->prefixlen);
	if (err < 0)
		DBG("session add subnet route %p %s", session, strerror(-err));
}

static void del_nat_rules(struct connman_session *session)
{
	struct fw_snat *fw_snat;

	if (!session->snat_enabled)
		return;

	session->snat_enabled = false;
	fw_snat = fw_snat_lookup(session->index, session->addr);

	if (!fw_snat)
		return;

	fw_snat_unref(session, fw_snat);
}

static void add_nat_rules(struct connman_session *session)
{
	struct connman_ipconfig *ipconfig;
	struct fw_snat *fw_snat;
	const char *addr;
	int index, err;
	char *ifname;

	if (!session->service)
		return;

	ipconfig = __connman_service_get_ip4config(session->service);
	index = __connman_ipconfig_get_index(ipconfig);
	ifname = connman_inet_ifname(index);
	addr = __connman_ipconfig_get_local(ipconfig);

	if (!addr)
		return;

	g_free(session->addr);
	session->addr = g_strdup(addr);

	session->snat_enabled = true;
	fw_snat = fw_snat_lookup(index, session->addr);
	if (fw_snat) {
		fw_snat_ref(session, fw_snat);
		return;
	}

	err = fw_snat_create(session, index, ifname, addr);
	if (err < 0) {
		DBG("failed to add SNAT rule");
		session->snat_enabled = false;
	}

	g_free(ifname);
}

uint32_t connman_session_firewall_get_fwmark(struct connman_session *session)
{
	return session->mark;
}

static void cleanup_routing_table(struct connman_session *session)
{
	DBG("");

	if (session->policy_routing) {
		__connman_inet_del_fwmark_rule(session->mark,
					AF_INET6, session->mark);

		__connman_inet_del_fwmark_rule(session->mark,
					AF_INET, session->mark);
		session->policy_routing = false;
	}

	del_default_route(session);
}

static void update_firewall(struct connman_session *session)
{
	cleanup_firewall_session(session);
	init_firewall_session(session);
}

static void update_routing_table(struct connman_session *session)
{
	cleanup_routing_table(session);
	init_routing_table(session);
	add_default_route(session);
}

static void cleanup_nat_rules(struct connman_session *session)
{
	del_nat_rules(session);
}

static void destroy_policy_config(struct connman_session *session)
{
	if (!policy) {
		g_free(session->policy_config);
		return;
	}

	policy->destroy(session);
}

static void free_session(struct connman_session *session)
{
	if (!session)
		return;

	if (session->notify_watch > 0)
		g_dbus_remove_watch(connection, session->notify_watch);

	g_dbus_unregister_interface(connection, session->session_path,
				    CONNMAN_SESSION_INTERFACE);

	destroy_policy_config(session);
	g_slist_free(session->info->config.allowed_bearers);
	g_free(session->info->config.allowed_interface);
	g_free(session->owner);
	g_free(session->session_path);
	g_free(session->notify_path);
	g_free(session->info);
	g_free(session->info_last);
	g_free(session->gateway);
	g_free(session->addr);

	g_free(session);
}

static void set_active_session(struct connman_session *session, bool enable)
{

	if (policy && policy->session_changed)
		policy->session_changed(session, enable,
					session->info->config.allowed_bearers);

	__connman_service_set_active_session(enable,
				session->info->config.allowed_bearers);
}

static void cleanup_session(gpointer user_data)
{
	struct connman_session *session = user_data;

	DBG("remove %s", session->session_path);

	cleanup_nat_rules(session);
	cleanup_routing_table(session);
	cleanup_firewall_session(session);

	if (session->active)
		set_active_session(session, false);

	session_deactivate(session);
	update_session_state(session);

	g_slist_free(session->user_allowed_bearers);
	g_free(session->user_allowed_interface);

	free_session(session);
}

struct creation_data {
	DBusMessage *pending;
	struct connman_session *session;

	/* user config */
	enum connman_session_type type;
	GSList *allowed_bearers;
	char *allowed_interface;
	bool source_ip_rule;
	char *context_identifier;
};

static void cleanup_creation_data(struct creation_data *creation_data)
{
	if (!creation_data)
		return;

	if (creation_data->pending)
		dbus_message_unref(creation_data->pending);
	if (creation_data->context_identifier)
		g_free(creation_data->context_identifier);

	g_slist_free(creation_data->allowed_bearers);
	g_free(creation_data->allowed_interface);
	g_free(creation_data);
}

static int create_policy_config(struct connman_session *session,
				connman_session_config_func_t cb,
				struct creation_data *creation_data)
{
	struct connman_session_config *config;

	if (!policy) {
		config = connman_session_create_default_config();
		if (!config) {
			free_session(session);
			cleanup_creation_data(creation_data);
			return -ENOMEM;
		}

		return cb(session, config, creation_data, 0);
	}

	return policy->create(session, cb, creation_data);
}

int connman_session_policy_register(struct connman_session_policy *plugin)
{
	if (policy)
		return -EINVAL;

	if (!plugin->create || !plugin->destroy)
		return -EINVAL;

	DBG("name %s", plugin->name);

	policy = plugin;

	return 0;
}

void connman_session_policy_unregister(struct connman_session_policy *plugin)
{
	if (plugin != policy)
		return;

	DBG("name %s", policy->name);

	policy = NULL;
}

static int default_bearers[] = {
	CONNMAN_SERVICE_TYPE_ETHERNET,
	CONNMAN_SERVICE_TYPE_WIFI,
	CONNMAN_SERVICE_TYPE_BLUETOOTH,
	CONNMAN_SERVICE_TYPE_CELLULAR,
	CONNMAN_SERVICE_TYPE_GADGET,
};

static void add_default_bearer_types(GSList **list)
{
	unsigned int i;

	for (i = 0; i < G_N_ELEMENTS(default_bearers); i++)
		*list = g_slist_append(*list,
				GINT_TO_POINTER(default_bearers[i]));
}

void connman_session_set_default_config(struct connman_session_config *config)
{
	config->id_type = CONNMAN_SESSION_ID_TYPE_UNKNOWN;
	g_free(config->id);
	config->id = NULL;

	config->priority = FALSE;
	config->roaming_policy = CONNMAN_SESSION_ROAMING_POLICY_DEFAULT;
	config->type = CONNMAN_SESSION_TYPE_ANY;
	config->ecall = FALSE;

	g_slist_free(config->allowed_bearers);
	config->allowed_bearers = NULL;
	add_default_bearer_types(&config->allowed_bearers);
}

struct connman_session_config *connman_session_create_default_config(void)
{
	struct connman_session_config *config;

	config = g_new0(struct connman_session_config, 1);
	connman_session_set_default_config(config);

	return config;
}

static enum connman_session_type apply_policy_on_type(
			enum connman_session_type policy,
			enum connman_session_type type)
{
	if (type == CONNMAN_SESSION_TYPE_UNKNOWN)
		return CONNMAN_SESSION_TYPE_UNKNOWN;

	if (policy == CONNMAN_SESSION_TYPE_ANY)
		return type;

	if (policy == CONNMAN_SESSION_TYPE_LOCAL)
		return CONNMAN_SESSION_TYPE_LOCAL;

	return CONNMAN_SESSION_TYPE_INTERNET;
}

int connman_session_parse_bearers(const char *token, GSList **list)
{
	enum connman_service_type bearer;
	int err;

	if (g_strcmp0(token, "") == 0)
		return 0;

	if (g_strcmp0(token, "*") == 0) {
		add_default_bearer_types(list);
		return 0;
	}

	err = bearer2service(token, &bearer);
	if (err < 0)
		return err;

	*list = g_slist_append(*list, GINT_TO_POINTER(bearer));

	return 0;
}

static int parse_bearers(DBusMessageIter *iter, GSList **list)
{
	DBusMessageIter array;
	int type, err;

	dbus_message_iter_recurse(iter, &array);

	*list = NULL;

	while ((type = dbus_message_iter_get_arg_type(&array)) !=
			DBUS_TYPE_INVALID) {
		char *bearer_name = NULL;

		if (type != DBUS_TYPE_STRING) {
			g_slist_free(*list);
			*list = NULL;
			return -EINVAL;
		}

		dbus_message_iter_get_basic(&array, &bearer_name);

		err = connman_session_parse_bearers(bearer_name, list);
		if (err < 0) {
			g_slist_free(*list);
			*list = NULL;
			return err;
		}

		dbus_message_iter_next(&array);
	}

	return 0;
}

static void filter_bearer(GSList *bearers,
				enum connman_service_type policy,
				GSList **list)
{
	enum connman_service_type bearer;
	GSList *it;

	if (!bearers)
		return;

	for (it = bearers; it; it = it->next) {
		bearer = GPOINTER_TO_INT(it->data);

		if (policy != bearer)
			continue;

		*list = g_slist_append(*list, GINT_TO_POINTER(bearer));
		return;
	}
}

static void apply_policy_on_bearers(GSList *policy_bearers, GSList *bearers,
				GSList **list)
{
	enum connman_service_type policy_bearer;
	GSList *it;

	*list = NULL;

	for (it = policy_bearers; it; it = it->next) {
		policy_bearer = GPOINTER_TO_INT(it->data);

		filter_bearer(bearers, policy_bearer, list);
	}
}

static char * apply_policy_on_interface(const char *policy_interface,
				const char *user_interface)
{
	if (policy_interface)
		return g_strdup(policy_interface);
	else if (user_interface)
		return g_strdup(user_interface);
	else
		return NULL;
}

const char *connman_session_get_owner(struct connman_session *session)
{
	return session->owner;
}

static void append_allowed_bearers(DBusMessageIter *iter, void *user_data)
{
	struct session_info *info = user_data;
	GSList *list;

	for (list = info->config.allowed_bearers;
			list; list = list->next) {
		enum connman_service_type bearer = GPOINTER_TO_INT(list->data);
		const char *name = __connman_service_type2string(bearer);

		if (!name)
			name = "";

		dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
						&name);
	}
}

static void append_ipconfig_ipv4(DBusMessageIter *iter, void *user_data)
{
	struct connman_service *service = user_data;
	struct connman_ipconfig *ipconfig_ipv4;

	if (!service)
		return;

	if (!__connman_service_is_connected_state(service,
						CONNMAN_IPCONFIG_TYPE_IPV4))
		return;

	ipconfig_ipv4 = __connman_service_get_ip4config(service);
	if (!ipconfig_ipv4)
		return;

	__connman_ipconfig_append_ipv4(ipconfig_ipv4, iter);
}

static void append_ipconfig_ipv6(DBusMessageIter *iter, void *user_data)
{
	struct connman_service *service = user_data;
	struct connman_ipconfig *ipconfig_ipv4, *ipconfig_ipv6;

	if (!service)
		return;

	if (!__connman_service_is_connected_state(service,
						CONNMAN_IPCONFIG_TYPE_IPV6))
		return;

	ipconfig_ipv4 = __connman_service_get_ip4config(service);
	ipconfig_ipv6 = __connman_service_get_ip6config(service);
	if (!ipconfig_ipv6)
		return;

	__connman_ipconfig_append_ipv6(ipconfig_ipv6, iter, ipconfig_ipv4);
}

static void append_notify(DBusMessageIter *dict,
					struct connman_session *session)
{
	struct session_info *info = session->info;
	struct session_info *info_last = session->info_last;
	struct connman_service *service;
	enum connman_service_type type;
	const char *name, *bearer;
	char *ifname;
	int idx;

	if (session->append_all || info->state != info_last->state) {
		const char *state = state2string(info->state);

		connman_dbus_dict_append_basic(dict, "State",
						DBUS_TYPE_STRING,
						&state);
		info_last->state = info->state;
	}

	if (session->append_all || session->service != session->service_last) {
		if (session->service) {
			service = session->service;
			name = __connman_service_get_name(service);
			idx = __connman_service_get_index(service);

			ifname = connman_inet_ifname(idx);
			if (!ifname)
				ifname = g_strdup("");

			type = connman_service_get_type(service);
			bearer = service2bearer(type);
		} else {
			service = NULL;
			name = "";
			ifname = g_strdup("");
			bearer = "";
		}

		connman_dbus_dict_append_basic(dict, "Name",
						DBUS_TYPE_STRING,
						&name);

		connman_dbus_dict_append_dict(dict, "IPv4",
						append_ipconfig_ipv4,
						service);

		connman_dbus_dict_append_dict(dict, "IPv6",
						append_ipconfig_ipv6,
						service);

		connman_dbus_dict_append_basic(dict, "Interface",
						DBUS_TYPE_STRING,
						&ifname);

		connman_dbus_dict_append_basic(dict, "Bearer",
						DBUS_TYPE_STRING,
						&bearer);

		g_free(ifname);

		session->service_last = session->service;
	}

	if (session->append_all ||
			info->config.type != info_last->config.type) {
		const char *type = type2string(info->config.type);

		connman_dbus_dict_append_basic(dict, "ConnectionType",
						DBUS_TYPE_STRING,
						&type);
		info_last->config.type = info->config.type;
	}

	if (session->append_all ||
			info->config.allowed_bearers != info_last->config.allowed_bearers) {
		connman_dbus_dict_append_array(dict, "AllowedBearers",
						DBUS_TYPE_STRING,
						append_allowed_bearers,
						info);
		info_last->config.allowed_bearers = info->config.allowed_bearers;
	}

	if (session->append_all ||
			info->config.allowed_interface != info_last->config.allowed_interface) {
		char *ifname = info->config.allowed_interface;
		if (!ifname)
			ifname = "*";
		connman_dbus_dict_append_basic(dict, "AllowedInterface",
						DBUS_TYPE_STRING,
						&ifname);
		info_last->config.allowed_interface = info->config.allowed_interface;
	}

	if (session->append_all ||
			info->config.context_identifier != info_last->config.context_identifier) {
		char *ifname = info->config.context_identifier;
		if (!ifname)
			ifname = "";
		connman_dbus_dict_append_basic(dict, "ContextIdentifier",
						DBUS_TYPE_STRING,
						&ifname);
		info_last->config.context_identifier = info->config.context_identifier;
	}

	if (session->append_all ||
			info->config.source_ip_rule != info_last->config.source_ip_rule) {
		dbus_bool_t source_ip_rule = FALSE;
		if (info->config.source_ip_rule)
			source_ip_rule = TRUE;
		connman_dbus_dict_append_basic(dict, "SourceIPRule",
						DBUS_TYPE_BOOLEAN,
						&source_ip_rule);
		info_last->config.source_ip_rule = info->config.source_ip_rule;
	}

	session->append_all = false;
}

static bool compute_notifiable_changes(struct connman_session *session)
{
	struct session_info *info_last = session->info_last;
	struct session_info *info = session->info;

	if (session->append_all)
		return true;

	if (info->state != info_last->state)
		return true;

	if (session->service != session->service_last &&
			info->state >= CONNMAN_SESSION_STATE_CONNECTED)
		return true;

	if (info->config.allowed_bearers != info_last->config.allowed_bearers ||
			info->config.type != info_last->config.type ||
			info->config.allowed_interface != info_last->config.allowed_interface ||
			info->config.source_ip_rule != info_last->config.source_ip_rule)
		return true;

	return false;
}

static gboolean session_notify(gpointer user_data)
{
	struct connman_session *session = user_data;
	DBusMessage *msg;
	DBusMessageIter array, dict;

	if (!compute_notifiable_changes(session))
		return FALSE;

	DBG("session %p owner %s notify_path %s", session,
		session->owner, session->notify_path);

	msg = dbus_message_new_method_call(session->owner, session->notify_path,
						CONNMAN_NOTIFICATION_INTERFACE,
						"Update");
	if (!msg)
		return FALSE;

	dbus_message_iter_init_append(msg, &array);
	connman_dbus_dict_open(&array, &dict);

	append_notify(&dict, session);

	connman_dbus_dict_close(&array, &dict);

	g_dbus_send_message(connection, msg);

	return FALSE;
}

static void ipconfig_ipv4_changed(struct connman_session *session)
{
	connman_dbus_setting_changed_dict(session->owner, session->notify_path,
						"IPv4", append_ipconfig_ipv4,
						session->service);
}

static void ipconfig_ipv6_changed(struct connman_session *session)
{
	connman_dbus_setting_changed_dict(session->owner, session->notify_path,
						"IPv6", append_ipconfig_ipv6,
						session->service);
}

int connman_session_config_update(struct connman_session *session)
{
	struct session_info *info = session->info;
	GSList *allowed_bearers;
	char *allowed_interface;
	int err;

	DBG("session %p", session);

	/*
	 * We update all configuration even though only one entry
	 * might have changed. We can still optimize this later.
	 */

	if (session->id_type != session->policy_config->id_type) {
		cleanup_firewall_session(session);
		err = init_firewall_session(session);
		if (err < 0) {
			connman_session_destroy(session);
			return err;
		}

		session->id_type = session->policy_config->id_type;
	}

	apply_policy_on_bearers(
		session->policy_config->allowed_bearers,
		session->user_allowed_bearers,
		&allowed_bearers);

	allowed_interface = apply_policy_on_interface(
		session->policy_config->allowed_interface,
		session->user_allowed_interface);

	if (session->active)
		set_active_session(session, false);

	session->active = false;
	session_deactivate(session);

	g_slist_free(info->config.allowed_bearers);
	info->config.allowed_bearers = allowed_bearers;

	g_free(info->config.allowed_interface);
	info->config.allowed_interface = allowed_interface;

	session_activate(session);

	info->config.type = apply_policy_on_type(
				session->policy_config->type,
				info->config.type);

	info->config.roaming_policy = session->policy_config->roaming_policy;

	info->config.ecall = session->policy_config->ecall;
	if (info->config.ecall)
		ecall_session = session;

	info->config.priority = session->policy_config->priority;

	session_notify(session);

	return 0;
}

static DBusMessage *connect_session(DBusConnection *conn,
					DBusMessage *msg, void *user_data)
{
	struct connman_session *session = user_data;

	DBG("session %p", session);

	if (ecall_session) {
		if (ecall_session->ecall && ecall_session != session)
			return __connman_error_failed(msg, EBUSY);

		session->ecall = true;
	}

	if (!session->active) {
		session->active = true;
		set_active_session(session, true);
	}

	session_activate(session);

	__connman_service_auto_connect(CONNMAN_SERVICE_CONNECT_REASON_SESSION);

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *disconnect_session(DBusConnection *conn,
					DBusMessage *msg, void *user_data)
{
	struct connman_session *session = user_data;

	DBG("session %p", session);

	if (ecall_session) {
		if (ecall_session->ecall && ecall_session != session)
			return __connman_error_failed(msg, EBUSY);

		session->ecall = false;
	}

	if (session->active) {
		session->active = false;
		set_active_session(session, false);
	}

	session_deactivate(session);
	update_session_state(session);

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *change_session(DBusConnection *conn,
					DBusMessage *msg, void *user_data)
{
	struct connman_session *session = user_data;
	struct session_info *info = session->info;
	DBusMessageIter iter, value;
	const char *name;
	const char *val;
	GSList *allowed_bearers;
	int err;

	DBG("session %p", session);
	if (!dbus_message_iter_init(msg, &iter))
		return __connman_error_invalid_arguments(msg);

	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING)
		return __connman_error_invalid_arguments(msg);

	dbus_message_iter_get_basic(&iter, &name);
	dbus_message_iter_next(&iter);

	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT)
		return __connman_error_invalid_arguments(msg);

	dbus_message_iter_recurse(&iter, &value);

	switch (dbus_message_iter_get_arg_type(&value)) {
	case DBUS_TYPE_ARRAY:
		if (g_str_equal(name, "AllowedBearers")) {
			err = parse_bearers(&value, &allowed_bearers);
			if (err < 0)
				return __connman_error_failed(msg, -err);

			if (session->active)
				set_active_session(session, false);

			session->active = false;
			session_deactivate(session);
			update_session_state(session);

			g_slist_free(info->config.allowed_bearers);
			session->user_allowed_bearers = allowed_bearers;

			apply_policy_on_bearers(
					session->policy_config->allowed_bearers,
					session->user_allowed_bearers,
					&info->config.allowed_bearers);

			session_activate(session);
		} else {
			goto err;
		}
		break;
	case DBUS_TYPE_STRING:
		if (g_str_equal(name, "ConnectionType")) {
			dbus_message_iter_get_basic(&value, &val);
			info->config.type = apply_policy_on_type(
				session->policy_config->type,
				connman_session_parse_connection_type(val));
		} else if (g_str_equal(name, "AllowedInterface")) {
			dbus_message_iter_get_basic(&value, &val);
			if (session->active)
				set_active_session(session, false);

			session->active = false;
			session_deactivate(session);
			update_session_state(session);

			g_free(session->user_allowed_interface);
			/* empty string means allow any interface */
			if (!g_strcmp0(val, ""))
				session->user_allowed_interface = NULL;
			else
				session->user_allowed_interface = g_strdup(val);

			info->config.allowed_interface = apply_policy_on_interface(
				session->policy_config->allowed_interface,
				session->user_allowed_interface);

			session_activate(session);
		} else {
			goto err;
		}
		break;
	case DBUS_TYPE_BOOLEAN:
		if (g_str_equal(name, "SourceIPRule")) {
			dbus_bool_t source_ip_rule;
			dbus_message_iter_get_basic(&value, &source_ip_rule);

			info->config.source_ip_rule = source_ip_rule;
			update_session_state(session);
		} else {
			goto err;
		}
		break;
	default:
		goto err;
	}

	session_notify(session);

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);

err:
	return __connman_error_invalid_arguments(msg);
}

static void release_session(gpointer key, gpointer value, gpointer user_data)
{
	struct connman_session *session = value;
	DBusMessage *message;

	DBG("owner %s path %s", session->owner, session->notify_path);

	if (session->notify_watch > 0)
		g_dbus_remove_watch(connection, session->notify_watch);

	g_dbus_unregister_interface(connection, session->session_path,
						CONNMAN_SESSION_INTERFACE);

	message = dbus_message_new_method_call(session->owner,
						session->notify_path,
						CONNMAN_NOTIFICATION_INTERFACE,
						"Release");
	if (!message)
		return;

	dbus_message_set_no_reply(message, TRUE);

	g_dbus_send_message(connection, message);
}

static int session_disconnect(struct connman_session *session)
{
	DBG("session %p, %s", session, session->owner);

	if (session->notify_watch > 0)
		g_dbus_remove_watch(connection, session->notify_watch);

	g_dbus_unregister_interface(connection, session->session_path,
						CONNMAN_SESSION_INTERFACE);

	g_hash_table_remove(session_hash, session->session_path);

	return 0;
}

static void owner_disconnect(DBusConnection *conn, void *user_data)
{
	struct connman_session *session = user_data;

	DBG("session %p, %s died", session, session->owner);

	session_disconnect(session);
}

static DBusMessage *destroy_session(DBusConnection *conn,
					DBusMessage *msg, void *user_data)
{
	struct connman_session *session = user_data;

	DBG("session %p", session);

	if (ecall_session && ecall_session != session)
		return __connman_error_failed(msg, EBUSY);

	session_disconnect(session);

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static const GDBusMethodTable session_methods[] = {
	{ GDBUS_METHOD("Destroy", NULL, NULL, destroy_session) },
	{ GDBUS_METHOD("Connect", NULL, NULL, connect_session) },
	{ GDBUS_METHOD("Disconnect", NULL, NULL,
			disconnect_session ) },
	{ GDBUS_METHOD("Change",
			GDBUS_ARGS({ "name", "s" }, { "value", "v" }),
			NULL, change_session) },
	{ },
};

static int session_policy_config_cb(struct connman_session *session,
				struct connman_session_config *config,
				void *user_data, int err)
{
	struct creation_data *creation_data = user_data;
	struct session_info *info, *info_last;
	DBusMessage *reply;

	DBG("session %p config %p", session, config);

	if (err < 0)
		goto err;

	session->policy_config = config;
	session->info->config.source_ip_rule = creation_data->source_ip_rule;

	session->mark = session_mark++;
	session->index = -1;

	err = init_firewall_session(session);
	if (err < 0)
		goto err;

	err = init_routing_table(session);
	if (err < 0)
		goto err;

	info = session->info;
	info_last = session->info_last;

	if (session->policy_config->ecall)
		ecall_session = session;

	info->state = CONNMAN_SESSION_STATE_DISCONNECTED;
	info->config.type = apply_policy_on_type(
				session->policy_config->type,
				creation_data->type);
	info->config.priority = session->policy_config->priority;
	info->config.roaming_policy = session->policy_config->roaming_policy;

	session->user_allowed_bearers = creation_data->allowed_bearers;
	creation_data->allowed_bearers = NULL;

	session->user_allowed_interface = creation_data->allowed_interface;
	creation_data->allowed_interface = NULL;

	apply_policy_on_bearers(
			session->policy_config->allowed_bearers,
			session->user_allowed_bearers,
			&info->config.allowed_bearers);

	info->config.allowed_interface = apply_policy_on_interface(
		session->policy_config->allowed_interface,
		session->user_allowed_interface);

	g_hash_table_replace(session_hash, session->session_path, session);

	DBG("add %s", session->session_path);

	if (!g_dbus_register_interface(connection, session->session_path,
					CONNMAN_SESSION_INTERFACE,
					session_methods, NULL, NULL,
					session, NULL)) {
		connman_error("Failed to register %s", session->session_path);
		g_hash_table_remove(session_hash, session->session_path);
		err = -EINVAL;
		goto err_notify;
	}

	reply = g_dbus_create_reply(creation_data->pending,
				DBUS_TYPE_OBJECT_PATH, &session->session_path,
				DBUS_TYPE_INVALID);
	g_dbus_send_message(connection, reply);
	creation_data->pending = NULL;

	info_last->state = info->state;
	info_last->config.priority = info->config.priority;
	info_last->config.roaming_policy = info->config.roaming_policy;
	info_last->config.allowed_bearers = info->config.allowed_bearers;
	info_last->config.allowed_interface = info->config.allowed_interface;
	info_last->config.source_ip_rule = info->config.source_ip_rule;

	session->append_all = true;

	cleanup_creation_data(creation_data);

	session_activate(session);

	return 0;

err:
	cleanup_session(session);

err_notify:
	reply = __connman_error_failed(creation_data->pending, -err);
	g_dbus_send_message(connection, reply);
	creation_data->pending = NULL;

	cleanup_creation_data(creation_data);

	return err;
}

int __connman_session_create(DBusMessage *msg)
{
	const char *owner, *notify_path;
	char *session_path = NULL;
	DBusMessageIter iter, array;
	struct connman_session *session = NULL;
	struct creation_data *creation_data = NULL;
	bool user_allowed_bearers = false;
	bool user_connection_type = false;
	int err, i;
	char *str;

	owner = dbus_message_get_sender(msg);

	DBG("owner %s", owner);

	if (ecall_session && ecall_session->ecall) {
		/*
		 * If there is an emergency call already going on,
		 * ignore session creation attempt
		 */
		err = -EBUSY;
		goto err;
	}

	creation_data = g_try_new0(struct creation_data, 1);
	if (!creation_data) {
		err = -ENOMEM;
		goto err;
	}

	creation_data->pending = dbus_message_ref(msg);

	dbus_message_iter_init(msg, &iter);
	dbus_message_iter_recurse(&iter, &array);

	while (dbus_message_iter_get_arg_type(&array) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key, *val;

		dbus_message_iter_recurse(&array, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		switch (dbus_message_iter_get_arg_type(&value)) {
		case DBUS_TYPE_ARRAY:
			if (g_str_equal(key, "AllowedBearers")) {
				err = parse_bearers(&value,
					&creation_data->allowed_bearers);
				if (err < 0)
					goto err;

				user_allowed_bearers = true;
			} else {
				err = -EINVAL;
				goto err;
			}
			break;
		case DBUS_TYPE_STRING:
			if (g_str_equal(key, "ConnectionType")) {
				dbus_message_iter_get_basic(&value, &val);
				creation_data->type =
					connman_session_parse_connection_type(val);

				user_connection_type = true;
			} else if (g_str_equal(key, "ContextIdentifier")) {
				dbus_message_iter_get_basic(&value, &val);
				creation_data->context_identifier = g_strdup(val);
			} else if (g_str_equal(key, "AllowedInterface")) {
				dbus_message_iter_get_basic(&value, &val);
				creation_data->allowed_interface = g_strdup(val);
			} else {
				err = -EINVAL;
				goto err;
			}
			break;
		case DBUS_TYPE_BOOLEAN:
			if (g_str_equal(key, "SourceIPRule")) {
				dbus_bool_t source_ip_rule;
				dbus_message_iter_get_basic(&value, &source_ip_rule);
				creation_data->source_ip_rule = source_ip_rule;
			} else {
				err = -EINVAL;
				goto err;
			}
			break;
		default:
			err = -EINVAL;
			goto err;
		}

		dbus_message_iter_next(&array);
	}

	/*
	 * If the user hasn't provided a configuration, we set
	 * the default configuration.
	 *
	 * For AllowedBearers this is '*', ...
	 */
	if (!user_allowed_bearers) {
		add_default_bearer_types(&creation_data->allowed_bearers);
		if (!creation_data->allowed_bearers) {
			err = -ENOMEM;
			goto err;
		}
	}

	/* ... and for ConnectionType it is 'any'. */
	if (!user_connection_type)
		creation_data->type = CONNMAN_SESSION_TYPE_ANY;

	dbus_message_iter_next(&iter);
	dbus_message_iter_get_basic(&iter, &notify_path);

	if (!notify_path) {
		err = -EINVAL;
		goto err;
	}

	str = g_strdup(owner);
	for (i = 0; str[i] != '\0'; i++)
		if (str[i] == ':' || str[i] == '.')
			str[i] = '_';
	session_path = g_strdup_printf("/sessions/%s%s", str, notify_path);
	g_free(str);

	if (!session_path) {
		err = -ENOMEM;
		goto err;
	}

	session = g_hash_table_lookup(session_hash, session_path);
	if (session) {
		g_free(session_path);
		session = NULL;
		err = -EEXIST;
		goto err;
	}

	session = g_try_new0(struct connman_session, 1);
	if (!session) {
		g_free(session_path);
		err = -ENOMEM;
		goto err;
	}

	creation_data->session = session;
	session->session_path = session_path;

	session->info = g_try_new0(struct session_info, 1);
	if (!session->info) {
		err = -ENOMEM;
		goto err;
	}

	session->info_last = g_try_new0(struct session_info, 1);
	if (!session->info_last) {
		err = -ENOMEM;
		goto err;
	}

	session->owner = g_strdup(owner);
	session->notify_path = g_strdup(notify_path);
	session->notify_watch =
		g_dbus_add_disconnect_watch(connection, session->owner,
					owner_disconnect, session, NULL);

	err = create_policy_config(session, session_policy_config_cb,
					creation_data);
	if (err < 0 && err != -EINPROGRESS)
		return err;

	return -EINPROGRESS;

err:
	connman_error("Failed to create session");

	free_session(session);

	cleanup_creation_data(creation_data);
	return err;
}

bool __connman_session_policy_autoconnect(enum connman_service_connect_reason reason)
{
	if (!policy || !policy->autoconnect)
		return true;

	return policy->autoconnect(reason);
}

void connman_session_destroy(struct connman_session *session)
{
	DBG("session %p", session);

	session_disconnect(session);
}

int __connman_session_destroy(DBusMessage *msg)
{
	const char *owner, *session_path;
	struct connman_session *session;

	owner = dbus_message_get_sender(msg);

	DBG("owner %s", owner);

	dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &session_path,
							DBUS_TYPE_INVALID);
	if (!session_path)
		return -EINVAL;

	session = g_hash_table_lookup(session_hash, session_path);
	if (!session)
		return -EINVAL;

	if (g_strcmp0(owner, session->owner) != 0)
		return -EACCES;

	connman_session_destroy(session);

	return 0;
}

int connman_session_connect(struct connman_service *service)
{
	DBG("service %p name %s", service, __connman_service_get_name(service));

	return __connman_service_connect(service,
				CONNMAN_SERVICE_CONNECT_REASON_SESSION);
}

int connman_session_disconnect(struct connman_service *service)
{
	DBG("service %p", service);

	return __connman_service_disconnect(service);
}

static enum connman_session_state service_to_session_state(
					enum connman_service_state state)
{
	switch (state) {
	case CONNMAN_SERVICE_STATE_UNKNOWN:
	case CONNMAN_SERVICE_STATE_IDLE:
	case CONNMAN_SERVICE_STATE_ASSOCIATION:
	case CONNMAN_SERVICE_STATE_CONFIGURATION:
	case CONNMAN_SERVICE_STATE_DISCONNECT:
	case CONNMAN_SERVICE_STATE_FAILURE:
		break;
	case CONNMAN_SERVICE_STATE_READY:
		return CONNMAN_SESSION_STATE_CONNECTED;
	case CONNMAN_SERVICE_STATE_ONLINE:
		return CONNMAN_SESSION_STATE_ONLINE;
	}

	return CONNMAN_SESSION_STATE_DISCONNECTED;
}

static void update_session_state(struct connman_session *session)
{
	enum connman_service_state service_state;
	enum connman_session_state state = CONNMAN_SESSION_STATE_DISCONNECTED;

	if (session->service) {
		service_state = connman_service_get_state(session->service);
		state = service_to_session_state(service_state);
		session->info->state = state;
	}
	session->info->state = state;

	DBG("session %p state %s", session, state2string(state));

	update_firewall(session);
	del_nat_rules(session);
	update_routing_table(session);
	add_nat_rules(session);

	if (policy && policy->update_session_state)
		policy->update_session_state(session, state);

	session_notify(session);
}

static bool session_match_service(struct connman_session *session,
				struct connman_service *service)
{
	enum connman_service_type bearer_type;
	enum connman_service_type service_type;
	enum connman_service_type current_service_type;
	GSList *list;
	char *ifname;

	if (policy && policy->allowed)
		return policy->allowed(session, service);

	current_service_type = connman_service_get_type(session->service);

	for (list = session->info->config.allowed_bearers; list; list = list->next) {
		bearer_type = GPOINTER_TO_INT(list->data);
		service_type = connman_service_get_type(service);
		ifname = connman_service_get_interface(service);

		if (bearer_type == current_service_type)
			return false;

		if (bearer_type == service_type &&
			(session->info->config.allowed_interface == NULL ||
			!g_strcmp0(session->info->config.allowed_interface, "*") ||
			!g_strcmp0(session->info->config.allowed_interface, ifname))) {
				g_free(ifname);
				return true;
		}
		g_free(ifname);
	}

	return false;
}

static bool is_session_connected(struct connman_session *session,
				enum connman_service_state state)

{
	switch (state) {
	case CONNMAN_SERVICE_STATE_UNKNOWN:
	case CONNMAN_SERVICE_STATE_IDLE:
	case CONNMAN_SERVICE_STATE_ASSOCIATION:
	case CONNMAN_SERVICE_STATE_CONFIGURATION:
	case CONNMAN_SERVICE_STATE_DISCONNECT:
	case CONNMAN_SERVICE_STATE_FAILURE:
			  break;
	case CONNMAN_SERVICE_STATE_READY:
		if (session->info->config.type == CONNMAN_SESSION_TYPE_INTERNET)
			return false;
		/* fall through */
	case CONNMAN_SERVICE_STATE_ONLINE:
		return true;
	}

	return false;
}

static void session_activate(struct connman_session *session)
{
	GHashTableIter iter;
	gpointer key, value;

	if (!service_hash)
		return;

	if (policy && policy->get_service_for_session) {
		struct connman_service *service;
		struct connman_service_info *info;
		GSList *service_list = NULL;
		enum connman_service_state state = CONNMAN_SERVICE_STATE_DISCONNECT;

		g_hash_table_iter_init(&iter, service_hash);

		while (g_hash_table_iter_next(&iter, &key, &value)) {
			struct connman_service_info *info = value;
			state = connman_service_get_state(info->service);

			if (is_session_connected(session, state))
				service_list = g_slist_prepend(service_list,
							       info->service);
		}

		service_list = g_slist_reverse(service_list);
		service = policy->get_service_for_session(session, service_list);

		if (service) {
			info = g_hash_table_lookup(service_hash, service);
			DBG("session %p add service %p", session, info->service);

			info->sessions = g_slist_prepend(info->sessions,
							session);
			session->service = info->service;
			update_session_state(session);
		}

		g_slist_free(service_list);
		return;
	}

	g_hash_table_iter_init(&iter, service_hash);
	while (g_hash_table_iter_next(&iter, &key, &value)) {
		struct connman_service_info *info = value;
		enum connman_service_state state;

		state = connman_service_get_state(info->service);

		if (is_session_connected(session, state) &&
				session_match_service(session, info->service)) {
			DBG("session %p add service %p", session, info->service);

			info->sessions = g_slist_prepend(info->sessions,
							session);
			session->service = info->service;
			update_session_state(session);

			return;
		}
	}

	session_notify(session);
}

static void session_deactivate(struct connman_session *session)
{
	struct connman_service_info *info;

	if (!service_hash)
		return;

	if (!session->service)
		return;

	info = g_hash_table_lookup(service_hash, session->service);
	if (!info)
		return;

	info->sessions = g_slist_remove(info->sessions, session);
	session->service = NULL;

	session->info->state = CONNMAN_SESSION_STATE_DISCONNECTED;
}

static void handle_service_state_online(struct connman_service *service,
					enum connman_service_state state,
					struct connman_service_info *info)
{
	GHashTableIter iter;
	gpointer key, value;

	g_hash_table_iter_init(&iter, session_hash);
	while (g_hash_table_iter_next(&iter, &key, &value)) {
		struct connman_session *session = value;
		bool connected;

		connected = is_session_connected(session, state);

		if (session->service == service) {
			if (!connected) {
				DBG("session %p remove service %p", session, service);
				info->sessions = g_slist_remove(info->sessions,
							session);
				session->service = NULL;
				update_session_state(session);
			}
		} else if (connected && session_match_service(session, service)) {
			DBG("session %p add service %p", session, service);

			info->sessions = g_slist_prepend(info->sessions,
							session);
			session->service = service;
			update_session_state(session);
		}
	}
}

static void handle_service_state_offline(struct connman_service *service,
					struct connman_service_info *info)
{
	GSList *list;

	for (list = info->sessions; list; list = list->next) {
		struct connman_session *session = list->data;

		if (session->service != service) {
			connman_warn("session %p should have session %p assigned",
					session, service);
			continue;
		}

		DBG("session %p remove service %p", session, service);

		session->service = NULL;
		update_session_state(session);
		session_activate(session);
	}
}

static void service_state_changed(struct connman_service *service,
				enum connman_service_state state)
{
	struct connman_service_info *info;

	DBG("service %p (%s) state %d",
		service, connman_service_get_identifier(service),
		state);

	info = g_hash_table_lookup(service_hash, service);

	switch (state) {
	case CONNMAN_SERVICE_STATE_UNKNOWN:
	case CONNMAN_SERVICE_STATE_IDLE:
	case CONNMAN_SERVICE_STATE_ASSOCIATION:
	case CONNMAN_SERVICE_STATE_CONFIGURATION:
	case CONNMAN_SERVICE_STATE_FAILURE:
	case CONNMAN_SERVICE_STATE_DISCONNECT:
		if (!info)
			return;

		handle_service_state_offline(service, info);

		g_hash_table_remove(service_hash, service);

		return;
	case CONNMAN_SERVICE_STATE_READY:
	case CONNMAN_SERVICE_STATE_ONLINE:
		if (!info) {
			info = g_new0(struct connman_service_info, 1);
			g_hash_table_replace(service_hash, service, info);
		}

		info->service = service;
		handle_service_state_online(service, state, info);
	}
}

static void ipconfig_changed(struct connman_service *service,
				struct connman_ipconfig *ipconfig)
{
	GHashTableIter iter;
	gpointer key, value;
	struct connman_session *session;
	struct session_info *info;
	enum connman_ipconfig_type type;

	DBG("service %p ipconfig %p", service, ipconfig);

	type = __connman_ipconfig_get_config_type(ipconfig);

	g_hash_table_iter_init(&iter, session_hash);

	while (g_hash_table_iter_next(&iter, &key, &value)) {
		session = value;
		info = session->info;

		if (info->state == CONNMAN_SESSION_STATE_DISCONNECTED)
			continue;

		if (session->service && session->service == service) {
			update_session_state(session);

			if (type == CONNMAN_IPCONFIG_TYPE_IPV4)
				ipconfig_ipv4_changed(session);
			else if (type == CONNMAN_IPCONFIG_TYPE_IPV6)
				ipconfig_ipv6_changed(session);
		}
	}
}

static const struct connman_notifier session_notifier = {
	.name			= "session",
	.service_state_changed	= service_state_changed,
	.ipconfig_changed	= ipconfig_changed,
};

int __connman_session_init(void)
{
	int err;

	DBG("");

	connection = connman_dbus_get_connection();
	if (!connection)
		return -1;

	err = connman_notifier_register(&session_notifier);
	if (err < 0) {
		dbus_connection_unref(connection);
		return err;
	}

	session_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
						NULL, cleanup_session);

	service_hash = g_hash_table_new_full(g_direct_hash, g_direct_equal,
						NULL, cleanup_service);
	return 0;
}

void __connman_session_cleanup(void)
{
	DBG("");

	if (!connection)
		return;

	connman_notifier_unregister(&session_notifier);

	g_hash_table_foreach(session_hash, release_session, NULL);
	g_hash_table_destroy(session_hash);
	session_hash = NULL;
	g_hash_table_destroy(service_hash);
	service_hash = NULL;

	dbus_connection_unref(connection);
}
