Handle unescaped accented chars in incoming MSN messages.
[thrasher.git] / thft.c
blobeb15336a39469b16f0ad8e3f08ac3c7ba48e297a
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, buffer, size);
374 purple_debug_info("thrasher ft",
375 "%d: wrote %d bytes.\n",
377 written_sz);
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. */
390 128);
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);
396 xfer->watcher = 0;
399 return size;
402 gssize
403 thrasher_ui_read(PurpleXfer *xfer,
404 guchar **buffer,
405 gssize size) {
406 guint id = get_id_by_xfer(xfer);
407 purple_debug_info("thrasher ft",
408 "%d: asked to read %d bytes.\n",
410 size);
411 gssize read_sz = thrasher_wrapper_ft_read(id, buffer, size);
412 purple_debug_info("thrasher ft",
413 "%d: read %d bytes.\n",
415 read_sz);
416 return read_sz;
419 void
420 thrasher_data_not_sent(PurpleXfer *xfer,
421 const guchar *buffer,
422 gsize size) {
423 guint id = get_id_by_xfer(xfer);
424 purple_debug_info("thrasher ft",
425 "%d: asked to unread %d bytes.\n",
426 id, size);
427 thrasher_wrapper_ft_data_not_sent(id, buffer, size);
431 * Initialization for libpurple file transfers
434 static gpointer
435 thrasher_xfer_get_handle()
437 static int handle;
438 return &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,
448 GCallback cancel_cb,
449 PurpleAccount *account,
450 const char *who,
451 PurpleConversation *conv,
452 void *user_data) {
453 PurpleXfer* xfer = user_data;
454 purple_debug_info("thrasher ft",
455 "thrasher_request_file (%s -> %s)\n",
456 filename, who);
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");
463 return NULL;
466 guint id = get_id_by_xfer(xfer);
467 purple_debug_info("thrasher ft",
468 "%d: about to purple_xfer_request_accepted\n",
469 id);
470 /* local_filename already set, but do want xfer_add and ops.init called. */
471 purple_xfer_request_accepted(xfer,
472 xfer->filename);
474 /* no meaningful ui handle */
475 return NULL;
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",
492 id);
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,
516 PurpleXfer *xfer) {
517 purple_debug_info("thrasher ft",
518 "thrasher_xfer_write called with %d bytes\n",
519 size);
522 void thrasher_xfer_ack(PurpleXfer *xfer, const guchar *buffer,
523 size_t size) {
524 purple_debug_info("thrasher ft",
525 "thrasher_xfer_ack called\n");
528 static PurpleXferUiOps thrasher_xfer_uiops = {
529 thrasher_new_xfer,
530 thrasher_destroy_xfer,
531 thrasher_add_xfer,
532 thrasher_update_xfer_progress,
533 thrasher_xfer_cancel_local,
534 thrasher_xfer_cancel_remote,
535 thrasher_ui_write,
536 thrasher_ui_read,
537 thrasher_data_not_sent,
539 // 1 reserved
540 NULL
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);
550 purple_xfers_init();
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,
557 "file-recv-request",
558 thrasher_xfer_handle,
559 PURPLE_CALLBACK(thrasher_xfer_recv_request_cb),
560 NULL);
561 purple_signal_connect(ft_handle,
562 "file-recv-accept",
563 thrasher_xfer_handle,
564 PURPLE_CALLBACK(thrasher_xfer_recv_accept_cb),
565 NULL);
566 purple_signal_connect(ft_handle,
567 "file-recv-start",
568 thrasher_xfer_handle,
569 PURPLE_CALLBACK(thrasher_xfer_recv_start_cb),
570 NULL);
571 purple_signal_connect(ft_handle,
572 "file-recv-cancel",
573 thrasher_xfer_handle,
574 PURPLE_CALLBACK(thrasher_xfer_recv_cancel_cb),
575 NULL);
576 purple_signal_connect(ft_handle,
577 "file-recv-complete",
578 thrasher_xfer_handle,
579 PURPLE_CALLBACK(thrasher_xfer_recv_complete_cb),
580 NULL);
581 purple_signal_connect(ft_handle,
582 "file-send-accept",
583 thrasher_xfer_handle,
584 PURPLE_CALLBACK(thrasher_xfer_send_accept_cb),
585 NULL);
586 purple_signal_connect(ft_handle,
587 "file-send-start",
588 thrasher_xfer_handle,
589 PURPLE_CALLBACK(thrasher_xfer_send_start_cb),
590 NULL);
591 purple_signal_connect(ft_handle,
592 "file-send-cancel",
593 thrasher_xfer_handle,
594 PURPLE_CALLBACK(thrasher_xfer_send_cancel_cb),
595 NULL);
596 purple_signal_connect(ft_handle,
597 "file-send-complete",
598 thrasher_xfer_handle,
599 PURPLE_CALLBACK(thrasher_xfer_send_complete_cb),
600 NULL);
605 * Actually initiate a file transfer
607 size_t
608 thrasher_send_file(PurpleAccount *account,
609 const char *who,
610 char* filename,
611 size_t size,
612 char* desc) {
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);
620 if (!connection) {
621 purple_debug_info("thrasher ft",
622 "Connection could not be found for account!?\n");
623 return 0;
626 PurplePlugin *prpl = purple_connection_get_prpl(connection);
627 if (! prpl) {
628 purple_debug_info("thrasher ft",
629 "prpl could not be found for connection!?\n");
630 return 0;
633 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
634 if (! prpl_info) {
635 purple_debug_info("thrasher ft",
636 "prpl_info could not be found for prpl!?\n");
637 return 0;
640 if (prpl_info->can_receive_file && ! prpl_info->can_receive_file(connection,
641 who)) {
642 purple_debug_info("thrasher ft",
643 "prpl says %s can't receive a file.\n",
644 who);
645 return 0;
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;
668 ui_data->id = id;
669 ui_data->desc = g_strdup(desc);
671 /* FIXME: better not have more than one FT to the same who at the
672 * same time.... */
673 GHashTable* pending_send_file
674 = thrasher_account_get_pending_send_file(account);
675 g_hash_table_insert(pending_send_file,
676 (void*)who,
677 ui_data);
679 purple_debug_info("thrasher ft",
680 "%d: calling serv_send for file %s size %d to %s\n",
682 filename,
683 size,
684 who);
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);
699 return id;