Migrate certificates, icons, logs to XDG dirs
[pidgin-git.git] / libpurple / protocols / jabber / auth_cyrus.c
blob4e6d2539687437c52dd463410869c9058a2f53ca
1 /*
2 * purple - Jabber Protocol Plugin
4 * Purple is the legal property of its developers, whose names are too numerous
5 * to list here. Please refer to the COPYRIGHT file distributed with this
6 * source distribution.
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
23 #include "internal.h"
24 #include "core.h"
25 #include "debug.h"
26 #include "request.h"
28 #include "auth.h"
29 #include "jabber.h"
31 static JabberSaslState jabber_auth_start_cyrus(JabberStream *js, PurpleXmlNode **reply,
32 char **error);
33 static void jabber_sasl_build_callbacks(JabberStream *);
35 static void disallow_plaintext_auth(PurpleAccount *account)
37 purple_connection_error(purple_account_get_connection(account),
38 PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR,
39 _("Server may require plaintext authentication over an unencrypted stream"));
42 static void start_cyrus_wrapper(JabberStream *js)
44 char *error = NULL;
45 PurpleXmlNode *response = NULL;
46 JabberSaslState state = jabber_auth_start_cyrus(js, &response, &error);
48 if (state == JABBER_SASL_STATE_FAIL) {
49 purple_connection_error(js->gc,
50 PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
51 error);
52 g_free(error);
53 } else if (response) {
54 jabber_send(js, response);
55 purple_xmlnode_free(response);
60 /* Callbacks for Cyrus SASL */
62 static int jabber_sasl_cb_realm(void *ctx, int id, const char **avail, const char **result)
64 JabberStream *js = ctx;
66 if (id != SASL_CB_GETREALM || !result) return SASL_BADPARAM;
68 *result = js->user->domain;
70 return SASL_OK;
73 static int jabber_sasl_cb_simple(void *ctx, int id, const char **res, unsigned *len)
75 JabberStream *js = ctx;
77 switch(id) {
78 case SASL_CB_AUTHNAME:
79 *res = js->user->node;
80 break;
81 case SASL_CB_USER:
82 *res = "";
83 break;
84 default:
85 return SASL_BADPARAM;
87 if (len) *len = strlen((char *)*res);
88 return SASL_OK;
91 static int jabber_sasl_cb_secret(sasl_conn_t *conn, void *ctx, int id, sasl_secret_t **secret)
93 JabberStream *js = ctx;
94 size_t len;
96 if (!conn || !secret || id != SASL_CB_PASS)
97 return SASL_BADPARAM;
99 len = strlen(js->sasl_password);
100 /* Not an off-by-one because sasl_secret_t defines char data[1] */
101 /* TODO: This can probably be moved to glib's allocator */
102 js->sasl_secret = malloc(sizeof(sasl_secret_t) + len);
103 if (!js->sasl_secret)
104 return SASL_NOMEM;
106 js->sasl_secret->len = len;
107 strcpy((char*)js->sasl_secret->data, js->sasl_password);
109 *secret = js->sasl_secret;
110 return SASL_OK;
113 static void allow_cyrus_plaintext_auth(PurpleAccount *account)
115 PurpleConnection *gc;
116 JabberStream *js;
118 gc = purple_account_get_connection(account);
119 js = purple_connection_get_protocol_data(gc);
121 purple_account_set_bool(account, "auth_plain_in_clear", TRUE);
123 start_cyrus_wrapper(js);
126 static void auth_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields)
128 PurpleAccount *account;
129 JabberStream *js;
130 const char *entry;
131 gboolean remember;
133 /* TODO: the password prompt dialog doesn't get disposed if the account disconnects */
134 PURPLE_ASSERT_CONNECTION_IS_VALID(gc);
136 account = purple_connection_get_account(gc);
137 js = purple_connection_get_protocol_data(gc);
139 entry = purple_request_fields_get_string(fields, "password");
140 remember = purple_request_fields_get_bool(fields, "remember");
142 if (!entry || !*entry)
144 purple_notify_error(account, NULL,
145 _("Password is required to sign on."), NULL,
146 purple_request_cpar_from_connection(gc));
147 return;
150 if (remember)
151 purple_account_set_remember_password(account, TRUE);
153 purple_account_set_password(account, entry, NULL, NULL);
155 js->sasl_password = g_strdup(entry);
157 /* Rebuild our callbacks as we now have a password to offer */
158 jabber_sasl_build_callbacks(js);
160 /* Restart our negotiation */
161 start_cyrus_wrapper(js);
164 static void
165 auth_no_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields)
167 PurpleAccount *account;
169 /* TODO: the password prompt dialog doesn't get disposed if the account disconnects */
170 PURPLE_ASSERT_CONNECTION_IS_VALID(gc);
172 account = purple_connection_get_account(gc);
174 /* Disable the account as the user has cancelled connecting */
175 purple_account_set_enabled(account, purple_core_get_ui(), FALSE);
178 static gboolean remove_current_mech(JabberStream *js) {
179 char *pos;
180 if ((pos = strstr(js->sasl_mechs->str, js->current_mech))) {
181 size_t len = strlen(js->current_mech);
182 /* Clean up space that separated this Mech from the one before or after it */
183 if (pos > js->sasl_mechs->str && *(pos - 1) == ' ') {
184 /* Handle removing space before when current_mech isn't the first mech in the list */
185 pos--;
186 len++;
187 } else if (strlen(pos) > len && *(pos + len) == ' ') {
188 /* Handle removing space after */
189 len++;
191 g_string_erase(js->sasl_mechs, pos - js->sasl_mechs->str, len);
192 return TRUE;
194 return FALSE;
197 static JabberSaslState
198 jabber_auth_start_cyrus(JabberStream *js, PurpleXmlNode **reply, char **error)
200 PurpleAccount *account;
201 const char *clientout = NULL;
202 char *enc_out;
203 unsigned coutlen = 0;
204 sasl_security_properties_t secprops;
205 gboolean again;
206 gboolean plaintext = TRUE;
208 /* Set up security properties and options */
209 secprops.min_ssf = 0;
210 secprops.security_flags = SASL_SEC_NOANONYMOUS;
212 account = purple_connection_get_account(js->gc);
214 if (!jabber_stream_is_ssl(js)) {
215 secprops.max_ssf = -1;
216 secprops.maxbufsize = 4096;
217 plaintext = purple_account_get_bool(account, "auth_plain_in_clear", FALSE);
218 if (!plaintext)
219 secprops.security_flags |= SASL_SEC_NOPLAINTEXT;
220 } else {
221 secprops.max_ssf = 0;
222 secprops.maxbufsize = 0;
223 plaintext = TRUE;
225 secprops.property_names = 0;
226 secprops.property_values = 0;
228 do {
229 again = FALSE;
231 js->sasl_state = sasl_client_new("xmpp", js->serverFQDN, NULL, NULL, js->sasl_cb, 0, &js->sasl);
232 if (js->sasl_state==SASL_OK) {
233 sasl_setprop(js->sasl, SASL_SEC_PROPS, &secprops);
234 purple_debug_info("sasl", "Mechs found: %s\n", js->sasl_mechs->str);
235 js->sasl_state = sasl_client_start(js->sasl, js->sasl_mechs->str, NULL, &clientout, &coutlen, &js->current_mech);
237 switch (js->sasl_state) {
238 /* Success */
239 case SASL_OK:
240 case SASL_CONTINUE:
241 break;
242 case SASL_NOMECH:
243 /* No mechanisms have offered to help */
245 /* Firstly, if we don't have a password try
246 * to get one
249 if (!js->sasl_password) {
250 purple_account_request_password(account, G_CALLBACK(auth_pass_cb), G_CALLBACK(auth_no_pass_cb), js->gc);
251 return JABBER_SASL_STATE_CONTINUE;
253 /* If we've got a password, but aren't sending
254 * it in plaintext, see if we can turn on
255 * plaintext auth
257 /* XXX Should we just check for PLAIN/LOGIN being offered mechanisms? */
258 } else if (!plaintext) {
259 char *msg = g_strdup_printf(_("%s may require plaintext authentication over an unencrypted connection. Allow this and continue authentication?"),
260 purple_account_get_username(account));
261 purple_request_yes_no(js->gc, _("Plaintext Authentication"),
262 _("Plaintext Authentication"),
263 msg,
264 1, purple_request_cpar_from_account(account), account,
265 allow_cyrus_plaintext_auth,
266 disallow_plaintext_auth);
267 g_free(msg);
268 return JABBER_SASL_STATE_CONTINUE;
270 } else
271 js->auth_fail_count++;
273 if (js->auth_fail_count == 1 &&
274 (js->sasl_mechs->str && g_str_equal(js->sasl_mechs->str, "GSSAPI"))) {
275 /* If we tried GSSAPI first, it failed, and it was the only method we had to try, try jabber:iq:auth
276 * for compatibility with iChat 10.5 Server and other jabberd based servers.
278 * iChat Server 10.5 and certain other corporate servers offer SASL GSSAPI by default, which is often
279 * not configured on the client side, and expects a fallback to jabber:iq:auth when it (predictably) fails.
281 * Note: xep-0078 points out that using jabber:iq:auth after a sasl failure is wrong. However,
282 * I believe this refers to actual authentication failure, not a simple lack of concordant mechanisms.
283 * Doing otherwise means that simply compiling with SASL support renders the client unable to connect to servers
284 * which would connect without issue otherwise. -evands
286 js->auth_mech = NULL;
287 jabber_auth_start_old(js);
288 return JABBER_SASL_STATE_CONTINUE;
291 break;
293 /* Fatal errors. Give up and go home */
294 case SASL_BADPARAM:
295 case SASL_NOMEM:
296 *error = g_strdup(_("SASL authentication failed"));
297 break;
299 /* For everything else, fail the mechanism and try again */
300 default:
301 purple_debug_info("sasl", "sasl_state is %d, failing the mech and trying again\n", js->sasl_state);
303 js->auth_fail_count++;
306 * DAA: is this right?
307 * The manpage says that "mech" will contain the chosen mechanism on success.
308 * Presumably, if we get here that isn't the case and we shouldn't try again?
309 * I suspect that this never happens.
312 * SXW: Yes, this is right. What this handles is the situation where a
313 * mechanism, say GSSAPI, is tried. If that mechanism fails, it may be
314 * due to mechanism specific issues, so we want to try one of the other
315 * supported mechanisms. This code handles that case
317 if (js->current_mech && *js->current_mech) {
318 remove_current_mech(js);
319 /* Should we only try again if we've removed the mech? */
320 again = TRUE;
323 sasl_dispose(&js->sasl);
325 } while (again);
327 if (js->sasl_state == SASL_CONTINUE || js->sasl_state == SASL_OK) {
328 PurpleXmlNode *auth = purple_xmlnode_new("auth");
329 purple_xmlnode_set_namespace(auth, NS_XMPP_SASL);
330 purple_xmlnode_set_attrib(auth, "mechanism", js->current_mech);
332 purple_xmlnode_set_attrib(auth, "xmlns:ga", "http://www.google.com/talk/protocol/auth");
333 purple_xmlnode_set_attrib(auth, "ga:client-uses-full-bind-result", "true");
335 if (clientout) {
336 if (coutlen == 0) {
337 purple_xmlnode_insert_data(auth, "=", -1);
338 } else {
339 enc_out = purple_base64_encode((unsigned char*)clientout, coutlen);
340 purple_xmlnode_insert_data(auth, enc_out, -1);
341 g_free(enc_out);
345 *reply = auth;
346 return JABBER_SASL_STATE_CONTINUE;
347 } else {
348 return JABBER_SASL_STATE_FAIL;
352 static int
353 jabber_sasl_cb_log(void *context, int level, const char *message)
355 if(level <= SASL_LOG_TRACE)
356 purple_debug_info("sasl", "%s\n", message);
358 return SASL_OK;
361 static void
362 jabber_sasl_build_callbacks(JabberStream *js)
364 int id;
366 /* Set up our callbacks structure */
367 if (js->sasl_cb == NULL)
368 js->sasl_cb = g_new0(sasl_callback_t,6);
370 id = 0;
371 js->sasl_cb[id].id = SASL_CB_GETREALM;
372 js->sasl_cb[id].proc = (void *)jabber_sasl_cb_realm;
373 js->sasl_cb[id].context = (void *)js;
374 id++;
376 js->sasl_cb[id].id = SASL_CB_AUTHNAME;
377 js->sasl_cb[id].proc = (void *)jabber_sasl_cb_simple;
378 js->sasl_cb[id].context = (void *)js;
379 id++;
381 js->sasl_cb[id].id = SASL_CB_USER;
382 js->sasl_cb[id].proc = (void *)jabber_sasl_cb_simple;
383 js->sasl_cb[id].context = (void *)js;
384 id++;
386 if (js->sasl_password != NULL) {
387 js->sasl_cb[id].id = SASL_CB_PASS;
388 js->sasl_cb[id].proc = (void *)jabber_sasl_cb_secret;
389 js->sasl_cb[id].context = (void *)js;
390 id++;
393 js->sasl_cb[id].id = SASL_CB_LOG;
394 js->sasl_cb[id].proc = (void *)jabber_sasl_cb_log;
395 js->sasl_cb[id].context = (void*)js;
396 id++;
398 js->sasl_cb[id].id = SASL_CB_LIST_END;
401 static JabberSaslState
402 jabber_cyrus_start(JabberStream *js, PurpleXmlNode *mechanisms,
403 PurpleXmlNode **reply, char **error)
405 PurpleXmlNode *mechnode;
406 JabberSaslState ret;
408 js->sasl_mechs = g_string_new("");
409 js->sasl_password = g_strdup(purple_connection_get_password(js->gc));
411 for(mechnode = purple_xmlnode_get_child(mechanisms, "mechanism"); mechnode;
412 mechnode = purple_xmlnode_get_next_twin(mechnode))
414 char *mech_name = purple_xmlnode_get_data(mechnode);
416 /* Ignore blank mechanisms and EXTERNAL. External isn't
417 * supported, and Cyrus SASL's mechanism returns
418 * SASL_NOMECH when the caller (us) doesn't configure it.
419 * Except SASL_NOMECH is supposed to mean "no concordant
420 * mechanisms"... Easiest just to blacklist it (for now).
422 if (!mech_name || !*mech_name ||
423 g_str_equal(mech_name, "EXTERNAL")) {
424 g_free(mech_name);
425 continue;
428 g_string_append(js->sasl_mechs, mech_name);
429 g_string_append_c(js->sasl_mechs, ' ');
430 g_free(mech_name);
433 /* Strip off the trailing ' ' */
434 if (js->sasl_mechs->len > 1)
435 g_string_truncate(js->sasl_mechs, js->sasl_mechs->len - 1);
437 jabber_sasl_build_callbacks(js);
438 ret = jabber_auth_start_cyrus(js, reply, error);
441 * Triggered if no overlap between server and client
442 * supported mechanisms.
444 if (ret == JABBER_SASL_STATE_FAIL && *error == NULL)
445 *error = g_strdup(_("Server does not use any supported authentication method"));
447 return ret;
450 static JabberSaslState
451 jabber_cyrus_handle_challenge(JabberStream *js, PurpleXmlNode *packet,
452 PurpleXmlNode **reply, char **error)
454 char *enc_in = purple_xmlnode_get_data(packet);
455 unsigned char *dec_in;
456 char *enc_out;
457 const char *c_out;
458 unsigned int clen;
459 gsize declen;
461 dec_in = purple_base64_decode(enc_in, &declen);
463 js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen,
464 NULL, &c_out, &clen);
465 g_free(enc_in);
466 g_free(dec_in);
467 if (js->sasl_state != SASL_CONTINUE && js->sasl_state != SASL_OK) {
468 gchar *tmp = g_strdup_printf(_("SASL error: %s"),
469 sasl_errdetail(js->sasl));
470 purple_debug_error("jabber", "Error is %d : %s\n",
471 js->sasl_state, sasl_errdetail(js->sasl));
472 *error = tmp;
473 return JABBER_SASL_STATE_FAIL;
474 } else {
475 PurpleXmlNode *response = purple_xmlnode_new("response");
476 purple_xmlnode_set_namespace(response, NS_XMPP_SASL);
477 if (clen > 0) {
478 /* Cyrus SASL 2.1.22 appears to contain code to add the charset
479 * to the response for DIGEST-MD5 but there is no possibility
480 * it will be executed.
482 * My reading of the digestmd5 plugin indicates the username and
483 * realm are always encoded in UTF-8 (they seem to be the values
484 * we pass in), so we need to ensure charset=utf-8 is set.
486 if (!purple_strequal(js->current_mech, "DIGEST-MD5") ||
487 strstr(c_out, ",charset="))
488 /* If we're not using DIGEST-MD5 or Cyrus SASL is fixed */
489 enc_out = purple_base64_encode((unsigned char*)c_out, clen);
490 else {
491 char *tmp = g_strdup_printf("%s,charset=utf-8", c_out);
492 enc_out = purple_base64_encode((unsigned char*)tmp, clen + 14);
493 g_free(tmp);
496 purple_xmlnode_insert_data(response, enc_out, -1);
497 g_free(enc_out);
500 *reply = response;
501 return JABBER_SASL_STATE_CONTINUE;
505 static JabberSaslState
506 jabber_cyrus_handle_success(JabberStream *js, PurpleXmlNode *packet,
507 char **error)
509 const void *x;
511 /* The SASL docs say that if the client hasn't returned OK yet, we
512 * should try one more round against it
514 if (js->sasl_state != SASL_OK) {
515 char *enc_in = purple_xmlnode_get_data(packet);
516 unsigned char *dec_in = NULL;
517 const char *c_out;
518 unsigned int clen;
519 gsize declen = 0;
521 if(enc_in != NULL)
522 dec_in = purple_base64_decode(enc_in, &declen);
524 js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, NULL, &c_out, &clen);
526 g_free(enc_in);
527 g_free(dec_in);
529 if (js->sasl_state != SASL_OK) {
530 /* This happens when the server sends back jibberish
531 * in the "additional data with success" case.
532 * Seen with Wildfire 3.0.1.
534 *error = g_strdup(_("Invalid response from server"));
535 return JABBER_SASL_STATE_FAIL;
539 /* If we've negotiated a security layer, we need to enable it */
540 if (js->sasl) {
541 sasl_getprop(js->sasl, SASL_SSF, &x);
542 if (*(int *)x > 0) {
543 sasl_getprop(js->sasl, SASL_MAXOUTBUF, &x);
544 js->sasl_maxbuf = *(int *)x;
548 return JABBER_SASL_STATE_OK;
551 static JabberSaslState
552 jabber_cyrus_handle_failure(JabberStream *js, PurpleXmlNode *packet,
553 PurpleXmlNode **reply, char **error)
555 if (js->auth_fail_count++ < 5) {
556 if (js->current_mech && *js->current_mech) {
557 remove_current_mech(js);
560 /* Should we only try again if we've actually removed a mech? */
561 if (*js->sasl_mechs->str) {
562 /* If we have remaining mechs to try, do so */
563 sasl_dispose(&js->sasl);
565 return jabber_auth_start_cyrus(js, reply, error);
567 } else if ((js->auth_fail_count == 1) &&
568 (js->current_mech && g_str_equal(js->current_mech, "GSSAPI"))) {
569 /* If we tried GSSAPI first, it failed, and it was the only method we had to try, try jabber:iq:auth
570 * for compatibility with iChat 10.5 Server and other jabberd based servers.
572 * iChat Server 10.5 and certain other corporate servers offer SASL GSSAPI by default, which is often
573 * not configured on the client side, and expects a fallback to jabber:iq:auth when it (predictably) fails.
575 * Note: xep-0078 points out that using jabber:iq:auth after a sasl failure is wrong. However,
576 * I believe this refers to actual authentication failure, not a simple lack of concordant mechanisms.
577 * Doing otherwise means that simply compiling with SASL support renders the client unable to connect to servers
578 * which would connect without issue otherwise. -evands
580 sasl_dispose(&js->sasl);
581 js->sasl = NULL;
582 js->auth_mech = NULL;
583 jabber_auth_start_old(js);
584 return JABBER_SASL_STATE_CONTINUE;
588 /* Nothing to send */
589 return JABBER_SASL_STATE_FAIL;
592 static JabberSaslMech cyrus_mech = {
593 100, /* priority */
594 "*", /* name; Cyrus provides a bunch of mechanisms, so use an invalid
595 * mechanism name (per rfc4422 3.1). */
596 jabber_cyrus_start,
597 jabber_cyrus_handle_challenge,
598 jabber_cyrus_handle_success,
599 jabber_cyrus_handle_failure,
600 NULL,
603 JabberSaslMech *jabber_auth_get_cyrus_mech(void)
605 return &cyrus_mech;