/*
 * $Id: pproxy.c,v 1.7 2004/10/24 01:11:12 cbiere Exp $
 *
 * Copyright (c) 2003, Raphael Manfredi
 *
 * Push proxy HTTP management.
 *
 *----------------------------------------------------------------------
 * This file is part of gtk-gnutella.
 *
 *  gtk-gnutella 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.
 *
 *  gtk-gnutella 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 gtk-gnutella; if not, write to the Free Software
 *  Foundation, Inc.:
 *      59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *----------------------------------------------------------------------
 */

#include "common.h"

RCSID("$Id: pproxy.c,v 1.7 2004/10/24 01:11:12 cbiere Exp $");

#include "pproxy.h"
#include "http.h"
#include "hosts.h"
#include "version.h"

/* Following extra needed for the server-side */

#include "sockets.h"
#include "ioheader.h"
#include "bsched.h"
#include "routing.h"
#include "gmsg.h"
#include "uploads.h"
#include "gnet_stats.h"
#include "lib/url.h"

/* Following extra needed for the client-side */

#include "settings.h"		/* For listen_ip() */
#include "token.h"
#include "downloads.h"

#include "lib/misc.h"
#include "lib/atoms.h"
#include "lib/getline.h"
#include "lib/header.h"
#include "lib/glib-missing.h"
#include "lib/endian.h"
#include "lib/walloc.h"

#include "if/gnet_property_priv.h"

#include "lib/override.h"		/* Must be the last header included */

/***
 *** Server-side of push-proxy
 ***/

static GSList *pproxies = NULL;	/* Currently active push-proxy requests */

static void send_pproxy_error(struct pproxy *pp, int code,
	const gchar *msg, ...) G_GNUC_PRINTF(3, 4);
static void pproxy_error_remove(struct pproxy *pp, int code,
	const gchar *msg, ...) G_GNUC_PRINTF(3, 4);

/**
 * Get rid of all the resources attached to the push-proxy struct.
 * But not the structure itself.
 */
static void
pproxy_free_resources(struct pproxy *pp)
{
	if (pp->guid != NULL) {
		atom_guid_free(pp->guid);
		pp->guid = NULL;
	}
	if (pp->io_opaque != NULL) {
		io_free(pp->io_opaque);
		g_assert(pp->io_opaque == NULL);
	}
	if (pp->user_agent != NULL) {
		atom_str_free(pp->user_agent);
		pp->user_agent = NULL;
	}
	if (pp->socket != NULL) {
		g_assert(pp->socket->resource.pproxy == pp);
		socket_free(pp->socket);
		pp->socket = NULL;
	}
}

/**
 * The vectorized (message-wise) version of send_pproxy_error().
 */
static void
send_pproxy_error_v(
	struct pproxy *pp,
	const gchar *ext,
	int code,
	const gchar *msg, va_list ap)
{
	gchar reason[1024];
	gchar extra[1024];
	http_extra_desc_t hev[1];
	gint hevcnt = 0;

	if (msg) {
		gm_vsnprintf(reason, sizeof(reason), msg, ap);
	} else
		reason[0] = '\0';

	if (pp->error_sent) {
		g_warning("push-proxy: already sent an error %d to %s, "
			"not sending %d (%s)",
			pp->error_sent, ip_to_gchar(pp->socket->ip), code, reason);
		return;
	}

	extra[0] = '\0';

	/*
	 * If `ext' is not null, we have extra header information to propagate.
	 */

	if (ext) {
		size_t slen = g_strlcpy(extra, ext, sizeof(extra));
		
		if (slen < sizeof(extra)) {
			hev[hevcnt].he_type = HTTP_EXTRA_LINE;
			hev[hevcnt++].he_msg = extra;
		} else
			g_warning("send_pproxy_error_v: "
				"ignoring too large extra header (%d bytes)", (int) slen);
	}

	http_send_status(pp->socket, code, FALSE,
			hevcnt ? hev : NULL, hevcnt, "%s", reason);

	pp->error_sent = code;
}

