/**
 * @file sipe-webticket.c
 *
 * pidgin-sipe
 *
 * Copyright (C) 2011-2016 SIPE Project <http://sipe.sourceforge.net/>
 *
 *
 * This program 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.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *
 * Specification references:
 *
 *   - [MS-OCAUTHWS]: http://msdn.microsoft.com/en-us/library/ff595592.aspx
 *   - MS Tech-Ed Europe 2010 "UNC310: Microsoft Lync 2010 Technology Explained"
 *     http://ecn.channel9.msdn.com/o9/te/Europe/2010/pptx/unc310.pptx
 */

#include <string.h>
#include <time.h>

#include <glib.h>

#include "sipe-common.h"
#include "sipe-backend.h"
#include "sipe-core.h"
#include "sipe-core-private.h"
#include "sipe-digest.h"
#include "sipe-svc.h"
#include "sipe-tls.h"
#include "sipe-webticket.h"
#include "sipe-utils.h"
#include "sipe-xml.h"

struct webticket_queued_data {
	sipe_webticket_callback *callback;
	gpointer callback_data;
};

struct webticket_callback_data {
	gchar *service_uri;
	const gchar *service_port;
	gchar *service_auth_uri;

	gchar *webticket_negotiate_uri;
	gchar *webticket_fedbearer_uri;

	gboolean tried_fedbearer;
	gboolean requires_signing;
	enum {
		TOKEN_STATE_NONE       = 0,
		TOKEN_STATE_SERVICE,
		TOKEN_STATE_FEDERATION,
		TOKEN_STATE_FED_BEARER,
	} token_state;

	struct sipe_tls_random entropy;

	sipe_webticket_callback *callback;
	gpointer callback_data;

	struct sipe_svc_session *session;

	GSList *queued;
};

struct webticket_token {
	gchar *auth_uri;
	gchar *token;
	time_t expires;
};

struct sipe_webticket {
	GHashTable *cache;
	GHashTable *pending;

	gchar *webticket_adfs_uri;
	gchar *adfs_token;
	time_t adfs_token_expires;

	gboolean retrieved_realminfo;
	gboolean shutting_down;
};

void sipe_webticket_free(struct sipe_core_private *sipe_private)
{
	struct sipe_webticket *webticket = sipe_private->webticket;
	if (!webticket)
		return;

	/* Web Ticket stack is shutting down: reject all new requests */
	webticket->shutting_down = TRUE;

	g_free(webticket->webticket_adfs_uri);
	g_free(webticket->adfs_token);
	if (webticket->pending)
		g_hash_table_destroy(webticket->pending);
	if (webticket->cache)
		g_hash_table_destroy(webticket->cache);
	g_free(webticket);
	sipe_private->webticket = NULL;
}

static void free_token(gpointer data)
{
	struct webticket_token *wt = data;
	g_free(wt->auth_uri);
	g_free(wt->token);
	g_free(wt);
}

static void sipe_webticket_init(struct sipe_core_private *sipe_private)
{
	struct sipe_webticket *webticket;

	if (sipe_private->webticket)
		return;

	sipe_private->webticket = webticket = g_new0(struct sipe_webticket, 1);

	webticket->cache   = g_hash_table_new_full(g_str_hash,
						   g_str_equal,
						   g_free,
						   free_token);
	webticket->pending = g_hash_table_new(g_str_hash,
					      g_str_equal);
}

/* takes ownership of "token" */
static void cache_token(struct sipe_core_private *sipe_private,
			const gchar *service_uri,
			const gchar *auth_uri,
			gchar *token,
			time_t expires)
{
	struct webticket_token *wt = g_new0(struct webticket_token, 1);
	wt->auth_uri = g_strdup(auth_uri);
	wt->token    = token;
	wt->expires  = expires;
	g_hash_table_insert(sipe_private->webticket->cache,
			    g_strdup(service_uri),
			    wt);
}

static const struct webticket_token *cache_hit(struct sipe_core_private *sipe_private,
					       const gchar *service_uri)
{
	const struct webticket_token *wt;

	/* make sure a cached Web Ticket is still valid for 60 seconds */
	wt = g_hash_table_lookup(sipe_private->webticket->cache,
				 service_uri);
	if (wt && (wt->expires < time(NULL) + 60)) {
		SIPE_DEBUG_INFO("cache_hit: cached token for URI %s has expired",
				service_uri);
		wt = NULL;
	}

	return(wt);
}

