/* Connections management */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdlib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <sys/types.h>
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h> /* OS/2 needs this after sys/types.h */
#endif

#include "elinks.h"

#include "cache/cache.h"
#include "config/options.h"
#include "document/document.h"
#include "encoding/encoding.h"
#include "intl/libintl.h"
#include "main/main.h"
#include "main/object.h"
#include "main/select.h"
#include "main/timer.h"
#include "network/connection.h"
#include "network/dns.h"
#include "network/progress.h"
#include "network/socket.h"
#include "network/ssl/ssl.h"
#include "protocol/http/http.h"
#include "protocol/protocol.h"
#include "protocol/proxy.h"
#include "protocol/uri.h"
#include "session/session.h"
#include "util/error.h"
#include "util/memory.h"
#include "util/string.h"
#include "util/time.h"


struct keepalive_connection {
	LIST_HEAD_EL(struct keepalive_connection);

	/* XXX: This is just the URI of the connection that registered the
	 * keepalive connection so only rely on the protocol, user, password,
	 * host and port part. */
	struct uri *uri;

	/* Function called when the keepalive has timed out or is deleted */
	void (*done)(struct connection *);

	timeval_T timeout;
	timeval_T creation_time;

	unsigned int protocol_family:1; /* see network/socket.h, EL_PF_INET, EL_PF_INET6 */
	int socket;
};


static unsigned int connection_id = 0;
static int active_connections = 0;
static timer_id_T keepalive_timeout = TIMER_ID_UNDEF;

static INIT_LIST_OF(struct connection, connection_queue);
static INIT_LIST_OF(struct host_connection, host_connections);
static INIT_LIST_OF(struct keepalive_connection, keepalive_connections);

/* Prototypes */
static void check_keepalive_connections(void);
static void notify_connection_callbacks(struct connection *conn);

static /* inline */ connection_priority_T
get_priority(struct connection *conn)
{
	ELOG
	int priority;

	for (priority = 0; priority < PRIORITIES; priority++)
		if (conn->pri[priority])
			break;

	assertm(priority != PRIORITIES, "Connection has no owner");
	/* Recovery path ;-). (XXX?) */

	return priority;
}

int
get_connections_count(void)
{
	ELOG
	return list_size(&connection_queue);
}

int
get_keepalive_connections_count(void)
{
	ELOG
	return list_size(&keepalive_connections);
}

int
get_connections_connecting_count(void)
{
	ELOG
	struct connection *conn;
	int i = 0;

	foreach (conn, connection_queue)
		i += is_in_connecting_state(conn->state);

	return i;
}

int
get_connections_transfering_count(void)
{
	ELOG
	struct connection *conn;
	int i = 0;

	foreach (conn, connection_queue)
		i += is_in_transfering_state(conn->state);

	return i;
}

/** Check whether the pointer @a conn still points to a connection
 * with the given @a id.  If the struct connection has already been
 * freed, this returns 0.  By comparing connection.id, this function
 * can usually detect even the case where a different connection has
 * been created at the same address.  For that to work, the caller
 * must save the connection.id before the connection can be deleted.  */
static inline int
connection_disappeared(struct connection *conn, unsigned int id)
{
	ELOG
	struct connection *c;

	foreach (c, connection_queue)
		if (conn == c && id == c->id)
			return 0;

	return 1;
}

/* Host connection management: */
/* Used to keep track on the number of connections to any given host. When
 * trying to setup a new connection the list is searched to see if the maximum
 * number of connection has been reached. If that is the case we try to suspend
 * an already established connection. */
/* Some connections (like file://) that do not involve hosts are not maintained
 * in the list. */

struct host_connection {
	OBJECT_HEAD(struct host_connection);

	/* XXX: This is just the URI of the connection that registered the
	 * host connection so only rely on the host part. */
	struct uri *uri;
};

static struct host_connection *
get_host_connection(struct connection *conn)
{
	ELOG
	struct host_connection *host_conn;

	if (!conn->uri->host) return NULL;

	foreach (host_conn, host_connections)
		if (compare_uri(host_conn->uri, conn->uri, URI_HOST))
			return host_conn;

	return NULL;
}

/* Returns if the connection was successfully added. */
/* Don't add hostnameless host connections but they're valid. */
static int
add_host_connection(struct connection *conn)
{
	ELOG
	struct host_connection *host_conn = get_host_connection(conn);

	if (!host_conn && conn->uri->host) {
		host_conn = (struct host_connection *)mem_calloc(1, sizeof(*host_conn));
		if (!host_conn) return 0;

		host_conn->uri = get_uri_reference(conn->uri);
		object_nolock(host_conn, "host_connection");
		add_to_list(host_connections, host_conn);
	}
	if (host_conn) object_lock(host_conn);

	return 1;
}

