Mercurial > pidgin
annotate libpurple/protocols/myspace/myspace.c @ 16400:9c9c627dbbfe
Use Purple Cipher API for RC4.
author | Jeffrey Connelly <jaconnel@calpoly.edu> |
---|---|
date | Wed, 25 Apr 2007 03:46:17 +0000 |
parents | 70c069168459 |
children | 10d2958bd632 |
rev | line source |
---|---|
16394 | 1 /* MySpaceIM Protocol Plugin |
2 * | |
3 * \author Jeff Connelly | |
4 * | |
5 * Copyright (C) 2007, Jeff Connelly <myspaceim@xyzzy.cjb.net> | |
6 * | |
16396 | 7 * Based on Purple's "C Plugin HOWTO" hello world example. |
16394 | 8 * |
9 * Code also drawn from myspace: | |
16396 | 10 * http://snarfed.org/space/purple+mock+protocol+plugin |
16394 | 11 * Copyright (C) 2004-2007, Ryan Barrett <mockprpl@ryanb.org> |
12 * | |
16396 | 13 * and some constructs also based on existing Purple plugins, which are: |
14 * Copyright (C) 2003, Robbert Haarman <purple@inglorion.net> | |
16394 | 15 * Copyright (C) 2003, Ethan Blanton <eblanton@cs.purdue.edu> |
16 * Copyright (C) 2000-2003, Rob Flynn <rob@tgflinux.com> | |
17 * Copyright (C) 1998-1999, Mark Spencer <markster@marko.net> | |
18 * | |
19 * This program is free software; you can redistribute it and/or modify | |
20 * it under the terms of the GNU General Public License as published by | |
21 * the Free Software Foundation; either version 2 of the License, or | |
22 * (at your option) any later version. | |
23 * | |
24 * This program is distributed in the hope that it will be useful, | |
25 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
27 * GNU General Public License for more details. | |
28 * | |
29 * You should have received a copy of the GNU General Public License | |
30 * along with this program; if not, write to the Free Software | |
31 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
32 */ | |
33 | |
16396 | 34 #define PURPLE_PLUGIN |
16394 | 35 |
36 #include <string.h> | |
37 #include <errno.h> /* for EAGAIN */ | |
38 | |
39 #include <glib.h> | |
40 | |
41 #ifdef _WIN32 | |
42 #include "win32dep.h" | |
43 #else | |
44 /* For recv() and send(); needed to match Win32 */ | |
45 #include <sys/types.h> | |
46 #include <sys/socket.h> | |
47 #endif | |
48 | |
16396 | 49 #include "internal.h" |
50 | |
16394 | 51 #include "notify.h" |
52 #include "plugin.h" | |
53 #include "version.h" | |
54 #include "cipher.h" /* for SHA-1 */ | |
55 #include "util.h" /* for base64 */ | |
16396 | 56 #include "debug.h" /* for purple_debug_info */ |
57 | |
16394 | 58 #define MSIM_STATUS_ONLINE "online" |
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 */ | |
16396 | 89 PurpleAccount *account; |
90 PurpleConnection *gc; | |
16394 | 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; | |
16396 | 114 PurpleMessageFlags flags; |
16394 | 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 | |
16396 | 123 static void init_plugin(PurplePlugin *plugin) |
16394 | 124 { |
16396 | 125 purple_notify_message(plugin, PURPLE_NOTIFY_MSG_INFO, "Hello World!", |
16394 | 126 "This is the Hello World! plugin :)", NULL, NULL, NULL); |
127 } | |
128 | |
129 /** | |
130 * Get possible user status types. Based on mockprpl. | |
131 * | |
132 * @return GList of status types. | |
133 */ | |
16396 | 134 static GList *msim_status_types(PurpleAccount *acct) |
16394 | 135 { |
136 GList *types; | |
16396 | 137 PurpleStatusType *type; |
16394 | 138 |
16396 | 139 purple_debug_info("myspace", "returning status types for %s: %s, %s, %s\n", |
16394 | 140 acct->username, |
141 MSIM_STATUS_ONLINE, MSIM_STATUS_AWAY, MSIM_STATUS_OFFLINE, MSIM_STATUS_INVISIBLE); | |
142 | |
143 | |
144 types = NULL; | |
145 | |
16396 | 146 type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, MSIM_STATUS_ONLINE, |
16394 | 147 MSIM_STATUS_ONLINE, TRUE); |
16396 | 148 purple_status_type_add_attr(type, "message", "Online", |
149 purple_value_new(PURPLE_TYPE_STRING)); | |
16394 | 150 types = g_list_append(types, type); |
151 | |
16396 | 152 type = purple_status_type_new(PURPLE_STATUS_AWAY, MSIM_STATUS_AWAY, |
16394 | 153 MSIM_STATUS_AWAY, TRUE); |
16396 | 154 purple_status_type_add_attr(type, "message", "Away", |
155 purple_value_new(PURPLE_TYPE_STRING)); | |
16394 | 156 types = g_list_append(types, type); |
157 | |
16396 | 158 type = purple_status_type_new(PURPLE_STATUS_OFFLINE, MSIM_STATUS_OFFLINE, |
16394 | 159 MSIM_STATUS_OFFLINE, TRUE); |
16396 | 160 purple_status_type_add_attr(type, "message", "Offline", |
161 purple_value_new(PURPLE_TYPE_STRING)); | |
16394 | 162 types = g_list_append(types, type); |
163 | |
16396 | 164 type = purple_status_type_new(PURPLE_STATUS_INVISIBLE, MSIM_STATUS_INVISIBLE, |
16394 | 165 MSIM_STATUS_INVISIBLE, TRUE); |
16396 | 166 purple_status_type_add_attr(type, "message", "Invisible", |
167 purple_value_new(PURPLE_TYPE_STRING)); | |
16394 | 168 types = g_list_append(types, type); |
169 | |
170 return types; | |
171 } | |
172 | |
173 /** | |
174 * Parse a MySpaceIM protocol message into a hash table. | |
175 * | |
176 * @param msg The message string to parse, will be g_free()'d. | |
177 * | |
178 * @return Hash table of message. Caller should destroy with | |
179 * g_hash_table_destroy() when done. | |
180 */ | |
181 static GHashTable* msim_parse(gchar* msg) | |
182 { | |
183 GHashTable *table; | |
184 gchar *token; | |
185 gchar **tokens; | |
186 gchar *key; | |
187 gchar *value; | |
188 int i; | |
189 | |
190 g_return_val_if_fail(msg != NULL, NULL); | |
191 | |
16396 | 192 purple_debug_info("msim", "msim_parse: got <%s>\n", msg); |
16394 | 193 |
194 key = NULL; | |
195 | |
196 /* All messages begin with a \ */ | |
197 if (msg[0] != '\\' || msg[1] == 0) | |
198 { | |
16396 | 199 purple_debug_info("msim", "msim_parse: incomplete/bad msg, " |
16394 | 200 "missing initial backslash: <%s>", msg); |
201 /* XXX: Should we try to recover, and read to first backslash? */ | |
202 | |
203 g_free(msg); | |
204 return NULL; | |
205 } | |
206 | |
207 /* Create table of strings, set to call g_free on destroy. */ | |
208 table = g_hash_table_new_full((GHashFunc)g_str_hash, | |
209 (GEqualFunc)g_str_equal, g_free, g_free); | |
210 | |
211 for (tokens = g_strsplit(msg + 1, "\\", 0), i = 0; | |
212 (token = tokens[i]); | |
213 i++) | |
214 { | |
215 //printf("tok=<%s>, i%2=%d\n", token, i % 2); | |
216 if (i % 2) | |
217 { | |
218 value = token; | |
219 | |
220 /* Check if key already exists */ | |
221 if (g_hash_table_lookup(table, key) == NULL) | |
222 { | |
223 //printf("insert: |%s|=|%s|\n", key, value); | |
224 g_hash_table_insert(table, g_strdup(key), g_strdup(value)); | |
225 } else { | |
226 /* TODO: Some dictionaries have multiple values for the same | |
227 * key. Should append to a GList to handle this case. */ | |
16396 | 228 purple_debug_info("msim", "msim_parse: key %s already exists, " |
16394 | 229 "not overwriting or replacing; ignoring new value %s", key, |
230 value); | |
231 } | |
232 } else { | |
233 key = token; | |
234 } | |
235 } | |
236 g_strfreev(tokens); | |
237 | |
238 /* Can free now since all data was copied to hash key/values */ | |
239 g_free(msg); | |
240 | |
241 return table; | |
242 } | |
243 | |
244 /** | |
245 * Compute the base64'd login challenge response based on username, password, nonce, and IPs. | |
246 * | |
247 * @param nonce The base64 encoded nonce ('nc') field from the server. | |
248 * @param email User's email address (used as login name). | |
249 * @param password User's cleartext password. | |
250 * | |
251 * @return Encoded login challenge response, ready to send to the server. Must be g_free()'d | |
252 * when finished. | |
253 */ | |
254 static gchar* msim_compute_login_response(guchar nonce[2*NONCE_HALF_SIZE], gchar* email, gchar* password) | |
255 { | |
16396 | 256 PurpleCipherContext *key_context; |
257 PurpleCipher *sha1; | |
16400
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
258 PurpleCipherContext *rc4; |
16394 | 259 guchar hash_pw[HASH_SIZE]; |
260 guchar key[HASH_SIZE]; | |
261 gchar* password_utf16le; | |
262 guchar* data; | |
16400
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
263 guchar* data_out; |
16394 | 264 gchar* response; |
16400
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
265 int i; |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
266 size_t data_len, data_out_len; |
16394 | 267 |
268 //memset(nonce, 0, NONCE_HALF_SIZE); | |
269 //memset(nonce + NONCE_HALF_SIZE, 1, NONCE_HALF_SIZE); | |
270 | |
271 /* Convert ASCII password to UTF16 little endian */ | |
272 /* TODO: use the built-in facility to do this, like Nathan Peterson does. */ | |
16396 | 273 purple_debug_info("msim", "converting password to utf16le\n"); |
16394 | 274 //printf("pw=<%s>\n",password); |
275 password_utf16le = g_new0(gchar, strlen(password) * 2); | |
276 for (i = 0; i < strlen(password) * 2; i += 2) | |
277 { | |
278 password_utf16le[i] = password[i / 2]; | |
279 password_utf16le[i + 1] = 0; | |
280 } | |
281 | |
282 /* Compute password hash */ | |
16396 | 283 purple_cipher_digest_region("sha1", (guchar*)password_utf16le, strlen(password) * 2, |
16394 | 284 sizeof(hash_pw), hash_pw, NULL); |
285 | |
286 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE | |
287 printf("pwhash = "); | |
288 for (i = 0; i < sizeof(hash_pw); i++) | |
289 printf("%.2x ", hash_pw[i]); | |
290 printf("\n"); | |
291 #endif | |
292 | |
293 /* key = sha1(sha1(pw) + nonce2) */ | |
16396 | 294 sha1 = purple_ciphers_find_cipher("sha1"); |
295 key_context = purple_cipher_context_new(sha1, NULL); | |
296 purple_cipher_context_append(key_context, hash_pw, HASH_SIZE); | |
297 purple_cipher_context_append(key_context, nonce + NONCE_HALF_SIZE, NONCE_HALF_SIZE); | |
298 purple_cipher_context_digest(key_context, sizeof(key), key, NULL); | |
16394 | 299 |
300 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE | |
301 printf("key = "); | |
302 for (i = 0; i < sizeof(key); i++) | |
303 { | |
304 printf("%.2x ", key[i]); | |
305 } | |
306 printf("\n"); | |
307 #endif | |
308 | |
16400
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
309 rc4 = purple_cipher_context_new_by_name("rc4", NULL); |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
310 |
16394 | 311 /* Note: 'key' variable is 0x14 bytes (from SHA-1 hash), |
312 * but only first 0x10 used for the RC4 key. */ | |
16400
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
313 purple_cipher_context_set_option(rc4, "key_len", (gpointer)0x10); |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
314 purple_cipher_context_set_key(rc4, key); |
16394 | 315 |
316 /* TODO: obtain IPs of network interfaces. This is not immediately | |
317 * important because you can still connect and perform basic | |
318 * functions of the protocol. There is also a high chance that the addreses | |
319 * are RFC1918 private, so the servers couldn't do anything with them | |
320 * anyways except make note of that fact. Probably important for any | |
321 * kind of direct connection, or file transfer functionality. | |
322 */ | |
323 /* rc4 encrypt: | |
324 * nonce1+email+IP list */ | |
325 data_len = NONCE_HALF_SIZE + strlen(email) + 25; | |
326 data = g_new0(guchar, data_len); | |
327 memcpy(data, nonce, NONCE_HALF_SIZE); | |
328 memcpy(data + NONCE_HALF_SIZE, email, strlen(email)); | |
329 memcpy(data + NONCE_HALF_SIZE + strlen(email), | |
330 /* IP addresses of network interfaces */ | |
331 "\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); | |
16400
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
332 // crypt_rc4(&rc4, data, data_len); |
16394 | 333 |
16400
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
334 data_out = g_new0(guchar, data_len); |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
335 purple_cipher_context_encrypt(rc4, (const guchar*)data, |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
336 data_len, data_out, &data_out_len); |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
337 g_assert(data_out_len == data_len); |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
338 purple_cipher_context_destroy(rc4); |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
339 |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
340 response = purple_base64_encode(data_out, data_out_len); |
9c9c627dbbfe
Use Purple Cipher API for RC4.
Jeffrey Connelly <jaconnel@calpoly.edu>
parents:
16396
diff
changeset
|
341 g_free(data_out); |
16394 | 342 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE |
343 printf("response=<%s>\n", response); | |
344 #endif | |
345 | |
346 return response; | |
347 } | |
348 | |
349 static void print_hash_item(gpointer key, gpointer value, gpointer user_data) | |
350 { | |
351 printf("%s=%s\n", (char*)key, (char*)value); | |
352 } | |
353 | |
354 /** | |
355 * Send an arbitrary protocol message. | |
356 * | |
357 * @param session | |
358 * @param msg The textual, encoded message to send. | |
359 * | |
360 * Note: this does not send instant messages. For that, see msim_send_im. | |
361 */ | |
362 static void msim_send(MsimSession *session, const gchar *msg) | |
363 { | |
364 int ret; | |
365 | |
366 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
367 g_return_if_fail(msg != NULL); | |
368 | |
16396 | 369 purple_debug_info("msim", "msim_send: writing <%s>\n", msg); |
16394 | 370 |
371 ret = send(session->fd, msg, strlen(msg), 0); | |
372 | |
373 if (ret != strlen(msg)) | |
374 { | |
16396 | 375 purple_debug_info("msim", "msim_send(%s): strlen=%d, but only wrote %s\n", |
16394 | 376 msg, strlen(msg), ret); |
377 /* TODO: better error */ | |
378 } | |
379 } | |
380 | |
381 /** | |
382 * Process a login challenge, sending a response. | |
383 * | |
384 * @param session | |
385 * @param table Hash table of login challenge message. | |
386 * | |
387 * @return 0, since the 'table' parameter is no longer needed. | |
388 */ | |
389 static int msim_login_challenge(MsimSession *session, GHashTable *table) | |
390 { | |
16396 | 391 PurpleAccount *account; |
16394 | 392 gchar *nc_str; |
393 guchar *nc; | |
394 gchar *response_str; | |
395 gsize nc_len; | |
396 gchar *buf; | |
397 | |
398 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
399 g_return_val_if_fail(table != NULL, 0); | |
400 | |
401 nc_str = g_hash_table_lookup(table, "nc"); | |
402 | |
403 account = session->account; | |
404 //assert(account); | |
405 | |
16396 | 406 purple_connection_update_progress(session->gc, "Reading challenge", 1, 4); |
16394 | 407 |
16396 | 408 purple_debug_info("msim", "nc=<%s>\n", nc_str); |
16394 | 409 |
16396 | 410 nc = (guchar*)purple_base64_decode(nc_str, &nc_len); |
411 purple_debug_info("msim", "base64 decoded to %d bytes\n", nc_len); | |
16394 | 412 if (nc_len != 0x40) |
413 { | |
16396 | 414 purple_debug_info("msim", "bad nc length: %x != 0x40\n", nc_len); |
415 purple_connection_error(session->gc, "Unexpected challenge length from server"); | |
16394 | 416 return 0; |
417 } | |
418 | |
16396 | 419 purple_connection_update_progress(session->gc, "Logging in", 2, 4); |
16394 | 420 |
421 printf("going to compute login response\n"); | |
422 //response_str = msim_compute_login_response(nc_str, "testuser", "testpw"); //session->gc->account->username, session->gc->account->password); | |
423 response_str = msim_compute_login_response(nc, account->username, account->password); | |
424 printf("got back login response\n"); | |
425 | |
426 g_free(nc); | |
427 | |
428 /* Reply */ | |
429 buf = g_strdup_printf("\\login2\\%d\\username\\%s\\response\\%s\\clientver\\%d\\reconn\\%d\\status\\%d\\id\\1\\final\\", | |
430 196610, account->username, response_str, MSIM_CLIENT_VERSION, 0, 100); | |
431 | |
432 g_free(response_str); | |
433 | |
16396 | 434 purple_debug_info("msim", "response=<%s>\n", buf); |
16394 | 435 |
436 msim_send(session, buf); | |
437 | |
438 g_free(buf); | |
439 | |
440 return 0; | |
441 } | |
442 | |
443 /** | |
444 * Parse a \x1c-separated "dictionary" of key=value pairs into a hash table. | |
445 * | |
446 * @param body_str The text of the dictionary to parse. Often the | |
447 * value for the 'body' field. | |
448 * | |
449 * @return Hash table of the keys and values. Must g_hash_table_destroy() when done. | |
450 */ | |
451 static GHashTable *msim_parse_body(const gchar *body_str) | |
452 { | |
453 GHashTable *table; | |
454 gchar *item; | |
455 gchar **items; | |
456 gchar **elements; | |
457 guint i; | |
458 | |
459 g_return_val_if_fail(body_str != NULL, NULL); | |
460 | |
461 table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); | |
462 | |
463 for (items = g_strsplit(body_str, "\x1c", 0), i = 0; | |
464 (item = items[i]); | |
465 i++) | |
466 { | |
467 gchar *key, *value; | |
468 | |
469 //printf("TOK=<%s>\n", token); | |
470 elements = g_strsplit(item, "=", 2); | |
471 | |
472 key = elements[0]; | |
473 if (!key) | |
474 { | |
16396 | 475 purple_debug_info("msim", "msim_parse_body(%s): null key", body_str); |
16394 | 476 g_strfreev(elements); |
477 break; | |
478 } | |
479 | |
480 value = elements[1]; | |
481 if (!value) | |
482 { | |
16396 | 483 purple_debug_info("msim", "msim_parse_body(%s): null value", body_str); |
16394 | 484 g_strfreev(elements); |
485 break; | |
486 } | |
487 | |
488 //printf("-- %s: %s\n", key, value); | |
489 | |
490 /* XXX: This overwrites duplicates. */ | |
491 /* TODO: make the GHashTable values be GList's, and append to the list if | |
492 * there is already a value of the same key name. This is important for | |
493 * the WebChallenge message. */ | |
494 g_hash_table_insert(table, g_strdup(key), g_strdup(value)); | |
495 | |
496 g_strfreev(elements); | |
497 } | |
498 | |
499 g_strfreev(items); | |
500 | |
501 return table; | |
502 } | |
503 | |
504 /** | |
505 * Immediately send an IM to a user, by their numeric user ID. | |
506 * | |
507 * @param session | |
508 * @param userid ASCII numeric userid. | |
509 * @param message Text of message to send. | |
16396 | 510 * @param flags Purple instant message flags. |
16394 | 511 * |
512 * @return 0, since the 'table' parameter is no longer needed. | |
513 * | |
514 */ | |
16396 | 515 static int msim_send_im_by_userid(MsimSession *session, const gchar *userid, const gchar *message, PurpleMessageFlags flags) |
16394 | 516 { |
517 gchar *msg_string; | |
518 | |
519 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
520 g_return_val_if_fail(userid != NULL, 0); | |
521 g_return_val_if_fail(msim_is_userid(userid) == TRUE, 0); | |
522 g_return_val_if_fail(message != NULL, 0); | |
523 | |
524 /* TODO: constants for bm types */ | |
525 msg_string = g_strdup_printf("\\bm\\121\\sesskey\\%s\\t\\%s\\cv\\%d\\msg\\%s\\final\\", | |
526 session->sesskey, userid, MSIM_CLIENT_VERSION, message); | |
527 | |
16396 | 528 purple_debug_info("msim", "going to write: %s\n", msg_string); |
16394 | 529 |
530 msim_send(session, msg_string); | |
531 | |
16396 | 532 /* TODO: notify Purple that we sent the IM. */ |
16394 | 533 |
534 /* Not needed since sending messages to yourself is allowed by MSIM! */ | |
535 /*if (strcmp(from_username, who) == 0) | |
16396 | 536 serv_got_im(gc, from_username, message, PURPLE_MESSAGE_RECV, time(NULL)); |
16394 | 537 */ |
538 | |
539 return 0; | |
540 } | |
541 | |
542 | |
543 /** | |
544 * Callback called when ready to send an IM by userid (the userid has been looked up). | |
545 * Calls msim_send_im_by_userid. | |
546 * | |
547 * @param session | |
548 * @param userinfo User info message from server containing a 'body' field | |
549 * with a 'UserID' key. This is where the user ID is taken from. | |
550 * @param data A send_im_cb_struct* of information on the IM to send. | |
551 * | |
552 */ | |
553 static void msim_send_im_by_userid_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
554 { | |
555 send_im_cb_struct *s; | |
556 gchar *userid; | |
557 GHashTable *body; | |
558 | |
559 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
560 g_return_if_fail(userinfo != NULL); | |
561 | |
562 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
563 g_assert(body); | |
564 | |
565 userid = g_hash_table_lookup(body, "UserID"); | |
566 | |
567 s = (send_im_cb_struct*)data; | |
568 msim_send_im_by_userid(session, userid, s->message, s->flags); | |
569 | |
570 g_hash_table_destroy(body); | |
571 g_hash_table_destroy(userinfo); | |
572 g_free(s->message); | |
573 g_free(s->who); | |
574 } | |
575 | |
576 /** | |
577 * Process a message reply from the server. | |
578 * | |
579 * @param session | |
580 * @param table Message reply from server. | |
581 * | |
582 * @return 0, since the 'table' field is no longer needed. | |
583 */ | |
584 static int msim_process_reply(MsimSession *session, GHashTable *table) | |
585 { | |
586 gchar *rid_str; | |
587 | |
588 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
589 g_return_val_if_fail(table != NULL, 0); | |
590 | |
591 rid_str = g_hash_table_lookup(table, "rid"); | |
592 | |
593 if (rid_str) /* msim_lookup_user sets callback for here */ | |
594 { | |
595 MSIM_USER_LOOKUP_CB cb; | |
596 gpointer data; | |
597 guint rid; | |
598 | |
599 GHashTable *body; | |
600 gchar *username; | |
601 | |
602 rid = atol(rid_str); | |
603 | |
604 /* Cache the user info. Currently, the GHashTable of user info in | |
605 * this cache never expires so is never freed. TODO: expire and destroy | |
606 * | |
607 * Some information never changes (username->userid map), some does. | |
608 * TODO: Cache what doesn't change only | |
609 */ | |
610 body = msim_parse_body(g_hash_table_lookup(table, "body")); | |
611 username = g_hash_table_lookup(body, "UserName"); | |
612 if (username) | |
613 { | |
614 g_hash_table_insert(session->user_lookup_cache, g_strdup(username), body); | |
615 } else { | |
16396 | 616 purple_debug_info("msim", "msim_process_reply: not caching <%s>, no UserName", |
16394 | 617 g_hash_table_lookup(table, "body")); |
618 } | |
619 | |
620 /* If a callback is registered for this userid lookup, call it. */ | |
621 | |
622 cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid)); | |
623 data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); | |
624 | |
625 if (cb) | |
626 { | |
16396 | 627 purple_debug_info("msim", "msim_process_body: calling callback now\n"); |
16394 | 628 cb(session, table, data); |
629 g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid)); | |
630 g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); | |
631 | |
632 /* Return 1 to tell caller of msim_process (msim_input_cb) to | |
633 * not destroy 'table'; allow 'cb' to hang on to it and destroy | |
634 * it when it wants. */ | |
635 return 1; | |
636 } else { | |
16396 | 637 purple_debug_info("msim", "msim_process_body: no callback for rid %d\n", rid); |
16394 | 638 } |
639 } | |
640 return 0; | |
641 } | |
642 | |
643 /** | |
644 * Handle an error from the server. | |
645 * | |
646 * @param session | |
647 * @param table The message. | |
648 * | |
649 * @return 0, since 'table' can be freed. | |
650 */ | |
651 static int msim_error(MsimSession *session, GHashTable *table) | |
652 { | |
653 gchar *err, *errmsg, *full_errmsg; | |
654 | |
655 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
656 g_return_val_if_fail(table != NULL, 0); | |
657 | |
658 err = g_hash_table_lookup(table, "err"); | |
659 errmsg = g_hash_table_lookup(table, "errmsg"); | |
660 | |
661 full_errmsg = g_strdup_printf("Protocol error, code %s: %s", err, errmsg); | |
662 | |
16396 | 663 purple_debug_info("msim", "msim_error: %s\n", full_errmsg); |
16394 | 664 |
665 /* TODO: check 'fatal' and die if asked to. | |
666 * TODO: do something with the error # (localization of errmsg?) */ | |
16396 | 667 purple_notify_error(session->account, g_strdup("MySpaceIM Error"), |
16394 | 668 full_errmsg, NULL); |
669 | |
670 if (g_hash_table_lookup(table, "fatal")) | |
671 { | |
16396 | 672 purple_debug_info("msim", "fatal error, destroy session\n"); |
673 purple_connection_error(session->gc, full_errmsg); | |
16394 | 674 close(session->fd); |
675 //msim_session_destroy(session); | |
676 } | |
677 | |
678 return 0; | |
679 } | |
680 | |
681 /** | |
682 * Callback to handle incoming messages, after resolving userid. | |
683 * | |
684 * @param session | |
685 * @param userinfo Message from server on user's info, containing UserName. | |
686 * @param data A gchar* of the incoming instant message's text. | |
687 */ | |
688 static void msim_incoming_im_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
689 { | |
690 gchar *msg; | |
691 gchar *username; | |
692 GHashTable *body; | |
693 | |
694 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
695 g_return_if_fail(userinfo != NULL); | |
696 | |
697 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
698 g_assert(body != NULL); | |
699 | |
700 username = g_hash_table_lookup(body, "UserName"); | |
701 | |
702 msg = (gchar*)data; | |
16396 | 703 serv_got_im(session->gc, username, msg, PURPLE_MESSAGE_RECV, time(NULL)); |
16394 | 704 |
705 g_hash_table_destroy(body); | |
706 g_hash_table_destroy(userinfo); | |
707 } | |
708 | |
709 /** | |
710 * Handle an incoming message. | |
711 * | |
712 * @param session The session | |
713 * @param table Message from the server, containing 'f' (userid from) and 'msg'. | |
714 * | |
715 * @return 0, since table can be freed. | |
716 */ | |
717 static int msim_incoming_im(MsimSession *session, GHashTable *table) | |
718 { | |
719 gchar *userid; | |
720 gchar *msg; | |
721 | |
722 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
723 g_return_val_if_fail(table != NULL, 0); | |
724 | |
725 | |
726 userid = g_hash_table_lookup(table, "f"); | |
727 msg = g_hash_table_lookup(table, "msg"); | |
728 | |
16396 | 729 purple_debug_info("msim", "msim_incoming_im: got msg <%s> from <%s>, resolving username\n", |
16394 | 730 msg, userid); |
731 | |
732 msim_lookup_user(session, userid, msim_incoming_im_cb, g_strdup(msg)); | |
733 | |
734 return 0; | |
735 } | |
736 | |
737 #if 0 | |
738 /* Not sure about this */ | |
739 static void msim_status_now(gchar *who, gpointer data) | |
740 { | |
741 printf("msim_status_now: %s\n", who); | |
742 } | |
743 #endif | |
744 | |
745 /** | |
746 * Callback to update incoming status messages, after looked up username. | |
747 * | |
748 * @param session | |
749 * @param userinfo Looked up user information from server. | |
750 * @param data gchar* status string. | |
751 * | |
752 */ | |
753 static void msim_status_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
754 { | |
16396 | 755 PurpleBuddyList *blist; |
756 PurpleBuddy *buddy; | |
757 PurplePresence *presence; | |
16394 | 758 GHashTable *body; |
16396 | 759 //PurpleStatus *status; |
16394 | 760 gchar **status_array; |
761 GList *list; | |
762 gchar *status_text, *status_code; | |
763 gchar *status_str; | |
764 gint i; | |
765 gchar *username; | |
766 | |
767 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
768 g_return_if_fail(userinfo != NULL); | |
769 | |
770 status_str = (gchar*)data; | |
771 | |
772 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
773 g_assert(body); | |
774 | |
775 username = g_hash_table_lookup(body, "UserName"); | |
776 /* Note: DisplayName doesn't seem to be resolvable. It could be displayed on | |
777 * the buddy list, if the UserID was stored along with it. */ | |
778 | |
779 if (!username) | |
780 { | |
16396 | 781 purple_debug_info("msim", "msim_status_cb: no username?!\n"); |
16394 | 782 return; |
783 } | |
784 | |
16396 | 785 purple_debug_info("msim", "msim_status_cb: updating status for <%s> to <%s>\n", username, status_str); |
16394 | 786 |
787 /* TODO: generic functions to split into a GList */ | |
788 status_array = g_strsplit(status_str, "|", 0); | |
789 for (list = NULL, i = 0; | |
790 status_array[i]; | |
791 i++) | |
792 { | |
793 list = g_list_append(list, status_array[i]); | |
794 } | |
795 | |
796 /* Example fields: |s|0|ss|Offline */ | |
797 status_code = g_list_nth_data(list, 2); | |
798 status_text = g_list_nth_data(list, 4); | |
799 | |
16396 | 800 blist = purple_get_blist(); |
16394 | 801 |
802 /* Add buddy if not found */ | |
16396 | 803 buddy = purple_find_buddy(session->account, username); |
16394 | 804 if (!buddy) |
805 { | |
16396 | 806 /* TODO: purple aliases, userids and usernames */ |
807 purple_debug_info("msim", "msim_status: making new buddy for %s\n", username); | |
808 buddy = purple_buddy_new(session->account, username, NULL); | |
16394 | 809 |
810 /* TODO: sometimes (when click on it), buddy list disappears. Fix. */ | |
16396 | 811 purple_blist_add_buddy(buddy, NULL, NULL, NULL); |
16394 | 812 } else { |
16396 | 813 purple_debug_info("msim", "msim_status: found buddy %s\n", username); |
16394 | 814 } |
815 | |
816 /* For now, always set status to online. | |
817 * TODO: make status reflect reality | |
818 * TODO: show headline */ | |
16396 | 819 presence = purple_presence_new_for_buddy(buddy); |
820 purple_presence_set_status_active(presence, MSIM_STATUS_ONLINE, TRUE); | |
16394 | 821 |
822 g_strfreev(status_array); | |
823 g_list_free(list); | |
824 g_hash_table_destroy(body); | |
825 g_hash_table_destroy(userinfo); | |
826 /* Do not free status_str - it will be freed by g_hash_table_destroy on session->userid_lookup_cb_data */ | |
827 } | |
828 | |
829 /** | |
830 * Process incoming status messages. | |
831 * | |
832 * @param session | |
833 * @param table Status update message. | |
834 * | |
835 * @return 0, since 'table' can be freed. | |
836 */ | |
837 static int msim_status(MsimSession *session, GHashTable *table) | |
838 { | |
839 gchar *status_str; | |
840 gchar *userid; | |
841 | |
842 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
843 g_return_val_if_fail(table != NULL, 0); | |
844 | |
845 status_str = g_hash_table_lookup(table, "msg"); | |
846 if (!status_str) | |
847 { | |
16396 | 848 purple_debug_info("msim", "msim_status: bm=100 but no status msg\n"); |
16394 | 849 return 0; |
850 } | |
851 | |
852 userid = g_hash_table_lookup(table, "f"); | |
853 if (!userid) | |
854 { | |
16396 | 855 purple_debug_info("msim", "msim_status: bm=100 but no f field\n"); |
16394 | 856 return 0; |
857 } | |
858 | |
859 /* TODO: if buddies were identified on buddy list by uid, wouldn't have to lookup | |
860 * before updating the status! Much more efficient. */ | |
16396 | 861 purple_debug_info("msim", "msim_status: got status msg <%s> for <%s>, scheduling lookup\n", |
16394 | 862 status_str, userid); |
863 | |
864 /* Actually update status once obtain username */ | |
865 msim_lookup_user(session, userid, msim_status_cb, g_strdup(status_str)); | |
866 | |
867 return 0; | |
868 } | |
869 | |
870 | |
871 /** | |
872 * Process a message. | |
873 * | |
874 * @param gc Connection. | |
875 * @param table Any message from the server. | |
876 * | |
877 * @return The return value of the function used to process the message, or -1 if | |
878 * called with invalid parameters. | |
879 */ | |
16396 | 880 static int msim_process(PurpleConnection *gc, GHashTable *table) |
16394 | 881 { |
882 MsimSession *session; | |
883 | |
884 g_return_val_if_fail(gc != NULL, -1); | |
885 g_return_val_if_fail(table != NULL, -1); | |
886 | |
887 session = (MsimSession*)gc->proto_data; | |
888 | |
889 printf("-------- message -------------\n"); | |
890 g_hash_table_foreach(table, print_hash_item, NULL); | |
891 printf("------------------------------\n"); | |
892 | |
893 if (g_hash_table_lookup(table, "nc")) | |
894 { | |
895 return msim_login_challenge(session, table); | |
896 } else if (g_hash_table_lookup(table, "sesskey")) { | |
897 printf("SESSKEY=<%s>\n", (gchar*)g_hash_table_lookup(table, "sesskey")); | |
898 | |
16396 | 899 purple_connection_update_progress(gc, "Connected", 3, 4); |
16394 | 900 |
901 session->sesskey = g_strdup(g_hash_table_lookup(table, "sesskey")); | |
902 | |
903 /* Comes with: proof,profileid,userid,uniquenick -- all same values | |
904 * (at least for me). */ | |
905 session->userid = g_strdup(g_hash_table_lookup(table, "userid")); | |
906 | |
16396 | 907 purple_connection_set_state(gc, PURPLE_CONNECTED); |
16394 | 908 |
909 return 0; | |
910 } else if (g_hash_table_lookup(table, "bm")) { | |
911 guint bm; | |
912 | |
913 bm = atoi(g_hash_table_lookup(table, "bm")); | |
914 switch (bm) | |
915 { | |
916 case MSIM_BM_STATUS: | |
917 return msim_status(session, table); | |
918 case MSIM_BM_INSTANT: | |
919 return msim_incoming_im(session, table); | |
920 default: | |
921 /* Not really an IM, but show it for informational | |
922 * purposes during development. */ | |
923 return msim_incoming_im(session, table); | |
924 } | |
925 | |
926 if (bm == MSIM_BM_STATUS) | |
927 { | |
928 return msim_status(session, table); | |
929 } else { /* else if strcmp(bm, "1") == 0) */ | |
930 return msim_incoming_im(session, table); | |
931 } | |
932 } else if (g_hash_table_lookup(table, "rid")) { | |
933 return msim_process_reply(session, table); | |
934 } else if (g_hash_table_lookup(table, "error")) { | |
935 return msim_error(session, table); | |
936 } else if (g_hash_table_lookup(table, "ka")) { | |
16396 | 937 purple_debug_info("msim", "msim_process: got keep alive\n"); |
16394 | 938 return 0; |
939 } else { | |
940 printf("<<unhandled>>\n"); | |
941 return 0; | |
942 } | |
943 } | |
944 | |
945 /** | |
946 * Callback when input available. | |
947 * | |
16396 | 948 * @param gc_uncasted A PurpleConnection pointer. |
16394 | 949 * @param source File descriptor. |
16396 | 950 * @param cond PURPLE_INPUT_READ |
16394 | 951 * |
952 * Reads the input, and dispatches calls msim_process to handle it. | |
953 */ | |
16396 | 954 static void msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond) |
16394 | 955 { |
16396 | 956 PurpleConnection *gc; |
957 PurpleAccount *account; | |
16394 | 958 MsimSession *session; |
959 gchar *end; | |
960 int n; | |
961 | |
962 g_return_if_fail(gc_uncasted != NULL); | |
963 g_return_if_fail(source >= 0); /* Note: 0 is a valid fd */ | |
964 | |
16396 | 965 gc = (PurpleConnection*)(gc_uncasted); |
966 account = purple_connection_get_account(gc); | |
16394 | 967 session = gc->proto_data; |
968 | |
969 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
970 | |
16396 | 971 g_assert(cond == PURPLE_INPUT_READ); |
16394 | 972 |
973 /* Only can handle so much data at once... | |
974 * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE. | |
975 * Should be large enough to hold the largest protocol message. | |
976 */ | |
977 if (session->rxoff == MSIM_READ_BUF_SIZE) | |
978 { | |
16396 | 979 purple_debug_error("msim", "msim_input_cb: %d-byte read buffer full!\n", |
16394 | 980 MSIM_READ_BUF_SIZE); |
16396 | 981 purple_connection_error(gc, "Read buffer full"); |
16394 | 982 /* TODO: fix 100% CPU after closing */ |
983 close(source); | |
984 return; | |
985 } | |
986 | |
16396 | 987 purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n", |
16394 | 988 session->rxoff, MSIM_READ_BUF_SIZE, MSIM_READ_BUF_SIZE - session->rxoff); |
989 | |
990 /* Read into buffer. On Win32, need recv() not read(). session->fd also holds | |
991 * the file descriptor, but it sometimes differs from the 'source' parameter. | |
992 */ | |
993 n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0); | |
994 | |
995 if (n < 0 && errno == EAGAIN) | |
996 { | |
997 return; | |
998 } | |
999 else if (n < 0) | |
1000 { | |
16396 | 1001 purple_connection_error(gc, "Read error"); |
1002 purple_debug_error("msim", "msim_input_cb: read error, ret=%d, " | |
16394 | 1003 "error=%s, source=%d, fd=%d (%X))\n", |
1004 n, strerror(errno), source, session->fd, session->fd); | |
1005 close(source); | |
1006 return; | |
1007 } | |
1008 else if (n == 0) | |
1009 { | |
16396 | 1010 purple_debug_info("msim", "msim_input_cb: server disconnected\n"); |
1011 purple_connection_error(gc, "Server has disconnected"); | |
16394 | 1012 return; |
1013 } | |
1014 | |
1015 /* Null terminate */ | |
1016 session->rxbuf[session->rxoff + n] = 0; | |
1017 | |
1018 /* Check for embedded NULs. I don't handle them, and they shouldn't occur. */ | |
1019 if (strlen(session->rxbuf + session->rxoff) != n) | |
1020 { | |
1021 /* Occurs after login, but it is not a null byte. */ | |
16396 | 1022 purple_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes" |
16394 | 1023 "--null byte encountered?\n", strlen(session->rxbuf + session->rxoff), n); |
16396 | 1024 //purple_connection_error(gc, "Invalid message - null byte on input"); |
16394 | 1025 return; |
1026 } | |
1027 | |
1028 session->rxoff += n; | |
16396 | 1029 purple_debug_info("msim", "msim_input_cb: read=%d\n", n); |
16394 | 1030 |
1031 //printf("buf=<%s>\n", session->rxbuf); | |
1032 | |
1033 /* Look for \\final\\ end markers. If found, process message. */ | |
1034 while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) | |
1035 { | |
1036 GHashTable *table; | |
1037 | |
1038 //printf("in loop: buf=<%s>\n", session->rxbuf); | |
1039 *end = 0; | |
1040 table = msim_parse(g_strdup(session->rxbuf)); | |
1041 if (!table) | |
1042 { | |
16396 | 1043 purple_debug_info("msim", "msim_input_cb: couldn't parse <%s>\n", session->rxbuf); |
1044 purple_connection_error(gc, "Unparseable message"); | |
16394 | 1045 } |
1046 else | |
1047 { | |
1048 /* Process message. Returns 0 to free */ | |
1049 if (msim_process(gc, table) == 0) | |
1050 g_hash_table_destroy(table); | |
1051 } | |
1052 | |
1053 /* Move remaining part of buffer to beginning. */ | |
1054 session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING); | |
1055 memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), | |
1056 MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf)); | |
1057 | |
1058 /* Clear end of buffer */ | |
1059 //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf)); | |
1060 } | |
1061 } | |
1062 | |
1063 /** | |
1064 * Callback when connected. Sets up input handlers. | |
1065 * | |
16396 | 1066 * @param data A PurpleConnection pointer. |
16394 | 1067 * @param source File descriptor. |
1068 * @param error_message | |
1069 */ | |
1070 static void msim_connect_cb(gpointer data, gint source, const gchar *error_message) | |
1071 { | |
16396 | 1072 PurpleConnection *gc; |
16394 | 1073 MsimSession *session; |
1074 | |
1075 g_return_if_fail(data != NULL); | |
1076 | |
16396 | 1077 gc = (PurpleConnection*)data; |
16394 | 1078 session = gc->proto_data; |
1079 | |
1080 if (source < 0) | |
1081 { | |
16396 | 1082 purple_connection_error(gc, "Couldn't connect to host"); |
1083 purple_connection_error(gc, g_strdup_printf("Couldn't connect to host: %s (%d)", | |
16394 | 1084 error_message, source)); |
1085 return; | |
1086 } | |
1087 | |
1088 session->fd = source; | |
1089 | |
16396 | 1090 gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc); |
16394 | 1091 } |
1092 | |
1093 /* Session methods */ | |
1094 | |
1095 /** | |
1096 * Create a new MSIM session. | |
1097 * | |
1098 * @param acct The account to create the session from. | |
1099 * | |
1100 * @return Pointer to a new session. Free with msim_session_destroy. | |
1101 */ | |
16396 | 1102 static MsimSession *msim_session_new(PurpleAccount *acct) |
16394 | 1103 { |
1104 MsimSession *session; | |
1105 | |
1106 g_return_val_if_fail(acct != NULL, NULL); | |
1107 | |
1108 session = g_new0(MsimSession, 1); | |
1109 | |
1110 session->magic = MSIM_SESSION_STRUCT_MAGIC; | |
1111 session->account = acct; | |
16396 | 1112 session->gc = purple_account_get_connection(acct); |
16394 | 1113 session->fd = -1; |
1114 session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL); /* do NOT free function pointers! */ | |
1115 session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free); | |
1116 session->user_lookup_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_hash_table_destroy); | |
1117 session->rxoff = 0; | |
1118 session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE); | |
1119 | |
1120 return session; | |
1121 } | |
1122 | |
1123 /** | |
1124 * Free a session. | |
1125 * | |
1126 * @param session The session to destroy. | |
1127 */ | |
1128 static void msim_session_destroy(MsimSession *session) | |
1129 { | |
1130 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
1131 | |
1132 session->magic = -1; | |
1133 | |
1134 g_free(session->rxbuf); | |
1135 g_free(session->userid); | |
1136 g_free(session->sesskey); | |
1137 | |
1138 g_free(session); | |
1139 } | |
1140 | |
1141 /** | |
1142 * Start logging in to the MSIM servers. | |
1143 * | |
1144 * @param acct Account information to use to login. | |
1145 */ | |
16396 | 1146 static void msim_login(PurpleAccount *acct) |
16394 | 1147 { |
16396 | 1148 PurpleConnection *gc; |
16394 | 1149 const char *host; |
1150 int port; | |
1151 | |
1152 g_return_if_fail(acct != NULL); | |
1153 | |
16396 | 1154 purple_debug_info("myspace", "logging in %s\n", acct->username); |
16394 | 1155 |
16396 | 1156 gc = purple_account_get_connection(acct); |
16394 | 1157 gc->proto_data = msim_session_new(acct); |
1158 | |
1159 /* 1. connect to server */ | |
16396 | 1160 purple_connection_update_progress(gc, "Connecting", |
16394 | 1161 0, /* which connection step this is */ |
1162 4); /* total number of steps */ | |
1163 | |
1164 /* TODO: GUI option to be user-modifiable. */ | |
16396 | 1165 host = purple_account_get_string(acct, "server", MSIM_SERVER); |
1166 port = purple_account_get_int(acct, "port", MSIM_PORT); | |
16394 | 1167 /* TODO: connect */ |
16396 | 1168 /* From purple.sf.net/api: |
16394 | 1169 * """Note that this function name can be misleading--although it is called |
1170 * "proxy connect," it is used for establishing any outgoing TCP connection, | |
1171 * whether through a proxy or not.""" */ | |
1172 | |
1173 /* Calls msim_connect_cb when connected. */ | |
16396 | 1174 if (purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc) == NULL) |
16394 | 1175 { |
1176 /* TODO: try other ports if in auto mode, then save | |
1177 * working port and try that first next time. */ | |
16396 | 1178 purple_connection_error(gc, "Couldn't create socket"); |
16394 | 1179 return; |
1180 } | |
1181 | |
1182 } | |
1183 | |
1184 | |
1185 /** | |
1186 * Close the connection. | |
1187 * | |
1188 * @param gc The connection. | |
1189 */ | |
16396 | 1190 static void msim_close(PurpleConnection *gc) |
16394 | 1191 { |
1192 g_return_if_fail(gc != NULL); | |
1193 | |
1194 msim_session_destroy(gc->proto_data); | |
1195 } | |
1196 | |
1197 /** | |
1198 * Return the icon name for a buddy and account. | |
1199 * | |
1200 * @param acct The account to find the icon for. | |
1201 * @param buddy The buddy to find the icon for, or NULL for the accoun icon. | |
1202 * | |
1203 * @return The base icon name string. | |
1204 */ | |
16396 | 1205 static const gchar *msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy) |
16394 | 1206 { |
1207 /* TODO: use a MySpace icon. hbons submitted one to | |
1208 * http://developer.pidgin.im/wiki/MySpaceIM - tried placing in | |
16396 | 1209 * C:\cygwin\home\Jeff\purple-2.0.0beta6\gtk\pixmaps\status\default |
16394 | 1210 * and returning "myspace" but icon shows up blank. |
1211 */ | |
1212 if (acct == NULL) | |
1213 { | |
16396 | 1214 purple_debug_info("msim", "msim_list_icon: acct == NULL!\n"); |
16394 | 1215 //exit(-2); |
1216 } | |
1217 return "meanwhile"; | |
1218 } | |
1219 | |
1220 /** | |
1221 * Check if a string is a userid (all numeric). | |
1222 * | |
1223 * @param user The user id, email, or name. | |
1224 * | |
1225 * @return TRUE if is userid, FALSE if not. | |
1226 */ | |
1227 static inline gboolean msim_is_userid(const gchar *user) | |
1228 { | |
1229 g_return_val_if_fail(user != NULL, FALSE); | |
1230 | |
1231 return strspn(user, "0123456789") == strlen(user); | |
1232 } | |
1233 | |
1234 /** | |
1235 * Check if a string is an email address (contains an @). | |
1236 * | |
1237 * @param user The user id, email, or name. | |
1238 * | |
1239 * @return TRUE if is an email, FALSE if not. | |
1240 * | |
1241 * This function is not intended to be used as a generic | |
1242 * means of validating email addresses, but to distinguish | |
1243 * between a user represented by an email address from | |
1244 * other forms of identification. | |
1245 */ | |
1246 static inline gboolean msim_is_email(const gchar *user) | |
1247 { | |
1248 g_return_val_if_fail(user != NULL, FALSE); | |
1249 | |
1250 return strchr(user, '@') != NULL; | |
1251 } | |
1252 | |
1253 | |
1254 /** | |
1255 * Asynchronously lookup user information, calling callback when receive result. | |
1256 * | |
1257 * @param session | |
1258 * @param user The user id, email address, or username. | |
1259 * @param cb Callback, called with user information when available. | |
1260 * @param data An arbitray data pointer passed to the callback. | |
1261 */ | |
1262 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data) | |
1263 { | |
1264 gchar *field_name; | |
1265 gchar *msg_string; | |
1266 guint rid, cmd, dsn, lid; | |
1267 | |
1268 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
1269 g_return_if_fail(user != NULL); | |
1270 g_return_if_fail(cb != NULL); | |
1271 | |
16396 | 1272 purple_debug_info("msim", "msim_lookup_userid", "asynchronously looking up <%s>\n", user); |
16394 | 1273 |
1274 /* TODO: check if this user's info was cached and fresh; if so return immediately */ | |
1275 #if 0 | |
1276 /* If already know userid, then call callback immediately */ | |
1277 cached_userid = g_hash_table_lookup(session->userid_cache, who); | |
1278 if (cached_userid && !by_userid) | |
1279 { | |
1280 cb(cached_userid, NULL, NULL, data); | |
1281 return; | |
1282 } | |
1283 #endif | |
1284 | |
1285 rid = rand(); //om(); | |
1286 | |
1287 /* Setup callback. Response will be associated with request using 'rid'. */ | |
1288 g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb); | |
1289 g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data); | |
1290 | |
1291 /* Send request */ | |
1292 | |
1293 cmd = 1; | |
1294 | |
1295 if (msim_is_userid(user)) | |
1296 { | |
1297 /* TODO: document cmd,dsn,lid */ | |
1298 field_name = "UserID"; | |
1299 dsn = 4; | |
1300 lid = 3; | |
1301 } else if (msim_is_email(user)) { | |
1302 field_name = "Email"; | |
1303 dsn = 5; | |
1304 lid = 7; | |
1305 } else { | |
1306 field_name = "UserName"; | |
1307 dsn = 5; | |
1308 lid = 7; | |
1309 } | |
1310 | |
1311 msg_string = g_strdup_printf("\\persist\\1\\sesskey\\%s\\cmd\\1\\dsn\\%d\\uid\\%s\\lid\\%d\\rid\\%d\\body\\%s=%s\\final\\", | |
1312 session->sesskey, dsn, session->userid, lid, rid, field_name, user); | |
1313 | |
1314 msim_send(session, msg_string); | |
1315 } | |
1316 | |
1317 /** | |
1318 * Schedule an IM to be sent once the user ID is looked up. | |
1319 * | |
1320 * @param gc Connection. | |
1321 * @param who A user id, email, or username to send the message to. | |
1322 * @param message Instant message text to send. | |
1323 * @param flags Flags. | |
1324 * | |
1325 * @return 1 in all cases, even if the message delivery is destined to fail. | |
1326 * | |
1327 * Allows sending to a user by username, email address, or userid. If | |
1328 * a username or email address is given, the userid must be looked up. | |
1329 * This function does that by calling msim_lookup_user(), setting up | |
1330 * a msim_send_im_by_userid_cb() callback function called when the userid | |
1331 * response is received from the server. | |
1332 * | |
1333 * The callback function calls msim_send_im_by_userid() to send the actual | |
1334 * instant message. If a userid is specified directly, this function is called | |
1335 * immediately here. | |
1336 */ | |
16396 | 1337 static int msim_send_im(PurpleConnection *gc, const char *who, |
1338 const char *message, PurpleMessageFlags flags) | |
16394 | 1339 { |
1340 MsimSession *session; | |
1341 const char *from_username = gc->account->username; | |
1342 send_im_cb_struct *cbinfo; | |
1343 | |
1344 g_return_val_if_fail(gc != NULL, 0); | |
1345 g_return_val_if_fail(who != NULL, 0); | |
1346 g_return_val_if_fail(message != NULL, 0); | |
1347 | |
16396 | 1348 purple_debug_info("msim", "sending message from %s to %s: %s\n", |
16394 | 1349 from_username, who, message); |
1350 | |
1351 session = gc->proto_data; | |
1352 | |
1353 /* If numeric ID, can send message immediately without userid lookup */ | |
1354 if (msim_is_userid(who)) | |
1355 { | |
16396 | 1356 purple_debug_info("msim", "msim_send_im: numeric 'who' detected, sending asap\n"); |
16394 | 1357 msim_send_im_by_userid(session, who, message, flags); |
1358 return 1; | |
1359 } | |
1360 | |
1361 /* Otherwise, add callback to IM when userid of destination is available */ | |
1362 | |
1363 /* Setup a callback for when the userid is available */ | |
1364 cbinfo = g_new0(send_im_cb_struct, 1); | |
1365 cbinfo->who = g_strdup(who); | |
1366 cbinfo->message = g_strdup(message); | |
1367 cbinfo->flags = flags; | |
1368 | |
1369 /* Send the request to lookup the userid */ | |
1370 msim_lookup_user(session, who, msim_send_im_by_userid_cb, cbinfo); | |
1371 | |
1372 /* msim_send_im_by_userid_cb will now be called once userid is looked up */ | |
1373 | |
16396 | 1374 /* Return 1 to have Purple show this IM as being sent, 0 to not. I always |
16394 | 1375 * return 1 even if the message could not be sent, since I don't know if |
1376 * it has failed yet--because the IM is only sent after the userid is | |
1377 * retrieved from the server (which happens after this function returns). | |
1378 * | |
1379 * TODO: In MySpace, you login with your email address, but don't talk to other | |
1380 * users using their email address. So there is currently an asymmetry in the | |
1381 * IM windows when using this plugin: | |
1382 * | |
1383 * you@example.com: hello | |
1384 * some_other_user: what's going on? | |
1385 * you@example.com: just coding a prpl | |
1386 * | |
1387 * TODO: Make the sent IM's appear as from the user's username, instead of | |
16396 | 1388 * their email address. Purple uses the login (in MSIM, the email)--change this. |
16394 | 1389 */ |
1390 return 1; | |
1391 } | |
1392 | |
1393 /** | |
1394 * Obtain the status text for a buddy. | |
1395 * | |
1396 * @param buddy The buddy to obtain status text for. | |
1397 * | |
1398 * @return Status text. | |
1399 * | |
1400 * Currently returns the display name. | |
1401 */ | |
16396 | 1402 static char *msim_status_text(PurpleBuddy *buddy) |
16394 | 1403 { |
1404 MsimSession *session; | |
1405 GHashTable *userinfo; | |
1406 gchar *display_name; | |
1407 | |
1408 g_return_val_if_fail(buddy != NULL, NULL); | |
1409 | |
1410 session = (MsimSession*)buddy->account->gc->proto_data; | |
1411 g_assert(MSIM_SESSION_VALID(session)); | |
1412 g_assert(session->user_lookup_cache != NULL); | |
1413 | |
1414 userinfo = g_hash_table_lookup(session->user_lookup_cache, buddy->name); | |
1415 if (!userinfo) | |
1416 { | |
1417 return g_strdup(""); | |
1418 } | |
1419 | |
1420 display_name = g_hash_table_lookup(userinfo, "DisplayName"); | |
1421 g_assert(display_name != NULL); | |
1422 | |
1423 return g_strdup(display_name); | |
1424 } | |
1425 | |
1426 /** | |
1427 * Obtain the tooltip text for a buddy. | |
1428 * | |
1429 * @param buddy Buddy to obtain tooltip text on. | |
1430 * @param user_info Variable modified to have the tooltip text. | |
1431 * @param full TRUE if should obtain full tooltip text. | |
1432 * | |
1433 */ | |
16396 | 1434 static void msim_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, gboolean full) |
16394 | 1435 { |
1436 g_return_if_fail(buddy != NULL); | |
1437 g_return_if_fail(user_info != NULL); | |
1438 | |
16396 | 1439 if (PURPLE_BUDDY_IS_ONLINE(buddy)) |
16394 | 1440 { |
1441 MsimSession *session; | |
1442 GHashTable *userinfo; | |
1443 | |
1444 session = (MsimSession*)buddy->account->gc->proto_data; | |
1445 | |
1446 g_assert(MSIM_SESSION_VALID(session)); | |
1447 g_assert(session->user_lookup_cache); | |
1448 | |
1449 userinfo = g_hash_table_lookup(session->user_lookup_cache, buddy->name); | |
1450 | |
1451 g_assert(userinfo != NULL); | |
1452 | |
1453 // TODO: if (full), do something different | |
16396 | 1454 purple_notify_user_info_add_pair(user_info, "User ID", g_hash_table_lookup(userinfo, "UserID")); |
1455 purple_notify_user_info_add_pair(user_info, "Display Name", g_hash_table_lookup(userinfo, "DisplayName")); | |
1456 purple_notify_user_info_add_pair(user_info, "User Name", g_hash_table_lookup(userinfo, "UserName")); | |
1457 purple_notify_user_info_add_pair(user_info, "Total Friends", g_hash_table_lookup(userinfo, "TotalFriends")); | |
1458 purple_notify_user_info_add_pair(user_info, "Song", | |
16394 | 1459 g_strdup_printf("%s - %s", |
1460 (gchar*)g_hash_table_lookup(userinfo, "BandName"), | |
1461 (gchar*)g_hash_table_lookup(userinfo, "SongName"))); | |
1462 } | |
1463 } | |
1464 | |
16396 | 1465 /** Callbacks called by Purple, to access this plugin. */ |
1466 static PurplePluginProtocolInfo prpl_info = | |
16394 | 1467 { |
1468 OPT_PROTO_MAIL_CHECK,/* options - TODO: myspace will notify of mail */ | |
1469 NULL, /* user_splits */ | |
1470 NULL, /* protocol_options */ | |
1471 NO_BUDDY_ICONS, /* icon_spec - TODO: eventually should add this */ | |
1472 msim_list_icon, /* list_icon */ | |
1473 NULL, /* list_emblems */ | |
1474 msim_status_text, /* status_text */ | |
1475 msim_tooltip_text, /* tooltip_text */ | |
1476 msim_status_types, /* status_types */ | |
1477 NULL, /* blist_node_menu */ | |
1478 NULL, /* chat_info */ | |
1479 NULL, /* chat_info_defaults */ | |
1480 msim_login, /* login */ | |
1481 msim_close, /* close */ | |
1482 msim_send_im, /* send_im */ | |
1483 NULL, /* set_info */ | |
1484 NULL, /* send_typing */ | |
1485 NULL, /* get_info */ | |
1486 NULL, /* set_away */ | |
1487 NULL, /* set_idle */ | |
1488 NULL, /* change_passwd */ | |
1489 NULL, /* add_buddy */ | |
1490 NULL, /* add_buddies */ | |
1491 NULL, /* remove_buddy */ | |
1492 NULL, /* remove_buddies */ | |
1493 NULL, /* add_permit */ | |
1494 NULL, /* add_deny */ | |
1495 NULL, /* rem_permit */ | |
1496 NULL, /* rem_deny */ | |
1497 NULL, /* set_permit_deny */ | |
1498 NULL, /* join_chat */ | |
1499 NULL, /* reject chat invite */ | |
1500 NULL, /* get_chat_name */ | |
1501 NULL, /* chat_invite */ | |
1502 NULL, /* chat_leave */ | |
1503 NULL, /* chat_whisper */ | |
1504 NULL, /* chat_send */ | |
1505 NULL, /* keepalive */ | |
1506 NULL, /* register_user */ | |
1507 NULL, /* get_cb_info */ | |
1508 NULL, /* get_cb_away */ | |
1509 NULL, /* alias_buddy */ | |
1510 NULL, /* group_buddy */ | |
1511 NULL, /* rename_group */ | |
1512 NULL, /* buddy_free */ | |
1513 NULL, /* convo_closed */ | |
1514 NULL, /* normalize */ | |
1515 NULL, /* set_buddy_icon */ | |
1516 NULL, /* remove_group */ | |
1517 NULL, /* get_cb_real_name */ | |
1518 NULL, /* set_chat_topic */ | |
1519 NULL, /* find_blist_chat */ | |
1520 NULL, /* roomlist_get_list */ | |
1521 NULL, /* roomlist_cancel */ | |
1522 NULL, /* roomlist_expand_category */ | |
1523 NULL, /* can_receive_file */ | |
1524 NULL, /* send_file */ | |
1525 NULL, /* new_xfer */ | |
1526 NULL, /* offline_message */ | |
1527 NULL, /* whiteboard_prpl_ops */ | |
1528 NULL, /* send_raw */ | |
1529 NULL /* roomlist_room_serialize */ | |
1530 }; | |
1531 | |
1532 | |
1533 | |
1534 /** Based on MSN's plugin info comments. */ | |
16396 | 1535 static PurplePluginInfo info = |
16394 | 1536 { |
16396 | 1537 PURPLE_PLUGIN_MAGIC, |
1538 PURPLE_MAJOR_VERSION, | |
1539 PURPLE_MINOR_VERSION, | |
1540 PURPLE_PLUGIN_PROTOCOL, /**< type */ | |
16394 | 1541 NULL, /**< ui_requirement */ |
1542 0, /**< flags */ | |
1543 NULL, /**< dependencies */ | |
16396 | 1544 PURPLE_PRIORITY_DEFAULT, /**< priority */ |
16394 | 1545 |
1546 "prpl-myspace", /**< id */ | |
1547 "MySpaceIM", /**< name */ | |
1548 "0.4", /**< version */ | |
1549 /** summary */ | |
1550 "MySpaceIM Protocol Plugin", | |
1551 /** description */ | |
1552 "MySpaceIM Protocol Plugin", | |
1553 "Jeff Connelly <myspaceim@xyzzy.cjb.net>", /**< author */ | |
1554 "http://developer.pidgin.im/wiki/MySpaceIM/", /**< homepage */ | |
1555 | |
1556 NULL, /**< load */ | |
1557 NULL, /**< unload */ | |
1558 NULL, /**< destroy */ | |
1559 NULL, /**< ui_info */ | |
1560 &prpl_info, /**< extra_info */ | |
1561 NULL, /**< prefs_info */ | |
1562 | |
1563 /* msim_actions */ | |
1564 NULL | |
1565 }; | |
1566 | |
1567 | |
16396 | 1568 PURPLE_INIT_PLUGIN(myspace, init_plugin, info); |