/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/***************************************************************************
 *            kolab-util-http.c
 *
 *  Fri Aug  6 10:30:54 2010
 *  Copyright  2010 Christian Hilberg <hilberg@kernelconcepts.de>
 *         and 2011 Silvan Marco Fin <silvan@kernelconcepts.de>
 ****************************************************************************/

/*
 * This program 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 program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor Boston, MA 02110-1301,  USA
 */

/*----------------------------------------------------------------------------*/

/*
 * Wrapper functions for HTTP access.
 *
 * The functions herein must provide an interface to the rest of the
 * plugin which will hide all HTTP- and TLS-Library specifics so the
 * libs used now can be replaced when the GIO TLS works have landed
 * and LibSoup will be suitable for our needs.
 *
 */

/*----------------------------------------------------------------------------*/

/* TODO settle on either one of
 * - LibSoup (no client certs via GnuTLS)
 * - LibCurl
 *
 * TODO add proxy support
 *
 * TODO Move LibCurl-stuff into its own files
 *      if it will also be used for LDAP
 *      (add kolab-util-curl.[hc] to libekolab)
 */

/*----------------------------------------------------------------------------*/

#include <config.h>

#include <glib.h>
#include <glib/gi18n-lib.h>

#include <camel/camel-kolab-stream.h>

/* HTTP lib specifics */
#include <libsoup/soup-gnome.h>
#include <curl/curl.h>

#include "camel-system-headers.h"
#include "kolab-util-http.h"

/*----------------------------------------------------------------------------*/

/* TEMPORARY */
/* TODO: move to config file, since this is configurable */
#define KOLAB_UTIL_HTTP_CURL_DEBUG 0		/* 1 or 0 */
#define KOLAB_UTIL_HTTP_CURL_SSL_VERIFY_PEER 1L /* 1L or 0L */
#define KOLAB_UTIL_HTTP_CURL_SSL_VERIFY_HOST 2L /* 2L, 1L or 0L */
#define KOLAB_UTIL_HTTP_CURL_FOLLOW_REDIRECTS 1 /* 1 or 0 */

#define KOLAB_UTIL_HTTP_CURL_NSS_CERTDB ".pki/nssdb"

/*----------------------------------------------------------------------------*/

/* Error handling for kolab-util-http methods */

GQuark
kolab_util_http_error_quark (void)
{
	return g_quark_from_static_string ("kolab-util-http-error-quark");
}

/* TODO change this to conditional compilation once
 *      things have settled.
 *      We keep this for now to make sure all variants
 *      compile cleanly
 */
typedef enum {
	KOLAB_UTIL_HTTP_LIB_SOUP = 0,
	KOLAB_UTIL_HTTP_LIB_CURL
} KolabUtilHttpLib;

/*----------------------------------------------------------------------------*/

/* TODO get rid of these globals if possible */
/* static SoupSession *kolab_soup_session = NULL; */
static CURL *kolab_curl_handle = NULL;

/*----------------------------------------------------------------------------*/
/* KolabUtilHttpJob */

KolabUtilHttpJob*
kolab_util_http_job_new (void)
{
	KolabUtilHttpJob *job = g_new0 (KolabUtilHttpJob, 1);
	job->url = NULL;
	job->buffer = NULL;
	job->nbytes = 0;
	job->passwd = NULL;
	job->pkcs11pin = NULL;

	return job;
}

void
kolab_util_http_job_free (KolabUtilHttpJob *job)
{
	if (job == NULL)
		return;

	if (job->url != NULL)
		camel_url_free (job->url);
	if (job->buffer != NULL)
		g_byte_array_free (job->buffer, TRUE);
	if (job->passwd != NULL)
		g_free (job->passwd);
	if (job->pkcs11pin != NULL)
		g_free (job->pkcs11pin);
	g_free (job);
}