/* Decrements and free()s the host connection if it is the last 'refcount'. */
static void
done_host_connection(struct connection *conn)
{
	ELOG
	struct host_connection *host_conn = get_host_connection(conn);

	if (!host_conn) return;

	object_unlock(host_conn);
	if (is_object_used(host_conn)) return;

	del_from_list(host_conn);
	done_uri(host_conn->uri);
	mem_free(host_conn);
}


static void sort_queue(void);

#ifdef CONFIG_DEBUG
static void
check_queue_bugs(void)
{
	ELOG
	struct connection *conn;
	connection_priority_T prev_priority = 0;
	int cc = 0;

	foreach (conn, connection_queue) {
		connection_priority_T priority = get_priority(conn);

		cc += conn->running;

		assertm(priority >= prev_priority, "queue is not sorted");
		assertm(is_in_progress_state(conn->state),
			"interrupted connection on queue (conn %s, state %d)",
			struri(conn->uri), conn->state);
		prev_priority = priority;
	}

	assertm(cc == active_connections,
		"bad number of active connections (counted %d, stored %d)",
		cc, active_connections);
}
#else
#define check_queue_bugs()
#endif

static void
set_connection_socket_state(struct socket *socket, struct connection_state state)
{
	ELOG
	assert(socket);
	set_connection_state((struct connection *)socket->conn, state);
}

static void
set_connection_socket_timeout(struct socket *socket, struct connection_state state)
{
	ELOG
	assert(socket);
	set_connection_timeout((struct connection *)socket->conn);
}

static void
retry_connection_socket(struct socket *socket, struct connection_state state)
{
	ELOG
	assert(socket);
	retry_connection((struct connection *)socket->conn, state);
}

static void
done_connection_socket(struct socket *socket, struct connection_state state)
{
	ELOG
	assert(socket);
	abort_connection((struct connection *)socket->conn, state);
}

static struct connection *
init_connection(struct uri *uri, struct uri *proxied_uri, struct uri *referrer,
		off_t start, cache_mode_T cache_mode,
		connection_priority_T priority)
{
	ELOG
	static struct socket_operations connection_socket_operations = {
		set_connection_socket_state,
		set_connection_socket_timeout,
		retry_connection_socket,
		done_connection_socket,
	};
	struct connection *conn = (struct connection *)mem_calloc(1, sizeof(*conn));

	if (!conn) return NULL;

	assert(proxied_uri->protocol != PROTOCOL_PROXY);

	conn->socket = init_socket(conn, &connection_socket_operations);
	if (!conn->socket) {
		mem_free(conn);
		return NULL;
	}

	conn->data_socket = init_socket(conn, &connection_socket_operations);
	if (!conn->data_socket) {
		mem_free(conn->socket);
		mem_free(conn);
		return NULL;
	}

	conn->progress = init_progress(start);
	if (!conn->progress) {
		mem_free(conn->data_socket);
		mem_free(conn->socket);
		mem_free(conn);
		return NULL;
	}

	/* load_uri() gets the URI from get_proxy() which grabs a reference for
	 * us. */
	conn->uri = uri;
	conn->proxied_uri = proxied_uri;
	conn->id = connection_id++;
	conn->pri[priority] = 1;
	conn->cache_mode = cache_mode;

	conn->content_encoding = ENCODING_NONE;
	init_list(conn->downloads);
	conn->est_length = -1;
	conn->timer = TIMER_ID_UNDEF;

	if (referrer) {
		/* Don't set referrer when it is the file protocol and the URI
		 * being loaded is not. This means CGI scripts will have it
		 * available while preventing information about the local
		 * system from being leaked to external servers. */
		if (referrer->protocol != PROTOCOL_FILE
		    || (uri->protocol == PROTOCOL_FILE
			|| uri->protocol == PROTOCOL_MAILCAP
			|| uri->protocol == PROTOCOL_MAILCAP_HTML)) {
				conn->referrer = get_uri_reference(referrer);
			}
	}

	return conn;
}

static void
update_connection_progress(struct connection *conn)
{
	ELOG
	update_progress(conn->progress, conn->received, conn->est_length, conn->from);
}

/** Progress timer callback for @a conn->progress.  */
static void
stat_timer(struct connection *conn)
{
	ELOG
	update_connection_progress(conn);
	notify_connection_callbacks(conn);
}

