/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2013  Intel Corporation. All rights reserved.
 *
 *  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 <stdio.h>
#include <stdint.h>
#include <string.h>
#include <arpa/inet.h>

#include <glib.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/plugin.h>
#include <connman/dbus.h>
#include <connman/technology.h>
#include <connman/provision.h>

#include <gdbus.h>

#define TIMEOUT 30000

#define NEARD_SERVICE "org.neard"
#define NEARD_PATH "/"
#define NEARD_MANAGER_INTERFACE "org.neard.Manager"
#define NEARD_AGENT_INTERFACE "org.neard.HandoverAgent"
#define NEARD_ERROR_INTERFACE "org.neard.HandoverAgent.Error"

#define AGENT_PATH "/net/connman/neard_handover_agent"
#define AGENT_TYPE "wifi"

#define NEARD_SERVICE_GROUP "service_neard_provision"

/* Configuration keys */
#define SERVICE_KEY_TYPE               "Type"
#define SERVICE_KEY_SSID               "SSID"
#define SERVICE_KEY_PASSPHRASE         "Passphrase"
#define SERVICE_KEY_HIDDEN             "Hidden"

struct data_elements {
	unsigned int value;
	unsigned int length;
	bool fixed_length;
};

typedef enum {
	DE_AUTHENTICATION_TYPE = 0,
	DE_NETWORK_KEY         = 1,
	DE_SSID                = 2,
	DE_MAX                 = 3,
} DEid;

static const struct data_elements  DEs[DE_MAX] = {
	{ 0x1003, 2,  TRUE  },
	{ 0x1027, 64, FALSE },
	{ 0x1045, 32, FALSE },
};

#define DE_VAL_OPEN                 0x0001
#define DE_VAL_PSK                  0x0022

struct wifi_sc {
	gchar *ssid;
	gchar *passphrase;
};

static DBusConnection *connection = NULL;
DBusPendingCall *register_call = NULL;
bool agent_registered = false;
static guint watch_id = 0;

static int set_2b_into_tlv(uint8_t *tlv_msg, uint16_t val)
{
	uint16_t ne_val;
	uint8_t *ins;

	ins = (uint8_t *) &ne_val;

	ne_val = htons(val);
	tlv_msg[0] = ins[0];
	tlv_msg[1] = ins[1];

	return 2;
}

static int set_byte_array_into_tlv(uint8_t *tlv_msg,
					uint8_t *array, int length)
{
	memcpy((void *)tlv_msg, (void *)array, length);
	return length;
}

static uint8_t *encode_to_tlv(const char *ssid, const char *psk, int *length)
{
	uint16_t ssid_len, psk_len;
	uint8_t *tlv_msg;
	int pos = 0;

	if (!ssid || !length)
		return NULL;

	ssid_len = strlen(ssid);

	*length = 6 + 4 + ssid_len;
	if (psk) {
		psk_len = strlen(psk);
		*length += 4 + psk_len;
	} else
		psk_len = 0;

	tlv_msg = g_try_malloc0(sizeof(uint8_t) * (*length));
	if (!tlv_msg)
		return NULL;

	pos += set_2b_into_tlv(tlv_msg+pos, DEs[DE_SSID].value);
	pos += set_2b_into_tlv(tlv_msg+pos, ssid_len);
	pos += set_byte_array_into_tlv(tlv_msg+pos, (uint8_t *)ssid, ssid_len);

	pos += set_2b_into_tlv(tlv_msg+pos, DEs[DE_AUTHENTICATION_TYPE].value);
	pos += set_2b_into_tlv(tlv_msg+pos,
					DEs[DE_AUTHENTICATION_TYPE].length);
	if (psk) {
		pos += set_2b_into_tlv(tlv_msg+pos, DE_VAL_PSK);
		pos += set_2b_into_tlv(tlv_msg+pos, DEs[DE_NETWORK_KEY].value);
		pos += set_2b_into_tlv(tlv_msg+pos, psk_len);
		pos += set_byte_array_into_tlv(tlv_msg+pos,
						(uint8_t *)psk, psk_len);
	} else
		pos += set_2b_into_tlv(tlv_msg+pos, DE_VAL_OPEN);

	return tlv_msg;
}

