diff libpurple/protocols/jabber/auth_cyrus.c @ 29078:8ada06fb65ed

jabber: Factor the SASL auth methods into their own files. This works with and without Cyrus SASL, though there's room for cleanup and de-duplication (some code is now duplicated between auth.c and auth_cyrus.c).
author Paul Aurich <paul@darkrain42.org>
date Sat, 07 Nov 2009 06:10:17 +0000
parents
children de8565bc63fe
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/jabber/auth_cyrus.c	Sat Nov 07 06:10:17 2009 +0000
@@ -0,0 +1,540 @@
+/*
+ * purple - Jabber Protocol Plugin
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ *
+ */
+#include "internal.h"
+#include "core.h"
+#include "debug.h"
+#include "request.h"
+
+#include "auth.h"
+#include "jabber.h"
+
+static xmlnode *jabber_auth_start_cyrus(JabberStream *);
+static void jabber_sasl_build_callbacks(JabberStream *);
+
+static void disallow_plaintext_auth(PurpleAccount *account)
+{
+	purple_connection_error_reason(purple_account_get_connection(account),
+		PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR,
+		_("Server requires plaintext authentication over an unencrypted stream"));
+}
+
+/* Callbacks for Cyrus SASL */
+
+static int jabber_sasl_cb_realm(void *ctx, int id, const char **avail, const char **result)
+{
+	JabberStream *js = ctx;
+
+	if (id != SASL_CB_GETREALM || !result) return SASL_BADPARAM;
+
+	*result = js->user->domain;
+
+	return SASL_OK;
+}
+
+static int jabber_sasl_cb_simple(void *ctx, int id, const char **res, unsigned *len)
+{
+	JabberStream *js = ctx;
+
+	switch(id) {
+		case SASL_CB_AUTHNAME:
+			*res = js->user->node;
+			break;
+		case SASL_CB_USER:
+			*res = "";
+			break;
+		default:
+			return SASL_BADPARAM;
+	}
+	if (len) *len = strlen((char *)*res);
+	return SASL_OK;
+}
+
+static int jabber_sasl_cb_secret(sasl_conn_t *conn, void *ctx, int id, sasl_secret_t **secret)
+{
+	JabberStream *js = ctx;
+	const char *pw = purple_account_get_password(js->gc->account);
+	size_t len;
+	static sasl_secret_t *x = NULL;
+
+	if (!conn || !secret || id != SASL_CB_PASS)
+		return SASL_BADPARAM;
+
+	len = strlen(pw);
+	x = (sasl_secret_t *) realloc(x, sizeof(sasl_secret_t) + len);
+
+	if (!x)
+		return SASL_NOMEM;
+
+	x->len = len;
+	strcpy((char*)x->data, pw);
+
+	*secret = x;
+	return SASL_OK;
+}
+
+static void allow_cyrus_plaintext_auth(PurpleAccount *account)
+{
+	PurpleConnection *gc;
+	JabberStream *js;
+	xmlnode *response;
+
+	gc = purple_account_get_connection(account);
+	js = purple_connection_get_protocol_data(gc);
+
+	purple_account_set_bool(account, "auth_plain_in_clear", TRUE);
+
+	response = jabber_auth_start_cyrus(js);
+	if (response) {
+		jabber_send(js, response);
+		xmlnode_free(response);
+	}
+}
+
+static void auth_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields)
+{
+	PurpleAccount *account;
+	JabberStream *js;
+	xmlnode *response;
+	const char *entry;
+	gboolean remember;
+
+	/* The password prompt dialog doesn't get disposed if the account disconnects */
+	if (!PURPLE_CONNECTION_IS_VALID(gc))
+		return;
+
+	account = purple_connection_get_account(gc);
+	js = purple_connection_get_protocol_data(gc);
+
+	entry = purple_request_fields_get_string(fields, "password");
+	remember = purple_request_fields_get_bool(fields, "remember");
+
+	if (!entry || !*entry)
+	{
+		purple_notify_error(account, NULL, _("Password is required to sign on."), NULL);
+		return;
+	}
+
+	if (remember)
+		purple_account_set_remember_password(account, TRUE);
+
+	purple_account_set_password(account, entry);
+
+	/* Rebuild our callbacks as we now have a password to offer */
+	jabber_sasl_build_callbacks(js);
+
+	/* Restart our connection */
+	response = jabber_auth_start_cyrus(js);
+	if (response) {
+		jabber_send(js, response);
+		xmlnode_free(response);
+	}
+}
+
+static void
+auth_no_pass_cb(PurpleConnection *conn, PurpleRequestFields *fields)
+{
+	JabberStream *js;
+
+	/* The password prompt dialog doesn't get disposed if the account disconnects */
+	if (!PURPLE_CONNECTION_IS_VALID(conn))
+		return;
+
+	js = conn->proto_data;
+
+	/* Disable the account as the user has canceled connecting */
+	purple_account_set_enabled(conn->account, purple_core_get_ui(), FALSE);
+}
+
+static xmlnode *jabber_auth_start_cyrus(JabberStream *js)
+{
+	const char *clientout = NULL;
+	char *enc_out;
+	unsigned coutlen = 0;
+	xmlnode *auth;
+	sasl_security_properties_t secprops;
+	gboolean again;
+	gboolean plaintext = TRUE;
+
+	/* Set up security properties and options */
+	secprops.min_ssf = 0;
+	secprops.security_flags = SASL_SEC_NOANONYMOUS;
+
+	if (!jabber_stream_is_ssl(js)) {
+		secprops.max_ssf = -1;
+		secprops.maxbufsize = 4096;
+		plaintext = purple_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE);
+		if (!plaintext)
+			secprops.security_flags |= SASL_SEC_NOPLAINTEXT;
+	} else {
+		secprops.max_ssf = 0;
+		secprops.maxbufsize = 0;
+		plaintext = TRUE;
+	}
+	secprops.property_names = 0;
+	secprops.property_values = 0;
+
+	do {
+		again = FALSE;
+
+		js->sasl_state = sasl_client_new("xmpp", js->serverFQDN, NULL, NULL, js->sasl_cb, 0, &js->sasl);
+		if (js->sasl_state==SASL_OK) {
+			sasl_setprop(js->sasl, SASL_SEC_PROPS, &secprops);
+			purple_debug_info("sasl", "Mechs found: %s\n", js->sasl_mechs->str);
+			js->sasl_state = sasl_client_start(js->sasl, js->sasl_mechs->str, NULL, &clientout, &coutlen, &js->current_mech);
+		}
+		switch (js->sasl_state) {
+			/* Success */
+			case SASL_OK:
+			case SASL_CONTINUE:
+				break;
+			case SASL_NOMECH:
+				/* No mechanisms have offered to help */
+
+				/* Firstly, if we don't have a password try
+				 * to get one
+				 */
+
+				if (!purple_account_get_password(js->gc->account)) {
+					purple_account_request_password(js->gc->account, G_CALLBACK(auth_pass_cb), G_CALLBACK(auth_no_pass_cb), js->gc);
+					return NULL;
+
+				/* If we've got a password, but aren't sending
+				 * it in plaintext, see if we can turn on
+				 * plaintext auth
+				 */
+				} else if (!plaintext) {
+					char *msg = g_strdup_printf(_("%s requires plaintext authentication over an unencrypted connection.  Allow this and continue authentication?"),
+							js->gc->account->username);
+					purple_request_yes_no(js->gc, _("Plaintext Authentication"),
+							_("Plaintext Authentication"),
+							msg,
+							1, js->gc->account, NULL, NULL, js->gc->account,
+							allow_cyrus_plaintext_auth,
+							disallow_plaintext_auth);
+					g_free(msg);
+					return NULL;
+
+				} else {
+					/* We have no mechs which can work.
+					 * Try falling back on the old jabber:iq:auth method. We get here if the server supports
+					 * one or more sasl mechs, we are compiled with cyrus-sasl support, but we support or can connect with none of
+					 * the offerred mechs. jabberd 2.0 w/ SASL and Apple's iChat Server 10.5 both handle and expect
+					 * jabber:iq:auth in this situation.  iChat Server in particular offers SASL GSSAPI by default, which is often
+					 * not configured on the client side, and expects a fallback to jabber:iq:auth when it (predictably) fails.
+					 *
+					 * Note: xep-0078 points out that using jabber:iq:auth after a sasl failure is wrong. However,
+					 * I believe this refers to actual authentication failure, not a simple lack of concordant mechanisms.
+					 * Doing otherwise means that simply compiling with SASL support renders the client unable to connect to servers
+					 * which would connect without issue otherwise. -evands
+					 */
+					jabber_auth_start_old(js);
+					return NULL;
+				}
+				/* not reached */
+				break;
+
+				/* Fatal errors. Give up and go home */
+			case SASL_BADPARAM:
+			case SASL_NOMEM:
+				break;
+
+				/* For everything else, fail the mechanism and try again */
+			default:
+				purple_debug_info("sasl", "sasl_state is %d, failing the mech and trying again\n", js->sasl_state);
+
+				/*
+				 * DAA: is this right?
+				 * The manpage says that "mech" will contain the chosen mechanism on success.
+				 * Presumably, if we get here that isn't the case and we shouldn't try again?
+				 * I suspect that this never happens.
+				 */
+				/*
+				 * SXW: Yes, this is right. What this handles is the situation where a
+				 * mechanism, say GSSAPI, is tried. If that mechanism fails, it may be
+				 * due to mechanism specific issues, so we want to try one of the other
+				 * supported mechanisms. This code handles that case
+				 */
+				if (js->current_mech && *js->current_mech) {
+					char *pos;
+					if ((pos = strstr(js->sasl_mechs->str, js->current_mech))) {
+						g_string_erase(js->sasl_mechs, pos-js->sasl_mechs->str, strlen(js->current_mech));
+					}
+					/* Remove space which separated this mech from the next */
+					if ((js->sasl_mechs->str)[0] == ' ') {
+						g_string_erase(js->sasl_mechs, 0, 1);
+					}
+					again = TRUE;
+				}
+
+				sasl_dispose(&js->sasl);
+		}
+	} while (again);
+
+	if (js->sasl_state == SASL_CONTINUE || js->sasl_state == SASL_OK) {
+		auth = xmlnode_new("auth");
+		xmlnode_set_namespace(auth, "urn:ietf:params:xml:ns:xmpp-sasl");
+		xmlnode_set_attrib(auth, "mechanism", js->current_mech);
+
+		xmlnode_set_attrib(auth, "xmlns:ga", "http://www.google.com/talk/protocol/auth");
+		xmlnode_set_attrib(auth, "ga:client-uses-full-bind-result", "true");
+
+		if (clientout) {
+			if (coutlen == 0) {
+				xmlnode_insert_data(auth, "=", -1);
+			} else {
+				enc_out = purple_base64_encode((unsigned char*)clientout, coutlen);
+				xmlnode_insert_data(auth, enc_out, -1);
+				g_free(enc_out);
+			}
+		}
+
+		return auth;
+	} else {
+		purple_connection_error_reason(js->gc,
+			PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
+			_("SASL authentication failed"));
+	
+		return NULL;
+	}
+}
+
+static int
+jabber_sasl_cb_log(void *context, int level, const char *message)
+{
+	if(level <= SASL_LOG_TRACE)
+		purple_debug_info("sasl", "%s\n", message);
+
+	return SASL_OK;
+}
+
+static void
+jabber_sasl_build_callbacks(JabberStream *js)
+{
+	int id;
+
+	/* Set up our callbacks structure */
+	if (js->sasl_cb == NULL)
+		js->sasl_cb = g_new0(sasl_callback_t,6);
+
+	id = 0;
+	js->sasl_cb[id].id = SASL_CB_GETREALM;
+	js->sasl_cb[id].proc = jabber_sasl_cb_realm;
+	js->sasl_cb[id].context = (void *)js;
+	id++;
+
+	js->sasl_cb[id].id = SASL_CB_AUTHNAME;
+	js->sasl_cb[id].proc = jabber_sasl_cb_simple;
+	js->sasl_cb[id].context = (void *)js;
+	id++;
+
+	js->sasl_cb[id].id = SASL_CB_USER;
+	js->sasl_cb[id].proc = jabber_sasl_cb_simple;
+	js->sasl_cb[id].context = (void *)js;
+	id++;
+
+	if (purple_account_get_password(js->gc->account) != NULL ) {
+		js->sasl_cb[id].id = SASL_CB_PASS;
+		js->sasl_cb[id].proc = jabber_sasl_cb_secret;
+		js->sasl_cb[id].context = (void *)js;
+		id++;
+	}
+
+	js->sasl_cb[id].id = SASL_CB_LOG;
+	js->sasl_cb[id].proc = jabber_sasl_cb_log;
+	js->sasl_cb[id].context = (void*)js;
+	id++;
+
+	js->sasl_cb[id].id = SASL_CB_LIST_END;
+}
+
+static xmlnode *jabber_cyrus_start(JabberStream *js, xmlnode *mechanisms)
+{
+	xmlnode *mechnode;
+
+	js->sasl_mechs = g_string_new("");
+
+	for(mechnode = xmlnode_get_child(mechanisms, "mechanism"); mechnode;
+			mechnode = xmlnode_get_next_twin(mechnode))
+	{
+		char *mech_name = xmlnode_get_data(mechnode);
+
+		if (!mech_name || !*mech_name) {
+			g_free(mech_name);
+			continue;
+		}
+
+		/* Don't include Google Talk's X-GOOGLE-TOKEN mechanism, as we will not
+		 * support it and including it gives a false fall-back to other mechs offerred,
+		 * leading to incorrect error handling.
+		 */
+		if (g_str_equal(mech_name, "X-GOOGLE-TOKEN")) {
+			g_free(mech_name);
+			continue;
+		}
+
+		g_string_append(js->sasl_mechs, mech_name);
+		g_string_append_c(js->sasl_mechs, ' ');
+		g_free(mech_name);
+	}
+
+	jabber_sasl_build_callbacks(js);
+	return jabber_auth_start_cyrus(js);
+}
+
+static xmlnode *jabber_cyrus_handle_challenge(JabberStream *js, xmlnode *packet)
+{
+	char *enc_in = xmlnode_get_data(packet);
+	unsigned char *dec_in;
+	char *enc_out;
+	const char *c_out;
+	unsigned int clen;
+	gsize declen;
+	xmlnode *response = NULL;
+
+	dec_in = purple_base64_decode(enc_in, &declen);
+
+	js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen,
+					  NULL, &c_out, &clen);
+	g_free(enc_in);
+	g_free(dec_in);
+	if (js->sasl_state != SASL_CONTINUE && js->sasl_state != SASL_OK) {
+		gchar *tmp = g_strdup_printf(_("SASL error: %s"),
+				sasl_errdetail(js->sasl));
+		purple_debug_error("jabber", "Error is %d : %s\n",
+				js->sasl_state, sasl_errdetail(js->sasl));
+		purple_connection_error_reason(js->gc,
+			PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
+		g_free(tmp);
+	} else {
+		response = xmlnode_new("response");
+		xmlnode_set_namespace(response, "urn:ietf:params:xml:ns:xmpp-sasl");
+		if (clen > 0) {
+			/* Cyrus SASL 2.1.22 appears to contain code to add the charset
+			 * to the response for DIGEST-MD5 but there is no possibility
+			 * it will be executed.
+			 *
+			 * My reading of the digestmd5 plugin indicates the username and
+			 * realm are always encoded in UTF-8 (they seem to be the values
+			 * we pass in), so we need to ensure charset=utf-8 is set.
+			 */
+			if (!purple_strequal(js->current_mech, "DIGEST-MD5") ||
+					strstr(c_out, ",charset="))
+				/* If we're not using DIGEST-MD5 or Cyrus SASL is fixed */
+				enc_out = purple_base64_encode((unsigned char*)c_out, clen);
+			else {
+				char *tmp = g_strdup_printf("%s,charset=utf-8", c_out);
+				enc_out = purple_base64_encode((unsigned char*)tmp, clen + 14);
+				g_free(tmp);
+			}
+
+			xmlnode_insert_data(response, enc_out, -1);
+			g_free(enc_out);
+		}
+	}
+
+	return response;
+}
+
+static gboolean jabber_cyrus_handle_success(JabberStream *js, xmlnode *packet)
+{
+	const void *x;
+
+	/* The SASL docs say that if the client hasn't returned OK yet, we
+	 * should try one more round against it
+	 */
+	if (js->sasl_state != SASL_OK) {
+		char *enc_in = xmlnode_get_data(packet);
+		unsigned char *dec_in = NULL;
+		const char *c_out;
+		unsigned int clen;
+		gsize declen = 0;
+
+		if(enc_in != NULL)
+			dec_in = purple_base64_decode(enc_in, &declen);
+
+		js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, NULL, &c_out, &clen);
+
+		g_free(enc_in);
+		g_free(dec_in);
+
+		if (js->sasl_state != SASL_OK) {
+			/* This should never happen! */
+			purple_connection_error_reason(js->gc,
+				PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
+				_("Invalid response from server"));
+			g_return_val_if_reached(FALSE);
+		}
+	}
+
+	/* If we've negotiated a security layer, we need to enable it */
+	if (js->sasl) {
+		sasl_getprop(js->sasl, SASL_SSF, &x);
+		if (*(int *)x > 0) {
+			sasl_getprop(js->sasl, SASL_MAXOUTBUF, &x);
+			js->sasl_maxbuf = *(int *)x;
+		}
+	}
+
+	return TRUE;
+}
+
+static xmlnode *jabber_cyrus_handle_failure(JabberStream *js, xmlnode *packet)
+{
+	if (js->auth_fail_count++ < 5) {
+		if (js->current_mech && *js->current_mech) {
+			char *pos;
+			if ((pos = strstr(js->sasl_mechs->str, js->current_mech))) {
+				g_string_erase(js->sasl_mechs, pos-js->sasl_mechs->str, strlen(js->current_mech));
+			}
+			/* Remove space which separated this mech from the next */
+			if ((js->sasl_mechs->str)[0] == ' ') {
+				g_string_erase(js->sasl_mechs, 0, 1);
+			}
+		}
+		if (*js->sasl_mechs->str) {
+			/* If we have remaining mechs to try, do so */
+			sasl_dispose(&js->sasl);
+
+			return jabber_auth_start_cyrus(js);
+		}
+	}
+
+	/* Nothing to send */
+	return NULL;
+}
+
+static JabberSaslMech cyrus_mech = {
+	100, /* priority */
+	"*", /* name; Cyrus provides a bunch of mechanisms, so use an invalid
+	      * mechanism name (per rfc4422 3.1). */
+	jabber_cyrus_start,
+	jabber_cyrus_handle_challenge,
+	jabber_cyrus_handle_success,
+	jabber_cyrus_handle_failure,
+	NULL,
+};
+
+JabberSaslMech *jabber_auth_get_cyrus_mech(void)
+{
+	return &cyrus_mech;
+}