/** Progress timer callback for @a conn->upload_progress.  */
static void
upload_stat_timer(struct connection *conn)
{
	ELOG
	struct http_connection_info *http = (struct http_connection_info *)conn->info;

	assert(conn->http_upload_progress);
	assert(http);
	if_assert_failed return;

	update_progress(conn->http_upload_progress, http->post.uploaded,
		http->post.total_upload_length, http->post.uploaded);
	notify_connection_callbacks(conn);
}

void
set_connection_state(struct connection *conn, struct connection_state state)
{
	ELOG
	struct download *download;
	struct progress *progress = conn->progress;
	struct progress *upload_progress = conn->http_upload_progress;

	if (is_in_result_state(conn->state) && is_in_progress_state(state))
		conn->prev_error = conn->state;

	conn->state = state;
	if (is_in_state(conn->state, S_TRANS)) {
		const unsigned int id = conn->id;

		if (upload_progress && upload_progress->timer == TIMER_ID_UNDEF) {
			start_update_progress(upload_progress,
				(void (*)(void *)) upload_stat_timer, conn);
			upload_stat_timer(conn);
			if (connection_disappeared(conn, id))
				return;
		}
		if (progress->timer == TIMER_ID_UNDEF) {
			start_update_progress(progress, (void (*)(void *)) stat_timer, conn);
			update_connection_progress(conn);
			if (connection_disappeared(conn, id))
				return;
		}

	} else {
		kill_timer(&progress->timer);
		if (upload_progress) kill_timer(&upload_progress->timer);
	}

	foreach (download, conn->downloads) {
		download->state = state;
		download->prev_error = conn->prev_error;
	}

	if (is_in_progress_state(state)) notify_connection_callbacks(conn);
}

void
shutdown_connection_stream(struct connection *conn)
{
	ELOG
	if (conn->stream) {
		close_encoded(conn->stream);
		conn->stream = NULL;
	}
}

static void
free_connection_data(struct connection *conn)
{
	ELOG
	assertm(conn->running, "connection already suspended");
	/* XXX: Recovery path? Originally, there was none. I think we'll get
	 * at least active_connections underflows along the way. --pasky */
	conn->running = 0;

	active_connections--;
	assertm(active_connections >= 0, "active connections underflow");
	if_assert_failed active_connections = 0;

#ifdef CONFIG_SSL
	if (conn->socket->ssl && conn->cached)
		mem_free_set(&conn->cached->ssl_info, get_ssl_connection_cipher(conn->socket));
#endif

	if (conn->done)
		conn->done(conn);

	done_socket(conn->socket);
	done_socket(conn->data_socket);

	shutdown_connection_stream(conn);

	mem_free_set(&conn->info, NULL);
	/* If conn->done is not NULL, it probably points to a function
	 * that expects conn->info to be a specific kind of structure.
	 * Such a function should not be called if a different pointer
	 * is later assigned to conn->info.  Actually though, each
	 * free_connection_data() call seems to be soon followed by
	 * done_connection() so that conn->done would not be called
	 * again in any case.  However, this assignment costs little
	 * and may make things a bit safer.  */
	conn->done = NULL;

	kill_timer(&conn->timer);

	if (!is_in_state(conn->state, S_WAIT))
		done_host_connection(conn);
}

static void
notify_connection_callbacks(struct connection *conn)
{
	ELOG
	struct connection_state state = conn->state;
	unsigned int id = conn->id;
	struct download *download, *next;

	foreachsafe (download, next, conn->downloads) {
		download->cached = conn->cached;
		if (download->callback)
			download->callback(download, download->data);
		if (is_in_progress_state(state)
		    && connection_disappeared(conn, id))
			return;
	}
}

static void
done_connection(struct connection *conn)
{
	ELOG
	/* When removing the connection callbacks should always be aware of it
	 * so they can unregister themselves. We do this by enforcing that the
	 * connection is in a result state. If it is not already it is an
	 * internal bug. This should never happen but it does. ;) --jonas */
	if (!is_in_result_state(conn->state))
		set_connection_state(conn, connection_state(S_INTERNAL));

	del_from_list(conn);
	notify_connection_callbacks(conn);
	if (conn->referrer) done_uri(conn->referrer);
	done_uri(conn->uri);
	done_uri(conn->proxied_uri);
	mem_free(conn->socket);
	mem_free(conn->data_socket);
	done_progress(conn->progress);
	if (conn->http_upload_progress)
		done_progress(conn->http_upload_progress);
	mem_free(conn);
	check_queue_bugs();
}

