/* $Id$
 *
 * Lasso - A free implementation of the Liberty Alliance specifications.
 *
 * Copyright (C) 2004-2007 Entr'ouvert
 * http://lasso.entrouvert.org
 *
 * Authors: See AUTHORS file in top-level directory.
 *
 * 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
 */

#include "../xml/private.h"
#include <xmlsec/base64.h>

#include "providerprivate.h"
#include "../id-ff/providerprivate.h"
#include "../utils.h"
#include "./provider.h"

const char *profile_names[] = {
	"", /* No fedterm in SAML 2.0 */
	"NameIDMappingService",
	"", /* No rni in SAML 2.0 */
	"SingleLogoutService",
	"SingleSignOnService",
	"ArtifactResolutionService",
	"ManageNameIDService",
	"AssertionIDRequestService",
	NULL
};

static void add_assertion_consumer_url_to_list(gchar *key, G_GNUC_UNUSED gpointer value, GList **list);

static void
load_descriptor(xmlNode *xmlnode, GHashTable *descriptor, LassoProvider *provider)
{
	char *descriptor_attrs[] = {"AuthnRequestsSigned", "WantAuthnRequestsSigned", NULL};
	int i;
	xmlNode *t;
	GList *elements;
	char *name, *binding, *response_name;
	xmlChar *value;
	xmlChar *response_value;
	xmlChar *use;

	t = xmlnode->children;
	while (t) {
		if (t->type != XML_ELEMENT_NODE) {
			t = t->next;
			continue;
		}
		if (strcmp((char*)t->name, "KeyDescriptor") == 0) {
			use = xmlGetProp(t, (xmlChar*)"use");
			if (use && strcmp((char*)use, "signing") == 0) {
				provider->private_data->signing_key_descriptor = xmlCopyNode(t, 1);
			}
			if (use && strcmp((char*)use, "encryption") == 0) {
				provider->private_data->encryption_key_descriptor =
					xmlCopyNode(t, 1);
			}
			if (use) {
				xmlFree(use);
			}
			t = t->next;
			continue;
		}
		binding = (char*)xmlGetProp(t, (xmlChar*)"Binding");
		if (binding) {
			/* Endpoint type */
			char *binding_s = NULL;
			if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_SOAP) == 0) {
				binding_s = "SOAP";
			} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_REDIRECT) == 0) {
				binding_s = "HTTP-Redirect";
			} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_POST) == 0) {
				binding_s = "HTTP-POST";
			} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_ARTIFACT) == 0) {
				binding_s = "HTTP-Artifact";
			} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_PAOS) == 0) {
				binding_s = "PAOS";
			} else {
				message(G_LOG_LEVEL_CRITICAL, "XXX: unknown binding: %s", binding);
				xmlFree(binding);
				t = t->next;
				continue;
			}
			value = xmlGetProp(t, (xmlChar*)"Location");
			if (value == NULL) {
				message(G_LOG_LEVEL_CRITICAL, "XXX: missing location");
				xmlFree(binding);
				t = t->next;
				continue;
			}
			if (strcmp((char*)t->name, "AssertionConsumerService") == 0) {
				char *index = (char*)xmlGetProp(t, (xmlChar*)"index");
				char *is_default = (char*)xmlGetProp(t, (xmlChar*)"isDefault");
				if (is_default && strcmp(is_default, "true") == 0) {
					provider->private_data->default_assertion_consumer =
						g_strdup(index);
				}
				name = g_strdup_printf("%s %s %s", t->name, binding_s, index);
				xmlFree(index);
				xmlFree(is_default);
			} else {
				name = g_strdup_printf("%s %s", t->name, binding_s);
			}
			xmlFree(binding);

			response_value = xmlGetProp(t, (xmlChar*)"ResponseLocation");
			if (response_value) {
				response_name = g_strdup_printf("%s ResponseLocation", name);
				elements = g_hash_table_lookup(descriptor, response_name);
				elements = g_list_append(elements, g_strdup((char*)response_value));
				g_hash_table_insert(descriptor, response_name, elements);
				xmlFree(response_value);
			}
		} else {
			name = g_strdup((char*)t->name);
			value = xmlNodeGetContent(t);
		}
		elements = g_hash_table_lookup(descriptor, name);
		elements = g_list_append(elements, g_strdup((char*)value));
		xmlFree(value);
		g_hash_table_insert(descriptor, name, elements);
		t = t->next;
	}

	for (i = 0; descriptor_attrs[i]; i++) {
		value = xmlGetProp(xmlnode, (xmlChar*)descriptor_attrs[i]);
		if (value == NULL) {
			continue;
		}
		name = g_strdup(descriptor_attrs[i]);
		elements = g_hash_table_lookup(descriptor, name);
		elements = g_list_append(elements, g_strdup((char*)value));
		xmlFree(value);
		g_hash_table_insert(descriptor, name, elements);
	}

}

