libpurple/protocols/gg/edisc.c

Thu, 09 Oct 2014 20:53:42 -0400

author
Daniel Atallah <datallah@pidgin.im>
date
Thu, 09 Oct 2014 20:53:42 -0400
changeset 36168
0203e8dce52d
parent 36068
e9b9320a985a
child 37158
96b5ab42da00
permissions
-rw-r--r--

Improve gg error message. Fixes #16196.

/* purple
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here. Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * Component written by Tomek Wasilczyk (http://www.wasilczyk.pl).
 *
 * This file is dual-licensed under the GPL2+ and the X11 (MIT) licences.
 * As a recipient of this file you may choose, which license to receive the
 * code under. As a contributor, you have to ensure the new code is
 * compatible with both.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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 Street, Fifth Floor, Boston, MA  02111-1301  USA
 */
#include "edisc.h"

#include <debug.h>
#include <glibcompat.h>

#include "gg.h"
#include "libgaduw.h"
#include "utils.h"
#include <http.h>

#include <json-glib/json-glib.h>

#define GGP_EDISC_OS "WINNT x86-msvc"
#define GGP_EDISC_TYPE "desktop"
#define GGP_EDISC_API "6"

#define GGP_EDISC_RESPONSE_MAX 10240
#define GGP_EDISC_FNAME_ALLOWED "1234567890" \
	"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" \
	" [](){}-+=_;'<>,.&$!"

typedef struct _ggp_edisc_auth_data ggp_edisc_auth_data;

typedef struct _ggp_edisc_xfer ggp_edisc_xfer;

struct _ggp_edisc_session_data
{
	GHashTable *xfers_initialized;
	GHashTable *xfers_history;

	PurpleHttpCookieJar *cookies;
	gchar *security_token;

	PurpleHttpConnection *auth_request;
	gboolean auth_done;
	GList *auth_pending;
};

struct _ggp_edisc_xfer
{
	gchar *filename;
	gchar *ticket_id;

	gboolean allowed, ready;

	PurpleConnection *gc;
	PurpleHttpConnection *hc;

	gsize already_read;
};

typedef enum
{
	GGP_EDISC_XFER_ACK_STATUS_UNKNOWN,
	GGP_EDISC_XFER_ACK_STATUS_ALLOWED,
	GGP_EDISC_XFER_ACK_STATUS_REJECTED
} ggp_edisc_xfer_ack_status;

typedef void (*ggp_ggdrive_auth_cb)(PurpleConnection *gc, gboolean success,
	gpointer user_data);

/* Setting up. */
static inline ggp_edisc_session_data *
ggp_edisc_get_sdata(PurpleConnection *gc);

/* Misc. */
static void ggp_edisc_set_defaults(PurpleHttpRequest *req);
static int ggp_edisc_parse_error(const gchar *data);

/* General xfer functions. */
static void ggp_edisc_xfer_free(PurpleXfer *xfer);
static void ggp_edisc_xfer_error(PurpleXfer *xfer, const gchar *msg);
static void ggp_edisc_xfer_cancel(PurpleXfer *xfer);
static const gchar * ggp_edisc_xfer_ticket_url(const gchar *ticket_id);
static void ggp_edisc_xfer_progress_watcher(PurpleHttpConnection *hc,
	gboolean reading_state, int processed, int total, gpointer _xfer);
static ggp_edisc_xfer_ack_status
ggp_edisc_xfer_parse_ack_status(const gchar *str);


/* Sending a file. */
static void ggp_edisc_xfer_send_ticket_changed(PurpleConnection *gc,
	PurpleXfer *xfer, gboolean is_allowed);

/* Receiving a file. */
static void ggp_edisc_xfer_recv_reject(PurpleXfer *xfer);
static void ggp_edisc_xfer_recv_ticket_got(PurpleConnection *gc,
	const gchar *ticket_id);
static void ggp_edisc_xfer_recv_ticket_completed(PurpleXfer *xfer);

/* Authentication. */
static void ggp_ggdrive_auth(PurpleConnection *gc, ggp_ggdrive_auth_cb cb,
	gpointer user_data);

/*******************************************************************************
 * Setting up.
 ******************************************************************************/

static inline ggp_edisc_session_data *
ggp_edisc_get_sdata(PurpleConnection *gc)
{
	GGPInfo *accdata;

	PURPLE_ASSERT_CONNECTION_IS_VALID(gc);

	accdata = purple_connection_get_protocol_data(gc);
	g_return_val_if_fail(accdata != NULL, NULL);

	return accdata->edisc_data;
}

void ggp_edisc_setup(PurpleConnection *gc)
{
	GGPInfo *accdata = purple_connection_get_protocol_data(gc);
	ggp_edisc_session_data *sdata = g_new0(ggp_edisc_session_data, 1);

	accdata->edisc_data = sdata;

	sdata->cookies = purple_http_cookie_jar_new();
	sdata->xfers_initialized = g_hash_table_new(g_str_hash, g_str_equal);
	sdata->xfers_history = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
}

void ggp_edisc_cleanup(PurpleConnection *gc)
{
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(gc);

	g_return_if_fail(sdata != NULL);

	purple_http_conn_cancel(sdata->auth_request);
	g_list_free_full(sdata->auth_pending, g_free);
	g_free(sdata->security_token);

	purple_http_cookie_jar_unref(sdata->cookies);
	g_hash_table_destroy(sdata->xfers_initialized);
	g_hash_table_destroy(sdata->xfers_history);

	g_free(sdata);
}

/*******************************************************************************
 * Misc.
 ******************************************************************************/

