/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * soup-connection.c: A single HTTP/HTTPS connection
 *
 * Copyright (C) 2000-2003, Ximian, Inc.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "soup-connection.h"
#include "soup.h"
#include "soup-message-queue.h"
#include "soup-socket-private.h"
#include "soup-misc-private.h"

typedef struct {
	SoupSocket  *socket;
	SoupSocketProperties *socket_props;

	SoupURI *remote_uri, *proxy_uri;
	gboolean ssl;

	SoupMessage *current_msg;
	SoupConnectionState state;
	time_t       unused_timeout;
	GSource     *idle_timeout_src;
	gboolean     reusable;
} SoupConnectionPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (SoupConnection, soup_connection, G_TYPE_OBJECT)

enum {
	EVENT,
	DISCONNECTED,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

enum {
	PROP_0,

	PROP_REMOTE_URI,
	PROP_SOCKET_PROPERTIES,
	PROP_STATE,
	PROP_SSL,

	LAST_PROP
};

static void stop_idle_timer (SoupConnectionPrivate *priv);

/* Number of seconds after which we close a connection that hasn't yet
 * been used.
 */
#define SOUP_CONNECTION_UNUSED_TIMEOUT 3

static void
soup_connection_init (SoupConnection *conn)
{
}

static void
soup_connection_finalize (GObject *object)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (SOUP_CONNECTION (object));

	g_clear_pointer (&priv->remote_uri, soup_uri_free);
	g_clear_pointer (&priv->proxy_uri, soup_uri_free);
	g_clear_pointer (&priv->socket_props, soup_socket_properties_unref);
	g_clear_object (&priv->current_msg);

	if (priv->socket) {
		g_signal_handlers_disconnect_by_data (priv->socket, object);
		g_object_unref (priv->socket);
	}

	G_OBJECT_CLASS (soup_connection_parent_class)->finalize (object);
}

static void
soup_connection_dispose (GObject *object)
{
	SoupConnection *conn = SOUP_CONNECTION (object);
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	stop_idle_timer (priv);

	G_OBJECT_CLASS (soup_connection_parent_class)->dispose (object);
}

