changeset 12508:5cfc53ead482

[gaim-migrate @ 14820] patch from Simon Wilkinson to add Cyrus SASL support for jabber Give him credit if it works flawlessly. Blame me if it doesn't, as the patch was against 1.3.1 (yeah, I've been sitting on it for that long), and I had to merge it to HEAD, and clean up a bunch of warnings committer: Tailor Script <tailor@pidgin.im>
author Nathan Walp <nwalp@pidgin.im>
date Sat, 17 Dec 2005 02:24:05 +0000
parents 5bf6c0c908b2
children b102b1ed99a1
files COPYRIGHT configure.ac src/protocols/jabber/Makefile.am src/protocols/jabber/auth.c src/protocols/jabber/jabber.c src/protocols/jabber/jabber.h
diffstat 6 files changed, 363 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/COPYRIGHT	Fri Dec 16 21:41:46 2005 +0000
+++ b/COPYRIGHT	Sat Dec 17 02:24:05 2005 +0000
@@ -264,6 +264,7 @@
 Dave West
 Daniel Westermann-Clark
 Andrew Whewell
+Simon Wilkinson
 Dan Willemsen
 Jason Willis
 Matt Wilson
--- a/configure.ac	Fri Dec 16 21:41:46 2005 +0000
+++ b/configure.ac	Sat Dec 17 02:24:05 2005 +0000
@@ -1402,6 +1402,11 @@
 dnl checks for jabber
 dnl AC_CHECK_SIZEOF(short)
 AC_CHECK_FUNCS(snprintf connect)
+AC_SUBST(SASL_LIBS)
+AC_ARG_ENABLE(cyrus-sasl, AC_HELP_STRING([--enable-cyrus-sasl], [enable Cyrus SASL support for jabberd (no)]), enable_cyrus_sasl=$enableval, enable_cyrus_sasl=no)
+if test "x-$enable_cyrus_sasl" = "x-yes" ; then
+  AC_CHECK_LIB(sasl2, sasl_client_init, [AC_DEFINE(HAVE_CYRUS_SASL, [1], [Define to 1 if Cyrus SASL is present]) SASL_LIBS=-"lsasl2"], [AC_ERROR(Cyrus SASL library not found)])
+fi
 
 dnl checks for zephyr
 AC_DEFINE(ZEPHYR_INT32, long, [Size of an int32.])
@@ -1676,11 +1681,13 @@
 echo Build with GtkSpell support... : $enable_gtkspell
 echo Build with Voice/Video support : $enable_vv
 echo Build with DBUS support....... : $enable_dbus
+echo Build with Cyrus SASL support. : $enable_cyrus_sasl
 if test x$enable_dbus = xyes ; then
 echo DBUS session directory........ : $DBUS_SESSION_DIR
 fi
 echo Has you....................... : yes
 echo
+echo
 echo Use kerberos 4 with zephyr.... : $kerberos
 echo Use external libzephyr........ : $zephyr
 echo
--- a/src/protocols/jabber/Makefile.am	Fri Dec 16 21:41:46 2005 +0000
+++ b/src/protocols/jabber/Makefile.am	Sat Dec 17 02:24:05 2005 +0000
@@ -36,7 +36,7 @@
 
 AM_CFLAGS = $(st)
 
-libjabber_la_LDFLAGS = -module -avoid-version $(GLIB_LIBS)
+libjabber_la_LDFLAGS = -module -avoid-version $(GLIB_LIBS) $(SASL_LIBS)
 
 if STATIC_JABBER
 
--- a/src/protocols/jabber/auth.c	Fri Dec 16 21:41:46 2005 +0000
+++ b/src/protocols/jabber/auth.c	Sat Dec 17 02:24:05 2005 +0000
@@ -114,12 +114,188 @@
 	gaim_connection_error(account->gc, _("Server requires plaintext authentication over an unencrypted stream"));
 }
 