static void ggp_edisc_set_defaults(PurpleHttpRequest *req)
{
	purple_http_request_set_max_len(req, GGP_EDISC_RESPONSE_MAX);
	purple_http_request_header_set(req, "X-gged-api-version",
		GGP_EDISC_API);

	/* optional fields */
	purple_http_request_header_set(req, "User-Agent", "Mozilla/5.0 (Windows"
		" NT 6.1; rv:11.0) Gecko/20120613 GG/11.0.0.8169 (WINNT_x86-msv"
		"c; pl; beta; standard)");
	purple_http_request_header_set(req, "Accept", "text/html,application/xh"
		"tml+xml,application/xml;q=0.9,*/*;q=0.8");
	purple_http_request_header_set(req, "Accept-Language",
		"pl,en-us;q=0.7,en;q=0.3");
	/* purple_http_request_header_set(req, "Accept-Encoding",
	 * "gzip, deflate"); */
	purple_http_request_header_set(req, "Accept-Charset",
		"ISO-8859-2,utf-8;q=0.7,*;q=0.7");
	purple_http_request_header_set(req, "Connection", "keep-alive");
	purple_http_request_header_set(req, "Content-Type",
		"application/x-www-form-urlencoded; charset=UTF-8");
}

static int ggp_edisc_parse_error(const gchar *data)
{
	JsonParser *parser;
	JsonObject *result;
	int error_id;

	parser = ggp_json_parse(data);
	result = json_node_get_object(json_parser_get_root(parser));
	result = json_object_get_object_member(result, "result");
	error_id = json_object_get_int_member(result, "appStatus");
	purple_debug_info("gg", "edisc error: %s (%d)\n",
		json_object_get_string_member(result, "errorMsg"),
		error_id);
	g_object_unref(parser);

	return error_id;
}

/*******************************************************************************
 * General xfer functions.
 ******************************************************************************/

static void ggp_edisc_xfer_free(PurpleXfer *xfer)
{
	ggp_edisc_session_data *sdata;
	ggp_edisc_xfer *edisc_xfer = purple_xfer_get_protocol_data(xfer);

	if (edisc_xfer == NULL)
		return;

	g_free(edisc_xfer->filename);
	purple_http_conn_cancel(edisc_xfer->hc);

	sdata = ggp_edisc_get_sdata(edisc_xfer->gc);
	g_return_if_fail(sdata != NULL);
	if (edisc_xfer->ticket_id != NULL)
		g_hash_table_remove(sdata->xfers_initialized,
			edisc_xfer->ticket_id);

	g_free(edisc_xfer);
	purple_xfer_set_protocol_data(xfer, NULL);

}

static void ggp_edisc_xfer_error(PurpleXfer *xfer, const gchar *msg)
{
	if (purple_xfer_is_cancelled(xfer))
		g_return_if_reached();
	purple_xfer_set_status(xfer, PURPLE_XFER_STATUS_CANCEL_REMOTE);
	purple_xfer_conversation_write(xfer, msg, TRUE);
	purple_xfer_error(
		purple_xfer_get_xfer_type(xfer),
		purple_xfer_get_account(xfer),
		purple_xfer_get_remote_user(xfer),
		msg);
	ggp_edisc_xfer_free(xfer);
	purple_xfer_end(xfer);
}

void ggp_edisc_xfer_ticket_changed(PurpleConnection *gc, const char *data)
{
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(gc);
	PurpleXfer *xfer;
	JsonParser *parser;
	JsonObject *ticket;
	const gchar *ticket_id, *send_status;
	ggp_edisc_xfer_ack_status ack_status;
	gboolean is_completed;

	g_return_if_fail(sdata != NULL);

	parser = ggp_json_parse(data);
	ticket = json_node_get_object(json_parser_get_root(parser));
	ticket_id = json_object_get_string_member(ticket, "id");
	ack_status = ggp_edisc_xfer_parse_ack_status(
		json_object_get_string_member(ticket, "ack_status"));
	send_status = json_object_get_string_member(ticket, "send_status");

	if (ticket_id == NULL)
		ticket_id = "";
	xfer = g_hash_table_lookup(sdata->xfers_initialized, ticket_id);
	if (xfer == NULL) {
		purple_debug_misc("gg", "ggp_edisc_event_ticket_changed: "
			"ticket %s not found, updating it...\n",
			purple_debug_is_unsafe() ? ticket_id : "");
		ggp_edisc_xfer_recv_ticket_got(gc, ticket_id);
		g_object_unref(parser);
		return;
	}

	is_completed = FALSE;
	if (g_strcmp0("in_progress", send_status) == 0) {
		/* do nothing */
	} else if (g_strcmp0("completed", send_status) == 0) {
		is_completed = TRUE;
	} else if (g_strcmp0("expired", send_status) == 0)
		ggp_edisc_xfer_error(xfer, _("File transfer expired."));
	else {
		purple_debug_warning("gg", "ggp_edisc_event_ticket_changed: "
			"unknown send_status=%s\n", send_status);
		g_object_unref(parser);
		return;
	}

	g_object_unref(parser);

	if (purple_xfer_get_xfer_type(xfer) == PURPLE_XFER_TYPE_RECEIVE) {
		if (is_completed)
			ggp_edisc_xfer_recv_ticket_completed(xfer);
	} else {
		if (ack_status != GGP_EDISC_XFER_ACK_STATUS_UNKNOWN)
			ggp_edisc_xfer_send_ticket_changed(gc, xfer, ack_status
				== GGP_EDISC_XFER_ACK_STATUS_ALLOWED);
	}

}

static void ggp_edisc_xfer_cancel(PurpleXfer *xfer)
{
	g_return_if_fail(xfer != NULL);

	ggp_edisc_xfer_free(xfer);
}

