Better handling for MSN logout before the full list is received.
[thrasher.git] / thft.c
blobdbc489c4ee1347a3010aab1c616b52ca6a5715fd
1 /*
2 * Thrasher Bird - XMPP transport via libpurple
3 * Copyright (C) 2008 Barracuda Networks, Inc.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with Thrasher Bird; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
19 #include <glib.h>
20 #include "thrasher.h"
21 #include "thperl.h"
23 #include <ft.h>
24 #include <server.h>
26 #include "thft.h"
28 /* GENERAL NOTES:
30 * The Thrasher FT ID indexes file transfer structs by an integer,
31 * which can be easily passed back to Perl as a token it can use to
32 * manipulate the file transfer as needed. A value <= 0 indicates an
33 * error. There isn't much value to having Perl manipulate the structs
34 * directly, so the Perl-side FT object is entirely separate from the
35 * libpurple xfer struct.
37 * SENDING:
39 * 1. Perl-side accepts the file and negotiates a SOCKS connection.
40 * Stash parameters by *stream* ID for this step only; everything
41 * else uses the Thrasher-generated ID from step 2.1.
43 * 2. Once Perl-side completes negotiation, it starts libpurple-side
44 * setup with thrasher_send_file().
46 * 2.1. Stash xfer parameters in the account until xfer is constructed.
48 * 2.2. Call server.c:serv_send_file() should end up calling
50 * 2.3. prpl:send_file, which calls
52 * 2.4. purple_xfer_new(), which calls
54 * 2.4. thrasher_new_xfer callback in ui_ops to copy parameters out of
55 * stash and store the xfer in Thrasher's C-side ID lookup table.
57 * 2.5. thrasher_send_file() returns the ID to Perl for building the
58 * lookup table for Perl-side FT data.
60 * 3. Perl-side connects to SOCKS and sets a watch on the FD to
61 * trigger ui_read.
63 * 4. Loop of ft.c:do_transfer() calling ui_read/data_not_sent
64 * callbacks until canceled or complete.
66 * RECEIVING:
67 * 1. thrasher_new_xfer() stores the xfer by ID.
69 * 2. recv_request_cb() hands off to Perl-side to do negotiation.
70 * Eventually, a Perl-side event accepts or denies the FT with
71 * thrasher_xfer_recv_request_responder().
73 * 3. Loop of ft.cdo_transfer() calling ui_write. Perl-side stashes
74 * data if it is unable to keep up forwarding data to the Thrasher
75 * user. ui_write hides such problems (even removing the FD watch
76 * and taking over the loop if necessary) from its caller because
77 * libpurple 2.6 will cancel the FT instead of slowing down.
82 * Set up the mapping and associated functions
85 size_t next_file_transfer_id = 1;
86 /* guint id_ptr => PurpleXfer* xfer */
87 GHashTable *id_to_xfer = NULL;
89 void free_id (gpointer id)
91 free(id);
94 size_t get_next_file_transfer_id () {
95 return next_file_transfer_id++;
98 int store_xfer(PurpleXfer* xfer) {
99 guint id = get_id_by_xfer(xfer);
100 g_hash_table_insert(id_to_xfer, (void*)id, xfer);
101 purple_debug_info("thrasher ft",
102 "%d: stored\n",
103 id);
104 return id;
107 // Both of these functions assume the transfer is present and
108 // will segfault if it is not, so FIXME: Soften this. This
109 // is cool for debugging but production ought to be more graceful.
110 PurpleXfer* get_xfer_by_id(guint id)
112 return g_hash_table_lookup(id_to_xfer, (void*)id);
115 guint get_id_by_xfer(PurpleXfer* xfer)
117 Thrasher_Xfer_UI_Data *ui_data = xfer->ui_data;
118 g_return_val_if_fail(ui_data, 0);
119 g_return_val_if_fail(ui_data->id >= 1, 0);
120 return ui_data->id;
123 // Correctly remove the transfer from our hash tables.
124 void thrasher_remove_xfer(PurpleXfer* xfer)
126 guint id = get_id_by_xfer(xfer);
127 purple_debug_info("thrasher ft",
128 "%d: thrasher_remove_xfer\n",
129 id);
130 g_hash_table_remove(id_to_xfer, (void*)id);
134 * Callbacks for transfers
137 /* thrasher_xfer_recv_request_cb tells Perl side to send an offer IQ
138 * for xfer to the transport user...
140 void
141 thrasher_xfer_recv_request_cb(PurpleXfer *xfer,
142 gpointer data) {
143 guint id = get_id_by_xfer(xfer);
144 const char* remote_filename = purple_xfer_get_filename(xfer);
145 purple_debug_info("thrasher ft",
146 "%d: thrasher_xfer_recv_request_cb with filename %s\n",
148 remote_filename);
150 /* Can accept/deny this xfer if this signal CB either sets
151 * local_filename and status or somehow eventually results in
152 * purple_xfer_request_* being called.
154 * Otherwise, would have to implement a request_action in
155 * thrasher_request_uiops that could tell whether the request was
156 * for an FT, cert, etc. Based on matching the human-readable
157 * message? Ugh.
159 thrasher_wrapper_ft_recv_request(xfer, remote_filename);
162 /* ...and Perl-side calls thrasher_xfer_recv_request_responder when
163 * the response to the offer arrives (first setting up the FT proxy if
164 * accepting).
166 void
167 thrasher_xfer_recv_request_responder(guint id,
168 guint accept) {
169 purple_debug_info("thrasher ft",
170 "%d: thrasher_xfer_recv_request_responder(%d)\n",
172 accept);
173 PurpleXfer *xfer = get_xfer_by_id(id);
174 g_return_if_fail(xfer);
176 const char* remote_filename = purple_xfer_get_filename(xfer);
177 if (accept) {
178 /* local_filename doesn't matter--Thrasher user's XMPP client
179 * maintains its own destination path--but libpurple checks it
180 * before proceeding.
182 purple_xfer_request_accepted(xfer, remote_filename);
184 else {
185 purple_xfer_request_denied(xfer);
189 void thrasher_xfer_recv_accept_cb(PurpleXfer *xfer,
190 gpointer data)
192 guint id = get_id_by_xfer(xfer);
193 purple_debug_info("thrasher ft",
194 "%d: thrasher_xfer_recv_accept_cb\n",
195 id);
198 void thrasher_xfer_recv_start_cb(PurpleXfer *xfer,
199 gpointer data)
201 guint id = get_id_by_xfer(xfer);
202 purple_debug_info("thrasher ft",
203 "%d: thrasher_xfer_recv_start_cb\n",
204 id);
205 purple_xfer_ui_ready(xfer);
208 void thrasher_xfer_recv_cancel_cb(PurpleXfer *xfer,
209 gpointer data)
211 guint id = get_id_by_xfer(xfer);
212 purple_debug_info("thrasher ft",
213 "%d: thrasher_xfer_recv_cancel_cb\n",
214 id);
215 thrasher_wrapper_ft_recv_cancel(id);
218 void
219 thrasher_xfer_recv_complete_cb(PurpleXfer *xfer,
220 gpointer data) {
221 guint id = get_id_by_xfer(xfer);
222 purple_debug_info("thrasher ft",
223 "%d: thrasher_xfer_recv_complete_cb\n",
224 id);
225 thrasher_wrapper_ft_recv_complete(id);
228 void thrasher_xfer_send_accept_cb(PurpleXfer *xfer,
229 gpointer data)
231 guint id = get_id_by_xfer(xfer);
232 purple_debug_info("thrasher ft",
233 "%d: thrasher_xfer_send_accept_cb\n",
234 id);
237 void
238 thrasher_xfer_send_start_cb(PurpleXfer *xfer,
239 gpointer data) {
240 guint id = get_id_by_xfer(xfer);
241 purple_debug_info("thrasher ft",
242 "%d: thrasher_xfer_send_start_cb\n",
243 id);
245 thrasher_wrapper_ft_send_start(id);
248 void thrasher_xfer_send_cancel_cb(PurpleXfer *xfer,
249 gpointer data)
251 guint id = get_id_by_xfer(xfer);
252 purple_debug_info("thrasher ft",
253 "%d: thrasher_xfer_send_cancel_cb\n",
254 id);
255 thrasher_wrapper_ft_send_cancel(id);
258 void
259 thrasher_xfer_send_complete_cb(PurpleXfer *xfer,
260 gpointer data) {
261 guint id = get_id_by_xfer(xfer);
262 purple_debug_info("thrasher ft",
263 "%d: thrasher_xfer_send_complete_cb\n",
264 id);
265 thrasher_wrapper_ft_send_complete(id);
269 * UI Ops for transfers
272 void thrasher_new_xfer(PurpleXfer *xfer) {
273 purple_debug_info("thrasher ft",
274 "new xfer!\n");
276 if (purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE) {
277 Thrasher_Xfer_UI_Data* ui_data = g_new(Thrasher_Xfer_UI_Data, 1);
278 ui_data->id = get_next_file_transfer_id();
279 xfer->ui_data = ui_data;
281 else if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND) {
282 PurpleAccount* account = purple_xfer_get_account(xfer);
283 GHashTable* pending_send_file
284 = thrasher_account_get_pending_send_file(account);
285 const char* who = purple_xfer_get_remote_user(xfer);
286 Thrasher_Xfer_UI_Data* ui_data = g_hash_table_lookup(pending_send_file,
287 who);
288 g_return_if_fail(ui_data);
289 g_hash_table_remove(pending_send_file, who);
290 xfer->ui_data = ui_data;
292 purple_xfer_set_message(xfer, ui_data->desc);
293 g_free(ui_data->desc);
294 ui_data->desc = NULL;
296 purple_xfer_set_local_filename(xfer, ui_data->filename);
297 purple_xfer_set_filename(xfer, ui_data->filename);
298 g_free(ui_data->filename);
299 ui_data->filename = NULL;
300 purple_xfer_set_size(xfer, ui_data->size);
301 purple_debug_info("thrasher ft",
302 "%d: xfer filename now %s and size now %d\n",
303 ui_data->id,
304 xfer->local_filename,
305 xfer->size);
308 store_xfer(xfer);
311 void thrasher_destroy_xfer(PurpleXfer *xfer)
313 guint id = get_id_by_xfer(xfer);
314 purple_debug_info("thrasher ft",
315 "%d: thrasher_destroy_xfer\n", id);
317 thrasher_remove_xfer(xfer);
319 free(xfer->ui_data);
320 xfer->ui_data = NULL;
323 // Called to add a transfer to a UI we don't have
324 void thrasher_add_xfer(PurpleXfer *xfer)
326 int id = get_id_by_xfer(xfer);
327 if (id == -1) {
328 id = store_xfer(xfer);
330 purple_debug_info("thrasher ft",
331 "%d: thrasher_add_xfer\n",
332 id);
333 return;
336 // Called to update a UI we don't have
337 void thrasher_update_xfer_progress(PurpleXfer *xfer, double percent)
339 guint id = get_id_by_xfer(xfer);
340 percent *= 100;
341 purple_debug_info("thrasher ft",
342 "%d: %0.2f%% complete\n",
344 percent);
345 return;
348 void thrasher_xfer_cancel_local(PurpleXfer *xfer)
350 guint id = get_id_by_xfer(xfer);
351 purple_debug_info("thrasher ft",
352 "%d: thrasher_xfer_cancel_local\n", id);
353 return;
356 void thrasher_xfer_cancel_remote(PurpleXfer *xfer)
358 guint id = get_id_by_xfer(xfer);
359 purple_debug_info("thrasher ft",
360 "%d: thrasher_xfer_cancel_remote\n",
361 id);
364 gssize
365 thrasher_ui_write(PurpleXfer *xfer,
366 const guchar *buffer,
367 gssize size) {
368 guint id = get_id_by_xfer(xfer);
369 purple_debug_info("thrasher ft",
370 "%d: asked to write %d bytes.\n",
372 size);
373 gssize written_sz = thrasher_wrapper_ft_write(id,
374 (const char*) buffer,
375 size);
376 purple_debug_info("thrasher ft",
377 "%d: wrote %d bytes.\n",
379 written_sz);
381 /* HACK: For some reason, libpurple 2.6 doesn't scale the window
382 * if write is incomplete like when reading--it cancels the
383 * transfer entirely! So:
384 * 1. Have Perl-side write stash the leftover data.
385 * 2. Do our own window scaling here.
386 * 3. Lie to libpurple and say write completed.
387 * 4. Have Perl-side slip stashed data into next write.
389 if (written_sz < size) {
390 xfer->current_buffer_size = MAX(xfer->current_buffer_size * 0.5,
391 /* Very low because of above hackery. */
392 128);
393 if (written_sz == 0) {
394 /* Extreme case: wasn't ready to write anything at all.
395 * Perl-side will call ui_ready when caught up.
397 purple_input_remove(xfer->watcher);
398 xfer->watcher = 0;
401 return size;
404 gssize
405 thrasher_ui_read(PurpleXfer *xfer,
406 guchar **buffer,
407 gssize size) {
408 guint id = get_id_by_xfer(xfer);
409 purple_debug_info("thrasher ft",
410 "%d: asked to read %d bytes.\n",
412 size);
413 gssize read_sz = thrasher_wrapper_ft_read(id, buffer, size);
414 purple_debug_info("thrasher ft",
415 "%d: read %d bytes.\n",
417 read_sz);
418 return read_sz;
421 void
422 thrasher_data_not_sent(PurpleXfer *xfer,
423 const guchar *buffer,
424 gsize size) {
425 guint id = get_id_by_xfer(xfer);
426 purple_debug_info("thrasher ft",
427 "%d: asked to unread %d bytes.\n",
428 id, size);
429 thrasher_wrapper_ft_data_not_sent(id,
430 (const char*) buffer,
431 size);
435 * Initialization for libpurple file transfers
438 static gpointer
439 thrasher_xfer_get_handle()
441 static int handle;
442 return &handle;
445 /* thrasher_request_file() exists only to skip over
446 * purple_xfer_choose_file_ok_cb() to purple_xfer_request_accepted().
447 * ok_cb verifies the file exists on disk, but with Thrasher it never
448 * does. Obviously, Thrasher need not prompt for a filename here.
450 void *thrasher_request_file(const char *title, const char *filename,
451 gboolean savedialog, GCallback ok_cb,
452 GCallback cancel_cb,
453 PurpleAccount *account,
454 const char *who,
455 PurpleConversation *conv,
456 void *user_data) {
457 PurpleXfer* xfer = user_data;
458 purple_debug_info("thrasher ft",
459 "thrasher_request_file (%s -> %s)\n",
460 filename, who);
462 if (purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE) {
463 purple_debug_info("thrasher ft",
464 "Should skip ask_accept/request_file"
465 " if receiving. Refer to"
466 " thrasher_xfer_recv_request_responder().\n");
467 return NULL;
470 guint id = get_id_by_xfer(xfer);
471 purple_debug_info("thrasher ft",
472 "%d: about to purple_xfer_request_accepted\n",
473 id);
474 /* local_filename already set, but do want xfer_add and ops.init called. */
475 purple_xfer_request_accepted(xfer,
476 xfer->filename);
478 /* no meaningful ui handle */
479 return NULL;
482 static PurpleXferUiOps thrasher_xfer_uiops = {
483 thrasher_new_xfer,
484 thrasher_destroy_xfer,
485 thrasher_add_xfer,
486 thrasher_update_xfer_progress,
487 thrasher_xfer_cancel_local,
488 thrasher_xfer_cancel_remote,
489 thrasher_ui_write,
490 thrasher_ui_read,
491 thrasher_data_not_sent,
493 // 1 reserved
494 NULL
497 void thrasher_xfer_init()
499 /* thrasher_xfer_destroy() would release id_to_xfer... but that is
500 * assumed to always coincide with process exit.
502 id_to_xfer = g_hash_table_new(g_direct_hash, g_direct_equal);
504 purple_xfers_init();
505 purple_xfers_set_ui_ops(&thrasher_xfer_uiops);
507 void *ft_handle = purple_xfers_get_handle();
508 gpointer thrasher_xfer_handle = thrasher_xfer_get_handle();
510 purple_signal_connect(ft_handle,
511 "file-recv-request",
512 thrasher_xfer_handle,
513 PURPLE_CALLBACK(thrasher_xfer_recv_request_cb),
514 NULL);
515 purple_signal_connect(ft_handle,
516 "file-recv-accept",
517 thrasher_xfer_handle,
518 PURPLE_CALLBACK(thrasher_xfer_recv_accept_cb),
519 NULL);
520 purple_signal_connect(ft_handle,
521 "file-recv-start",
522 thrasher_xfer_handle,
523 PURPLE_CALLBACK(thrasher_xfer_recv_start_cb),
524 NULL);
525 purple_signal_connect(ft_handle,
526 "file-recv-cancel",
527 thrasher_xfer_handle,
528 PURPLE_CALLBACK(thrasher_xfer_recv_cancel_cb),
529 NULL);
530 purple_signal_connect(ft_handle,
531 "file-recv-complete",
532 thrasher_xfer_handle,
533 PURPLE_CALLBACK(thrasher_xfer_recv_complete_cb),
534 NULL);
535 purple_signal_connect(ft_handle,
536 "file-send-accept",
537 thrasher_xfer_handle,
538 PURPLE_CALLBACK(thrasher_xfer_send_accept_cb),
539 NULL);
540 purple_signal_connect(ft_handle,
541 "file-send-start",
542 thrasher_xfer_handle,
543 PURPLE_CALLBACK(thrasher_xfer_send_start_cb),
544 NULL);
545 purple_signal_connect(ft_handle,
546 "file-send-cancel",
547 thrasher_xfer_handle,
548 PURPLE_CALLBACK(thrasher_xfer_send_cancel_cb),
549 NULL);
550 purple_signal_connect(ft_handle,
551 "file-send-complete",
552 thrasher_xfer_handle,
553 PURPLE_CALLBACK(thrasher_xfer_send_complete_cb),
554 NULL);
559 * Actually initiate a file transfer
561 size_t
562 thrasher_send_file(PurpleAccount *account,
563 const char *who,
564 char* filename,
565 size_t size,
566 char* desc) {
567 g_return_val_if_fail(account, 0);
568 g_return_val_if_fail(who, 0);
569 g_return_val_if_fail(filename, 0);
570 g_return_val_if_fail(size, 0);
571 g_return_val_if_fail(desc, 0);
573 PurpleConnection *connection = thrasher_connection_for_account(account);
574 if (!connection) {
575 purple_debug_info("thrasher ft",
576 "Connection could not be found for account!?\n");
577 return 0;
580 PurplePlugin *prpl = purple_connection_get_prpl(connection);
581 if (! prpl) {
582 purple_debug_info("thrasher ft",
583 "prpl could not be found for connection!?\n");
584 return 0;
587 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
588 if (! prpl_info) {
589 purple_debug_info("thrasher ft",
590 "prpl_info could not be found for prpl!?\n");
591 return 0;
594 if (prpl_info->can_receive_file && ! prpl_info->can_receive_file(connection,
595 who)) {
596 purple_debug_info("thrasher ft",
597 "prpl says %s can't receive a file.\n",
598 who);
599 return 0;
602 /* From now on, assume the FT will either succeed or be canceled.
604 * This is the last chance to decide how to proceed until/if
605 * libpurple calls us back. If we're wrong and the libpurple side
606 * ignores (neither uses nor cancels) the xfer, the XMPP user will
607 * be connected to a proxy that goes nowhere until the proxied
608 * transfer "succeeds"....
610 * Because serv_send_file() returns void and thus can't be
611 * checked, add checks as necessary above.
613 size_t id = get_next_file_transfer_id();
615 /* TODO: separate structures for this backchannel and ui_data.
616 * local_filename and size are redundant once there's an xfer for
617 * this to be a ui_data of.
619 Thrasher_Xfer_UI_Data* ui_data = g_new(Thrasher_Xfer_UI_Data, 1);
620 ui_data->filename = g_strdup(filename);
621 ui_data->size = size;
622 ui_data->id = id;
623 ui_data->desc = g_strdup(desc);
625 /* FIXME: better not have more than one FT to the same who at the
626 * same time.... */
627 GHashTable* pending_send_file
628 = thrasher_account_get_pending_send_file(account);
629 g_hash_table_insert(pending_send_file,
630 (void*)who,
631 ui_data);
633 purple_debug_info("thrasher ft",
634 "%d: calling serv_send for file %s size %d to %s\n",
636 filename,
637 size,
638 who);
640 /* Passing NULL filename causes the prpl to not call
641 * purple_xfer_request_accepted() which is mostly a noop because
642 * Thrasher has a ui_read callback. Would be nice if
644 * 1. request_accepted generated a file-send-accept signal and
646 * 2. it was only called after the remote side had in fact accepted
648 * but neither is currently true in most prpls. Instead, Perl-side
649 * always accepts but then cancel if no remote side appears.
651 serv_send_file(connection, who, NULL);
653 return id;