/**
 * Send error message to requestor.
 * This can only be done once per connection.
 */
static void
send_pproxy_error(struct pproxy *pp, int code, const gchar *msg, ...)
{
	va_list args;

	va_start(args, msg);
	send_pproxy_error_v(pp, NULL, code, msg, args);
	va_end(args);
}

/**
 * The vectorized (message-wise) version of pproxy_remove().
 */
static void
pproxy_remove_v(struct pproxy *pp, const gchar *reason, va_list ap)
{
	const gchar *logreason;
	gchar errbuf[1024];

	g_assert(pp != NULL);

	if (reason) {
		gm_vsnprintf(errbuf, sizeof(errbuf), reason, ap);
		logreason = errbuf;
	} else {
		if (pp->error_sent) {
			gm_snprintf(errbuf, sizeof(errbuf), "HTTP %d", pp->error_sent);
			logreason = errbuf;
		} else {
			errbuf[0] = '\0';
			logreason = "No reason given";
		}
	}

	if (dbg > 1) {
		printf("push-proxy: ending request from %s (%s): %s\n",
			pp->socket ? ip_to_gchar(pp->socket->ip) : "<no socket>",
			pproxy_vendor_str(pp),
			logreason);
	}

	if (!pp->error_sent) {
		if (reason == NULL)
			logreason = "Bad Request";
		send_pproxy_error(pp, 400, "%s", logreason);
	}

	pproxy_free_resources(pp);
	wfree(pp, sizeof(*pp));

	pproxies = g_slist_remove(pproxies, (gpointer) pp);
}

/**
 * Remove push proxy entry, log reason.
 *
 * If no status has been sent back on the HTTP stream yet, give
 * them a 400 error with the reason.
 */
void
pproxy_remove(struct pproxy *pp, const gchar *reason, ...)
{
	va_list args;
	
	g_assert(pp != NULL);
	
	va_start(args, reason);
	pproxy_remove_v(pp, reason, args);
	va_end(args);
}

/**
 * Utility routine.  Cancel the request, sending back the HTTP error message.
 */
static void
pproxy_error_remove(struct pproxy *pp, int code, const gchar *msg, ...)
{
	va_list args, errargs;

	g_assert(pp != NULL);

	va_start(args, msg);

	VA_COPY(errargs, args);
	send_pproxy_error_v(pp, NULL, code, msg, errargs);
	va_end(errargs);

	pproxy_remove_v(pp, msg, args);
	va_end(args);
}

/**
 * Push proxy timer.
 */
void
pproxy_timer(time_t now)
{
	GSList *sl;
	GSList *to_remove = NULL;

	for (sl = pproxies; sl; sl = g_slist_next(sl)) {
		struct pproxy *pp = (struct pproxy *) sl->data;
		
		/*
		 * We can't call pproxy_remove() since it will remove the structure
		 * from the list we are traversing.
		 */

		if (delta_time(now, pp->last_update) > upload_connecting_timeout) {
			to_remove = g_slist_prepend(to_remove, pp);
		}
	}

	for (sl = to_remove; sl; sl = g_slist_next(sl)) {
		struct pproxy *pp = (struct pproxy *) sl->data;
		pproxy_error_remove(pp, 408, "Request timeout");
	}

	g_slist_free(to_remove);
}

/**
 * pproxy_create
 */
static struct pproxy *
pproxy_create(struct gnutella_socket *s)
{
	struct pproxy *pp;

	pp = walloc0(sizeof(*pp));

	pp->socket = s;
	pp->last_update = time(NULL);
	s->resource.pproxy = pp;

	return pp;
}

/**
 * Extract GUID for push-proxyfication from the HTTP request line.
 * Extract file index if present (otherwise 0 will be used).
 *
 * Fills the GUID atom into `guid_atom' and the file index into `file_idx'.
 *
 * Returns TRUE if OK, FALSE if we could not figure it out, in which case
 * we also return an error to the calling party.
 */