+#ifdef HAVE_CYRUS_SASL
+
+static void jabber_auth_start_cyrus(JabberStream *);
+
+/* Callbacks for Cyrus SASL */
+
+static int jabber_sasl_cb_realm(void *ctx, int id, const char **avail, const char **result)
+{
+	JabberStream *js = (JabberStream *)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 = (JabberStream *)ctx;
+
+	switch(id) {
+		case SASL_CB_AUTHNAME:
+			*res = js->user->node;
+			break;
+		case SASL_CB_USER:
+			*res = js->user->node;
+			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 = (JabberStream *)ctx;
+	const char *pw = gaim_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(GaimAccount *account)
+{
+	gaim_account_set_bool(account, "auth_plain_in_clear", TRUE);
+
+	jabber_auth_start_cyrus(account->gc->proto_data);
+}
+
+static void jabber_auth_start_cyrus(JabberStream *js)
+{
+	const char *clientout, *mech;
+	char *enc_out;
+	unsigned coutlen;
+	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 (!js->gsc) {
+		plaintext = gaim_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE);
+		if (!plaintext)
+			secprops.security_flags |= SASL_SEC_NOPLAINTEXT;
+		secprops.max_ssf = -1;
+		secprops.maxbufsize = 4096;
+	} else {
+		plaintext = FALSE;
+		secprops.max_ssf = 0;
+		secprops.maxbufsize = 0;
+	}
+	secprops.property_names = 0;
+	secprops.property_values = 0;
+
+	do {
+		again = FALSE;
+		/* Use the user's domain for compatibility with the old
+		 * DIGESTMD5 code. Note that this may cause problems where
+		 * the user's domain doesn't match the FQDN of the jabber
+		 * service
+		 */
+
+		js->sasl_state = sasl_client_new("xmpp", js->user->domain, NULL, NULL, js->sasl_cb, 0, &js->sasl);
+		if (js->sasl_state==SASL_OK) {
+			sasl_setprop(js->sasl, SASL_SEC_PROPS, &secprops);
+			js->sasl_state = sasl_client_start(js->sasl, js->sasl_mechs->str, NULL, &clientout, &coutlen, &mech);
+		}
+		switch (js->sasl_state) {
+			/* Success */
+			case SASL_CONTINUE:
+				break;
+			case SASL_NOMECH:
+				/* No mechanisms do what we want. See if we can add
+				 * plaintext ones to the list. */
+
+				if (!gaim_account_get_password(js->gc->account)) {
+					gaim_connection_error(js->gc, _("Server couldn't authenticate you without a password"));
+					return;
+				} else if (!plaintext) {
+					gaim_request_yes_no(js->gc, _("Plaintext Authentication"),
+							_("Plaintext Authentication"),
+							_("This server requires plaintext authentication over an unencrypted connection.  Allow this and continue authentication?"),
+							2, js->gc->account,
+							allow_cyrus_plaintext_auth,
+							disallow_plaintext_auth);
+					return;
+				} else {
+					gaim_connection_error(js->gc, _("Server does not use any supported authentication method"));
+					return;
+				}
+				/* 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:
+				if (strlen(mech)>0) {
+					char *pos;
+					pos = strstr(js->sasl_mechs->str,mech);
+					g_assert(pos!=NULL);
+					g_string_erase(js->sasl_mechs, pos-js->sasl_mechs->str,strlen(mech));
+				}
+				sasl_dispose(&js->sasl);
+				again=TRUE;
+		}
+	} while (again);
+
+	if (js->sasl_state == SASL_CONTINUE) {
+		auth = xmlnode_new("auth");
+		xmlnode_set_attrib(auth, "xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+		xmlnode_set_attrib(auth,"mechanism", mech);
+		if (clientout) {
+			if (coutlen == 0) {
+				xmlnode_insert_data(auth, "=", -1);
+			} else {
+				enc_out = gaim_base64_encode((unsigned char*)clientout, coutlen);
+				xmlnode_insert_data(auth, enc_out, -1);
+				g_free(enc_out);
+			}
+		}
+		jabber_send(js, auth);
+		xmlnode_free(auth);
+	} else {
+		gaim_connection_error(js->gc, "SASL authentication failed\n");
+	}
+}
+
+#endif
+
 void
 jabber_auth_start(JabberStream *js, xmlnode *packet)
 {
-	xmlnode *mechs, *mechnode;
+#ifdef HAVE_CYRUS_SASL
+	int id;
+#else
+	gboolean digest_md5 = FALSE, plain=FALSE;
+#endif
 
-	gboolean digest_md5 = FALSE, plain=FALSE;
+	xmlnode *mechs, *mechnode;
 
 
 	if(js->registration) {
@@ -134,17 +310,59 @@
 		return;
 	}
 
+#ifdef HAVE_CYRUS_SASL
+	js->sasl_mechs = g_string_new("");
+#endif
+
 	for(mechnode = xmlnode_get_child(mechs, "mechanism"); mechnode;
 			mechnode = xmlnode_get_next_twin(mechnode))
 	{
 		char *mech_name = xmlnode_get_data(mechnode);
+#ifdef HAVE_CYRUS_SASL
+		g_string_append(js->sasl_mechs, mech_name);
+		g_string_append_c(js->sasl_mechs,' ');
+#else
 		if(mech_name && !strcmp(mech_name, "DIGEST-MD5"))
 			digest_md5 = TRUE;
 		else if(mech_name && !strcmp(mech_name, "PLAIN"))
 			plain = TRUE;
+#endif
 		g_free(mech_name);
 	}
 
+#ifdef HAVE_CYRUS_SASL
+	js->auth_type = JABBER_AUTH_CYRUS;
+
+	/* Set up our callbacks structure */
+	js->sasl_cb = g_new0(sasl_callback_t,5);
+
+	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 (gaim_account_get_password(js->gc->account)) {
+		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_LIST_END;
+
+	jabber_auth_start_cyrus(js);
+#else
 
 	if(digest_md5) {
 		xmlnode *auth;
@@ -172,6 +390,7 @@
 		gaim_connection_error(js->gc,
 				_("Server does not use any supported authentication method"));
 	}
+#endif
 }
 
 static void auth_old_result_cb(JabberStream *js, xmlnode *packet, gpointer data)
@@ -463,17 +682,70 @@
 		g_free(dec_in);
 		g_hash_table_destroy(parts);
 	}
+#ifdef HAVE_CYRUS_SASL
+	else if (js->auth_type == JABBER_AUTH_CYRUS) {
+		char *enc_in = xmlnode_get_data(packet);
+		unsigned char *dec_in;
+		char *enc_out;
+		const char *c_out;
+		unsigned int clen,declen;
+		xmlnode *response;
+
+		dec_in = gaim_base64_decode(enc_in, &declen);
+
+		js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen,
+						  NULL, &c_out, &clen);
+		g_free(dec_in);
+		if (js->sasl_state != SASL_CONTINUE && js->sasl_state != SASL_OK) {
+			gaim_debug_error("jabber", "Error is %d : %s\n",js->sasl_state,sasl_errdetail(js->sasl));
+			gaim_connection_error(js->gc, _("SASL error"));
+			return;
+		} else {
+			response = xmlnode_new("response");
+			xmlnode_set_attrib(response, "xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+			if (c_out) {
+				enc_out = gaim_base64_encode((unsigned char*)c_out, clen);
+				xmlnode_insert_data(response, enc_out, -1);
+				g_free(enc_out);
+			}
+			jabber_send(js, response);
+			xmlnode_free(response);
+		}
+	}
+#endif
 }
 
 void jabber_auth_handle_success(JabberStream *js, xmlnode *packet)
 {
 	const char *ns = xmlnode_get_attrib(packet, "xmlns");
+#ifdef HAVE_CYRUS_SASL
+	int *x;
+#endif
 
 	if(!ns || strcmp(ns, "urn:ietf:params:xml:ns:xmpp-sasl")) {
 		gaim_connection_error(js->gc, _("Invalid response from server."));
 		return;
 	}
 
+#if HAVE_CYRUS_SASL
+	/* 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) {
+		js->sasl_state = sasl_client_step(js->sasl, NULL, 0, NULL, NULL, NULL);
+		if (js->sasl_state != SASL_OK) {
+			/* This should never happen! */
+			gaim_connection_error(js->gc, _("Invalid response from server."));
+		}
+	}
+	/* If we've negotiated a security layer, we need to enable it */
+	sasl_getprop(js->sasl, SASL_SSF, (const void **)&x);
+	if (*x>0) {
+		sasl_getprop(js->sasl, SASL_MAXOUTBUF, (const void **)&x);
+		js->sasl_maxbuf = *x;
+	}
+#endif
+
 	jabber_stream_set_state(js, JABBER_STREAM_REINITIALIZING);
 }
 
