comparison libpurple/protocols/oscar/clientlogin.c @ 27590:a08e84032814

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