static void
soup_connection_set_property (GObject *object, guint prop_id,
			      const GValue *value, GParamSpec *pspec)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (SOUP_CONNECTION (object));

	switch (prop_id) {
	case PROP_REMOTE_URI:
		priv->remote_uri = g_value_dup_boxed (value);
		break;
	case PROP_SOCKET_PROPERTIES:
		priv->socket_props = g_value_dup_boxed (value);
		break;
	case PROP_STATE:
		soup_connection_set_state (SOUP_CONNECTION (object), g_value_get_uint (value));
		break;
	case PROP_SSL:
		priv->ssl = g_value_get_boolean (value);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
soup_connection_get_property (GObject *object, guint prop_id,
			      GValue *value, GParamSpec *pspec)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (SOUP_CONNECTION (object));

	switch (prop_id) {
	case PROP_REMOTE_URI:
		g_value_set_boxed (value, priv->remote_uri);
		break;
	case PROP_SOCKET_PROPERTIES:
		g_value_set_boxed (value, priv->socket_props);
		break;
	case PROP_STATE:
		g_value_set_enum (value, priv->state);
		break;
	case PROP_SSL:
		g_value_set_boolean (value, priv->ssl);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
soup_connection_class_init (SoupConnectionClass *connection_class)
{
	GObjectClass *object_class = G_OBJECT_CLASS (connection_class);

	/* virtual method override */
	object_class->dispose = soup_connection_dispose;
	object_class->finalize = soup_connection_finalize;
	object_class->set_property = soup_connection_set_property;
	object_class->get_property = soup_connection_get_property;

	/* signals */
	signals[EVENT] =
		g_signal_new ("event",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_FIRST,
			      0,
			      NULL, NULL,
			      NULL,
			      G_TYPE_NONE, 2,
			      G_TYPE_SOCKET_CLIENT_EVENT,
			      G_TYPE_IO_STREAM);
	signals[DISCONNECTED] =
		g_signal_new ("disconnected",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_FIRST,
			      G_STRUCT_OFFSET (SoupConnectionClass, disconnected),
			      NULL, NULL,
			      NULL,
			      G_TYPE_NONE, 0);

	/* properties */
	g_object_class_install_property (
		object_class, PROP_REMOTE_URI,
		g_param_spec_boxed (SOUP_CONNECTION_REMOTE_URI,
				    "Remote URI",
				    "The URI of the HTTP server",
				    SOUP_TYPE_URI,
				    G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
	g_object_class_install_property (
		object_class, PROP_SOCKET_PROPERTIES,
		g_param_spec_boxed (SOUP_CONNECTION_SOCKET_PROPERTIES,
				    "Socket properties",
				    "Socket properties",
				    SOUP_TYPE_SOCKET_PROPERTIES,
				    G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
	g_object_class_install_property (
		object_class, PROP_STATE,
		g_param_spec_enum (SOUP_CONNECTION_STATE,
				   "Connection state",
				   "Current state of connection",
				   SOUP_TYPE_CONNECTION_STATE, SOUP_CONNECTION_NEW,
				   G_PARAM_READWRITE));
	g_object_class_install_property (
		object_class, PROP_SSL,
		g_param_spec_boolean (SOUP_CONNECTION_SSL,
				      "Connection uses TLS",
				      "Whether the connection should use TLS",
				      FALSE, G_PARAM_READWRITE));
}

static void
soup_connection_event (SoupConnection      *conn,
		       GSocketClientEvent   event,
		       GIOStream           *connection)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	if (!connection && priv->socket)
		connection = soup_socket_get_connection (priv->socket);

	g_signal_emit (conn, signals[EVENT], 0,
		       event, connection);
}

static gboolean
idle_timeout (gpointer conn)
{
	soup_connection_disconnect (conn);
	return FALSE;
}

static void
start_idle_timer (SoupConnection *conn)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	if (priv->socket_props->idle_timeout > 0 && !priv->idle_timeout_src) {
		priv->idle_timeout_src =
			soup_add_timeout (priv->socket_props->async_context,
					  priv->socket_props->idle_timeout * 1000,
					  idle_timeout, conn);
	}
}

static void
stop_idle_timer (SoupConnectionPrivate *priv)
{
	if (priv->idle_timeout_src) {
		g_source_destroy (priv->idle_timeout_src);
		priv->idle_timeout_src = NULL;
	}
}

static void
current_msg_got_body (SoupMessage *msg, gpointer user_data)
{
	SoupConnection *conn = user_data;
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	priv->unused_timeout = 0;

	if (priv->proxy_uri &&
	    msg->method == SOUP_METHOD_CONNECT &&
	    SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
		soup_connection_event (conn, G_SOCKET_CLIENT_PROXY_NEGOTIATED, NULL);

		/* We're now effectively no longer proxying */
		g_clear_pointer (&priv->proxy_uri, soup_uri_free);
	}

	priv->reusable = soup_message_is_keepalive (msg);
}

static void
clear_current_msg (SoupConnection *conn)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);
	SoupMessage *msg;

	msg = priv->current_msg;
	priv->current_msg = NULL;

	g_signal_handlers_disconnect_by_func (msg, G_CALLBACK (current_msg_got_body), conn);
	g_object_unref (msg);
}

static void
set_current_msg (SoupConnection *conn, SoupMessage *msg)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	g_return_if_fail (priv->state == SOUP_CONNECTION_IN_USE);

	g_object_freeze_notify (G_OBJECT (conn));

	if (priv->current_msg) {
		g_return_if_fail (priv->current_msg->method == SOUP_METHOD_CONNECT);
		clear_current_msg (conn);
	}

	stop_idle_timer (priv);

	priv->current_msg = g_object_ref (msg);
	priv->reusable = FALSE;

	g_signal_connect (msg, "got-body",
			  G_CALLBACK (current_msg_got_body), conn);

	if (priv->proxy_uri && msg->method == SOUP_METHOD_CONNECT)
		soup_connection_event (conn, G_SOCKET_CLIENT_PROXY_NEGOTIATING, NULL);

	g_object_thaw_notify (G_OBJECT (conn));
}

static void
re_emit_socket_event (SoupSocket          *socket,
		      GSocketClientEvent   event,
		      GIOStream           *connection,
		      gpointer             user_data)
{
	SoupConnection *conn = user_data;

	/* We handle COMPLETE ourselves */
	if (event != G_SOCKET_CLIENT_COMPLETE)
		soup_connection_event (conn, event, connection);
}

static void
socket_connect_finished (GTask *task, SoupSocket *sock, GError *error)
{
	SoupConnection *conn = g_task_get_source_object (task);
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	if (!error) {
		if (!priv->ssl || !priv->proxy_uri) {
			soup_connection_event (conn,
					       G_SOCKET_CLIENT_COMPLETE,
					       NULL);
		}

		soup_connection_set_state (conn, SOUP_CONNECTION_IN_USE);
		priv->unused_timeout = time (NULL) + SOUP_CONNECTION_UNUSED_TIMEOUT;
		start_idle_timer (conn);

		g_task_return_boolean (task, TRUE);
	} else
		g_task_return_error (task, error);
	g_object_unref (task);
}

static void
socket_handshake_complete (GObject *object, GAsyncResult *result, gpointer user_data)
{
	SoupSocket *sock = SOUP_SOCKET (object);
	GTask *task = user_data;
	GError *error = NULL;

	soup_socket_handshake_finish (sock, result, &error);
	socket_connect_finished (task, sock, error);
}

static void
socket_connect_complete (GObject *object, GAsyncResult *result, gpointer user_data)
{
	SoupSocket *sock = SOUP_SOCKET (object);
	GTask *task = user_data;
	SoupConnection *conn = g_task_get_source_object (task);
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);
	GError *error = NULL;

	if (!soup_socket_connect_finish_internal (sock, result, &error)) {
		socket_connect_finished (task, sock, error);
		return;
	}

	priv->proxy_uri = soup_socket_get_http_proxy_uri (sock);

	if (priv->ssl && !priv->proxy_uri) {
		soup_socket_handshake_async (sock, priv->remote_uri->host,
					     g_task_get_cancellable (task),
					     socket_handshake_complete, task);
		return;
	}

	socket_connect_finished (task, sock, NULL);
}