/* frees just the main request data, when this is called "queued" is cleared */
static void callback_data_free(struct webticket_callback_data *wcd)
{
	if (wcd) {
		sipe_tls_free_random(&wcd->entropy);
		g_free(wcd->webticket_negotiate_uri);
		g_free(wcd->webticket_fedbearer_uri);
		g_free(wcd->service_auth_uri);
		g_free(wcd->service_uri);
		g_free(wcd);
	}
}

static void queue_request(struct webticket_callback_data *wcd,
			  sipe_webticket_callback *callback,
			  gpointer callback_data)
{
	struct webticket_queued_data *wqd = g_new0(struct webticket_queued_data, 1);

	wqd->callback      = callback;
	wqd->callback_data = callback_data;

	wcd->queued = g_slist_prepend(wcd->queued, wqd);
}

static void callback_execute(struct sipe_core_private *sipe_private,
			     struct webticket_callback_data *wcd,
			     const gchar *auth_uri,
			     const gchar *wsse_security,
			     const gchar *failure_msg)
{
	GSList *entry = wcd->queued;

	/* complete main request */
	wcd->callback(sipe_private,
		      wcd->service_uri,
		      auth_uri,
		      wsse_security,
		      failure_msg,
		      wcd->callback_data);

	/* complete queued requests */
	while (entry) {
		struct webticket_queued_data *wqd = entry->data;

		SIPE_DEBUG_INFO("callback_execute: completing queue request URI %s (Auth URI %s)",
				wcd->service_uri, auth_uri);
		wqd->callback(sipe_private,
			      wcd->service_uri,
			      auth_uri,
			      wsse_security,
			      failure_msg,
			      wqd->callback_data);

		g_free(wqd);
		entry = entry->next;
	}
	g_slist_free(wcd->queued);

	/* drop request from pending hash */
	g_hash_table_remove(sipe_private->webticket->pending,
			    wcd->service_uri);
}

static gchar *extract_raw_xml_attribute(const gchar *xml,
					const gchar *name)
{
	gchar *attr_start = g_strdup_printf("%s=\"", name);
	gchar *data       = NULL;
	const gchar *start = strstr(xml, attr_start);

	if (start) {
		const gchar *value = start + strlen(attr_start);
		const gchar *end = strchr(value, '"');
		if (end) {
			data = g_strndup(value, end - value);
		}
	}

	g_free(attr_start);
	return(data);
}

static gchar *generate_timestamp(const gchar *raw)
{
	gchar *lifetime = sipe_xml_extract_raw(raw, "Lifetime", FALSE);
	gchar *timestamp = NULL;
	if (lifetime)
		timestamp = g_strdup_printf("<wsu:Timestamp xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\" wsu:Id=\"timestamp\">%s</wsu:Timestamp>",
					    lifetime);
	g_free(lifetime);
	return(timestamp);
}

static gchar *generate_keydata(const gchar *raw)
{
	return(sipe_xml_extract_raw(raw, "Assertion", TRUE));
}

static gchar *generate_expires(const gchar *timestamp)
{
	return(sipe_xml_extract_raw(timestamp, "Expires", FALSE));
}

static gchar *generate_fedbearer_wsse(const gchar *raw)
{
	gchar *timestamp = generate_timestamp(raw);
	gchar *keydata   = sipe_xml_extract_raw(raw, "EncryptedData", TRUE);
	gchar *wsse_security = NULL;

	if (timestamp && keydata) {
		SIPE_DEBUG_INFO_NOFORMAT("generate_fedbearer_wsse: found timestamp & keydata");
		wsse_security = g_strconcat(timestamp, keydata, NULL);
	}

	g_free(keydata);
	g_free(timestamp);
	return(wsse_security);
}

static void generate_federation_wsse(struct sipe_webticket *webticket,
				     const gchar *raw)
{
	gchar *timestamp = generate_timestamp(raw);
	gchar *keydata   = generate_keydata(raw);

	/* clear old ADFS token */
	g_free(webticket->adfs_token);
	webticket->adfs_token = NULL;

	if (timestamp && keydata) {
		gchar *expires_string = generate_expires(timestamp);

		if (expires_string) {

			SIPE_DEBUG_INFO("generate_federation_wsse: found timestamp & keydata, expires %s",
					expires_string);

			/* cache ADFS token */
			webticket->adfs_token         = g_strconcat(timestamp,
								    keydata,
								    NULL);
			webticket->adfs_token_expires = sipe_utils_str_to_time(expires_string);
			g_free(expires_string);
		}
	}

	g_free(keydata);
	g_free(timestamp);
}