gboolean
lasso_saml20_provider_load_metadata(LassoProvider *provider, xmlNode *root_node)
{
	xmlNode *node, *descriptor_node;
	xmlChar *providerID;

	if (strcmp((char*)root_node->name, "EntityDescriptor") == 0) {
		node = root_node;
	} else if (strcmp((char*)root_node->name, "EntitiesDescriptor") == 0) {
		/* XXX: take the first entity; would it be possible to have an
		 * optional argument to take another one ? */
		node = root_node->children;
		while (node && strcmp((char*)node->name, "EntityDescriptor") != 0) {
			node = node->next;
		}
		if (node == NULL) {
			message (G_LOG_LEVEL_CRITICAL, "lasso_saml20_provider_load_metadata_from_doc: no EntityDescriptor");
			return FALSE;
		}
	} else {
		message (G_LOG_LEVEL_CRITICAL, "lasso_saml20_provider_load_metadata_from_doc: no EntityDescriptor");
		/* what? */
		return FALSE;
	}

	providerID = xmlGetProp(node, (xmlChar*)"entityID");
	lasso_assign_string(provider->ProviderID, (char*)providerID);
	lasso_release_xml_string(providerID);
	if (provider->ProviderID == NULL) {
		message (G_LOG_LEVEL_CRITICAL, "lasso_saml20_provider_load_metadata_from_doc: no entityID attribute");
		return FALSE;
	}

	for (descriptor_node = node->children; descriptor_node != NULL;
			descriptor_node = descriptor_node->next) {
		if (descriptor_node->type != XML_ELEMENT_NODE)
			continue;

		if (strcmp((char*)descriptor_node->name, "IDPSSODescriptor") == 0) {
			load_descriptor(descriptor_node,
					provider->private_data->IDPDescriptor, provider);
			provider->role = LASSO_PROVIDER_ROLE_IDP;
			continue;
		}

		if (strcmp((char*)descriptor_node->name, "SPSSODescriptor") == 0) {
			load_descriptor(descriptor_node,
					provider->private_data->SPDescriptor, provider);
			provider->role = LASSO_PROVIDER_ROLE_SP;
			continue;
		}

		if (strcmp((char*)descriptor_node->name, "Organization") == 0) {
			provider->private_data->organization = xmlCopyNode(
					descriptor_node, 1);
			continue;
		}
	}



	return TRUE;
}

LassoHttpMethod
lasso_saml20_provider_get_first_http_method(LassoProvider *provider,
		const LassoProvider *remote_provider, LassoMdProtocolType protocol_type)
{
	LassoHttpMethod method = LASSO_HTTP_METHOD_NONE;
	int i;
	const char *possible_bindings[] = {
		"HTTP-Redirect", "HTTP-Post", "SOAP", "HTTP-Artifact", NULL
	};
	LassoHttpMethod method_bindings[] = {
		LASSO_HTTP_METHOD_SOAP, LASSO_HTTP_METHOD_REDIRECT, LASSO_HTTP_METHOD_POST
	};

	if (remote_provider->role == LASSO_PROVIDER_ROLE_SP)
		provider->role = LASSO_PROVIDER_ROLE_IDP;
	if (remote_provider->role == LASSO_PROVIDER_ROLE_IDP)
		provider->role = LASSO_PROVIDER_ROLE_SP;

	for (i=0; possible_bindings[i] && method == LASSO_HTTP_METHOD_NONE; i++) {
		char *s;
		const GList *l1, *l2;

		s = g_strdup_printf("%s %s", profile_names[protocol_type], possible_bindings[i]);
		l1 = lasso_provider_get_metadata_list(provider, s);
		l2 = lasso_provider_get_metadata_list(remote_provider, s);
		if (l1 && l2) {
			method = method_bindings[i];
		}
	}

	return method;
}

