/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2012  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 <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <termios.h>
#include <netdb.h>

#include <gweb/gweb.h>

#define DEFAULT_URL  "http://www.connman.net/online/status.html"

static GTimer *timer;

static GMainLoop *main_loop;

static void web_debug(const char *str, void *data)
{
	g_print("%s: %s\n", (const char *) data, str);
}

static void sig_term(int sig)
{
	g_main_loop_quit(main_loop);
}

static const char *message_type_to_string(int message_type)
{
	switch (message_type) {
	case 100:
		return "Initial redirect message";
	case 110:
		return "Proxy notification";
	case 120:
		return "Authentication notification";
	case 130:
		return "Logoff notification";
	case 140:
		return "Response to Authentication Poll";
	case 150:
		return "Response to Abort Login";
	}

	return NULL;
}

static const char *response_code_to_string(int response_code)
{
	switch (response_code) {
	case 0:
		return "No error";
	case 50:
		return "Login succeeded";
	case 100:
		return "Login failed";
	case 102:
		return "RADIUS server error/timeout";
	case 105:
		return "RADIUS server not enabled";
	case 150:
		return "Logoff succeeded";
	case 151:
		return "Login aborted";
	case 200:
		return "Proxy detection/repeat operation";
	case 201:
		return "Authentication pending";
	case 255:
		return "Access gateway internal error";
	}

	return NULL;
}

struct wispr_msg {
	bool has_error;
	const char *current_element;
	int message_type;
	int response_code;
	char *login_url;
	char *abort_login_url;
	char *logoff_url;
	char *access_procedure;
	char *access_location;
	char *location_name;
};

static inline void wispr_msg_init(struct wispr_msg *msg)
{
	msg->has_error = false;
	msg->current_element = NULL;

	msg->message_type = -1;
	msg->response_code = -1;

	g_free(msg->login_url);
	msg->login_url = NULL;

	g_free(msg->abort_login_url);
	msg->abort_login_url = NULL;

	g_free(msg->logoff_url);
	msg->logoff_url = NULL;

	g_free(msg->access_procedure);
	msg->access_procedure = NULL;

	g_free(msg->access_location);
	msg->access_location = NULL;

	g_free(msg->location_name);
	msg->location_name = NULL;
}

struct wispr_session {
	GWeb *web;
	GWebParser *parser;
	guint request;
	struct wispr_msg msg;
	char *username;
	char *password;
	char *originurl;
	char *formdata;
};

static gboolean execute_login(gpointer user_data);

static struct {
	const char *str;
	enum {
		WISPR_ELEMENT_NONE,
		WISPR_ELEMENT_ACCESS_PROCEDURE,
		WISPR_ELEMENT_ACCESS_LOCATION,
		WISPR_ELEMENT_LOCATION_NAME,
		WISPR_ELEMENT_LOGIN_URL,
		WISPR_ELEMENT_ABORT_LOGIN_URL,
		WISPR_ELEMENT_MESSAGE_TYPE,
		WISPR_ELEMENT_RESPONSE_CODE,
		WISPR_ELEMENT_NEXT_URL,
		WISPR_ELEMENT_DELAY,
		WISPR_ELEMENT_REPLY_MESSAGE,
		WISPR_ELEMENT_LOGIN_RESULTS_URL,
		WISPR_ELEMENT_LOGOFF_URL,
	} element;
} wispr_element_map[] = {
	{ "AccessProcedure",	WISPR_ELEMENT_ACCESS_PROCEDURE	},
	{ "AccessLocation",	WISPR_ELEMENT_ACCESS_LOCATION	},
	{ "LocationName",	WISPR_ELEMENT_LOCATION_NAME	},
	{ "LoginURL",		WISPR_ELEMENT_LOGIN_URL		},
	{ "AbortLoginURL",	WISPR_ELEMENT_ABORT_LOGIN_URL	},
	{ "MessageType",	WISPR_ELEMENT_MESSAGE_TYPE	},
	{ "ResponseCode",	WISPR_ELEMENT_RESPONSE_CODE	},
	{ "NextURL",		WISPR_ELEMENT_NEXT_URL		},
	{ "Delay",		WISPR_ELEMENT_DELAY		},
	{ "ReplyMessage",	WISPR_ELEMENT_REPLY_MESSAGE	},
	{ "LoginResultsURL",	WISPR_ELEMENT_LOGIN_RESULTS_URL	},
	{ "LogoffURL",		WISPR_ELEMENT_LOGOFF_URL	},
	{ NULL,			WISPR_ELEMENT_NONE		},
};

static void start_element_handler(GMarkupParseContext *context,
					const gchar *element_name,
					const gchar **attribute_names,
					const gchar **attribute_values,
					gpointer user_data, GError **error)
{
	struct wispr_msg *msg = user_data;

	msg->current_element = element_name;
}