static DBusMessage *get_reply_on_error(DBusMessage *message, int error)
{
	if (error == ENOTSUP)
		return g_dbus_create_error(message,
					NEARD_ERROR_INTERFACE ".NotSupported",
					"Operation is not supported");

	return g_dbus_create_error(message,
		NEARD_ERROR_INTERFACE ".Failed", "Invalid parameters");
}

static int parse_request_oob_params(DBusMessage *message,
				const uint8_t **tlv_msg, int *length)
{
	DBusMessageIter iter;
	DBusMessageIter array;
	DBusMessageIter dict_entry;
	const char *key;
	int arg_type;

	if (!tlv_msg || !length)
		return -EINVAL;

	dbus_message_iter_init(message, &iter);

	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY)
		return -EINVAL;

	dbus_message_iter_recurse(&iter, &array);
	arg_type = dbus_message_iter_get_arg_type(&array);
	if (arg_type != DBUS_TYPE_DICT_ENTRY)
		return -EINVAL;

	while (arg_type != DBUS_TYPE_INVALID) {
		dbus_message_iter_recurse(&array, &dict_entry);
		if (dbus_message_iter_get_arg_type(&dict_entry) !=
							DBUS_TYPE_STRING)
			return -EINVAL;

		dbus_message_iter_get_basic(&dict_entry, &key);
		if (g_strcmp0(key, "WSC") == 0) {
			DBusMessageIter value;
			DBusMessageIter fixed_array;

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

			if (dbus_message_iter_get_arg_type(&value) !=
							DBUS_TYPE_ARRAY)
				return -EINVAL;

			dbus_message_iter_recurse(&value, &fixed_array);
			dbus_message_iter_get_fixed_array(&fixed_array,
							tlv_msg, length);
			return 0;
		}

		dbus_message_iter_next(&array);
		arg_type = dbus_message_iter_get_arg_type(&array);
	}

	return -EINVAL;
}

static DBusMessage *create_request_oob_reply(DBusMessage *message)
{
	DBusMessage *reply;
	DBusMessageIter iter;
	DBusMessageIter dict;
	const char *ssid, *psk;
	uint8_t *tlv_msg;
	int length, freq;

	if (!connman_technology_get_wifi_tethering(NULL, &ssid, &psk, &freq))
		return get_reply_on_error(message, ENOTSUP);

	tlv_msg = encode_to_tlv(ssid, psk, &length);
	if (!tlv_msg)
		return get_reply_on_error(message, ENOTSUP);

	reply = dbus_message_new_method_return(message);
	if (!reply)
		goto out;

	dbus_message_iter_init_append(reply, &iter);

	connman_dbus_dict_open(&iter, &dict);

	connman_dbus_dict_append_fixed_array(&dict, "WSC",
					DBUS_TYPE_BYTE, &tlv_msg, length);

	dbus_message_iter_close_container(&iter, &dict);

out:
	g_free(tlv_msg);

	return reply;
}

static DBusMessage *request_oob_method(DBusConnection *dbus_conn,
					DBusMessage *message, void *user_data)
{
	DBusMessageIter iter;

	DBG("");

	dbus_message_iter_init(message, &iter);

	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY)
		return get_reply_on_error(message, EINVAL);

	return create_request_oob_reply(message);
}

static void free_wifi_sc(struct wifi_sc *wsc)
{
	if (!wsc)
		return;

	g_free(wsc->ssid);
	g_free(wsc->passphrase);

	g_free(wsc);
}

static inline int get_2b_from_tlv(const uint8_t *tlv_msg, uint16_t *val)
{
	*val = ntohs(tlv_msg[0] | (tlv_msg[1] << 8));

	return 2;
}

static uint8_t *get_byte_array_from_tlv(const uint8_t *tlv_msg, int length)
{
	uint8_t *array;

	array = g_try_malloc0(sizeof(uint8_t) * length);
	if (!array)
		return NULL;

	memcpy((void *)array, (void *)tlv_msg, length*sizeof(uint8_t));

	return array;
}

static char *get_string_from_tlv(const uint8_t *tlv_msg, int length)
{
	char *str;

	str = g_try_malloc0((sizeof(char) * length) + 1);
	if (!str)
		return NULL;

	memcpy((void *)str, (void *)tlv_msg, length*sizeof(char));

	return str;
}