static gchar *generate_sha1_proof_wsse(const gchar *raw,
				       struct sipe_tls_random *entropy,
				       time_t *expires)
{
	gchar *timestamp = generate_timestamp(raw);
	gchar *keydata   = generate_keydata(raw);
	gchar *wsse_security = NULL;

	if (timestamp && keydata) {
		gchar *expires_string = generate_expires(timestamp);

		if (entropy) {
			gchar *assertionID = extract_raw_xml_attribute(keydata,
								       "AssertionID");

			/*
			 * WS-Trust 1.3
			 *
			 * http://docs.oasis-open.org/ws-sx/ws-trust/200512/CK/PSHA1:
			 *
			 * "The key is computed using P_SHA1() from the TLS sepcification to generate
			 *  a bit stream using entropy from both sides. The exact form is:
			 *
			 *       key = P_SHA1(Entropy_REQ, Entropy_RES)"
			 */
			gchar *entropy_res_base64 = sipe_xml_extract_raw(raw, "BinarySecret", FALSE);
			gsize entropy_res_length;
			guchar *entropy_response = g_base64_decode(entropy_res_base64,
								   &entropy_res_length);
			guchar *key = sipe_tls_p_sha1(entropy->buffer,
						      entropy->length,
						      entropy_response,
						      entropy_res_length,
						      entropy->length);
			g_free(entropy_response);
			g_free(entropy_res_base64);

			SIPE_DEBUG_INFO_NOFORMAT("generate_sha1_proof_wsse: found timestamp & keydata");

			if (assertionID && key) {
				/* same as SIPE_DIGEST_HMAC_SHA1_LENGTH */
				guchar digest[SIPE_DIGEST_SHA1_LENGTH];
				gchar *base64;
				gchar *signed_info;
				gchar *canon;

				SIPE_DEBUG_INFO_NOFORMAT("generate_sha1_proof_wsse: found assertionID and successfully computed the key");

				/* Digest over reference element (#timestamp -> wsu:Timestamp) */
				sipe_digest_sha1((guchar *) timestamp,
						 strlen(timestamp),
						 digest);
				base64 = g_base64_encode(digest,
							 SIPE_DIGEST_SHA1_LENGTH);

				/* XML-Sig: SignedInfo for reference element */
				signed_info = g_strdup_printf("<SignedInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">"
							      "<CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>"
							      "<SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#hmac-sha1\"/>"
							      "<Reference URI=\"#timestamp\">"
							      "<Transforms>"
							      "<Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>"
							      "</Transforms>"
							      "<DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
							      "<DigestValue>%s</DigestValue>"
							      "</Reference>"
							      "</SignedInfo>",
							      base64);
				g_free(base64);

				/* XML-Sig: SignedInfo in canonical form */
				canon = sipe_xml_exc_c14n(signed_info);
				g_free(signed_info);

				if (canon) {
					gchar *signature;

					/* calculate signature */
					sipe_digest_hmac_sha1(key, entropy->length,
							      (guchar *)canon,
							      strlen(canon),
							      digest);
					base64 = g_base64_encode(digest,
								 SIPE_DIGEST_HMAC_SHA1_LENGTH);

					/* XML-Sig: Signature from SignedInfo + Key */
					signature = g_strdup_printf("<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\">"
								    " %s"
								    " <SignatureValue>%s</SignatureValue>"
								    " <KeyInfo>"
								    "  <wsse:SecurityTokenReference wsse:TokenType=\"http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1\">"
								    "   <wsse:KeyIdentifier ValueType=\"http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID\">%s</wsse:KeyIdentifier>"
								    "  </wsse:SecurityTokenReference>"
								    " </KeyInfo>"
								    "</Signature>",
								    canon,
								    base64,
								    assertionID);
					g_free(base64);
					g_free(canon);

					wsse_security = g_strconcat(timestamp,
								    keydata,
								    signature,
								    NULL);
					g_free(signature);
				}

			}

			g_free(key);
			g_free(assertionID);
		} else {
			/* token doesn't require signature */
			SIPE_DEBUG_INFO_NOFORMAT("generate_sha1_proof_wsse: found timestamp & keydata, no signing required");
			wsse_security = g_strconcat(timestamp,
						    keydata,
						    NULL);
		}

		*expires = 0;
		if (expires_string) {
			*expires = sipe_utils_str_to_time(expires_string);
			g_free(expires_string);
		}
	}

	g_free(keydata);
	g_free(timestamp);
	return(wsse_security);
}