static gboolean
get_params(struct pproxy *pp, gchar *request,
	gchar **guid_atom, guint32 *file_idx)
{
	gchar *uri;
	gchar *attr;
	gchar *p;
	gchar *value;
	gint datalen;
	url_params_t *up;

	/*
	 * Move to the start of the requested path.  Note that sizeof("GET")
	 * accounts for the trailing NUL in the string.
	 */

	uri = request + ((request[0] == 'G') ? sizeof("GET") : sizeof("HEAD"));
	while (is_ascii_blank(*uri))
		uri++;

	/*
	 * Go patch the first space we encounter before HTTP to be a NUL.
	 * Indeed, the requesst shoud be "GET /get/12/foo.txt HTTP/1.0".
	 *
	 * Note that if we don't find HTTP/ after the space, it's not an
	 * error: they're just sending an HTTP/0.9 request, which is awkward
	 * but we accept it.
	 */

	p = strrchr(uri, ' ');
	if (p && p[1]=='H' && p[2]=='T' && p[3]=='T' && p[4]=='P' && p[5]=='/')
		*p = '\0';

	/*
	 * Determine proper parameter to extract based on the URL.
	 *
	 * The first two are legacy only, and the third one is the new form.
	 * However, we need some transition period before emitting the new form...
	 *		--RAM, 18/07/2003
	 */

	if (0 == strncmp(uri, "/gnutella/pushproxy?", 20)) {
		uri += 20;
		attr = "ServerId";
	} else if (0 == strncmp(uri, "/gnutella/push-proxy?", 21)) {
		uri += 21;
		attr = "ServerId";
	} else if (0 == strncmp(uri, "/gnet/push-proxy?", 17)) {
		uri += 17;
		attr = "guid";
	} else {
		pproxy_error_remove(pp, 400, "Request not understood");
		return FALSE;
	}

	/*
	 * Look for the proper "ServerId=" or "guid=" parameter.
	 */

	up = url_params_parse(uri);
	if (!up) {
		pproxy_error_remove(pp, 400,
			"Malformed push-proxy request: Bad URL encoding");
		goto error;
	}
	value = url_params_get(up, attr);

	if (value == NULL) {
		pproxy_error_remove(pp, 400,
			"Malformed push-proxy request: no %s found", attr);
		goto error;
	}

	/*
	 * Determine how much data we have for the parameter.
	 */

	datalen = strlen(value);

	if (0 == strcmp(attr, "ServerId")) {
		gchar *guid;

		/*
		 * GUID is base32-encoded: valid lengths are 26 (no padding) or 32.
		 */

		if (datalen != 26 && datalen != 32) {
			pproxy_error_remove(pp, 400, "Malformed push-proxy request: "
				"wrong length for parameter \"%s\": %d byte%s", attr, datalen,
				datalen == 1 ? "" : "s");
			goto error;
		}

		if (dbg > 4)
			printf("PUSH-PROXY: decoding %s=%s as base32\n", attr, value);
		
		guid = base32_to_guid(value);
		if (guid == NULL) {
			pproxy_error_remove(pp, 400, "Malformed push-proxy request: "
				"parameter \"%s\" is not valid base32", attr);
			goto error;
		}

		*guid_atom = atom_guid_get(guid);
	} else if (0 == strcmp(attr, "guid")) {
		gchar guid[16];

		/*
		 * GUID in hexadecimal: valid length is 32.
		 */

		if (datalen != 32) {
			pproxy_error_remove(pp, 400, "Malformed push-proxy request: "
				"wrong length for parameter \"%s\": %d byte%s", attr, datalen,
				datalen == 1 ? "" : "s");
			goto error;
		}

		if (dbg > 4)
			printf("PUSH-PROXY: decoding %s=%s as hexadecimal\n", attr, value);

		if (!hex_to_guid(value, guid)) {
			pproxy_error_remove(pp, 400, "Malformed push-proxy request: "
				"parameter \"%s\" is not valid hexadecimal", attr);
			goto error;
		}

		*guid_atom = atom_guid_get(guid);
	} else
		g_error("unhandled parameter \"%s\"", attr);

	/*
	 * Extract the optional "file=" parameter.
	 */

	value = url_params_get(up, "file");

	if (value == NULL)
		*file_idx = 0;
	else
		*file_idx = (guint32) atoi(value);		/* Silent about errors */

	url_params_free(up);
	return TRUE;

error:
	url_params_free(up);
	return FALSE;
}