gboolean
lasso_saml20_provider_check_assertion_consumer_service_url(LassoProvider *provider, const gchar *url, const gchar *binding)
{
	GHashTable *descriptor;
	GList *l = NULL, *r = NULL, *candidate = NULL;
	char *name;
	char *binding_s = NULL;
	int lname;

	descriptor = provider->private_data->SPDescriptor;
	if (descriptor == NULL || url == NULL || binding == NULL)
		return FALSE;

	if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_SOAP) == 0) {
		binding_s = "SOAP";
	} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_REDIRECT) == 0) {
		binding_s = "HTTP-Redirect";
	} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_POST) == 0) {
		binding_s = "HTTP-POST";
	} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_ARTIFACT) == 0) {
		binding_s = "HTTP-Artifact";
	} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_PAOS) == 0) {
		binding_s = "PAOS";
	}

	if (binding_s == NULL) {
		return FALSE;
	}

	g_hash_table_foreach(descriptor, (GHFunc)add_assertion_consumer_url_to_list, &r);

	name = g_strdup_printf("AssertionConsumerService %s ", binding_s);
	lname = strlen(name);
	for (l = r; l; l = g_list_next(l)) {
		char *b = l->data;
		if (strncmp(name, b, lname) == 0) {
			candidate = g_hash_table_lookup(descriptor, b);
			if (candidate && candidate->data && strcmp(candidate->data, url) == 0)
				break;
			else
				candidate = NULL;
		}
	}
	g_free(name);
	g_list_free(r);

	if (candidate)
		return TRUE;
	else
		return FALSE;
}

gchar*
lasso_saml20_provider_get_assertion_consumer_service_url(const LassoProvider *provider,
		int service_id)
{
	GHashTable *descriptor;
	GList *l = NULL;
	char *sid;
	char *name;
	const char *possible_bindings[] = {
		"HTTP-Artifact", "HTTP-Post", "HTTP-POST", "HTTP-Redirect", "SOAP", NULL
	};
	int i;

	if (service_id == -1) {
		sid = g_strdup(provider->private_data->default_assertion_consumer);
	} else {
		sid = g_strdup_printf("%d", service_id);
	}

	descriptor = provider->private_data->SPDescriptor;
	if (descriptor == NULL)
		return NULL;

	for (i=0; possible_bindings[i]; i++) {
		name = g_strdup_printf("AssertionConsumerService %s %s",
				possible_bindings[i], sid);
		l = g_hash_table_lookup(descriptor, name);
		g_free(name);
		if (l != NULL)
			break;
	}
	g_free(sid);
	if (l)
		return g_strdup(l->data);
	return NULL;
}

static void
add_assertion_consumer_url_to_list(gchar *key, G_GNUC_UNUSED gpointer value, GList **list)
{
	if (strncmp(key, "AssertionConsumerService", 24) == 0) {
		*list = g_list_append(*list, key);
	}
}


gchar*
lasso_saml20_provider_get_assertion_consumer_service_url_by_binding(const LassoProvider *provider,
		gchar *binding)
{
	GHashTable *descriptor;
	GList *l = NULL, *r = NULL;
	char *name;
	char *binding_s = NULL;
	int lname;

	descriptor = provider->private_data->SPDescriptor;
	if (descriptor == NULL)
		return NULL;

	if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_SOAP) == 0) {
		binding_s = "SOAP";
	} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_REDIRECT) == 0) {
		binding_s = "HTTP-Redirect";
	} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_POST) == 0) {
		binding_s = "HTTP-POST";
	} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_ARTIFACT) == 0) {
		binding_s = "HTTP-Artifact";
	} else if (strcmp(binding, LASSO_SAML2_METADATA_BINDING_PAOS) == 0) {
		binding_s = "PAOS";
	}

	if (binding_s == NULL) {
		return NULL;
	}

	g_hash_table_foreach(descriptor, (GHFunc)add_assertion_consumer_url_to_list, &r);

	name = g_strdup_printf("AssertionConsumerService %s ", binding_s);
	lname = strlen(name);
	for (l = r; l; l = g_list_next(l)) {
		char *b = l->data;
		if (strncmp(name, b, lname) == 0) {
			l = g_hash_table_lookup(descriptor, b);
			break;
		}
	}
	g_free(name);
	g_list_free(r);

	if (l) {
		return g_strdup(l->data);
	}

	return NULL;
}



