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
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.
35 * http://dev.aol.com/aim/oscar/#AUTH
36 * http://dev.aol.com/authentication_for_clients
40 #include "oscarcommon.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
)
84 PurpleXmlNode
*status_code_node
;
85 gboolean have_error_code
= TRUE
;
87 gchar
*details
= NULL
;
89 status_code_node
= purple_xmlnode_get_child(resp
, "statusCode");
90 if (status_code_node
) {
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
108 err
= g_strdup_printf(_("Received unexpected response from %s: %s"), url
, details
);
110 /* Translators: %s in this string is a URL */
111 err
= g_strdup_printf(_("Received unexpected response from %s"), url
);
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
)
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
);
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
);
152 signature
= hmac_sha256(session_key
, signature_base_string
);
153 g_free(signature_base_string
);
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
;
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
)
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
);
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
) {
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
);
201 purple_xmlnode_free(response_node
);
205 /* Make sure the status code was 200 */
209 PurpleXmlNode
*status_detail_node
;
210 guint status_detail
= 0;
212 status_detail_node
= purple_xmlnode_get_child(response_node
,
214 if (status_detail_node
) {
215 gchar
*data
= purple_xmlnode_get_data(status_detail_node
);
217 status_detail
= atoi(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 "
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
);
242 purple_xmlnode_free(response_node
);
247 /* Make sure we have everything else */
248 if (data_node
== NULL
|| host_node
== NULL
|| port_node
== NULL
|| cookie_node
== NULL
)
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
);
258 purple_xmlnode_free(response_node
);
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
);
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");
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
);
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')
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
);
299 purple_xmlnode_free(response_node
);
310 start_oscar_session_cb(PurpleHttpConnection
*http_conn
,
311 PurpleHttpResponse
*response
, gpointer _od
)
314 PurpleConnection
*gc
;
316 char *tls_certname
= NULL
;
319 gsize cookiedata_len
= 0;
320 const gchar
*got_data
;
327 if (!purple_http_response_is_successful(response
)) {
329 /* Translators: The first %s is a URL, the second is a brief error
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
);
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
))
344 cookiedata
= g_base64_decode(cookie
, &cookiedata_len
);
345 oscar_connect_to_bos(gc
, od
, host
, port
, cookiedata
, cookiedata_len
, tls_certname
);
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"
366 "&ts=%" G_GINT64_FORMAT
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
),
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
);
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
;
417 /* Parse the response as XML */
418 response_node
= purple_xmlnode_from_str(response
, response_len
);
419 if (response_node
== NULL
)
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
);
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
) {
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
);
453 purple_xmlnode_free(response_node
);
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
);
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
);
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"));
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
);
498 purple_xmlnode_free(response_node
);
503 /* Make sure we have everything else */
504 if (data_node
== NULL
|| secret_node
== NULL
||
505 token_node
== NULL
|| tokena_node
== NULL
)
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
);
515 purple_xmlnode_free(response_node
);
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')
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
);
536 purple_xmlnode_free(response_node
);
540 *hosttime
= strtol(tmp
, NULL
, 10);
543 purple_xmlnode_free(response_node
);
549 client_login_cb(PurpleHttpConnection
*http_conn
,
550 PurpleHttpResponse
*response
, gpointer _od
)
553 PurpleConnection
*gc
;
554 char *token
, *secret
, *session_key
;
558 const gchar
*got_data
;
565 if (!purple_http_response_is_successful(response
)) {
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
);
576 got_data
= purple_http_response_get_data(response
, &got_len
);
577 if (!parse_client_login_response(gc
, got_data
, got_len
, &token
, &secret
,
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
);
591 send_start_oscar_session(od
, token
, session_key
, hosttime
);
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
;
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
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
));
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
);