Mercurial > pidgin
comparison libpurple/protocols/myspace/myspace.c @ 16405:8063f163f411
Add MySpaceIM header file and reorganize functions.
author | Jeffrey Connelly <jaconnel@calpoly.edu> |
---|---|
date | Sat, 28 Apr 2007 21:19:40 +0000 |
parents | 47e07438f01c |
children | f62023eddfc6 |
comparison
equal
deleted
inserted
replaced
16404:47e07438f01c | 16405:8063f163f411 |
---|---|
1 /* MySpaceIM Protocol Plugin | 1 /* MySpaceIM Protocol Plugin |
2 * | 2 * |
3 * \author Jeff Connelly | 3 * \author Jeff Connelly |
4 * | 4 * |
5 * Copyright (C) 2007, Jeff Connelly <myspaceim@xyzzy.cjb.net> | 5 * Copyright (C) 2007, Jeff Connelly <jeff2@homing.pidgin.im> |
6 * | 6 * |
7 * Based on Purple's "C Plugin HOWTO" hello world example. | 7 * Based on Purple's "C Plugin HOWTO" hello world example. |
8 * | 8 * |
9 * Code also drawn from myspace: | 9 * Code also drawn from myspace: |
10 * http://snarfed.org/space/purple+mock+protocol+plugin | 10 * http://snarfed.org/space/purple+mock+protocol+plugin |
53 #include "version.h" | 53 #include "version.h" |
54 #include "cipher.h" /* for SHA-1 */ | 54 #include "cipher.h" /* for SHA-1 */ |
55 #include "util.h" /* for base64 */ | 55 #include "util.h" /* for base64 */ |
56 #include "debug.h" /* for purple_debug_info */ | 56 #include "debug.h" /* for purple_debug_info */ |
57 | 57 |
58 #define MSIM_STATUS_ONLINE "online" | 58 #include "myspace.h" |
59 #define MSIM_STATUS_AWAY "away" | |
60 #define MSIM_STATUS_OFFLINE "offline" | |
61 #define MSIM_STATUS_INVISIBLE "invisible" | |
62 | |
63 /* Build version of MySpaceIM to report to servers */ | |
64 #define MSIM_CLIENT_VERSION 673 | |
65 | |
66 #define MSIM_SERVER "im.myspace.akadns.net" | |
67 //#define MSIM_SERVER "localhost" | |
68 #define MSIM_PORT 1863 /* TODO: alternate ports and automatic */ | |
69 | |
70 /* Constants */ | |
71 #define HASH_SIZE 0x14 /**< Size of SHA-1 hash for login */ | |
72 #define NONCE_HALF_SIZE 0x20 /**< Half of decoded 'nc' field */ | |
73 #define MSIM_READ_BUF_SIZE 5*1024 /**< Receive buffer size */ | |
74 #define MSIM_FINAL_STRING "\\final\\" /**< Message end marker */ | |
75 | |
76 /* Messages */ | |
77 #define MSIM_BM_INSTANT 1 | |
78 #define MSIM_BM_STATUS 100 | |
79 #define MSIM_BM_ACTION 121 | |
80 /*#define MSIM_BM_UNKNOWN1 122*/ | |
81 | |
82 /* Random number in every MsimSession, to ensure it is valid. */ | |
83 #define MSIM_SESSION_STRUCT_MAGIC 0xe4a6752b | |
84 | |
85 /* Everything needed to keep track of a session. */ | |
86 typedef struct _MsimSession | |
87 { | |
88 guint magic; /**< MSIM_SESSION_STRUCT_MAGIC */ | |
89 PurpleAccount *account; | |
90 PurpleConnection *gc; | |
91 gchar *sesskey; /**< Session key text string from server */ | |
92 gchar *userid; /**< This user's numeric user ID */ | |
93 gint fd; /**< File descriptor to/from server */ | |
94 | |
95 GHashTable *user_lookup_cb; /**< Username -> userid lookup callback */ | |
96 GHashTable *user_lookup_cb_data; /**< Username -> userid lookup callback data */ | |
97 GHashTable *user_lookup_cache; /**< Cached information on users */ | |
98 | |
99 gchar *rxbuf; /**< Receive buffer */ | |
100 guint rxoff; /**< Receive buffer offset */ | |
101 } MsimSession; | |
102 | |
103 #define MSIM_SESSION_VALID(s) (session != NULL && session->magic == MSIM_SESSION_STRUCT_MAGIC) | |
104 | |
105 /* Callback for when a user's information is received, initiated from a user lookup. */ | |
106 typedef void (*MSIM_USER_LOOKUP_CB)(MsimSession *session, GHashTable *userinfo, gpointer data); | |
107 | |
108 /* Passed to MSIM_USER_LOOKUP_CB for msim_send_im_cb - called when | |
109 * user information is available, ready to send a message. */ | |
110 typedef struct _send_im_cb_struct | |
111 { | |
112 gchar *who; | |
113 gchar *message; | |
114 PurpleMessageFlags flags; | |
115 } send_im_cb_struct; | |
116 | |
117 | |
118 /* TODO: .h file */ | |
119 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data); | |
120 static inline gboolean msim_is_userid(const gchar *user); | |
121 static void msim_session_destroy(MsimSession *session); | |
122 | 59 |
123 static void init_plugin(PurplePlugin *plugin) | 60 static void init_plugin(PurplePlugin *plugin) |
124 { | 61 { |
125 purple_notify_message(plugin, PURPLE_NOTIFY_MSG_INFO, "Hello World!", | 62 purple_notify_message(plugin, PURPLE_NOTIFY_MSG_INFO, "Hello World!", |
126 "This is the Hello World! plugin :)", NULL, NULL, NULL); | 63 "This is the Hello World! plugin :)", NULL, NULL, NULL); |
141 MSIM_STATUS_ONLINE, MSIM_STATUS_AWAY, MSIM_STATUS_OFFLINE, MSIM_STATUS_INVISIBLE); | 78 MSIM_STATUS_ONLINE, MSIM_STATUS_AWAY, MSIM_STATUS_OFFLINE, MSIM_STATUS_INVISIBLE); |
142 | 79 |
143 | 80 |
144 types = NULL; | 81 types = NULL; |
145 | 82 |
83 /* TODO: Clean up - I don't like all this repetition */ | |
146 type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, MSIM_STATUS_ONLINE, | 84 type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, MSIM_STATUS_ONLINE, |
147 MSIM_STATUS_ONLINE, TRUE); | 85 MSIM_STATUS_ONLINE, TRUE); |
148 purple_status_type_add_attr(type, "message", "Online", | 86 purple_status_type_add_attr(type, "message", "Online", |
149 purple_value_new(PURPLE_TYPE_STRING)); | 87 purple_value_new(PURPLE_TYPE_STRING)); |
150 types = g_list_append(types, type); | 88 types = g_list_append(types, type); |
168 types = g_list_append(types, type); | 106 types = g_list_append(types, type); |
169 | 107 |
170 return types; | 108 return types; |
171 } | 109 } |
172 | 110 |
111 /** | |
112 * Return the icon name for a buddy and account. | |
113 * | |
114 * @param acct The account to find the icon for, or NULL for protocol icon. | |
115 * @param buddy The buddy to find the icon for, or NULL for the account icon. | |
116 * | |
117 * @return The base icon name string. | |
118 */ | |
119 static const gchar *msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy) | |
120 { | |
121 /* TODO: use a MySpace icon. hbons submitted one to | |
122 * http://developer.pidgin.im/wiki/MySpaceIM - tried placing in | |
123 * C:\cygwin\home\Jeff\purple-2.0.0beta6\gtk\pixmaps\status\default | |
124 * and returning "myspace" but icon shows up blank. | |
125 */ | |
126 if (acct == NULL) | |
127 { | |
128 purple_debug_info("msim", "msim_list_icon: acct == NULL!\n"); | |
129 //exit(-2); | |
130 } | |
131 return "meanwhile"; | |
132 } | |
173 /** | 133 /** |
174 * Parse a MySpaceIM protocol message into a hash table. | 134 * Parse a MySpaceIM protocol message into a hash table. |
175 * | 135 * |
176 * @param msg The message string to parse, will be g_free()'d. | 136 * @param msg The message string to parse, will be g_free()'d. |
177 * | 137 * |
240 | 200 |
241 return table; | 201 return table; |
242 } | 202 } |
243 | 203 |
244 /** | 204 /** |
205 * Parse a \x1c-separated "dictionary" of key=value pairs into a hash table. | |
206 * | |
207 * @param body_str The text of the dictionary to parse. Often the | |
208 * value for the 'body' field. | |
209 * | |
210 * @return Hash table of the keys and values. Must g_hash_table_destroy() when done. | |
211 */ | |
212 static GHashTable *msim_parse_body(const gchar *body_str) | |
213 { | |
214 GHashTable *table; | |
215 gchar *item; | |
216 gchar **items; | |
217 gchar **elements; | |
218 guint i; | |
219 | |
220 g_return_val_if_fail(body_str != NULL, NULL); | |
221 | |
222 table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); | |
223 | |
224 for (items = g_strsplit(body_str, "\x1c", 0), i = 0; | |
225 (item = items[i]); | |
226 i++) | |
227 { | |
228 gchar *key, *value; | |
229 | |
230 //printf("TOK=<%s>\n", token); | |
231 elements = g_strsplit(item, "=", 2); | |
232 | |
233 key = elements[0]; | |
234 if (!key) | |
235 { | |
236 purple_debug_info("msim", "msim_parse_body(%s): null key\n", | |
237 body_str); | |
238 g_strfreev(elements); | |
239 break; | |
240 } | |
241 | |
242 value = elements[1]; | |
243 if (!value) | |
244 { | |
245 purple_debug_info("msim", "msim_parse_body(%s): null value\n", | |
246 body_str); | |
247 g_strfreev(elements); | |
248 break; | |
249 } | |
250 | |
251 //printf("-- %s: %s\n", key, value); | |
252 | |
253 /* XXX: This overwrites duplicates. */ | |
254 /* TODO: make the GHashTable values be GList's, and append to the list if | |
255 * there is already a value of the same key name. This is important for | |
256 * the WebChallenge message. */ | |
257 g_hash_table_insert(table, g_strdup(key), g_strdup(value)); | |
258 | |
259 g_strfreev(elements); | |
260 } | |
261 | |
262 g_strfreev(items); | |
263 | |
264 return table; | |
265 } | |
266 | |
267 | |
268 | |
269 static void print_hash_item(gpointer key, gpointer value, gpointer user_data) | |
270 { | |
271 printf("%s=%s\n", (char*)key, (char*)value); | |
272 } | |
273 | |
274 /** | |
275 * Send an arbitrary protocol message. | |
276 * | |
277 * @param session | |
278 * @param msg The textual, encoded message to send. | |
279 * | |
280 * Note: this does not send instant messages. For that, see msim_send_im. | |
281 */ | |
282 static void msim_send(MsimSession *session, const gchar *msg) | |
283 { | |
284 int ret; | |
285 | |
286 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
287 g_return_if_fail(msg != NULL); | |
288 | |
289 purple_debug_info("msim", "msim_send: writing <%s>\n", msg); | |
290 | |
291 ret = send(session->fd, msg, strlen(msg), 0); | |
292 | |
293 if (ret != strlen(msg)) | |
294 { | |
295 purple_debug_info("msim", | |
296 "msim_send(%s): strlen=%d, but only wrote %s\n", | |
297 msg, strlen(msg), ret); | |
298 /* TODO: better error -or- TODO: send all, loop unless ret=-1 */ | |
299 } | |
300 } | |
301 | |
302 /** | |
303 * Start logging in to the MSIM servers. | |
304 * | |
305 * @param acct Account information to use to login. | |
306 */ | |
307 static void msim_login(PurpleAccount *acct) | |
308 { | |
309 PurpleConnection *gc; | |
310 const char *host; | |
311 int port; | |
312 | |
313 g_return_if_fail(acct != NULL); | |
314 | |
315 purple_debug_info("myspace", "logging in %s\n", acct->username); | |
316 | |
317 gc = purple_account_get_connection(acct); | |
318 gc->proto_data = msim_session_new(acct); | |
319 | |
320 /* 1. connect to server */ | |
321 purple_connection_update_progress(gc, "Connecting", | |
322 0, /* which connection step this is */ | |
323 4); /* total number of steps */ | |
324 | |
325 /* TODO: GUI option to be user-modifiable. */ | |
326 host = purple_account_get_string(acct, "server", MSIM_SERVER); | |
327 port = purple_account_get_int(acct, "port", MSIM_PORT); | |
328 /* TODO: connect */ | |
329 /* From purple.sf.net/api: | |
330 * """Note that this function name can be misleading--although it is called | |
331 * "proxy connect," it is used for establishing any outgoing TCP connection, | |
332 * whether through a proxy or not.""" */ | |
333 | |
334 /* Calls msim_connect_cb when connected. */ | |
335 if (purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc) == NULL) | |
336 { | |
337 /* TODO: try other ports if in auto mode, then save | |
338 * working port and try that first next time. */ | |
339 purple_connection_error(gc, "Couldn't create socket"); | |
340 return; | |
341 } | |
342 | |
343 } | |
344 /** | |
345 * Process a login challenge, sending a response. | |
346 * | |
347 * @param session | |
348 * @param table Hash table of login challenge message. | |
349 * | |
350 * @return 0, since the 'table' parameter is no longer needed. | |
351 */ | |
352 static int msim_login_challenge(MsimSession *session, GHashTable *table) | |
353 { | |
354 PurpleAccount *account; | |
355 gchar *nc_str; | |
356 guchar *nc; | |
357 gchar *response_str; | |
358 gsize nc_len; | |
359 gchar *buf; | |
360 | |
361 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
362 g_return_val_if_fail(table != NULL, 0); | |
363 | |
364 nc_str = g_hash_table_lookup(table, "nc"); | |
365 | |
366 account = session->account; | |
367 //assert(account); | |
368 | |
369 purple_connection_update_progress(session->gc, "Reading challenge", 1, 4); | |
370 | |
371 purple_debug_info("msim", "nc=<%s>\n", nc_str); | |
372 | |
373 nc = (guchar*)purple_base64_decode(nc_str, &nc_len); | |
374 purple_debug_info("msim", "base64 decoded to %d bytes\n", nc_len); | |
375 if (nc_len != 0x40) | |
376 { | |
377 purple_debug_info("msim", "bad nc length: %x != 0x40\n", nc_len); | |
378 purple_connection_error(session->gc, "Unexpected challenge length from server"); | |
379 return 0; | |
380 } | |
381 | |
382 purple_connection_update_progress(session->gc, "Logging in", 2, 4); | |
383 | |
384 printf("going to compute login response\n"); | |
385 //response_str = msim_compute_login_response(nc_str, "testuser", "testpw"); //session->gc->account->username, session->gc->account->password); | |
386 response_str = msim_compute_login_response(nc, account->username, account->password); | |
387 printf("got back login response\n"); | |
388 | |
389 g_free(nc); | |
390 | |
391 /* Reply */ | |
392 buf = g_strdup_printf("\\login2\\%d\\username\\%s\\response\\%s\\clientver\\%d\\reconn\\%d\\status\\%d\\id\\1\\final\\", | |
393 196610, account->username, response_str, MSIM_CLIENT_VERSION, 0, 100); | |
394 | |
395 g_free(response_str); | |
396 | |
397 purple_debug_info("msim", "response=<%s>\n", buf); | |
398 | |
399 msim_send(session, buf); | |
400 | |
401 g_free(buf); | |
402 | |
403 return 0; | |
404 } | |
405 | |
406 /** | |
245 * Compute the base64'd login challenge response based on username, password, nonce, and IPs. | 407 * Compute the base64'd login challenge response based on username, password, nonce, and IPs. |
246 * | 408 * |
247 * @param nonce The base64 encoded nonce ('nc') field from the server. | 409 * @param nonce The base64 encoded nonce ('nc') field from the server. |
248 * @param email User's email address (used as login name). | 410 * @param email User's email address (used as login name). |
249 * @param password User's cleartext password. | 411 * @param password User's cleartext password. |
250 * | 412 * |
251 * @return Encoded login challenge response, ready to send to the server. Must be g_free()'d | 413 * @return Encoded login challenge response, ready to send to the server. Must be g_free()'d |
252 * when finished. | 414 * when finished. |
253 */ | 415 */ |
254 static gchar* msim_compute_login_response(guchar nonce[2*NONCE_HALF_SIZE], | 416 static gchar* msim_compute_login_response(guchar nonce[2*NONCE_SIZE], |
255 gchar* email, gchar* password) | 417 gchar* email, gchar* password) |
256 { | 418 { |
257 PurpleCipherContext *key_context; | 419 PurpleCipherContext *key_context; |
258 PurpleCipher *sha1; | 420 PurpleCipher *sha1; |
259 PurpleCipherContext *rc4; | 421 PurpleCipherContext *rc4; |
265 gchar* response; | 427 gchar* response; |
266 size_t data_len, data_out_len; | 428 size_t data_len, data_out_len; |
267 gsize conv_bytes_read, conv_bytes_written; | 429 gsize conv_bytes_read, conv_bytes_written; |
268 GError* conv_error; | 430 GError* conv_error; |
269 | 431 |
270 //memset(nonce, 0, NONCE_HALF_SIZE); | 432 //memset(nonce, 0, NONCE_SIZE); |
271 //memset(nonce + NONCE_HALF_SIZE, 1, NONCE_HALF_SIZE); | 433 //memset(nonce + NONCE_SIZE, 1, NONCE_SIZE); |
272 | 434 |
273 /* Convert ASCII password to UTF16 little endian */ | 435 /* Convert ASCII password to UTF16 little endian */ |
274 purple_debug_info("msim", "converting password to UTF-16LE\n"); | 436 purple_debug_info("msim", "converting password to UTF-16LE\n"); |
275 conv_error = NULL; | 437 conv_error = NULL; |
276 password_utf16le = g_convert(password, -1, "UTF-16LE", "UTF-8", | 438 password_utf16le = g_convert(password, -1, "UTF-16LE", "UTF-8", |
282 "g_convert password UTF8->UTF16LE failed: %s", | 444 "g_convert password UTF8->UTF16LE failed: %s", |
283 conv_error->message); | 445 conv_error->message); |
284 g_error_free(conv_error); | 446 g_error_free(conv_error); |
285 } | 447 } |
286 | 448 |
287 #if 0 | |
288 password_utf16le = g_new0(gchar, strlen(password) * 2); | |
289 for (i = 0; i < strlen(password) * 2; i += 2) | |
290 { | |
291 password_utf16le[i] = password[i / 2]; | |
292 password_utf16le[i + 1] = 0; | |
293 } | |
294 #endif | |
295 | |
296 /* Compute password hash */ | 449 /* Compute password hash */ |
297 purple_cipher_digest_region("sha1", (guchar*)password_utf16le, | 450 purple_cipher_digest_region("sha1", (guchar*)password_utf16le, |
298 conv_bytes_written, sizeof(hash_pw), hash_pw, NULL); | 451 conv_bytes_written, sizeof(hash_pw), hash_pw, NULL); |
299 g_free(password_utf16le); | 452 g_free(password_utf16le); |
300 | 453 |
307 | 460 |
308 /* key = sha1(sha1(pw) + nonce2) */ | 461 /* key = sha1(sha1(pw) + nonce2) */ |
309 sha1 = purple_ciphers_find_cipher("sha1"); | 462 sha1 = purple_ciphers_find_cipher("sha1"); |
310 key_context = purple_cipher_context_new(sha1, NULL); | 463 key_context = purple_cipher_context_new(sha1, NULL); |
311 purple_cipher_context_append(key_context, hash_pw, HASH_SIZE); | 464 purple_cipher_context_append(key_context, hash_pw, HASH_SIZE); |
312 purple_cipher_context_append(key_context, nonce + NONCE_HALF_SIZE, NONCE_HALF_SIZE); | 465 purple_cipher_context_append(key_context, nonce + NONCE_SIZE, NONCE_SIZE); |
313 purple_cipher_context_digest(key_context, sizeof(key), key, NULL); | 466 purple_cipher_context_digest(key_context, sizeof(key), key, NULL); |
314 | 467 |
315 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE | 468 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE |
316 printf("key = "); | 469 printf("key = "); |
317 for (i = 0; i < sizeof(key); i++) | 470 for (i = 0; i < sizeof(key); i++) |
335 * anyways except make note of that fact. Probably important for any | 488 * anyways except make note of that fact. Probably important for any |
336 * kind of direct connection, or file transfer functionality. | 489 * kind of direct connection, or file transfer functionality. |
337 */ | 490 */ |
338 /* rc4 encrypt: | 491 /* rc4 encrypt: |
339 * nonce1+email+IP list */ | 492 * nonce1+email+IP list */ |
340 data_len = NONCE_HALF_SIZE + strlen(email) + 25; | 493 data_len = NONCE_SIZE + strlen(email) + 25; |
341 data = g_new0(guchar, data_len); | 494 data = g_new0(guchar, data_len); |
342 memcpy(data, nonce, NONCE_HALF_SIZE); | 495 memcpy(data, nonce, NONCE_SIZE); |
343 memcpy(data + NONCE_HALF_SIZE, email, strlen(email)); | 496 memcpy(data + NONCE_SIZE, email, strlen(email)); |
344 memcpy(data + NONCE_HALF_SIZE + strlen(email), | 497 memcpy(data + NONCE_SIZE + strlen(email), |
345 /* IP addresses of network interfaces */ | 498 /* IP addresses of network interfaces */ |
346 "\x00\x00\x00\x00\x05\x7f\x00\x00\x01\x00\x00\x00\x00\x0a\x00\x00\x40\xc0\xa8\x58\x01\xc0\xa8\x3c\x01", 25); | 499 "\x00\x00\x00\x00\x05\x7f\x00\x00\x01\x00\x00\x00\x00\x0a\x00\x00\x40\xc0\xa8\x58\x01\xc0\xa8\x3c\x01", 25); |
347 | 500 |
348 data_out = g_new0(guchar, data_len); | 501 data_out = g_new0(guchar, data_len); |
349 purple_cipher_context_encrypt(rc4, (const guchar*)data, | 502 purple_cipher_context_encrypt(rc4, (const guchar*)data, |
357 printf("response=<%s>\n", response); | 510 printf("response=<%s>\n", response); |
358 #endif | 511 #endif |
359 | 512 |
360 return response; | 513 return response; |
361 } | 514 } |
362 | |
363 static void print_hash_item(gpointer key, gpointer value, gpointer user_data) | |
364 { | |
365 printf("%s=%s\n", (char*)key, (char*)value); | |
366 } | |
367 | |
368 /** | |
369 * Send an arbitrary protocol message. | |
370 * | |
371 * @param session | |
372 * @param msg The textual, encoded message to send. | |
373 * | |
374 * Note: this does not send instant messages. For that, see msim_send_im. | |
375 */ | |
376 static void msim_send(MsimSession *session, const gchar *msg) | |
377 { | |
378 int ret; | |
379 | |
380 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
381 g_return_if_fail(msg != NULL); | |
382 | |
383 purple_debug_info("msim", "msim_send: writing <%s>\n", msg); | |
384 | |
385 ret = send(session->fd, msg, strlen(msg), 0); | |
386 | |
387 if (ret != strlen(msg)) | |
388 { | |
389 purple_debug_info("msim", | |
390 "msim_send(%s): strlen=%d, but only wrote %s\n", | |
391 msg, strlen(msg), ret); | |
392 /* TODO: better error */ | |
393 } | |
394 } | |
395 | |
396 /** | |
397 * Process a login challenge, sending a response. | |
398 * | |
399 * @param session | |
400 * @param table Hash table of login challenge message. | |
401 * | |
402 * @return 0, since the 'table' parameter is no longer needed. | |
403 */ | |
404 static int msim_login_challenge(MsimSession *session, GHashTable *table) | |
405 { | |
406 PurpleAccount *account; | |
407 gchar *nc_str; | |
408 guchar *nc; | |
409 gchar *response_str; | |
410 gsize nc_len; | |
411 gchar *buf; | |
412 | |
413 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
414 g_return_val_if_fail(table != NULL, 0); | |
415 | |
416 nc_str = g_hash_table_lookup(table, "nc"); | |
417 | |
418 account = session->account; | |
419 //assert(account); | |
420 | |
421 purple_connection_update_progress(session->gc, "Reading challenge", 1, 4); | |
422 | |
423 purple_debug_info("msim", "nc=<%s>\n", nc_str); | |
424 | |
425 nc = (guchar*)purple_base64_decode(nc_str, &nc_len); | |
426 purple_debug_info("msim", "base64 decoded to %d bytes\n", nc_len); | |
427 if (nc_len != 0x40) | |
428 { | |
429 purple_debug_info("msim", "bad nc length: %x != 0x40\n", nc_len); | |
430 purple_connection_error(session->gc, "Unexpected challenge length from server"); | |
431 return 0; | |
432 } | |
433 | |
434 purple_connection_update_progress(session->gc, "Logging in", 2, 4); | |
435 | |
436 printf("going to compute login response\n"); | |
437 //response_str = msim_compute_login_response(nc_str, "testuser", "testpw"); //session->gc->account->username, session->gc->account->password); | |
438 response_str = msim_compute_login_response(nc, account->username, account->password); | |
439 printf("got back login response\n"); | |
440 | |
441 g_free(nc); | |
442 | |
443 /* Reply */ | |
444 buf = g_strdup_printf("\\login2\\%d\\username\\%s\\response\\%s\\clientver\\%d\\reconn\\%d\\status\\%d\\id\\1\\final\\", | |
445 196610, account->username, response_str, MSIM_CLIENT_VERSION, 0, 100); | |
446 | |
447 g_free(response_str); | |
448 | |
449 purple_debug_info("msim", "response=<%s>\n", buf); | |
450 | |
451 msim_send(session, buf); | |
452 | |
453 g_free(buf); | |
454 | |
455 return 0; | |
456 } | |
457 | |
458 /** | |
459 * Parse a \x1c-separated "dictionary" of key=value pairs into a hash table. | |
460 * | |
461 * @param body_str The text of the dictionary to parse. Often the | |
462 * value for the 'body' field. | |
463 * | |
464 * @return Hash table of the keys and values. Must g_hash_table_destroy() when done. | |
465 */ | |
466 static GHashTable *msim_parse_body(const gchar *body_str) | |
467 { | |
468 GHashTable *table; | |
469 gchar *item; | |
470 gchar **items; | |
471 gchar **elements; | |
472 guint i; | |
473 | |
474 g_return_val_if_fail(body_str != NULL, NULL); | |
475 | |
476 table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); | |
477 | |
478 for (items = g_strsplit(body_str, "\x1c", 0), i = 0; | |
479 (item = items[i]); | |
480 i++) | |
481 { | |
482 gchar *key, *value; | |
483 | |
484 //printf("TOK=<%s>\n", token); | |
485 elements = g_strsplit(item, "=", 2); | |
486 | |
487 key = elements[0]; | |
488 if (!key) | |
489 { | |
490 purple_debug_info("msim", "msim_parse_body(%s): null key\n", | |
491 body_str); | |
492 g_strfreev(elements); | |
493 break; | |
494 } | |
495 | |
496 value = elements[1]; | |
497 if (!value) | |
498 { | |
499 purple_debug_info("msim", "msim_parse_body(%s): null value\n", | |
500 body_str); | |
501 g_strfreev(elements); | |
502 break; | |
503 } | |
504 | |
505 //printf("-- %s: %s\n", key, value); | |
506 | |
507 /* XXX: This overwrites duplicates. */ | |
508 /* TODO: make the GHashTable values be GList's, and append to the list if | |
509 * there is already a value of the same key name. This is important for | |
510 * the WebChallenge message. */ | |
511 g_hash_table_insert(table, g_strdup(key), g_strdup(value)); | |
512 | |
513 g_strfreev(elements); | |
514 } | |
515 | |
516 g_strfreev(items); | |
517 | |
518 return table; | |
519 } | |
520 | |
521 /** | |
522 * Immediately send an IM to a user, by their numeric user ID. | |
523 * | |
524 * @param session | |
525 * @param userid ASCII numeric userid. | |
526 * @param message Text of message to send. | |
527 * @param flags Purple instant message flags. | |
528 * | |
529 * @return 0, since the 'table' parameter is no longer needed. | |
530 * | |
531 */ | |
532 static int msim_send_im_by_userid(MsimSession *session, const gchar *userid, const gchar *message, PurpleMessageFlags flags) | |
533 { | |
534 gchar *msg_string; | |
535 | |
536 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
537 g_return_val_if_fail(userid != NULL, 0); | |
538 g_return_val_if_fail(msim_is_userid(userid) == TRUE, 0); | |
539 g_return_val_if_fail(message != NULL, 0); | |
540 | |
541 /* TODO: constants for bm types */ | |
542 msg_string = g_strdup_printf("\\bm\\121\\sesskey\\%s\\t\\%s\\cv\\%d\\msg\\%s\\final\\", | |
543 session->sesskey, userid, MSIM_CLIENT_VERSION, message); | |
544 | |
545 purple_debug_info("msim", "going to write: %s\n", msg_string); | |
546 | |
547 msim_send(session, msg_string); | |
548 | |
549 /* TODO: notify Purple that we sent the IM. */ | |
550 | |
551 /* Not needed since sending messages to yourself is allowed by MSIM! */ | |
552 /*if (strcmp(from_username, who) == 0) | |
553 serv_got_im(gc, from_username, message, PURPLE_MESSAGE_RECV, time(NULL)); | |
554 */ | |
555 | |
556 return 0; | |
557 } | |
558 | |
559 | |
560 /** | |
561 * Callback called when ready to send an IM by userid (the userid has been looked up). | |
562 * Calls msim_send_im_by_userid. | |
563 * | |
564 * @param session | |
565 * @param userinfo User info message from server containing a 'body' field | |
566 * with a 'UserID' key. This is where the user ID is taken from. | |
567 * @param data A send_im_cb_struct* of information on the IM to send. | |
568 * | |
569 */ | |
570 static void msim_send_im_by_userid_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
571 { | |
572 send_im_cb_struct *s; | |
573 gchar *userid; | |
574 GHashTable *body; | |
575 | |
576 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
577 g_return_if_fail(userinfo != NULL); | |
578 | |
579 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
580 g_assert(body); | |
581 | |
582 userid = g_hash_table_lookup(body, "UserID"); | |
583 | |
584 s = (send_im_cb_struct*)data; | |
585 msim_send_im_by_userid(session, userid, s->message, s->flags); | |
586 | |
587 g_hash_table_destroy(body); | |
588 g_hash_table_destroy(userinfo); | |
589 g_free(s->message); | |
590 g_free(s->who); | |
591 } | |
592 | |
593 /** | |
594 * Process a message reply from the server. | |
595 * | |
596 * @param session | |
597 * @param table Message reply from server. | |
598 * | |
599 * @return 0, since the 'table' field is no longer needed. | |
600 */ | |
601 static int msim_process_reply(MsimSession *session, GHashTable *table) | |
602 { | |
603 gchar *rid_str; | |
604 | |
605 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
606 g_return_val_if_fail(table != NULL, 0); | |
607 | |
608 rid_str = g_hash_table_lookup(table, "rid"); | |
609 | |
610 if (rid_str) /* msim_lookup_user sets callback for here */ | |
611 { | |
612 MSIM_USER_LOOKUP_CB cb; | |
613 gpointer data; | |
614 guint rid; | |
615 | |
616 GHashTable *body; | |
617 gchar *username; | |
618 | |
619 rid = atol(rid_str); | |
620 | |
621 /* Cache the user info. Currently, the GHashTable of user info in | |
622 * this cache never expires so is never freed. TODO: expire and destroy | |
623 * | |
624 * Some information never changes (username->userid map), some does. | |
625 * TODO: Cache what doesn't change only | |
626 */ | |
627 body = msim_parse_body(g_hash_table_lookup(table, "body")); | |
628 username = g_hash_table_lookup(body, "UserName"); | |
629 if (username) | |
630 { | |
631 g_hash_table_insert(session->user_lookup_cache, g_strdup(username), body); | |
632 } else { | |
633 purple_debug_info("msim", | |
634 "msim_process_reply: not caching <%s>, no UserName\n", | |
635 g_hash_table_lookup(table, "body")); | |
636 } | |
637 | |
638 /* If a callback is registered for this userid lookup, call it. */ | |
639 | |
640 cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid)); | |
641 data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); | |
642 | |
643 if (cb) | |
644 { | |
645 purple_debug_info("msim", | |
646 "msim_process_body: calling callback now\n"); | |
647 cb(session, table, data); | |
648 g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid)); | |
649 g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); | |
650 | |
651 /* Return 1 to tell caller of msim_process (msim_input_cb) to | |
652 * not destroy 'table'; allow 'cb' to hang on to it and destroy | |
653 * it when it wants. */ | |
654 return 1; | |
655 } else { | |
656 purple_debug_info("msim", | |
657 "msim_process_body: no callback for rid %d\n", rid); | |
658 } | |
659 } | |
660 return 0; | |
661 } | |
662 | |
663 /** | |
664 * Handle an error from the server. | |
665 * | |
666 * @param session | |
667 * @param table The message. | |
668 * | |
669 * @return 0, since 'table' can be freed. | |
670 */ | |
671 static int msim_error(MsimSession *session, GHashTable *table) | |
672 { | |
673 gchar *err, *errmsg, *full_errmsg; | |
674 | |
675 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
676 g_return_val_if_fail(table != NULL, 0); | |
677 | |
678 err = g_hash_table_lookup(table, "err"); | |
679 errmsg = g_hash_table_lookup(table, "errmsg"); | |
680 | |
681 full_errmsg = g_strdup_printf("Protocol error, code %s: %s", err, errmsg); | |
682 | |
683 purple_debug_info("msim", "msim_error: %s\n", full_errmsg); | |
684 | |
685 /* TODO: check 'fatal' and die if asked to. | |
686 * TODO: do something with the error # (localization of errmsg?) */ | |
687 purple_notify_error(session->account, g_strdup("MySpaceIM Error"), | |
688 full_errmsg, NULL); | |
689 | |
690 if (g_hash_table_lookup(table, "fatal")) | |
691 { | |
692 purple_debug_info("msim", "fatal error, destroy session\n"); | |
693 purple_connection_error(session->gc, full_errmsg); | |
694 close(session->fd); | |
695 //msim_session_destroy(session); | |
696 } | |
697 | |
698 return 0; | |
699 } | |
700 | |
701 /** | |
702 * Callback to handle incoming messages, after resolving userid. | |
703 * | |
704 * @param session | |
705 * @param userinfo Message from server on user's info, containing UserName. | |
706 * @param data A gchar* of the incoming instant message's text. | |
707 */ | |
708 static void msim_incoming_im_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
709 { | |
710 gchar *msg; | |
711 gchar *username; | |
712 GHashTable *body; | |
713 | |
714 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
715 g_return_if_fail(userinfo != NULL); | |
716 | |
717 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
718 g_assert(body != NULL); | |
719 | |
720 username = g_hash_table_lookup(body, "UserName"); | |
721 | |
722 msg = (gchar*)data; | |
723 serv_got_im(session->gc, username, msg, PURPLE_MESSAGE_RECV, time(NULL)); | |
724 | |
725 g_hash_table_destroy(body); | |
726 g_hash_table_destroy(userinfo); | |
727 } | |
728 | |
729 /** | |
730 * Handle an incoming message. | |
731 * | |
732 * @param session The session | |
733 * @param table Message from the server, containing 'f' (userid from) and 'msg'. | |
734 * | |
735 * @return 0, since table can be freed. | |
736 */ | |
737 static int msim_incoming_im(MsimSession *session, GHashTable *table) | |
738 { | |
739 gchar *userid; | |
740 gchar *msg; | |
741 | |
742 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
743 g_return_val_if_fail(table != NULL, 0); | |
744 | |
745 | |
746 userid = g_hash_table_lookup(table, "f"); | |
747 msg = g_hash_table_lookup(table, "msg"); | |
748 | |
749 purple_debug_info("msim", | |
750 "msim_incoming_im: got msg <%s> from <%s>, resolving username\n", | |
751 msg, userid); | |
752 | |
753 msim_lookup_user(session, userid, msim_incoming_im_cb, g_strdup(msg)); | |
754 | |
755 return 0; | |
756 } | |
757 | |
758 #if 0 | |
759 /* Not sure about this */ | |
760 static void msim_status_now(gchar *who, gpointer data) | |
761 { | |
762 printf("msim_status_now: %s\n", who); | |
763 } | |
764 #endif | |
765 | |
766 /** | |
767 * Callback to update incoming status messages, after looked up username. | |
768 * | |
769 * @param session | |
770 * @param userinfo Looked up user information from server. | |
771 * @param data gchar* status string. | |
772 * | |
773 */ | |
774 static void msim_status_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
775 { | |
776 PurpleBuddyList *blist; | |
777 PurpleBuddy *buddy; | |
778 PurplePresence *presence; | |
779 GHashTable *body; | |
780 //PurpleStatus *status; | |
781 gchar **status_array; | |
782 GList *list; | |
783 gchar *status_text, *status_code; | |
784 gchar *status_str; | |
785 gint i; | |
786 gchar *username; | |
787 | |
788 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
789 g_return_if_fail(userinfo != NULL); | |
790 | |
791 status_str = (gchar*)data; | |
792 | |
793 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
794 g_assert(body); | |
795 | |
796 username = g_hash_table_lookup(body, "UserName"); | |
797 /* Note: DisplayName doesn't seem to be resolvable. It could be displayed on | |
798 * the buddy list, if the UserID was stored along with it. */ | |
799 | |
800 if (!username) | |
801 { | |
802 purple_debug_info("msim", "msim_status_cb: no username?!\n"); | |
803 return; | |
804 } | |
805 | |
806 purple_debug_info("msim", | |
807 "msim_status_cb: updating status for <%s> to <%s>\n", | |
808 username, status_str); | |
809 | |
810 /* TODO: generic functions to split into a GList */ | |
811 status_array = g_strsplit(status_str, "|", 0); | |
812 for (list = NULL, i = 0; | |
813 status_array[i]; | |
814 i++) | |
815 { | |
816 list = g_list_append(list, status_array[i]); | |
817 } | |
818 | |
819 /* Example fields: |s|0|ss|Offline */ | |
820 status_code = g_list_nth_data(list, 2); | |
821 status_text = g_list_nth_data(list, 4); | |
822 | |
823 blist = purple_get_blist(); | |
824 | |
825 /* Add buddy if not found */ | |
826 buddy = purple_find_buddy(session->account, username); | |
827 if (!buddy) | |
828 { | |
829 /* TODO: purple aliases, userids and usernames */ | |
830 purple_debug_info("msim", | |
831 "msim_status: making new buddy for %s\n", username); | |
832 buddy = purple_buddy_new(session->account, username, NULL); | |
833 | |
834 /* TODO: sometimes (when click on it), buddy list disappears. Fix. */ | |
835 purple_blist_add_buddy(buddy, NULL, NULL, NULL); | |
836 } else { | |
837 purple_debug_info("msim", "msim_status: found buddy %s\n", username); | |
838 } | |
839 | |
840 /* For now, always set status to online. | |
841 * TODO: make status reflect reality | |
842 * TODO: show headline */ | |
843 presence = purple_presence_new_for_buddy(buddy); | |
844 purple_presence_set_status_active(presence, MSIM_STATUS_ONLINE, TRUE); | |
845 | |
846 g_strfreev(status_array); | |
847 g_list_free(list); | |
848 g_hash_table_destroy(body); | |
849 g_hash_table_destroy(userinfo); | |
850 /* Do not free status_str - it will be freed by g_hash_table_destroy on session->userid_lookup_cb_data */ | |
851 } | |
852 | |
853 /** | |
854 * Process incoming status messages. | |
855 * | |
856 * @param session | |
857 * @param table Status update message. | |
858 * | |
859 * @return 0, since 'table' can be freed. | |
860 */ | |
861 static int msim_status(MsimSession *session, GHashTable *table) | |
862 { | |
863 gchar *status_str; | |
864 gchar *userid; | |
865 | |
866 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
867 g_return_val_if_fail(table != NULL, 0); | |
868 | |
869 status_str = g_hash_table_lookup(table, "msg"); | |
870 if (!status_str) | |
871 { | |
872 purple_debug_info("msim", "msim_status: bm=100 but no status msg\n"); | |
873 return 0; | |
874 } | |
875 | |
876 userid = g_hash_table_lookup(table, "f"); | |
877 if (!userid) | |
878 { | |
879 purple_debug_info("msim", "msim_status: bm=100 but no f field\n"); | |
880 return 0; | |
881 } | |
882 | |
883 /* TODO: if buddies were identified on buddy list by uid, wouldn't have to lookup | |
884 * before updating the status! Much more efficient. */ | |
885 purple_debug_info("msim", | |
886 "msim_status: got status msg <%s> for <%s>, scheduling lookup\n", | |
887 status_str, userid); | |
888 | |
889 /* Actually update status once obtain username */ | |
890 msim_lookup_user(session, userid, msim_status_cb, g_strdup(status_str)); | |
891 | |
892 return 0; | |
893 } | |
894 | |
895 | |
896 /** | |
897 * Process a message. | |
898 * | |
899 * @param gc Connection. | |
900 * @param table Any message from the server. | |
901 * | |
902 * @return The return value of the function used to process the message, or -1 if | |
903 * called with invalid parameters. | |
904 */ | |
905 static int msim_process(PurpleConnection *gc, GHashTable *table) | |
906 { | |
907 MsimSession *session; | |
908 | |
909 g_return_val_if_fail(gc != NULL, -1); | |
910 g_return_val_if_fail(table != NULL, -1); | |
911 | |
912 session = (MsimSession*)gc->proto_data; | |
913 | |
914 printf("-------- message -------------\n"); | |
915 g_hash_table_foreach(table, print_hash_item, NULL); | |
916 printf("------------------------------\n"); | |
917 | |
918 if (g_hash_table_lookup(table, "nc")) | |
919 { | |
920 return msim_login_challenge(session, table); | |
921 } else if (g_hash_table_lookup(table, "sesskey")) { | |
922 printf("SESSKEY=<%s>\n", (gchar*)g_hash_table_lookup(table, "sesskey")); | |
923 | |
924 purple_connection_update_progress(gc, "Connected", 3, 4); | |
925 | |
926 session->sesskey = g_strdup(g_hash_table_lookup(table, "sesskey")); | |
927 | |
928 /* Comes with: proof,profileid,userid,uniquenick -- all same values | |
929 * (at least for me). */ | |
930 session->userid = g_strdup(g_hash_table_lookup(table, "userid")); | |
931 | |
932 purple_connection_set_state(gc, PURPLE_CONNECTED); | |
933 | |
934 return 0; | |
935 } else if (g_hash_table_lookup(table, "bm")) { | |
936 guint bm; | |
937 | |
938 bm = atoi(g_hash_table_lookup(table, "bm")); | |
939 switch (bm) | |
940 { | |
941 case MSIM_BM_STATUS: | |
942 return msim_status(session, table); | |
943 case MSIM_BM_INSTANT: | |
944 return msim_incoming_im(session, table); | |
945 default: | |
946 /* Not really an IM, but show it for informational | |
947 * purposes during development. */ | |
948 return msim_incoming_im(session, table); | |
949 } | |
950 | |
951 if (bm == MSIM_BM_STATUS) | |
952 { | |
953 return msim_status(session, table); | |
954 } else { /* else if strcmp(bm, "1") == 0) */ | |
955 return msim_incoming_im(session, table); | |
956 } | |
957 } else if (g_hash_table_lookup(table, "rid")) { | |
958 return msim_process_reply(session, table); | |
959 } else if (g_hash_table_lookup(table, "error")) { | |
960 return msim_error(session, table); | |
961 } else if (g_hash_table_lookup(table, "ka")) { | |
962 purple_debug_info("msim", "msim_process: got keep alive\n"); | |
963 return 0; | |
964 } else { | |
965 printf("<<unhandled>>\n"); | |
966 return 0; | |
967 } | |
968 } | |
969 | |
970 /** | |
971 * Callback when input available. | |
972 * | |
973 * @param gc_uncasted A PurpleConnection pointer. | |
974 * @param source File descriptor. | |
975 * @param cond PURPLE_INPUT_READ | |
976 * | |
977 * Reads the input, and dispatches calls msim_process to handle it. | |
978 */ | |
979 static void msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond) | |
980 { | |
981 PurpleConnection *gc; | |
982 PurpleAccount *account; | |
983 MsimSession *session; | |
984 gchar *end; | |
985 int n; | |
986 | |
987 g_return_if_fail(gc_uncasted != NULL); | |
988 g_return_if_fail(source >= 0); /* Note: 0 is a valid fd */ | |
989 | |
990 gc = (PurpleConnection*)(gc_uncasted); | |
991 account = purple_connection_get_account(gc); | |
992 session = gc->proto_data; | |
993 | |
994 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
995 | |
996 g_assert(cond == PURPLE_INPUT_READ); | |
997 | |
998 /* Only can handle so much data at once... | |
999 * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE. | |
1000 * Should be large enough to hold the largest protocol message. | |
1001 */ | |
1002 if (session->rxoff == MSIM_READ_BUF_SIZE) | |
1003 { | |
1004 purple_debug_error("msim", "msim_input_cb: %d-byte read buffer full!\n", | |
1005 MSIM_READ_BUF_SIZE); | |
1006 purple_connection_error(gc, "Read buffer full"); | |
1007 /* TODO: fix 100% CPU after closing */ | |
1008 close(source); | |
1009 return; | |
1010 } | |
1011 | |
1012 purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n", | |
1013 session->rxoff, MSIM_READ_BUF_SIZE, | |
1014 MSIM_READ_BUF_SIZE - session->rxoff); | |
1015 | |
1016 /* Read into buffer. On Win32, need recv() not read(). session->fd also holds | |
1017 * the file descriptor, but it sometimes differs from the 'source' parameter. | |
1018 */ | |
1019 n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0); | |
1020 | |
1021 if (n < 0 && errno == EAGAIN) | |
1022 { | |
1023 return; | |
1024 } | |
1025 else if (n < 0) | |
1026 { | |
1027 purple_connection_error(gc, "Read error"); | |
1028 purple_debug_error("msim", "msim_input_cb: read error, ret=%d, " | |
1029 "error=%s, source=%d, fd=%d (%X))\n", | |
1030 n, strerror(errno), source, session->fd, session->fd); | |
1031 close(source); | |
1032 return; | |
1033 } | |
1034 else if (n == 0) | |
1035 { | |
1036 purple_debug_info("msim", "msim_input_cb: server disconnected\n"); | |
1037 purple_connection_error(gc, "Server has disconnected"); | |
1038 return; | |
1039 } | |
1040 | |
1041 /* Null terminate */ | |
1042 session->rxbuf[session->rxoff + n] = 0; | |
1043 | |
1044 /* Check for embedded NULs. I don't handle them, and they shouldn't occur. */ | |
1045 if (strlen(session->rxbuf + session->rxoff) != n) | |
1046 { | |
1047 /* Occurs after login, but it is not a null byte. */ | |
1048 purple_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes" | |
1049 "--null byte encountered?\n", | |
1050 strlen(session->rxbuf + session->rxoff), n); | |
1051 //purple_connection_error(gc, "Invalid message - null byte on input"); | |
1052 return; | |
1053 } | |
1054 | |
1055 session->rxoff += n; | |
1056 purple_debug_info("msim", "msim_input_cb: read=%d\n", n); | |
1057 | |
1058 //printf("buf=<%s>\n", session->rxbuf); | |
1059 | |
1060 /* Look for \\final\\ end markers. If found, process message. */ | |
1061 while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) | |
1062 { | |
1063 GHashTable *table; | |
1064 | |
1065 //printf("in loop: buf=<%s>\n", session->rxbuf); | |
1066 *end = 0; | |
1067 table = msim_parse(g_strdup(session->rxbuf)); | |
1068 if (!table) | |
1069 { | |
1070 purple_debug_info("msim", "msim_input_cb: couldn't parse <%s>\n", | |
1071 session->rxbuf); | |
1072 purple_connection_error(gc, "Unparseable message"); | |
1073 } | |
1074 else | |
1075 { | |
1076 /* Process message. Returns 0 to free */ | |
1077 if (msim_process(gc, table) == 0) | |
1078 g_hash_table_destroy(table); | |
1079 } | |
1080 | |
1081 /* Move remaining part of buffer to beginning. */ | |
1082 session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING); | |
1083 memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), | |
1084 MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf)); | |
1085 | |
1086 /* Clear end of buffer */ | |
1087 //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf)); | |
1088 } | |
1089 } | |
1090 | |
1091 /** | |
1092 * Callback when connected. Sets up input handlers. | |
1093 * | |
1094 * @param data A PurpleConnection pointer. | |
1095 * @param source File descriptor. | |
1096 * @param error_message | |
1097 */ | |
1098 static void msim_connect_cb(gpointer data, gint source, const gchar *error_message) | |
1099 { | |
1100 PurpleConnection *gc; | |
1101 MsimSession *session; | |
1102 | |
1103 g_return_if_fail(data != NULL); | |
1104 | |
1105 gc = (PurpleConnection*)data; | |
1106 session = gc->proto_data; | |
1107 | |
1108 if (source < 0) | |
1109 { | |
1110 purple_connection_error(gc, "Couldn't connect to host"); | |
1111 purple_connection_error(gc, g_strdup_printf("Couldn't connect to host: %s (%d)", | |
1112 error_message, source)); | |
1113 return; | |
1114 } | |
1115 | |
1116 session->fd = source; | |
1117 | |
1118 gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc); | |
1119 } | |
1120 | |
1121 /* Session methods */ | |
1122 | |
1123 /** | |
1124 * Create a new MSIM session. | |
1125 * | |
1126 * @param acct The account to create the session from. | |
1127 * | |
1128 * @return Pointer to a new session. Free with msim_session_destroy. | |
1129 */ | |
1130 static MsimSession *msim_session_new(PurpleAccount *acct) | |
1131 { | |
1132 MsimSession *session; | |
1133 | |
1134 g_return_val_if_fail(acct != NULL, NULL); | |
1135 | |
1136 session = g_new0(MsimSession, 1); | |
1137 | |
1138 session->magic = MSIM_SESSION_STRUCT_MAGIC; | |
1139 session->account = acct; | |
1140 session->gc = purple_account_get_connection(acct); | |
1141 session->fd = -1; | |
1142 session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL); /* do NOT free function pointers! */ | |
1143 session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free); | |
1144 session->user_lookup_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_hash_table_destroy); | |
1145 session->rxoff = 0; | |
1146 session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE); | |
1147 | |
1148 return session; | |
1149 } | |
1150 | |
1151 /** | |
1152 * Free a session. | |
1153 * | |
1154 * @param session The session to destroy. | |
1155 */ | |
1156 static void msim_session_destroy(MsimSession *session) | |
1157 { | |
1158 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
1159 | |
1160 session->magic = -1; | |
1161 | |
1162 g_free(session->rxbuf); | |
1163 g_free(session->userid); | |
1164 g_free(session->sesskey); | |
1165 | |
1166 g_free(session); | |
1167 } | |
1168 | |
1169 /** | |
1170 * Start logging in to the MSIM servers. | |
1171 * | |
1172 * @param acct Account information to use to login. | |
1173 */ | |
1174 static void msim_login(PurpleAccount *acct) | |
1175 { | |
1176 PurpleConnection *gc; | |
1177 const char *host; | |
1178 int port; | |
1179 | |
1180 g_return_if_fail(acct != NULL); | |
1181 | |
1182 purple_debug_info("myspace", "logging in %s\n", acct->username); | |
1183 | |
1184 gc = purple_account_get_connection(acct); | |
1185 gc->proto_data = msim_session_new(acct); | |
1186 | |
1187 /* 1. connect to server */ | |
1188 purple_connection_update_progress(gc, "Connecting", | |
1189 0, /* which connection step this is */ | |
1190 4); /* total number of steps */ | |
1191 | |
1192 /* TODO: GUI option to be user-modifiable. */ | |
1193 host = purple_account_get_string(acct, "server", MSIM_SERVER); | |
1194 port = purple_account_get_int(acct, "port", MSIM_PORT); | |
1195 /* TODO: connect */ | |
1196 /* From purple.sf.net/api: | |
1197 * """Note that this function name can be misleading--although it is called | |
1198 * "proxy connect," it is used for establishing any outgoing TCP connection, | |
1199 * whether through a proxy or not.""" */ | |
1200 | |
1201 /* Calls msim_connect_cb when connected. */ | |
1202 if (purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc) == NULL) | |
1203 { | |
1204 /* TODO: try other ports if in auto mode, then save | |
1205 * working port and try that first next time. */ | |
1206 purple_connection_error(gc, "Couldn't create socket"); | |
1207 return; | |
1208 } | |
1209 | |
1210 } | |
1211 | |
1212 | |
1213 /** | |
1214 * Close the connection. | |
1215 * | |
1216 * @param gc The connection. | |
1217 */ | |
1218 static void msim_close(PurpleConnection *gc) | |
1219 { | |
1220 g_return_if_fail(gc != NULL); | |
1221 | |
1222 msim_session_destroy(gc->proto_data); | |
1223 } | |
1224 | |
1225 /** | |
1226 * Return the icon name for a buddy and account. | |
1227 * | |
1228 * @param acct The account to find the icon for. | |
1229 * @param buddy The buddy to find the icon for, or NULL for the accoun icon. | |
1230 * | |
1231 * @return The base icon name string. | |
1232 */ | |
1233 static const gchar *msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy) | |
1234 { | |
1235 /* TODO: use a MySpace icon. hbons submitted one to | |
1236 * http://developer.pidgin.im/wiki/MySpaceIM - tried placing in | |
1237 * C:\cygwin\home\Jeff\purple-2.0.0beta6\gtk\pixmaps\status\default | |
1238 * and returning "myspace" but icon shows up blank. | |
1239 */ | |
1240 if (acct == NULL) | |
1241 { | |
1242 purple_debug_info("msim", "msim_list_icon: acct == NULL!\n"); | |
1243 //exit(-2); | |
1244 } | |
1245 return "meanwhile"; | |
1246 } | |
1247 | |
1248 /** | |
1249 * Check if a string is a userid (all numeric). | |
1250 * | |
1251 * @param user The user id, email, or name. | |
1252 * | |
1253 * @return TRUE if is userid, FALSE if not. | |
1254 */ | |
1255 static inline gboolean msim_is_userid(const gchar *user) | |
1256 { | |
1257 g_return_val_if_fail(user != NULL, FALSE); | |
1258 | |
1259 return strspn(user, "0123456789") == strlen(user); | |
1260 } | |
1261 | |
1262 /** | |
1263 * Check if a string is an email address (contains an @). | |
1264 * | |
1265 * @param user The user id, email, or name. | |
1266 * | |
1267 * @return TRUE if is an email, FALSE if not. | |
1268 * | |
1269 * This function is not intended to be used as a generic | |
1270 * means of validating email addresses, but to distinguish | |
1271 * between a user represented by an email address from | |
1272 * other forms of identification. | |
1273 */ | |
1274 static inline gboolean msim_is_email(const gchar *user) | |
1275 { | |
1276 g_return_val_if_fail(user != NULL, FALSE); | |
1277 | |
1278 return strchr(user, '@') != NULL; | |
1279 } | |
1280 | |
1281 | |
1282 /** | |
1283 * Asynchronously lookup user information, calling callback when receive result. | |
1284 * | |
1285 * @param session | |
1286 * @param user The user id, email address, or username. | |
1287 * @param cb Callback, called with user information when available. | |
1288 * @param data An arbitray data pointer passed to the callback. | |
1289 */ | |
1290 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data) | |
1291 { | |
1292 gchar *field_name; | |
1293 gchar *msg_string; | |
1294 guint rid, cmd, dsn, lid; | |
1295 | |
1296 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
1297 g_return_if_fail(user != NULL); | |
1298 g_return_if_fail(cb != NULL); | |
1299 | |
1300 purple_debug_info("msim", "msim_lookup_userid", | |
1301 "asynchronously looking up <%s>\n", user); | |
1302 | |
1303 /* TODO: check if this user's info was cached and fresh; if so return immediately */ | |
1304 #if 0 | |
1305 /* If already know userid, then call callback immediately */ | |
1306 cached_userid = g_hash_table_lookup(session->userid_cache, who); | |
1307 if (cached_userid && !by_userid) | |
1308 { | |
1309 cb(cached_userid, NULL, NULL, data); | |
1310 return; | |
1311 } | |
1312 #endif | |
1313 | |
1314 rid = rand(); //om(); | |
1315 | |
1316 /* Setup callback. Response will be associated with request using 'rid'. */ | |
1317 g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb); | |
1318 g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data); | |
1319 | |
1320 /* Send request */ | |
1321 | |
1322 cmd = 1; | |
1323 | |
1324 if (msim_is_userid(user)) | |
1325 { | |
1326 /* TODO: document cmd,dsn,lid */ | |
1327 field_name = "UserID"; | |
1328 dsn = 4; | |
1329 lid = 3; | |
1330 } else if (msim_is_email(user)) { | |
1331 field_name = "Email"; | |
1332 dsn = 5; | |
1333 lid = 7; | |
1334 } else { | |
1335 field_name = "UserName"; | |
1336 dsn = 5; | |
1337 lid = 7; | |
1338 } | |
1339 | |
1340 msg_string = g_strdup_printf("\\persist\\1\\sesskey\\%s\\cmd\\1\\dsn\\%d\\uid\\%s\\lid\\%d\\rid\\%d\\body\\%s=%s\\final\\", | |
1341 session->sesskey, dsn, session->userid, lid, rid, field_name, user); | |
1342 | |
1343 msim_send(session, msg_string); | |
1344 } | |
1345 | 515 |
1346 /** | 516 /** |
1347 * Schedule an IM to be sent once the user ID is looked up. | 517 * Schedule an IM to be sent once the user ID is looked up. |
1348 * | 518 * |
1349 * @param gc Connection. | 519 * @param gc Connection. |
1417 * TODO: Make the sent IM's appear as from the user's username, instead of | 587 * TODO: Make the sent IM's appear as from the user's username, instead of |
1418 * their email address. Purple uses the login (in MSIM, the email)--change this. | 588 * their email address. Purple uses the login (in MSIM, the email)--change this. |
1419 */ | 589 */ |
1420 return 1; | 590 return 1; |
1421 } | 591 } |
592 | |
593 /** | |
594 * Immediately send an IM to a user, by their numeric user ID. | |
595 * | |
596 * @param session | |
597 * @param userid ASCII numeric userid. | |
598 * @param message Text of message to send. | |
599 * @param flags Purple instant message flags. | |
600 * | |
601 * @return 0, since the 'table' parameter is no longer needed. | |
602 * | |
603 */ | |
604 static int msim_send_im_by_userid(MsimSession *session, const gchar *userid, const gchar *message, PurpleMessageFlags flags) | |
605 { | |
606 gchar *msg_string; | |
607 | |
608 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
609 g_return_val_if_fail(userid != NULL, 0); | |
610 g_return_val_if_fail(msim_is_userid(userid) == TRUE, 0); | |
611 g_return_val_if_fail(message != NULL, 0); | |
612 | |
613 /* TODO: constants for bm types */ | |
614 msg_string = g_strdup_printf("\\bm\\121\\sesskey\\%s\\t\\%s\\cv\\%d\\msg\\%s\\final\\", | |
615 session->sesskey, userid, MSIM_CLIENT_VERSION, message); | |
616 | |
617 purple_debug_info("msim", "going to write: %s\n", msg_string); | |
618 | |
619 msim_send(session, msg_string); | |
620 | |
621 /* TODO: notify Purple that we sent the IM. */ | |
622 | |
623 /* Not needed since sending messages to yourself is allowed by MSIM! */ | |
624 /*if (strcmp(from_username, who) == 0) | |
625 serv_got_im(gc, from_username, message, PURPLE_MESSAGE_RECV, time(NULL)); | |
626 */ | |
627 | |
628 return 0; | |
629 } | |
630 | |
631 | |
632 /** | |
633 * Callback called when ready to send an IM by userid (the userid has been looked up). | |
634 * Calls msim_send_im_by_userid. | |
635 * | |
636 * @param session | |
637 * @param userinfo User info message from server containing a 'body' field | |
638 * with a 'UserID' key. This is where the user ID is taken from. | |
639 * @param data A send_im_cb_struct* of information on the IM to send. | |
640 * | |
641 */ | |
642 static void msim_send_im_by_userid_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
643 { | |
644 send_im_cb_struct *s; | |
645 gchar *userid; | |
646 GHashTable *body; | |
647 | |
648 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
649 g_return_if_fail(userinfo != NULL); | |
650 | |
651 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
652 g_assert(body); | |
653 | |
654 userid = g_hash_table_lookup(body, "UserID"); | |
655 | |
656 s = (send_im_cb_struct*)data; | |
657 msim_send_im_by_userid(session, userid, s->message, s->flags); | |
658 | |
659 g_hash_table_destroy(body); | |
660 g_hash_table_destroy(userinfo); | |
661 g_free(s->message); | |
662 g_free(s->who); | |
663 } | |
664 | |
665 /** | |
666 * Callback to handle incoming messages, after resolving userid. | |
667 * | |
668 * @param session | |
669 * @param userinfo Message from server on user's info, containing UserName. | |
670 * @param data A gchar* of the incoming instant message's text. | |
671 */ | |
672 static void msim_incoming_im_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
673 { | |
674 gchar *msg; | |
675 gchar *username; | |
676 GHashTable *body; | |
677 | |
678 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
679 g_return_if_fail(userinfo != NULL); | |
680 | |
681 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
682 g_assert(body != NULL); | |
683 | |
684 username = g_hash_table_lookup(body, "UserName"); | |
685 | |
686 msg = (gchar*)data; | |
687 serv_got_im(session->gc, username, msg, PURPLE_MESSAGE_RECV, time(NULL)); | |
688 | |
689 g_hash_table_destroy(body); | |
690 g_hash_table_destroy(userinfo); | |
691 } | |
692 | |
693 /** | |
694 * Handle an incoming message. | |
695 * | |
696 * @param session The session | |
697 * @param table Message from the server, containing 'f' (userid from) and 'msg'. | |
698 * | |
699 * @return 0, since table can be freed. | |
700 */ | |
701 static int msim_incoming_im(MsimSession *session, GHashTable *table) | |
702 { | |
703 gchar *userid; | |
704 gchar *msg; | |
705 | |
706 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
707 g_return_val_if_fail(table != NULL, 0); | |
708 | |
709 | |
710 userid = g_hash_table_lookup(table, "f"); | |
711 msg = g_hash_table_lookup(table, "msg"); | |
712 | |
713 purple_debug_info("msim", | |
714 "msim_incoming_im: got msg <%s> from <%s>, resolving username\n", | |
715 msg, userid); | |
716 | |
717 msim_lookup_user(session, userid, msim_incoming_im_cb, g_strdup(msg)); | |
718 | |
719 return 0; | |
720 } | |
721 | |
722 | |
723 /** | |
724 * Process a message. | |
725 * | |
726 * @param gc Connection. | |
727 * @param table Any message from the server. | |
728 * | |
729 * @return The return value of the function used to process the message, or -1 if | |
730 * called with invalid parameters. | |
731 */ | |
732 static int msim_process(PurpleConnection *gc, GHashTable *table) | |
733 { | |
734 MsimSession *session; | |
735 | |
736 g_return_val_if_fail(gc != NULL, -1); | |
737 g_return_val_if_fail(table != NULL, -1); | |
738 | |
739 session = (MsimSession*)gc->proto_data; | |
740 | |
741 printf("-------- message -------------\n"); | |
742 g_hash_table_foreach(table, print_hash_item, NULL); | |
743 printf("------------------------------\n"); | |
744 | |
745 if (g_hash_table_lookup(table, "nc")) | |
746 { | |
747 return msim_login_challenge(session, table); | |
748 } else if (g_hash_table_lookup(table, "sesskey")) { | |
749 printf("SESSKEY=<%s>\n", (gchar*)g_hash_table_lookup(table, "sesskey")); | |
750 | |
751 purple_connection_update_progress(gc, "Connected", 3, 4); | |
752 | |
753 session->sesskey = g_strdup(g_hash_table_lookup(table, "sesskey")); | |
754 | |
755 /* Comes with: proof,profileid,userid,uniquenick -- all same values | |
756 * (at least for me). */ | |
757 session->userid = g_strdup(g_hash_table_lookup(table, "userid")); | |
758 | |
759 purple_connection_set_state(gc, PURPLE_CONNECTED); | |
760 | |
761 return 0; | |
762 } else if (g_hash_table_lookup(table, "bm")) { | |
763 guint bm; | |
764 | |
765 bm = atoi(g_hash_table_lookup(table, "bm")); | |
766 switch (bm) | |
767 { | |
768 case MSIM_BM_STATUS: | |
769 return msim_status(session, table); | |
770 case MSIM_BM_INSTANT: | |
771 return msim_incoming_im(session, table); | |
772 default: | |
773 /* Not really an IM, but show it for informational | |
774 * purposes during development. */ | |
775 return msim_incoming_im(session, table); | |
776 } | |
777 | |
778 if (bm == MSIM_BM_STATUS) | |
779 { | |
780 return msim_status(session, table); | |
781 } else { /* else if strcmp(bm, "1") == 0) */ | |
782 return msim_incoming_im(session, table); | |
783 } | |
784 } else if (g_hash_table_lookup(table, "rid")) { | |
785 return msim_process_reply(session, table); | |
786 } else if (g_hash_table_lookup(table, "error")) { | |
787 return msim_error(session, table); | |
788 } else if (g_hash_table_lookup(table, "ka")) { | |
789 purple_debug_info("msim", "msim_process: got keep alive\n"); | |
790 return 0; | |
791 } else { | |
792 printf("<<unhandled>>\n"); | |
793 return 0; | |
794 } | |
795 } | |
796 /** | |
797 * Process a message reply from the server. | |
798 * | |
799 * @param session | |
800 * @param table Message reply from server. | |
801 * | |
802 * @return 0, since the 'table' field is no longer needed. | |
803 */ | |
804 static int msim_process_reply(MsimSession *session, GHashTable *table) | |
805 { | |
806 gchar *rid_str; | |
807 | |
808 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
809 g_return_val_if_fail(table != NULL, 0); | |
810 | |
811 rid_str = g_hash_table_lookup(table, "rid"); | |
812 | |
813 if (rid_str) /* msim_lookup_user sets callback for here */ | |
814 { | |
815 MSIM_USER_LOOKUP_CB cb; | |
816 gpointer data; | |
817 guint rid; | |
818 | |
819 GHashTable *body; | |
820 gchar *username; | |
821 | |
822 rid = atol(rid_str); | |
823 | |
824 /* Cache the user info. Currently, the GHashTable of user info in | |
825 * this cache never expires so is never freed. TODO: expire and destroy | |
826 * | |
827 * Some information never changes (username->userid map), some does. | |
828 * TODO: Cache what doesn't change only | |
829 */ | |
830 body = msim_parse_body(g_hash_table_lookup(table, "body")); | |
831 username = g_hash_table_lookup(body, "UserName"); | |
832 if (username) | |
833 { | |
834 g_hash_table_insert(session->user_lookup_cache, g_strdup(username), body); | |
835 } else { | |
836 purple_debug_info("msim", | |
837 "msim_process_reply: not caching <%s>, no UserName\n", | |
838 g_hash_table_lookup(table, "body")); | |
839 } | |
840 | |
841 /* If a callback is registered for this userid lookup, call it. */ | |
842 | |
843 cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid)); | |
844 data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); | |
845 | |
846 if (cb) | |
847 { | |
848 purple_debug_info("msim", | |
849 "msim_process_body: calling callback now\n"); | |
850 cb(session, table, data); | |
851 g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid)); | |
852 g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); | |
853 | |
854 /* Return 1 to tell caller of msim_process (msim_input_cb) to | |
855 * not destroy 'table'; allow 'cb' to hang on to it and destroy | |
856 * it when it wants. */ | |
857 return 1; | |
858 } else { | |
859 purple_debug_info("msim", | |
860 "msim_process_body: no callback for rid %d\n", rid); | |
861 } | |
862 } | |
863 return 0; | |
864 } | |
865 | |
866 /** | |
867 * Handle an error from the server. | |
868 * | |
869 * @param session | |
870 * @param table The message. | |
871 * | |
872 * @return 0, since 'table' can be freed. | |
873 */ | |
874 static int msim_error(MsimSession *session, GHashTable *table) | |
875 { | |
876 gchar *err, *errmsg, *full_errmsg; | |
877 | |
878 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
879 g_return_val_if_fail(table != NULL, 0); | |
880 | |
881 err = g_hash_table_lookup(table, "err"); | |
882 errmsg = g_hash_table_lookup(table, "errmsg"); | |
883 | |
884 full_errmsg = g_strdup_printf("Protocol error, code %s: %s", err, errmsg); | |
885 | |
886 purple_debug_info("msim", "msim_error: %s\n", full_errmsg); | |
887 | |
888 /* TODO: check 'fatal' and die if asked to. | |
889 * TODO: do something with the error # (localization of errmsg?) */ | |
890 purple_notify_error(session->account, g_strdup("MySpaceIM Error"), | |
891 full_errmsg, NULL); | |
892 | |
893 if (g_hash_table_lookup(table, "fatal")) | |
894 { | |
895 purple_debug_info("msim", "fatal error, destroy session\n"); | |
896 purple_connection_error(session->gc, full_errmsg); | |
897 close(session->fd); | |
898 //msim_session_destroy(session); | |
899 } | |
900 | |
901 return 0; | |
902 } | |
903 | |
904 #if 0 | |
905 /* Not sure about this */ | |
906 static void msim_status_now(gchar *who, gpointer data) | |
907 { | |
908 printf("msim_status_now: %s\n", who); | |
909 } | |
910 #endif | |
911 | |
912 /** | |
913 * Callback to update incoming status messages, after looked up username. | |
914 * | |
915 * @param session | |
916 * @param userinfo Looked up user information from server. | |
917 * @param data gchar* status string. | |
918 * | |
919 */ | |
920 static void msim_status_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
921 { | |
922 PurpleBuddyList *blist; | |
923 PurpleBuddy *buddy; | |
924 PurplePresence *presence; | |
925 GHashTable *body; | |
926 //PurpleStatus *status; | |
927 gchar **status_array; | |
928 GList *list; | |
929 gchar *status_text, *status_code; | |
930 gchar *status_str; | |
931 gint i; | |
932 gchar *username; | |
933 | |
934 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
935 g_return_if_fail(userinfo != NULL); | |
936 | |
937 status_str = (gchar*)data; | |
938 | |
939 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
940 g_assert(body); | |
941 | |
942 username = g_hash_table_lookup(body, "UserName"); | |
943 /* Note: DisplayName doesn't seem to be resolvable. It could be displayed on | |
944 * the buddy list, if the UserID was stored along with it. */ | |
945 | |
946 if (!username) | |
947 { | |
948 purple_debug_info("msim", "msim_status_cb: no username?!\n"); | |
949 return; | |
950 } | |
951 | |
952 purple_debug_info("msim", | |
953 "msim_status_cb: updating status for <%s> to <%s>\n", | |
954 username, status_str); | |
955 | |
956 /* TODO: generic functions to split into a GList */ | |
957 status_array = g_strsplit(status_str, "|", 0); | |
958 for (list = NULL, i = 0; | |
959 status_array[i]; | |
960 i++) | |
961 { | |
962 list = g_list_append(list, status_array[i]); | |
963 } | |
964 | |
965 /* Example fields: |s|0|ss|Offline */ | |
966 status_code = g_list_nth_data(list, 2); | |
967 status_text = g_list_nth_data(list, 4); | |
968 | |
969 blist = purple_get_blist(); | |
970 | |
971 /* Add buddy if not found */ | |
972 buddy = purple_find_buddy(session->account, username); | |
973 if (!buddy) | |
974 { | |
975 /* TODO: purple aliases, userids and usernames */ | |
976 purple_debug_info("msim", | |
977 "msim_status: making new buddy for %s\n", username); | |
978 buddy = purple_buddy_new(session->account, username, NULL); | |
979 | |
980 /* TODO: sometimes (when click on it), buddy list disappears. Fix. */ | |
981 purple_blist_add_buddy(buddy, NULL, NULL, NULL); | |
982 } else { | |
983 purple_debug_info("msim", "msim_status: found buddy %s\n", username); | |
984 } | |
985 | |
986 /* For now, always set status to online. | |
987 * TODO: make status reflect reality | |
988 * TODO: show headline */ | |
989 presence = purple_presence_new_for_buddy(buddy); | |
990 purple_presence_set_status_active(presence, MSIM_STATUS_ONLINE, TRUE); | |
991 | |
992 g_strfreev(status_array); | |
993 g_list_free(list); | |
994 g_hash_table_destroy(body); | |
995 g_hash_table_destroy(userinfo); | |
996 /* Do not free status_str - it will be freed by g_hash_table_destroy on session->userid_lookup_cb_data */ | |
997 } | |
998 | |
999 /** | |
1000 * Process incoming status messages. | |
1001 * | |
1002 * @param session | |
1003 * @param table Status update message. | |
1004 * | |
1005 * @return 0, since 'table' can be freed. | |
1006 */ | |
1007 static int msim_status(MsimSession *session, GHashTable *table) | |
1008 { | |
1009 gchar *status_str; | |
1010 gchar *userid; | |
1011 | |
1012 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
1013 g_return_val_if_fail(table != NULL, 0); | |
1014 | |
1015 status_str = g_hash_table_lookup(table, "msg"); | |
1016 if (!status_str) | |
1017 { | |
1018 purple_debug_info("msim", "msim_status: bm=100 but no status msg\n"); | |
1019 return 0; | |
1020 } | |
1021 | |
1022 userid = g_hash_table_lookup(table, "f"); | |
1023 if (!userid) | |
1024 { | |
1025 purple_debug_info("msim", "msim_status: bm=100 but no f field\n"); | |
1026 return 0; | |
1027 } | |
1028 | |
1029 /* TODO: if buddies were identified on buddy list by uid, wouldn't have to lookup | |
1030 * before updating the status! Much more efficient. */ | |
1031 purple_debug_info("msim", | |
1032 "msim_status: got status msg <%s> for <%s>, scheduling lookup\n", | |
1033 status_str, userid); | |
1034 | |
1035 /* Actually update status once obtain username */ | |
1036 msim_lookup_user(session, userid, msim_status_cb, g_strdup(status_str)); | |
1037 | |
1038 return 0; | |
1039 } | |
1040 | |
1041 | |
1042 | |
1043 /** | |
1044 * Callback when input available. | |
1045 * | |
1046 * @param gc_uncasted A PurpleConnection pointer. | |
1047 * @param source File descriptor. | |
1048 * @param cond PURPLE_INPUT_READ | |
1049 * | |
1050 * Reads the input, and dispatches calls msim_process to handle it. | |
1051 */ | |
1052 static void msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond) | |
1053 { | |
1054 PurpleConnection *gc; | |
1055 PurpleAccount *account; | |
1056 MsimSession *session; | |
1057 gchar *end; | |
1058 int n; | |
1059 | |
1060 g_return_if_fail(gc_uncasted != NULL); | |
1061 g_return_if_fail(source >= 0); /* Note: 0 is a valid fd */ | |
1062 | |
1063 gc = (PurpleConnection*)(gc_uncasted); | |
1064 account = purple_connection_get_account(gc); | |
1065 session = gc->proto_data; | |
1066 | |
1067 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
1068 | |
1069 g_assert(cond == PURPLE_INPUT_READ); | |
1070 | |
1071 /* Only can handle so much data at once... | |
1072 * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE. | |
1073 * Should be large enough to hold the largest protocol message. | |
1074 */ | |
1075 if (session->rxoff == MSIM_READ_BUF_SIZE) | |
1076 { | |
1077 purple_debug_error("msim", "msim_input_cb: %d-byte read buffer full!\n", | |
1078 MSIM_READ_BUF_SIZE); | |
1079 purple_connection_error(gc, "Read buffer full"); | |
1080 /* TODO: fix 100% CPU after closing */ | |
1081 close(source); | |
1082 return; | |
1083 } | |
1084 | |
1085 purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n", | |
1086 session->rxoff, MSIM_READ_BUF_SIZE, | |
1087 MSIM_READ_BUF_SIZE - session->rxoff); | |
1088 | |
1089 /* Read into buffer. On Win32, need recv() not read(). session->fd also holds | |
1090 * the file descriptor, but it sometimes differs from the 'source' parameter. | |
1091 */ | |
1092 n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0); | |
1093 | |
1094 if (n < 0 && errno == EAGAIN) | |
1095 { | |
1096 return; | |
1097 } | |
1098 else if (n < 0) | |
1099 { | |
1100 purple_connection_error(gc, "Read error"); | |
1101 purple_debug_error("msim", "msim_input_cb: read error, ret=%d, " | |
1102 "error=%s, source=%d, fd=%d (%X))\n", | |
1103 n, strerror(errno), source, session->fd, session->fd); | |
1104 close(source); | |
1105 return; | |
1106 } | |
1107 else if (n == 0) | |
1108 { | |
1109 purple_debug_info("msim", "msim_input_cb: server disconnected\n"); | |
1110 purple_connection_error(gc, "Server has disconnected"); | |
1111 return; | |
1112 } | |
1113 | |
1114 /* Null terminate */ | |
1115 session->rxbuf[session->rxoff + n] = 0; | |
1116 | |
1117 /* Check for embedded NULs. I don't handle them, and they shouldn't occur. */ | |
1118 if (strlen(session->rxbuf + session->rxoff) != n) | |
1119 { | |
1120 /* Occurs after login, but it is not a null byte. */ | |
1121 purple_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes" | |
1122 "--null byte encountered?\n", | |
1123 strlen(session->rxbuf + session->rxoff), n); | |
1124 //purple_connection_error(gc, "Invalid message - null byte on input"); | |
1125 return; | |
1126 } | |
1127 | |
1128 session->rxoff += n; | |
1129 purple_debug_info("msim", "msim_input_cb: read=%d\n", n); | |
1130 | |
1131 //printf("buf=<%s>\n", session->rxbuf); | |
1132 | |
1133 /* Look for \\final\\ end markers. If found, process message. */ | |
1134 while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) | |
1135 { | |
1136 GHashTable *table; | |
1137 | |
1138 //printf("in loop: buf=<%s>\n", session->rxbuf); | |
1139 *end = 0; | |
1140 table = msim_parse(g_strdup(session->rxbuf)); | |
1141 if (!table) | |
1142 { | |
1143 purple_debug_info("msim", "msim_input_cb: couldn't parse <%s>\n", | |
1144 session->rxbuf); | |
1145 purple_connection_error(gc, "Unparseable message"); | |
1146 } | |
1147 else | |
1148 { | |
1149 /* Process message. Returns 0 to free */ | |
1150 if (msim_process(gc, table) == 0) | |
1151 g_hash_table_destroy(table); | |
1152 } | |
1153 | |
1154 /* Move remaining part of buffer to beginning. */ | |
1155 session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING); | |
1156 memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), | |
1157 MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf)); | |
1158 | |
1159 /* Clear end of buffer */ | |
1160 //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf)); | |
1161 } | |
1162 } | |
1163 | |
1164 /** | |
1165 * Callback when connected. Sets up input handlers. | |
1166 * | |
1167 * @param data A PurpleConnection pointer. | |
1168 * @param source File descriptor. | |
1169 * @param error_message | |
1170 */ | |
1171 static void msim_connect_cb(gpointer data, gint source, const gchar *error_message) | |
1172 { | |
1173 PurpleConnection *gc; | |
1174 MsimSession *session; | |
1175 | |
1176 g_return_if_fail(data != NULL); | |
1177 | |
1178 gc = (PurpleConnection*)data; | |
1179 session = gc->proto_data; | |
1180 | |
1181 if (source < 0) | |
1182 { | |
1183 purple_connection_error(gc, "Couldn't connect to host"); | |
1184 purple_connection_error(gc, g_strdup_printf("Couldn't connect to host: %s (%d)", | |
1185 error_message, source)); | |
1186 return; | |
1187 } | |
1188 | |
1189 session->fd = source; | |
1190 | |
1191 gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc); | |
1192 } | |
1193 | |
1194 /* Session methods */ | |
1195 | |
1196 /** | |
1197 * Create a new MSIM session. | |
1198 * | |
1199 * @param acct The account to create the session from. | |
1200 * | |
1201 * @return Pointer to a new session. Free with msim_session_destroy. | |
1202 */ | |
1203 static MsimSession *msim_session_new(PurpleAccount *acct) | |
1204 { | |
1205 MsimSession *session; | |
1206 | |
1207 g_return_val_if_fail(acct != NULL, NULL); | |
1208 | |
1209 session = g_new0(MsimSession, 1); | |
1210 | |
1211 session->magic = MSIM_SESSION_STRUCT_MAGIC; | |
1212 session->account = acct; | |
1213 session->gc = purple_account_get_connection(acct); | |
1214 session->fd = -1; | |
1215 session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL); /* do NOT free function pointers! */ | |
1216 session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free); | |
1217 session->user_lookup_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_hash_table_destroy); | |
1218 session->rxoff = 0; | |
1219 session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE); | |
1220 | |
1221 return session; | |
1222 } | |
1223 | |
1224 /** | |
1225 * Free a session. | |
1226 * | |
1227 * @param session The session to destroy. | |
1228 */ | |
1229 static void msim_session_destroy(MsimSession *session) | |
1230 { | |
1231 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
1232 | |
1233 session->magic = -1; | |
1234 | |
1235 g_free(session->rxbuf); | |
1236 g_free(session->userid); | |
1237 g_free(session->sesskey); | |
1238 | |
1239 g_free(session); | |
1240 } | |
1241 | |
1242 | |
1243 | |
1244 /** | |
1245 * Close the connection. | |
1246 * | |
1247 * @param gc The connection. | |
1248 */ | |
1249 static void msim_close(PurpleConnection *gc) | |
1250 { | |
1251 g_return_if_fail(gc != NULL); | |
1252 | |
1253 msim_session_destroy(gc->proto_data); | |
1254 } | |
1255 | |
1256 | |
1257 /** | |
1258 * Check if a string is a userid (all numeric). | |
1259 * | |
1260 * @param user The user id, email, or name. | |
1261 * | |
1262 * @return TRUE if is userid, FALSE if not. | |
1263 */ | |
1264 static inline gboolean msim_is_userid(const gchar *user) | |
1265 { | |
1266 g_return_val_if_fail(user != NULL, FALSE); | |
1267 | |
1268 return strspn(user, "0123456789") == strlen(user); | |
1269 } | |
1270 | |
1271 /** | |
1272 * Check if a string is an email address (contains an @). | |
1273 * | |
1274 * @param user The user id, email, or name. | |
1275 * | |
1276 * @return TRUE if is an email, FALSE if not. | |
1277 * | |
1278 * This function is not intended to be used as a generic | |
1279 * means of validating email addresses, but to distinguish | |
1280 * between a user represented by an email address from | |
1281 * other forms of identification. | |
1282 */ | |
1283 static inline gboolean msim_is_email(const gchar *user) | |
1284 { | |
1285 g_return_val_if_fail(user != NULL, FALSE); | |
1286 | |
1287 return strchr(user, '@') != NULL; | |
1288 } | |
1289 | |
1290 | |
1291 /** | |
1292 * Asynchronously lookup user information, calling callback when receive result. | |
1293 * | |
1294 * @param session | |
1295 * @param user The user id, email address, or username. | |
1296 * @param cb Callback, called with user information when available. | |
1297 * @param data An arbitray data pointer passed to the callback. | |
1298 */ | |
1299 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data) | |
1300 { | |
1301 gchar *field_name; | |
1302 gchar *msg_string; | |
1303 guint rid, cmd, dsn, lid; | |
1304 | |
1305 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
1306 g_return_if_fail(user != NULL); | |
1307 g_return_if_fail(cb != NULL); | |
1308 | |
1309 purple_debug_info("msim", "msim_lookup_userid", | |
1310 "asynchronously looking up <%s>\n", user); | |
1311 | |
1312 /* TODO: check if this user's info was cached and fresh; if so return immediately */ | |
1313 #if 0 | |
1314 /* If already know userid, then call callback immediately */ | |
1315 cached_userid = g_hash_table_lookup(session->userid_cache, who); | |
1316 if (cached_userid && !by_userid) | |
1317 { | |
1318 cb(cached_userid, NULL, NULL, data); | |
1319 return; | |
1320 } | |
1321 #endif | |
1322 | |
1323 rid = rand(); //om(); | |
1324 | |
1325 /* Setup callback. Response will be associated with request using 'rid'. */ | |
1326 g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb); | |
1327 g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data); | |
1328 | |
1329 /* Send request */ | |
1330 | |
1331 cmd = 1; | |
1332 | |
1333 if (msim_is_userid(user)) | |
1334 { | |
1335 /* TODO: document cmd,dsn,lid */ | |
1336 field_name = "UserID"; | |
1337 dsn = 4; | |
1338 lid = 3; | |
1339 } else if (msim_is_email(user)) { | |
1340 field_name = "Email"; | |
1341 dsn = 5; | |
1342 lid = 7; | |
1343 } else { | |
1344 field_name = "UserName"; | |
1345 dsn = 5; | |
1346 lid = 7; | |
1347 } | |
1348 | |
1349 msg_string = g_strdup_printf("\\persist\\1\\sesskey\\%s\\cmd\\1\\dsn\\%d\\uid\\%s\\lid\\%d\\rid\\%d\\body\\%s=%s\\final\\", | |
1350 session->sesskey, dsn, session->userid, lid, rid, field_name, user); | |
1351 | |
1352 msim_send(session, msg_string); | |
1353 } | |
1354 | |
1422 | 1355 |
1423 /** | 1356 /** |
1424 * Obtain the status text for a buddy. | 1357 * Obtain the status text for a buddy. |
1425 * | 1358 * |
1426 * @param buddy The buddy to obtain status text for. | 1359 * @param buddy The buddy to obtain status text for. |