static gboolean federated_authentication(struct sipe_core_private *sipe_private,
					 struct webticket_callback_data *wcd);
static gboolean initiate_fedbearer(struct sipe_core_private *sipe_private,
				   struct webticket_callback_data *wcd);
static void webticket_token(struct sipe_core_private *sipe_private,
			    const gchar *uri,
			    const gchar *raw,
			    sipe_xml *soap_body,
			    gpointer callback_data)
{
	struct webticket_callback_data *wcd = callback_data;
	gboolean failed = TRUE;

	if (soap_body) {
		switch (wcd->token_state) {
		case TOKEN_STATE_NONE:
			SIPE_DEBUG_INFO_NOFORMAT("webticket_token: ILLEGAL STATE - should not happen...");
			break;

		case TOKEN_STATE_SERVICE: {
			/* WebTicket for Web Service */
			time_t expires;
			gchar *wsse_security = generate_sha1_proof_wsse(raw,
									wcd->requires_signing ? &wcd->entropy : NULL,
									&expires);

			if (wsse_security) {
				/* cache takes ownership of wsse_security */
				cache_token(sipe_private,
					    wcd->service_uri,
					    wcd->service_auth_uri,
					    wsse_security,
					    expires);
				callback_execute(sipe_private,
						 wcd,
						 wcd->service_auth_uri,
						 wsse_security,
						 NULL);
				failed = FALSE;
			}
			break;
		}

		case TOKEN_STATE_FEDERATION:
			/* WebTicket from ADFS for federated authentication */
			generate_federation_wsse(sipe_private->webticket,
						 raw);

			if (sipe_private->webticket->adfs_token) {

				SIPE_DEBUG_INFO("webticket_token: received valid SOAP message from ADFS %s",
						uri);

				if (federated_authentication(sipe_private,
							     wcd)) {
					/* callback data passed down the line */
					wcd = NULL;
				}
			}
			break;

		case TOKEN_STATE_FED_BEARER: {
			/* WebTicket for federated authentication */
			gchar *wsse_security = generate_fedbearer_wsse(raw);

			if (wsse_security) {

				SIPE_DEBUG_INFO("webticket_token: received valid SOAP message from service %s",
						uri);

				if (sipe_svc_webticket(sipe_private,
						       wcd->session,
						       wcd->webticket_fedbearer_uri,
						       wsse_security,
						       wcd->service_auth_uri,
						       &wcd->entropy,
						       webticket_token,
						       wcd)) {
					wcd->token_state = TOKEN_STATE_SERVICE;

					/* callback data passed down the line */
					wcd = NULL;
				}
				g_free(wsse_security);
			}
			break;
		}

		/* end of: switch (wcd->token_state) { */
		}

	} else if (uri) {
		/* Retry with federated authentication? */
		if (wcd->webticket_fedbearer_uri) {

			/* Authentication against ADFS failed? */
			if (wcd->token_state == TOKEN_STATE_FEDERATION) {
				struct sipe_webticket *webticket = sipe_private->webticket;

				SIPE_LOG_WARNING_NOFORMAT("webticket_token: ADFS authentication failed - assuming Multi-Factor Authentication (MFA)");

				/* forget ADFS URI */
				g_free(webticket->webticket_adfs_uri);
				webticket->webticket_adfs_uri = NULL;
			}

			if (!wcd->tried_fedbearer) {
				SIPE_DEBUG_INFO("webticket_token: anonymous authentication to service %s failed, retrying with federated authentication",
						uri);

				if (initiate_fedbearer(sipe_private, wcd)) {
					/* callback data passed down the line */
					wcd = NULL;
				}
			}
		}
	}

	if (wcd) {
		if (failed) {
			gchar *failure_msg = NULL;

			if (soap_body) {
				failure_msg = sipe_xml_data(sipe_xml_child(soap_body,
									   "Body/Fault/Detail/error/internalerror/text"));
				/* XML data can end in &#x000D;&#x000A; */
				g_strstrip(failure_msg);
			}

			callback_execute(sipe_private,
					 wcd,
					 uri,
					 NULL,
					 failure_msg);
			g_free(failure_msg);
		}
		callback_data_free(wcd);
	}
}