gchar*
lasso_saml20_provider_get_assertion_consumer_service_binding(const LassoProvider *provider,
		int service_id)
{
	GHashTable *descriptor;
	GList *l = NULL;
	char *sid;
	char *name;
	char *binding = NULL;
	const char *possible_bindings[] = {
		"HTTP-Artifact", "HTTP-Post", "HTTP-POST", "HTTP-Redirect", "SOAP", NULL
	};
	int i;

	if (service_id == -1) {
		sid = g_strdup(provider->private_data->default_assertion_consumer);
	} else {
		sid = g_strdup_printf("%d", service_id);
	}

	descriptor = provider->private_data->SPDescriptor;
	if (descriptor == NULL)
		return NULL;

	for (i=0; possible_bindings[i]; i++) {
		name = g_strdup_printf("AssertionConsumerService %s %s",
				possible_bindings[i], sid);
		l = g_hash_table_lookup(descriptor, name);
		g_free(name);
		if (l != NULL) {
			binding = g_strdup(possible_bindings[i]);
			break;
		}
	}
	g_free(sid);
	return binding;
}

gboolean
lasso_saml20_provider_accept_http_method(LassoProvider *provider, const LassoProvider *remote_provider,
		LassoMdProtocolType protocol_type, LassoHttpMethod http_method,
		gboolean initiate_profile)
{
	LassoProviderRole initiating_role;
	char *protocol_profile;
	char *http_methods[] = {
		NULL,
		NULL,
		NULL,
		NULL,
		"HTTP-Post",
		"HTTP-Redirect",
		"SOAP",
		"HTTP-Artifact",
		"HTTP-Artifact",
		NULL
	};
	gboolean rc = FALSE;


	initiating_role = remote_provider->role;
	if (remote_provider->role == LASSO_PROVIDER_ROLE_SP) {
		provider->role = LASSO_PROVIDER_ROLE_IDP;
	}
	if (remote_provider->role == LASSO_PROVIDER_ROLE_IDP) {
		provider->role = LASSO_PROVIDER_ROLE_SP;
	}
	if (initiate_profile)
		initiating_role = provider->role;

	/* exclude bad input */
	if (http_method > (int)G_N_ELEMENTS(http_methods) || http_method < 0 ||  http_methods[http_method+1] == NULL) {
		return FALSE;
	}

	protocol_profile = g_strdup_printf("%s %s", profile_names[protocol_type],
			http_methods[http_method+1]);

	/* special hack for single sign on */
	if (protocol_type == LASSO_MD_PROTOCOL_TYPE_SINGLE_SIGN_ON) {
		/* no need to check for the response, it uses another canal
		 * (AssertionConsumingService) */
		rc = (lasso_provider_get_metadata_list(remote_provider, protocol_profile) != NULL);

	} else {
		if (lasso_provider_get_metadata_list(provider, protocol_profile) &&
				lasso_provider_get_metadata_list(remote_provider, protocol_profile)) {
			rc = TRUE;
		}
	}
	lasso_release_string(protocol_profile);
	return rc;
}

/**
 * lasso_provider_saml2_node_encrypt:
 * @provider: a #LassoProvider object
 * @lasso_node: a #LassoNode object
 *
 * Dump the node object to an XML fragment, then encrypt this fragment using encryption key of
 * @provider, then encapsulate the resulting encrypted content into a #LassoSaml2EncryptedElement.
 *
 * Return value: a newly created #LassoSaml2EncryptedElement if successfull, NULL otherwise.
 */
LassoSaml2EncryptedElement*
lasso_provider_saml2_node_encrypt(const LassoProvider *provider, LassoNode *lasso_node)
{
	LassoSaml2EncryptedElement *saml2_encrypted_element;

	g_return_val_if_fail(LASSO_IS_PROVIDER (provider), NULL);
	g_return_val_if_fail(LASSO_IS_NODE (lasso_node), NULL);

	saml2_encrypted_element = lasso_node_encrypt(lasso_node,
			lasso_provider_get_encryption_public_key(provider),
			lasso_provider_get_encryption_sym_key_type(provider),
			provider->ProviderID);

	return saml2_encrypted_element;
}