/*----------------------------------------------------------------------------*/
/* LibSoup */
#if 0
static void
kolab_util_http_soup_authenticate (SoupSession *session,
                                   SoupMessage *msg,
                                   SoupAuth *auth,
                                   gboolean retrying,
                                   gpointer data)
{
	KolabUtilHttpJob *job;

	(void)session;
	(void)msg;

	/* no second auth attempt */
	if (retrying)
		return;

	job = (KolabUtilHttpJob*) data;
	g_assert( job != NULL);
	soup_auth_authenticate (auth,
	                        job->url->user,
	                        job->url->passwd);
}

static gssize
kolab_util_http_soup_get (const KolabUtilHttpJob *job, GError **error)
{
	g_error ("%s: not implemented.", __func__);
	SoupMessage *msg = NULL;
	CamelStream *mem = NULL;
	guint status = 0;
	gssize nbytes = -1;
	gchar *url_string = NULL;

	static gulong kolab_soup_auth_handler_id = 0; /* TODO is 0 an invalid id? */

	g_return_val_if_fail (error == NULL || *error == NULL, -1);

	/* connect the "authenticate" signal handler,
	 * disconnect a previously connected one.
	 * TODO: check whether we need to disconnect-connect
	 *       a handler here (KolabUtilHttpJob* may point to
	 *       an address different from the one we were
	 *       called upon last time)
	 */
	if (g_signal_handler_is_connected (kolab_soup_session,
	                                   kolab_soup_auth_handler_id))
		g_signal_handler_disconnect (kolab_soup_session,
		                             kolab_soup_auth_handler_id);
	kolab_soup_auth_handler_id = g_signal_connect (kolab_soup_session,
	                                               "authenticate",
	                                               G_CALLBACK (kolab_util_http_soup_authenticate),
	                                               (gpointer)job);

	/* issue HTTP GET */
	url_string = camel_url_to_string (job->url, CAMEL_URL_HIDE_PASSWORD | CAMEL_URL_HIDE_AUTH);
	msg = soup_message_new (SOUP_METHOD_GET, url_string);
	g_free (url_string);
	/* soup_message_set_flags (msg, SOUP_MESSAGE_NO_REDIRECT); */
	status = soup_session_send_message (kolab_soup_session, msg);

	/* copy msg contents into buffer */
	if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
		mem = camel_kolab_stream_new_memstream (job->buffer);
		nbytes = camel_stream_write (mem,
		                             (gchar*)msg->response_body->data,
		                             (gsize)msg->response_body->length);
		camel_kolab_stream_free (mem);
		g_debug ("%s: %d bytes received", __func__, nbytes);
	}
	else {
		KolabUtilHttpError error_code;
		if (status > 500)
			error_code = KOLAB_UTIL_HTTP_ERROR_SERVER_ERROR;
		else if (status > 400)
			error_code = KOLAB_UTIL_HTTP_ERROR_CLIENT_ERROR;
		else /* if (status > 100) */
			/* 1xx informational and 2xx is considered success, then why are we here? */
			error_code = KOLAB_UTIL_HTTP_ERROR_UNKNOWN_ERROR;

		g_set_error (error,
		             KOLAB_UTIL_HTTP_ERROR,
		             error_code,
		             _("Libsoup failed with status %u"),
		             status);
	}

	return nbytes;
}
#endif

/*----------------------------------------------------------------------------*/
/* LibCurl */

static size_t
kolab_util_http_curl_writefunc (const void *data,
                                const size_t size,
                                const size_t nmemb,
                                void *stream)
{
	GByteArray *buffer = (GByteArray*) stream;
	size_t nbytes = size * nmemb;

	/* TODO read from data, write into buffer */
	g_byte_array_append (buffer,
	                     (const guint8*) data,
	                     (guint) nbytes);
	return nbytes;
}