static inline DEid get_de_id(uint16_t attr)
{
	int i;

	for (i = 0; i < DE_MAX && DEs[i].value != 0; i++) {
		if (attr == DEs[i].value)
			return i;
	}

	return DE_MAX;
}

static inline bool is_de_length_fine(DEid id, uint16_t length)
{
	if (DEs[id].fixed_length)
		return (length == DEs[id].length);

	return (length <= DEs[id].length);
}

static char *get_hexstr_from_byte_array(const uint8_t *bt, int length)
{
	char *hex_str;
	int i, j;

	hex_str = g_try_malloc0(((length*2)+1) * sizeof(char));
	if (!hex_str)
		return NULL;

	for (i = 0, j = 0; i < length; i++, j += 2)
		snprintf(hex_str+j, 3, "%02x", bt[i]);

	return hex_str;
}

static struct wifi_sc *decode_from_tlv(const uint8_t *tlv_msg, int length)
{
	struct wifi_sc *wsc;
	uint16_t attr, len;
	uint8_t *bt;
	int pos;
	DEid id;

	if (!tlv_msg || length == 0)
		return NULL;

	wsc = g_try_malloc0(sizeof(struct wifi_sc));
	if (!wsc)
		return NULL;

	pos = 0;
	while (pos < length) {
		pos += get_2b_from_tlv(tlv_msg+pos, &attr);
		pos += get_2b_from_tlv(tlv_msg+pos, &len);

		if (len > length - pos)
			goto error;

		id = get_de_id(attr);
		if (id == DE_MAX) {
			pos += len;
			continue;
		}

		if (!is_de_length_fine(id, len))
			goto error;

		switch (id) {
		case DE_AUTHENTICATION_TYPE:
			break;
		case DE_NETWORK_KEY:
			wsc->passphrase = get_string_from_tlv(tlv_msg+pos,
									len);
			break;
		case DE_SSID:
			bt = get_byte_array_from_tlv(tlv_msg+pos, len);
			wsc->ssid = get_hexstr_from_byte_array(bt, len);
			g_free(bt);

			break;
		case DE_MAX:
			/* Falling back. Should never happen though. */
		default:
			break;
		}

		pos += len;
	}

	return wsc;

error:
	free_wifi_sc(wsc);
	return NULL;
}

static int handle_wcs_data(const uint8_t *tlv_msg, int length)
{
	struct wifi_sc *wsc = NULL;
	GKeyFile *keyfile = NULL;
	int ret = -EINVAL;

	wsc = decode_from_tlv(tlv_msg, length);
	if (!wsc)
		return -EINVAL;

	if (!wsc->ssid)
		goto out;

	keyfile = g_key_file_new();
	if (!keyfile) {
		ret = -ENOMEM;
		goto out;
	}

	g_key_file_set_string(keyfile, NEARD_SERVICE_GROUP,
					SERVICE_KEY_TYPE, "wifi");
	g_key_file_set_string(keyfile, NEARD_SERVICE_GROUP,
					SERVICE_KEY_SSID, wsc->ssid);
	g_key_file_set_boolean(keyfile, NEARD_SERVICE_GROUP,
					SERVICE_KEY_HIDDEN, TRUE);

	if (wsc->passphrase)
		g_key_file_set_string(keyfile, NEARD_SERVICE_GROUP,
				SERVICE_KEY_PASSPHRASE, wsc->passphrase);

	ret = connman_config_provision_mutable_service(keyfile);

out:
	g_key_file_free(keyfile);
	free_wifi_sc(wsc);
	return ret;
}

static DBusMessage *push_oob_method(DBusConnection *dbus_conn,
					DBusMessage *message, void *user_data)
{
	const uint8_t *tlv_msg;
	int err = EINVAL;
	int length;

	DBG("");

	if (parse_request_oob_params(message, &tlv_msg, &length) != 0)
		return get_reply_on_error(message, err);

	err = handle_wcs_data(tlv_msg, length);
	if (err != 0)
		return get_reply_on_error(message, err);

	return g_dbus_create_reply(message, DBUS_TYPE_INVALID);
}

static DBusMessage *release_method(DBusConnection *dbus_conn,
					DBusMessage *message, void *user_data)
{
	DBG("");

	agent_registered = false;
	g_dbus_unregister_interface(connection,
					AGENT_PATH, NEARD_AGENT_INTERFACE);

	return g_dbus_create_reply(message, DBUS_TYPE_INVALID);
}