static const gchar * ggp_edisc_xfer_ticket_url(const gchar *ticket_id)
{
	static gchar ticket_url[150];

	g_snprintf(ticket_url, sizeof(ticket_url),
		"https://drive.mpa.gg.pl/send_ticket/%s", ticket_id);

	return ticket_url;
}

static void ggp_edisc_xfer_progress_watcher(PurpleHttpConnection *hc,
	gboolean reading_state, int processed, int total, gpointer _xfer)
{
	PurpleXfer *xfer = _xfer;
	gboolean eof;
	int total_real;

	if (purple_xfer_get_xfer_type(xfer) == PURPLE_XFER_TYPE_RECEIVE) {
		if (!reading_state)
			return;
	} else {
		if (reading_state)
			return;
	}

	eof = (processed >= total);
	total_real = purple_xfer_get_size(xfer);
	if (eof || processed > total_real)
		processed = total_real; /* just to be sure */

	purple_xfer_set_bytes_sent(xfer, processed);
	purple_xfer_update_progress(xfer);
}

static ggp_edisc_xfer_ack_status
ggp_edisc_xfer_parse_ack_status(const gchar *str)
{
	g_return_val_if_fail(str != NULL, GGP_EDISC_XFER_ACK_STATUS_UNKNOWN);

	if (g_strcmp0("unknown", str) == 0)
		return GGP_EDISC_XFER_ACK_STATUS_UNKNOWN;
	if (g_strcmp0("allowed", str) == 0)
		return GGP_EDISC_XFER_ACK_STATUS_ALLOWED;
	if (g_strcmp0("rejected", str) == 0)
		return GGP_EDISC_XFER_ACK_STATUS_REJECTED;

	purple_debug_warning("gg", "ggp_edisc_xfer_parse_ack_status: "
		"unknown status (%s)\n", str);
	return GGP_EDISC_XFER_ACK_STATUS_UNKNOWN;
}

/*******************************************************************************
 * Sending a file.
 ******************************************************************************/

static void ggp_edisc_xfer_send_init(PurpleXfer *xfer);
static void ggp_edisc_xfer_send_init_authenticated(PurpleConnection *gc,
	gboolean success, gpointer _xfer);
static void ggp_edisc_xfer_send_init_ticket_created(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer _xfer);
static void ggp_edisc_xfer_send_reader(PurpleHttpConnection *hc,
	gchar *buffer, size_t offset, size_t length, gpointer _xfer,
	PurpleHttpContentReaderCb cb);
static void ggp_edisc_xfer_send_start(PurpleXfer *xfer);
static void ggp_edisc_xfer_send_done(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer _xfer);

gboolean ggp_edisc_xfer_can_receive_file(PurpleConnection *gc,
	const char *who)
{
	PurpleBuddy *buddy;

	g_return_val_if_fail(gc != NULL, FALSE);
	g_return_val_if_fail(who != NULL, FALSE);

	buddy = purple_blist_find_buddy(purple_connection_get_account(gc), who);
	if (buddy == NULL)
		return FALSE;

	/* TODO: check, if this buddy have us on his list */

	return PURPLE_BUDDY_IS_ONLINE(buddy);
}

static void ggp_edisc_xfer_send_init(PurpleXfer *xfer)
{
	ggp_edisc_xfer *edisc_xfer = purple_xfer_get_protocol_data(xfer);

	purple_xfer_set_status(xfer, PURPLE_XFER_STATUS_NOT_STARTED);

	edisc_xfer->filename = g_strdup(purple_xfer_get_filename(xfer));
	g_strcanon(edisc_xfer->filename, GGP_EDISC_FNAME_ALLOWED, '_');

	ggp_ggdrive_auth(edisc_xfer->gc, ggp_edisc_xfer_send_init_authenticated,
		xfer);
}

static void ggp_edisc_xfer_send_init_authenticated(PurpleConnection *gc,
	gboolean success, gpointer _xfer)
{
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(gc);
	PurpleHttpRequest *req;
	PurpleXfer *xfer = _xfer;
	ggp_edisc_xfer *edisc_xfer = purple_xfer_get_protocol_data(xfer);
	gchar *data;

	if (purple_xfer_is_cancelled(xfer))
		return;

	if (!success) {
		ggp_edisc_xfer_error(xfer, _("Authentication failed"));
		return;
	}

	g_return_if_fail(sdata != NULL);

	req = purple_http_request_new("https://drive.mpa.gg.pl/send_ticket");
	purple_http_request_set_method(req, "PUT");

	ggp_edisc_set_defaults(req);
	purple_http_request_set_cookie_jar(req, sdata->cookies);

	purple_http_request_header_set(req, "X-gged-security-token",
		sdata->security_token);

	data = g_strdup_printf("{\"send_ticket\":{"
		"\"recipient\":\"%s\","
		"\"file_name\":\"%s\","
		"\"file_size\":\"%u\""
		"}}",
		purple_xfer_get_remote_user(xfer),
		edisc_xfer->filename,
		(int)purple_xfer_get_size(xfer));
	purple_http_request_set_contents(req, data, -1);
	g_free(data);

	edisc_xfer->hc = purple_http_request(gc, req,
		ggp_edisc_xfer_send_init_ticket_created, xfer);
	purple_http_request_unref(req);
}