void
soup_connection_connect_async (SoupConnection      *conn,
			       GCancellable        *cancellable,
			       GAsyncReadyCallback  callback,
			       gpointer             user_data)
{
	SoupConnectionPrivate *priv;
	SoupAddress *remote_addr;
	GTask *task;

	g_return_if_fail (SOUP_IS_CONNECTION (conn));
	priv = soup_connection_get_instance_private (conn);
	g_return_if_fail (priv->socket == NULL);

	soup_connection_set_state (conn, SOUP_CONNECTION_CONNECTING);

	/* Set the protocol to ensure correct proxy resolution. */
	remote_addr =
		g_object_new (SOUP_TYPE_ADDRESS,
			      SOUP_ADDRESS_NAME, priv->remote_uri->host,
			      SOUP_ADDRESS_PORT, priv->remote_uri->port,
			      SOUP_ADDRESS_PROTOCOL, priv->remote_uri->scheme,
			      NULL);

	priv->socket =
		soup_socket_new (SOUP_SOCKET_REMOTE_ADDRESS, remote_addr,
				 SOUP_SOCKET_SOCKET_PROPERTIES, priv->socket_props,
				 NULL);
	g_object_unref (remote_addr);

	g_signal_connect (priv->socket, "event",
			  G_CALLBACK (re_emit_socket_event), conn);

	soup_socket_properties_push_async_context (priv->socket_props);
	task = g_task_new (conn, cancellable, callback, user_data);

	soup_socket_connect_async_internal (priv->socket, cancellable,
					    socket_connect_complete, task);
	soup_socket_properties_pop_async_context (priv->socket_props);
}

gboolean
soup_connection_connect_finish (SoupConnection  *conn,
				GAsyncResult    *result,
				GError         **error)
{
	return g_task_propagate_boolean (G_TASK (result), error);
}