/**
 * Build a push request to send.  We set TTL=max_ttl-1 and hops=1 since
 * it does not come from our node really.  The file ID may be set to 0, but
 * it should be ignored when the GIV is received anyway.
 */
static void
build_push(struct gnutella_msg_push_request *m,
	gchar *guid, guint32 ip, guint16 port, guint32 file_idx)
{
	message_set_muid(&m->header, GTA_MSG_PUSH_REQUEST);

	m->header.function = GTA_MSG_PUSH_REQUEST;
	m->header.ttl = max_ttl - 1;
	m->header.hops = 1;

	WRITE_GUINT32_LE(sizeof(struct gnutella_push_request), m->header.size);

	memcpy(m->request.guid, guid, 16);

	WRITE_GUINT32_LE(file_idx, m->request.file_id);
	WRITE_GUINT32_BE(ip, m->request.host_ip);
	WRITE_GUINT16_LE(port, m->request.host_port);
}

/**
 * Validate vendor.
 *
 * @return atom, or NULL.
 */
static gchar *
validate_vendor(gchar *vendor, gchar *token, guint32 ip)
{
	gchar *result = NULL;

	if (vendor) {
		gboolean faked = !version_check(vendor, token, ip);

		if (faked) {
			gchar name[1024];

			name[0] = '!';
			g_strlcpy(&name[1], vendor, sizeof name - 1);
			result = atom_str_get(name);
		} else
			result = atom_str_get(vendor);
	}

	return result;
}

/**
 * Called once all the HTTP headers have been read to proceed with
 * the push proxyfication.
 */