static void ggp_edisc_xfer_send_init_ticket_created(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer _xfer)
{
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(
		purple_http_conn_get_purple_connection(hc));
	PurpleXfer *xfer = _xfer;
	ggp_edisc_xfer *edisc_xfer = purple_xfer_get_protocol_data(xfer);
	const gchar *data = purple_http_response_get_data(response, NULL);
	ggp_edisc_xfer_ack_status ack_status;
	JsonParser *parser;
	JsonObject *ticket;

	if (purple_xfer_is_cancelled(xfer))
		return;

	g_return_if_fail(sdata != NULL);

	edisc_xfer->hc = NULL;

	if (!purple_http_response_is_successful(response)) {
		int error_id = ggp_edisc_parse_error(data);
		if (error_id == 206) /* recipient not logged in */
			ggp_edisc_xfer_error(xfer,
				_("Recipient not logged in"));
		else if (error_id == 207) /* bad sender recipient relation */
			ggp_edisc_xfer_error(xfer, _("You aren't on the "
                                "recipient's buddy list"));
		else
			ggp_edisc_xfer_error(xfer,
				_("Unable to send file"));
		return;
	}

	parser = ggp_json_parse(data);
	ticket = json_node_get_object(json_parser_get_root(parser));
	ticket = json_object_get_object_member(ticket, "result");
	ticket = json_object_get_object_member(ticket, "send_ticket");
	edisc_xfer->ticket_id = g_strdup(json_object_get_string_member(
		ticket, "id"));
	ack_status = ggp_edisc_xfer_parse_ack_status(
		json_object_get_string_member(ticket, "ack_status"));
	/* send_mode: "normal", "publink" (for legacy clients) */

	g_object_unref(parser);

	if (edisc_xfer->ticket_id == NULL) {
		purple_debug_error("gg",
			"ggp_edisc_xfer_send_init_ticket_created: "
			"couldn't get ticket id\n");
		return;
	}

	purple_debug_info("gg", "ggp_edisc_xfer_send_init_ticket_created: "
		"ticket \"%s\" created\n", edisc_xfer->ticket_id);

	g_hash_table_insert(sdata->xfers_initialized,
		edisc_xfer->ticket_id, xfer);
	g_hash_table_insert(sdata->xfers_history,
		g_strdup(edisc_xfer->ticket_id), GINT_TO_POINTER(1));

	if (ack_status != GGP_EDISC_XFER_ACK_STATUS_UNKNOWN)
		ggp_edisc_xfer_send_ticket_changed(edisc_xfer->gc, xfer,
			ack_status == GGP_EDISC_XFER_ACK_STATUS_ALLOWED);
}

void ggp_edisc_xfer_send_ticket_changed(PurpleConnection *gc, PurpleXfer *xfer,
	gboolean is_allowed)
{
	ggp_edisc_xfer *edisc_xfer = purple_xfer_get_protocol_data(xfer);
	if (!edisc_xfer) {
		purple_debug_fatal("gg", "ggp_edisc_event_ticket_changed: "
			"transfer %p already free'd\n", xfer);
		return;
	}

	if (!is_allowed) {
		purple_debug_info("gg", "ggp_edisc_event_ticket_changed: "
			"transfer %p rejected\n", xfer);
		purple_xfer_cancel_remote(xfer);
		ggp_edisc_xfer_free(xfer);
		return;
	}

	if (edisc_xfer->allowed) {
		purple_debug_misc("gg", "ggp_edisc_event_ticket_changed: "
			"transfer %p already allowed\n", xfer);
		return;
	}
	edisc_xfer->allowed = TRUE;

	purple_xfer_start(xfer, -1, NULL, 0);
}

static void ggp_edisc_xfer_send_reader(PurpleHttpConnection *hc,
	gchar *buffer, size_t offset, size_t length, gpointer _xfer,
	PurpleHttpContentReaderCb cb)
{
	PurpleXfer *xfer = _xfer;
	ggp_edisc_xfer *edisc_xfer;
	int stored;
	gboolean success, eof = FALSE;

	g_return_if_fail(xfer != NULL);
	edisc_xfer = purple_xfer_get_protocol_data(xfer);
	g_return_if_fail(edisc_xfer != NULL);

	if (edisc_xfer->already_read != offset) {
		purple_debug_error("gg", "ggp_edisc_xfer_send_reader: "
			"Invalid offset (%" G_GSIZE_FORMAT " != %" G_GSIZE_FORMAT ")\n",
			edisc_xfer->already_read, offset);
		ggp_edisc_xfer_error(xfer, _("Error while reading a file"));
		return;
	}

	stored = purple_xfer_read_file(xfer, (guchar *)buffer, length);

	if (stored < 0)
		success = FALSE;
	else {
		success = TRUE;
		edisc_xfer->already_read += stored;
		eof = ((goffset)edisc_xfer->already_read >= purple_xfer_get_size(xfer));
	}

	cb(hc, success, eof, stored);
}

static void ggp_edisc_xfer_send_start(PurpleXfer *xfer)
{
	ggp_edisc_session_data *sdata;
	ggp_edisc_xfer *edisc_xfer;
	gchar *upload_url, *filename_e;
	PurpleHttpRequest *req;

	g_return_if_fail(xfer != NULL);
	edisc_xfer = purple_xfer_get_protocol_data(xfer);
	g_return_if_fail(edisc_xfer != NULL);
	sdata = ggp_edisc_get_sdata(edisc_xfer->gc);
	g_return_if_fail(sdata != NULL);

	filename_e = purple_strreplace(edisc_xfer->filename, " ", "%20");
	upload_url = g_strdup_printf("https://drive.mpa.gg.pl/me/file/outbox/"
		"%s%%2C%s", edisc_xfer->ticket_id, filename_e);
	g_free(filename_e);
	req = purple_http_request_new(upload_url);
	g_free(upload_url);

	purple_http_request_set_method(req, "PUT");
	purple_http_request_set_timeout(req, -1);

	ggp_edisc_set_defaults(req);
	purple_http_request_set_cookie_jar(req, sdata->cookies);

	purple_http_request_header_set(req, "X-gged-local-revision", "0");
	purple_http_request_header_set(req, "X-gged-security-token",
		sdata->security_token);
	purple_http_request_header_set(req, "X-gged-metadata",
		"{\"node_type\": \"file\"}");

	purple_http_request_set_contents_reader(req, ggp_edisc_xfer_send_reader,
		purple_xfer_get_size(xfer), xfer);

	edisc_xfer->hc = purple_http_request(edisc_xfer->gc, req,
		ggp_edisc_xfer_send_done, xfer);
	purple_http_request_unref(req);

	purple_http_conn_set_progress_watcher(edisc_xfer->hc,
		ggp_edisc_xfer_progress_watcher, xfer, 250000);
}

