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
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.
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
63 * 4. Loop of ft.c:do_transfer() calling ui_read/data_not_sent
64 * callbacks until canceled or complete.
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
)
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",
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);
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",
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...
141 thrasher_xfer_recv_request_cb(PurpleXfer
*xfer
,
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",
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
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
167 thrasher_xfer_recv_request_responder(guint id
,
169 purple_debug_info("thrasher ft",
170 "%d: thrasher_xfer_recv_request_responder(%d)\n",
173 PurpleXfer
*xfer
= get_xfer_by_id(id
);
174 g_return_if_fail(xfer
);
176 const char* remote_filename
= purple_xfer_get_filename(xfer
);
178 /* local_filename doesn't matter--Thrasher user's XMPP client
179 * maintains its own destination path--but libpurple checks it
182 purple_xfer_request_accepted(xfer
, remote_filename
);
185 purple_xfer_request_denied(xfer
);
189 void thrasher_xfer_recv_accept_cb(PurpleXfer
*xfer
,
192 guint id
= get_id_by_xfer(xfer
);
193 purple_debug_info("thrasher ft",
194 "%d: thrasher_xfer_recv_accept_cb\n",
198 void thrasher_xfer_recv_start_cb(PurpleXfer
*xfer
,
201 guint id
= get_id_by_xfer(xfer
);
202 purple_debug_info("thrasher ft",
203 "%d: thrasher_xfer_recv_start_cb\n",
205 purple_xfer_ui_ready(xfer
);
208 void thrasher_xfer_recv_cancel_cb(PurpleXfer
*xfer
,
211 guint id
= get_id_by_xfer(xfer
);
212 purple_debug_info("thrasher ft",
213 "%d: thrasher_xfer_recv_cancel_cb\n",
215 thrasher_wrapper_ft_recv_cancel(id
);
219 thrasher_xfer_recv_complete_cb(PurpleXfer
*xfer
,
221 guint id
= get_id_by_xfer(xfer
);
222 purple_debug_info("thrasher ft",
223 "%d: thrasher_xfer_recv_complete_cb\n",
225 thrasher_wrapper_ft_recv_complete(id
);
228 void thrasher_xfer_send_accept_cb(PurpleXfer
*xfer
,
231 guint id
= get_id_by_xfer(xfer
);
232 purple_debug_info("thrasher ft",
233 "%d: thrasher_xfer_send_accept_cb\n",
238 thrasher_xfer_send_start_cb(PurpleXfer
*xfer
,
240 guint id
= get_id_by_xfer(xfer
);
241 purple_debug_info("thrasher ft",
242 "%d: thrasher_xfer_send_start_cb\n",
245 thrasher_wrapper_ft_send_start(id
);
248 void thrasher_xfer_send_cancel_cb(PurpleXfer
*xfer
,
251 guint id
= get_id_by_xfer(xfer
);
252 purple_debug_info("thrasher ft",
253 "%d: thrasher_xfer_send_cancel_cb\n",
255 thrasher_wrapper_ft_send_cancel(id
);
259 thrasher_xfer_send_complete_cb(PurpleXfer
*xfer
,
261 guint id
= get_id_by_xfer(xfer
);
262 purple_debug_info("thrasher ft",
263 "%d: thrasher_xfer_send_complete_cb\n",
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",
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
,
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",
304 xfer
->local_filename
,
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
);
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
);
328 id
= store_xfer(xfer
);
330 purple_debug_info("thrasher ft",
331 "%d: thrasher_add_xfer\n",
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
);
341 purple_debug_info("thrasher ft",
342 "%d: %0.2f%% complete\n",
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
);
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",
365 thrasher_ui_write(PurpleXfer
*xfer
,
366 const guchar
*buffer
,
368 guint id
= get_id_by_xfer(xfer
);
369 purple_debug_info("thrasher ft",
370 "%d: asked to write %d bytes.\n",
373 gssize written_sz
= thrasher_wrapper_ft_write(id
, buffer
, size
);
374 purple_debug_info("thrasher ft",
375 "%d: wrote %d bytes.\n",
379 /* HACK: For some reason, libpurple 2.6 doesn't scale the window
380 * if write is incomplete like when reading--it cancels the
381 * transfer entirely! So:
382 * 1. Have Perl-side write stash the leftover data.
383 * 2. Do our own window scaling here.
384 * 3. Lie to libpurple and say write completed.
385 * 4. Have Perl-side slip stashed data into next write.
387 if (written_sz
< size
) {
388 xfer
->current_buffer_size
= MAX(xfer
->current_buffer_size
* 0.5,
389 /* Very low because of above hackery. */
391 if (written_sz
== 0) {
392 /* Extreme case: wasn't ready to write anything at all.
393 * Perl-side will call ui_ready when caught up.
395 purple_input_remove(xfer
->watcher
);
403 thrasher_ui_read(PurpleXfer
*xfer
,
406 guint id
= get_id_by_xfer(xfer
);
407 purple_debug_info("thrasher ft",
408 "%d: asked to read %d bytes.\n",
411 gssize read_sz
= thrasher_wrapper_ft_read(id
, buffer
, size
);
412 purple_debug_info("thrasher ft",
413 "%d: read %d bytes.\n",
420 thrasher_data_not_sent(PurpleXfer
*xfer
,
421 const guchar
*buffer
,
423 guint id
= get_id_by_xfer(xfer
);
424 purple_debug_info("thrasher ft",
425 "%d: asked to unread %d bytes.\n",
427 thrasher_wrapper_ft_data_not_sent(id
, buffer
, size
);
431 * Initialization for libpurple file transfers
435 thrasher_xfer_get_handle()
441 /* thrasher_request_file() exists only to skip over
442 * purple_xfer_choose_file_ok_cb() to purple_xfer_request_accepted().
443 * ok_cb verifies the file exists on disk, but with Thrasher it never
444 * does. Obviously, Thrasher need not prompt for a filename here.
446 void *thrasher_request_file(const char *title
, const char *filename
,
447 gboolean savedialog
, GCallback ok_cb
,
449 PurpleAccount
*account
,
451 PurpleConversation
*conv
,
453 PurpleXfer
* xfer
= user_data
;
454 purple_debug_info("thrasher ft",
455 "thrasher_request_file (%s -> %s)\n",
458 if (purple_xfer_get_type(xfer
) == PURPLE_XFER_RECEIVE
) {
459 purple_debug_info("thrasher ft",
460 "Should skip ask_accept/request_file"
461 " if receiving. Refer to"
462 " thrasher_xfer_recv_request_responder().\n");
466 guint id
= get_id_by_xfer(xfer
);
467 purple_debug_info("thrasher ft",
468 "%d: about to purple_xfer_request_accepted\n",
470 /* local_filename already set, but do want xfer_add and ops.init called. */
471 purple_xfer_request_accepted(xfer
,
474 /* no meaningful ui handle */
478 void thrasher_xfer_individual_init(PurpleXfer
*xfer
) {
479 purple_debug_info("thrasher ft",
480 "thrasher_xfer_individual_init called\n");
483 void thrasher_xfer_request_denied(PurpleXfer
*xfer
) {
484 purple_debug_info("thrasher ft",
485 "thrasher_xfer_request_denied called\n");
488 void thrasher_xfer_start(PurpleXfer
*xfer
) {
489 guint id
= get_id_by_xfer(xfer
);
490 purple_debug_info("thrasher ft",
491 "%d: thrasher_xfer_start\n",
495 void thrasher_xfer_end(PurpleXfer
*xfer
) {
496 purple_debug_info("thrasher ft",
497 "thrasher_xfer_end called\n");
500 void thrasher_xfer_cancel_send(PurpleXfer
*xfer
) {
501 purple_debug_info("thrasher ft",
502 "thrasher_xfer_cancel_send called\n");
505 void thrasher_xfer_cancel_receive(PurpleXfer
*xfer
) {
506 purple_debug_info("thrasher ft",
507 "thraher_xfer_cancel_receive called\n");
510 gssize
thrasher_xfer_read(guchar
**buffer
, PurpleXfer
*xfer
) {
511 purple_debug_info("thrasher ft",
512 "trasher_xfer_read called\n");
515 gssize
thrasher_xfer_write(const guchar
*buffer
, size_t size
,
517 purple_debug_info("thrasher ft",
518 "thrasher_xfer_write called with %d bytes\n",
522 void thrasher_xfer_ack(PurpleXfer
*xfer
, const guchar
*buffer
,
524 purple_debug_info("thrasher ft",
525 "thrasher_xfer_ack called\n");
528 static PurpleXferUiOps thrasher_xfer_uiops
= {
530 thrasher_destroy_xfer
,
532 thrasher_update_xfer_progress
,
533 thrasher_xfer_cancel_local
,
534 thrasher_xfer_cancel_remote
,
537 thrasher_data_not_sent
,
543 void thrasher_xfer_init()
545 /* thrasher_xfer_destroy() would release id_to_xfer... but that is
546 * assumed to always coincide with process exit.
548 id_to_xfer
= g_hash_table_new(g_direct_hash
, g_direct_equal
);
551 purple_xfers_set_ui_ops(&thrasher_xfer_uiops
);
553 void *ft_handle
= purple_xfers_get_handle();
554 gpointer thrasher_xfer_handle
= thrasher_xfer_get_handle();
556 purple_signal_connect(ft_handle
,
558 thrasher_xfer_handle
,
559 PURPLE_CALLBACK(thrasher_xfer_recv_request_cb
),
561 purple_signal_connect(ft_handle
,
563 thrasher_xfer_handle
,
564 PURPLE_CALLBACK(thrasher_xfer_recv_accept_cb
),
566 purple_signal_connect(ft_handle
,
568 thrasher_xfer_handle
,
569 PURPLE_CALLBACK(thrasher_xfer_recv_start_cb
),
571 purple_signal_connect(ft_handle
,
573 thrasher_xfer_handle
,
574 PURPLE_CALLBACK(thrasher_xfer_recv_cancel_cb
),
576 purple_signal_connect(ft_handle
,
577 "file-recv-complete",
578 thrasher_xfer_handle
,
579 PURPLE_CALLBACK(thrasher_xfer_recv_complete_cb
),
581 purple_signal_connect(ft_handle
,
583 thrasher_xfer_handle
,
584 PURPLE_CALLBACK(thrasher_xfer_send_accept_cb
),
586 purple_signal_connect(ft_handle
,
588 thrasher_xfer_handle
,
589 PURPLE_CALLBACK(thrasher_xfer_send_start_cb
),
591 purple_signal_connect(ft_handle
,
593 thrasher_xfer_handle
,
594 PURPLE_CALLBACK(thrasher_xfer_send_cancel_cb
),
596 purple_signal_connect(ft_handle
,
597 "file-send-complete",
598 thrasher_xfer_handle
,
599 PURPLE_CALLBACK(thrasher_xfer_send_complete_cb
),
605 * Actually initiate a file transfer
608 thrasher_send_file(PurpleAccount
*account
,
613 g_return_val_if_fail(account
, 0);
614 g_return_val_if_fail(who
, 0);
615 g_return_val_if_fail(filename
, 0);
616 g_return_val_if_fail(size
, 0);
617 g_return_val_if_fail(desc
, 0);
619 PurpleConnection
*connection
= thrasher_connection_for_account(account
);
621 purple_debug_info("thrasher ft",
622 "Connection could not be found for account!?\n");
626 PurplePlugin
*prpl
= purple_connection_get_prpl(connection
);
628 purple_debug_info("thrasher ft",
629 "prpl could not be found for connection!?\n");
633 PurplePluginProtocolInfo
*prpl_info
= PURPLE_PLUGIN_PROTOCOL_INFO(prpl
);
635 purple_debug_info("thrasher ft",
636 "prpl_info could not be found for prpl!?\n");
640 if (prpl_info
->can_receive_file
&& ! prpl_info
->can_receive_file(connection
,
642 purple_debug_info("thrasher ft",
643 "prpl says %s can't receive a file.\n",
648 /* From now on, assume the FT will either succeed or be canceled.
650 * This is the last chance to decide how to proceed until/if
651 * libpurple calls us back. If we're wrong and the libpurple side
652 * ignores (neither uses nor cancels) the xfer, the XMPP user will
653 * be connected to a proxy that goes nowhere until the proxied
654 * transfer "succeeds"....
656 * Because serv_send_file() returns void and thus can't be
657 * checked, add checks as necessary above.
659 size_t id
= get_next_file_transfer_id();
661 /* TODO: separate structures for this backchannel and ui_data.
662 * local_filename and size are redundant once there's an xfer for
663 * this to be a ui_data of.
665 Thrasher_Xfer_UI_Data
* ui_data
= g_new(Thrasher_Xfer_UI_Data
, 1);
666 ui_data
->filename
= g_strdup(filename
);
667 ui_data
->size
= size
;
669 ui_data
->desc
= g_strdup(desc
);
671 /* FIXME: better not have more than one FT to the same who at the
673 GHashTable
* pending_send_file
674 = thrasher_account_get_pending_send_file(account
);
675 g_hash_table_insert(pending_send_file
,
679 purple_debug_info("thrasher ft",
680 "%d: calling serv_send for file %s size %d to %s\n",
686 /* Passing NULL filename causes the prpl to not call
687 * purple_xfer_request_accepted() which is mostly a noop because
688 * Thrasher has a ui_read callback. Would be nice if
690 * 1. request_accepted generated a file-send-accept signal and
692 * 2. it was only called after the remote side had in fact accepted
694 * but neither is currently true in most prpls. Instead, Perl-side
695 * always accepts but then cancel if no remote side appears.
697 serv_send_file(connection
, who
, NULL
);