static CURLcode
kolab_util_http_curl_set_genopts (void)
{
	/* TODO change this to table-based for-loop */
	CURLcode curlcode; /* can't bit-or this... */
	curlcode =  curl_easy_setopt (kolab_curl_handle,
	                              CURLOPT_VERBOSE,
	                              KOLAB_UTIL_HTTP_CURL_DEBUG);
	if (curlcode) return curlcode;
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_FAILONERROR,
	                             1);
	if (curlcode) return curlcode;
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_NETRC,
	                             CURL_NETRC_IGNORED);
	if (curlcode) return curlcode;
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_NOPROGRESS,
	                             1);
	if (curlcode) return curlcode;
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_WRITEFUNCTION,
	                             kolab_util_http_curl_writefunc);
	if (curlcode) return curlcode;
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_HTTPAUTH,
	                             CURLAUTH_ANY);
	if (curlcode) return curlcode;
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_FOLLOWLOCATION,
	                             KOLAB_UTIL_HTTP_CURL_FOLLOW_REDIRECTS);
	if (curlcode) return curlcode;
	/* SSL setup
	 * The following may fail if NSS is not yet initialized
	 */
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_SSL_VERIFYPEER,
	                             KOLAB_UTIL_HTTP_CURL_SSL_VERIFY_PEER);
	if (curlcode) return curlcode;
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_SSL_VERIFYHOST,
	                             KOLAB_UTIL_HTTP_CURL_SSL_VERIFY_HOST);
	if (curlcode) return curlcode;

	/* TODO more here
	 * - HTTP options (referrers, encoding, follow, user-agent...)
	 */

	return curlcode;
}

static CURLcode
kolab_util_http_curl_set_jobopts (const KolabUtilHttpJob *job)
{
	CURLcode curlcode;
	gchar *usrpwd = NULL;
	gchar *url_string;

	/* buffer to write answer data to */
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_WRITEDATA,
	                             (void*)job->buffer);
	if (curlcode)
		return curlcode;

	url_string = camel_url_to_string (job->url, CAMEL_URL_HIDE_AUTH);
	curlcode = curl_easy_setopt (kolab_curl_handle,
	                             CURLOPT_URL,
	                             url_string);
	g_free (url_string);
	if (curlcode)
		return curlcode;

	/* TODO limit valid protocols to HTTP(S) here, using
	 *      CURLOPT_PROTOCOLS and CURLPROTO_* masks
	 */

	/* set username/password for basic auth */
	if (job->url->user != NULL) {
		if (job->passwd != NULL) {
			usrpwd = g_strconcat (job->url->user,
			                      ":",
			                      job->passwd,
			                      NULL);
		} else {
			usrpwd = g_strdup (job->url->user);
			g_debug ("%s: username given but password not set",
			         __func__);
		}
	}
	if (usrpwd != NULL) {
		if (job->passwd != NULL)
			curlcode = curl_easy_setopt (kolab_curl_handle,
			                             CURLOPT_USERPWD,
			                             usrpwd);
		else
			curlcode = curl_easy_setopt (kolab_curl_handle,
			                             CURLOPT_USERNAME,
			                             usrpwd);
		g_free (usrpwd);
		if (curlcode)
			return curlcode;
	}
	/* TODO more here
	 * - SSL options (cert, cert key, ... maybe better done in genopts)
	 * - Proxy usage and auth
	 */

	/* CURLOPT_SSLCERT (nss): provide a nickname for certificate. In this
	 * case, we don't supply a nickname and expect the nss to do "the right
	 * thing".
	 */
	if (job->pkcs11pin != NULL) {
		curlcode = curl_easy_setopt (kolab_curl_handle,
		                             CURLOPT_KEYPASSWD,
		                             job->pkcs11pin);
		if (curlcode) return curlcode;
	}

	return curlcode;
}