static void
pproxy_request(struct pproxy *pp, header_t *header) 
{
	struct gnutella_socket *s = pp->socket;
	gchar *request = getline_str(s->getline);
	struct gnutella_node *n;
	gchar *buf;
	gchar *token;
	gchar *user_agent;
	struct gnutella_msg_push_request m;
	GSList *nodes;

	if (dbg > 2) {
		printf("----Push-proxy request from %s:\n", ip_to_gchar(s->ip));
		printf("%s\n", request);
		header_dump(header, stdout);
		printf("----\n");
		fflush(stdout);
	}

	/*
	 * Extract User-Agent and X-Token if needed.
	 */

	token = header_get(header, "X-Token");
	user_agent = header_get(header, "User-Agent");

	pp->user_agent = validate_vendor(user_agent, token, s->ip);

	/*
	 * Determine the servent ID.
	 */

	if (!get_params(pp, request, &pp->guid, &pp->file_idx))
		return;				/* Already reported the error in get_params() */

	if (dbg > 2)
		printf("PUSH-PROXY: %s requesting a push to %s for file #%d\n",
			ip_to_gchar(s->ip), guid_hex_str(pp->guid), pp->file_idx);

	/*
	 * Make sure they provide an X-Node header so we know whom to set up
	 * as the originator of the push.  Then validate the address.
	 */

	buf = header_get(header, "X-Node");

	if (buf == NULL) {
		pproxy_error_remove(pp, 400,
			"Malformed push-proxy request: missing X-Node header");
		return;
	}

	if (!gchar_to_ip_port(buf, &pp->ip, &pp->port)) {
		pproxy_error_remove(pp, 400,
			"Malformed push-proxy request: cannot parse X-Node");
		return;
	}

	if (!host_is_valid(pp->ip, pp->port)) {
		pproxy_error_remove(pp, 400,
			"Malformed push-proxy request: supplied address %s unreachable",
			ip_port_to_gchar(pp->ip, pp->port));
		return;
	}

	if (pp->ip != s->ip)
		g_warning("push-proxy request from %s (%s) said node was at %s",
			ip_to_gchar(s->ip), pproxy_vendor_str(pp),
			ip_port_to_gchar(pp->ip, pp->port));

	/*
	 * Locate a route to that servent.
	 */

	n = route_proxy_find(pp->guid);

	if (n != NULL) {
		build_push(&m, pp->guid, pp->ip, pp->port, pp->file_idx);
		message_add(m.header.muid, GTA_MSG_PUSH_REQUEST, NULL);

		gmsg_sendto_one(n, (gchar *) &m, sizeof(m));
		gnet_stats_count_general(GNR_PUSH_PROXY_RELAYED, 1);

		http_send_status(pp->socket, 202, FALSE, NULL, 0,
			"Push-proxy: message sent to node");

		pp->error_sent = 202;
		pproxy_remove(pp, "Push sent directly to node GUID %s",
			guid_hex_str(pp->guid));

		return;
	}

	/*
	 * Bad luck, no direct connection.  Look for a Gnutella route.
	 */

	nodes = route_towards_guid(pp->guid);

	if (nodes != NULL) {
		gint cnt;

		build_push(&m, pp->guid, pp->ip, pp->port, pp->file_idx);
		message_add(m.header.muid, GTA_MSG_PUSH_REQUEST, NULL);

		gmsg_sendto_all(nodes, (gchar *) &m, sizeof(m));
		gnet_stats_count_general(GNR_PUSH_PROXY_BROADCASTED, 1);

		cnt = g_slist_length(nodes);
		g_slist_free(nodes);

		http_send_status(pp->socket, 203, FALSE, NULL, 0,
			"Push-proxy: message sent through Gnutella (via %d node%s)",
			cnt, cnt == 1 ? "" : "s");

		pp->error_sent = 203;
		pproxy_remove(pp, "Push sent via Gnutella (%d node%s) for GUID %s",
			cnt, cnt == 1 ? "" : "s", guid_hex_str(pp->guid));

		return;
	}

	/*
	 * If by extraordinary the GUID is ours, honour it immediately by
	 * sending a GIV back.
	 */

	if (guid_eq(pp->guid, guid)) {
		upload_send_giv(pp->ip, pp->port, 0, 1, 0, "<from push-proxy>", FALSE);

		http_send_status(pp->socket, 202, FALSE, NULL, 0,
			"Push-proxy: you found the target GUID %s",
			guid_hex_str(pp->guid));

		pp->error_sent = 202;
		pproxy_remove(pp, "Push was for our GUID %s", guid_hex_str(pp->guid));

		return;
	}

	/*
	 * Sorry.
	 */

	gnet_stats_count_general(GNR_PUSH_PROXY_FAILED, 1);

	pproxy_error_remove(pp, 410, "Push proxy: no route to servent GUID %s",
		guid_hex_str(pp->guid));
}

/***
 *** I/O header parsing callbacks.
 ***/

#define PPROXY(x)	((struct pproxy *) (x))

static void
err_line_too_long(gpointer obj)
{
	pproxy_error_remove(PPROXY(obj), 413, "Header too large");
}

static void
err_header_error_tell(gpointer obj, gint error)
{
	send_pproxy_error(PPROXY(obj), 413, "%s", header_strerror(error));
}

static void
err_header_error(gpointer obj, gint error)
{
	pproxy_remove(PPROXY(obj), "Failed (%s)", header_strerror(error));
}

static void
err_input_exception(gpointer obj)
{
	pproxy_remove(PPROXY(obj), "Failed (Input Exception)");
}

static void
err_input_buffer_full(gpointer obj)
{
	pproxy_error_remove(PPROXY(obj), 500, "Input buffer full");
}

static void
err_header_read_error(gpointer obj, gint error)
{
	pproxy_remove(PPROXY(obj), "Failed (Input error: %s)", g_strerror(error));
}

static void
err_header_read_eof(gpointer obj)
{
	pproxy_remove(PPROXY(obj), "Failed (EOF)");
}

static void
err_header_extra_data(gpointer obj)
{
	pproxy_error_remove(PPROXY(obj), 400, "Extra data after HTTP header");
}

