/*
 * libsyncml - A syncml protocol implementation
 * Copyright (C) 2005  Armin Bauer <armin.bauer@opensync.org>
 * Copyright (C) 2007-2009  Michael Bell <michael.bell@opensync.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 *
 */

#include <config.h>
 
#include <libsyncml/sml_support.h>
#include <libsyncml/sml_error_internals.h>

#include <libsyncml/sml_transport_internals.h>
#include "../data_sync_api/sml_data_sync_defines.h"

#include "http_client_internals.h"
#include <string.h>

static void     smlTransportHttpClientSend       (void *userdata, void *link, SmlTransportData *data, GError *error);
static void     smlTransportHttpClientDisconnect (void *data, void *linkdata);
static gboolean smlTransportHttpClientFinalize   (void *data, GError **error);

#ifdef HAVE_LIBSOUP22
#define soup_message_headers_get soup_message_get_header
#define soup_message_headers_append soup_message_add_header
#endif

#ifdef HAVE_LIBSOUP22
static void smlTransportHttpClientCallback(SoupMessage *msg, gpointer userdata)
{
	smlTrace(TRACE_ENTRY, "%s(%p, %p)", __func__, msg, userdata);
#else
static void smlTransportHttpClientCallback(SoupSession *session, SoupMessage *msg, gpointer userdata)
{
	/* session is only traced to avoid warnings from gcc profiling */
	smlTrace(TRACE_ENTRY, "%s(%p, %p, %p)", __func__, session, msg, userdata);
#endif
	SmlTransportHttpClientEnv *env = userdata;
	GError *error = NULL;

	/* handle potential session shutdown */
	if (env->connectDone && msg->status_code == SOUP_STATUS_CANCELLED)
	{
		/* FIXME: This is clearly a disconnect.
		 * FIXME: You must check if a disconnect is ongoing.
		 */
		smlTrace(TRACE_EXIT, "%s - ignoring due to shutdown", __func__);
		return;
	}

	/* handle a fresh connection */
	if (!env->connectDone)
	{
		/* This is the first answer or any other event which is
		 * detected after the first message was sent.
		 * We have to check and signal if the connection is ok or not.
		 */
		if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code))
		{
			g_set_error(&error,
				 SML_ERROR, SML_ERROR_GENERIC,
				 "Connection failed (%d) - %s", msg->status_code, msg->reason_phrase);
			goto error;
		}
		else
		{
			/* If a transport has no public connect function
			 * then the connect event is sent by the
			 * transport layer itself.
			 */
			smlTrace(TRACE_INTERNAL, "%s: Connect done", __func__);
			if (!env->tsp->connected)
			{
				/* If a user of the library know that connect
				 * is not available then this is tolerated.
				 */
        			smlTransportReceiveEvent(
					env->tsp, NULL,
					SML_TRANSPORT_EVENT_CONNECT_DONE,
					NULL, NULL);
			}
		}
		env->connectDone = TRUE;
	}
	smlTrace(TRACE_INTERNAL, "%s: Result: %d %s",
		__func__, msg->status_code, msg->reason_phrase);

	/* start http handling with some checks */

	/* check the library status for errors */
	if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
		g_set_error(&error, SML_ERROR, SML_ERROR_GENERIC, "Call not successfull: %d %s",
			msg->status_code, msg->reason_phrase);
		goto error;
	}

	/* check the header of the received message */
	const char *header = soup_message_headers_get(msg->response_headers, "Content-Type");
	smlTrace(TRACE_INTERNAL, "%s: content type ::= %s",
		__func__, header);
	
	SmlMimeType mimetype = SML_MIMETYPE_UNKNOWN;
	if (header && !g_strncasecmp(header, SML_ELEMENT_XML, strlen(SML_ELEMENT_XML)))
		mimetype = SML_MIMETYPE_XML;
	else if(header && !g_strncasecmp(header, SML_ELEMENT_WBXML, strlen(SML_ELEMENT_WBXML)))
		mimetype = SML_MIMETYPE_WBXML;
	else if(header && !g_strncasecmp(header, SML_ELEMENT_SAN, strlen(SML_ELEMENT_SAN)))
		mimetype = SML_MIMETYPE_SAN;
	else if (header) {
		g_set_error(&error, SML_ERROR, SML_ERROR_GENERIC, "Unknown mimetype");
		goto error;
	} else {
		g_set_error(&error, SML_ERROR, SML_ERROR_GENERIC, "Faulty mimetype");
		goto error;
		
	}

	/* prepare the received message */
	char *data;
	gsize length;