gboolean
soup_connection_connect_sync (SoupConnection  *conn,
			      GCancellable    *cancellable,
			      GError         **error)
{
	SoupConnectionPrivate *priv;
	SoupAddress *remote_addr;

	g_return_val_if_fail (SOUP_IS_CONNECTION (conn), FALSE);
	priv = soup_connection_get_instance_private (conn);
	g_return_val_if_fail (priv->socket == NULL, FALSE);

	soup_connection_set_state (conn, SOUP_CONNECTION_CONNECTING);

	/* Set the protocol to ensure correct proxy resolution. */
	remote_addr =
		g_object_new (SOUP_TYPE_ADDRESS,
			      SOUP_ADDRESS_NAME, priv->remote_uri->host,
			      SOUP_ADDRESS_PORT, priv->remote_uri->port,
			      SOUP_ADDRESS_PROTOCOL, priv->remote_uri->scheme,
			      NULL);

	priv->socket =
		soup_socket_new (SOUP_SOCKET_REMOTE_ADDRESS, remote_addr,
				 SOUP_SOCKET_SOCKET_PROPERTIES, priv->socket_props,
				 SOUP_SOCKET_FLAG_NONBLOCKING, FALSE,
				 NULL);
	g_object_unref (remote_addr);

	g_signal_connect (priv->socket, "event",
			  G_CALLBACK (re_emit_socket_event), conn);
	if (!soup_socket_connect_sync_internal (priv->socket, cancellable, error))
		return FALSE;

	priv->proxy_uri = soup_socket_get_http_proxy_uri (priv->socket);

	if (priv->ssl && !priv->proxy_uri) {
		if (!soup_socket_handshake_sync (priv->socket,
						 priv->remote_uri->host,
						 cancellable, error))
			return FALSE;
	}

	if (!priv->ssl || !priv->proxy_uri) {
		soup_connection_event (conn,
				       G_SOCKET_CLIENT_COMPLETE,
				       NULL);
	}
	soup_connection_set_state (conn, SOUP_CONNECTION_IN_USE);
	priv->unused_timeout = time (NULL) + SOUP_CONNECTION_UNUSED_TIMEOUT;
	start_idle_timer (conn);

	return TRUE;
}

gboolean
soup_connection_is_tunnelled (SoupConnection *conn)
{
	SoupConnectionPrivate *priv;

	g_return_val_if_fail (SOUP_IS_CONNECTION (conn), FALSE);
	priv = soup_connection_get_instance_private (conn);

	return priv->ssl && priv->proxy_uri != NULL;
}

gboolean
soup_connection_start_ssl_sync (SoupConnection  *conn,
				GCancellable    *cancellable,
				GError         **error)
{
	SoupConnectionPrivate *priv;

	g_return_val_if_fail (SOUP_IS_CONNECTION (conn), FALSE);
	priv = soup_connection_get_instance_private (conn);

	if (soup_socket_handshake_sync (priv->socket, priv->remote_uri->host,
					cancellable, error)) {
		soup_connection_event (conn, G_SOCKET_CLIENT_COMPLETE, NULL);
		return TRUE;
	} else
		return FALSE;
}

static void
start_ssl_completed (GObject *object, GAsyncResult *result, gpointer user_data)
{
	GTask *task = user_data;
	SoupConnection *conn = g_task_get_source_object (task);
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);
	GError *error = NULL;

	if (soup_socket_handshake_finish (priv->socket, result, &error)) {
		soup_connection_event (conn, G_SOCKET_CLIENT_COMPLETE, NULL);
		g_task_return_boolean (task, TRUE);
	} else
		g_task_return_error (task, error);
	g_object_unref (task);
}

void
soup_connection_start_ssl_async (SoupConnection      *conn,
				 GCancellable        *cancellable,
				 GAsyncReadyCallback  callback,
				 gpointer             user_data)
{
	SoupConnectionPrivate *priv;
	GTask *task;

	g_return_if_fail (SOUP_IS_CONNECTION (conn));
	priv = soup_connection_get_instance_private (conn);

	soup_socket_properties_push_async_context (priv->socket_props);
	task = g_task_new (conn, cancellable, callback, user_data);

	soup_socket_handshake_async (priv->socket, priv->remote_uri->host,
				     cancellable, start_ssl_completed, task);

	soup_socket_properties_pop_async_context (priv->socket_props);
}

gboolean
soup_connection_start_ssl_finish (SoupConnection  *conn,
				  GAsyncResult    *result,
				  GError         **error)
{
	return g_task_propagate_boolean (G_TASK (result), error);
}

/**
 * soup_connection_disconnect:
 * @conn: a connection
 *
 * Disconnects @conn's socket and emits a %disconnected signal.
 * After calling this, @conn will be essentially useless.
 **/