static inline void
add_to_queue(struct connection *conn)
{
	ELOG
	struct connection *c;
	connection_priority_T priority = get_priority(conn);

	foreach (c, connection_queue)
		if (get_priority(c) > priority)
			break;

	add_at_pos(c->prev, conn);
}


/* Returns zero if no callback was done and the keepalive connection should be
 * deleted or non-zero if the keepalive connection should not be deleted. */
static int
do_keepalive_connection_callback(struct keepalive_connection *keep_conn)
{
	ELOG
	struct uri *proxied_uri = get_proxied_uri(keep_conn->uri);
	struct uri *proxy_uri   = get_proxy_uri(keep_conn->uri, NULL);

	if (proxied_uri && proxy_uri) {
		struct connection *conn;

		conn = init_connection(proxy_uri, proxied_uri, NULL, 0,
				       CACHE_MODE_NEVER, PRI_CANCEL);

		if (conn) {
			void (*done)(struct connection *) = keep_conn->done;

			add_to_queue(conn);

			/* Get the keepalive info and let it clean up */
			if (!has_keepalive_connection(conn)
			    || !add_host_connection(conn)) {
				free_connection_data(conn);
				done_connection(conn);
				return 0;
			}

			active_connections++;
			conn->running = 1;
			done(conn);
			return 1;
		}
	}

	if (proxied_uri) done_uri(proxied_uri);
	if (proxy_uri) done_uri(proxy_uri);

	return 0;
}

static inline void
done_keepalive_connection(struct keepalive_connection *keep_conn)
{
	ELOG
	if (keep_conn->done && do_keepalive_connection_callback(keep_conn))
		return;

	del_from_list(keep_conn);
	if (keep_conn->socket != -1) close(keep_conn->socket);
	done_uri(keep_conn->uri);
	mem_free(keep_conn);
}

static struct keepalive_connection *
init_keepalive_connection(struct connection *conn, long timeout_in_seconds,
			  void (*done)(struct connection *))
{
	ELOG
	struct keepalive_connection *keep_conn;
	struct uri *uri = conn->uri;

	assert(uri->host);
	if_assert_failed return NULL;

	keep_conn = (struct keepalive_connection *)mem_calloc(1, sizeof(*keep_conn));
	if (!keep_conn) return NULL;

	keep_conn->uri = get_uri_reference(uri);
	keep_conn->done = done;
	keep_conn->protocol_family = conn->socket->protocol_family;
	keep_conn->socket = conn->socket->fd;
	timeval_from_seconds(&keep_conn->timeout, timeout_in_seconds);
	timeval_now(&keep_conn->creation_time);

	return keep_conn;
}

static struct keepalive_connection *
get_keepalive_connection(struct connection *conn)
{
	ELOG
	struct keepalive_connection *keep_conn;

	if (!conn->uri->host) return NULL;

	foreach (keep_conn, keepalive_connections)
		if (compare_uri(keep_conn->uri, conn->uri, URI_KEEPALIVE))
			return keep_conn;

	return NULL;
}

int
has_keepalive_connection(struct connection *conn)
{
	ELOG
	struct keepalive_connection *keep_conn = get_keepalive_connection(conn);

	if (!keep_conn) return 0;

	conn->socket->fd = keep_conn->socket;
	conn->socket->protocol_family = keep_conn->protocol_family;

	/* Mark that the socket should not be closed and the callback should be
	 * ignored. */
	keep_conn->socket = -1;
	keep_conn->done = NULL;
	done_keepalive_connection(keep_conn);

	return 1;
}

void
add_keepalive_connection(struct connection *conn, long timeout_in_seconds,
			 void (*done)(struct connection *))
{
	ELOG
	struct keepalive_connection *keep_conn;

	assertm(conn->socket->fd != -1, "keepalive connection not connected");
	if_assert_failed goto done;

	keep_conn = init_keepalive_connection(conn, timeout_in_seconds, done);
	if (keep_conn) {
		/* Make sure that the socket descriptor will not periodically be
		 * checked or closed by free_connection_data(). */
		clear_handlers(conn->socket->fd);
		conn->socket->fd = -1;
		add_to_list(keepalive_connections, keep_conn);

	} else if (done) {
		/* It will take just a little more time */
		done(conn);
		return;
	}

done:
	free_connection_data(conn);
	done_connection(conn);
	register_check_queue();
}

/* Timer callback for @keepalive_timeout.  As explained in @install_timer,
 * this function must erase the expired timer ID from all variables.  */