static void ggp_edisc_xfer_send_done(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer _xfer)
{
	PurpleXfer *xfer = _xfer;
	ggp_edisc_xfer *edisc_xfer = purple_xfer_get_protocol_data(xfer);
	const gchar *data = purple_http_response_get_data(response, NULL);
	JsonParser *parser;
	JsonObject *result;
	int result_status = -1;

	if (purple_xfer_is_cancelled(xfer))
		return;

	g_return_if_fail(edisc_xfer != NULL);

	edisc_xfer->hc = NULL;

	if (!purple_http_response_is_successful(response)) {
		ggp_edisc_xfer_error(xfer, _("Error while sending a file"));
		return;
	}

	parser = ggp_json_parse(data);
	result = json_node_get_object(json_parser_get_root(parser));
	result = json_object_get_object_member(result, "result");
	if (json_object_has_member(result, "status"))
		result_status = json_object_get_int_member(result, "status");
	g_object_unref(parser);

	if (result_status == 0) {
		purple_xfer_set_completed(xfer, TRUE);
		purple_xfer_end(xfer);
		ggp_edisc_xfer_free(xfer);
	} else
		ggp_edisc_xfer_error(xfer, _("Error while sending a file"));
}

PurpleXfer * ggp_edisc_xfer_send_new(PurpleConnection *gc, const char *who)
{
	PurpleXfer *xfer;
	ggp_edisc_xfer *edisc_xfer;

	g_return_val_if_fail(gc != NULL, NULL);
	g_return_val_if_fail(who != NULL, NULL);

	xfer = purple_xfer_new(purple_connection_get_account(gc),
		PURPLE_XFER_TYPE_SEND, who);
	edisc_xfer = g_new0(ggp_edisc_xfer, 1);
	purple_xfer_set_protocol_data(xfer, edisc_xfer);

	edisc_xfer->gc = gc;

	purple_xfer_set_init_fnc(xfer, ggp_edisc_xfer_send_init);
	purple_xfer_set_start_fnc(xfer, ggp_edisc_xfer_send_start);
	purple_xfer_set_cancel_send_fnc(xfer, ggp_edisc_xfer_cancel);

	return xfer;
}

void ggp_edisc_xfer_send_file(PurpleConnection *gc, const char *who,
	const char *filename)
{
	PurpleXfer *xfer;

	g_return_if_fail(gc != NULL);
	g_return_if_fail(who != NULL);

	/* Nothing interesting here, this code is common among prpls.
	 * See ggp_edisc_xfer_send_new. */

	xfer = ggp_edisc_xfer_send_new(gc, who);
	if (filename)
		purple_xfer_request_accepted(xfer, filename);
	else
		purple_xfer_request(xfer);
}

/*******************************************************************************
 * Receiving a file.
 ******************************************************************************/

static void ggp_edisc_xfer_recv_ticket_update_authenticated(
	PurpleConnection *gc, gboolean success, gpointer _ticket);
static void ggp_edisc_xfer_recv_ticket_update_got(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer user_data);
static PurpleXfer * ggp_edisc_xfer_recv_new(PurpleConnection *gc,
	const char *who);
static void ggp_edisc_xfer_recv_accept(PurpleXfer *xfer);
static void ggp_edisc_xfer_recv_start(PurpleXfer *xfer);
static void ggp_edisc_xfer_recv_ack(PurpleXfer *xfer, gboolean accept);
static void ggp_edisc_xfer_recv_ack_done(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer _xfer);
static gboolean ggp_edisc_xfer_recv_writer(PurpleHttpConnection *http_conn,
	PurpleHttpResponse *response, const gchar *buffer, size_t offset,
	size_t length, gpointer user_data);
static void ggp_edisc_xfer_recv_done(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer _xfer);

static void ggp_edisc_xfer_recv_ticket_got(PurpleConnection *gc,
	const gchar *ticket_id)
{
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(gc);

	g_return_if_fail(sdata != NULL);

	if (g_hash_table_lookup(sdata->xfers_history, ticket_id))
		return;

	ggp_ggdrive_auth(gc, ggp_edisc_xfer_recv_ticket_update_authenticated,
		g_strdup(ticket_id));
}

static void ggp_edisc_xfer_recv_ticket_update_authenticated(
	PurpleConnection *gc, gboolean success, gpointer _ticket)
{
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(gc);
	PurpleHttpRequest *req;
	gchar *ticket = _ticket;

	g_return_if_fail(sdata != NULL);

	if (!success) {
		purple_debug_warning("gg",
			"ggp_edisc_xfer_recv_ticket_update_authenticated: "
			"update of ticket %s aborted due to authentication "
			"failure\n", ticket);
		g_free(ticket);
		return;
	}

	req = purple_http_request_new(ggp_edisc_xfer_ticket_url(ticket));
	g_free(ticket);

	ggp_edisc_set_defaults(req);
	purple_http_request_set_cookie_jar(req, sdata->cookies);

	purple_http_request_header_set(req, "X-gged-security-token",
		sdata->security_token);

	purple_http_request(gc, req, ggp_edisc_xfer_recv_ticket_update_got,
		NULL);
	purple_http_request_unref(req);
}

