/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
/*
 * soup-auth-domain-basic.c: HTTP Basic Authentication (server-side)
 *
 * Copyright (C) 2007 Novell, Inc.
 */

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

#include <string.h>

#include "soup-auth-domain-basic.h"
#include "soup-auth-domain-private.h"
#include "soup-message-headers-private.h"
#include "soup.h"

/**
 * SoupAuthDomainBasic:
 *
 * Server-side "Basic" authentication.
 *
 * #SoupAuthDomainBasic handles the server side of HTTP "Basic" (ie,
 * cleartext password) authentication.
 */

enum {
	PROP_0,

	PROP_AUTH_CALLBACK,
	PROP_AUTH_DATA,

	LAST_PROPERTY
};

static GParamSpec *properties[LAST_PROPERTY] = { NULL, };

struct _SoupAuthDomainBasic {
	SoupAuthDomain parent;
};

typedef struct {
	SoupAuthDomainBasicAuthCallback auth_callback;
	gpointer auth_data;
	GDestroyNotify auth_dnotify;
} SoupAuthDomainBasicPrivate;

G_DEFINE_FINAL_TYPE_WITH_PRIVATE (SoupAuthDomainBasic, soup_auth_domain_basic, SOUP_TYPE_AUTH_DOMAIN)

static void
soup_auth_domain_basic_init (SoupAuthDomainBasic *basic)
{
}

static void
soup_auth_domain_basic_finalize (GObject *object)
{
	SoupAuthDomainBasicPrivate *priv =
		soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (object));

	if (priv->auth_dnotify)
		priv->auth_dnotify (priv->auth_data);

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