void
soup_connection_disconnect (SoupConnection *conn)
{
	SoupConnectionPrivate *priv;
	SoupConnectionState old_state;

	g_return_if_fail (SOUP_IS_CONNECTION (conn));
	priv = soup_connection_get_instance_private (conn);

	old_state = priv->state;
	if (old_state != SOUP_CONNECTION_DISCONNECTED)
		soup_connection_set_state (conn, SOUP_CONNECTION_DISCONNECTED);

	if (priv->socket) {
		SoupSocket *socket = priv->socket;

		g_signal_handlers_disconnect_by_func (socket, G_CALLBACK (re_emit_socket_event), conn);

		priv->socket = NULL;
		soup_socket_disconnect (socket);
		g_object_unref (socket);
	}

	if (old_state != SOUP_CONNECTION_DISCONNECTED)
		g_signal_emit (conn, signals[DISCONNECTED], 0);
}

SoupSocket *
soup_connection_get_socket (SoupConnection *conn)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	g_return_val_if_fail (SOUP_IS_CONNECTION (conn), NULL);

	return priv->socket;
}

SoupURI *
soup_connection_get_remote_uri (SoupConnection *conn)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	g_return_val_if_fail (SOUP_IS_CONNECTION (conn), NULL);

	return priv->remote_uri;
}

SoupURI *
soup_connection_get_proxy_uri (SoupConnection *conn)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	g_return_val_if_fail (SOUP_IS_CONNECTION (conn), NULL);

	return priv->proxy_uri;
}

gboolean
soup_connection_is_via_proxy (SoupConnection *conn)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	g_return_val_if_fail (SOUP_IS_CONNECTION (conn), FALSE);

	return priv->proxy_uri != NULL;
}

SoupConnectionState
soup_connection_get_state (SoupConnection *conn)
{
	SoupConnectionPrivate *priv;

	g_return_val_if_fail (SOUP_IS_CONNECTION (conn),
			      SOUP_CONNECTION_DISCONNECTED);
	priv = soup_connection_get_instance_private (conn);

	if (priv->state == SOUP_CONNECTION_IDLE &&
	    (!soup_socket_is_connected (priv->socket) ||
	     soup_socket_is_readable (priv->socket)))
		soup_connection_set_state (conn, SOUP_CONNECTION_REMOTE_DISCONNECTED);

	if (priv->state == SOUP_CONNECTION_IDLE &&
	    priv->unused_timeout && priv->unused_timeout < time (NULL))
		soup_connection_set_state (conn, SOUP_CONNECTION_REMOTE_DISCONNECTED);

	return priv->state;
}

void
soup_connection_set_state (SoupConnection *conn, SoupConnectionState state)
{
	SoupConnectionPrivate *priv;

	g_return_if_fail (SOUP_IS_CONNECTION (conn));
	g_return_if_fail (state >= SOUP_CONNECTION_NEW &&
			  state <= SOUP_CONNECTION_DISCONNECTED);

	g_object_freeze_notify (G_OBJECT (conn));

	priv = soup_connection_get_instance_private (conn);

	if (priv->current_msg) {
		g_warn_if_fail (state == SOUP_CONNECTION_IDLE ||
				state == SOUP_CONNECTION_DISCONNECTED);
		clear_current_msg (conn);
	}

	if (state == SOUP_CONNECTION_IDLE && !priv->reusable) {
		/* This will recursively call set_state() */
		soup_connection_disconnect (conn);
	} else {
		priv->state = state;

		if (priv->state == SOUP_CONNECTION_IDLE)
			start_idle_timer (conn);

		g_object_notify (G_OBJECT (conn), "state");
	}

	g_object_thaw_notify (G_OBJECT (conn));
}

gboolean
soup_connection_get_ever_used (SoupConnection *conn)
{
	SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);

	g_return_val_if_fail (SOUP_IS_CONNECTION (conn), FALSE);

	return priv->unused_timeout == 0;
}

void
soup_connection_send_request (SoupConnection          *conn,
			      SoupMessageQueueItem    *item,
			      SoupMessageCompletionFn  completion_cb,
			      gpointer                 user_data)
{
	SoupConnectionPrivate *priv;

	g_return_if_fail (SOUP_IS_CONNECTION (conn));
	g_return_if_fail (item != NULL);
	priv = soup_connection_get_instance_private (conn);
	g_return_if_fail (priv->state != SOUP_CONNECTION_NEW &&
			  priv->state != SOUP_CONNECTION_DISCONNECTED);

	if (item->msg != priv->current_msg)
		set_current_msg (conn, item->msg);
	else
		priv->reusable = FALSE;

	soup_message_send_request (item, completion_cb, user_data);
}