static gboolean federated_authentication(struct sipe_core_private *sipe_private,
					 struct webticket_callback_data *wcd)
{
	gboolean success;

	if ((success = sipe_svc_webticket_lmc_federated(sipe_private,
							wcd->session,
							sipe_private->webticket->adfs_token,
							wcd->webticket_fedbearer_uri,
							webticket_token,
							wcd)))
		wcd->token_state = TOKEN_STATE_FED_BEARER;

	/* If TRUE then callback data has been passed down the line */
	return(success);
}

static gboolean fedbearer_authentication(struct sipe_core_private *sipe_private,
					 struct webticket_callback_data *wcd)
{
	struct sipe_webticket *webticket = sipe_private->webticket;
	gboolean success;

	/* make sure a cached ADFS token is still valid for 60 seconds */
	if (webticket->adfs_token &&
	    (webticket->adfs_token_expires >= time(NULL) + 60)) {

		SIPE_DEBUG_INFO_NOFORMAT("fedbearer_authentication: reusing cached ADFS token");
		success = federated_authentication(sipe_private, wcd);

	} else if (webticket->webticket_adfs_uri) {
		if ((success = sipe_svc_webticket_adfs(sipe_private,
						       wcd->session,
						       webticket->webticket_adfs_uri,
						       webticket_token,
						       wcd)))
			wcd->token_state = TOKEN_STATE_FEDERATION;
	} else {
		if ((success = sipe_svc_webticket_lmc(sipe_private,
						      wcd->session,
						      wcd->webticket_fedbearer_uri,
						      webticket_token,
						      wcd)))
			wcd->token_state = TOKEN_STATE_FED_BEARER;
	}

	/* If TRUE then callback data has been passed down the line */
	return(success);
}

static void realminfo(struct sipe_core_private *sipe_private,
		      const gchar *uri,
		      SIPE_UNUSED_PARAMETER const gchar *raw,
		      sipe_xml *realminfo,
		      gpointer callback_data)
{
	struct sipe_webticket *webticket = sipe_private->webticket;
	struct webticket_callback_data *wcd = callback_data;

	/* Only try retrieving of RealmInfo once */
	webticket->retrieved_realminfo = TRUE;

	/*
	 * We must specifically check for abort, because
	 * realminfo == NULL is a valid response
	 */
	if (uri) {
		if (realminfo) {
			/* detect ADFS setup. See also:
			 *
			 *   http://en.wikipedia.org/wiki/Active_Directory_Federation_Services
			 *
			 * NOTE: this is based on observed behaviour.
			 *       It is unkown if this is documented somewhere...
			 */
			SIPE_DEBUG_INFO("realminfo: data for user %s retrieved successfully",
					sipe_private->username);

			webticket->webticket_adfs_uri = sipe_xml_data(sipe_xml_child(realminfo,
										     "STSAuthURL"));
		}

		if (webticket->webticket_adfs_uri) {
			SIPE_LOG_INFO_NOFORMAT("realminfo: ADFS setup detected");
			SIPE_DEBUG_INFO("realminfo: ADFS URI: %s",
					webticket->webticket_adfs_uri);
		} else
			SIPE_DEBUG_INFO_NOFORMAT("realminfo: no RealmInfo found or no ADFS setup detected - try direct login");

		if (fedbearer_authentication(sipe_private, wcd)) {
			/* callback data passed down the line */
			wcd = NULL;
		}
	}

	if (wcd) {
		callback_execute(sipe_private,
				 wcd,
				 uri,
				 NULL,
				 NULL);
		callback_data_free(wcd);
	}
}

static gboolean initiate_fedbearer(struct sipe_core_private *sipe_private,
				   struct webticket_callback_data *wcd)
{
	gboolean success;

	if (sipe_private->webticket->retrieved_realminfo) {
		/* skip retrieval and go to authentication */
		wcd->tried_fedbearer = TRUE;
		success = fedbearer_authentication(sipe_private, wcd);
	} else {
		success = sipe_svc_realminfo(sipe_private,
					     wcd->session,
					     realminfo,
					     wcd);
	}

	return(success);
}

