Mercurial > pidgin
view libpurple/protocols/jabber/auth_cyrus.c @ 29872:a044ddee7878
Improve on a hack I commited in 12/2007 which allows connection via XMPP to iChat Server 10.5 when CYRUS-SASL is compiled with GSSAPI support but no GSSAPI credentials are valid to connect to the server. Instead of always trying jabber:iq:auth if all SASL mechs fail, we now only do so in the specific case of a single mech having been attempted and that mech being GSSAPI. In general, this means that we now gracefully fail authentication with SASL rather than getting ourselves booted from servers not expecting a jabber:iq:auth stanza.
author | Evan Schoenberg <evan.s@dreskin.net> |
---|---|
date | Tue, 04 May 2010 01:41:28 +0000 |
parents | a54e59c1c520 |
children | a81d44a11d99 |
line wrap: on
line source
/* * 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 JabberSaslState jabber_auth_start_cyrus(JabberStream *js, xmlnode **reply, char **error); 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")); } static void start_cyrus_wrapper(JabberStream *js) { char *error = NULL; xmlnode *response = NULL; JabberSaslState state = jabber_auth_start_cyrus(js, &response, &error); if (state == JABBER_SASL_STATE_FAIL) { purple_connection_error_reason(js->gc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, error); g_free(error); } else if (response) { jabber_send(js, response); xmlnode_free(response); } } /* 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; PurpleAccount *account; const char *pw; size_t len; static sasl_secret_t *x = NULL; account = purple_connection_get_account(js->gc); pw = purple_account_get_password(account); 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; gc = purple_account_get_connection(account); js = purple_connection_get_protocol_data(gc); purple_account_set_bool(account, "auth_plain_in_clear", TRUE); start_cyrus_wrapper(js); } static void auth_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields) { PurpleAccount *account; JabberStream *js; 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 negotiation */ start_cyrus_wrapper(js); } static void auth_no_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields) { PurpleAccount *account; JabberStream *js; /* 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); /* Disable the account as the user has canceled connecting */ purple_account_set_enabled(account, purple_core_get_ui(), FALSE); } static JabberSaslState jabber_auth_start_cyrus(JabberStream *js, xmlnode **reply, char **error) { PurpleAccount *account; const char *clientout = NULL; char *enc_out; unsigned coutlen = 0; 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; account = purple_connection_get_account(js->gc); if (!jabber_stream_is_ssl(js)) { secprops.max_ssf = -1; secprops.maxbufsize = 4096; plaintext = purple_account_get_bool(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(account)) { purple_account_request_password(account, G_CALLBACK(auth_pass_cb), G_CALLBACK(auth_no_pass_cb), js->gc); return JABBER_SASL_STATE_CONTINUE; /* 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?"), purple_account_get_username(account)); purple_request_yes_no(js->gc, _("Plaintext Authentication"), _("Plaintext Authentication"), msg, 1, account, NULL, NULL, account, allow_cyrus_plaintext_auth, disallow_plaintext_auth); g_free(msg); return JABBER_SASL_STATE_CONTINUE; } 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) { xmlnode *auth = xmlnode_new("auth"); xmlnode_set_namespace(auth, NS_XMPP_SASL); xmlnode_set_attrib(auth, "mechanism", js->current_mech); if (g_str_equal(js->user->domain, "gmail.com") || g_str_equal(js->user->domain, "googlemail.com")) { 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); } } *reply = auth; return JABBER_SASL_STATE_CONTINUE; } else { *error = g_strdup(_("SASL authentication failed")); return JABBER_SASL_STATE_FAIL; } } 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) { PurpleAccount *account; 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++; account = purple_connection_get_account(js->gc); if (purple_account_get_password(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 JabberSaslState jabber_cyrus_start(JabberStream *js, xmlnode *mechanisms, xmlnode **reply, char **error) { 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 * or Facebook Chat's X-FACEBOOK-PLATFORM mechansim, * as we will not support them and including them gives a false fall-back * to other mechs offerred, leading to incorrect error handling. */ if (g_str_equal(mech_name, "X-GOOGLE-TOKEN") || g_str_equal(mech_name, "X-FACEBOOK-PLATFORM") ) { 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, reply, error); } static JabberSaslState jabber_cyrus_handle_challenge(JabberStream *js, xmlnode *packet, xmlnode **reply, char **error) { char *enc_in = xmlnode_get_data(packet); unsigned char *dec_in; char *enc_out; const char *c_out; unsigned int clen; gsize declen; 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)); *error = tmp; return JABBER_SASL_STATE_FAIL; } else { xmlnode *response = xmlnode_new("response"); xmlnode_set_namespace(response, 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); } *reply = response; return JABBER_SASL_STATE_CONTINUE; } } static JabberSaslState jabber_cyrus_handle_success(JabberStream *js, xmlnode *packet, char **error) { 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! */ *error = g_strdup(_("Invalid response from server")); g_return_val_if_reached(JABBER_SASL_STATE_FAIL); } } /* 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 JABBER_SASL_STATE_OK; } static JabberSaslState jabber_cyrus_handle_failure(JabberStream *js, xmlnode *packet, xmlnode **reply, char **error) { 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, reply, error); } else if ((js->auth_fail_count == 1) && (js->current_mech && g_str_equal(js->current_mech, "GSSAPI"))) { /* If we tried GSSAPI first, it failed, and it was the only method we had to try, try jabber:iq:auth * for compatibility with iChat 10.5 Server. * * iChat Server 10.5 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 */ sasl_dispose(&js->sasl); js->sasl = NULL; js->auth_mech = NULL; jabber_auth_start_old(js); return JABBER_SASL_STATE_CONTINUE; } } /* Nothing to send */ return JABBER_SASL_STATE_FAIL; } 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; }