static void ggp_edisc_xfer_recv_ticket_update_got(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer user_data)
{
	PurpleConnection *gc = purple_http_conn_get_purple_connection(hc);
	PurpleXfer *xfer;
	ggp_edisc_xfer *edisc_xfer;
	JsonParser *parser;
	JsonObject *result;
	int status = -1;
	ggp_edisc_session_data *sdata;

	const gchar *ticket_id, *file_name, *send_mode_str;
	uin_t sender, recipient;
	int file_size;

	if (!purple_http_response_is_successful(response)) {
		purple_debug_error("gg",
			"ggp_edisc_xfer_recv_ticket_update_got: "
			"cannot fetch update for ticket (code=%d)\n",
			purple_http_response_get_code(response));
		return;
	}

	sdata = ggp_edisc_get_sdata(gc);
	g_return_if_fail(sdata != NULL);

	parser = ggp_json_parse(purple_http_response_get_data(response, NULL));
	result = json_node_get_object(json_parser_get_root(parser));
	result = json_object_get_object_member(result, "result");
	if (json_object_has_member(result, "status"))
		status = json_object_get_int_member(result, "status");
	result = json_object_get_object_member(result, "send_ticket");

	if (status != 0) {
		purple_debug_warning("gg",
			"ggp_edisc_xfer_recv_ticket_update_got: failed to get "
			"update (status=%d)\n", status);
		g_object_unref(parser);
		return;
	}

	ticket_id = json_object_get_string_member(result, "id");
	sender = ggp_str_to_uin(json_object_get_string_member(result,
		"sender"));
	recipient = ggp_str_to_uin(json_object_get_string_member(result,
		"recipient"));
	file_size = g_ascii_strtoll(json_object_get_string_member(result,
		"file_size"), NULL, 10);
	file_name = json_object_get_string_member(result, "file_name");

	/* GG11: normal
	 * AQQ 2.4.2.10: direct_inbox
	 */
	send_mode_str = json_object_get_string_member(result, "send_mode");

	/* more fields:
	 * send_progress (float), ack_status, send_status
	 */

	if (purple_debug_is_verbose() && purple_debug_is_unsafe())
		purple_debug_info("gg", "Got ticket update: id=%s, sender=%u, "
			"recipient=%u, file name=\"%s\", file size=%d, "
			"send mode=%s)\n",
			ticket_id,
			sender, recipient,
			file_name, file_size,
			send_mode_str);

	xfer = g_hash_table_lookup(sdata->xfers_initialized, ticket_id);
	if (xfer != NULL) {
		purple_debug_misc("gg", "ggp_edisc_xfer_recv_ticket_update_got:"
			" ticket %s already updated\n",
			purple_debug_is_unsafe() ? ticket_id : "");
		g_object_unref(parser);
		return;
	}

	if (recipient != ggp_get_my_uin(gc)) {
		purple_debug_misc("gg", "ggp_edisc_xfer_recv_ticket_update_got:"
			" ticket %s is not for incoming transfer "
			"(its from %u to %u)\n",
			purple_debug_is_unsafe() ? ticket_id : "",
			sender, recipient);
		g_object_unref(parser);
		return;
	}

	xfer = ggp_edisc_xfer_recv_new(gc, ggp_uin_to_str(sender));
	purple_xfer_set_filename(xfer, file_name);
	purple_xfer_set_size(xfer, file_size);
	purple_xfer_request(xfer);
	edisc_xfer = purple_xfer_get_protocol_data(xfer);
	edisc_xfer->ticket_id = g_strdup(ticket_id);
	g_hash_table_insert(sdata->xfers_initialized,
		edisc_xfer->ticket_id, xfer);
	g_hash_table_insert(sdata->xfers_history,
		g_strdup(ticket_id), GINT_TO_POINTER(1));

	g_object_unref(parser);
}

static PurpleXfer * ggp_edisc_xfer_recv_new(PurpleConnection *gc,
	const char *who)
{
	PurpleXfer *xfer;
	ggp_edisc_xfer *edisc_xfer;

	g_return_val_if_fail(gc != NULL, NULL);
	g_return_val_if_fail(who != NULL, NULL);

	xfer = purple_xfer_new(purple_connection_get_account(gc),
		PURPLE_XFER_TYPE_RECEIVE, who);
	edisc_xfer = g_new0(ggp_edisc_xfer, 1);
	purple_xfer_set_protocol_data(xfer, edisc_xfer);

	edisc_xfer->gc = gc;

	purple_xfer_set_init_fnc(xfer, ggp_edisc_xfer_recv_accept);
	purple_xfer_set_request_denied_fnc(xfer, ggp_edisc_xfer_recv_reject);
	purple_xfer_set_start_fnc(xfer, ggp_edisc_xfer_recv_start);
	purple_xfer_set_cancel_recv_fnc(xfer, ggp_edisc_xfer_cancel);

	return xfer;
}

static void ggp_edisc_xfer_recv_reject(PurpleXfer *xfer)
{
	ggp_edisc_xfer_recv_ack(xfer, FALSE);
}

static void ggp_edisc_xfer_recv_accept(PurpleXfer *xfer)
{
	ggp_edisc_xfer_recv_ack(xfer, TRUE);
}