static const GDBusMethodTable neard_methods[] = {
	{ GDBUS_ASYNC_METHOD("RequestOOB",
		GDBUS_ARGS({ "data", "a{sv}" }),
		GDBUS_ARGS({ "data", "a{sv}" }), request_oob_method) },
	{ GDBUS_ASYNC_METHOD("PushOOB",
		GDBUS_ARGS({ "data", "a{sv}"}), NULL, push_oob_method) },
	{ GDBUS_METHOD("Release", NULL, NULL, release_method) },
	{ },
};

static void cleanup_register_call(void)
{
	if (register_call) {
		dbus_pending_call_cancel(register_call);
		dbus_pending_call_unref(register_call);
		register_call = NULL;
	}
}

static void register_agent_cb(DBusPendingCall *pending, void *user_data)
{
	DBusMessage *reply;

	if (!dbus_pending_call_get_completed(pending))
		goto out;

	register_call = NULL;

	reply = dbus_pending_call_steal_reply(pending);
	if (!reply)
		goto out;

	if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
		g_dbus_unregister_interface(connection,
					AGENT_PATH, NEARD_AGENT_INTERFACE);
	} else
		agent_registered = true;

	dbus_message_unref(reply);
out:
	dbus_pending_call_unref(pending);
}

static void register_agent(void)
{
	const char *path = AGENT_PATH;
	const char *type = AGENT_TYPE;
	DBusMessage *message;

	message = dbus_message_new_method_call(NEARD_SERVICE, NEARD_PATH,
						NEARD_MANAGER_INTERFACE,
						"RegisterHandoverAgent");
	if (!message)
		return;

	dbus_message_append_args(message, DBUS_TYPE_OBJECT_PATH,
			&path, DBUS_TYPE_STRING, &type, DBUS_TYPE_INVALID);

	if (!dbus_connection_send_with_reply(connection, message,
						&register_call, TIMEOUT))
		goto out;

	if (!dbus_pending_call_set_notify(register_call,
						register_agent_cb, NULL, NULL))
		cleanup_register_call();

out:
	dbus_message_unref(message);
}

static void unregister_agent(void)
{
	const char *path = AGENT_PATH;
	const char *type = AGENT_TYPE;
	DBusMessage *message;

	if (!agent_registered)
		return cleanup_register_call();

	agent_registered = false;

	message = dbus_message_new_method_call(NEARD_SERVICE, NEARD_PATH,
						NEARD_MANAGER_INTERFACE,
						"UnregisterHandoverAgent");
	if (message) {
		dbus_message_append_args(message, DBUS_TYPE_OBJECT_PATH,
			&path, DBUS_TYPE_STRING, &type, DBUS_TYPE_INVALID);
		g_dbus_send_message(connection, message);
	}

	g_dbus_unregister_interface(connection,
					AGENT_PATH, NEARD_AGENT_INTERFACE);
}

static void neard_is_present(DBusConnection *conn, void *user_data)
{
	DBG("");

	if (agent_registered)
		return;

	if (g_dbus_register_interface(connection, AGENT_PATH,
					NEARD_AGENT_INTERFACE, neard_methods,
					NULL, NULL, NULL, NULL))
		register_agent();
}

static void neard_is_out(DBusConnection *conn, void *user_data)
{
	DBG("");

	if (agent_registered) {
		g_dbus_unregister_interface(connection,
					AGENT_PATH, NEARD_AGENT_INTERFACE);
		agent_registered = false;
	}

	cleanup_register_call();
}

static int neard_init(void)
{
	connection = connman_dbus_get_connection();
	if (!connection)
		return -EIO;

	watch_id = g_dbus_add_service_watch(connection, NEARD_SERVICE,
						neard_is_present, neard_is_out,
						NULL, NULL);
	if (watch_id == 0) {
		dbus_connection_unref(connection);
		return -ENOMEM;
	}

	return 0;
}

static void neard_exit(void)
{
	unregister_agent();

	if (watch_id != 0)
		g_dbus_remove_watch(connection, watch_id);
	if (connection)
		dbus_connection_unref(connection);
}

CONNMAN_PLUGIN_DEFINE(neard, "Neard handover plugin", VERSION,
		CONNMAN_PLUGIN_PRIORITY_DEFAULT, neard_init, neard_exit)