#ifdef HAVE_LIBSOUP22
	length = msg->response.length;
#else
	length = msg->response_body->length;
#endif
	smlTrace(TRACE_INTERNAL, "%s: The message length is %i.", __func__, length);

	/* We need one byte more than the response length
	 * because the data can be a native XML message.
	 * If the data is a native XML message then it is
	 * sometimes directly used as string.
	 *
	 * The string is automatically NULL terminated
	 * because smlTryMalloc0 fills the new memory with NULLs.
	 */
	data = smlTryMalloc0(length + 1, &error);
	if (!data)
		goto error;

#ifdef HAVE_LIBSOUP22
	memcpy(data, msg->response.body, length);
#else
	memcpy(data, msg->response_body->data, length);
#endif

	SmlTransportData *tspdata = smlTransportDataNew(data, length, mimetype, TRUE, &error);
	data = NULL;
	if (!tspdata)
		goto error_free_data;

	/* signal the received message */
	smlTransportReceiveEvent(env->tsp, NULL, SML_TRANSPORT_EVENT_DATA, tspdata, NULL);

	/* cleanup */
	smlTransportDataDeref(tspdata);

	smlTrace(TRACE_EXIT, "%s", __func__);
	return;
	
error_free_data:
	smlSafeCFree(&data);
error:
	smlTransportReceiveEvent(env->tsp, NULL, SML_TRANSPORT_EVENT_ERROR, NULL, error);
	smlTrace(TRACE_EXIT_ERROR, "%s: %s", __func__, error->message);
	g_error_free(error);
}

/* This is the authentication callback for libsoup.
 * If a SyncML server uses HTTP then the specification allows
 * standard HTTP authentication. This callback provides the
 * http library with the user and passphrase for the SyncML
 * account. Usually this is not required because the most
 * SyncML server manage the authentication via SyncHdr.
 */

#ifdef HAVE_LIBSOUP22
static void
_authenticate (SoupSession *session, SoupMessage *msg,
	      const char *auth_type, const char *auth_realm,
	      char **username, char **password, gpointer data)
{
	SmlTransportHttpClientEnv *env = data;
	smlTrace(TRACE_INTERNAL, "%s: authentication via auth_type %s", __func__, VA_STRING(auth_type));
	*username = g_strdup(env->username);
	*password = g_strdup(env->password);
}
#else
static void
_authenticate (SoupSession *session, SoupMessage *msg,
	       SoupAuth *auth, gboolean retrying, gpointer data)
{
	smlTrace(TRACE_INTERNAL, "%s(%p, %p, %p, %d, %p)", __func__, session, msg, auth, retrying, data);
	SmlTransportHttpClientEnv *env = data;
	smlTrace(TRACE_INTERNAL, "%s: authentication via auth_type %s", __func__, VA_STRING(soup_auth_get_scheme_name(auth)));

	if (!retrying)
		soup_auth_authenticate(auth, env->username, env->password);
}
#endif