static void ggp_edisc_xfer_recv_ack(PurpleXfer *xfer, gboolean accept)
{
	ggp_edisc_xfer *edisc_xfer = purple_xfer_get_protocol_data(xfer);
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(edisc_xfer->gc);
	PurpleHttpRequest *req;

	g_return_if_fail(sdata != NULL);

	edisc_xfer->allowed = accept;

	req = purple_http_request_new(ggp_edisc_xfer_ticket_url(
		edisc_xfer->ticket_id));
	purple_http_request_set_method(req, "PUT");

	ggp_edisc_set_defaults(req);
	purple_http_request_set_cookie_jar(req, sdata->cookies);
	purple_http_request_header_set(req, "X-gged-security-token",
		sdata->security_token);

	purple_http_request_header_set(req, "X-gged-ack-status",
		accept ? "allow" : "reject");

	edisc_xfer->hc = purple_http_request(edisc_xfer->gc, req,
		accept ? ggp_edisc_xfer_recv_ack_done : NULL, xfer);
	purple_http_request_unref(req);

	if (!accept) {
		edisc_xfer->hc = NULL;
		ggp_edisc_xfer_free(xfer);
	}
}

static void ggp_edisc_xfer_recv_ack_done(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer _xfer)
{
	PurpleXfer *xfer = _xfer;
	ggp_edisc_xfer *edisc_xfer;

	if (purple_xfer_is_cancelled(xfer))
		g_return_if_reached();

	edisc_xfer = purple_xfer_get_protocol_data(xfer);
	edisc_xfer->hc = NULL;

	if (!purple_http_response_is_successful(response)) {
		ggp_edisc_xfer_error(xfer, _("Cannot confirm file transfer."));
		return;
	}

	purple_debug_info("gg", "ggp_edisc_xfer_recv_ack_done: [%s]\n",
		purple_http_response_get_data(response, NULL));
}

static void ggp_edisc_xfer_recv_ticket_completed(PurpleXfer *xfer)
{
	ggp_edisc_xfer *edisc_xfer = purple_xfer_get_protocol_data(xfer);

	if (edisc_xfer->ready)
		return;
	edisc_xfer->ready = TRUE;

	purple_xfer_start(xfer, -1, NULL, 0);
}

static void ggp_edisc_xfer_recv_start(PurpleXfer *xfer)
{
	ggp_edisc_session_data *sdata;
	ggp_edisc_xfer *edisc_xfer;
	gchar *upload_url;
	PurpleHttpRequest *req;

	g_return_if_fail(xfer != NULL);
	edisc_xfer = purple_xfer_get_protocol_data(xfer);
	g_return_if_fail(edisc_xfer != NULL);
	sdata = ggp_edisc_get_sdata(edisc_xfer->gc);
	g_return_if_fail(sdata != NULL);

	upload_url = g_strdup_printf("https://drive.mpa.gg.pl/me/file/inbox/"
		"%s,%s?api_version=%s&security_token=%s",
		edisc_xfer->ticket_id, purple_url_encode(purple_xfer_get_filename(xfer)),
		GGP_EDISC_API, sdata->security_token);
	req = purple_http_request_new(upload_url);
	g_free(upload_url);

	purple_http_request_set_timeout(req, -1);

	ggp_edisc_set_defaults(req);
	purple_http_request_set_max_len(req, purple_xfer_get_size(xfer) + 1);
	purple_http_request_set_cookie_jar(req, sdata->cookies);

	purple_http_request_set_response_writer(req, ggp_edisc_xfer_recv_writer,
		xfer);

	edisc_xfer->hc = purple_http_request(edisc_xfer->gc, req,
		ggp_edisc_xfer_recv_done, xfer);
	purple_http_request_unref(req);

	purple_http_conn_set_progress_watcher(edisc_xfer->hc,
		ggp_edisc_xfer_progress_watcher, xfer, 250000);
}

static gboolean ggp_edisc_xfer_recv_writer(PurpleHttpConnection *http_conn,
	PurpleHttpResponse *response, const gchar *buffer, size_t offset,
	size_t length, gpointer _xfer)
{
	PurpleXfer *xfer = _xfer;
	ggp_edisc_xfer *edisc_xfer;
	gssize stored;

	g_return_val_if_fail(xfer != NULL, FALSE);
	edisc_xfer = purple_xfer_get_protocol_data(xfer);
	g_return_val_if_fail(edisc_xfer != NULL, FALSE);

	stored = purple_xfer_write_file(xfer, (guchar *)buffer, length) ?
			(gssize)length : -1;

	if (stored < 0 || (gsize)stored != length) {
		purple_debug_error("gg", "ggp_edisc_xfer_recv_writer: "
			"saved too less\n");
		return FALSE;
	}

	if (stored > purple_xfer_get_bytes_remaining(xfer)) {
		purple_debug_error("gg", "ggp_edisc_xfer_recv_writer: "
			"saved too much (%" G_GSSIZE_FORMAT " > %" G_GOFFSET_FORMAT ")\n",
			stored, purple_xfer_get_bytes_remaining(xfer));
		return FALSE;
	}

	/* May look redundant with ggp_edisc_xfer_progress_watcher,
	 * but it isn't!
	 */
	purple_xfer_set_bytes_sent(xfer,
		purple_xfer_get_bytes_sent(xfer) + stored);

	return TRUE;
}

static void ggp_edisc_xfer_recv_done(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer _xfer)
{
	PurpleXfer *xfer = _xfer;
	ggp_edisc_xfer *edisc_xfer = purple_xfer_get_protocol_data(xfer);

	if (purple_xfer_is_cancelled(xfer))
		return;

	g_return_if_fail(edisc_xfer != NULL);

	edisc_xfer->hc = NULL;

	if (!purple_http_response_is_successful(response)) {
		ggp_edisc_xfer_error(xfer, _("Error while receiving a file"));
		return;
	}

	if (purple_xfer_get_bytes_remaining(xfer) == 0) {
		purple_xfer_set_completed(xfer, TRUE);
		purple_xfer_end(xfer);
		ggp_edisc_xfer_free(xfer);
	} else {
		purple_debug_warning("gg", "ggp_edisc_xfer_recv_done: didn't "
			"received everything\n");
		ggp_edisc_xfer_error(xfer, _("Error while receiving a file"));
	}
}