static const struct io_error pproxy_io_error = {
	err_line_too_long,
	err_header_error_tell,
	err_header_error,
	err_input_exception,
	err_input_buffer_full,
	err_header_read_error,
	err_header_read_eof,
	err_header_extra_data,
};

static void
call_pproxy_request(gpointer obj, header_t *header)
{
	pproxy_request(PPROXY(obj), header);
}

#undef PPROXY

/**
 * Create new push-proxy request and begin reading HTTP headers.
 */
void
pproxy_add(struct gnutella_socket *s)
{
	struct pproxy *pp;

	s->type = SOCK_TYPE_PPROXY;
	socket_tos_default(s);

	pp = pproxy_create(s);

	/*
	 * Read HTTP headers, then call pproxy_request() when done.
	 */

	io_get_header(pp, &pp->io_opaque, bws.in, s, IO_HEAD_ONLY,
		call_pproxy_request, NULL, &pproxy_io_error);
}

/**
 * Called a shutdown time.
 */
void
pproxy_close(void)
{
	GSList *l;

	for (l = pproxies; l; l = g_slist_next(l)) {
		struct pproxy *pp = (struct pproxy *) l->data;

		pproxy_free_resources(pp);
		wfree(pp, sizeof(*pp));
	}

	g_slist_free(pproxies);
}

/***
 *** Client-side of push-proxy
 ***/

/* XXX needed only because of the temporary hack below -- RAM, 05/08/2003 */
static gboolean cproxy_http_header_ind(
	gpointer handle, header_t *header, gint code, const gchar *message);
static size_t cproxy_build_request(gpointer handle, gchar *buf, size_t len,
	const gchar *verb, const gchar *path, const gchar *host, guint16 port);
static void cproxy_http_newstate(gpointer handle, http_state_t newstate);

#define CPROXY_MAGIC	0xc8301U

/*
 * Operating flags.
 */

#define CP_F_SWAPPED_IP	0x00000001		/* Tried to swap IP address */

/**
 * Free the structure and all its dependencies.
 */
void
cproxy_free(struct cproxy *cp)
{
	g_assert(cp->magic == CPROXY_MAGIC);

	if (cp->guid != NULL) {
		atom_guid_free(cp->guid);
		cp->guid = NULL;
	}
	if (cp->http_handle != NULL) {
		http_async_cancel(cp->http_handle);
		cp->http_handle = NULL;
	}
	if (cp->server != NULL) {
		atom_str_free(cp->server);
		cp->server = NULL;
	}

	cp->magic = 0;
	wfree(cp, sizeof(*cp));
}

/**
 * HTTP async callback for error notifications.
 */
static void
cproxy_http_error_ind(gpointer handle, http_errtype_t type, gpointer v)
{
	struct cproxy *cp = (struct cproxy *) http_async_get_opaque(handle);
	guint32 new_ip;

	g_assert(cp != NULL);
	g_assert(cp->magic == CPROXY_MAGIC);

	http_async_log_error(handle, type, v);

	/*
	 * Temporary hack.
	 *
	 * Old servents used little-endian for encoding IP addresses in PUSH.
	 * If we were unable to connect, and we have not yet swapped the
	 * IP address, try doing so now.
	 *
	 *	--RAM, 04/08/2003
	 */

	new_ip = swap_guint32(cp->ip);

	if (
		!(cp->flags & CP_F_SWAPPED_IP) &&
		type == HTTP_ASYNC_ERROR &&
		GPOINTER_TO_INT(v) == HTTP_ASYNC_CONN_TIMEOUT &&
		host_is_valid(new_ip, cp->port)
	) {
		static gchar path[128];

		(void) gm_snprintf(path, sizeof(path),
			"/gnutella/push-proxy?ServerId=%s", guid_base32_str(cp->guid));

		cp->flags |= CP_F_SWAPPED_IP;
		cp->http_handle = http_async_get_ip(path, new_ip, cp->port,
			cproxy_http_header_ind, NULL, cproxy_http_error_ind);

		if (cp->http_handle != NULL) {
			g_warning("retrying with swapped push-proxy address %s (was %s)",
				ip_to_gchar(new_ip), ip_port_to_gchar(cp->ip, cp->port));
			cp->ip = new_ip;

			/*
			 * Customize async HTTP layer.
			 */

			http_async_set_opaque(cp->http_handle, cp, NULL);
			http_async_set_op_request(cp->http_handle, cproxy_build_request);
			http_async_on_state_change(cp->http_handle, cproxy_http_newstate);

			return;
		}

		cp->flags &= ~CP_F_SWAPPED_IP;

		/* FALL THROUGH */
	}

	if (cp->flags & CP_F_SWAPPED_IP)
		cp->ip = swap_guint32(cp->ip);		/* Restore original IP */

	cp->http_handle = NULL;
	cp->done = TRUE;

	if (
		type == HTTP_ASYNC_ERROR &&
		GPOINTER_TO_INT(v) == HTTP_ASYNC_CANCELLED
	)
		return;		/* Was an explicit cancel */

	download_proxy_failed(cp->d);
}