static void webticket_metadata(struct sipe_core_private *sipe_private,
			       const gchar *uri,
			       SIPE_UNUSED_PARAMETER const gchar *raw,
			       sipe_xml *metadata,
			       gpointer callback_data)
{
	struct webticket_callback_data *wcd = callback_data;

	if (metadata) {
		const sipe_xml *node;

		SIPE_DEBUG_INFO("webticket_metadata: metadata for service %s retrieved successfully",
				uri);

		/* Authentication ports accepted by WebTicket Service */
		for (node = sipe_xml_child(metadata, "service/port");
		     node;
		     node = sipe_xml_twin(node)) {
			const gchar *auth_uri = sipe_xml_attribute(sipe_xml_child(node,
										  "address"),
								   "location");

			if (auth_uri) {
				if (sipe_strcase_equal(sipe_xml_attribute(node, "name"),
						       "WebTicketServiceWinNegotiate")) {
					SIPE_DEBUG_INFO("webticket_metadata: WebTicket Windows Negotiate Auth URI %s", auth_uri);
					g_free(wcd->webticket_negotiate_uri);
					wcd->webticket_negotiate_uri = g_strdup(auth_uri);
				} else if (sipe_strcase_equal(sipe_xml_attribute(node, "name"),
							      "WsFedBearer")) {
					SIPE_DEBUG_INFO("webticket_metadata: WebTicket FedBearer Auth URI %s", auth_uri);
					g_free(wcd->webticket_fedbearer_uri);
					wcd->webticket_fedbearer_uri = g_strdup(auth_uri);
				}
			}
		}

		if (wcd->webticket_negotiate_uri || wcd->webticket_fedbearer_uri) {
			gboolean success;

			/* Entropy: 256 random bits */
			if (!wcd->entropy.buffer)
				sipe_tls_fill_random(&wcd->entropy, 256);

			if (wcd->webticket_negotiate_uri) {
				/* Try Negotiate authentication first */

				success = sipe_svc_webticket(sipe_private,
							     wcd->session,
							     wcd->webticket_negotiate_uri,
							     NULL,
							     wcd->service_auth_uri,
							     &wcd->entropy,
							     webticket_token,
							     wcd);
				wcd->token_state = TOKEN_STATE_SERVICE;
			} else {
				success = initiate_fedbearer(sipe_private,
							     wcd);
			}

			if (success) {
				/* callback data passed down the line */
				wcd = NULL;
			}
		}
	}

	if (wcd) {
		callback_execute(sipe_private,
				 wcd,
				 uri,
				 NULL,
				 NULL);
		callback_data_free(wcd);
	}
}

static void service_metadata(struct sipe_core_private *sipe_private,
			     const gchar *uri,
			     SIPE_UNUSED_PARAMETER const gchar *raw,
			     sipe_xml *metadata,
			     gpointer callback_data)
{
	struct webticket_callback_data *wcd = callback_data;

	if (metadata) {
		const sipe_xml *node;
		gchar *policy = g_strdup_printf("%s_policy", wcd->service_port);
		gchar *ticket_uri = NULL;

		SIPE_DEBUG_INFO("service_metadata: metadata for service %s retrieved successfully",
				uri);

		/* WebTicket policies accepted by Web Service */
		for (node = sipe_xml_child(metadata, "Policy");
		     node;
		     node = sipe_xml_twin(node)) {
			if (sipe_strcase_equal(sipe_xml_attribute(node, "Id"),
					       policy)) {

				SIPE_DEBUG_INFO_NOFORMAT("service_metadata: WebTicket policy found");

				ticket_uri = sipe_xml_data(sipe_xml_child(node,
									  "ExactlyOne/All/EndorsingSupportingTokens/Policy/IssuedToken/Issuer/Address"));
				if (ticket_uri) {
					/* this token type requires signing */
					wcd->requires_signing = TRUE;
				} else {
					/* try alternative token type */
					ticket_uri = sipe_xml_data(sipe_xml_child(node,
										  "ExactlyOne/All/SignedSupportingTokens/Policy/IssuedToken/Issuer/Address"));
				}
				if (ticket_uri) {
					SIPE_DEBUG_INFO("service_metadata: WebTicket URI %s", ticket_uri);
				}
				break;
			}
		}
		g_free(policy);

		if (ticket_uri) {

			/* Authentication ports accepted by Web Service */
			for (node = sipe_xml_child(metadata, "service/port");
			     node;
			     node = sipe_xml_twin(node)) {
				if (sipe_strcase_equal(sipe_xml_attribute(node, "name"),
						       wcd->service_port)) {
					const gchar *auth_uri;

					SIPE_DEBUG_INFO_NOFORMAT("service_metadata: authentication port found");

					auth_uri = sipe_xml_attribute(sipe_xml_child(node,
										     "address"),
								      "location");
					if (auth_uri) {
						SIPE_DEBUG_INFO("service_metadata: Auth URI %s", auth_uri);

						if (sipe_svc_metadata(sipe_private,
								      wcd->session,
								      ticket_uri,
								      webticket_metadata,
								      wcd)) {
							/* Remember for later */
							wcd->service_auth_uri = g_strdup(auth_uri);

							/* callback data passed down the line */
							wcd = NULL;
						}
					}
					break;
				}
			}
			g_free(ticket_uri);
		}
	}

	if (wcd) {
		callback_execute(sipe_private,
				 wcd,
				 uri,
				 NULL,
				 NULL);
		callback_data_free(wcd);
	}
}