static void end_element_handler(GMarkupParseContext *context,
					const gchar *element_name,
					gpointer user_data, GError **error)
{
	struct wispr_msg *msg = user_data;

	msg->current_element = NULL;
}

static void text_handler(GMarkupParseContext *context,
					const gchar *text, gsize text_len,
					gpointer user_data, GError **error)
{
	struct wispr_msg *msg = user_data;
	int i;

	if (!msg->current_element)
		return;

	for (i = 0; wispr_element_map[i].str; i++) {
		if (!g_str_equal(wispr_element_map[i].str, msg->current_element))
			continue;

		switch (wispr_element_map[i].element) {
		case WISPR_ELEMENT_NONE:
		case WISPR_ELEMENT_ACCESS_PROCEDURE:
			g_free(msg->access_procedure);
			msg->access_procedure = g_strdup(text);
			break;
		case WISPR_ELEMENT_ACCESS_LOCATION:
			g_free(msg->access_location);
			msg->access_location = g_strdup(text);
			break;
		case WISPR_ELEMENT_LOCATION_NAME:
			g_free(msg->location_name);
			msg->location_name = g_strdup(text);
			break;
		case WISPR_ELEMENT_LOGIN_URL:
			g_free(msg->login_url);
			msg->login_url = g_strdup(text);
			break;
		case WISPR_ELEMENT_ABORT_LOGIN_URL:
			g_free(msg->abort_login_url);
			msg->abort_login_url = g_strdup(text);
			break;
		case WISPR_ELEMENT_MESSAGE_TYPE:
			msg->message_type = atoi(text);
			break;
		case WISPR_ELEMENT_RESPONSE_CODE:
			msg->response_code = atoi(text);
			break;
		case WISPR_ELEMENT_NEXT_URL:
		case WISPR_ELEMENT_DELAY:
		case WISPR_ELEMENT_REPLY_MESSAGE:
		case WISPR_ELEMENT_LOGIN_RESULTS_URL:
			break;
		case WISPR_ELEMENT_LOGOFF_URL:
			g_free(msg->logoff_url);
			msg->logoff_url = g_strdup(text);
			break;
		}
	}
}

static void error_handler(GMarkupParseContext *context,
					GError *error, gpointer user_data)
{
	struct wispr_msg *msg = user_data;

	msg->has_error = true;
}

static const GMarkupParser wispr_parser = {
	start_element_handler,
	end_element_handler,
	text_handler,
	NULL,
	error_handler,
};

static void parser_callback(const char *str, gpointer user_data)
{
	struct wispr_session *wispr = user_data;
	GMarkupParseContext *context;
	bool result;

	//printf("%s\n", str);

	context = g_markup_parse_context_new(&wispr_parser,
			G_MARKUP_TREAT_CDATA_AS_TEXT, &wispr->msg, NULL);

	result = g_markup_parse_context_parse(context, str, strlen(str), NULL);
	if (result)
		g_markup_parse_context_end_parse(context, NULL);

	g_markup_parse_context_free(context);
}

typedef void (*user_input_cb)(const char *value, gpointer user_data);

struct user_input_data {
	GString *str;
	user_input_cb cb;
	gpointer user_data;
	bool hidden;
	int fd;
	struct termios saved_termios;
};

static void user_callback(struct user_input_data *data)
{
	char *value;

	if (data->hidden) {
		ssize_t len;

		len = write(data->fd, "\n", 1);
		if (len < 0)
			return;
	}

	tcsetattr(data->fd, TCSADRAIN, &data->saved_termios);

	close(data->fd);

	value = g_string_free(data->str, FALSE);

	if (data->cb)
		data->cb(value, data->user_data);

	g_free(value);

	g_free(data);
}

static gboolean keyboard_input(GIOChannel *channel, GIOCondition condition,
							gpointer user_data)
{
	struct user_input_data *data = user_data;
	char buf[1];
	int len;

	len = read(data->fd, buf, 1);

	if (len != 1)
		return TRUE;

	if (buf[0] == '\n') {
		user_callback(data);
		return FALSE;
	}

	g_string_append_c(data->str, buf[0]);

	if (data->hidden)
		len = write(data->fd, "*", 1);

	return TRUE;
}