static void
keepalive_timer(void *x)
{
	ELOG
	keepalive_timeout = TIMER_ID_UNDEF;
	/* The expired timer ID has now been erased.  */
	check_keepalive_connections();
}

void
check_keepalive_connections(void)
{
	ELOG
	struct keepalive_connection *keep_conn, *next;
	timeval_T now;
	int p = 0;

	timeval_now(&now);

	kill_timer(&keepalive_timeout);

	foreachsafe (keep_conn, next, keepalive_connections) {
		timeval_T age;

		if (can_read(keep_conn->socket)) {
			done_keepalive_connection(keep_conn);
			continue;
		}

		timeval_sub(&age, &keep_conn->creation_time, &now);
		if (timeval_cmp(&age, &keep_conn->timeout) > 0) {
			done_keepalive_connection(keep_conn);
			continue;
		}

		p++;
	}

	for (; p > MAX_KEEPALIVE_CONNECTIONS; p--) {
		assertm(!list_empty(keepalive_connections), "keepalive list empty");
		if_assert_failed return;
		done_keepalive_connection((struct keepalive_connection *)keepalive_connections.prev);
	}

	if (!list_empty(keepalive_connections))
		install_timer(&keepalive_timeout, KEEPALIVE_CHECK_TIME,
			      keepalive_timer, NULL);
}

static inline void
abort_all_keepalive_connections(void)
{
	ELOG
	while (!list_empty(keepalive_connections))
		done_keepalive_connection((struct keepalive_connection *)keepalive_connections.next);

	check_keepalive_connections();
}


static void
sort_queue(void)
{
	ELOG
	while (1) {
		struct connection *conn;
		int swp = 0;

		foreach (conn, connection_queue) {
			if (!list_has_next(connection_queue, conn)) break;

			if (get_priority(conn->next) < get_priority(conn)) {
				struct connection *c = conn->next;

				del_from_list(conn);
				add_at_pos(c, conn);
				swp = 1;
			}
		}

		if (!swp) break;
	};
}

static void
interrupt_connection(struct connection *conn)
{
	ELOG
	free_connection_data(conn);
}

static inline void
suspend_connection(struct connection *conn)
{
	ELOG
	interrupt_connection(conn);
	set_connection_state(conn, connection_state(S_WAIT));
}

static void
run_connection(struct connection *conn)
{
	ELOG
	protocol_handler_T *func = get_protocol_handler(conn->uri->protocol);

	assert(func);

	assertm(!conn->running, "connection already running");
	if_assert_failed return;

	if (!add_host_connection(conn)) {
		set_connection_state(conn, connection_state(S_OUT_OF_MEM));
		done_connection(conn);
		return;
	}

	active_connections++;
	conn->running = 1;

	func(conn);
}

/* Set certain state on a connection and then abort the connection. */
void
abort_connection(struct connection *conn, struct connection_state state)
{
	ELOG
	assertm(is_in_result_state(state),
		"connection didn't end in result state (%d)", state);

	if (conn->curl && is_in_state(state, S_INTERRUPTED) && !program.terminate) {
		conn->is_aborted = 1;
		return;
	}

	if (is_in_state(state, S_OK) && conn->cached)
		normalize_cache_entry(conn->cached, conn->from);

	set_connection_state(conn, state);

	if (conn->running) interrupt_connection(conn);
	done_connection(conn);
	register_check_queue();
}

/* Set certain state on a connection and then retry the connection. */
void
retry_connection(struct connection *conn, struct connection_state state)
{
	ELOG
	int max_tries = get_opt_int("connection.retries", NULL);

	assertm(is_in_result_state(state),
		"connection didn't end in result state (%d)", state);

	set_connection_state(conn, state);

	interrupt_connection(conn);
	if (conn->uri->post || (max_tries && ++conn->tries >= max_tries)) {
		done_connection(conn);
		register_check_queue();
	} else {
		conn->prev_error = conn->state;
		run_connection(conn);
	}
}

static int
try_to_suspend_connection(struct connection *conn, struct uri *uri)
{
	ELOG
	connection_priority_T priority = get_priority(conn);
	struct connection *c;

	foreachback (c, connection_queue) {
		if (get_priority(c) <= priority) return -1;
		if (is_in_state(c->state, S_WAIT)) continue;
		if (c->uri->post && get_priority(c) < PRI_CANCEL) continue;
		if (uri && !compare_uri(uri, c->uri, URI_HOST)) continue;
		suspend_connection(c);
		return 0;
	}

	return -1;
}

