rename accountopt.[ch] to purpleaccountoption.[ch]
[pidgin-git.git] / libpurple / protocols / oscar / clientlogin.c
blob8c9e393e5fdd7df8196b97d038f75f8f2f765c07
1 /*
2 * Purple's oscar protocol plugin
3 * This file is the legal property of its developers.
4 * Please see the AUTHORS file distributed alongside this file.
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
21 /**
22 * This file implements AIM's clientLogin procedure for authenticating
23 * users. This replaces the older MD5-based and XOR-based
24 * authentication methods that use SNAC family 0x0017.
26 * This doesn't use SNACs or FLAPs at all. It makes http and https
27 * POSTs to AOL to validate the user based on the password they
28 * provided to us. Upon successful authentication we request a
29 * connection to the BOS server by calling startOSCARsession. The
30 * AOL server gives us the hostname and port number to use, as well
31 * as the cookie to use to authenticate to the BOS server. And then
32 * everything else is the same as with BUCP.
34 * For details, see:
35 * http://dev.aol.com/aim/oscar/#AUTH
36 * http://dev.aol.com/authentication_for_clients
39 #include "oscar.h"
40 #include "oscarcommon.h"
41 #include "core.h"
43 #define AIM_LOGIN_HOST "api.screenname.aol.com"
44 #define ICQ_LOGIN_HOST "api.login.icq.net"
46 #define AIM_API_HOST "api.oscar.aol.com"
47 #define ICQ_API_HOST "api.icq.net"
49 #define CLIENT_LOGIN_PAGE "/auth/clientLogin"
50 #define START_OSCAR_SESSION_PAGE "/aim/startOSCARSession"
52 #define HTTPS_FORMAT_URL(host, page) "https://" host page
54 static const gchar *client_login_urls[] = {
55 HTTPS_FORMAT_URL(AIM_LOGIN_HOST, CLIENT_LOGIN_PAGE),
56 HTTPS_FORMAT_URL(ICQ_LOGIN_HOST, CLIENT_LOGIN_PAGE),
59 static const gchar *start_oscar_session_urls[] = {
60 HTTPS_FORMAT_URL(AIM_API_HOST, START_OSCAR_SESSION_PAGE),
61 HTTPS_FORMAT_URL(ICQ_API_HOST, START_OSCAR_SESSION_PAGE),
64 static const gchar *get_client_login_url(OscarData *od)
66 return client_login_urls[od->icq ? 1 : 0];
69 static const gchar *get_start_oscar_session_url(OscarData *od)
71 return start_oscar_session_urls[od->icq ? 1 : 0];
74 static const char *get_client_key(OscarData *od)
76 return oscar_get_ui_info_string(
77 od->icq ? "prpl-icq-clientkey" : "prpl-aim-clientkey",
78 od->icq ? ICQ_DEFAULT_CLIENT_KEY : AIM_DEFAULT_CLIENT_KEY);
81 static gchar *generate_error_message(PurpleXmlNode *resp, const char *url)
83 PurpleXmlNode *text;
84 PurpleXmlNode *status_code_node;
85 gboolean have_error_code = TRUE;
86 gchar *err = NULL;
87 gchar *details = NULL;
89 status_code_node = purple_xmlnode_get_child(resp, "statusCode");
90 if (status_code_node) {
91 gchar *status_code;
93 /* We can get 200 OK here if the server omitted something we think it shouldn't have (see #12783).
94 * No point in showing the "Ok" string to the user.
96 status_code = purple_xmlnode_get_data_unescaped(status_code_node);
97 if (purple_strequal(status_code, "200")) {
98 have_error_code = FALSE;
101 if (have_error_code && resp && (text = purple_xmlnode_get_child(resp, "statusText"))) {
102 details = purple_xmlnode_get_data(text);
105 if (details && *details) {
106 /* Translators: The first %s is a URL. The second is a brief error
107 message. */
108 err = g_strdup_printf(_("Received unexpected response from %s: %s"), url, details);
109 } else {
110 /* Translators: %s in this string is a URL */
111 err = g_strdup_printf(_("Received unexpected response from %s"), url);
114 g_free(details);
115 return err;
119 * @return A null-terminated base64 encoded version of the HMAC
120 * calculated using the given key and data.
122 static gchar *hmac_sha256(const char *key, const char *message)
124 GHmac *hmac;
125 guchar digest[32];
126 gsize digest_len = 32;
128 hmac = g_hmac_new(G_CHECKSUM_SHA256, (guchar *)key, strlen(key));
129 g_hmac_update(hmac, (guchar *)message, -1);
130 g_hmac_get_digest(hmac, digest, &digest_len);
131 g_hmac_unref(hmac);
133 return g_base64_encode(digest, sizeof(digest));
137 * @return A base-64 encoded HMAC-SHA256 signature created using the
138 * technique documented at
139 * http://dev.aol.com/authentication_for_clients#signing
141 static gchar *generate_signature(const char *method, const char *url, const char *parameters, const char *session_key)
143 char *encoded_url, *signature_base_string, *signature;
144 const char *encoded_parameters;
146 encoded_url = g_strdup(purple_url_encode(url));
147 encoded_parameters = purple_url_encode(parameters);
148 signature_base_string = g_strdup_printf("%s&%s&%s",
149 method, encoded_url, encoded_parameters);
150 g_free(encoded_url);
152 signature = hmac_sha256(session_key, signature_base_string);
153 g_free(signature_base_string);
155 return signature;
158 static gboolean parse_start_oscar_session_response(PurpleConnection *gc, const gchar *response, gsize response_len, char **host, unsigned short *port, char **cookie, char **tls_certname)
160 OscarData *od = purple_connection_get_protocol_data(gc);
161 PurpleXmlNode *response_node, *tmp_node, *data_node;
162 PurpleXmlNode *host_node = NULL, *port_node = NULL, *cookie_node = NULL, *tls_node = NULL;
163 char *tmp;
164 guint code;
165 const gchar *encryption_type = purple_account_get_string(purple_connection_get_account(gc), "encryption", OSCAR_DEFAULT_ENCRYPTION);
167 /* Parse the response as XML */
168 response_node = purple_xmlnode_from_str(response, response_len);
169 if (response_node == NULL)
171 char *msg;
172 purple_debug_error("oscar", "startOSCARSession could not parse "
173 "response as XML: %s\n", response);
174 msg = generate_error_message(response_node,
175 get_start_oscar_session_url(od));
176 purple_connection_error(gc,
177 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, msg);
178 g_free(msg);
179 return FALSE;
182 /* Grab the necessary XML nodes */
183 tmp_node = purple_xmlnode_get_child(response_node, "statusCode");
184 data_node = purple_xmlnode_get_child(response_node, "data");
185 if (data_node != NULL) {
186 host_node = purple_xmlnode_get_child(data_node, "host");
187 port_node = purple_xmlnode_get_child(data_node, "port");
188 cookie_node = purple_xmlnode_get_child(data_node, "cookie");
191 /* Make sure we have a status code */
192 if (tmp_node == NULL || (tmp = purple_xmlnode_get_data_unescaped(tmp_node)) == NULL) {
193 char *msg;
194 purple_debug_error("oscar", "startOSCARSession response was "
195 "missing statusCode: %s\n", response);
196 msg = generate_error_message(response_node,
197 get_start_oscar_session_url(od));
198 purple_connection_error(gc,
199 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, msg);
200 g_free(msg);
201 purple_xmlnode_free(response_node);
202 return FALSE;
205 /* Make sure the status code was 200 */
206 code = atoi(tmp);
207 if (code != 200)
209 PurpleXmlNode *status_detail_node;
210 guint status_detail = 0;
212 status_detail_node = purple_xmlnode_get_child(response_node,
213 "statusDetailCode");
214 if (status_detail_node) {
215 gchar *data = purple_xmlnode_get_data(status_detail_node);
216 if (data) {
217 status_detail = atoi(data);
218 g_free(data);
222 purple_debug_error("oscar", "startOSCARSession response statusCode "
223 "was %s: %s\n", tmp, response);
225 if ((code == 401 && status_detail != 1014) || code == 607)
226 purple_connection_error(gc,
227 PURPLE_CONNECTION_ERROR_OTHER_ERROR,
228 _("You have been connecting and disconnecting too "
229 "frequently. Wait ten minutes and try again. If "
230 "you continue to try, you will need to wait even "
231 "longer."));
232 else {
233 char *msg;
234 msg = generate_error_message(response_node,
235 get_start_oscar_session_url(od));
236 purple_connection_error(gc,
237 PURPLE_CONNECTION_ERROR_OTHER_ERROR, msg);
238 g_free(msg);
241 g_free(tmp);
242 purple_xmlnode_free(response_node);
243 return FALSE;
245 g_free(tmp);
247 /* Make sure we have everything else */
248 if (data_node == NULL || host_node == NULL || port_node == NULL || cookie_node == NULL)
250 char *msg;
251 purple_debug_error("oscar", "startOSCARSession response was missing "
252 "something: %s\n", response);
253 msg = generate_error_message(response_node,
254 get_start_oscar_session_url(od));
255 purple_connection_error(gc,
256 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, msg);
257 g_free(msg);
258 purple_xmlnode_free(response_node);
259 return FALSE;
262 if (!purple_strequal(encryption_type, OSCAR_NO_ENCRYPTION)) {
263 tls_node = purple_xmlnode_get_child(data_node, "tlsCertName");
264 if (tls_node != NULL) {
265 *tls_certname = purple_xmlnode_get_data_unescaped(tls_node);
266 } else {
267 if (purple_strequal(encryption_type, OSCAR_OPPORTUNISTIC_ENCRYPTION)) {
268 purple_debug_warning("oscar", "We haven't received a tlsCertName to use. We will not do SSL to BOS.\n");
269 } else {
270 purple_debug_error("oscar", "startOSCARSession was missing tlsCertName: %s\n", response);
271 purple_connection_error(
273 PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT,
274 _("You required encryption in your account settings, but one of the servers doesn't support it."));
275 purple_xmlnode_free(response_node);
276 return FALSE;
281 /* Extract data from the XML */
282 *host = purple_xmlnode_get_data_unescaped(host_node);
283 tmp = purple_xmlnode_get_data_unescaped(port_node);
284 *cookie = purple_xmlnode_get_data_unescaped(cookie_node);
286 if (*host == NULL || **host == '\0' || tmp == NULL || *tmp == '\0' || *cookie == NULL || **cookie == '\0')
288 char *msg;
289 purple_debug_error("oscar", "startOSCARSession response was missing "
290 "something: %s\n", response);
291 msg = generate_error_message(response_node,
292 get_start_oscar_session_url(od));
293 purple_connection_error(gc,
294 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, msg);
295 g_free(msg);
296 g_free(*host);
297 g_free(tmp);
298 g_free(*cookie);
299 purple_xmlnode_free(response_node);
300 return FALSE;
303 *port = atoi(tmp);
304 g_free(tmp);
306 return TRUE;
309 static void
310 start_oscar_session_cb(PurpleHttpConnection *http_conn,
311 PurpleHttpResponse *response, gpointer _od)
313 OscarData *od = _od;
314 PurpleConnection *gc;
315 char *host, *cookie;
316 char *tls_certname = NULL;
317 unsigned short port;
318 guint8 *cookiedata;
319 gsize cookiedata_len = 0;
320 const gchar *got_data;
321 size_t got_len;
323 gc = od->gc;
325 od->hc = NULL;
327 if (!purple_http_response_is_successful(response)) {
328 gchar *tmp;
329 /* Translators: The first %s is a URL, the second is a brief error
330 message. */
331 tmp = g_strdup_printf(_("Error requesting %s: %s"),
332 get_start_oscar_session_url(od),
333 purple_http_response_get_error(response));
334 purple_connection_error(gc,
335 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
336 g_free(tmp);
337 return;
340 got_data = purple_http_response_get_data(response, &got_len);
341 if (!parse_start_oscar_session_response(gc, got_data, got_len, &host, &port, &cookie, &tls_certname))
342 return;
344 cookiedata = g_base64_decode(cookie, &cookiedata_len);
345 oscar_connect_to_bos(gc, od, host, port, cookiedata, cookiedata_len, tls_certname);
346 g_free(cookiedata);
348 g_free(host);
349 g_free(cookie);
350 g_free(tls_certname);
353 static void send_start_oscar_session(OscarData *od, const char *token, const char *session_key, time_t hosttime)
355 char *query_string, *signature;
356 PurpleAccount *account = purple_connection_get_account(od->gc);
357 const gchar *encryption_type = purple_account_get_string(account, "encryption", OSCAR_DEFAULT_ENCRYPTION);
360 * Construct the GET parameters.
362 query_string = g_strdup_printf("a=%s"
363 "&distId=%d"
364 "&f=xml"
365 "&k=%s"
366 "&ts=%" G_GINT64_FORMAT
367 "&useTLS=%d",
368 purple_url_encode(token),
369 oscar_get_ui_info_int(od->icq ? "prpl-icq-distid" : "prpl-aim-distid",
370 od->icq ? ICQ_DEFAULT_DIST_ID : AIM_DEFAULT_DIST_ID),
371 get_client_key(od),
372 (gint64)hosttime,
373 !purple_strequal(encryption_type, OSCAR_NO_ENCRYPTION));
374 signature = generate_signature("GET", get_start_oscar_session_url(od),
375 query_string, session_key);
377 od->hc = purple_http_get_printf(od->gc, start_oscar_session_cb, od,
378 "%s?%s&sig_sha256=%s", get_start_oscar_session_url(od),
379 query_string, signature);
381 g_free(query_string);
382 g_free(signature);
386 * This function parses the given response from a clientLogin request
387 * and extracts the useful information.
389 * @param gc The PurpleConnection. If the response data does
390 * not indicate then purple_connection_error()
391 * will be called to close this connection.
392 * @param response The response data from the clientLogin request.
393 * @param response_len The length of the above response, or -1 if
394 * @response is NUL terminated.
395 * @param token If parsing was successful then this will be set to
396 * a newly allocated string containing the token. The
397 * caller should g_free this string when it is finished
398 * with it. On failure this value will be untouched.
399 * @param secret If parsing was successful then this will be set to
400 * a newly allocated string containing the secret. The
401 * caller should g_free this string when it is finished
402 * with it. On failure this value will be untouched.
403 * @param hosttime If parsing was successful then this will be set to
404 * the time on the OpenAuth Server in seconds since the
405 * Unix epoch. On failure this value will be untouched.
407 * @return TRUE if the request was successful and we were able to
408 * extract all info we need. Otherwise FALSE.
410 static gboolean parse_client_login_response(PurpleConnection *gc, const gchar *response, gsize response_len, char **token, char **secret, time_t *hosttime)
412 OscarData *od = purple_connection_get_protocol_data(gc);
413 PurpleXmlNode *response_node, *tmp_node, *data_node;
414 PurpleXmlNode *secret_node = NULL, *hosttime_node = NULL, *token_node = NULL, *tokena_node = NULL;
415 char *tmp;
417 /* Parse the response as XML */
418 response_node = purple_xmlnode_from_str(response, response_len);
419 if (response_node == NULL)
421 char *msg;
422 purple_debug_error("oscar", "clientLogin could not parse "
423 "response as XML: %s\n", response);
424 msg = generate_error_message(response_node,
425 get_client_login_url(od));
426 purple_connection_error(gc,
427 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, msg);
428 g_free(msg);
429 return FALSE;
432 /* Grab the necessary XML nodes */
433 tmp_node = purple_xmlnode_get_child(response_node, "statusCode");
434 data_node = purple_xmlnode_get_child(response_node, "data");
435 if (data_node != NULL) {
436 secret_node = purple_xmlnode_get_child(data_node, "sessionSecret");
437 hosttime_node = purple_xmlnode_get_child(data_node, "hostTime");
438 token_node = purple_xmlnode_get_child(data_node, "token");
439 if (token_node != NULL)
440 tokena_node = purple_xmlnode_get_child(token_node, "a");
443 /* Make sure we have a status code */
444 if (tmp_node == NULL || (tmp = purple_xmlnode_get_data_unescaped(tmp_node)) == NULL) {
445 char *msg;
446 purple_debug_error("oscar", "clientLogin response was "
447 "missing statusCode: %s\n", response);
448 msg = generate_error_message(response_node,
449 get_client_login_url(od));
450 purple_connection_error(gc,
451 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, msg);
452 g_free(msg);
453 purple_xmlnode_free(response_node);
454 return FALSE;
457 /* Make sure the status code was 200 */
458 if (!purple_strequal(tmp, "200"))
460 int status_code, status_detail_code = 0;
462 status_code = atoi(tmp);
463 g_free(tmp);
464 tmp_node = purple_xmlnode_get_child(response_node, "statusDetailCode");
465 if (tmp_node != NULL && (tmp = purple_xmlnode_get_data_unescaped(tmp_node)) != NULL) {
466 status_detail_code = atoi(tmp);
467 g_free(tmp);
470 purple_debug_error("oscar", "clientLogin response statusCode "
471 "was %d (%d): %s\n", status_code, status_detail_code, response);
473 if (status_code == 330 && status_detail_code == 3011) {
474 PurpleAccount *account = purple_connection_get_account(gc);
475 if (!purple_account_get_remember_password(account))
476 purple_account_set_password(account, NULL, NULL, NULL);
477 purple_connection_error(gc,
478 PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED,
479 _("Incorrect password"));
480 } else if (status_code == 330 && status_detail_code == 3015) {
481 purple_connection_error(gc,
482 PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED,
483 _("Server requested that you fill out a CAPTCHA in order to "
484 "sign in, but this client does not currently support CAPTCHAs."));
485 } else if (status_code == 401 && status_detail_code == 3019) {
486 purple_connection_error(gc,
487 PURPLE_CONNECTION_ERROR_OTHER_ERROR,
488 _("AOL does not allow your screen name to authenticate here"));
489 } else {
490 char *msg;
491 msg = generate_error_message(response_node,
492 get_client_login_url(od));
493 purple_connection_error(gc,
494 PURPLE_CONNECTION_ERROR_OTHER_ERROR, msg);
495 g_free(msg);
498 purple_xmlnode_free(response_node);
499 return FALSE;
501 g_free(tmp);
503 /* Make sure we have everything else */
504 if (data_node == NULL || secret_node == NULL ||
505 token_node == NULL || tokena_node == NULL)
507 char *msg;
508 purple_debug_error("oscar", "clientLogin response was missing "
509 "something: %s\n", response);
510 msg = generate_error_message(response_node,
511 get_client_login_url(od));
512 purple_connection_error(gc,
513 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, msg);
514 g_free(msg);
515 purple_xmlnode_free(response_node);
516 return FALSE;
519 /* Extract data from the XML */
520 *token = purple_xmlnode_get_data_unescaped(tokena_node);
521 *secret = purple_xmlnode_get_data_unescaped(secret_node);
522 tmp = purple_xmlnode_get_data_unescaped(hosttime_node);
523 if (*token == NULL || **token == '\0' || *secret == NULL || **secret == '\0' || tmp == NULL || *tmp == '\0')
525 char *msg;
526 purple_debug_error("oscar", "clientLogin response was missing "
527 "something: %s\n", response);
528 msg = generate_error_message(response_node,
529 get_client_login_url(od));
530 purple_connection_error(gc,
531 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, msg);
532 g_free(msg);
533 g_free(*token);
534 g_free(*secret);
535 g_free(tmp);
536 purple_xmlnode_free(response_node);
537 return FALSE;
540 *hosttime = strtol(tmp, NULL, 10);
541 g_free(tmp);
543 purple_xmlnode_free(response_node);
545 return TRUE;
548 static void
549 client_login_cb(PurpleHttpConnection *http_conn,
550 PurpleHttpResponse *response, gpointer _od)
552 OscarData *od = _od;
553 PurpleConnection *gc;
554 char *token, *secret, *session_key;
555 time_t hosttime;
556 int password_len;
557 char *password;
558 const gchar *got_data;
559 size_t got_len;
561 gc = od->gc;
563 od->hc = NULL;
565 if (!purple_http_response_is_successful(response)) {
566 gchar *tmp;
567 tmp = g_strdup_printf(_("Error requesting %s: %s"),
568 get_client_login_url(od),
569 purple_http_response_get_error(response));
570 purple_connection_error(gc,
571 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
572 g_free(tmp);
573 return;
576 got_data = purple_http_response_get_data(response, &got_len);
577 if (!parse_client_login_response(gc, got_data, got_len, &token, &secret,
578 &hosttime))
580 return;
583 password_len = strlen(purple_connection_get_password(gc));
584 password = g_strdup_printf("%.*s",
585 od->icq ? MIN(password_len, MAXICQPASSLEN) : password_len,
586 purple_connection_get_password(gc));
587 session_key = hmac_sha256(password, secret);
588 g_free(password);
589 g_free(secret);
591 send_start_oscar_session(od, token, session_key, hosttime);
593 g_free(token);
594 g_free(session_key);
598 * This function sends a request to
599 * https://api.screenname.aol.com/auth/clientLogin with the user's
600 * username and password and receives the user's session key, which is
601 * used to request a connection to the BOSS server.
603 void send_client_login(OscarData *od, const char *username)
605 PurpleConnection *gc;
606 PurpleHttpRequest *req;
607 GString *body;
608 const char *tmp;
609 char *password;
610 int password_len;
612 gc = od->gc;
615 * We truncate ICQ passwords to 8 characters. There is probably a
616 * limit for AIM passwords, too, but we really only need to do
617 * this for ICQ because older ICQ clients let you enter a password
618 * as long as you wanted and then they truncated it silently.
620 * And we can truncate based on the number of bytes and not the
621 * number of characters because passwords for AIM and ICQ are
622 * supposed to be plain ASCII (I don't know if this has always been
623 * the case, though).
625 tmp = purple_connection_get_password(gc);
626 password_len = strlen(tmp);
627 password = g_strndup(tmp, od->icq ? MIN(password_len, MAXICQPASSLEN) : password_len);
629 /* Construct the body of the HTTP POST request */
630 body = g_string_new("");
631 g_string_append_printf(body, "devId=%s", get_client_key(od));
632 g_string_append_printf(body, "&f=xml");
633 g_string_append_printf(body, "&pwd=%s", purple_url_encode(password));
634 g_string_append_printf(body, "&s=%s", purple_url_encode(username));
635 g_free(password);
637 req = purple_http_request_new(get_client_login_url(od));
638 purple_http_request_set_method(req, "POST");
639 purple_http_request_header_set(req, "Content-Type",
640 "application/x-www-form-urlencoded; charset=UTF-8");
641 purple_http_request_set_contents(req, body->str, body->len);
642 od->hc = purple_http_request(gc, req, client_login_cb, od);
643 purple_http_request_unref(req);
645 g_string_free(body, TRUE);