/**
 * HTTP async callback for header reception notification.
 * Returns whether processing can continue.
 */
static gboolean
cproxy_http_header_ind(gpointer handle, header_t *header,
	gint code, const gchar *message)
{
	struct cproxy *cp = (struct cproxy *) http_async_get_opaque(handle);
	gchar *token;
	gchar *server;

	g_assert(cp != NULL);
	g_assert(cp->d != NULL);
	g_assert(cp->magic == CPROXY_MAGIC);

	/*
	 * Extract vendor information.
	 */

	token = header_get(header, "X-Token");
	server = header_get(header, "Server");
	if (server == NULL)
		server = header_get(header, "User-Agent");

	cp->server = validate_vendor(server, token, cp->ip);

	/*
	 * Don't continue past headers, we don't expect data, and besides the
	 * error codes are non-standard.
	 */

	g_assert(handle == cp->http_handle);

	http_async_cancel(cp->http_handle);
	cp->http_handle = NULL;

	g_assert(cp->done);		/* Set by the error_ind callback during cancel */

	/*
	 * Analyze status.
	 */

	switch (code) {
	case 202:
		download_proxy_sent(cp->d);
		cp->sent = TRUE;
		cp->directly = TRUE;
		break;
	case 203:
		download_proxy_sent(cp->d);
		cp->sent = TRUE;
		cp->directly = FALSE;
		break;
	case 400:
		g_warning("push-proxy at %s (%s) for %s file #%u reported HTTP %d: %s",
			ip_port_to_gchar(cp->ip, cp->port), cproxy_vendor_str(cp),
			guid_hex_str(cp->guid), cp->file_idx, code, message);
		/* FALL THROUGH */
	case 410:
		download_proxy_failed(cp->d);
		break;
	default:
		g_warning("push-proxy at %s (%s) for %s file #%u "
			"sent unexpected HTTP %d: %s",
			ip_port_to_gchar(cp->ip, cp->port), cproxy_vendor_str(cp),
			guid_hex_str(cp->guid), cp->file_idx, code, message);
		download_proxy_failed(cp->d);
		break;
	}

	if (dbg > 2 && cp->sent)
		printf("PUSH-PROXY at %s (%s) sent PUSH for %s file #%u %s\n",
			ip_port_to_gchar(cp->ip, cp->port), cproxy_vendor_str(cp),
			guid_hex_str(cp->guid), cp->file_idx,
			cp->directly ? "directly" : "via Gnet");

	return FALSE;		/* Don't continue -- handle invalid now anyway */
}

/**
 * Redefines the HTTP request building.
 *
 * See http_async_build_request() for the model and details about
 * the various parameters.
 *
 * @return length of generated request.
 */
