Mercurial > pidgin
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 } |