/*
 * Purple's oscar protocol plugin
 * This file is the legal property of its developers.
 * Please see the AUTHORS file distributed alongside this file.
 *
 * 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  02111-1301  USA
*/

/**
 * This file implements AIM's kerberos procedure for authenticating
 * users.  This replaces the older MD5-based and XOR-based
 * authentication methods that use SNAC family 0x0017.
 *
 * This doesn't use SNACs or FLAPs at all.  It makes https
 * POSTs to AOL KDC server to validate the user based on the password they
 * provided to us.  Upon successful authentication we receive two tokens
 * in the response. One is assumed to be the kerberos ticket for authentication
 * on the various AOL websites, while the other contains BOSS information, such
 * as the hostname and port number to use, the TLS certificate name as well as
 * the cookie to use to authenticate to the BOS server.
 * And then everything else is the same as with BUCP.
 *
 */

#include "oscar.h"
#include "oscarcommon.h"
#include "core.h"

#define MAXAIMPASSLEN 16

/*
 * Incomplete X-SNAC format taken from reverse engineering doen by digsby:
 * https://github.com/ifwe/digsby/blob/master/digsby/src/oscar/login2.py
 */
typedef struct {
	aim_tlv_t *main_tlv;
	gchar *principal1;
	gchar *service;
	gchar *principal1_again;
	gchar *principal2;
	gchar unknown;
	guint8 *footer;
	struct {
		guint32 unknown1;
		guint32 unknown2;
		guint32 epoch_now;
		guint32 epoch_valid;
		guint32 epoch_renew;
		guint32 epoch_expire;
		guint32 unknown3;
		guint32 unknown4;
		guint32 unknown5;
	} dates;
	GSList *tlvlist;
} aim_xsnac_token_t;

typedef struct {
	guint16 family;
	guint16 subtype;
	guint8 flags[8];
	guint16 request_id;
	guint32 epoch;
	guint32 unknown;
	gchar *principal1;
	gchar *principal2;
	guint16 num_tokens;
	aim_xsnac_token_t *tokens;
	GSList *tlvlist;
} aim_xsnac_t;

static gchar *get_kdc_url(OscarData *od)
{
	PurpleAccount *account = purple_connection_get_account(od->gc);
	const gchar *server;
	gchar *url;
	gchar *port_str = NULL;
	gint port;

	server = purple_account_get_string(account, "server", AIM_DEFAULT_KDC_SERVER);
	port = purple_account_get_int(account, "port", AIM_DEFAULT_KDC_PORT);
	if (port != 443)
		port_str = g_strdup_printf(":%d", port);
	url = g_strdup_printf("https://%s%s/", server, port_str ? port_str : "");
	g_free(port_str);

	return url;
}

/*
 * Using kerberos auth requires a developer ID. This key is for libpurple.
 * It is the default key for all libpurple-based clients.  AOL encourages
 * UIs (especially ones with lots of users) to override this with their
 * own key.  This key is owned by the AIM account "markdoliner"
 *
 * Keys can be managed at http://developer.aim.com/manageKeys.jsp
 */
#define DEFAULT_CLIENT_KEY "ma15d7JTxbmVG-RP"

static const char *get_client_key(OscarData *od)
{
	return oscar_get_ui_info_string(
			od->icq ? "prpl-icq-clientkey" : "prpl-aim-clientkey",
			DEFAULT_CLIENT_KEY);
}

static void
aim_encode_password(const char *password, gchar *encoded)
{
	guint8 encoding_table[] = {
		0x76, 0x91, 0xc5, 0xe7,
		0xd0, 0xd9, 0x95, 0xdd,
		0x9e, 0x2F, 0xea, 0xd8,
		0x6B, 0x21, 0xc2, 0xbc,

	};
	guint i;

	/*
	 * We truncate AIM passwords to 16 characters since that's what
	 * the official client does as well.
	 */
	for (i = 0; i < strlen(password) && i < MAXAIMPASSLEN; i++)
		encoded[i] = (password[i] ^ encoding_table[i]);
}