static inline int
try_connection(struct connection *conn, int max_conns_to_host, int max_conns)
{
	ELOG
	struct host_connection *host_conn = get_host_connection(conn);

	if (host_conn && get_object_refcount(host_conn) >= max_conns_to_host)
		return try_to_suspend_connection(conn, host_conn->uri) ? 0 : -1;

	if (active_connections >= max_conns)
		return try_to_suspend_connection(conn, NULL) ? 0 : -1;

	run_connection(conn);
	return 1;
}

static void
check_queue(void)
{
	ELOG
	struct connection *conn;
	int max_conns_to_host = get_opt_int("connection.max_connections_to_host", NULL);
	int max_conns = get_opt_int("connection.max_connections", NULL);

again:
	conn = (struct connection *)connection_queue.next;
	check_queue_bugs();
	check_keepalive_connections();

	while (conn != (struct connection *) &connection_queue) {
		struct connection *c;
		connection_priority_T pri = get_priority(conn);

		for (c = conn; c != (struct connection *) &connection_queue && get_priority(c) == pri;) {
			struct connection *cc = c;

			c = c->next;
			if (is_in_state(cc->state, S_WAIT)
			    && get_keepalive_connection(cc)
			    && try_connection(cc, max_conns_to_host, max_conns))
				goto again;
		}

		for (c = conn; c != (struct connection *) &connection_queue && get_priority(c) == pri;) {
			struct connection *cc = c;

			c = c->next;
			if (is_in_state(cc->state, S_WAIT)
			    && try_connection(cc, max_conns_to_host, max_conns))
				goto again;
		}
		conn = c;
	}

again2:
	foreachback (conn, connection_queue) {
		if (get_priority(conn) < PRI_CANCEL) break;
		if (is_in_state(conn->state, S_WAIT)) {
			set_connection_state(conn, connection_state(S_INTERRUPTED));
			done_connection(conn);
			goto again2;
		}
	}

	check_queue_bugs();
}

int
register_check_queue(void)
{
	ELOG
	return register_bottom_half(check_queue, NULL);
}

int
load_uri(struct uri *uri, struct uri *referrer, struct download *download,
	 connection_priority_T pri, cache_mode_T cache_mode, off_t start)
{
	ELOG
	struct cache_entry *cached;
	struct connection *conn;
	struct uri *proxy_uri, *proxied_uri;
	struct connection_state error_state = connection_state(S_OK);

	if (download) {
		download->conn = NULL;
		download->cached = NULL;
		download->pri = pri;
		download->state = connection_state(S_OUT_OF_MEM);
		download->prev_error = connection_state(0);
	}

#ifdef CONFIG_DEBUG
	foreach (conn, connection_queue) {
		struct download *assigned;

		foreach (assigned, conn->downloads) {
			assertm(assigned != download, "Download assigned to '%s'", struri(conn->uri));
			if_assert_failed {
				download->state = connection_state(S_INTERNAL);
				if (download->callback)
					download->callback(download, download->data);
				return 0;
			}
			/* No recovery path should be necessary. */
		}
	}
#endif

	cached = get_validated_cache_entry(uri, cache_mode);
	if (cached) {
		if (download) {
			download->cached = cached;
			download->state = connection_state(S_OK);
			/* XXX:
			 * This doesn't work since sometimes |download->progress|
			 * is undefined and contains random memory locations.
			 * It's not supposed to point on anything here since
			 * |download| has no connection attached.
			 * Downloads resuming will probably break in some
			 * cases without this, though.
			 * FIXME: Needs more investigation. --pasky */
			/* if (download->progress) download->progress->start = start; */
			if (download->callback)
				download->callback(download, download->data);
		}
		return 0;
	}

	proxied_uri = get_proxied_uri(uri);
	proxy_uri   = get_proxy_uri(uri, &error_state);

	if (!proxy_uri
	    || !proxied_uri
	    || (get_protocol_need_slash_after_host(proxy_uri->protocol)
		&& !proxy_uri->hostlen)) {

		if (download) {
			if (is_in_state(error_state, S_OK)) {
				error_state = proxy_uri && proxied_uri
					? connection_state(S_BAD_URL)
					: connection_state(S_OUT_OF_MEM);
			}

			download->state = error_state;
			download->callback(download, download->data);
		}
		if (proxy_uri) done_uri(proxy_uri);
		if (proxied_uri) done_uri(proxied_uri);
		return -1;
	}

	foreach (conn, connection_queue) {
		if (conn->detached
		    || !compare_uri(conn->uri, proxy_uri, 0))
			continue;

		done_uri(proxy_uri);
		done_uri(proxied_uri);

		if (get_priority(conn) > pri) {
			del_from_list(conn);
			conn->pri[pri]++;
			add_to_queue(conn);
			register_check_queue();
		} else {
			conn->pri[pri]++;
		}

		if (download) {
			download->progress = conn->progress;
			download->conn = conn;
			download->cached = conn->cached;
			add_to_list(conn->downloads, download);
			/* This is likely to call download->callback() now! */
			set_connection_state(conn, conn->state);
		}
		check_queue_bugs();
		return 0;
	}

	if (download && cache_mode == CACHE_MODE_ALWAYS) {
		cached = find_in_cache(uri);
		if (cached) {
			download->cached = cached;
			download->state = connection_state(S_OK);
			if (download->callback)
				download->callback(download, download->data);
			if (proxy_uri) done_uri(proxy_uri);
			if (proxied_uri) done_uri(proxied_uri);
			return 0;
		}
	}

	conn = init_connection(proxy_uri, proxied_uri, referrer, start, cache_mode, pri);
	if (!conn) {
		if (download) {
			download->state = connection_state(S_OUT_OF_MEM);
			download->callback(download, download->data);
		}
		if (proxy_uri) done_uri(proxy_uri);
		if (proxied_uri) done_uri(proxied_uri);
		return -1;
	}

	if (cache_mode < CACHE_MODE_FORCE_RELOAD && cached && !list_empty(cached->frag)
	    && !((struct fragment *) cached->frag.next)->offset)
		conn->from = ((struct fragment *) cached->frag.next)->length;

	if (download) {
		download->progress = conn->progress;
		download->conn = conn;
		download->cached = NULL;
		download->state = connection_state(S_OK);
		add_to_list(conn->downloads, download);
	}

	add_to_queue(conn);
	set_connection_state(conn, connection_state(S_WAIT));

	check_queue_bugs();

	register_check_queue();
	return 0;
}