static size_t
cproxy_build_request(gpointer unused_handle, gchar *buf, size_t len,
	const gchar *verb, const gchar *path, const gchar *unused_host,
	guint16 unused_port)
{
	/*
	 * Note that we send an HTTP/1.0 request here, hence we need neither
	 * the Host: nor the Connection: header.
	 */

	(void) unused_handle;
	(void) unused_host;
	(void) unused_port;
	g_assert(len <= INT_MAX);

	return gm_snprintf(buf, len,
		"%s %s HTTP/1.0\r\n"
		"User-Agent: %s\r\n"
		"X-Token: %s\r\n"
		"X-Node: %s\r\n"
		"\r\n",
		verb, path, version_string,
		tok_version(),
		ip_port_to_gchar(listen_ip(), listen_port));
}

/**
 * Invoked when the state of the HTTP async request changes.
 */
static void
cproxy_http_newstate(gpointer handle, http_state_t newstate)
{
	struct cproxy *cp = (struct cproxy *) http_async_get_opaque(handle);

	g_assert(cp != NULL);
	g_assert(cp->d != NULL);
	g_assert(cp->magic == CPROXY_MAGIC);

	cp->state = newstate;
	download_proxy_newstate(cp->d);
}

/**
 * Create client proxy.
 *
 * @returns NULL if problem during connection.
 */
struct cproxy *
cproxy_create(struct download *d, guint32 ip, guint16 port,
	gchar *guid, guint32 file_idx)
{
	struct cproxy *cp;
	gpointer handle;
	static gchar path[128];
	gboolean swapped = FALSE;

	(void) gm_snprintf(path, sizeof(path),
		"/gnutella/push-proxy?ServerId=%s", guid_base32_str(guid));

	/*
	 * Try to connect immediately: if we can't connect, no need to continue.
	 */

	handle = http_async_get_ip(path, ip, port,
		cproxy_http_header_ind, NULL, cproxy_http_error_ind);

	/*
	 * Temporary hack.
	 *
	 * Old servents used little-endian for encoding IP addresses in PUSH.
	 * If we were unable to connect, try swapping the IP address.
	 *
	 *	--RAM, 04/08/2003
	 */

	if (handle == NULL) {
		guint32 new_ip;

		new_ip = swap_guint32(ip);
		if (host_is_valid(new_ip, port)) {
			g_warning("can't connect to push-proxy %s for GUID %s: %s "
				"-- swapping to %s",
				ip_port_to_gchar(ip, port), guid_hex_str(guid),
				http_async_strerror(http_async_errno),
				ip_to_gchar(new_ip));

			ip = new_ip;
			swapped = TRUE;

			handle = http_async_get_ip(path, ip, port,
				cproxy_http_header_ind, NULL, cproxy_http_error_ind);
		}
	}

	if (handle == NULL) {
		g_warning("can't connect to push-proxy %s for GUID %s: %s",
			ip_port_to_gchar(ip, port), guid_hex_str(guid),
			http_async_strerror(http_async_errno));
		return NULL;
	}

	cp = walloc0(sizeof(*cp));

	cp->magic = CPROXY_MAGIC;
	cp->d = d;
	cp->ip = ip;
	cp->port = port;
	cp->guid = atom_guid_get(guid);
	cp->file_idx = file_idx;
	cp->http_handle = handle;
	cp->flags = swapped ? CP_F_SWAPPED_IP : 0;

	/*
	 * Customize async HTTP layer.
	 */

	http_async_set_opaque(handle, cp, NULL);
	http_async_set_op_request(handle, cproxy_build_request);
	http_async_on_state_change(handle, cproxy_http_newstate);

	return cp;
}

/**
 * Updates the proxy structures to point to the right download when a download
 * was cloned.
 */
void
cproxy_reparent(struct download *d, struct download *cd)
{
	g_assert(d != cd);
	g_assert(d->cproxy != NULL);
	g_assert(cd->cproxy != NULL);
	g_assert(d->cproxy == cd->cproxy);
	g_assert(d->cproxy->magic == CPROXY_MAGIC);
	
	cd->cproxy->d = cd;
	d->cproxy = NULL;
	
	g_assert(d->cproxy == NULL);
	g_assert(cd->cproxy != NULL);
	g_assert(cd == cd->cproxy->d);
}

/* vi: set ts=4 sw=4 cindent: */