static void
soup_auth_domain_basic_set_property (GObject *object, guint prop_id,
				     const GValue *value, GParamSpec *pspec)
{
	SoupAuthDomainBasicPrivate *priv =
		soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (object));

	switch (prop_id) {
	case PROP_AUTH_CALLBACK:
		priv->auth_callback = g_value_get_pointer (value);
		break;
	case PROP_AUTH_DATA:
		if (priv->auth_dnotify) {
			priv->auth_dnotify (priv->auth_data);
			priv->auth_dnotify = NULL;
		}
		priv->auth_data = g_value_get_pointer (value);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
soup_auth_domain_basic_get_property (GObject *object, guint prop_id,
				     GValue *value, GParamSpec *pspec)
{
	SoupAuthDomainBasicPrivate *priv =
		soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (object));

	switch (prop_id) {
	case PROP_AUTH_CALLBACK:
		g_value_set_pointer (value, priv->auth_callback);
		break;
	case PROP_AUTH_DATA:
		g_value_set_pointer (value, priv->auth_data);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

/**
 * soup_auth_domain_basic_new: (constructor)
 * @optname1: name of first option, or %NULL
 * @...: option name/value pairs
 *
 * Creates a #SoupAuthDomainBasic.
 *
 * You must set the [property@AuthDomain:realm] property, to indicate the realm
 * name to be returned with the authentication challenge to the client. Other
 * parameters are optional.
 *
 * Returns: the new #SoupAuthDomain
 **/
SoupAuthDomain *
soup_auth_domain_basic_new (const char *optname1, ...)
{
	SoupAuthDomain *domain;
	va_list ap;

	va_start (ap, optname1);
	domain = (SoupAuthDomain *)g_object_new_valist (SOUP_TYPE_AUTH_DOMAIN_BASIC,
							optname1, ap);
	va_end (ap);

	g_return_val_if_fail (soup_auth_domain_get_realm (domain) != NULL, NULL);

	return domain;
}

/**
 * SoupAuthDomainBasicAuthCallback:
 * @domain: (type SoupAuthDomainBasic): the domain
 * @msg: the message being authenticated
 * @username: the username provided by the client
 * @password: the password provided by the client
 * @user_data: the data passed to [method@AuthDomainBasic.set_auth_callback]
 *
 * Callback used by #SoupAuthDomainBasic for authentication purposes.
 *
 * The application should verify that @username and @password and valid
 * and return %TRUE or %FALSE.
 *
 * If you are maintaining your own password database (rather than
 * using the password to authenticate against some other system like
 * PAM or a remote server), you should make sure you know what you are
 * doing. In particular, don't store cleartext passwords, or
 * easily-computed hashes of cleartext passwords, even if you don't
 * care that much about the security of your server, because users
 * will frequently use the same password for multiple sites, and so
 * compromising any site with a cleartext (or easily-cracked) password
 * database may give attackers access to other more-interesting sites
 * as well.
 *
 * Returns: %TRUE if @username and @password are valid
 **/

/**
 * soup_auth_domain_basic_set_auth_callback: (attributes org.gtk.Method.set_property=auth-callback)
 * @domain: (type SoupAuthDomainBasic): the domain
 * @callback: the callback
 * @user_data: data to pass to @auth_callback
 * @dnotify: destroy notifier to free @user_data when @domain
 *   is destroyed
 *
 * Sets the callback that @domain will use to authenticate incoming
 * requests.
 *
 * For each request containing authorization, @domain will invoke the callback,
 * and then either accept or reject the request based on @callback's return
 * value.
 *
 * You can also set the auth callback by setting the
 * [property@AuthDomainBasic:auth-callback] and
 * [property@AuthDomainBasic:auth-data] properties, which can also be used to
 * set the callback at construct time.
 **/
void
soup_auth_domain_basic_set_auth_callback (SoupAuthDomain *domain,
					  SoupAuthDomainBasicAuthCallback callback,
					  gpointer        user_data,
					  GDestroyNotify  dnotify)
{
	SoupAuthDomainBasicPrivate *priv =
		soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (domain));

	if (priv->auth_dnotify)
		priv->auth_dnotify (priv->auth_data);

	priv->auth_callback = callback;
	priv->auth_data = user_data;
	priv->auth_dnotify = dnotify;

	g_object_notify_by_pspec (G_OBJECT (domain), properties[PROP_AUTH_CALLBACK]);
	g_object_notify_by_pspec (G_OBJECT (domain), properties[PROP_AUTH_DATA]);
}

static void
pw_free (char *pw)
{
	memset (pw, 0, strlen (pw));
	g_free (pw);
}

static gboolean
parse_basic (const char *header,
	     char      **username,
	     char      **password)
{
	char *decoded, *colon;
	gsize len, plen;

	if (!header || (strncmp (header, "Basic ", 6) != 0))
		return FALSE;

	decoded = (char *)g_base64_decode (header + 6, &len);
	if (!decoded)
		return FALSE;

	colon = memchr (decoded, ':', len);
	if (!colon) {
		pw_free (decoded);
		return FALSE;
	}
	*colon = '\0';
	plen = len - (colon - decoded) - 1;

	*password = g_strndup (colon + 1, plen);
	memset (colon + 1, 0, plen);
	*username = decoded;
	return TRUE;
}

static char *
soup_auth_domain_basic_accepts (SoupAuthDomain    *domain,
				SoupServerMessage *msg,
				const char        *header)
{
	SoupAuthDomainBasicPrivate *priv =
		soup_auth_domain_basic_get_instance_private (SOUP_AUTH_DOMAIN_BASIC (domain));
	char *username, *password;
	gboolean ok = FALSE;

	if (!parse_basic (header, &username, &password))
		return NULL;

	if (priv->auth_callback) {
		ok = priv->auth_callback (domain, msg, username, password,
					  priv->auth_data);
	} else {
		ok = soup_auth_domain_try_generic_auth_callback (
			domain, msg, username);
	}

	pw_free (password);

	if (ok)
		return username;
	else {
		g_free (username);
		return NULL;
	}
}

static char *
soup_auth_domain_basic_challenge (SoupAuthDomain    *domain,
				  SoupServerMessage *msg)
{
	GString *challenge;

	challenge = g_string_new ("Basic ");
	soup_header_g_string_append_param (challenge, "realm", soup_auth_domain_get_realm (domain));
	return g_string_free (challenge, FALSE);
}

static gboolean
soup_auth_domain_basic_check_password (SoupAuthDomain    *domain,
				       SoupServerMessage *msg,
				       const char        *username,
				       const char        *password)
{
	const char *header;
	char *msg_username, *msg_password;
	gboolean ok;

	header = soup_message_headers_get_one_common (soup_server_message_get_request_headers (msg),
                                                      SOUP_HEADER_AUTHORIZATION);
	if (!parse_basic (header, &msg_username, &msg_password))
		return FALSE;

	ok = (!strcmp (username, msg_username) &&
	      !strcmp (password, msg_password));
	g_free (msg_username);
	pw_free (msg_password);

	return ok;
}

static void
soup_auth_domain_basic_class_init (SoupAuthDomainBasicClass *basic_class)
{
	SoupAuthDomainClass *auth_domain_class =
		SOUP_AUTH_DOMAIN_CLASS (basic_class);
	GObjectClass *object_class = G_OBJECT_CLASS (basic_class);

	auth_domain_class->accepts        = soup_auth_domain_basic_accepts;
	auth_domain_class->challenge      = soup_auth_domain_basic_challenge;
	auth_domain_class->check_password = soup_auth_domain_basic_check_password;

	object_class->finalize     = soup_auth_domain_basic_finalize;
	object_class->set_property = soup_auth_domain_basic_set_property;
	object_class->get_property = soup_auth_domain_basic_get_property;

	/**
	 * SoupAuthDomainBasic:auth-callback: (type SoupAuthDomainBasicAuthCallback) (attributes org.gtk.Property.set=soup_auth_domain_basic_set_auth_callback)
	 *
	 * The [callback@AuthDomainBasicAuthCallback].
	 */
        properties[PROP_AUTH_CALLBACK] =
		g_param_spec_pointer ("auth-callback",
				      "Authentication callback",
				      "Password-checking callback",
				      G_PARAM_READWRITE |
				      G_PARAM_STATIC_STRINGS);
	/**
	 * SoupAuthDomainBasic:auth-data:
	 *
	 * The data to pass to the [callback@AuthDomainBasicAuthCallback].
	 */
        properties[PROP_AUTH_DATA] =
		g_param_spec_pointer ("auth-data",
				      "Authentication callback data",
				      "Data to pass to authentication callback",
				      G_PARAM_READWRITE |
				      G_PARAM_STATIC_STRINGS);

        g_object_class_install_properties (object_class, LAST_PROPERTY, properties);
}