static gssize
kolab_util_http_curl_get (KolabUtilHttpJob *job, GError **error)
{
	gssize nbytes = -1;
	CURLcode curlcode;
	KolabUtilHttpError error_code;

	g_return_val_if_fail (error == NULL || *error == NULL, -1);
	/* curl job setup */
	curlcode = kolab_util_http_curl_set_jobopts (job);
	if (curlcode) {
		g_set_error (error,
		             KOLAB_UTIL_HTTP_ERROR,
		             KOLAB_UTIL_HTTP_ERROR_CONFIGURATION_ERROR,
		             _("Configuring libcurl failed with CURLcode: %u"),
		             curlcode);
		return -1;
	}

	/* issue request */
	curlcode = curl_easy_perform (kolab_curl_handle);
	if (curlcode) {
		switch (curlcode) {
		case CURLE_OK:
			g_assert_not_reached ();
			break;
		case CURLE_HTTP_RETURNED_ERROR:
			error_code = KOLAB_UTIL_HTTP_ERROR_CLIENT_ERROR;
			break;
		default:
			error_code = KOLAB_UTIL_HTTP_ERROR_UNKNOWN_ERROR;
		}

		g_set_error (error,
		             KOLAB_UTIL_HTTP_ERROR,
		             error_code,
		             _("Access to URL '%s' failed in libcurl with CURLcode: %u"),
		             camel_url_to_string (job->url, CAMEL_URL_HIDE_AUTH), curlcode);
	}
	else {
		nbytes = (gssize)job->buffer->len;
	}

	/* TODO check whether we will have to add NULL
	 *      termination to job->buffer
	 *      if so, also increment nbytes
	 */

	return nbytes;
}

/*----------------------------------------------------------------------------*/

/**
 * kolab_util_http_init:
 *
 * Initializes the HTTP subsystem. This means
 * <itemizedlist>
 *   <listitem>init the camel session</listitem>
 *   <listitem>init the libsoup session</listitem>
 *   <listitem>init libcurl (set global flags)</listitem>
 * </itemizedlist>
 * This function must be called before any of the other kolab_util_http_*
 * functions.
 */
void
kolab_util_http_init (void)
{
	glong curl_global_flags;
	static gboolean http_is_initialized = FALSE;

	if (http_is_initialized == TRUE)
		return;
#if 0
	/* libsoup */
	kolab_soup_session = (SoupSession*)soup_session_sync_new ();
	g_assert (kolab_soup_session != NULL);
#endif

	/* libcurl */

	/* CURL_GLOBAL_ALL will also init SSL,
	 * which we want to leave to Camel...
	 * CURL_GLOBAL_NOTHING should init libcurl,
	 * but nothing extra (so say the docs)
	 */
	curl_global_flags = CURL_GLOBAL_ALL; /* & ~CURL_GLOBAL_SSL; */
	curl_global_init (curl_global_flags);

	kolab_curl_handle = curl_easy_init ();
	g_assert (kolab_curl_handle != NULL);
	kolab_util_http_curl_set_genopts ();
	http_is_initialized = TRUE;
}

/**
 * kolab_util_http_shutdown:
 *
 * Shuts down the HTTP subsystem. This means
 * <itemizedlist>
 *   <listitem>cleanup libcurl</listitem>
 *   <listitem>uninit the libsoup session</listitem>
 *   <listitem>shut down the camel session</listitem>
 * </itemizedlist>
 * None of the other kolab_util_http_* functions must be called
 * after a call to this function.
 *
 * Caution: Currently, there is no safeguard against improper use.
 */
void
kolab_util_http_shutdown (void)
{
	/* libcurl */
	curl_easy_cleanup (kolab_curl_handle);
	curl_global_cleanup ();

#if 0
	/* libsoup */
	g_object_unref (kolab_soup_session);
#endif
}

/*----------------------------------------------------------------------------*/

/**
 * kolab_util_http_get:
 * @job: the job description structure for issuing the HTTP request
 *
 * Issue a HTTP-GET request. The protocol part of the #KolabUtilHttpJob.url
 * specifies whether or not SSL will be used for transport security.
 *
 * Returns: the number of bytes read from the server. If an error occured,
 * before data was read, -1 is returned.
 */