static gboolean webticket_request(struct sipe_core_private *sipe_private,
				  struct sipe_svc_session *session,
				  const gchar *base_uri,
				  const gchar *auth_uri,
				  const gchar *port_name,
				  sipe_webticket_callback *callback,
				  gpointer callback_data)
{
	struct sipe_webticket *webticket;
	gboolean ret = FALSE;

	sipe_webticket_init(sipe_private);
	webticket = sipe_private->webticket;

	if (webticket->shutting_down) {
		SIPE_DEBUG_ERROR("webticket_request: new Web Ticket request during shutdown: THIS SHOULD NOT HAPPEN! Debugging information:\n"
				 "Base URI:  %s\n"
				 "Port Name: %s\n",
				 base_uri,
				 port_name);

	} else {
		const struct webticket_token *wt = cache_hit(sipe_private, base_uri);

		/* cache hit for this URI? */
		if (wt) {
			SIPE_DEBUG_INFO("webticket_request: using cached token for URI %s (Auth URI %s)",
					base_uri, wt->auth_uri);
			callback(sipe_private,
				 base_uri,
				 wt->auth_uri,
				 wt->token,
				 NULL,
				 callback_data);
			ret = TRUE;
		} else {
			GHashTable *pending = webticket->pending;
			struct webticket_callback_data *wcd = g_hash_table_lookup(pending,
										  base_uri);

			/* is there already a pending request for this URI? */
			if (wcd) {
				SIPE_DEBUG_INFO("webticket_request: pending request found for URI %s - queueing",
						base_uri);
				queue_request(wcd, callback, callback_data);
				ret = TRUE;
			} else {
				wcd = g_new0(struct webticket_callback_data, 1);

				ret = sipe_svc_metadata(sipe_private,
							session,
							base_uri,
							port_name ? service_metadata : webticket_metadata,
							wcd);

				if (ret) {
					wcd->service_uri      = g_strdup(base_uri);
					wcd->service_port     = port_name;
					wcd->service_auth_uri = g_strdup(auth_uri);
					wcd->callback         = callback;
					wcd->callback_data    = callback_data;
					wcd->session          = session;
					wcd->token_state      = TOKEN_STATE_NONE;
					g_hash_table_insert(pending,
							    wcd->service_uri, /* borrowed */
							    wcd);             /* borrowed */
				} else {
					g_free(wcd);
				}
			}
		}
	}

	return(ret);
}

gboolean sipe_webticket_request_with_port(struct sipe_core_private *sipe_private,
					  struct sipe_svc_session *session,
					  const gchar *base_uri,
					  const gchar *port_name,
					  sipe_webticket_callback *callback,
					  gpointer callback_data)
{
	return(webticket_request(sipe_private,
				 session,
				 base_uri,
				 NULL, /* Auth URI is determined via port_name */
				 port_name,
				 callback,
				 callback_data));
}

gboolean sipe_webticket_request_with_auth(struct sipe_core_private *sipe_private,
					  struct sipe_svc_session *session,
					  const gchar *base_uri,
					  const gchar *auth_uri,
					  sipe_webticket_callback *callback,
					  gpointer callback_data)
{
	return(webticket_request(sipe_private,
				 session,
				 base_uri,
				 auth_uri,
				 NULL,
				 callback,
				 callback_data));
}

/*
  Local Variables:
  mode: c
  c-file-style: "bsd"
  indent-tabs-mode: t
  tab-width: 8
  End:
*/