void
try_to_load_image(struct uri *uri)
{
	struct connection_state error_state = connection_state(S_OK);
	struct uri *proxy_uri   = get_proxy_uri(uri, &error_state);
	struct uri *proxied_uri = get_proxied_uri(uri);

	if (!proxy_uri || !proxied_uri) {
		return;
	}
	struct connection *conn = init_connection(proxy_uri, proxied_uri, NULL, 0, CACHE_MODE_NORMAL, PRI_CSS);

	if (!conn) {
		return;
	}
	add_to_queue(conn);
	set_connection_state(conn, connection_state(S_WAIT));
	run_connection(conn);
}


/* FIXME: one object in more connections */
void
cancel_download(struct download *download, int interrupt)
{
	ELOG
	struct connection *conn;

	assert(download);
	if_assert_failed return;

	/* Did the connection already end? */
	if (is_in_result_state(download->state))
		return;

	assertm(download->conn != NULL, "last state is %d", download->state);

	check_queue_bugs();

	download->state = connection_state(S_INTERRUPTED);
	del_from_list(download);

	conn = download->conn;

	conn->pri[download->pri]--;
	assertm(conn->pri[download->pri] >= 0, "priority counter underflow");
	if_assert_failed conn->pri[download->pri] = 0;

	if (list_empty(conn->downloads)) {
		/* Necessary because of assertion in get_priority(). */
		conn->pri[PRI_CANCEL]++;

		if (conn->detached || interrupt)
			abort_connection(conn, connection_state(S_INTERRUPTED));
	}

	sort_queue();
	check_queue_bugs();

	register_check_queue();
}

void
move_download(struct download *old, struct download *new_,
	      connection_priority_T newpri)
{
	ELOG
	struct connection *conn;

	assert(old);

	/* The download doesn't necessarily have a connection attached, for
	 * example the file protocol loads it's object immediately. This is
	 * catched by the result state check below. */

	conn = old->conn;

	new_->conn	= conn;
	new_->cached	= old->cached;
	new_->prev_error	= old->prev_error;
	new_->progress	= old->progress;
	new_->state	= old->state;
	new_->pri	= newpri;

	if (is_in_result_state(old->state)) {
		/* Ensure that new_->conn is always "valid", that is NULL if the
		 * connection has been detached and non-NULL otherwise. */
		if (new_->callback) {
			new_->conn = NULL;
			new_->progress = NULL;
			new_->callback(new_, new_->data);
		}
		return;
	}

	assertm(old->conn != NULL, "last state is %d", old->state);

	conn->pri[new_->pri]++;
	add_to_list(conn->downloads, new_);
	/* In principle, we need to sort_queue() only if conn->pri[new_->pri]
	 * just changed from 0 to 1.  But the risk of bugs is smaller if we
	 * sort every time.  */
	sort_queue();

	cancel_download(old, 0);
}