static bool user_input(const char *label, bool hidden,
				user_input_cb func, gpointer user_data)
{
	struct user_input_data *data;
	struct termios new_termios;
	GIOChannel *channel;
	guint watch;
	ssize_t len;

	data = g_try_new0(struct user_input_data, 1);
	if (!data)
		return false;

	data->str = g_string_sized_new(32);
	data->cb = func;
	data->user_data = user_data;
	data->hidden = hidden;

	data->fd = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
	if (data->fd < 0)
		goto error;

	if (tcgetattr(data->fd, &data->saved_termios) < 0) {
		close(data->fd);
		goto error;
	}

	new_termios = data->saved_termios;
	if (data->hidden)
		new_termios.c_lflag &= ~(ICANON|ECHO);
	else
		new_termios.c_lflag &= ~ICANON;
	new_termios.c_cc[VMIN] = 1;
	new_termios.c_cc[VTIME] = 0;

	tcsetattr(data->fd, TCSADRAIN, &new_termios);

	channel = g_io_channel_unix_new(data->fd);
	g_io_channel_set_encoding(channel, NULL, NULL);
	g_io_channel_set_buffered(channel, FALSE);
	watch = g_io_add_watch(channel, G_IO_IN, keyboard_input, data);
	g_io_channel_unref(channel);

	if (watch == 0)
		goto error;

	len = write(data->fd, label, strlen(label));
	if (len < 0)
		goto error;

	len = write(data->fd, ": ", 2);
	if (len < 0)
		goto error;

	return true;

error:
	g_string_free(data->str, TRUE);
	g_free(data);

	return false;
}

static void password_callback(const char *value, gpointer user_data)
{
	struct wispr_session *wispr = user_data;

	g_free(wispr->password);
	wispr->password = g_strdup(value);

	printf("\n");

	execute_login(wispr);
}

static void username_callback(const char *value, gpointer user_data)
{
	struct wispr_session *wispr = user_data;

	g_free(wispr->username);
	wispr->username = g_strdup(value);

	if (!wispr->password) {
		user_input("Password", true, password_callback, wispr);
		return;
	}

	printf("\n");

	execute_login(wispr);
}

static bool wispr_input(const guint8 **data, gsize *length,
						gpointer user_data)
{
	struct wispr_session *wispr = user_data;
	GString *buf;
	gsize count;

	buf = g_string_sized_new(100);

	g_string_append(buf, "button=Login&UserName=");
	g_string_append_uri_escaped(buf, wispr->username, NULL, FALSE);
	g_string_append(buf, "&Password=");
	g_string_append_uri_escaped(buf, wispr->password, NULL, FALSE);
	g_string_append(buf, "&FNAME=0&OriginatingServer=");
	g_string_append_uri_escaped(buf, wispr->originurl, NULL, FALSE);

	count = buf->len;

	g_free(wispr->formdata);
	wispr->formdata = g_string_free(buf, FALSE);

	*data = (guint8 *) wispr->formdata;
	*length = count;

	return false;
}

static bool wispr_route(const char *addr, int ai_family, int if_index,
		gpointer user_data)
{
	char *family = "unknown";

	if (ai_family == AF_INET)
		family = "IPv4";
	else if (ai_family == AF_INET6)
		family = "IPv6";

	printf("Route request: %s %s index %d\n", family, addr, if_index);

	if (ai_family != AF_INET && ai_family != AF_INET6)
		return false;

	return true;
}

static bool wispr_result(const GError *error, GWebResult *result, gpointer user_data)
{
	struct wispr_session *wispr = user_data;
	const guint8 *chunk;
	gsize length;
	guint16 status;
	gdouble elapsed;

	g_web_result_get_chunk(result, &chunk, &length);

	if (length > 0) {
		//printf("%s\n", (char *) chunk);
		g_web_parser_feed_data(wispr->parser, chunk, length);
		return true;
	}

	g_web_parser_end_data(wispr->parser);

	status = g_web_result_get_status(result);

	g_print("status: %03u\n", status);

	elapsed = g_timer_elapsed(timer, NULL);

	g_print("elapse: %f seconds\n", elapsed);

	if (wispr->msg.message_type < 0) {
		const char *redirect;

		if (status != 302)
			goto done;

		if (!g_web_result_get_header(result, "Location", &redirect))
			goto done;

		printf("Redirect URL: %s\n", redirect);
		printf("\n");

		wispr->request = g_web_request_get(wispr->web, redirect,
				wispr_result, wispr_route, wispr, NULL);

		return false;
	}

	printf("Message type: %s (%d)\n",
			message_type_to_string(wispr->msg.message_type),
						wispr->msg.message_type);
	printf("Response code: %s (%d)\n",
			response_code_to_string(wispr->msg.response_code),
						wispr->msg.response_code);
	if (wispr->msg.access_procedure)
		printf("Access procedure: %s\n", wispr->msg.access_procedure);
	if (wispr->msg.access_location)
		printf("Access location: %s\n", wispr->msg.access_location);
	if (wispr->msg.location_name)
		printf("Location name: %s\n", wispr->msg.location_name);
	if (wispr->msg.login_url)
		printf("Login URL: %s\n", wispr->msg.login_url);
	if (wispr->msg.abort_login_url)
		printf("Abort login URL: %s\n", wispr->msg.abort_login_url);
	if (wispr->msg.logoff_url)
		printf("Logoff URL: %s\n", wispr->msg.logoff_url);
	printf("\n");

	if (status != 200 && status != 302 && status != 404)
		goto done;

	if (wispr->msg.message_type == 100) {
		if (!wispr->username) {
			user_input("Username", false,
				   username_callback, wispr);
			return false;
		}

		if (!wispr->password) {
			user_input("Password", true, password_callback, wispr);
			return false;
		}

		g_idle_add(execute_login, wispr);
		return false;
	} else if (wispr->msg.message_type == 120 ||
					wispr->msg.message_type == 140) {
		int code = wispr->msg.response_code;
		printf("Login process: %s\n",
					code == 50 ? "SUCCESS" : "FAILURE");
	}

	if (status == 302) {
		const char *redirect;

		if (!g_web_result_get_header(result, "Location", &redirect))
			goto done;

		printf("\n");
		printf("Redirect URL: %s\n", redirect);
		printf("\n");

		wispr->request = g_web_request_get(wispr->web, redirect,
				wispr_result, NULL, wispr, NULL);

		return false;
	}

done:
	g_main_loop_quit(main_loop);

	return false;
}