--- a/src/protocols/jabber/jabber.c	Fri Dec 16 21:41:46 2005 +0000
+++ b/src/protocols/jabber/jabber.c	Sat Dec 17 02:24:05 2005 +0000
@@ -203,6 +203,42 @@
 		gaim_debug(GAIM_DEBUG_MISC, "jabber", "Sending%s: %s\n",
 				js->gsc ? " (ssl)" : "", data);
 
+	/* If we've got a security layer, we need to encode the data,
+	 * splitting it on the maximum buffer length negotiated */
+
+#ifdef HAVE_CYRUS_SASL
+	if (js->sasl_maxbuf>0) {
+		int pos;
+
+		if (!js->gsc && js->fd<0)
+			return;
+		pos = 0;
+		if (len == -1)
+			len = strlen(data);
+		while (pos < len) {
+			int towrite;
+			const char *out;
+			unsigned olen;
+
+			if ((len - pos) < js->sasl_maxbuf)
+				towrite = len - pos;
+			else
+				towrite = js->sasl_maxbuf;
+
+			sasl_encode(js->sasl, &data[pos], towrite, &out, &olen);
+			pos += towrite;
+
+			if (js->gsc)
+				ret = gaim_ssl_write(js->gsc, out, olen);
+			else
+				ret = write(js->fd, out, olen);
+			if (ret < 0)
+				gaim_connection_error(js->gc, _("Write error"));
+		}
+		return;
+	}
+#endif
+	
 	if(js->gsc) {
 		ret = gaim_ssl_write(js->gsc, data, len == -1 ? strlen(data) : len);
 	} else {
@@ -266,6 +302,18 @@
 		return;
 
 	if((len = read(js->fd, buf, sizeof(buf) - 1)) > 0) {
+#ifdef HAVE_CYRUS_SASL
+		if (js->sasl_maxbuf>0) {
+			const char *out;
+			int olen;
+			sasl_decode(js->sasl, buf, len, &out, &olen);
+			if (olen>0) {
+				gaim_debug(GAIM_DEBUG_INFO, "jabber", "RecvSASL (%d): %s\n", olen, out);
+				jabber_parser_process(js,out,olen);
+			}
+			return;
+		}
+#endif
 		buf[len] = '\0';
 		gaim_debug(GAIM_DEBUG_INFO, "jabber", "Recv (%d): %s\n", len, buf);
 		jabber_parser_process(js, buf, len);
@@ -819,6 +867,14 @@
 		jabber_id_free(js->user);
 	if(js->avatar_hash)
 		g_free(js->avatar_hash);
+#ifdef HAVE_CYRUS_SASL
+	if(js->sasl)
+		sasl_dispose(&js->sasl);
+	if(js->sasl_mechs)
+		g_string_free(js->sasl_mechs, TRUE);
+	if(js->sasl_cb)
+		g_free(js->sasl_cb);
+#endif
 	g_free(js);
 
 	gc->proto_data = NULL;
@@ -1608,7 +1664,8 @@
 
 static GaimPluginProtocolInfo prpl_info =
 {
-	OPT_PROTO_CHAT_TOPIC | OPT_PROTO_UNIQUE_CHATNAME,
+	OPT_PROTO_CHAT_TOPIC | OPT_PROTO_UNIQUE_CHATNAME |
+	OPT_PROTO_PASSWORD_OPTIONAL,
 	NULL,							/* user_splits */
 	NULL,							/* protocol_options */
 	{"jpeg,gif,png", 0, 0, 96, 96, GAIM_ICON_SCALE_DISPLAY}, /* icon_spec */
@@ -1743,6 +1800,10 @@
 
 	gaim_prefs_remove("/plugins/prpl/jabber");
 
+	/* XXX - If any other plugin wants SASL this won't be good ... */
+#ifdef HAVE_CYRUS_SASL
+	sasl_client_init(NULL);
+#endif
 	jabber_register_commands();
 }
 
--- a/src/protocols/jabber/jabber.h	Fri Dec 16 21:41:46 2005 +0000
+++ b/src/protocols/jabber/jabber.h	Sat Dec 17 02:24:05 2005 +0000
@@ -30,6 +30,10 @@
 #include "jutil.h"
 #include "xmlnode.h"
 
+#ifdef HAVE_CYRUS_SASL
+#include <sasl/sasl.h>
+#endif
+
 typedef enum {
 	JABBER_CAP_NONE           = 0,
 	JABBER_CAP_XHTML          = 1 << 0,
@@ -68,7 +72,8 @@
 		JABBER_AUTH_UNKNOWN,
 		JABBER_AUTH_DIGEST_MD5,
 		JABBER_AUTH_PLAIN,
-		JABBER_AUTH_IQ_AUTH
+		JABBER_AUTH_IQ_AUTH,
+		JABBER_AUTH_CYRUS
 	} auth_type;
 	char *stream_id;
 	JabberStreamState state;
@@ -102,6 +107,18 @@
 
 	char *avatar_hash;
 	GSList *pending_avatar_requests;
+
+	/* OK, this stays at the end of the struct, so plugins can depend
+	 * on the rest of the stuff being in the right place
+	 */
+#ifdef HAVE_CYRUS_SASL
+	sasl_conn_t *sasl;
+	sasl_callback_t *sasl_cb;
+	int sasl_state;
+	int sasl_maxbuf;
+	GString *sasl_mechs;
+#endif
+
 } JabberStream;
 
 void jabber_process_packet(JabberStream *js, xmlnode *packet);