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 static PurpleXferUiOps thrasher_xfer_uiops
= {
480 thrasher_destroy_xfer
,
482 thrasher_update_xfer_progress
,
483 thrasher_xfer_cancel_local
,
484 thrasher_xfer_cancel_remote
,
487 thrasher_data_not_sent
,
493 void thrasher_xfer_init()
495 /* thrasher_xfer_destroy() would release id_to_xfer... but that is
496 * assumed to always coincide with process exit.
498 id_to_xfer
= g_hash_table_new(g_direct_hash
, g_direct_equal
);
501 purple_xfers_set_ui_ops(&thrasher_xfer_uiops
);
503 void *ft_handle
= purple_xfers_get_handle();
504 gpointer thrasher_xfer_handle
= thrasher_xfer_get_handle();
506 purple_signal_connect(ft_handle
,
508 thrasher_xfer_handle
,
509 PURPLE_CALLBACK(thrasher_xfer_recv_request_cb
),
511 purple_signal_connect(ft_handle
,
513 thrasher_xfer_handle
,
514 PURPLE_CALLBACK(thrasher_xfer_recv_accept_cb
),
516 purple_signal_connect(ft_handle
,
518 thrasher_xfer_handle
,
519 PURPLE_CALLBACK(thrasher_xfer_recv_start_cb
),
521 purple_signal_connect(ft_handle
,
523 thrasher_xfer_handle
,
524 PURPLE_CALLBACK(thrasher_xfer_recv_cancel_cb
),
526 purple_signal_connect(ft_handle
,
527 "file-recv-complete",
528 thrasher_xfer_handle
,
529 PURPLE_CALLBACK(thrasher_xfer_recv_complete_cb
),
531 purple_signal_connect(ft_handle
,
533 thrasher_xfer_handle
,
534 PURPLE_CALLBACK(thrasher_xfer_send_accept_cb
),
536 purple_signal_connect(ft_handle
,
538 thrasher_xfer_handle
,
539 PURPLE_CALLBACK(thrasher_xfer_send_start_cb
),
541 purple_signal_connect(ft_handle
,
543 thrasher_xfer_handle
,
544 PURPLE_CALLBACK(thrasher_xfer_send_cancel_cb
),
546 purple_signal_connect(ft_handle
,
547 "file-send-complete",
548 thrasher_xfer_handle
,
549 PURPLE_CALLBACK(thrasher_xfer_send_complete_cb
),
555 * Actually initiate a file transfer
558 thrasher_send_file(PurpleAccount
*account
,
563 g_return_val_if_fail(account
, 0);
564 g_return_val_if_fail(who
, 0);
565 g_return_val_if_fail(filename
, 0);
566 g_return_val_if_fail(size
, 0);
567 g_return_val_if_fail(desc
, 0);
569 PurpleConnection
*connection
= thrasher_connection_for_account(account
);
571 purple_debug_info("thrasher ft",
572 "Connection could not be found for account!?\n");
576 PurplePlugin
*prpl
= purple_connection_get_prpl(connection
);
578 purple_debug_info("thrasher ft",
579 "prpl could not be found for connection!?\n");
583 PurplePluginProtocolInfo
*prpl_info
= PURPLE_PLUGIN_PROTOCOL_INFO(prpl
);
585 purple_debug_info("thrasher ft",
586 "prpl_info could not be found for prpl!?\n");
590 if (prpl_info
->can_receive_file
&& ! prpl_info
->can_receive_file(connection
,
592 purple_debug_info("thrasher ft",
593 "prpl says %s can't receive a file.\n",
598 /* From now on, assume the FT will either succeed or be canceled.
600 * This is the last chance to decide how to proceed until/if
601 * libpurple calls us back. If we're wrong and the libpurple side
602 * ignores (neither uses nor cancels) the xfer, the XMPP user will
603 * be connected to a proxy that goes nowhere until the proxied
604 * transfer "succeeds"....
606 * Because serv_send_file() returns void and thus can't be
607 * checked, add checks as necessary above.
609 size_t id
= get_next_file_transfer_id();
611 /* TODO: separate structures for this backchannel and ui_data.
612 * local_filename and size are redundant once there's an xfer for
613 * this to be a ui_data of.
615 Thrasher_Xfer_UI_Data
* ui_data
= g_new(Thrasher_Xfer_UI_Data
, 1);
616 ui_data
->filename
= g_strdup(filename
);
617 ui_data
->size
= size
;
619 ui_data
->desc
= g_strdup(desc
);
621 /* FIXME: better not have more than one FT to the same who at the
623 GHashTable
* pending_send_file
624 = thrasher_account_get_pending_send_file(account
);
625 g_hash_table_insert(pending_send_file
,
629 purple_debug_info("thrasher ft",
630 "%d: calling serv_send for file %s size %d to %s\n",
636 /* Passing NULL filename causes the prpl to not call
637 * purple_xfer_request_accepted() which is mostly a noop because
638 * Thrasher has a ui_read callback. Would be nice if
640 * 1. request_accepted generated a file-send-accept signal and
642 * 2. it was only called after the remote side had in fact accepted
644 * but neither is currently true in most prpls. Instead, Perl-side
645 * always accepts but then cancel if no remote side appears.
647 serv_send_file(connection
, who
, NULL
);