/* This will remove 'pos' bytes from the start of the cache for the specified
 * connection, if the cached object is already too big. */
void
detach_connection(struct download *download, off_t pos)
{
	ELOG
	struct connection *conn = download->conn;

	if (is_in_result_state(download->state)) return;

	if (!conn->detached) {
		off_t total_len;
		off_t i, total_pri = 0;

		if (!conn->cached)
			return;

		total_len = (conn->est_length == -1) ? conn->from
						     : conn->est_length;

		if (total_len < (get_opt_long("document.cache.memory.size",
		                              NULL)
				 * MAX_CACHED_OBJECT_PERCENT / 100)) {
			/* This whole thing will fit to the memory anyway, so
			 * there's no problem in detaching the connection. */
			return;
		}

		for (i = 0; i < PRI_CANCEL; i++)
			total_pri += conn->pri[i];
		assertm(total_pri, "detaching free connection");
		/* No recovery path should be necessary...? */

		/* Pre-clean cache. */
		shrink_format_cache(0);

		if (total_pri != 1 || is_object_used(conn->cached)) {
			/* We're too important, or someone uses our cache
			 * entry. */
			return;
		}

		/* DBG("detached"); */

		/* We aren't valid cache entry anymore. */
		conn->cached->valid = 0;
		conn->detached = 1;
	}

	/* Strip the entry. */
	free_entry_to(conn->cached, pos);
}

/* Timer callback for @conn->timer.  As explained in @install_timer,
 * this function must erase the expired timer ID from all variables.  */
static void
connection_timeout(struct connection *conn)
{
	ELOG
	conn->timer = TIMER_ID_UNDEF;
	/* The expired timer ID has now been erased.  */
	timeout_socket(conn->socket);
}

/* Timer callback for @conn->timer.  As explained in @install_timer,
 * this function must erase the expired timer ID from all variables.
 *
 * Huh, using two timers? Is this to account for changes of c->unrestartable
 * or can it be reduced? --jonas */
static void
connection_timeout_1(struct connection *conn)
{
	ELOG
	install_timer(&conn->timer, (milliseconds_T)
			((conn->unrestartable
			 ? get_opt_int("connection.unrestartable_receive_timeout", NULL)
			 : get_opt_int("connection.receive_timeout", NULL))
			* 500), (void (*)(void *)) connection_timeout, conn);
	/* The expired timer ID has now been erased.  */
}

void
set_connection_timeout(struct connection *conn)
{
	ELOG
	if (conn->xhr_timeout) {
		return;
	}
	kill_timer(&conn->timer);

	install_timer(&conn->timer, (milliseconds_T)
			((conn->unrestartable
			 ? get_opt_int("connection.unrestartable_receive_timeout", NULL)
			 : get_opt_int("connection.receive_timeout", NULL))
			* 500), (void (*)(void *)) connection_timeout_1, conn);
}

static void
connection_timeout_xhr_1(struct connection *conn)
{
	ELOG
	install_timer(&conn->timer, conn->xhr_timeout / 2, (void (*)(void *)) connection_timeout, conn);
}

void
set_connection_timeout_xhr(struct connection *conn, milliseconds_T timeout)
{
	ELOG
	if (!conn) {
		return;
	}
	kill_timer(&conn->timer);
	conn->xhr_timeout = timeout;
	install_timer(&conn->timer, timeout / 2, (void (*)(void *)) connection_timeout_xhr_1, conn);
}

void
abort_all_connections(void)
{
	ELOG
	while (!list_empty(connection_queue)) {
		abort_connection((struct connection *)connection_queue.next,
				 connection_state(S_INTERRUPTED));
	}

	abort_all_keepalive_connections();
}

void
abort_background_connections(void)
{
	ELOG
	struct connection *conn, *next;

	foreachsafe (conn, next, connection_queue) {
		if (get_priority(conn) >= PRI_CANCEL)
			abort_connection(conn, connection_state(S_INTERRUPTED));
	}
}

int
is_entry_used(struct cache_entry *cached)
{
	ELOG
	struct connection *conn;

	foreach (conn, connection_queue)
		if (conn->cached == cached)
			return 1;

	return 0;
}