static gboolean
smlTransportHttpClientSetConfigOption (SmlTransport *tsp,
                                       const gchar *name,
                                       const gchar *value,
                                       GError **error)
{
	smlTrace(TRACE_ENTRY, "%s(%p, %s, %s, %p)", __func__, tsp, VA_STRING(name), VA_STRING(value), error);
	CHECK_ERROR_REF
	smlAssert(tsp);
	smlAssert(tsp->transport_data);
	SmlTransportHttpClientEnv *env = tsp->transport_data;	

	if (!strcmp(name, SML_TRANSPORT_CONFIG_URL)) {
		if (env->url)
			smlSafeCFree(&(env->url));
		if (env->uri)
			soup_uri_free(env->uri);
		env->url = g_strdup(value);
		env->uri = soup_uri_new(env->url);
		if (env->uri == NULL) {
			g_set_error(error, SML_ERROR, SML_ERROR_INTERNAL_MISCONFIGURATION,
				"The configured url %s is wrong.", env->url);
			smlTrace(TRACE_EXIT_ERROR, "%s - %s", __func__, (*error)->message);
			return FALSE;
		}
		smlTrace(TRACE_INTERNAL, "%s: URL %s detected", __func__, VA_STRING(env->url));
	} else if (!strcmp(name, SML_TRANSPORT_CONFIG_PROXY)) {
		env->proxy = g_strdup(value);
		smlTrace(TRACE_INTERNAL, "%s: PROXY %s detected", __func__, VA_STRING(env->proxy));
	} else if (!strcmp(name, SML_TRANSPORT_CONFIG_USERNAME)) {
		env->username = g_strdup(value);
		smlTrace(TRACE_INTERNAL, "%s: USERNAME %s detected", __func__, VA_STRING(env->username));
	} else if (!strcmp(name, SML_TRANSPORT_CONFIG_PASSWORD)) {
		env->password = g_strdup(value);
		smlTrace(TRACE_INTERNAL, "%s: PASSWORD ******* detected", __func__);
	} else if (!strcmp(name, SML_TRANSPORT_CONFIG_SSL_CA_FILE)) {
		env->cafile = g_strdup(value);
		smlTrace(TRACE_INTERNAL, "%s: SSL_CA_FILE %s detected", __func__, VA_STRING(env->cafile));
	} else {
		g_set_error(error, SML_ERROR, SML_ERROR_INTERNAL_MISCONFIGURATION, "Unknown parameter %s found.", name);
		smlTrace(TRACE_EXIT_ERROR, "%s - %s", __func__, (*error)->message);
		return FALSE;
	}

	smlTrace(TRACE_EXIT, "%s", __func__);
	return TRUE;
}

/* WARNING: Do not register this function as connect function.
 *          This is only an internal connect function.
 */