static void
aim_xsnac_free(aim_xsnac_t *xsnac)
{
	gint i;

	g_free(xsnac->principal1);
	g_free(xsnac->principal2);
	aim_tlvlist_free(xsnac->tlvlist);

	for (i = 0; i < xsnac->num_tokens; i++) {
		g_free(xsnac->tokens[i].main_tlv->value);
		g_free(xsnac->tokens[i].main_tlv);
		g_free(xsnac->tokens[i].principal1);
		g_free(xsnac->tokens[i].service);
		g_free(xsnac->tokens[i].principal1_again);
		g_free(xsnac->tokens[i].principal2);
		g_free(xsnac->tokens[i].footer);
		aim_tlvlist_free(xsnac->tokens[i].tlvlist);
	}
	g_free(xsnac->tokens);
}

static void
kerberos_login_cb(PurpleUtilFetchUrlData *url_data, gpointer user_data,
	const gchar *got_data, gsize got_len, const gchar *error_message)
{
	OscarData *od = user_data;
	PurpleConnection *gc;
	ByteStream bs;
	aim_xsnac_t xsnac = {0};
	guint16 len;
	gchar *bosip = NULL;
	gchar *tlsCertName = NULL;
	guint8 *cookie = NULL;
	guint32 cookie_len = 0;
	char *host; int port;
	gsize i;

	gc = od->gc;

	od->url_data = NULL;

	if (error_message != NULL || got_len == 0) {
		gchar *tmp;
		gchar *url;

		url = get_kdc_url(od);
		tmp = g_strdup_printf(_("Error requesting %s: %s"),
				url, error_message ?
				error_message : _("The server returned an empty response"));
		purple_connection_error_reason(gc,
				PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
		g_free(tmp);
		g_free(url);
		return;
	}

	purple_debug_info("oscar", "Received kerberos login HTTP response %lu : ", got_len);

	byte_stream_init(&bs, (guint8 *)got_data, got_len);

	xsnac.family = byte_stream_get16(&bs);
	xsnac.subtype = byte_stream_get16(&bs);
	byte_stream_getrawbuf(&bs, (guint8 *) xsnac.flags, 8);

	if (xsnac.family == 0x50C && xsnac.subtype == 0x0005) {
		purple_connection_error_reason(gc,
			PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED,
			_("Incorrect password"));
		return;
	}
	if (xsnac.family != 0x50C || xsnac.subtype != 0x0003) {
		purple_connection_error_reason(gc,
				PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
			_("Error parsing response from authentication server"));
		return;
	}
	xsnac.request_id = byte_stream_get16(&bs);
	xsnac.epoch = byte_stream_get32(&bs);
	xsnac.unknown = byte_stream_get32(&bs);
	len = byte_stream_get16(&bs);
	xsnac.principal1 = byte_stream_getstr(&bs, len);
	len = byte_stream_get16(&bs);
	xsnac.principal2 = byte_stream_getstr(&bs, len);
	xsnac.num_tokens = byte_stream_get16(&bs);

	purple_debug_info("oscar", "KDC: %d tokens between '%s' and '%s'\n",
		xsnac.num_tokens, xsnac.principal1, xsnac.principal2);
	xsnac.tokens = g_new0(aim_xsnac_token_t, xsnac.num_tokens);
	for (i = 0; i < xsnac.num_tokens; i++) {
		GSList *tlv;

		tlv = aim_tlvlist_readnum(&bs, 1);
		if (tlv)
			xsnac.tokens[i].main_tlv = tlv->data;
		g_slist_free(tlv);

		len = byte_stream_get16(&bs);
		xsnac.tokens[i].principal1 = byte_stream_getstr(&bs, len);
		len = byte_stream_get16(&bs);
		xsnac.tokens[i].service = byte_stream_getstr(&bs, len);
		len = byte_stream_get16(&bs);
		xsnac.tokens[i].principal1_again = byte_stream_getstr(&bs, len);
		len = byte_stream_get16(&bs);
		xsnac.tokens[i].principal2 = byte_stream_getstr(&bs, len);
		xsnac.tokens[i].unknown = byte_stream_get8(&bs);
		len = byte_stream_get16(&bs);
		xsnac.tokens[i].footer = byte_stream_getraw(&bs, len);

		xsnac.tokens[i].dates.unknown1 = byte_stream_get32(&bs);
		xsnac.tokens[i].dates.unknown2 = byte_stream_get32(&bs);
		xsnac.tokens[i].dates.epoch_now = byte_stream_get32(&bs);
		xsnac.tokens[i].dates.epoch_valid = byte_stream_get32(&bs);
		xsnac.tokens[i].dates.epoch_renew = byte_stream_get32(&bs);
		xsnac.tokens[i].dates.epoch_expire = byte_stream_get32(&bs);
		xsnac.tokens[i].dates.unknown3 = byte_stream_get32(&bs);
		xsnac.tokens[i].dates.unknown4 = byte_stream_get32(&bs);
		xsnac.tokens[i].dates.unknown5 = byte_stream_get32(&bs);

		len = byte_stream_get16(&bs);
		xsnac.tokens[i].tlvlist = aim_tlvlist_readnum(&bs, len);

		purple_debug_info("oscar", "Token %lu has %d TLVs for service '%s'\n",
			i, len, xsnac.tokens[i].service);
	}
	len = byte_stream_get16(&bs);
	xsnac.tlvlist = aim_tlvlist_readnum(&bs, len);

	for (i = 0; i < xsnac.num_tokens; i++) {
		if (purple_strequal(xsnac.tokens[i].service, "im/boss")) {
			aim_tlv_t *tlv;
			GSList *tlvlist;
			ByteStream tbs;

			tlv = aim_tlv_gettlv(xsnac.tokens[i].tlvlist, 0x0003, 1);
			if (tlv != NULL) {
				byte_stream_init(&tbs, tlv->value, tlv->length);
				byte_stream_get32(&tbs);
				tlvlist =  aim_tlvlist_read(&tbs);
				if (aim_tlv_gettlv(tlvlist, 0x0005, 1))
					bosip = aim_tlv_getstr(tlvlist, 0x0005, 1);
				if (aim_tlv_gettlv(tlvlist, 0x0005, 1))
					tlsCertName = aim_tlv_getstr(tlvlist, 0x008D, 1);
				tlv = aim_tlv_gettlv(tlvlist, 0x0006, 1);
				if (tlv) {
					cookie_len = tlv->length;
					cookie = tlv->value;
				}
			}
			break;
		}
	}
	if (bosip && cookie) {
		port = AIM_DEFAULT_KDC_PORT;
		for (i = 0; i < strlen(bosip); i++) {
			if (bosip[i] == ':') {
				port = atoi(&(bosip[i+1]));
				break;
			}
		}
		host = g_strndup(bosip, i);
		oscar_connect_to_bos(gc, od, host, port, cookie, cookie_len, tlsCertName);
		g_free(host);
	} else {
		purple_connection_error_reason(gc,
			PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
			_("Unknown error during authentication"));
	}
	aim_xsnac_free(&xsnac);
	g_free(tlsCertName);
	g_free(bosip);
}

/**
 * This function sends a binary blob request to the Kerberos KDC server
 * https://kdc.uas.aol.com with the user's username and password and
 * receives the IM cookie, which is used to request a connection to the
 * BOSS server.
 * The binary data below is what AIM 8.0.8.1 sends in order to authenticate
 * to the KDC server. It is an 'X-SNAC' packet, which is relatively similar
 * to SNAC packets but somehow different.
 * The header starts with the 0x50C family follow by 0x0002 subtype, then
 * some fixed length data and TLVs. The string "COOL" appears in there for
 * some reason followed by the 'US' and 'en' strings.
 * Then the 'imApp key=<client key>' comes after that, and then the username
 * and the string "im/boss" which seems to represent the service we are
 * requesting the authentication for. Changing that will lead to a
 * 'unknown service' error. The client key is then added again (without the
 * 'imApp key' string prepended to it) then a XOR-ed version of the password.
 * The meaning of the header/footer/in-between bytes is not known but never
 * seems to change so there is no need to reverse engineer their meaning at
 * this point.
 */
void send_kerberos_login(OscarData *od, const char *username)
{
	PurpleConnection *gc;
	GString *request;
	gchar *url;
	const gchar *password;
	gchar password_xored[MAXAIMPASSLEN];
	const gchar *client_key;
	gchar *imapp_key;
	GString *body;
	guint16 len_be;
	guint16 reqid;
	const gchar header[] = {
		0x05, 0x0C, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00,
		0x00, 0x08, 0x10, 0x00, 0x00, 0x00, 0x00, 0x05,
		0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x05,
		0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x18, 0x99,
		0x00, 0x05, 0x00, 0x04, 0x43, 0x4F, 0x4F, 0x4C,
		0x00, 0x0A, 0x00, 0x02, 0x00, 0x01, 0x00, 0x0B,
		0x00, 0x04, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00,
		0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
		0x55, 0x53, 0x00, 0x02, 0x65, 0x6E, 0x00, 0x04,
		0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0D,
		0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04,
		0x00, 0x05};
	const gchar pre_username[] = {
		0x00, 0x07, 0x00, 0x04, 0x00, 0x00, 0x01, 0x8B,
		0x01, 0x00, 0x00, 0x00, 0x00};
	const gchar post_username[] = {
		0x00, 0x07, 0x69, 0x6D, 0x2F, 0x62, 0x6F, 0x73,
		0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x04, 0x00, 0x02};
	const gchar pre_password[] = {
		0x40, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01,
		0x00, 0x00};
	const gchar post_password[] = {0x00, 0x00, 0x00, 0x1D};
	const gchar footer[] = {
		0x00, 0x21, 0x00, 0x32, 0x00, 0x01, 0x10, 0x03,
		0x00, 0x2C, 0x00, 0x07, 0x00, 0x14, 0x00, 0x04,
		0x00, 0x00, 0x01, 0x8B, 0x00, 0x16, 0x00, 0x02,
		0x00, 0x26, 0x00, 0x17, 0x00, 0x02, 0x00, 0x07,
		0x00, 0x18, 0x00, 0x02, 0x00, 0x00, 0x00, 0x19,
		0x00, 0x02, 0x00, 0x0D, 0x00, 0x1A, 0x00, 0x02,
		0x00, 0x04, 0x00, 0xAB, 0x00, 0x00, 0x00, 0x28,
		0x00, 0x00};

	gc = od->gc;

	password = purple_connection_get_password(gc);
	aim_encode_password(password, password_xored);

	client_key = get_client_key(od);
	imapp_key = g_strdup_printf("imApp key=%s", client_key);

	/* Construct the body of the HTTP POST request */
	body = g_string_new(NULL);
	g_string_append_len(body, header, sizeof(header));
	reqid = (guint16) g_random_int();
	g_string_overwrite_len(body, 0xC, (void *)&reqid, sizeof(guint16));

	len_be = GUINT16_TO_BE(strlen(imapp_key));
	g_string_append_len(body, (void *)&len_be, sizeof(guint16));
	g_string_append(body, imapp_key);

	len_be = GUINT16_TO_BE(strlen(username));
	g_string_append_len(body, pre_username, sizeof(pre_username));
	g_string_append_len(body, (void *)&len_be, sizeof(guint16));
	g_string_append(body, username);
	g_string_append_len(body, post_username, sizeof(post_username));

	len_be = GUINT16_TO_BE(strlen(password) + 0x10);
	g_string_append_len(body, (void *)&len_be, sizeof(guint16));
	g_string_append_len(body, pre_password, sizeof(pre_password));
	len_be = GUINT16_TO_BE(strlen(password) + 4);
	g_string_append_len(body, (void *)&len_be, sizeof(guint16));
	len_be = GUINT16_TO_BE(strlen(password));
	g_string_append_len(body, (void *)&len_be, sizeof(guint16));
	g_string_append_len(body, password_xored, strlen(password));
	g_string_append_len(body, post_password, sizeof(post_password));

	len_be = GUINT16_TO_BE(strlen(client_key));
	g_string_append_len(body, (void *)&len_be, sizeof(guint16));
	g_string_append(body, client_key);
	g_string_append_len(body, footer, sizeof(footer));

	g_free(imapp_key);

	url = get_kdc_url(od);

	/* Construct an HTTP POST request */
	request = g_string_new("POST / HTTP/1.1\n"
			"Connection: close\n"
			"Accept: application/x-snac\n");

	/* Tack on the body */
	g_string_append_printf(request, "Content-Type: application/x-snac\n");
	g_string_append_printf(request, "Content-Length: %" G_GSIZE_FORMAT "\n\n", body->len);
	g_string_append_len(request, body->str, body->len);

	/* Send the POST request  */
	od->url_data = purple_util_fetch_url_request_data_len_with_account(
			purple_connection_get_account(gc), url,
			TRUE, NULL, TRUE, request->str, request->len, FALSE, -1,
			kerberos_login_cb, od);
	g_string_free(request, TRUE);

	g_string_free(body, TRUE);
	g_free(url);
}