gssize
kolab_util_http_get (KolabUtilHttpJob *job, GError **error)
{
	gssize nbytes = -1;

	nbytes = kolab_util_http_curl_get (job, error);

#if 0
	/* TODO change this to conditional compilation once
	 *      things have settled.
	 *      We keep this for now to make sure all variants
	 *      compile cleanly
	 */
	KolabUtilHttpLib httplib = KOLAB_UTIL_HTTP_LIB_CURL;

	switch (httplib) {
	case KOLAB_UTIL_HTTP_LIB_SOUP:
		nbytes = kolab_util_http_soup_get (job, error);
		break;
	case KOLAB_UTIL_HTTP_LIB_CURL:
		nbytes = kolab_util_http_curl_get (job, error);
		break;
	default:
		/* can't happen */
		g_assert_not_reached();
	}
#endif

	return nbytes;
}

#if 0
/**
 * kolab_util_http_new_hostname_from_url:
 * @url_string: the URL to extract the hostname part from
 *
 * The returned string should be freed using g_free() if no longer needed.
 *
 * Returns: The host name part from a given URL
 */
gchar*
kolab_util_http_new_hostname_from_url (const gchar *url_string)
{
	/* TODO move this to http-independent place
	 *      and rename
	 */
	CamelURL *url = NULL;
	gchar *hostname = NULL;
	CamelException *ex = NULL;

	g_assert (url_string != NULL);

	ex = camel_exception_new ();
	url = camel_url_new (url_string, ex);
	if (camel_exception_is_set (ex)) {
		g_warning ("%s: %s",
		           __func__, camel_exception_get_description (ex));
		if (url != NULL)
			camel_url_free (url);
		url = NULL;
	}
	camel_exception_free (ex);
	if (url == NULL)
		return NULL;

	hostname = g_strdup (url->host);
	camel_url_free (url);
	return hostname;
}
#endif

/**
 * kolab_util_http_protocol_is_ssl:
 * @url_string: the HTTP URL to check for SSL use. Must be
 *              in valid UTF-8 encoding.
 *
 * This function checks the protocol part of a HTTP URL whether
 * SSL transport security is requested.
 *
 * Returns: TRUE if we have 'https' URL, FALSE otherwise
 *
 */
gboolean
kolab_util_http_protocol_is_ssl (const gchar *url_string)
{
	gchar *scheme = NULL;
	gchar *scheme_utf8_tmp1 = NULL;
	gchar *scheme_utf8_tmp2 = NULL;
	gboolean is_ssl = FALSE;
	scheme = g_uri_parse_scheme (url_string);
	if (scheme == NULL) {
		g_debug ("%s: cannot parse url!", __func__);
		goto skip;
	}
	scheme_utf8_tmp1 = g_utf8_normalize (scheme,
	                                     g_utf8_strlen (scheme, -1),
	                                     G_NORMALIZE_DEFAULT);
	if (scheme_utf8_tmp1 == NULL) {
		g_debug ("%s: UTF-8 normalize error", __func__);
		goto skip;
	}
	scheme_utf8_tmp2 = g_utf8_casefold (scheme_utf8_tmp1, -1);
	if (scheme_utf8_tmp2 == NULL) {
		g_debug ("%s: UTF-8 casefold error", __func__);
		goto skip;
	}
	g_free (scheme_utf8_tmp1);
	scheme_utf8_tmp1 = g_utf8_casefold ("https\0", -1); /* TODO improve! */
	if (g_utf8_collate (scheme_utf8_tmp1, scheme_utf8_tmp2) == 0)
		is_ssl = TRUE;
 skip:
	if (scheme_utf8_tmp2 != NULL)
		g_free (scheme_utf8_tmp2);
	if (scheme_utf8_tmp1 != NULL)
		g_free (scheme_utf8_tmp1);
	if (scheme != NULL)
		g_free (scheme);
	return is_ssl;
}

/*----------------------------------------------------------------------------*/