static void smlTransportHttpClientConnect(void *data)
{
	smlTrace(TRACE_ENTRY, "%s(%p)", __func__, data);
	smlAssert(data);
	SmlTransportHttpClientEnv *env = data;
	GError *error = NULL;

	if (!env->tsp->context)
	{
		g_set_error(&error, SML_ERROR, SML_ERROR_INTERNAL_MISCONFIGURATION,
			"HTTP client runs only in asynchronous mode. So the context must be set.");
		goto error;
	}

	env->connectDone = FALSE;
	env->disconnectDone = FALSE;

	if (env->proxy != NULL && strlen(env->proxy) > 0)
	{
		if (env->cafile != NULL && strlen(env->cafile))
		{
#ifdef HAVE_LIBSOUP22_SOLARIS
			env->session = soup_session_sync_new_with_options(
#else
			env->session = soup_session_async_new_with_options(
					SOUP_SESSION_ASYNC_CONTEXT, env->tsp->context,
#endif
					SOUP_SESSION_PROXY_URI, env->proxy,
					SOUP_SESSION_SSL_CA_FILE, env->cafile,
					NULL);
			/* FIXME: libsoup does not support CRLs
			 * FIXME: CRL support is required for minimum security.
			 * FIXME: http://bugzilla.gnome.org/show_bug.cgi?id=547881
			 */
		} else {
#ifdef HAVE_LIBSOUP22_SOLARIS
			env->session = soup_session_sync_new_with_options(
#else
			env->session = soup_session_async_new_with_options(
					SOUP_SESSION_ASYNC_CONTEXT, env->tsp->context,
#endif
					SOUP_SESSION_PROXY_URI, env->proxy,
					NULL);
		}
	} else {
		if (env->cafile != NULL && strlen(env->cafile))
		{
#ifdef HAVE_LIBSOUP22_SOLARIS
			env->session = soup_session_sync_new_with_options(
#else
			env->session = soup_session_async_new_with_options(
					SOUP_SESSION_ASYNC_CONTEXT, env->tsp->context,
#endif
					SOUP_SESSION_SSL_CA_FILE, env->cafile,
					NULL);
		} else {
#ifdef HAVE_LIBSOUP22_SOLARIS
			env->session = soup_session_sync_new();
#else
			env->session = soup_session_async_new_with_options(
					SOUP_SESSION_ASYNC_CONTEXT, env->tsp->context,
					NULL);
#endif
		}
	}
	if (!env->session) {
		g_set_error(&error, SML_ERROR, SML_ERROR_GENERIC, "Unable to create new session");
		goto error;
	}

	/* add credentials to the environment and
	 * enable authentication callback for http authentication
	 */
	if (env->username || env->password)
	{
		g_signal_connect (env->session, "authenticate", G_CALLBACK (_authenticate), env);
	} else {
		env->username = NULL;
		env->password = NULL;
	}

	smlTrace(TRACE_EXIT, "%s: %p", __func__, env);
	return;
error:
	smlTransportReceiveEvent(env->tsp, NULL, SML_TRANSPORT_EVENT_ERROR, NULL, error);
	smlTrace(TRACE_EXIT_ERROR, "%s: %s", __func__, error->message);
	g_error_free(error);
}

static gboolean
smlTransportHttpClientSetResponseURI (SmlTransport *tsp,
                                      const gchar *uri,
                                      GError **error)
{
	smlTrace(TRACE_ENTRY, "%s(%p, %s, %p)", __func__, tsp, VA_STRING(uri), error);
	CHECK_ERROR_REF
	smlAssert(tsp);
	smlAssert(tsp->transport_data);
	SmlTransportHttpClientEnv *env = tsp->transport_data;	
	smlAssert(uri);

	if (env->url)
		smlSafeCFree(&(env->url));
	if (env->uri)
		soup_uri_free(env->uri);

	env->url = g_strdup(uri);
	env->uri = soup_uri_new(env->url);
	if (env->uri == NULL) {
		g_set_error(error, SML_ERROR, SML_ERROR_INTERNAL_MISCONFIGURATION,
			"The specified url \"%s\" is wrong.", env->url);
		smlTrace(TRACE_EXIT_ERROR, "%s - %s", __func__, (*error)->message);
		return FALSE;
	}

	smlTrace(TRACE_EXIT, "%s", __func__);
	return TRUE;
}

static gboolean
smlTransportHttpClientCleanupSession (gpointer data,
                                      GError **error)
{
	smlTrace(TRACE_ENTRY, "%s", __func__);
	CHECK_ERROR_REF
	smlAssert(data);

	SoupSession *session = data;
	
	soup_session_abort(session);
	g_object_unref(session);

	smlTrace(TRACE_EXIT, "%s", __func__);
	return TRUE;
}

static gboolean
smlTransportHttpClientFinalize (void *data,
                                GError **error)
{
	smlTrace(TRACE_ENTRY, "%s(%p, %p)", __func__, data, error);
	CHECK_ERROR_REF
	smlAssert(data);
	SmlTransportHttpClientEnv *env = data;
	
	smlAssert(env->tsp);

	if (env->session)
	{
		if (!smlThreadCallFunction(env->tsp->thread, smlTransportHttpClientCleanupSession, env->session, error))
			goto error;
		env->session = NULL;
	}

	if (env->uri)
		soup_uri_free(env->uri);

	if (env->url)
		smlSafeCFree(&(env->url));

	if (env->proxy)
		smlSafeCFree(&(env->proxy));

	smlSafeFree((gpointer *)&env);
	
	smlTrace(TRACE_EXIT, "%s", __func__);
	return TRUE;
error:
	smlTrace(TRACE_EXIT_ERROR, "%s - %s", __func__, (*error)->message);
	return FALSE;
}

static void
smlTransportHttpClientSend (void *userdata,
                            void *link_,
                            SmlTransportData *data,
                            GError *error)
{
	smlTrace(TRACE_ENTRY, "%s(%p, %p, %p, %p)", __func__, userdata, link_, data, error);
	smlAssert(error || data);
	smlAssert(userdata);
	SmlTransportHttpClientEnv *env = userdata;
	smlAssert(env);
	smlAssert(env->uri);
	
	if (error)
		goto error;

	if (!env->connectDone)
		smlTransportHttpClientConnect(userdata);
		
	SoupMessage *msg = soup_message_new_from_uri(SOUP_METHOD_POST, env->uri);
	if (!msg) {
		g_set_error(&error, SML_ERROR, SML_ERROR_GENERIC, "unknown libsoup error during message_new");
		goto error;
	}

	const char *content_type;
	switch (data->type) {
		case SML_MIMETYPE_XML:
			content_type = SML_ELEMENT_XML;
			break;
		case SML_MIMETYPE_WBXML:
			content_type = SML_ELEMENT_WBXML;
			break;
		case SML_MIMETYPE_SAN:
			content_type = SML_ELEMENT_SAN;
			break;
		default:
			g_set_error(&error, SML_ERROR, SML_ERROR_GENERIC, "Unknown Mimetype %d", data->type);
			goto error_free_message;
	}

 	soup_message_headers_append(msg->request_headers, "Accept", content_type);
 	soup_message_set_request (msg, content_type,
#ifdef HAVE_LIBSOUP22
				  SOUP_BUFFER_SYSTEM_OWNED,
#else
				  SOUP_MEMORY_TAKE,
#endif
				  g_memdup(data->data, data->size), data->size);

#ifdef HAVE_LIBSOUP22
	smlTrace(TRACE_INTERNAL, "%s: data length: %i", __func__, msg->request.length);

#else
	smlTrace(TRACE_INTERNAL, "%s: data length: %i", __func__, msg->request_body->length);
#endif
	
#ifdef HAVE_LIBSOUP22_SOLARIS
	soup_session_send_message(env->session, msg);
	smlTrace(TRACE_INTERNAL, "%s: sent message and starting callback", __func__);
	smlTransportHttpClientCallback(msg, userdata);
	g_object_unref(msg); /* synchronous mode !!! */
#else
	soup_session_queue_message(env->session, msg, smlTransportHttpClientCallback, userdata);
#endif
	
	smlTrace(TRACE_EXIT, "%s", __func__);
	return;
	
error_free_message:
	g_object_unref(msg);
error:
	smlTransportReceiveEvent(env->tsp, NULL, SML_TRANSPORT_EVENT_ERROR, NULL, error);
	smlTrace(TRACE_EXIT_ERROR, "%s: %s", __func__, error->message);
	g_error_free(error);
}

static void smlTransportHttpClientDisconnect(void *data, void *linkdata)
{
	smlTrace(TRACE_ENTRY, "%s(%p, %p)", __func__, data, linkdata);
	smlAssert(data);
	SmlTransportHttpClientEnv *env = data;
	smlAssert(env);
	smlAssert(env->tsp);

	if (env->disconnectDone)
	{
		smlTrace(TRACE_EXIT, "%s - ignoring because already disconnect", __func__);
		return;
	}

	if (!env->connectDone && !env->session)
	{
		/* The client is not connected. */
		env->disconnectDone = TRUE;
        	smlTransportReceiveEvent(env->tsp, NULL, SML_TRANSPORT_EVENT_DISCONNECT_DONE, NULL, NULL);
		smlTrace(TRACE_EXIT, "%s - ignoring because never connected", __func__);
		return;
	}

	smlAssert(env->session);

	/* The only reason to implement a disconnect function is the fact
	 * that some servers need a TCP/IP disconnect to cleanly finish
	 * a SyncML session.
	 *
	 * Otherwise we are locked into an endless loop of Slow-Syncs.
	 */

	soup_session_abort(env->session);
	g_object_unref(env->session);
	env->session = NULL;

	/* Make a new disconnect object */
	env->disconnectDone = TRUE;
        smlTransportReceiveEvent(env->tsp, NULL, SML_TRANSPORT_EVENT_DISCONNECT_DONE, NULL, NULL);

        smlTrace(TRACE_EXIT, "%s", __func__);
}

gboolean
smlTransportHttpClientNew (SmlTransport *tsp,
                           GError **error)
{
	CHECK_ERROR_REF
	smlAssert(tsp);

#ifdef HAVE_LIBSOUP22_SOLARIS
	smlTrace(TRACE_INTERNAL, "%s: Enabled libsoup 2.2 Solaris workarounds", __func__);
#endif

	tsp->functions.set_config_option = smlTransportHttpClientSetConfigOption;
	tsp->functions.set_response_uri = smlTransportHttpClientSetResponseURI;
	tsp->functions.disconnect = smlTransportHttpClientDisconnect;
	tsp->functions.finalize = smlTransportHttpClientFinalize;
	tsp->functions.send = smlTransportHttpClientSend;

	SmlTransportHttpClientEnv *env = smlTryMalloc0(sizeof(SmlTransportHttpClientEnv), error);
	if (!env)
		return FALSE;
	tsp->transport_data = env;
	env->tsp = tsp;

	return TRUE;
}

