comparison libpurple/protocols/oscar/clientlogin.c @ 27161:7054f810b0f9

Check in code that connects to oscar using clientLogin. This is the authentication scheme they've publically documented. We still use the old MD5-style login as the default, but you can optionally try this out by checking a check box on the advanced tab of your oscar account. Functionally everything is supposed to be the same. However, for some reason users with Mobile IM forwarding turned on don't show up online and can't be messaged. Not sure why. Using clientLogin DOES make it easier for AOL to track us. And yes, it probably makes it easier for AOL to block us, too. But I don't believe they want to do that. I believe they're trying to keep their network open, and I think we should appreciate that and try to work with them. We're not just some small open source project that slips under the radar unnoticed anymore. It's good to have options, right? None of this code was taken from anywhere (outside of libpurple). I wrote it all from scratch (and took a few bits from other places in libpurple). I did use the documentation on http://dev.aol.com/aim , but I don't believe that affects us from a licensing standpoint in any way. If you disagree we should talk about it on the devel mailing list.
author Mark Doliner <mark@kingant.net>
date Tue, 23 Jun 2009 18:20:12 +0000
parents
children 1a255e11c02b
comparison
equal deleted inserted replaced
27160:763247959e00 27161:7054f810b0f9
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.
5 *
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.
10 *
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.
15 *
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
19 */
20
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.
25 *
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.
33 *
34 * For details, see:
35 * http://dev.aol.com/aim/oscar/#AUTH
36 * http://dev.aol.com/authentication_for_clients
37 */
38
39 #include "cipher.h"
40 #include "core.h"
41
42 #include "oscar.h"
43
44 #define URL_CLIENT_LOGIN "https://api.screenname.aol.com/auth/clientLogin"
45 #define URL_START_OSCAR_SESSION "http://api.oscar.aol.com/aim/startOSCARSession"
46
47 /*
48 * Using clientLogin requires a developer ID. This dev ID is owned by
49 * the AIM account "markdoliner"
50 */
51 #define CLIENT_KEY "ma15d7JTxbmVG-RP"
52
53 /**
54 * This is similar to purple_url_encode() except that it follows
55 * RFC3986 a little more closely by not encoding - . _ and ~
56 * It also uses capital letters as hex characters because capital
57 * letters are required by AOL. The RFC says that capital letters
58 * are a SHOULD and that URLs that use capital letters are
59 * equivalent to URLs that use small letters.
60 *
61 * TODO: Check if purple_url_encode() can be replaced with this
62 * version without breaking anything.
63 */
64 static const char *oscar_auth_url_encode(const char *str)
65 {
66 const char *iter;
67 static char buf[BUF_LEN];
68 char utf_char[6];
69 guint i, j = 0;
70
71 g_return_val_if_fail(str != NULL, NULL);
72 g_return_val_if_fail(g_utf8_validate(str, -1, NULL), NULL);
73
74 iter = str;
75 for (; *iter && j < (BUF_LEN - 1) ; iter = g_utf8_next_char(iter)) {
76 gunichar c = g_utf8_get_char(iter);
77 /* If the character is an ASCII character and is alphanumeric
78 * no need to escape */
79 if ((c < 128 && isalnum(c)) || c =='-' || c == '.' || c == '_' || c == '~') {
80 buf[j++] = c;
81 } else {
82 int bytes = g_unichar_to_utf8(c, utf_char);
83 for (i = 0; i < bytes; i++) {
84 if (j > (BUF_LEN - 4))
85 break;
86 sprintf(buf + j, "%%%02X", utf_char[i] & 0xff);
87 j += 3;
88 }
89 }
90 }
91
92 buf[j] = '\0';
93
94 return buf;
95 }
96
97 /**
98 * @return A null-terminated base64 encoded version of the HMAC
99 * calculated using the given key and data.
100 */
101 static gchar *hmac_sha256(const char *key, const char *message)
102 {
103 PurpleCipherContext *context;
104 guchar digest[32];
105
106 context = purple_cipher_context_new_by_name("hmac", NULL);
107 purple_cipher_context_set_option(context, "hash", "sha256");
108 purple_cipher_context_set_key(context, (guchar *)key);
109 purple_cipher_context_append(context, (guchar *)message, strlen(message));
110 purple_cipher_context_digest(context, sizeof(digest), digest, NULL);
111 purple_cipher_context_destroy(context);
112
113 return purple_base64_encode(digest, sizeof(digest));
114 }
115
116 /**
117 * @return A base-64 encoded HMAC-SHA256 signature created using the
118 * technique documented at
119 * http://dev.aol.com/authentication_for_clients#signing
120 */
121 static gchar *generate_signature(const char *method, const char *url, const char *parameters, const char *session_key)
122 {
123 char *encoded_url, *signature_base_string, *signature;
124 const char *encoded_parameters;
125
126 encoded_url = g_strdup(oscar_auth_url_encode(url));
127 encoded_parameters = oscar_auth_url_encode(parameters);
128 signature_base_string = g_strdup_printf("%s&%s&%s",
129 method, encoded_url, encoded_parameters);
130 g_free(encoded_url);
131
132 signature = hmac_sha256(session_key, signature_base_string);
133 g_free(signature_base_string);
134
135 return signature;
136 }
137
138 static gboolean parse_start_oscar_session_response(PurpleConnection *gc, const gchar *response, gsize response_len, char **host, unsigned short *port, char **cookie)
139 {
140 xmlnode *response_node, *tmp_node, *data_node;
141 xmlnode *host_node, *port_node, *cookie_node;
142 char *tmp;
143
144 /* Parse the response as XML */
145 response_node = xmlnode_from_str(response, response_len);
146 if (response_node == NULL)
147 {
148 purple_debug_error("oscar", "startOSCARSession could not parse "
149 "response as XML: %s\n", response);
150 purple_connection_error_reason(gc,
151 PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
152 _("Received unexpected response from " URL_START_OSCAR_SESSION));
153 return FALSE;
154 }
155
156 /* Grab the necessary XML nodes */
157 tmp_node = xmlnode_get_child(response_node, "statusCode");
158 data_node = xmlnode_get_child(response_node, "data");
159 if (data_node != NULL) {
160 host_node = xmlnode_get_child(data_node, "host");
161 port_node = xmlnode_get_child(data_node, "port");
162 cookie_node = xmlnode_get_child(data_node, "cookie");
163 }
164
165 /* Make sure we have a status code */
166 if (tmp_node == NULL || (tmp = xmlnode_get_data_unescaped(tmp_node)) == NULL) {
167 purple_debug_error("oscar", "startOSCARSession response was "
168 "missing statusCode: %s\n", response);
169 purple_connection_error_reason(gc,
170 PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
171 _("Received unexpected response from " URL_START_OSCAR_SESSION));
172 xmlnode_free(response_node);
173 return FALSE;
174 }
175
176 /* Make sure the status code was 200 */
177 if (strcmp(tmp, "200") != 0)
178 {
179 purple_debug_error("oscar", "startOSCARSession response statusCode "
180 "was %s: %s\n", tmp, response);
181
182 if (strcmp(tmp, "401") == 0)
183 purple_connection_error_reason(gc,
184 PURPLE_CONNECTION_ERROR_OTHER_ERROR,
185 _("You have been connecting and disconnecting too "
186 "frequently. Wait ten minutes and try again. If "
187 "you continue to try, you will need to wait even "
188 "longer."));
189 else
190 purple_connection_error_reason(gc,
191 PURPLE_CONNECTION_ERROR_OTHER_ERROR,
192 _("Received unexpected response from " URL_START_OSCAR_SESSION));
193
194 g_free(tmp);
195 xmlnode_free(response_node);
196 return FALSE;
197 }
198 g_free(tmp);
199
200 /* Make sure we have everything else */
201 if (data_node == NULL || host_node == NULL ||
202 port_node == NULL || cookie_node == NULL)
203 {
204 purple_debug_error("oscar", "startOSCARSession response was missing "
205 "something: %s\n", response);
206 purple_connection_error_reason(gc,
207 PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
208 _("Received unexpected response from " URL_START_OSCAR_SESSION));
209 xmlnode_free(response_node);
210 return FALSE;
211 }
212
213 /* Extract data from the XML */
214 *host = xmlnode_get_data_unescaped(host_node);
215 tmp = xmlnode_get_data_unescaped(port_node);
216 *cookie = xmlnode_get_data_unescaped(cookie_node);
217 if (*host == NULL || **host == '\0' || tmp == NULL || *tmp == '\0' || cookie == NULL || *cookie == '\0')
218 {
219 purple_debug_error("oscar", "startOSCARSession response was missing "
220 "something: %s\n", response);
221 purple_connection_error_reason(gc,
222 PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
223 _("Received unexpected response from " URL_START_OSCAR_SESSION));
224 g_free(*host);
225 g_free(tmp);
226 g_free(*cookie);
227 xmlnode_free(response_node);
228 return FALSE;
229 }
230
231 *port = atoi(tmp);
232 g_free(tmp);
233
234 return TRUE;
235 }
236
237 static void start_oscar_session_cb(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text, gsize len, const gchar *error_message)
238 {
239 OscarData *od;
240 PurpleConnection *gc;
241 char *host, *cookie;
242 unsigned short port;
243 guint8 *cookiedata;
244 gsize cookiedata_len;
245
246 od = user_data;
247 gc = od->gc;
248
249 od->url_data = NULL;
250
251 if (error_message != NULL || len == 0) {
252 gchar *tmp;
253 tmp = g_strdup_printf(_("Error requesting " URL_START_OSCAR_SESSION
254 ": %s"), error_message);
255 purple_connection_error_reason(gc,
256 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
257 g_free(tmp);
258 return;
259 }
260
261 if (!parse_start_oscar_session_response(gc, url_text, len, &host, &port, &cookie))
262 return;
263
264 cookiedata = purple_base64_decode(cookie, &cookiedata_len);
265 oscar_connect_to_bos(gc, od, host, port, cookiedata, cookiedata_len);
266 g_free(cookiedata);
267
268 g_free(host);
269 g_free(cookie);
270 }
271
272 static void send_start_oscar_session(OscarData *od, const char *token, const char *session_key, time_t hosttime)
273 {
274 char *query_string, *signature, *url;
275
276 /* Construct the GET parameters */
277 query_string = g_strdup_printf("a=%s"
278 "&f=xml"
279 "&k=" CLIENT_KEY
280 "&ts=%zu"
281 "&useTLS=0",
282 oscar_auth_url_encode(token), hosttime);
283 signature = generate_signature("GET", URL_START_OSCAR_SESSION,
284 query_string, session_key);
285 url = g_strdup_printf(URL_START_OSCAR_SESSION "?%s&sig_sha256=%s",
286 query_string, signature);
287 g_free(query_string);
288 g_free(signature);
289
290 /* Make the request */
291 od->url_data = purple_util_fetch_url(url, TRUE, NULL, FALSE,
292 start_oscar_session_cb, od);
293 g_free(url);
294 }
295
296 /**
297 * This function parses the given response from a clientLogin request
298 * and extracts the useful information.
299 *
300 * @param gc The PurpleConnection. If the response data does
301 * not indicate then purple_connection_error_reason()
302 * will be called to close this connection.
303 * @param response The response data from the clientLogin request.
304 * @param response_len The length of the above response, or -1 if
305 * @response is NUL terminated.
306 * @param token If parsing was successful then this will be set to
307 * a newly allocated string containing the token. The
308 * caller should g_free this string when it is finished
309 * with it. On failure this value will be untouched.
310 * @param secret If parsing was successful then this will be set to
311 * a newly allocated string containing the secret. The
312 * caller should g_free this string when it is finished
313 * with it. On failure this value will be untouched.
314 * @param hosttime If parsing was successful then this will be set to
315 * the time on the OpenAuth Server in seconds since the
316 * Unix epoch. On failure this value will be untouched.
317 *
318 * @return TRUE if the request was successful and we were able to
319 * extract all info we need. Otherwise FALSE.
320 */
321 static gboolean parse_client_login_response(PurpleConnection *gc, const gchar *response, gsize response_len, char **token, char **secret, time_t *hosttime)
322 {
323 xmlnode *response_node, *tmp_node, *data_node;
324 xmlnode *secret_node, *hosttime_node, *token_node, *tokena_node;
325 char *tmp;
326
327 /* Parse the response as XML */
328 response_node = xmlnode_from_str(response, response_len);
329 if (response_node == NULL)
330 {
331 purple_debug_error("oscar", "clientLogin could not parse "
332 "response as XML: %s\n", response);
333 purple_connection_error_reason(gc,
334 PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
335 _("Received unexpected response from " URL_CLIENT_LOGIN));
336 return FALSE;
337 }
338
339 /* Grab the necessary XML nodes */
340 tmp_node = xmlnode_get_child(response_node, "statusCode");
341 data_node = xmlnode_get_child(response_node, "data");
342 if (data_node != NULL) {
343 secret_node = xmlnode_get_child(data_node, "sessionSecret");
344 hosttime_node = xmlnode_get_child(data_node, "hostTime");
345 token_node = xmlnode_get_child(data_node, "token");
346 if (token_node != NULL)
347 tokena_node = xmlnode_get_child(token_node, "a");
348 }
349
350 /* Make sure we have a status code */
351 if (tmp_node == NULL || (tmp = xmlnode_get_data_unescaped(tmp_node)) == NULL) {
352 purple_debug_error("oscar", "clientLogin response was "
353 "missing statusCode: %s\n", response);
354 purple_connection_error_reason(gc,
355 PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
356 _("Received unexpected response from " URL_CLIENT_LOGIN));
357 xmlnode_free(response_node);
358 return FALSE;
359 }
360
361 /* Make sure the status code was 200 */
362 if (strcmp(tmp, "200") != 0)
363 {
364 int status_code, status_detail_code = 0;
365
366 status_code = atoi(tmp);
367 g_free(tmp);
368 tmp_node = xmlnode_get_child(response_node, "statusDetailCode");
369 if (tmp_node != NULL && (tmp = xmlnode_get_data_unescaped(tmp_node)) != NULL) {
370 status_detail_code = atoi(tmp);
371 g_free(tmp);
372 }
373
374 purple_debug_error("oscar", "clientLogin response statusCode "
375 "was %d (%d): %s\n", status_code, status_detail_code, response);
376
377 if (status_code == 330 && status_detail_code == 3011) {
378 purple_connection_error_reason(gc,
379 PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED,
380 _("Incorrect password."));
381 } else if (status_code == 401 && status_detail_code == 3019) {
382 purple_connection_error_reason(gc,
383 PURPLE_CONNECTION_ERROR_OTHER_ERROR,
384 _("AOL does not allow your screen name to authenticate via this site."));
385 } else
386 purple_connection_error_reason(gc,
387 PURPLE_CONNECTION_ERROR_OTHER_ERROR,
388 _("Received unexpected response from " URL_CLIENT_LOGIN));
389
390 xmlnode_free(response_node);
391 return FALSE;
392 }
393 g_free(tmp);
394
395 /* Make sure we have everything else */
396 if (data_node == NULL || secret_node == NULL ||
397 token_node == NULL || tokena_node == NULL)
398 {
399 purple_debug_error("oscar", "clientLogin response was missing "
400 "something: %s\n", response);
401 purple_connection_error_reason(gc,
402 PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
403 _("Received unexpected response from " URL_CLIENT_LOGIN));
404 xmlnode_free(response_node);
405 return FALSE;
406 }
407
408 /* Extract data from the XML */
409 *token = xmlnode_get_data_unescaped(tokena_node);
410 *secret = xmlnode_get_data_unescaped(secret_node);
411 tmp = xmlnode_get_data_unescaped(hosttime_node);
412 if (*token == NULL || **token == '\0' || *secret == NULL || **secret == '\0' || tmp == NULL || *tmp == '\0')
413 {
414 purple_debug_error("oscar", "clientLogin response was missing "
415 "something: %s\n", response);
416 purple_connection_error_reason(gc,
417 PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
418 _("Received unexpected response from " URL_CLIENT_LOGIN));
419 g_free(*token);
420 g_free(*secret);
421 g_free(tmp);
422 xmlnode_free(response_node);
423 return FALSE;
424 }
425
426 *hosttime = strtol(tmp, NULL, 10);
427 g_free(tmp);
428
429 xmlnode_free(response_node);
430
431 return TRUE;
432 }
433
434 static void client_login_cb(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text, gsize len, const gchar *error_message)
435 {
436 OscarData *od;
437 PurpleConnection *gc;
438 char *token, *secret, *session_key;
439 time_t hosttime;
440 int password_len;
441 char *password;
442
443 od = user_data;
444 gc = od->gc;
445
446 od->url_data = NULL;
447
448 if (error_message != NULL || len == 0) {
449 gchar *tmp;
450 tmp = g_strdup_printf(_("Error requesting " URL_CLIENT_LOGIN
451 ": %s"), error_message);
452 purple_connection_error_reason(gc,
453 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
454 g_free(tmp);
455 return;
456 }
457
458 if (!parse_client_login_response(gc, url_text, len, &token, &secret, &hosttime))
459 return;
460
461 password_len = strlen(purple_connection_get_password(gc));
462 password = g_strdup_printf("%.*s",
463 od->icq ? MIN(password_len, MAXICQPASSLEN) : password_len,
464 purple_connection_get_password(gc));
465 session_key = hmac_sha256(password, secret);
466 g_free(password);
467 g_free(secret);
468
469 send_start_oscar_session(od, token, session_key, hosttime);
470
471 g_free(token);
472 g_free(session_key);
473 }
474
475 /**
476 * This function sends a request to
477 * https://api.screenname.aol.com/auth/clientLogin with the user's
478 * username and password and receives the user's session key, which is
479 * used to request a connection to the BOSS server.
480 */
481 void send_client_login(OscarData *od, const char *username)
482 {
483 PurpleConnection *gc;
484 GString *request, *body;
485 const char *tmp;
486 char *password;
487 int password_len;
488
489 gc = od->gc;
490
491 /*
492 * We truncate ICQ passwords to 8 characters. There is probably a
493 * limit for AIM passwords, too, but we really only need to do
494 * this for ICQ because older ICQ clients let you enter a password
495 * as long as you wanted and then they truncated it silently.
496 *
497 * And we can truncate based on the number of bytes and not the
498 * number of characters because passwords for AIM and ICQ are
499 * supposed to be plain ASCII (I don't know if this has always been
500 * the case, though).
501 */
502 tmp = purple_connection_get_password(gc);
503 password_len = strlen(tmp);
504 password = g_strndup(tmp, od->icq ? MIN(password_len, MAXICQPASSLEN) : password_len);
505
506 /* Construct the body of the HTTP POST request */
507 body = g_string_new("");
508 g_string_append_printf(body, "devId=" CLIENT_KEY);
509 g_string_append_printf(body, "&f=xml");
510 g_string_append_printf(body, "&pwd=%s", oscar_auth_url_encode(password));
511 g_string_append_printf(body, "&s=%s", oscar_auth_url_encode(username));
512 g_free(password);
513
514 /* Construct an HTTP POST request */
515 request = g_string_new("POST /auth/clientLogin HTTP/1.0\r\n"
516 "Connection: close\r\n"
517 "Accept: */*\r\n");
518
519 /* Tack on the body */
520 g_string_append_printf(request, "Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n");
521 g_string_append_printf(request, "Content-Length: %lu\r\n\r\n", body->len);
522 g_string_append_len(request, body->str, body->len);
523 g_string_free(body, TRUE);
524
525 /* Send the POST request */
526 od->url_data = purple_util_fetch_url_request(URL_CLIENT_LOGIN,
527 TRUE, NULL, FALSE, request->str, FALSE,
528 client_login_cb, od);
529 g_string_free(request, TRUE);
530 }