static gboolean execute_login(gpointer user_data)
{
	struct wispr_session *wispr = user_data;

	wispr->request = g_web_request_post(wispr->web, wispr->msg.login_url,
					"application/x-www-form-urlencoded",
					wispr_input, wispr_result, wispr, NULL);

	wispr_msg_init(&wispr->msg);

	return FALSE;
}

static bool option_debug = false;
static gchar *option_nameserver = NULL;
static gchar *option_username = NULL;
static gchar *option_password = NULL;
static gchar *option_url = NULL;

static GOptionEntry options[] = {
	{ "debug", 'd', 0, G_OPTION_ARG_NONE, &option_debug,
					"Enable debug output" },
	{ "nameserver", 'n', 0, G_OPTION_ARG_STRING, &option_nameserver,
					"Specify nameserver", "ADDRESS" },
	{ "username", 'u', 0, G_OPTION_ARG_STRING, &option_username,
					"Specify username", "USERNAME" },
	{ "password", 'p', 0, G_OPTION_ARG_STRING, &option_password,
					"Specify password", "PASSWORD" },
	{ "url", 'U', 0, G_OPTION_ARG_STRING, &option_url,
					"Specify arbitrary request", "URL" },
	{ NULL },
};

int main(int argc, char *argv[])
{
	GOptionContext *context;
	GError *error = NULL;
	struct sigaction sa;
	struct wispr_session wispr;
	int index = 0;

	context = g_option_context_new(NULL);
	g_option_context_add_main_entries(context, options, NULL);

	if (!g_option_context_parse(context, &argc, &argv, &error)) {
		if (error) {
			g_printerr("%s\n", error->message);
			g_error_free(error);
		} else
			g_printerr("An unknown error occurred\n");
		return 1;
	}

	g_option_context_free(context);

	memset(&wispr, 0, sizeof(wispr));
	wispr_msg_init(&wispr.msg);

	wispr.web = g_web_new(index);
	if (!wispr.web) {
		fprintf(stderr, "Failed to create web service\n");
		return 1;
	}

	if (option_debug)
		g_web_set_debug(wispr.web, web_debug, "WEB");

	main_loop = g_main_loop_new(NULL, FALSE);

	if (option_nameserver) {
		g_web_add_nameserver(wispr.web, option_nameserver);
		g_free(option_nameserver);
	}

	g_web_set_accept(wispr.web, NULL);
	g_web_set_user_agent(wispr.web, "SmartClient/%s wispr", VERSION);
	g_web_set_close_connection(wispr.web, TRUE);

	if (!option_url)
		option_url = g_strdup(DEFAULT_URL);

	wispr.username = option_username;
	wispr.password = option_password;
	wispr.originurl = option_url;

	timer = g_timer_new();

	wispr.parser = g_web_parser_new("<WISPAccessGatewayParam",
						"WISPAccessGatewayParam>",
						parser_callback, &wispr);

	wispr.request = g_web_request_get(wispr.web, option_url,
			wispr_result, wispr_route, &wispr, NULL);

	if (wispr.request == 0) {
		fprintf(stderr, "Failed to start request\n");
		return 1;
	}

	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = sig_term;
	sigaction(SIGINT, &sa, NULL);
	sigaction(SIGTERM, &sa, NULL);

	g_main_loop_run(main_loop);

	g_timer_destroy(timer);

	if (wispr.request > 0)
		g_web_cancel_request(wispr.web, wispr.request);

	g_web_parser_unref(wispr.parser);
	g_web_unref(wispr.web);

	g_main_loop_unref(main_loop);

	g_free(wispr.username);
	g_free(wispr.password);
	g_free(wispr.originurl);

	return 0;
}