/*******************************************************************************
 * Authentication.
 ******************************************************************************/

struct _ggp_edisc_auth_data
{
	ggp_ggdrive_auth_cb cb;
	gpointer user_data;
};

static void ggp_ggdrive_auth_done(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer user_data);

static void ggp_ggdrive_auth(PurpleConnection *gc, ggp_ggdrive_auth_cb cb,
	gpointer user_data)
{
	GGPInfo *accdata = purple_connection_get_protocol_data(gc);
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(gc);
	ggp_edisc_auth_data *auth;
	const gchar *imtoken;
	gchar *metadata;
	PurpleHttpRequest *req;

	g_return_if_fail(sdata != NULL);

	imtoken = ggp_get_imtoken(gc);
	if (!imtoken) {
		cb(gc, FALSE, user_data);
		return;
	}

	if (sdata->auth_done) {
		cb(gc, sdata->security_token != NULL, user_data);
		return;
	}

	auth = g_new0(ggp_edisc_auth_data, 1);
	auth->cb = cb;
	auth->user_data = user_data;
	sdata->auth_pending = g_list_prepend(sdata->auth_pending, auth);

	if (sdata->auth_request)
		return;

	purple_debug_info("gg", "ggp_ggdrive_auth(gc=%p)\n", gc);

	req = purple_http_request_new("https://drive.mpa.gg.pl/signin");
	purple_http_request_set_method(req, "PUT");

	ggp_edisc_set_defaults(req);
	purple_http_request_set_cookie_jar(req, sdata->cookies);

	metadata = g_strdup_printf("{"
		"\"id\": \"%032x\", "
		"\"name\": \"%s\", "
		"\"os_version\": \"" GGP_EDISC_OS "\", "
		"\"client_version\": \"%s\", "
		"\"type\": \"" GGP_EDISC_TYPE "\"}",
		g_random_int_range(1, 1 << 16),
		purple_get_host_name(),
		ggp_libgaduw_version(gc));

	purple_http_request_header_set_printf(req, "Authorization",
		"IMToken %s", imtoken);
	purple_http_request_header_set_printf(req, "X-gged-user",
		"gg/pl:%u", accdata->session->uin);
	purple_http_request_header_set(req, "X-gged-client-metadata", metadata);
	g_free(metadata);

	sdata->auth_request = purple_http_request(gc, req,
		ggp_ggdrive_auth_done, NULL);
	purple_http_request_unref(req);
}

static void ggp_ggdrive_auth_results(PurpleConnection *gc, gboolean success)
{
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(gc);
	GList *it;

	purple_debug_info("gg", "ggp_ggdrive_auth_results(gc=%p): %d\n",
		gc, success);

	g_return_if_fail(sdata != NULL);

	it = g_list_first(sdata->auth_pending);
	while (it) {
		ggp_edisc_auth_data *auth = it->data;
		it = g_list_next(it);

		auth->cb(gc, success, auth->user_data);
		g_free(auth);
	}
	g_list_free(sdata->auth_pending);
	sdata->auth_pending = NULL;
	sdata->auth_done = TRUE;
}

static void ggp_ggdrive_auth_done(PurpleHttpConnection *hc,
	PurpleHttpResponse *response, gpointer user_data)
{
	PurpleConnection *gc = purple_http_conn_get_purple_connection(hc);
	ggp_edisc_session_data *sdata = ggp_edisc_get_sdata(gc);
	JsonParser *parser;
	JsonObject *result;
	int status = -1;

	g_return_if_fail(sdata != NULL);

	sdata->auth_request = NULL;

	if (!purple_http_response_is_successful(response)) {
		purple_debug_misc("gg", "ggp_ggdrive_auth_done: authentication "
			"failed due to unsuccessful request (code = %d)\n",
			purple_http_response_get_code(response));
		ggp_ggdrive_auth_results(gc, FALSE);
		return;
	}

	parser = ggp_json_parse(purple_http_response_get_data(response, NULL));
	result = json_node_get_object(json_parser_get_root(parser));
	result = json_object_get_object_member(result, "result");
	if (json_object_has_member(result, "status"))
		status = json_object_get_int_member(result, "status");
	g_object_unref(parser);

	if (status != 0 ) {
		purple_debug_misc("gg", "ggp_ggdrive_auth_done: authentication "
			"failed due to bad result (status=%d)\n", status);
		if (purple_debug_is_verbose())
			purple_debug_misc("gg", "ggp_ggdrive_auth_done: "
				"result = %s\n",
				purple_http_response_get_data(response, NULL));
		ggp_ggdrive_auth_results(gc, FALSE);
		return;
	}

	sdata->security_token = g_strdup(purple_http_response_get_header(
		response, "X-gged-security-token"));
	if (!sdata->security_token) {
		purple_debug_misc("gg", "ggp_ggdrive_auth_done: authentication "
			"failed due to missing security token header\n");
		ggp_ggdrive_auth_results(gc, FALSE);
		return;
	}

	if (purple_debug_is_unsafe())
		purple_debug_misc("gg", "ggp_ggdrive_auth_done: "
			"security_token=%s\n", sdata->security_token);
	ggp_ggdrive_auth_results(gc, TRUE);
}

mercurial