diff options
44 files changed, 5008 insertions, 248 deletions
diff --git a/configure.ac b/configure.ac index 98d0901b2..538b731c1 100644 --- a/configure.ac +++ b/configure.ac @@ -256,6 +256,11 @@ PKG_CHECK_MODULES(SOUP, libsoup-2.4) AC_SUBST(SOUP_CFLAGS) AC_SUBST(SOUP_LIBS) +dnl Check for libnice +PKG_CHECK_MODULES(NICE, nice >= 0.0.11) +AC_SUBST(NICE_CFLAGS) +AC_SUBST(NICE_LIBS) + PKG_CHECK_MODULES([UUID], [uuid]) AC_SUBST([UUID_CFLAGS]) AC_SUBST([UUID_LIBS]) diff --git a/extensions/Channel_Type_FileTransfer_Future.xml b/extensions/Channel_Type_FileTransfer_Future.xml new file mode 100644 index 000000000..b155136e0 --- /dev/null +++ b/extensions/Channel_Type_FileTransfer_Future.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" ?> +<node name="/Channel_Type_FileTransfer_Future" + xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0"> + <tp:copyright>Copyright (C) 2010 Collabora Ltd.</tp:copyright> + <tp:license xmlns="http://www.w3.org/1999/xhtml"> + <p>This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version.</p> + +<p>This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details.</p> + +<p>You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</p> + </tp:license> + <interface name="org.freedesktop.Telepathy.Channel.Type.FileTransfer.FUTURE" + tp:causes-havoc="a staging area for future File Transfer Channel functionality"> + + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + <p>This interface contains functionality which we intend to incorporate + into the File Transfer Channel interface in future. + It should be considered to be conceptually part of the core + File Transfer Channel interface, but without API or ABI guarantees.</p> + + <tp:rationale> + <p>If we add new functionality to the Channel interface, libraries + that use generated code (notably telepathy-glib) will have it as + part of their ABI forever, meaning we can't make incompatible + changes. By using this interface as a staging area for future + Channel functionality, we can try out new properties, signals + and methods as application-specific extensions, then merge them + into the core Channel interface when we have enough implementation + experience to declare them to be stable.</p> + + <p>The name is by analogy to Python's <code>__future__</code> + pseudo-module.</p> + </tp:rationale> + </tp:docstring> + + <property name="FileCollection" tp:name-for-bindings="FileCollection" + type="s" access="read"> + <tp:added version="0.19.2">(in Channel.Type.FileTransfer.FUTURE + pseudo-interface)</tp:added> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + <p>The FileCollection to which this channel belongs.</p> + + <p>A channel's FileCollection property can never change.</p> + + <p>At least on GTalk and apparently also on iChat the user can + send a set of files to a contact and that contact can then + pick and choose which files to actually receive. + + The CM should emit all new FT channels belonging to one collection + at the same time, UIs supporting this feature can then + bundle all these channels together in some way and show a + nice UI. UIs not supporting it will treat them as seperate + transfers, which is not great but a reasonable fallback</p> + </tp:docstring> + </property> + + </interface> +</node> +<!-- vim:set sw=2 sts=2 et ft=xml: --> diff --git a/extensions/Makefile.am b/extensions/Makefile.am index 76bbc84d3..ba369af96 100644 --- a/extensions/Makefile.am +++ b/extensions/Makefile.am @@ -12,6 +12,7 @@ EXTRA_DIST = \ Channel_Future.xml \ Channel_Interface_Conference.xml \ Channel_Type_Call.xml \ + Channel_Type_FileTransfer_Future.xml \ Connection_Future.xml \ Connection_Interface_Gabble_Decloak.xml \ Connection_Interface_Mail_Notification.xml \ diff --git a/extensions/all.xml b/extensions/all.xml index 9d33bfe7a..22827090f 100644 --- a/extensions/all.xml +++ b/extensions/all.xml @@ -43,6 +43,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA</p> <xi:include href="OLPC_Channel_Type_ActivityView.xml"/> <xi:include href="Channel_Type_Contact_Search.xml"/> +<xi:include href="Channel_Type_FileTransfer_Future.xml"/> <xi:include href="Connection_Interface_Gabble_Decloak.xml"/> <xi:include href="Channel_Interface_Conference.xml"/> <xi:include href="Connection_Interface_Mail_Notification.xml"/> diff --git a/src/Makefile.am b/src/Makefile.am index e690d4532..755ee01e7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -93,6 +93,8 @@ libgabble_convenience_la_SOURCES = \ ft-manager.h \ gabble.c \ gabble.h \ + gtalk-file-collection.h \ + gtalk-file-collection.c \ im-channel.h \ im-channel.c \ im-factory.h \ @@ -101,6 +103,8 @@ libgabble_convenience_la_SOURCES = \ jingle-content.c \ jingle-factory.h \ jingle-factory.c \ + jingle-share.h \ + jingle-share.c \ jingle-media-rtp.h \ jingle-media-rtp.c \ jingle-session.h \ @@ -216,14 +220,14 @@ noinst_LTLIBRARIES = libgabble-convenience.la AM_CFLAGS = $(ERROR_CFLAGS) -I$(top_srcdir) -I$(top_builddir) \ @DBUS_CFLAGS@ @GLIB_CFLAGS@ @WOCKY_CFLAGS@ \ @HANDLE_LEAK_DEBUG_CFLAGS@ @TP_GLIB_CFLAGS@ \ - @SOUP_CFLAGS@ @UUID_CFLAGS@ @GMODULE_CFLAGS@ \ + @SOUP_CFLAGS@ @NICE_CFLAGS@ @UUID_CFLAGS@ @GMODULE_CFLAGS@ \ @SQLITE_CFLAGS@ \ -I $(top_srcdir)/lib -I $(top_builddir)/lib \ -DG_LOG_DOMAIN=\"gabble\" \ -DPLUGIN_DIR=\"$(libdir)/telepathy/gabble-0\" ALL_LIBS = @DBUS_LIBS@ @GLIB_LIBS@ @WOCKY_LIBS@ @TP_GLIB_LIBS@ \ - @SOUP_LIBS@ @UUID_LIBS@ @GMODULE_LIBS@ @SQLITE_LIBS@ + @SOUP_LIBS@ @NICE_LIBS@ @UUID_LIBS@ @GMODULE_LIBS@ @SQLITE_LIBS@ # build gibber first all: gibber diff --git a/src/capabilities.c b/src/capabilities.c index 2c6f3bcdb..671953b28 100644 --- a/src/capabilities.c +++ b/src/capabilities.c @@ -68,6 +68,7 @@ static const Feature self_advertised_features[] = { FEATURE_OPTIONAL, NS_GOOGLE_TRANSPORT_P2P }, { FEATURE_OPTIONAL, NS_JINGLE_TRANSPORT_ICEUDP }, + { FEATURE_OPTIONAL, NS_GOOGLE_FEAT_SHARE }, { FEATURE_OPTIONAL, NS_GOOGLE_FEAT_VOICE }, { FEATURE_OPTIONAL, NS_GOOGLE_FEAT_VIDEO }, { FEATURE_OPTIONAL, NS_JINGLE_DESCRIPTION_AUDIO }, @@ -92,6 +93,7 @@ static const Feature quirks[] = { }; static GabbleCapabilitySet *legacy_caps = NULL; +static GabbleCapabilitySet *share_v1_caps = NULL; static GabbleCapabilitySet *voice_v1_caps = NULL; static GabbleCapabilitySet *video_v1_caps = NULL; static GabbleCapabilitySet *any_audio_caps = NULL; @@ -111,6 +113,12 @@ gabble_capabilities_get_legacy (void) } const GabbleCapabilitySet * +gabble_capabilities_get_bundle_share_v1 (void) +{ + return share_v1_caps; +} + +const GabbleCapabilitySet * gabble_capabilities_get_bundle_voice_v1 (void) { return voice_v1_caps; @@ -255,6 +263,9 @@ gabble_capabilities_init (GabbleConnection *conn) gabble_capability_set_add (legacy_caps, feat->ns); } + share_v1_caps = gabble_capability_set_new (); + gabble_capability_set_add (share_v1_caps, NS_GOOGLE_FEAT_SHARE); + voice_v1_caps = gabble_capability_set_new (); gabble_capability_set_add (voice_v1_caps, NS_GOOGLE_FEAT_VOICE); @@ -320,6 +331,7 @@ gabble_capabilities_finalize (GabbleConnection *conn) if (--feature_handles_refcount == 0) { gabble_capability_set_free (legacy_caps); + gabble_capability_set_free (share_v1_caps); gabble_capability_set_free (voice_v1_caps); gabble_capability_set_free (video_v1_caps); gabble_capability_set_free (any_audio_caps); @@ -333,6 +345,7 @@ gabble_capabilities_finalize (GabbleConnection *conn) gabble_capability_set_free (olpc_caps); legacy_caps = NULL; + share_v1_caps = NULL; voice_v1_caps = NULL; video_v1_caps = NULL; any_audio_caps = NULL; @@ -373,8 +386,10 @@ capabilities_fill_cache (GabblePresenceCache *cache) GOOGLE_BUNDLE ("voice-v1", NS_GOOGLE_FEAT_VOICE); GOOGLE_BUNDLE ("video-v1", NS_GOOGLE_FEAT_VIDEO); - /* Not really sure what these ones are. */ - GOOGLE_BUNDLE ("share-v1", NULL); + /* File transfer support */ + GOOGLE_BUNDLE ("share-v1", NS_GOOGLE_FEAT_SHARE); + + /* Not really sure what this ones is. */ GOOGLE_BUNDLE ("sms-v1", NULL); /* TODO: remove this when we fix fd.o#22768. */ @@ -396,6 +411,8 @@ capabilities_fill_cache (GabblePresenceCache *cache) NS_GABBLE_CAPS "#" BUNDLE_VOICE_V1, NS_GOOGLE_FEAT_VOICE); gabble_presence_cache_add_bundle_caps (cache, NS_GABBLE_CAPS "#" BUNDLE_VIDEO_V1, NS_GOOGLE_FEAT_VIDEO); + gabble_presence_cache_add_bundle_caps (cache, + NS_GABBLE_CAPS "#" BUNDLE_SHARE_V1, NS_GOOGLE_FEAT_SHARE); } const CapabilityConversionData capabilities_conversions[] = diff --git a/src/capabilities.h b/src/capabilities.h index ed33ed411..9f78bf3f1 100644 --- a/src/capabilities.h +++ b/src/capabilities.h @@ -59,10 +59,12 @@ const GabbleCapabilitySet *gabble_capabilities_get_olpc_notify (void); * clients require the bundle names "voice-v1" and "video-v1". We keep these * names for compatibility. */ +#define BUNDLE_SHARE_V1 "share-v1" #define BUNDLE_VOICE_V1 "voice-v1" #define BUNDLE_VIDEO_V1 "video-v1" #define BUNDLE_PMUC_V1 "pmuc-v1" +const GabbleCapabilitySet *gabble_capabilities_get_bundle_share_v1 (void); const GabbleCapabilitySet *gabble_capabilities_get_bundle_voice_v1 (void); const GabbleCapabilitySet *gabble_capabilities_get_bundle_video_v1 (void); diff --git a/src/connection.c b/src/connection.c index 8a27f7eac..09164fc4c 100644 --- a/src/connection.c +++ b/src/connection.c @@ -2183,7 +2183,7 @@ gabble_connection_fill_in_caps (GabbleConnection *self, GabblePresence *presence = self->self_presence; LmMessageNode *node = lm_message_get_node (presence_message); gchar *caps_hash; - gboolean voice_v1, video_v1; + gboolean share_v1, voice_v1, video_v1; GString *ext = g_string_new (""); /* XEP-0115 version 1.5 uses a verification string in the 'ver' attribute */ @@ -2206,9 +2206,13 @@ gabble_connection_fill_in_caps (GabbleConnection *self, g_string_append (ext, BUNDLE_PMUC_V1); + share_v1 = gabble_presence_has_cap (presence, NS_GOOGLE_FEAT_SHARE); voice_v1 = gabble_presence_has_cap (presence, NS_GOOGLE_FEAT_VOICE); video_v1 = gabble_presence_has_cap (presence, NS_GOOGLE_FEAT_VIDEO); + if (share_v1) + g_string_append (ext, " " BUNDLE_SHARE_V1); + if (voice_v1) g_string_append (ext, " " BUNDLE_VOICE_V1); @@ -2542,6 +2546,10 @@ connection_iq_disco_cb (LmMessageHandler *handler, * because capabilities_get_features() always includes a few bonus * features... */ + + if (!tp_strdiff (suffix, BUNDLE_SHARE_V1)) + features = gabble_capabilities_get_bundle_share_v1 (); + if (!tp_strdiff (suffix, BUNDLE_VOICE_V1)) features = gabble_capabilities_get_bundle_voice_v1 (); diff --git a/src/debug.c b/src/debug.c index 701e9c9f0..014c80193 100644 --- a/src/debug.c +++ b/src/debug.c @@ -45,6 +45,7 @@ static GDebugKey keys[] = { { "plugins", GABBLE_DEBUG_PLUGINS }, { "mail", GABBLE_DEBUG_MAIL_NOTIF }, { "authentication", GABBLE_DEBUG_AUTH }, + { "share", GABBLE_DEBUG_SHARE }, { 0, }, }; diff --git a/src/debug.h b/src/debug.h index d79372681..85d5971a1 100644 --- a/src/debug.h +++ b/src/debug.h @@ -32,7 +32,8 @@ typedef enum GABBLE_DEBUG_PLUGINS = 1 << 21, GABBLE_DEBUG_MAIL_NOTIF = 1 << 22, GABBLE_DEBUG_AUTH = 1 << 23, - GABBLE_DEBUG_SLACKER = 1 << 24 + GABBLE_DEBUG_SLACKER = 1 << 24, + GABBLE_DEBUG_SHARE = 1 << 25 } GabbleDebugFlags; void gabble_debug_set_flags_from_env (void); diff --git a/src/ft-channel.c b/src/ft-channel.c index a50608c08..12bbb1442 100644 --- a/src/ft-channel.c +++ b/src/ft-channel.c @@ -1,6 +1,6 @@ /* * ft-channel.c - Source for GabbleFileTransferChannel - * Copyright (C) 2009 Collabora Ltd. + * Copyright (C) 2009-2010 Collabora Ltd. * @author: Guillaume Desmottes <guillaume.desmottes@collabora.co.uk> * * This library is free software; you can redistribute it and/or @@ -18,7 +18,7 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#include <config.h> +#include "config.h" #include <glib/gstdio.h> #include <dbus/dbus-glib.h> @@ -43,7 +43,6 @@ #include <gibber/gibber-transport.h> #include <gibber/gibber-unix-transport.h> /* just for the feature-test */ -#include "bytestream-factory.h" #include "connection.h" #include "ft-channel.h" #include "gabble-signals-marshal.h" @@ -59,8 +58,16 @@ #include <telepathy-glib/svc-generic.h> #include <telepathy-glib/svc-channel.h> + static void channel_iface_init (gpointer g_iface, gpointer iface_data); static void file_transfer_iface_init (gpointer g_iface, gpointer iface_data); +static void transferred_chunk (GabbleFileTransferChannel *self, guint64 count); +static gboolean set_bytestream (GabbleFileTransferChannel *self, + GabbleBytestreamIface *bytestream); +static gboolean set_gtalk_file_collection (GabbleFileTransferChannel *self, + GTalkFileCollection *gtalk_file_collection); + + G_DEFINE_TYPE_WITH_CODE (GabbleFileTransferChannel, gabble_file_transfer_channel, G_TYPE_OBJECT, @@ -71,6 +78,8 @@ G_DEFINE_TYPE_WITH_CODE (GabbleFileTransferChannel, gabble_file_transfer_channel G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_IFACE, NULL); G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_TYPE_FILE_TRANSFER, file_transfer_iface_init); + G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SVC_CHANNEL_TYPE_FILETRANSFER_FUTURE, + NULL); ); #define GABBLE_UNDEFINED_FILE_SIZE G_MAXUINT64 @@ -108,9 +117,11 @@ enum PROP_TRANSFERRED_BYTES, PROP_INITIAL_OFFSET, PROP_RESUME_SUPPORTED, + PROP_FILE_COLLECTION, PROP_CONNECTION, PROP_BYTESTREAM, + PROP_GTALK_FILE_COLLECTION, LAST_PROPERTY }; @@ -126,9 +137,10 @@ struct _GabbleFileTransferChannelPrivate { TpSocketAddressType socket_type; GValue *socket_address; TpHandle initiator; - gboolean remote_accepted; gboolean resume_supported; + GTalkFileCollection *gtalk_file_collection; + GabbleBytestreamIface *bytestream; GibberListener *listener; GibberTransport *transport; @@ -145,20 +157,20 @@ struct _GabbleFileTransferChannelPrivate { guint64 transferred_bytes; guint64 initial_offset; guint64 date; + gchar *file_collection; + gboolean channel_opened; }; -static void set_bytestream (GabbleFileTransferChannel *self, - GabbleBytestreamIface *bytestream); -static void +void gabble_file_transfer_channel_do_close (GabbleFileTransferChannel *self) { if (self->priv->closed) return; DEBUG ("Emitting closed signal for %s", self->priv->object_path); - tp_svc_channel_emit_closed (self); self->priv->closed = TRUE; + tp_svc_channel_emit_closed (self); } static void @@ -265,6 +277,9 @@ gabble_file_transfer_channel_get_property (GObject *object, case PROP_DATE: g_value_set_uint64 (value, self->priv->date); break; + case PROP_FILE_COLLECTION: + g_value_set_string (value, self->priv->file_collection); + break; case PROP_CHANNEL_DESTROYED: g_value_set_boolean (value, self->priv->closed); break; @@ -293,11 +308,15 @@ gabble_file_transfer_channel_get_property (GObject *object, TP_IFACE_CHANNEL_TYPE_FILE_TRANSFER, "AvailableSocketTypes", TP_IFACE_CHANNEL_TYPE_FILE_TRANSFER, "TransferredBytes", TP_IFACE_CHANNEL_TYPE_FILE_TRANSFER, "InitialOffset", + GABBLE_IFACE_CHANNEL_TYPE_FILETRANSFER_FUTURE, "FileCollection", NULL)); break; case PROP_BYTESTREAM: g_value_set_object (value, self->priv->bytestream); break; + case PROP_GTALK_FILE_COLLECTION: + g_value_set_object (value, self->priv->gtalk_file_collection); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -370,12 +389,20 @@ gabble_file_transfer_channel_set_property (GObject *object, case PROP_INITIAL_OFFSET: self->priv->initial_offset = g_value_get_uint64 (value); break; + case PROP_FILE_COLLECTION: + g_free (self->priv->file_collection); + self->priv->file_collection = g_value_dup_string (value); + break; + case PROP_RESUME_SUPPORTED: + self->priv->resume_supported = g_value_get_boolean (value); + break; case PROP_BYTESTREAM: set_bytestream (self, GABBLE_BYTESTREAM_IFACE (g_value_get_object (value))); break; - case PROP_RESUME_SUPPORTED: - self->priv->resume_supported = g_value_get_boolean (value); + case PROP_GTALK_FILE_COLLECTION: + set_gtalk_file_collection (self, + GTALK_FILE_COLLECTION (g_value_get_object (value))); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); @@ -500,16 +527,10 @@ gabble_file_transfer_channel_constructor (GType type, tp_handle_inspect (contact_repo, self->priv->initiator), self->priv->filename, self->priv->size); - if (self->priv->initiator == base_conn->self_handle) - { - /* Outgoing FT , we'll need SOCK5 proxies when we'll offer the file */ - gabble_bytestream_factory_query_socks5_proxies ( - self->priv->connection->bytestream_factory); - } - return obj; } +static void close_session_and_transport (GabbleFileTransferChannel *self); static void gabble_file_transfer_channel_dispose (GObject *object); static void gabble_file_transfer_channel_finalize (GObject *object); @@ -548,6 +569,11 @@ gabble_file_transfer_channel_class_init ( { NULL } }; + static TpDBusPropertiesMixinPropImpl file_future_props[] = { + { "FileCollection", "file-collection", NULL }, + { NULL } + }; + static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = { { TP_IFACE_CHANNEL, tp_dbus_properties_mixin_getter_gobject_properties, @@ -559,6 +585,11 @@ gabble_file_transfer_channel_class_init ( NULL, file_props }, + { GABBLE_IFACE_CHANNEL_TYPE_FILETRANSFER_FUTURE, + tp_dbus_properties_mixin_getter_gobject_properties, + NULL, + file_future_props + }, { NULL } }; @@ -750,6 +781,15 @@ gabble_file_transfer_channel_class_init ( g_object_class_install_property (object_class, PROP_BYTESTREAM, param_spec); + param_spec = g_param_spec_object ( + "gtalk-file-collection", + "GTalkFileCollection object for gtalk-compatible file transfer", + "GTalk compatible file transfer collection", + G_TYPE_OBJECT, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_GTALK_FILE_COLLECTION, + param_spec); + param_spec = g_param_spec_boolean ( "resume-supported", "resume is supported", @@ -759,6 +799,16 @@ gabble_file_transfer_channel_class_init ( g_object_class_install_property (object_class, PROP_RESUME_SUPPORTED, param_spec); + param_spec = g_param_spec_string ( + "file-collection", + "gchar *file_colletion", + "Token identifying a collection of files", + "", + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_FILE_COLLECTION, + param_spec); + gabble_file_transfer_channel_class->dbus_props_class.interfaces = prop_interfaces; tp_dbus_properties_mixin_class_init (object_class, @@ -776,12 +826,13 @@ gabble_file_transfer_channel_dispose (GObject *object) if (self->priv->dispose_has_run) return; + DEBUG ("dispose called"); self->priv->dispose_has_run = TRUE; + gabble_file_transfer_channel_do_close (self); tp_handle_unref (handle_repo, self->priv->handle); tp_handle_unref (handle_repo, self->priv->initiator); - gabble_file_transfer_channel_do_close (self); if (self->priv->progress_timer != 0) { @@ -789,23 +840,8 @@ gabble_file_transfer_channel_dispose (GObject *object) self->priv->progress_timer = 0; } - if (self->priv->bytestream != NULL) - { - g_object_unref (self->priv->bytestream); - self->priv->bytestream = NULL; - } + close_session_and_transport (self); - if (self->priv->listener != NULL) - { - g_object_unref (self->priv->listener); - self->priv->listener = NULL; - } - - if (self->priv->transport != NULL) - { - g_object_unref (self->priv->transport); - self->priv->transport = NULL; - } /* release any references held by the object here */ @@ -847,13 +883,27 @@ gabble_file_transfer_channel_finalize (GObject *object) g_free (self->priv->content_hash); g_free (self->priv->description); g_hash_table_destroy (self->priv->available_socket_types); + g_free (self->priv->file_collection); G_OBJECT_CLASS (gabble_file_transfer_channel_parent_class)->finalize (object); } static void -close_bytestream_and_transport (GabbleFileTransferChannel *self) +close_session_and_transport (GabbleFileTransferChannel *self) { + + DEBUG ("Closing session and transport"); + if (self->priv->gtalk_file_collection != NULL) + { + gtalk_file_collection_terminate (self->priv->gtalk_file_collection, self); + /* the terminate could synchronously unref it and set it to NULL */ + if (self->priv->gtalk_file_collection != NULL) + { + g_object_unref (self->priv->gtalk_file_collection); + self->priv->gtalk_file_collection = NULL; + } + } + if (self->priv->bytestream != NULL) { gabble_bytestream_iface_close (self->priv->bytestream, NULL); @@ -861,6 +911,12 @@ close_bytestream_and_transport (GabbleFileTransferChannel *self) self->priv->bytestream = NULL; } + if (self->priv->listener != NULL) + { + g_object_unref (self->priv->listener); + self->priv->listener = NULL; + } + if (self->priv->transport != NULL) { g_object_unref (self->priv->transport); @@ -888,7 +944,7 @@ gabble_file_transfer_channel_close (TpSvcChannel *iface, TP_FILE_TRANSFER_STATE_CANCELLED, TP_FILE_TRANSFER_STATE_CHANGE_REASON_LOCAL_STOPPED); - close_bytestream_and_transport (self); + close_session_and_transport (self); } gabble_file_transfer_channel_do_close (GABBLE_FILE_TRANSFER_CHANNEL (iface)); @@ -1011,8 +1067,14 @@ check_address_and_access_control (GabbleFileTransferChannel *self, } static void -bytestream_open (GabbleFileTransferChannel *self) +channel_open (GabbleFileTransferChannel *self) { + DEBUG ("Channel open"); + + /* This is needed in case the ProvideFile wasn't called yet, to know if we + should go into OPEN state when ProvideFile gets called. */ + self->priv->channel_opened = TRUE; + if (self->priv->socket_address != NULL) { /* ProvideFile has already been called. Channel is Open */ @@ -1024,7 +1086,7 @@ bytestream_open (GabbleFileTransferChannel *self) TP_FILE_TRANSFER_STATE_OPEN, TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE); - if (self->priv->transport) + if (self->priv->transport != NULL) gibber_transport_block_receiving (self->priv->transport, FALSE); } else @@ -1040,7 +1102,8 @@ bytestream_open (GabbleFileTransferChannel *self) static void bytestream_closed (GabbleFileTransferChannel *self) { - if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED) + if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED && + self->priv->state != TP_FILE_TRANSFER_STATE_CANCELLED) { TpBaseConnection *base_conn = (TpBaseConnection *) self->priv->connection; @@ -1058,6 +1121,7 @@ bytestream_closed (GabbleFileTransferChannel *self) } } + static void bytestream_state_changed_cb (GabbleBytestreamIface *bytestream, GabbleBytestreamState state, @@ -1067,7 +1131,7 @@ bytestream_state_changed_cb (GabbleBytestreamIface *bytestream, if (state == GABBLE_BYTESTREAM_STATE_OPEN) { - bytestream_open (self); + channel_open (self); } else if (state == GABBLE_BYTESTREAM_STATE_CLOSED) { @@ -1078,19 +1142,45 @@ bytestream_state_changed_cb (GabbleBytestreamIface *bytestream, static void bytestream_write_blocked_cb (GabbleBytestreamIface *bytestream, gboolean blocked, GabbleFileTransferChannel *self); -static void +static gboolean set_bytestream (GabbleFileTransferChannel *self, - GabbleBytestreamIface *bytestream) + GabbleBytestreamIface *bytestream) + { if (bytestream == NULL) - return; + return FALSE; + + g_return_val_if_fail (self->priv->bytestream == NULL, FALSE); + g_return_val_if_fail (self->priv->gtalk_file_collection == NULL, FALSE); + + DEBUG ("Setting bytestream to %p", bytestream); self->priv->bytestream = g_object_ref (bytestream); gabble_signal_connect_weak (bytestream, "state-changed", G_CALLBACK (bytestream_state_changed_cb), G_OBJECT (self)); - gabble_signal_connect_weak (self->priv->bytestream, "write-blocked", + gabble_signal_connect_weak (bytestream, "write-blocked", G_CALLBACK (bytestream_write_blocked_cb), G_OBJECT (self)); + + return TRUE; +} + +static gboolean +set_gtalk_file_collection ( + GabbleFileTransferChannel *self, GTalkFileCollection *gtalk_file_collection) +{ + if (gtalk_file_collection == NULL) + return FALSE; + + g_return_val_if_fail (self->priv->bytestream == NULL, FALSE); + g_return_val_if_fail (self->priv->gtalk_file_collection == NULL, FALSE); + + self->priv->gtalk_file_collection = g_object_ref (gtalk_file_collection); + + /* No need to listen to any signals, the GTalkFileCollection will call our callbacks + on his own */ + + return TRUE; } static void @@ -1137,70 +1227,28 @@ bytestream_negotiate_cb (GabbleBytestreamIface *bytestream, set_bytestream (self, bytestream); - self->priv->remote_accepted = TRUE; } -gboolean -gabble_file_transfer_channel_offer_file (GabbleFileTransferChannel *self, - GError **error) +static gboolean +offer_bytestream (GabbleFileTransferChannel *self, const gchar *jid, + const gchar *resource, GError **error) { - GabblePresence *presence; gboolean result; LmMessage *msg; - TpHandleRepoIface *contact_repo, *room_repo; LmMessageNode *si_node, *file_node; - const gchar *jid; - gchar *full_jid, *stream_id, *size_str; - - g_assert (!CHECK_STR_EMPTY (self->priv->filename)); - g_assert (self->priv->size != GABBLE_UNDEFINED_FILE_SIZE); - g_assert (self->priv->bytestream == NULL); - - presence = gabble_presence_cache_get (self->priv->connection->presence_cache, - self->priv->handle); - if (presence == NULL) - { - DEBUG ("can't find contact's presence"); - g_set_error (error, TP_ERRORS, TP_ERROR_OFFLINE, - "can't find contact's presence"); - return FALSE; - } + gchar *stream_id, *size_str, *full_jid; - contact_repo = tp_base_connection_get_handles ( - (TpBaseConnection *) self->priv->connection, TP_HANDLE_TYPE_CONTACT); - room_repo = tp_base_connection_get_handles ( - (TpBaseConnection *) self->priv->connection, TP_HANDLE_TYPE_ROOM); + if (resource) + full_jid = g_strdup_printf ("%s/%s", jid, resource); + else + full_jid = g_strdup (jid); - jid = tp_handle_inspect (contact_repo, self->priv->handle); + DEBUG ("Offering SI Bytestream file transfer to %s", full_jid); - if (gabble_get_room_handle_from_jid (room_repo, jid) == 0) - { - /* Not a MUC jid, need to get a resource */ - const gchar *resource; + /* Outgoing FT , we'll need SOCK5 proxies */ + gabble_bytestream_factory_query_socks5_proxies ( + self->priv->connection->bytestream_factory); - /* FIXME: should we check for SI, bytestreams and/or IBB too? - * http://bugs.freedesktop.org/show_bug.cgi?id=23777 */ - resource = gabble_presence_pick_resource_by_caps (presence, - DEVICE_AGNOSTIC, - gabble_capability_set_predicate_has, NS_FILE_TRANSFER); - - if (resource == NULL) - { - DEBUG ("contact doesn't have file transfer capabilities"); - g_set_error (error, TP_ERRORS, TP_ERROR_NOT_CAPABLE, - "contact doesn't have file transfer capabilities"); - return FALSE; - } - - full_jid = g_strdup_printf ("%s/%s", jid, resource); - } - else - { - /* MUC jid, we already have the full jid */ - full_jid = g_strdup (jid); - } - - DEBUG ("Offering file transfer to %s", full_jid); stream_id = gabble_bytestream_factory_generate_stream_id (); @@ -1249,8 +1297,183 @@ gabble_file_transfer_channel_offer_file (GabbleFileTransferChannel *self, lm_message_unref (msg); g_free (stream_id); - g_free (full_jid); g_free (size_str); + g_free (full_jid); + + return result; +} + + +void +gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + GabbleFileTransferChannel *self, + GTalkFileCollectionState state, gboolean local_terminator) +{ + DEBUG ("gtalk ft state changed to %d", state); + switch (state) + { + case GTALK_FILE_COLLECTION_STATE_PENDING: + gabble_file_transfer_channel_set_state ( + TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self), + TP_FILE_TRANSFER_STATE_PENDING, + TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE); + break; + case GTALK_FILE_COLLECTION_STATE_ACCEPTED: + if (self->priv->state == TP_FILE_TRANSFER_STATE_PENDING) + { + gabble_file_transfer_channel_set_state ( + TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self), + TP_FILE_TRANSFER_STATE_ACCEPTED, + TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE); + } + break; + case GTALK_FILE_COLLECTION_STATE_OPEN: + channel_open (self); + break; + case GTALK_FILE_COLLECTION_STATE_TERMINATED: + if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED && + self->priv->state != TP_FILE_TRANSFER_STATE_CANCELLED) + { + gabble_file_transfer_channel_set_state ( + TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self), + TP_FILE_TRANSFER_STATE_CANCELLED, + local_terminator ? + TP_FILE_TRANSFER_STATE_CHANGE_REASON_LOCAL_STOPPED: + TP_FILE_TRANSFER_STATE_CHANGE_REASON_REMOTE_STOPPED); + } + close_session_and_transport (self); + break; + case GTALK_FILE_COLLECTION_STATE_ERROR: + case GTALK_FILE_COLLECTION_STATE_CONNECTION_FAILED: + gabble_file_transfer_channel_set_state ( + TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self), + TP_FILE_TRANSFER_STATE_CANCELLED, + TP_FILE_TRANSFER_STATE_CHANGE_REASON_LOCAL_ERROR); + + close_session_and_transport (self); + break; + case GTALK_FILE_COLLECTION_STATE_COMPLETED: + gabble_file_transfer_channel_set_state ( + TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self), + TP_FILE_TRANSFER_STATE_COMPLETED, + TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE); + + if (self->priv->transport && + gibber_transport_buffer_is_empty (self->priv->transport)) + gibber_transport_disconnect (self->priv->transport); + break; + } +} + +static gboolean +offer_gtalk_file_transfer (GabbleFileTransferChannel *self, + const gchar *full_jid, GError **error) +{ + + GTalkFileCollection *gtalk_file_collection; + + DEBUG ("Offering Gtalk file transfer to %s", full_jid); + + gtalk_file_collection = gtalk_file_collection_new (self, + self->priv->connection->jingle_factory, self->priv->handle, full_jid); + + g_return_val_if_fail (gtalk_file_collection != NULL, FALSE); + + set_gtalk_file_collection (self, gtalk_file_collection); + + gtalk_file_collection_initiate (self->priv->gtalk_file_collection, self); + + /* We would have gotten a set_gtalk_file_collection so we already hold an + additional reference to the object, so we can drop the reference we got + from the gtalk_file_collection_new. If we didn't get our + set_gtalk_file_collection called, then the ft manager doesn't handle us, + so it's best to just destroy it anyways */ + g_object_unref (gtalk_file_collection); + + return TRUE; +} + +gboolean +gabble_file_transfer_channel_offer_file (GabbleFileTransferChannel *self, + GError **error) +{ + GabblePresence *presence; + gboolean result; + TpHandleRepoIface *contact_repo, *room_repo; + const gchar *jid; + gboolean si = FALSE; + gboolean jingle_share = FALSE; + const gchar *si_resource = NULL; + const gchar *share_resource = NULL; + g_assert (!CHECK_STR_EMPTY (self->priv->filename)); + g_assert (self->priv->size != GABBLE_UNDEFINED_FILE_SIZE); + g_return_val_if_fail (self->priv->bytestream == NULL, FALSE); + g_return_val_if_fail (self->priv->gtalk_file_collection == NULL, FALSE); + + presence = gabble_presence_cache_get (self->priv->connection->presence_cache, + self->priv->handle); + + if (presence == NULL) + { + DEBUG ("can't find contact's presence"); + g_set_error (error, TP_ERRORS, TP_ERROR_OFFLINE, + "can't find contact's presence"); + + return FALSE; + } + + contact_repo = tp_base_connection_get_handles ( + (TpBaseConnection *) self->priv->connection, TP_HANDLE_TYPE_CONTACT); + room_repo = tp_base_connection_get_handles ( + (TpBaseConnection *) self->priv->connection, TP_HANDLE_TYPE_ROOM); + + jid = tp_handle_inspect (contact_repo, self->priv->handle); + if (gabble_get_room_handle_from_jid (room_repo, jid) == 0) + { + /* Not a MUC jid, need to get a resource */ + + /* FIXME: should we check for SI, bytestreams and/or IBB too? + * http://bugs.freedesktop.org/show_bug.cgi?id=23777 */ + si_resource = gabble_presence_pick_resource_by_caps (presence, + DEVICE_AGNOSTIC, gabble_capability_set_predicate_has, + NS_FILE_TRANSFER); + si = (si_resource != NULL); + + share_resource = gabble_presence_pick_resource_by_caps (presence, + DEVICE_AGNOSTIC, gabble_capability_set_predicate_has, + NS_GOOGLE_FEAT_SHARE); + jingle_share = (share_resource != NULL); + } + else + { + /* MUC jid, we already have the full jid */ + si = gabble_presence_has_cap (presence, NS_FILE_TRANSFER); + jingle_share = gabble_presence_has_cap (presence, NS_GOOGLE_FEAT_SHARE); + } + + /* Use bytestream if we have SI, but no jingle-share or if we have SI and + jingle-share but we have no google relay token */ + if (si && + (!jingle_share || + gabble_jingle_factory_get_google_relay_token ( + self->priv->connection->jingle_factory) == NULL)) + { + result = offer_bytestream (self, jid, si_resource, error); + } + else if (jingle_share) + { + gchar *full_jid = gabble_peer_to_jid (self->priv->connection, + self->priv->handle, share_resource); + result = offer_gtalk_file_transfer (self, full_jid, error); + g_free (full_jid); + } + else + { + DEBUG ("contact doesn't have file transfer capabilities"); + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_CAPABLE, + "contact doesn't have file transfer capabilities"); + result = FALSE; + } return result; } @@ -1339,20 +1562,14 @@ transferred_chunk (GabbleFileTransferChannel *self, emit_progress_update_cb, self); } - static void -data_received_cb (GabbleBytestreamIface *stream, - TpHandle sender, - GString *data, - gpointer user_data) +data_received_cb (GabbleFileTransferChannel *self, const guint8 *data, guint len) { - GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); GError *error = NULL; g_assert (self->priv->transport != NULL); - if (!gibber_transport_send (self->priv->transport, (const guint8 *) data->str, - data->len, &error)) + if (!gibber_transport_send (self->priv->transport, data, len, &error)) { DEBUG ("sending to transport failed: %s", error->message); g_error_free (error); @@ -1364,9 +1581,10 @@ data_received_cb (GabbleBytestreamIface *stream, return; } - transferred_chunk (self, (guint64) data->len); + transferred_chunk (self, (guint64) len); - if (self->priv->transferred_bytes + self->priv->initial_offset >= + if (self->priv->bytestream != NULL && + self->priv->transferred_bytes + self->priv->initial_offset >= self->priv->size) { DEBUG ("Received all the file. Transfer is complete"); @@ -1384,10 +1602,33 @@ data_received_cb (GabbleBytestreamIface *stream, if (!gibber_transport_buffer_is_empty (self->priv->transport)) { /* We don't want to send more data while the buffer isn't empty */ - gabble_bytestream_iface_block_reading (self->priv->bytestream, TRUE); + if (self->priv->bytestream != NULL) + gabble_bytestream_iface_block_reading (self->priv->bytestream, TRUE); + else if (self->priv->gtalk_file_collection != NULL) + gtalk_file_collection_block_reading (self->priv->gtalk_file_collection, + self, TRUE); } } + +void +gabble_file_transfer_channel_gtalk_file_collection_data_received ( + GabbleFileTransferChannel *self, const gchar *data, guint len) +{ + data_received_cb (self, (const guint8 *) data, len); +} + + +static void +bytestream_data_received_cb (GabbleBytestreamIface *stream, + TpHandle sender, + GString *data, + gpointer user_data) +{ + GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); + data_received_cb (self, (const guint8 *) data->str, data->len); +} + static void augment_si_reply (LmMessageNode *si, gpointer user_data) @@ -1487,17 +1728,31 @@ gabble_file_transfer_channel_accept_file (TpSvcChannelTypeFileTransfer *iface, self->priv->initial_offset = 0; } - g_assert (self->priv->bytestream != NULL); - gabble_signal_connect_weak (self->priv->bytestream, "data-received", - G_CALLBACK (data_received_cb), G_OBJECT (self)); + if (self->priv->bytestream != NULL) + { + gabble_signal_connect_weak (self->priv->bytestream, "data-received", + G_CALLBACK (bytestream_data_received_cb), G_OBJECT (self)); - /* Block the bytestream while the user is not connected to the socket */ - gabble_bytestream_iface_block_reading (self->priv->bytestream, TRUE); + /* Block the bytestream while the user is not connected to the socket */ + gabble_bytestream_iface_block_reading (self->priv->bytestream, TRUE); - /* channel state will change to open once the bytestream is open */ - gabble_bytestream_iface_accept (self->priv->bytestream, augment_si_reply, - self); + /* channel state will change to open once the bytestream is open */ + gabble_bytestream_iface_accept (self->priv->bytestream, augment_si_reply, + self); + } + else if (self->priv->gtalk_file_collection != NULL) + { + /* Block the gtalk ft stream while the user is not connected + to the socket */ + gtalk_file_collection_block_reading (self->priv->gtalk_file_collection, + self, TRUE); + gtalk_file_collection_accept (self->priv->gtalk_file_collection, self); + } + else + { + g_assert_not_reached (); + } } /** @@ -1565,7 +1820,7 @@ gabble_file_transfer_channel_provide_file ( return; } - if (self->priv->remote_accepted) + if (self->priv->channel_opened) { /* Remote already accepted the file. Channel is Open. * If not channel stay Pending. */ @@ -1633,12 +1888,25 @@ transport_handler (GibberTransport *transport, { GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); - if (!gabble_bytestream_iface_send (self->priv->bytestream, data->length, - (const gchar *) data->data)) + if (self->priv->bytestream != NULL) { - DEBUG ("Sending failed. Closing the bytestream"); - close_bytestream_and_transport (self); - return; + if (!gabble_bytestream_iface_send (self->priv->bytestream, data->length, + (const gchar *) data->data)) + { + DEBUG ("Sending failed. Closing the bytestream"); + close_session_and_transport (self); + return; + } + } + else if (self->priv->gtalk_file_collection != NULL) + { + if (!gtalk_file_collection_send_data (self->priv->gtalk_file_collection, + self, (const gchar *) data->data, data->length)) + { + DEBUG ("Sending failed. Closing the jingle session"); + close_session_and_transport (self); + return; + } } transferred_chunk (self, (guint64) data->length); @@ -1646,15 +1914,21 @@ transport_handler (GibberTransport *transport, if (self->priv->transferred_bytes + self->priv->initial_offset >= self->priv->size) { - DEBUG ("All the file has been sent. Closing the bytestream"); - - gabble_file_transfer_channel_set_state ( - TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self), - TP_FILE_TRANSFER_STATE_COMPLETED, - TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE); - - gabble_bytestream_iface_close (self->priv->bytestream, NULL); - return; + if (self->priv->bytestream != NULL) + { + DEBUG ("All the file has been sent. Closing the bytestream"); + gabble_file_transfer_channel_set_state ( + TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self), + TP_FILE_TRANSFER_STATE_COMPLETED, + TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE); + gabble_bytestream_iface_close (self->priv->bytestream, NULL); + } + else if (self->priv->gtalk_file_collection != NULL) + { + DEBUG ("All the file has been sent."); + gtalk_file_collection_completed (self->priv->gtalk_file_collection, + self); + } } } @@ -1663,44 +1937,71 @@ bytestream_write_blocked_cb (GabbleBytestreamIface *bytestream, gboolean blocked, GabbleFileTransferChannel *self) { - if (self->priv->transport) + if (self->priv->transport != NULL) gibber_transport_block_receiving (self->priv->transport, blocked); } +void +gabble_file_transfer_channel_gtalk_file_collection_write_blocked ( + GabbleFileTransferChannel *self, gboolean blocked) +{ + if (self->priv->transport != NULL) + gibber_transport_block_receiving (self->priv->transport, blocked); +} + + static void file_transfer_send (GabbleFileTransferChannel *self) { - gibber_transport_set_handler (self->priv->transport, transport_handler, self); - /* We shouldn't receive data if the bytestream isn't open otherwise it will - error out */ + /* We shouldn't receive data if the bytestream isn't open otherwise it + will error out */ if (self->priv->state == TP_FILE_TRANSFER_STATE_OPEN) gibber_transport_block_receiving (self->priv->transport, FALSE); else gibber_transport_block_receiving (self->priv->transport, TRUE); + + gibber_transport_set_handler (self->priv->transport, transport_handler, + self); } static void file_transfer_receive (GabbleFileTransferChannel *self) { /* Client is connected, we can now receive data. Unblock the bytestream */ - g_assert (self->priv->bytestream != NULL); - gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE); + if (self->priv->bytestream != NULL) + gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE); + else if (self->priv->gtalk_file_collection != NULL) + gtalk_file_collection_block_reading (self->priv->gtalk_file_collection, + self, FALSE); } static void transport_disconnected_cb (GibberTransport *transport, GabbleFileTransferChannel *self) { + TpBaseConnection *base_conn = (TpBaseConnection *) self->priv->connection; + gboolean requested = (self->priv->initiator == base_conn->self_handle); + DEBUG ("transport to local socket has been disconnected"); - if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED) + /* If we are sending the file, we can expect the transport to be closed as + soon as we received all the data. Otherwise, it should only get closed once + the channel has gone to state COMPLETED. + This allows to make sure we detect an error if the channel is closed while + receiving a gtalk-ft folder where the size is an approximation of the real + size to be received */ + if ((requested && + self->priv->transferred_bytes + self->priv->initial_offset < + self->priv->size) || + (!requested && self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED)) { - close_bytestream_and_transport (self); gabble_file_transfer_channel_set_state ( TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self), TP_FILE_TRANSFER_STATE_CANCELLED, TP_FILE_TRANSFER_STATE_CHANGE_REASON_LOCAL_ERROR); + + close_session_and_transport (self); } } @@ -1709,7 +2010,12 @@ transport_buffer_empty_cb (GibberTransport *transport, GabbleFileTransferChannel *self) { /* Buffer is empty so we can unblock the buffer if it was blocked */ - gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE); + if (self->priv->bytestream != NULL) + gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE); + + if (self->priv->gtalk_file_collection != NULL) + gtalk_file_collection_block_reading (self->priv->gtalk_file_collection, + self, FALSE); if (self->priv->state > TP_FILE_TRANSFER_STATE_OPEN) gibber_transport_disconnect (transport); @@ -1858,8 +2164,8 @@ setup_local_socket (GabbleFileTransferChannel *self, self->priv->socket_type = address_type; - g_signal_connect (self->priv->listener, "new-connection", - G_CALLBACK (new_connection_cb), self); + gabble_signal_connect_weak (self->priv->listener, "new-connection", + G_CALLBACK (new_connection_cb), G_OBJECT (self)); return TRUE; } @@ -1877,8 +2183,10 @@ gabble_file_transfer_channel_new (GabbleConnection *conn, const gchar *description, guint64 date, guint64 initial_offset, + gboolean resume_supported, GabbleBytestreamIface *bytestream, - gboolean resume_supported) + GTalkFileCollection *gtalk_file_collection, + const gchar *file_collection) { return g_object_new (GABBLE_TYPE_FILE_TRANSFER_CHANNEL, @@ -1894,7 +2202,9 @@ gabble_file_transfer_channel_new (GabbleConnection *conn, "description", description, "date", date, "initial-offset", initial_offset, - "bytestream", bytestream, "resume-supported", resume_supported, + "file-collection", file_collection, + "bytestream", bytestream, + "gtalk-file-collection", gtalk_file_collection, NULL); } diff --git a/src/ft-channel.h b/src/ft-channel.h index 2bd339d4b..9763f08ef 100644 --- a/src/ft-channel.h +++ b/src/ft-channel.h @@ -27,9 +27,14 @@ #include <extensions/_gen/interfaces.h> #include <extensions/_gen/enums.h> +typedef struct _GabbleFileTransferChannel GabbleFileTransferChannel; + +#include "gtalk-file-collection.h" + +#include "bytestream-factory.h" + G_BEGIN_DECLS -typedef struct _GabbleFileTransferChannel GabbleFileTransferChannel; typedef struct _GabbleFileTransferChannelClass GabbleFileTransferChannelClass; typedef struct _GabbleFileTransferChannelPrivate GabbleFileTransferChannelPrivate; @@ -68,11 +73,32 @@ gabble_file_transfer_channel_new (GabbleConnection *conn, const gchar *content_type, const gchar *filename, guint64 size, TpFileHashType content_hash_type, const gchar *content_hash, const gchar *description, guint64 date, guint64 initial_offset, - GabbleBytestreamIface *bytestream, gboolean resume_supported); + gboolean resume_supported, GabbleBytestreamIface *bytestream, + GTalkFileCollection *gtalk_fc, const gchar *file_collection); gboolean gabble_file_transfer_channel_offer_file ( GabbleFileTransferChannel *self, GError **error); +/* The following methods are a hack, they are 'signal-like' callbacks for the + GTalkFileCollection. They have to be made this way because the FileCollection + can't send out signals since it needs its signals to be sent to a specific + channel only. So instead it calls these callbacks directly on the channel it + needs to notify. This is a known layering violation and accepted as the lesser + of any other evil [hack]. */ +void gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + GabbleFileTransferChannel *self, GTalkFileCollectionState gtalk_fc_state, + gboolean local_terminator); + +void gabble_file_transfer_channel_gtalk_file_collection_write_blocked ( + GabbleFileTransferChannel *self, gboolean blocked); + +void gabble_file_transfer_channel_gtalk_file_collection_data_received ( + GabbleFileTransferChannel *self, const gchar *data, guint len); + +void +gabble_file_transfer_channel_do_close (GabbleFileTransferChannel *self); + + G_END_DECLS #endif /* #ifndef __GABBLE_FILE_TRANSFER_CHANNEL_H__*/ diff --git a/src/ft-manager.c b/src/ft-manager.c index e746cb5af..206c1c391 100644 --- a/src/ft-manager.c +++ b/src/ft-manager.c @@ -1,6 +1,6 @@ /* * ft-manager.c - Source for GabbleFtManager - * Copyright (C) 2009 Collabora Ltd. + * Copyright (C) 2009-2010 Collabora Ltd. * @author: Guillaume Desmottes <guillaume.desmottes@collabora.co.uk> * * This library is free software; you can redistribute it and/or @@ -28,6 +28,8 @@ #include <string.h> #include <glib/gstdio.h> +#include "jingle-session.h" +#include "jingle-share.h" #include "caps-channel-manager.h" #include "connection.h" #include "ft-manager.h" @@ -56,6 +58,7 @@ channel_manager_iface_init (gpointer, gpointer); static void gabble_ft_manager_channel_created (GabbleFtManager *mgr, GabbleFileTransferChannel *chan, gpointer request_token); + static void caps_channel_manager_iface_init (gpointer g_iface, gpointer iface_data); @@ -80,6 +83,7 @@ struct _GabbleFtManagerPrivate GList *channels; /* path of the temporary directory used to store UNIX sockets */ gchar *tmp_dir; + gulong status_changed_id; }; static void @@ -93,8 +97,13 @@ gabble_ft_manager_init (GabbleFtManager *obj) obj->priv->channels = NULL; } +static void gabble_ft_manager_constructed (GObject *object); static void gabble_ft_manager_dispose (GObject *object); static void gabble_ft_manager_finalize (GObject *object); +static void connection_status_changed_cb (GabbleConnection *conn, + guint status, + guint reason, + GabbleFtManager *self); static void gabble_ft_manager_get_property (GObject *object, @@ -143,6 +152,7 @@ gabble_ft_manager_class_init (GabbleFtManagerClass *gabble_ft_manager_class) g_type_class_add_private (gabble_ft_manager_class, sizeof (GabbleFtManagerPrivate)); + object_class->constructed = gabble_ft_manager_constructed; object_class->get_property = gabble_ft_manager_get_property; object_class->set_property = gabble_ft_manager_set_property; @@ -158,26 +168,46 @@ gabble_ft_manager_class_init (GabbleFtManagerClass *gabble_ft_manager_class) g_object_class_install_property (object_class, PROP_CONNECTION, param_spec); } + +static void +gabble_ft_manager_constructed (GObject *object) +{ + void (*chain_up) (GObject *) = + G_OBJECT_CLASS (gabble_ft_manager_parent_class)->constructed; + GabbleFtManager *self = GABBLE_FT_MANAGER (object); + + if (chain_up != NULL) + chain_up (object); + + self->priv->status_changed_id = g_signal_connect (self->priv->connection, + "status-changed", (GCallback) connection_status_changed_cb, object); +} + +static void +ft_manager_close_all (GabbleFtManager *self) +{ + GList *l; + + while ((l = self->priv->channels) != NULL) + { + gabble_file_transfer_channel_do_close (l->data); + /* Channels should have closed and disappeared from the list */ + g_assert (l != self->priv->channels); + } +} + + void gabble_ft_manager_dispose (GObject *object) { GabbleFtManager *self = GABBLE_FT_MANAGER (object); - GList *tmp, *l; if (self->priv->dispose_has_run) return; self->priv->dispose_has_run = TRUE; - tmp = self->priv->channels; - self->priv->channels = NULL; - - for (l = tmp; l != NULL; l = g_list_next (l)) - { - g_object_unref (l->data); - } - - g_list_free (tmp); + g_assert (self->priv->channels == NULL); if (G_OBJECT_CLASS (gabble_ft_manager_parent_class)->dispose) G_OBJECT_CLASS (gabble_ft_manager_parent_class)->dispose (object); @@ -257,6 +287,31 @@ file_channel_closed_cb (GabbleFileTransferChannel *chan, } static void +gabble_ft_manager_channels_created (GabbleFtManager *self, GList *channels) +{ + GList *i; + GHashTable *new_channels = g_hash_table_new_full (g_direct_hash, + g_direct_equal, NULL, NULL); + + for (i = channels; i ; i = i->next) + { + GabbleFileTransferChannel *chan = i->data; + + gabble_signal_connect_weak (chan, "closed", + G_CALLBACK (file_channel_closed_cb), G_OBJECT (self)); + + self->priv->channels = g_list_append (self->priv->channels, chan); + /* The channels can't satisfy a request because this will always be called + when we receive an incoming jingle-share session */ + g_hash_table_insert (new_channels, chan, NULL); + } + + tp_channel_manager_emit_new_channels (self, new_channels); + + g_hash_table_destroy (new_channels); +} + +static void gabble_ft_manager_channel_created (GabbleFtManager *self, GabbleFileTransferChannel *chan, gpointer request_token) @@ -277,6 +332,104 @@ gabble_ft_manager_channel_created (GabbleFtManager *self, g_slist_free (requests); } + +static void +new_jingle_session_cb (GabbleJingleFactory *jf, + GabbleJingleSession *sess, + gpointer data) +{ + GabbleFtManager *self = GABBLE_FT_MANAGER (data); + GTalkFileCollection *gtalk_fc = NULL; + GabbleJingleContent *content = NULL; + GabbleJingleShareManifest *manifest = NULL; + GList *channels = NULL; + GList *cs, *i; + + if (gabble_jingle_session_get_content_type (sess) == + GABBLE_TYPE_JINGLE_SHARE) + { + cs = gabble_jingle_session_get_contents (sess); + + if (cs != NULL) + { + content = GABBLE_JINGLE_CONTENT (cs->data); + g_list_free (cs); + } + + if (content == NULL) + return; + + gtalk_fc = gtalk_file_collection_new_from_session (jf, sess); + + if (gtalk_fc) + { + gchar *token = NULL; + + g_object_get (gtalk_fc, + "token", &token, + NULL); + + manifest = gabble_jingle_share_get_manifest ( + GABBLE_JINGLE_SHARE (content)); + for (i = manifest->entries; i; i = i->next) + { + GabbleJingleShareManifestEntry *entry = i->data; + GabbleFileTransferChannel *channel = NULL; + gchar *filename = NULL; + + filename = g_strdup_printf ("%s%s", + entry->name, entry->folder? ".tar":""); + channel = gabble_file_transfer_channel_new (self->priv->connection, + sess->peer, sess->peer, TP_FILE_TRANSFER_STATE_PENDING, + NULL, filename, entry->size, TP_FILE_HASH_TYPE_NONE, NULL, + NULL, 0, 0, FALSE, NULL, gtalk_fc, token); + g_free (filename); + + gtalk_file_collection_add_channel (gtalk_fc, channel); + channels = g_list_prepend (channels, channel); + } + + if (channels != NULL) + gabble_ft_manager_channels_created (self, channels); + + g_list_free (channels); + + /* Channels will hold the reference to the gtalk file collection, + so we can drop ours already. If no channels were created, + then we need to destroy it anyways */ + g_object_unref (gtalk_fc); + } + } +} + + +static void +connection_status_changed_cb (GabbleConnection *conn, + guint status, + guint reason, + GabbleFtManager *self) +{ + + switch (status) + { + case TP_CONNECTION_STATUS_CONNECTING: + g_signal_connect (self->priv->connection->jingle_factory, + "new-session", + G_CALLBACK (new_jingle_session_cb), self); + break; + + case TP_CONNECTION_STATUS_DISCONNECTED: + ft_manager_close_all (self); + if (self->priv->status_changed_id != 0) + { + g_signal_handler_disconnect (self->priv->connection, + self->priv->status_changed_id); + self->priv->status_changed_id = 0; + } + break; + } +} + static gboolean gabble_ft_manager_handle_request (TpChannelManager *manager, gpointer request_token, @@ -399,7 +552,7 @@ gabble_ft_manager_handle_request (TpChannelManager *manager, chan = gabble_file_transfer_channel_new (self->priv->connection, handle, base_connection->self_handle, TP_FILE_TRANSFER_STATE_PENDING, content_type, filename, size, content_hash_type, content_hash, - description, date, initial_offset, NULL, TRUE); + description, date, initial_offset, TRUE, NULL, NULL, NULL); if (!gabble_file_transfer_channel_offer_file (chan, &error)) { @@ -563,7 +716,7 @@ void gabble_ft_manager_handle_si_request (GabbleFtManager *self, chan = gabble_file_transfer_channel_new (self->priv->connection, handle, handle, TP_FILE_TRANSFER_STATE_PENDING, content_type, filename, size, content_hash_type, content_hash, - description, date, 0, bytestream, resume_supported); + description, date, 0, resume_supported, bytestream, NULL, NULL); gabble_ft_manager_channel_created (self, chan, NULL); } @@ -653,7 +806,8 @@ gabble_ft_manager_get_contact_caps ( const GabbleCapabilitySet *caps, GPtrArray *arr) { - if (gabble_capability_set_has (caps, NS_FILE_TRANSFER)) + if (gabble_capability_set_has (caps, NS_FILE_TRANSFER) || + gabble_capability_set_has (caps, NS_GOOGLE_FEAT_SHARE)) add_file_transfer_channel_class (arr); } @@ -683,6 +837,7 @@ gabble_ft_manager_represent_client ( DEBUG ("client %s supports file transfer", client_name); gabble_capability_set_add (cap_set, NS_FILE_TRANSFER); + gabble_capability_set_add (cap_set, NS_GOOGLE_FEAT_SHARE); /* there's no point in looking at the subsequent filters if we've * already added the FT capability */ break; diff --git a/src/gtalk-file-collection.c b/src/gtalk-file-collection.c new file mode 100644 index 000000000..4275b23dd --- /dev/null +++ b/src/gtalk-file-collection.c @@ -0,0 +1,1786 @@ +/* + * gtalk-file-collection.c - Source for GTalkFileCollection + * + * Copyright (C) 2010 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#include "gtalk-file-collection.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <glib.h> + +#define DEBUG_FLAG GABBLE_DEBUG_SHARE + +#include "debug.h" +#include "jingle-factory.h" +#include "jingle-session.h" +#include "jingle-share.h" +#include "namespaces.h" +#include "util.h" + +#include <nice/agent.h> + +/* + * This GTalk compatible file transfer protocol is a bit complicated, so here + * is an explanation on how it works : + * + * A pseudo-good initial source of information is available here : + * http://code.google.com/apis/talk/libjingle/file_share.html + * + * The current object interaction is like this : + * + * GabbleFileTransferChannelManager + * | + * | + * | + * | + * * ref + * GabbleFileTransferChannel + * weakref * * + * / \ + * / \ + * / \ + * / \ + * / \ + * / \ + * / \ + * ref / \ + * GTalkFileCollection \ + * | \ + * | \ + * ref | \ + * GabbleJingleSession \ + * | \ (one at a time) + * | \ + * ref | \ + * GabbleJingleShare ------------------*- ShareChannel -------- NiceAgent + * | | + * | | + * ref | ref | + * GabbleTransportGoogle ----------------*- JingleCandidate PseudoTCP + * + * The protocol works like this : + * Once you receive an invitation, the manifest will contain a number of files + * and folders. Some files might have image attributes (width/height) that help + * specify that they are images. + * If there are images in the invitation, a new ShareChannel gets created and + * connectivity must be established. Then on that stream, an HTTP GET request + * is sent for each image being transfered with the URL as : + * GET <preview-path>/filename?width=X&height=Y + * where X and Y are the thumbnail's requested width and height. + * The peer should at this point scale down the image to the requested width and + * height and send the thumbnail for showing the preview of the image in the FT + * UI. + * Once the invitation is accepted, a new ShareChannel is created, which will + * cause a new NiceAgent to be created and connectivity to be established on that + * ShareChannel. The resulting stream is then used as an HTTP server/client to + * request files on it using the <source-path> as prefix to the URL. + * Simple files are being transferred normally, while directories will be + * transferred as a tarball with 'chuncked' Transfer-Encoding since the resulting + * size of the tarball isn't known in advance. + * Once a file is completely transferred, then the next file is requested on the + * same ShareChannel. If all files are transferred, then the <complete> info + * action is being sent through the jingle signaling, and the session can then + * be terminated safely. + * + * Since telepathy doesn't currently support image previews, so the 'preview' + * ShareChannel is never created with gabble. + * Also note that we only create one ShareChannel and we serialize the file + * transfers one after the other, they do not each get one ShareChannel and they + * cannot be downloaded in parallel. + * + */ + +G_DEFINE_TYPE (GTalkFileCollection, gtalk_file_collection, G_TYPE_OBJECT); + +/* properties */ +enum +{ + PROP_TOKEN = 1, + LAST_PROPERTY +}; + +typedef enum + { + HTTP_SERVER_IDLE, + HTTP_SERVER_HEADERS, + HTTP_SERVER_SEND, + HTTP_CLIENT_IDLE, + HTTP_CLIENT_RECEIVE, + HTTP_CLIENT_HEADERS, + HTTP_CLIENT_CHUNK_SIZE, + HTTP_CLIENT_CHUNK_END, + HTTP_CLIENT_CHUNK_FINAL, + HTTP_CLIENT_BODY, + } HttpStatus; + + +typedef struct +{ + NiceAgent *agent; + guint stream_id; + guint component_id; + gboolean agent_attached; + GabbleJingleShare *content; + guint share_channel_id; + HttpStatus http_status; + gchar *status_line; + gboolean is_chunked; + guint64 content_length; + gchar *write_buffer; + guint write_len; + gchar *read_buffer; + guint read_len; +} ShareChannel; + + +typedef enum +{ + GTALK_FT_STATUS_PENDING, + GTALK_FT_STATUS_INITIATED, + GTALK_FT_STATUS_ACCEPTED, + GTALK_FT_STATUS_TRANSFERRING, + GTALK_FT_STATUS_WAITING, + GTALK_FT_STATUS_TERMINATED +} GtalkFtStatus; + +struct _GTalkFileCollectionPrivate +{ + gboolean dispose_has_run; + + GtalkFtStatus status; + /* GList of weakreffed GabbleFileTransferChannel */ + GList *channels; + /* GHashTable of GabbleFileTransferChannel => GINT_TO_POINTER (gboolean) */ + /* the weakref to the channel here is held through the GList *channels */ + GHashTable *channels_reading; + /* GHashTable of GabbleFileTransferChannel => GINT_TO_POINTER (gboolean) */ + /* the weakref to the channel here is held through the GList *channels */ + GHashTable *channels_usable; + GabbleFileTransferChannel *current_channel; + GabbleJingleFactory *jingle_factory; + GabbleJingleSession *jingle; + /* ICE component id to jingle share channel association + GINT_TO_POINTER (candidate->component) => g_slice_new (ShareChannel) */ + GHashTable *share_channels; + gboolean requested; + gchar *token; +}; + +static void free_share_channel (gpointer data); +static void nice_data_received_cb (NiceAgent *agent, + guint stream_id, guint component_id, guint len, gchar *buffer, + gpointer user_data); +static void set_current_channel (GTalkFileCollection *self, + GabbleFileTransferChannel *channel); +static void channel_disposed (gpointer data, GObject *where_the_object_was); + +static void +gtalk_file_collection_init (GTalkFileCollection *self) +{ + GTalkFileCollectionPrivate *priv = + G_TYPE_INSTANCE_GET_PRIVATE (self, GTALK_TYPE_FILE_COLLECTION, + GTalkFileCollectionPrivate); + gchar buf[16]; + guint32 *uint_buf = (guint32 *) buf; + guint i; + + + DEBUG ("GTalk file collection init called"); + self->priv = priv; + + self->priv->status = GTALK_FT_STATUS_PENDING; + + self->priv->channels_reading = g_hash_table_new_full (NULL, NULL, NULL, NULL); + self->priv->channels_usable = g_hash_table_new_full (NULL, NULL, NULL, NULL); + + self->priv->share_channels = g_hash_table_new_full (NULL, NULL, + NULL, free_share_channel); + + for (i = 0; i < sizeof (buf); i++) + buf[i] = g_random_int_range (0, 256); + + self->priv->token = g_strdup_printf ("%x%x%x%x", + uint_buf[0], uint_buf[1], uint_buf[2], uint_buf[3]); + + /* FIXME: we should start creating a nice agent already and have it start + the candidate gathering.. but we don't know which jingle-share transport + channel name to assign it to... */ + + priv->dispose_has_run = FALSE; +} + + +static void +gtalk_file_collection_dispose (GObject *object) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (object); + GList *i; + + if (self->priv->dispose_has_run) + return; + + DEBUG ("dispose called"); + self->priv->dispose_has_run = TRUE; + + if (self->priv->jingle != NULL) + { + gabble_jingle_session_terminate (self->priv->jingle, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL); + + /* the terminate could synchronously unref it and set it to NULL */ + if (self->priv->jingle != NULL) + { + g_object_unref (self->priv->jingle); + self->priv->jingle = NULL; + } + } + + set_current_channel (self, NULL); + + if (self->priv->channels_reading != NULL) + { + g_hash_table_destroy (self->priv->channels_reading); + self->priv->channels_reading = NULL; + } + + if (self->priv->channels_usable != NULL) + { + g_hash_table_destroy (self->priv->channels_usable); + self->priv->channels_usable = NULL; + } + + if (self->priv->share_channels != NULL) + { + g_hash_table_destroy (self->priv->share_channels); + self->priv->share_channels = NULL; + } + + for (i = self->priv->channels; i; i = i->next) + { + GabbleFileTransferChannel *channel = i->data; + g_object_weak_unref (G_OBJECT (channel), channel_disposed, self); + } + g_list_free (self->priv->channels); + + g_free (self->priv->token); + + if (G_OBJECT_CLASS (gtalk_file_collection_parent_class)->dispose) + G_OBJECT_CLASS (gtalk_file_collection_parent_class)->dispose (object); +} + +static void +gtalk_file_collection_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (object); + + switch (property_id) + { + case PROP_TOKEN: + g_value_set_string (value, self->priv->token); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +gtalk_file_collection_class_init (GTalkFileCollectionClass *cls) +{ + GObjectClass *object_class = G_OBJECT_CLASS (cls); + + g_type_class_add_private (cls, sizeof (GTalkFileCollectionPrivate)); + + object_class->get_property = gtalk_file_collection_get_property; + object_class->dispose = gtalk_file_collection_dispose; + + g_object_class_install_property (object_class, PROP_TOKEN, + g_param_spec_string ( + "token", + "Unique token identifiying the FileCollection", + "Token identifying a collection of files", + "", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); +} + + +static ShareChannel * +get_share_channel (GTalkFileCollection *self, NiceAgent *agent) +{ + GHashTableIter iter; + gpointer key, value; + ShareChannel *ret = NULL; + + g_hash_table_iter_init (&iter, self->priv->share_channels); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ShareChannel *share_channel = (ShareChannel *) value; + if (share_channel->agent == agent) + { + ret = share_channel; + break; + } + } + + return ret; +} + + +static GabbleFileTransferChannel * +get_channel_by_filename (GTalkFileCollection *self, gchar *filename) +{ + GList *i; + + for (i = self->priv->channels; i; i = i->next) + { + GabbleFileTransferChannel *channel = i->data; + gchar *file = NULL; + + g_object_get (channel, + "filename", &file, + NULL); + + if (strcmp (file, filename) == 0) + return channel; + } + + return NULL; +} + +static void +set_current_channel (GTalkFileCollection *self, + GabbleFileTransferChannel *channel) +{ + self->priv->current_channel = channel; + + if (channel != NULL) + { + gboolean reading = FALSE; + + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_OPEN, FALSE); + reading = GPOINTER_TO_INT (g_hash_table_lookup ( + self->priv->channels_reading, channel)); + gtalk_file_collection_block_reading (self, channel, !reading); + } +} + +static gboolean +channel_exists (GTalkFileCollection * self, GabbleFileTransferChannel *channel) +{ + GList *i; + + for (i = self->priv->channels; i; i = i->next) + { + if (channel == i->data) + return TRUE; + } + + return FALSE; +} + +static void +add_channel (GTalkFileCollection * self, GabbleFileTransferChannel *channel) +{ + self->priv->channels = g_list_append (self->priv->channels, channel); + g_hash_table_replace (self->priv->channels_reading, channel, + GINT_TO_POINTER (FALSE)); + g_object_weak_ref (G_OBJECT (channel), channel_disposed, self); +} + +static void +del_channel (GTalkFileCollection * self, GabbleFileTransferChannel *channel) +{ + g_return_if_fail (channel_exists (self, channel)); + + self->priv->channels = g_list_remove (self->priv->channels, channel); + g_hash_table_remove (self->priv->channels_reading, channel); + g_hash_table_remove (self->priv->channels_usable, channel); + g_object_weak_unref (G_OBJECT (channel), channel_disposed, self); + if (self->priv->current_channel == channel) + set_current_channel (self, NULL); +} + +static void +jingle_session_state_changed_cb (GabbleJingleSession *session, + GParamSpec *arg1, + GTalkFileCollection *self) +{ + JingleSessionState state; + GList *i; + + DEBUG ("called"); + + g_object_get (session, + "state", &state, + NULL); + + switch (state) + { + case JS_STATE_INVALID: + case JS_STATE_PENDING_CREATED: + break; + case JS_STATE_PENDING_INITIATE_SENT: + case JS_STATE_PENDING_INITIATED: + for (i = self->priv->channels; i;) + { + GabbleFileTransferChannel *channel = i->data; + + i = i->next; + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_PENDING, FALSE); + } + break; + case JS_STATE_PENDING_ACCEPT_SENT: + case JS_STATE_ACTIVE: + /* Do not set the channels to OPEN unless we're ready to send/receive + data from them */ + if (self->priv->status == GTALK_FT_STATUS_INITIATED) + self->priv->status = GTALK_FT_STATUS_ACCEPTED; + for (i = self->priv->channels; i;) + { + GabbleFileTransferChannel *channel = i->data; + gboolean usable; + + i = i->next; + + usable = GPOINTER_TO_INT (g_hash_table_lookup ( + self->priv->channels_usable, channel)); + if (usable) + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_ACCEPTED, FALSE); + } + break; + case JS_STATE_ENDED: + /* Do nothing, let the terminated signal set the correct state + depending on the termination reason */ + default: + break; + } +} + +static void +jingle_session_terminated_cb (GabbleJingleSession *session, + gboolean local_terminator, + TpChannelGroupChangeReason reason, + const gchar *text, + gpointer user_data) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data); + GList *i; + + g_assert (session == self->priv->jingle); + + self->priv->status = GTALK_FT_STATUS_TERMINATED; + + for (i = self->priv->channels; i;) + { + GabbleFileTransferChannel *channel = i->data; + + i = i->next; + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_TERMINATED, local_terminator); + } +} + +static void +content_new_remote_candidates_cb (GabbleJingleContent *content, + GList *clist, gpointer user_data) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data); + GList *li; + + DEBUG ("Got new remote candidates : %d", g_list_length (clist)); + + for (li = clist; li; li = li->next) + { + JingleCandidate *candidate = li->data; + NiceCandidate *cand = NULL; + ShareChannel *share_channel = NULL; + GSList *candidates = NULL; + + if (candidate->protocol != JINGLE_TRANSPORT_PROTOCOL_UDP) + { + DEBUG ("Ignoring candidate %s because of non-UDP protocol : %d", + candidate->username, candidate->protocol); + continue; + } + + share_channel = g_hash_table_lookup (self->priv->share_channels, + GINT_TO_POINTER (candidate->component)); + if (share_channel == NULL) + { + DEBUG ("Ignoring candidate %s because of unknown component id %d", + candidate->id, candidate->component); + continue; + } + + cand = nice_candidate_new ( + candidate->type == JINGLE_CANDIDATE_TYPE_LOCAL? + NICE_CANDIDATE_TYPE_HOST: + candidate->type == JINGLE_CANDIDATE_TYPE_STUN? + NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE: + NICE_CANDIDATE_TYPE_RELAYED); + + + cand->transport = JINGLE_TRANSPORT_PROTOCOL_UDP; + nice_address_init (&cand->addr); + nice_address_set_from_string (&cand->addr, candidate->address); + nice_address_set_port (&cand->addr, candidate->port); + cand->priority = candidate->preference * 1000; + cand->stream_id = share_channel->stream_id; + cand->component_id = share_channel->component_id; + /* + if (c->id == NULL) + candidate_id = g_strdup_printf ("R%d", ++priv->remote_candidate_count); + else + candidate_id = c->id;*/ + if (candidate->id != NULL) + strncpy (cand->foundation, candidate->id, + NICE_CANDIDATE_MAX_FOUNDATION - 1); + else if (candidate->username != NULL) + strncpy (cand->foundation, candidate->username, + NICE_CANDIDATE_MAX_FOUNDATION - 1); + + cand->username = g_strdup (candidate->username?candidate->username:""); + cand->password = g_strdup (candidate->password?candidate->password:""); + + candidates = g_slist_append (candidates, cand); + nice_agent_set_remote_candidates (share_channel->agent, + share_channel->stream_id, share_channel->component_id, candidates); + g_slist_foreach (candidates, (GFunc)nice_candidate_free, NULL); + g_slist_free (candidates); + } +} + +static void +nice_candidate_gathering_done (NiceAgent *agent, guint stream_id, + gpointer user_data) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data); + ShareChannel *share_channel = get_share_channel (self, agent); + GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (share_channel->content); + GList *candidates = NULL; + GList *remote_candidates = NULL; + GSList *local_candidates; + GSList *li; + + DEBUG ("libnice candidate gathering done!!!!"); + + /* Send remote candidates to libnice and listen to new signal */ + remote_candidates = gabble_jingle_content_get_remote_candidates (content); + content_new_remote_candidates_cb (content, remote_candidates, self); + + gabble_signal_connect_weak (content, "new-candidates", + (GCallback) content_new_remote_candidates_cb, G_OBJECT (self)); + + /* Send gathered local candidates to the content */ + local_candidates = nice_agent_get_local_candidates (agent, stream_id, + share_channel->component_id); + + for (li = local_candidates; li; li = li->next) + { + NiceCandidate *cand = li->data; + JingleCandidate *candidate; + gchar ip[NICE_ADDRESS_STRING_LEN]; + + nice_address_to_string (&cand->addr, ip); + + candidate = jingle_candidate_new ( + /* protocol */ + cand->transport == NICE_CANDIDATE_TRANSPORT_UDP? + JINGLE_TRANSPORT_PROTOCOL_UDP: + JINGLE_TRANSPORT_PROTOCOL_TCP, + /* candidate type */ + cand->type == NICE_CANDIDATE_TYPE_HOST? + JINGLE_CANDIDATE_TYPE_LOCAL: + cand->type == NICE_CANDIDATE_TYPE_RELAYED? + JINGLE_CANDIDATE_TYPE_RELAY: + JINGLE_CANDIDATE_TYPE_STUN, + /* id */ + cand->foundation, + /* component */ + share_channel->share_channel_id, + /* address */ + ip, + /* port */ + nice_address_get_port (&cand->addr), + /* generation */ + 0, + /* preference */ + (gfloat) cand->priority / 1000.0, + /* username */ + cand->username?cand->username:"", + /* password */ + cand->password?cand->password:"", + /* network */ + 0); + + candidates = g_list_prepend (candidates, candidate); + } + + gabble_jingle_content_add_candidates (content, candidates); +} + +static void +nice_component_state_changed (NiceAgent *agent, guint stream_id, + guint component_id, guint state, gpointer user_data) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data); + ShareChannel *share_channel = get_share_channel (self, agent); + GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (share_channel->content); + JingleTransportState ts = JINGLE_TRANSPORT_STATE_DISCONNECTED; + + DEBUG ("libnice component state changed %d!!!!", state); + + switch (state) + { + case NICE_COMPONENT_STATE_DISCONNECTED: + case NICE_COMPONENT_STATE_GATHERING: + ts = JINGLE_TRANSPORT_STATE_DISCONNECTED; + break; + case NICE_COMPONENT_STATE_CONNECTING: + ts = JINGLE_TRANSPORT_STATE_CONNECTING; + break; + case NICE_COMPONENT_STATE_CONNECTED: + case NICE_COMPONENT_STATE_READY: + ts = JINGLE_TRANSPORT_STATE_CONNECTED; + break; + case NICE_COMPONENT_STATE_FAILED: + { + GList *i; + + for (i = self->priv->channels; i;) + { + GabbleFileTransferChannel *channel = i->data; + + i = i->next; + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_CONNECTION_FAILED, + TRUE); + } + /* return because we don't want to use the content after it + has been destroyed.. */ + return; + } + } + gabble_jingle_content_set_transport_state (content, ts); +} + +static void +get_next_manifest_entry (GTalkFileCollection *self, + ShareChannel *share_channel, gboolean error) +{ + GabbleJingleShareManifest *manifest = NULL; + GabbleJingleShareManifestEntry *entry = NULL; + GabbleFileTransferChannel *channel = NULL; + GList *i; + + DEBUG ("called"); + + if (self->priv->current_channel != NULL) + { + if (g_list_length (self->priv->channels) == 1) + { + GabbleJingleContent *content = \ + GABBLE_JINGLE_CONTENT (share_channel->content); + + DEBUG ("Received all the files. Transfer is complete"); + gabble_jingle_content_send_complete (content); + } + + g_hash_table_replace (self->priv->channels_usable, + self->priv->current_channel, GINT_TO_POINTER (FALSE)); + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + self->priv->current_channel, + error ? GTALK_FILE_COLLECTION_STATE_ERROR: + GTALK_FILE_COLLECTION_STATE_COMPLETED, FALSE); + + set_current_channel (self, NULL); + } + + manifest = gabble_jingle_share_get_manifest (share_channel->content); + for (i = manifest->entries; i; i = i->next) + { + gchar *filename = NULL; + gboolean usable; + + entry = i->data; + + filename = g_strdup_printf ("%s%s", entry->name, + (entry->folder ? ".tar" : "")); + channel = get_channel_by_filename (self, filename); + g_free (filename); + if (channel != NULL) + { + usable = GPOINTER_TO_INT (g_hash_table_lookup ( + self->priv->channels_usable, channel)); + if (usable) + break; + } + entry = NULL; + } + + self->priv->status = GTALK_FT_STATUS_WAITING; + + + if (entry != NULL) + { + gchar *buffer = NULL; + gchar *source_url = manifest->source_url; + guint url_len = (source_url != NULL? strlen (source_url) : 0); + gchar *separator = ""; + gchar *filename = NULL; + + if (source_url != NULL && source_url[url_len -1] != '/') + separator = "/"; + + self->priv->status = GTALK_FT_STATUS_TRANSFERRING; + + filename = g_uri_escape_string (entry->name, NULL, TRUE); + + /* The session initiator will always be the full JID of the peer */ + buffer = g_strdup_printf ("GET %s%s%s HTTP/1.1\r\n" + "Connection: Keep-Alive\r\n" + "Content-Length: 0\r\n" + "Host: %s:0\r\n" /* e.g. alice@example.com/Empathy:0 */ + "User-Agent: %s\r\n\r\n", + (source_url != NULL ? source_url : ""), + separator, filename, + gabble_jingle_session_get_initiator (self->priv->jingle), + PACKAGE_STRING); + g_free (filename); + + /* FIXME: check for success */ + nice_agent_send (share_channel->agent, share_channel->stream_id, + share_channel->component_id, strlen (buffer), buffer); + g_free (buffer); + + share_channel->http_status = HTTP_CLIENT_RECEIVE; + /* Block or unblock accordingly */ + set_current_channel (self, channel); + } +} + +static void +nice_component_writable (NiceAgent *agent, guint stream_id, guint component_id, + gpointer user_data) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data); + ShareChannel *share_channel = get_share_channel (self, agent); + + if (share_channel->http_status == HTTP_CLIENT_IDLE) + { + get_next_manifest_entry (self, share_channel, FALSE); + } + else if (share_channel->http_status == HTTP_SERVER_SEND) + { + if (self->priv->current_channel == NULL) + { + GList *i; + + DEBUG ("Unexpected current_channel == NULL!"); + for (i = self->priv->channels; i;) + { + GabbleFileTransferChannel *channel = i->data; + + i = i->next; + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_ERROR, FALSE); + } + return; + } + gabble_file_transfer_channel_gtalk_file_collection_write_blocked ( + self->priv->current_channel, FALSE); + if (share_channel->write_buffer != NULL) + { + gint ret = nice_agent_send (agent, stream_id, component_id, + share_channel->write_len, share_channel->write_buffer); + + if (ret < 0 || (guint) ret < share_channel->write_len) + { + gchar *to_free = share_channel->write_buffer; + + if (ret < 0) + ret = 0; + + share_channel->write_buffer = g_memdup ( + share_channel->write_buffer + ret, + share_channel->write_len - ret); + share_channel->write_len = share_channel->write_len - ret; + g_free (to_free); + + gabble_file_transfer_channel_gtalk_file_collection_write_blocked ( + self->priv->current_channel, TRUE); + } + else + { + g_free (share_channel->write_buffer); + share_channel->write_buffer = NULL; + share_channel->write_len = 0; + } + } + } + +} + +typedef struct +{ + union { + gpointer ptr; + GTalkFileCollection *self; + } u; + ShareChannel *share_channel; +} GoogleRelaySessionData; + +static void +set_relay_info (gpointer item, gpointer user_data) +{ + GoogleRelaySessionData *data = user_data; + GHashTable *relay = item; + const gchar *server_ip = NULL; + const gchar *username = NULL; + const gchar *password = NULL; + const gchar *type_str = NULL; + guint server_port; + NiceRelayType type; + GValue *value; + + value = g_hash_table_lookup (relay, "ip"); + if (value != NULL) + server_ip = g_value_get_string (value); + else + return; + + value = g_hash_table_lookup (relay, "port"); + if (value != NULL) + server_port = g_value_get_uint (value); + else + return; + + value = g_hash_table_lookup (relay, "username"); + if (value != NULL) + username = g_value_get_string (value); + else + return; + + value = g_hash_table_lookup (relay, "password"); + if (value != NULL) + password = g_value_get_string (value); + else + return; + + value = g_hash_table_lookup (relay, "type"); + if (value != NULL) + type_str = g_value_get_string (value); + else + return; + + if (!strcmp (type_str, "udp")) + type = NICE_RELAY_TYPE_TURN_UDP; + else if (!strcmp (type_str, "tcp")) + type = NICE_RELAY_TYPE_TURN_TCP; + else if (!strcmp (type_str, "tls")) + type = NICE_RELAY_TYPE_TURN_TLS; + else + return; + + nice_agent_set_relay_info (data->share_channel->agent, + data->share_channel->stream_id, data->share_channel->component_id, + server_ip, server_port, + username, password, type); + +} + +static void +google_relay_session_cb (GPtrArray *relays, gpointer user_data) +{ + GoogleRelaySessionData *data = user_data; + + if (data->u.self == NULL) + { + DEBUG ("Received relay session callback but self got destroyed"); + g_slice_free (GoogleRelaySessionData, data); + return; + } + + if (relays != NULL) + g_ptr_array_foreach (relays, set_relay_info, user_data); + + nice_agent_gather_candidates (data->share_channel->agent, + data->share_channel->stream_id); + + g_object_remove_weak_pointer (G_OBJECT (data->u.self), &data->u.ptr); + g_slice_free (GoogleRelaySessionData, data); +} + + +static void +content_new_share_channel_cb (GabbleJingleContent *content, const gchar *name, + guint share_channel_id, gpointer user_data) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data); + ShareChannel *share_channel = g_slice_new0 (ShareChannel); + NiceAgent *agent = nice_agent_new_reliable (g_main_context_default (), + NICE_COMPATIBILITY_GOOGLE); + guint stream_id = nice_agent_add_stream (agent, 1); + gchar *stun_server; + guint stun_port; + GoogleRelaySessionData *relay_data = NULL; + + DEBUG ("New Share channel %s was created and linked to id %d", name, + share_channel_id); + + share_channel->agent = agent; + share_channel->stream_id = stream_id; + share_channel->component_id = NICE_COMPONENT_TYPE_RTP; + share_channel->content = GABBLE_JINGLE_SHARE (content); + share_channel->share_channel_id = share_channel_id; + + if (self->priv->requested) + share_channel->http_status = HTTP_SERVER_IDLE; + else + share_channel->http_status = HTTP_CLIENT_IDLE; + + gabble_signal_connect_weak (agent, "candidate-gathering-done", + G_CALLBACK (nice_candidate_gathering_done), G_OBJECT (self)); + + gabble_signal_connect_weak (agent, "component-state-changed", + G_CALLBACK (nice_component_state_changed), G_OBJECT (self)); + + gabble_signal_connect_weak (agent, "reliable-transport-writable", + G_CALLBACK (nice_component_writable), G_OBJECT (self)); + + + /* Add the agent to the hash table before gathering candidates in case the + gathering finishes synchronously, and the callback tries to add local + candidates to the content, it needs to find the share channel id.. */ + g_hash_table_insert (self->priv->share_channels, + GINT_TO_POINTER (share_channel_id), share_channel); + + share_channel->agent_attached = TRUE; + nice_agent_attach_recv (agent, stream_id, share_channel->component_id, + g_main_context_default (), nice_data_received_cb, self); + + if (gabble_jingle_factory_get_stun_server ( + self->priv->jingle_factory, &stun_server, &stun_port)) + { + g_object_set (agent, + "stun-server", stun_server, + "stun-server-port", stun_port, + NULL); + g_free (stun_server); + } + + relay_data = g_slice_new0 (GoogleRelaySessionData); + relay_data->u.self = self; + relay_data->share_channel = share_channel; + g_object_add_weak_pointer (G_OBJECT (relay_data->u.self), + &relay_data->u.ptr); + gabble_jingle_factory_create_google_relay_session ( + self->priv->jingle_factory, 1, + google_relay_session_cb, relay_data); +} + +static void +content_completed (GabbleJingleContent *content, gpointer user_data) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data); + GList *i; + + DEBUG ("Received content completed"); + + for (i = self->priv->channels; i;) + { + GabbleFileTransferChannel *channel = i->data; + + i = i->next; + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_COMPLETED, FALSE); + } +} + +static void +free_share_channel (gpointer data) +{ + ShareChannel *share_channel = (ShareChannel *) data; + + DEBUG ("Freeing jingle Share channel"); + + if (share_channel->write_buffer != NULL) + { + g_free (share_channel->write_buffer); + share_channel->write_buffer = NULL; + } + if (share_channel->read_buffer != NULL) + { + g_free (share_channel->read_buffer); + share_channel->read_buffer = NULL; + } + g_object_unref (share_channel->agent); + g_slice_free (ShareChannel, share_channel); +} + + +/* If buffer contains a line ending, 0-terminate the first line and + * return a pointer to the beginning of the next line. Otherwise + * return NULL. */ +static gchar * +http_read_line (gchar *buffer, guint len) +{ + gchar *p = memchr (buffer, '\n', len); + + if (p != NULL) + { + *p = 0; + if (p > buffer && *(p-1) == '\r') + *(p-1) = '\0'; + p++; + } + + return p; +} + +static guint +http_data_received (GTalkFileCollection *self, ShareChannel *share_channel, + gchar *buffer, guint len) +{ + + switch (share_channel->http_status) + { + case HTTP_SERVER_IDLE: + { + gchar *headers = http_read_line (buffer, len); + + if (headers == NULL) + return 0; + + share_channel->http_status = HTTP_SERVER_HEADERS; + share_channel->status_line = g_strdup (buffer); + + if (self->priv->current_channel != NULL) + { + DEBUG ("Received status line with current channel set"); + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + self->priv->current_channel, + GTALK_FILE_COLLECTION_STATE_COMPLETED, FALSE); + set_current_channel (self, NULL); + } + + return headers - buffer; + } + break; + case HTTP_SERVER_HEADERS: + { + gchar *line = buffer; + gchar *next_line = http_read_line (buffer, len); + + if (next_line == NULL) + return 0; + + DEBUG ("Found server headers line (%" G_GSIZE_FORMAT ") : %s", + strlen (line), line); + /* FIXME: how about content-length and an actual body ? */ + if (line[0] == '\0') + { + gchar *response = NULL; + gchar *get_line = NULL; + GabbleJingleShareManifest *manifest = NULL; + gchar *source_url = NULL; + guint url_len; + gchar *separator = ""; + gchar *filename = NULL; + GabbleFileTransferChannel *channel = NULL; + + g_assert (self->priv->current_channel == NULL); + + DEBUG ("Found empty line, received request : %s ", + share_channel->status_line); + + manifest = gabble_jingle_share_get_manifest ( + share_channel->content); + source_url = manifest->source_url; + url_len = (source_url != NULL? strlen (source_url) : 0); + if (source_url != NULL && source_url[url_len -1] != '/') + separator = "/"; + + get_line = g_strdup_printf ("GET %s%s%%s HTTP/1.1", + (source_url != NULL ? source_url : ""), + separator); + filename = g_malloc (strlen (share_channel->status_line)); + + if (sscanf (share_channel->status_line, get_line, filename) == 1) + { + gchar *unescaped = g_uri_unescape_string (filename, NULL); + + g_free (filename); + filename = unescaped; + channel = get_channel_by_filename (self, filename); + } + + if (channel != NULL) + { + guint64 size; + + g_object_get (channel, + "size", &size, + NULL); + + DEBUG ("Found valid filename, result : 200"); + + share_channel->http_status = HTTP_SERVER_SEND; + response = g_strdup_printf ("HTTP/1.1 200\r\n" + "Connection: Keep-Alive\r\n" + "Content-Length: %" G_GUINT64_FORMAT "\r\n" + "Content-Type: application/octet-stream\r\n\r\n", + size); + + } + else + { + DEBUG ("Unable to find valid filename (%s), result : 404", + (filename != NULL? filename : "")); + + share_channel->http_status = HTTP_SERVER_IDLE; + response = g_strdup_printf ("HTTP/1.1 404\r\n" + "Connection: Keep-Alive\r\n" + "Content-Length: 0\r\n\r\n"); + } + + /* FIXME: check for success of nice_agent_send */ + nice_agent_send (share_channel->agent, share_channel->stream_id, + share_channel->component_id, strlen (response), response); + + g_free (response); + g_free (filename); + g_free (get_line); + + /* Now that we sent our response, we can assign the current + channel which sets it to OPEN (if non NULL) so data can + start flowing */ + self->priv->status = GTALK_FT_STATUS_TRANSFERRING; + set_current_channel (self, channel); + } + + return next_line - buffer; + } + break; + case HTTP_SERVER_SEND: + DEBUG ("received data when we're supposed to be sending data.. " + "not supposed to happen"); + break; + case HTTP_CLIENT_IDLE: + DEBUG ("received data when we're supposed to be sending the GET.. " + "not supposed to happen"); + break; + case HTTP_CLIENT_RECEIVE: + { + gchar *headers = http_read_line (buffer, len); + + if (headers == NULL) + return 0; + + share_channel->http_status = HTTP_CLIENT_HEADERS; + share_channel->status_line = g_strdup (buffer); + + return headers - buffer; + } + case HTTP_CLIENT_HEADERS: + { + gchar *line = buffer; + gchar *next_line = http_read_line (buffer, len); + + if (next_line == NULL) + return 0; + + DEBUG ("Found client headers line (%" G_GSIZE_FORMAT ") : %s", + strlen (line), line); + if (line[0] == '\0') + { + DEBUG ("Found empty line, GET response : %s", + share_channel->status_line); + + if (g_str_has_prefix (share_channel->status_line, + "HTTP/1.1 200")) + { + if (share_channel->is_chunked) + { + share_channel->http_status = HTTP_CLIENT_CHUNK_SIZE; + } + else + { + share_channel->http_status = HTTP_CLIENT_BODY; + if (share_channel->content_length == 0) + get_next_manifest_entry (self, share_channel, FALSE); + } + } + else + { + /* We expect content-length to be 0 and no chunks for + non-200 statuses (404 error) */ + if (share_channel->is_chunked || + share_channel->content_length != 0) + { + GList *i; + + DEBUG ("Unexpected body for non-200 error!"); + for (i = self->priv->channels; i;) + { + GabbleFileTransferChannel *channel = i->data; + + i = i->next; + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_ERROR, FALSE); + } + } + else + { + get_next_manifest_entry (self, share_channel, TRUE); + } + } + } + else if (!g_ascii_strncasecmp (line, "Content-Length: ", 16)) + { + share_channel->is_chunked = FALSE; + /* Check strtoull read all the length */ + share_channel->content_length = g_ascii_strtoull (line + 16, + NULL, 10); + DEBUG ("Found data length : %" G_GUINT64_FORMAT, + share_channel->content_length); + } + else if (!g_ascii_strcasecmp (line, + "Transfer-Encoding: chunked")) + { + share_channel->is_chunked = TRUE; + share_channel->content_length = 0; + DEBUG ("Found file is chunked"); + } + + return next_line - buffer; + } + break; + case HTTP_CLIENT_CHUNK_SIZE: + { + gchar *line = buffer; + gchar *next_line = http_read_line (buffer, len); + + if (next_line == NULL) + return 0; + + /* FIXME : check validity of strtoul */ + share_channel->content_length = strtoul (line, NULL, 16); + if (share_channel->content_length > 0) + share_channel->http_status = HTTP_CLIENT_BODY; + else + share_channel->http_status = HTTP_CLIENT_CHUNK_FINAL; + + + return next_line - buffer; + } + break; + case HTTP_CLIENT_BODY: + { + guint consumed = 0; + + if (len >= share_channel->content_length) + { + if (self->priv->current_channel == NULL) + { + GList *i; + + DEBUG ("Unexpected current_channel == NULL!"); + for (i = self->priv->channels; i;) + { + GabbleFileTransferChannel *channel = i->data; + + i = i->next; + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_ERROR, FALSE); + } + /* FIXME: Who knows what might happen here if we got destroyed + It shouldn't crash since our object isn't dereferences + anymore, but.. */ + return len; + } + consumed = share_channel->content_length; + gabble_file_transfer_channel_gtalk_file_collection_data_received ( + self->priv->current_channel, buffer, consumed); + share_channel->content_length = 0; + if (share_channel->is_chunked) + share_channel->http_status = HTTP_CLIENT_CHUNK_END; + else + get_next_manifest_entry (self, share_channel, FALSE); + } + else + { + consumed = len; + share_channel->content_length -= len; + gabble_file_transfer_channel_gtalk_file_collection_data_received ( + self->priv->current_channel, buffer, consumed); + } + + return consumed; + } + break; + case HTTP_CLIENT_CHUNK_END: + { + gchar *chunk = http_read_line (buffer, len); + + if (chunk == NULL) + return 0; + + share_channel->http_status = HTTP_CLIENT_CHUNK_SIZE; + + return chunk - buffer; + } + break; + case HTTP_CLIENT_CHUNK_FINAL: + { + gchar *end = http_read_line (buffer, len); + + if (end == NULL) + return 0; + + share_channel->http_status = HTTP_CLIENT_IDLE; + get_next_manifest_entry (self, share_channel, FALSE); + + return end - buffer; + } + break; + } + + return 0; +} + +static void +nice_data_received_cb (NiceAgent *agent, + guint stream_id, + guint component_id, + guint len, + gchar *buffer, + gpointer user_data) +{ + GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data); + ShareChannel *share_channel = get_share_channel (self, agent); + gchar *free_buffer = NULL; + + if (share_channel->read_buffer != NULL) + { + gchar *tmp = g_malloc (share_channel->read_len + len); + + memcpy (tmp, share_channel->read_buffer, share_channel->read_len); + memcpy (tmp + share_channel->read_len, buffer, len); + + free_buffer = buffer = tmp; + len += share_channel->read_len; + + g_free (share_channel->read_buffer); + share_channel->read_buffer = NULL; + share_channel->read_len = 0; + } + while (len > 0) + { + guint consumed = http_data_received (self, share_channel, buffer, len); + + if (consumed == 0) + { + share_channel->read_buffer = g_memdup (buffer, len); + share_channel->read_len = len; + break; + } + else + { + /* we assume http_data_received never returns consumed > len */ + g_assert (consumed <= len); + + len -= consumed; + buffer += consumed; + } + } + + if (free_buffer != NULL) + g_free (free_buffer); + +} + +static void +set_session (GTalkFileCollection * self, + GabbleJingleSession *session, GabbleJingleContent *content) +{ + self->priv->jingle = g_object_ref (session); + + gabble_signal_connect_weak (session, "notify::state", + (GCallback) jingle_session_state_changed_cb, G_OBJECT (self)); + gabble_signal_connect_weak (session, "terminated", + (GCallback) jingle_session_terminated_cb, G_OBJECT (self)); + + gabble_signal_connect_weak (content, "new-share-channel", + (GCallback) content_new_share_channel_cb, G_OBJECT (self)); + gabble_signal_connect_weak (content, "completed", + (GCallback) content_completed, G_OBJECT (self)); + + self->priv->status = GTALK_FT_STATUS_PENDING; +} + +GTalkFileCollection * +gtalk_file_collection_new (GabbleFileTransferChannel *channel, + GabbleJingleFactory *jingle_factory, TpHandle handle, const gchar *jid) +{ + GTalkFileCollection * self = g_object_new (GTALK_TYPE_FILE_COLLECTION, NULL); + GabbleJingleSession *session = NULL; + GabbleJingleContent *content = NULL; + gchar *filename; + guint64 size; + + self->priv->jingle_factory = jingle_factory; + self->priv->requested = TRUE; + + session = gabble_jingle_factory_create_session (jingle_factory, + handle, jid, FALSE); + + if (session == NULL) + { + g_object_unref (self); + return NULL; + } + + g_object_set (session, + "dialect", JINGLE_DIALECT_GTALK4, + NULL); + + content = gabble_jingle_session_add_content (session, + JINGLE_MEDIA_TYPE_NONE, "share", NS_GOOGLE_SESSION_SHARE, + NS_GOOGLE_TRANSPORT_P2P); + + if (content == NULL) + { + g_object_unref (self); + g_object_unref (session); + return NULL; + } + + g_object_get (channel, + "filename", &filename, + "size", &size, + NULL); + g_object_set (content, + "filename", filename, + "filesize", size, + NULL); + + set_session (self, session, content); + + add_channel (self, channel); + + + return self; +} + +GTalkFileCollection * +gtalk_file_collection_new_from_session (GabbleJingleFactory *jingle_factory, + GabbleJingleSession *session) +{ + GTalkFileCollection * self = NULL; + GabbleJingleContent *content = NULL; + GList *cs; + + if (gabble_jingle_session_get_content_type (session) != + GABBLE_TYPE_JINGLE_SHARE) + return NULL; + + cs = gabble_jingle_session_get_contents (session); + + if (cs != NULL) + { + content = GABBLE_JINGLE_CONTENT (cs->data); + g_list_free (cs); + } + + if (content == NULL) + return NULL; + + self = g_object_new (GTALK_TYPE_FILE_COLLECTION, NULL); + + self->priv->jingle_factory = jingle_factory; + self->priv->requested = FALSE; + + set_session (self, session, content); + + return self; +} + +void +gtalk_file_collection_add_channel (GTalkFileCollection *self, + GabbleFileTransferChannel *channel) +{ + add_channel (self, channel); +} + +void +gtalk_file_collection_initiate (GTalkFileCollection *self, + GabbleFileTransferChannel * channel) +{ + if (channel_exists (self, channel)) + { + g_hash_table_replace (self->priv->channels_reading, channel, + GINT_TO_POINTER (TRUE)); + g_hash_table_replace (self->priv->channels_usable, channel, + GINT_TO_POINTER (TRUE)); + } + + if (self->priv->status == GTALK_FT_STATUS_PENDING) + { + gabble_jingle_session_accept (self->priv->jingle); + self->priv->status = GTALK_FT_STATUS_INITIATED; + } + else + { + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_ACCEPTED, FALSE); + } + +} + +void +gtalk_file_collection_accept (GTalkFileCollection *self, + GabbleFileTransferChannel * channel) +{ + GList *cs = gabble_jingle_session_get_contents (self->priv->jingle); + + DEBUG ("called"); + + if (channel_exists (self, channel)) + { + g_hash_table_replace (self->priv->channels_usable, channel, + GINT_TO_POINTER (TRUE)); + } + + if (self->priv->status == GTALK_FT_STATUS_PENDING) + { + if (cs != NULL) + { + GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (cs->data); + guint initial_id = 0; + guint share_channel_id; + + gabble_jingle_session_accept (self->priv->jingle); + self->priv->status = GTALK_FT_STATUS_ACCEPTED; + + /* The new-share-channel signal will take care of the rest.. */ + do + { + gchar *share_channel_name = NULL; + + share_channel_name = g_strdup_printf ("gabble-%d", ++initial_id); + share_channel_id = gabble_jingle_content_create_share_channel ( + content, share_channel_name); + g_free (share_channel_name); + } while (share_channel_id == 0 && initial_id < 10); + + /* FIXME: not assert but actually cancel the FT? */ + g_assert (share_channel_id > 0); + g_list_free (cs); + } + + } + else + { + gabble_file_transfer_channel_gtalk_file_collection_state_changed ( + channel, GTALK_FILE_COLLECTION_STATE_ACCEPTED, FALSE); + } + + if (self->priv->status == GTALK_FT_STATUS_WAITING) + { + /* FIXME: this and other lookups should not check for channel '1' */ + ShareChannel *share_channel = g_hash_table_lookup ( + self->priv->share_channels, GINT_TO_POINTER (1)); + + get_next_manifest_entry (self, share_channel, FALSE); + } +} + +gboolean +gtalk_file_collection_send_data (GTalkFileCollection *self, + GabbleFileTransferChannel *channel, const gchar *data, guint length) +{ + + ShareChannel *share_channel = g_hash_table_lookup (self->priv->share_channels, + GINT_TO_POINTER (1)); + gint ret; + + + g_return_val_if_fail (self->priv->current_channel == channel, FALSE); + + ret = nice_agent_send (share_channel->agent, share_channel->stream_id, + share_channel->component_id, length, data); + + if (ret < 0 || (guint) ret < length) + { + if (ret < 0) + ret = 0; + + share_channel->write_buffer = g_memdup (data + ret, + length - ret); + share_channel->write_len = length - ret; + + gabble_file_transfer_channel_gtalk_file_collection_write_blocked (channel, + TRUE); + } + return TRUE; +} + +void +gtalk_file_collection_block_reading (GTalkFileCollection *self, + GabbleFileTransferChannel *channel, gboolean block) +{ + ShareChannel *share_channel = g_hash_table_lookup (self->priv->share_channels, + GINT_TO_POINTER (1)); + + g_assert (channel_exists (self, channel)); + + if (self->priv->status != GTALK_FT_STATUS_TRANSFERRING) + DEBUG ("Channel %p %s reading ", channel, block?"blocks":"unblocks" ); + + g_hash_table_replace (self->priv->channels_reading, channel, + GINT_TO_POINTER (!block)); + + if (channel == self->priv->current_channel) + { + if (block) + { + if (share_channel && share_channel->agent_attached) + { + nice_agent_attach_recv (share_channel->agent, + share_channel->stream_id, share_channel->component_id, + NULL, NULL, NULL); + share_channel->agent_attached = FALSE; + } + } + else + { + if (share_channel && !share_channel->agent_attached) + { + share_channel->agent_attached = TRUE; + nice_agent_attach_recv (share_channel->agent, + share_channel->stream_id, share_channel->component_id, + g_main_context_default (), nice_data_received_cb, self); + } + } + } +} + +void +gtalk_file_collection_completed (GTalkFileCollection *self, + GabbleFileTransferChannel * channel) +{ + ShareChannel *share_channel = g_hash_table_lookup (self->priv->share_channels, + GINT_TO_POINTER (1)); + + DEBUG ("called"); + + g_return_if_fail (self->priv->current_channel == channel); + + /* We shouldn't set the FT to completed until we receive the 'complete' info + or we receive a new HTTP request otherwise we might terminate the session + and cause a race condition where the peer thinks it got canceled before it + completed. */ + share_channel->http_status = HTTP_SERVER_IDLE; + self->priv->status = GTALK_FT_STATUS_WAITING; +} + +void +gtalk_file_collection_terminate (GTalkFileCollection *self, + GabbleFileTransferChannel * channel) +{ + + DEBUG ("called"); + + if (!channel_exists (self, channel)) + return; + + if (self->priv->current_channel == channel) + { + + del_channel (self, channel); + + /* Cancel the whole thing if we terminate the current channel */ + if (self->priv->status == GTALK_FT_STATUS_TRANSFERRING) + { + + /* The terminate should call our terminated_cb callback which should + terminate all channels which should unref us which will unref the + jingle session */ + self->priv->status = GTALK_FT_STATUS_TERMINATED; + gabble_jingle_session_terminate (self->priv->jingle, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL); + return; + } + return; + } + else + { + del_channel (self, channel); + + /* If this was the last channel, it will cause it to unref us and + the dispose will be called, which will call + gabble_jingle_session_terminate */ + gabble_file_transfer_channel_gtalk_file_collection_state_changed (channel, + GTALK_FILE_COLLECTION_STATE_TERMINATED, TRUE); + } +} + + +static void +channel_disposed (gpointer data, GObject *object) +{ + GTalkFileCollection *self = data; + GabbleFileTransferChannel *channel = (GabbleFileTransferChannel *) object; + + DEBUG ("channel %p got destroyed", channel); + + g_return_if_fail (channel_exists (self, channel)); + + if (self->priv->current_channel == channel) + { + del_channel (self, channel); + + /* Cancel the whole thing if we terminate the current channel */ + if (self->priv->status == GTALK_FT_STATUS_TRANSFERRING) + { + + /* The terminate should call our terminated_cb callback which should + terminate all channels which should unref us which will unref the + jingle session */ + self->priv->status = GTALK_FT_STATUS_TERMINATED; + gabble_jingle_session_terminate (self->priv->jingle, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL); + return; + } + } + else + { + del_channel (self, channel); + } +} diff --git a/src/gtalk-file-collection.h b/src/gtalk-file-collection.h new file mode 100644 index 000000000..cd0e61594 --- /dev/null +++ b/src/gtalk-file-collection.h @@ -0,0 +1,100 @@ +/* + * gtalk-file-collection.h - Header for GTalkFileCollection + * Copyright (C) 2010 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __GTALK_FILE_COLLECTION_H__ +#define __GTALK_FILE_COLLECTION_H__ + +#include <glib-object.h> +#include "jingle-session.h" +#include "connection.h" + +typedef struct _GTalkFileCollection GTalkFileCollection; + +typedef enum { + GTALK_FILE_COLLECTION_STATE_PENDING, + GTALK_FILE_COLLECTION_STATE_ACCEPTED, + GTALK_FILE_COLLECTION_STATE_OPEN, + GTALK_FILE_COLLECTION_STATE_TERMINATED, + GTALK_FILE_COLLECTION_STATE_CONNECTION_FAILED, + GTALK_FILE_COLLECTION_STATE_ERROR, + GTALK_FILE_COLLECTION_STATE_COMPLETED +} GTalkFileCollectionState; + +#include "ft-channel.h" + +G_BEGIN_DECLS + +typedef struct _GTalkFileCollectionClass GTalkFileCollectionClass; + +GType gtalk_file_collection_get_type (void); + +/* TYPE MACROS */ +#define GTALK_TYPE_FILE_COLLECTION \ + (gtalk_file_collection_get_type ()) +#define GTALK_FILE_COLLECTION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), GTALK_TYPE_FILE_COLLECTION, \ + GTalkFileCollection)) +#define GTALK_FILE_COLLECTION_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), GTALK_TYPE_FILE_COLLECTION, \ + GTalkFileCollectionClass)) +#define GTALK_IS_FILE_COLLECTION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTALK_TYPE_FILE_COLLECTION)) +#define GTALK_IS_FILE_COLLECTION_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), GTALK_TYPE_FILE_COLLECTION)) +#define GTALK_FILE_COLLECTION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), GTALK_TYPE_FILE_COLLECTION, \ + GTalkFileCollectionClass)) + +struct _GTalkFileCollectionClass { + GObjectClass parent_class; +}; + +typedef struct _GTalkFileCollectionPrivate GTalkFileCollectionPrivate; + +struct _GTalkFileCollection { + GObject parent; + GTalkFileCollectionPrivate *priv; +}; + +GTalkFileCollection *gtalk_file_collection_new ( + GabbleFileTransferChannel *channel, GabbleJingleFactory *jingle_factory, + TpHandle handle, const gchar *jid); + +GTalkFileCollection *gtalk_file_collection_new_from_session ( + GabbleJingleFactory *jingle_factory, GabbleJingleSession *session); + +void gtalk_file_collection_add_channel (GTalkFileCollection *self, + GabbleFileTransferChannel *channel); + +void gtalk_file_collection_initiate (GTalkFileCollection *self, + GabbleFileTransferChannel *channel); +void gtalk_file_collection_accept (GTalkFileCollection *self, + GabbleFileTransferChannel *channel); +void gtalk_file_collection_terminate (GTalkFileCollection *self, + GabbleFileTransferChannel *channel); +void gtalk_file_collection_completed (GTalkFileCollection *self, + GabbleFileTransferChannel *channel); +void gtalk_file_collection_block_reading (GTalkFileCollection *self, + GabbleFileTransferChannel *channel, gboolean block); +gboolean gtalk_file_collection_send_data (GTalkFileCollection *self, + GabbleFileTransferChannel *channel, const gchar *data, guint length); + + +#endif /* __GTALK_FILE_COLLECTION_H__ */ + diff --git a/src/jingle-content.c b/src/jingle-content.c index f43a112f1..8219dcc97 100644 --- a/src/jingle-content.c +++ b/src/jingle-content.c @@ -33,8 +33,11 @@ #include "jingle-factory.h" #include "jingle-session.h" #include "jingle-transport-iface.h" +#include "jingle-transport-google.h" +#include "jingle-media-rtp.h" #include "namespaces.h" #include "util.h" +#include "gabble-signals-marshal.h" /* signal enum */ enum @@ -42,6 +45,8 @@ enum READY, NEW_CANDIDATES, REMOVED, + NEW_SHARE_CHANNEL, + COMPLETED, LAST_SIGNAL }; @@ -83,6 +88,7 @@ struct _GabbleJingleContentPrivate gboolean have_local_candidates; guint gtalk4_event_id; + guint last_share_channel_component_id; gboolean dispose_has_run; }; @@ -96,6 +102,7 @@ G_DEFINE_TYPE(GabbleJingleContent, gabble_jingle_content, G_TYPE_OBJECT); static void new_transport_candidates_cb (GabbleJingleTransportIface *trans, GList *candidates, GabbleJingleContent *content); static void _maybe_ready (GabbleJingleContent *self); +static void transport_created (GabbleJingleContent *c); static void gabble_jingle_content_init (GabbleJingleContent *obj) @@ -237,6 +244,8 @@ gabble_jingle_content_set_property (GObject *object, g_signal_connect (priv->transport, "new-candidates", (GCallback) new_transport_candidates_cb, self); + + transport_created (self); } break; case PROP_NAME: @@ -261,6 +270,13 @@ gabble_jingle_content_set_property (GObject *object, } } +static JingleContentSenders +get_default_senders_real (GabbleJingleContent *c) +{ + return JINGLE_CONTENT_SENDERS_BOTH; +} + + static void gabble_jingle_content_class_init (GabbleJingleContentClass *cls) { @@ -273,6 +289,8 @@ gabble_jingle_content_class_init (GabbleJingleContentClass *cls) object_class->set_property = gabble_jingle_content_set_property; object_class->dispose = gabble_jingle_content_dispose; + cls->get_default_senders = get_default_senders_real; + /* property definitions */ param_spec = g_param_spec_object ("connection", "GabbleConnection object", "Gabble connection object used for exchanging messages.", @@ -346,6 +364,27 @@ gabble_jingle_content_class_init (GabbleJingleContentClass *cls) NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); + signals[NEW_SHARE_CHANNEL] = g_signal_new ( + "new-share-channel", + G_TYPE_FROM_CLASS (cls), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + gabble_marshal_VOID__STRING_UINT, + G_TYPE_NONE, + 2, + G_TYPE_STRING, G_TYPE_UINT); + + signals[COMPLETED] = g_signal_new ( + "completed", + G_TYPE_FROM_CLASS (cls), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + /* This signal serves as notification that the GabbleJingleContent is now * meaningless; everything holding a reference should drop it after receiving * 'removed'. @@ -359,6 +398,18 @@ gabble_jingle_content_class_init (GabbleJingleContentClass *cls) G_TYPE_NONE, 0); } + +static JingleContentSenders +get_default_senders (GabbleJingleContent *c) +{ + JingleContentSenders (*virtual_method)(GabbleJingleContent *) = \ + GABBLE_JINGLE_CONTENT_GET_CLASS (c)->get_default_senders; + + g_assert (virtual_method != NULL); + return virtual_method (c); +} + + static JingleContentSenders parse_senders (const gchar *txt) { @@ -406,6 +457,17 @@ new_transport_candidates_cb (GabbleJingleTransportIface *trans, } static void +transport_created (GabbleJingleContent *c) +{ + void (*virtual_method)(GabbleJingleContent *, GabbleJingleTransportIface *) = \ + GABBLE_JINGLE_CONTENT_GET_CLASS (c)->transport_created; + + if (virtual_method != NULL) + virtual_method (c, c->priv->transport); +} + + +static void parse_description (GabbleJingleContent *c, LmMessageNode *desc_node, GError **error) { @@ -453,9 +515,6 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c, g_assert (priv->transport_ns == NULL); - if (senders == NULL) - senders = "both"; - if (google_mode) { if (creator == NULL) @@ -519,7 +578,11 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c, } priv->created_by_us = FALSE; - priv->senders = parse_senders (senders); + if (senders == NULL) + priv->senders = get_default_senders (c); + else + priv->senders = parse_senders (senders); + if (priv->senders == JINGLE_CONTENT_SENDERS_NONE) { SET_BAD_REQ ("invalid content senders"); @@ -561,6 +624,7 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c, g_assert (priv->transport == NULL); priv->transport = trans; + transport_created (c); g_assert (priv->creator == NULL); priv->creator = g_strdup (creator); @@ -577,6 +641,100 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c, return; } +static guint +new_share_channel (GabbleJingleContent *c, const gchar *name) +{ + GabbleJingleContentPrivate *priv = c->priv; + GabbleJingleTransportGoogle *gtrans = NULL; + + if (priv->transport && + GABBLE_IS_JINGLE_TRANSPORT_GOOGLE (priv->transport)) + { + guint id = priv->last_share_channel_component_id + 1; + + gtrans = GABBLE_JINGLE_TRANSPORT_GOOGLE (priv->transport); + + if (!jingle_transport_google_set_component_name (gtrans, name, id)) + return 0; + + priv->last_share_channel_component_id++; + + DEBUG ("New Share channel '%s' with id : %d", name, id); + + g_signal_emit (c, signals[NEW_SHARE_CHANNEL], 0, name, id); + + return priv->last_share_channel_component_id; + } + return 0; +} + +guint +gabble_jingle_content_create_share_channel (GabbleJingleContent *self, + const gchar *name) +{ + GabbleJingleContentPrivate *priv = self->priv; + LmMessageNode *sess_node, *channel_node; + LmMessage *msg = NULL; + + /* Send the info action before creating the channel, in case candidates need + to be sent on the signal emit. It doesn't matter if the channel already + exists anyways... */ + msg = gabble_jingle_session_new_message (self->session, + JINGLE_ACTION_INFO, &sess_node); + + DEBUG ("Sending 'info' message to peer : channel %s", name); + channel_node = lm_message_node_add_child (sess_node, "channel", NULL); + lm_message_node_set_attribute (channel_node, "xmlns", priv->content_ns); + lm_message_node_set_attribute (channel_node, "name", name); + + gabble_jingle_session_send (self->session, msg, NULL, NULL); + + return new_share_channel (self, name); +} + +void +gabble_jingle_content_send_complete (GabbleJingleContent *self) +{ + GabbleJingleContentPrivate *priv = self->priv; + LmMessageNode *sess_node, *complete_node; + LmMessage *msg = NULL; + + msg = gabble_jingle_session_new_message (self->session, + JINGLE_ACTION_INFO, &sess_node); + + DEBUG ("Sending 'info' message to peer : complete"); + complete_node = lm_message_node_add_child (sess_node, "complete", NULL); + lm_message_node_set_attribute (complete_node, "xmlns", priv->content_ns); + + gabble_jingle_session_send (self->session, msg, NULL, NULL); + +} + +void +gabble_jingle_content_parse_info (GabbleJingleContent *c, + LmMessageNode *content_node, GError **error) +{ + LmMessageNode *channel_node; + LmMessageNode *complete_node; + + channel_node = lm_message_node_get_child_any_ns (content_node, "channel"); + complete_node = lm_message_node_get_child_any_ns (content_node, "complete"); + + DEBUG ("parsing info message : %p - %p", channel_node, complete_node); + if (channel_node) + { + const gchar *name; + name = lm_message_node_get_attribute (channel_node, "name"); + if (name != NULL) + new_share_channel (c, name); + } + else if (complete_node) + { + g_signal_emit (c, signals[COMPLETED], 0); + } + +} + void gabble_jingle_content_parse_accept (GabbleJingleContent *c, LmMessageNode *content_node, gboolean google_mode, GError **error) @@ -591,7 +749,8 @@ gabble_jingle_content_parse_accept (GabbleJingleContent *c, trans_node = lm_message_node_get_child_any_ns (content_node, "transport"); senders = lm_message_node_get_attribute (content_node, "senders"); - if (JINGLE_IS_GOOGLE_DIALECT (dialect) && trans_node == NULL) + if (GABBLE_IS_JINGLE_MEDIA_RTP (c) && + JINGLE_IS_GOOGLE_DIALECT (dialect) && trans_node == NULL) { DEBUG ("no transport node, assuming GTalk3 dialect"); /* gtalk lj0.3 assumes google-p2p transport */ @@ -599,9 +758,10 @@ gabble_jingle_content_parse_accept (GabbleJingleContent *c, } if (senders == NULL) - senders = "both"; + newsenders = get_default_senders (c); + else + newsenders = parse_senders (senders); - newsenders = parse_senders (senders); if (newsenders == JINGLE_CONTENT_SENDERS_NONE) { SET_BAD_REQ ("invalid content senders"); @@ -610,7 +770,8 @@ gabble_jingle_content_parse_accept (GabbleJingleContent *c, if (newsenders != priv->senders) { - DEBUG ("changing senders from %s to %s", produce_senders (priv->senders), senders); + DEBUG ("changing senders from %s to %s", produce_senders (priv->senders), + produce_senders (newsenders)); priv->senders = newsenders; g_object_notify ((GObject *) c, "senders"); } @@ -774,17 +935,17 @@ gabble_jingle_content_is_ready (GabbleJingleContent *self) { /* If it's created by us, media ready, not signalled, and we have * at least one local candidate, it's ready to be added. */ - if (priv->media_ready && priv->have_local_candidates && - (priv->state == JINGLE_CONTENT_STATE_EMPTY)) + if (priv->media_ready && priv->state == JINGLE_CONTENT_STATE_EMPTY && + (!GABBLE_IS_JINGLE_MEDIA_RTP (self) || priv->have_local_candidates)) return TRUE; } else { /* If it's created by peer, media and transports ready, * and not acknowledged yet, it's ready for acceptance. */ - if (priv->media_ready && - gabble_jingle_transport_iface_can_accept (priv->transport) && - (priv->state == JINGLE_CONTENT_STATE_NEW)) + if (priv->media_ready && priv->state == JINGLE_CONTENT_STATE_NEW && + (!GABBLE_IS_JINGLE_MEDIA_RTP (self) || + gabble_jingle_transport_iface_can_accept (priv->transport))) return TRUE; } diff --git a/src/jingle-content.h b/src/jingle-content.h index 2efe47a06..4f04d551b 100644 --- a/src/jingle-content.h +++ b/src/jingle-content.h @@ -31,7 +31,7 @@ G_BEGIN_DECLS typedef enum { JINGLE_MEDIA_TYPE_NONE = 0, JINGLE_MEDIA_TYPE_AUDIO, - JINGLE_MEDIA_TYPE_VIDEO + JINGLE_MEDIA_TYPE_VIDEO, } JingleMediaType; typedef enum { @@ -85,6 +85,9 @@ struct _GabbleJingleContentClass { void (*parse_description) (GabbleJingleContent *, LmMessageNode *, GError **); void (*produce_description) (GabbleJingleContent *, LmMessageNode *); + void (*transport_created) (GabbleJingleContent *, + GabbleJingleTransportIface *); + JingleContentSenders (*get_default_senders) (GabbleJingleContent *); }; typedef struct _GabbleJingleContentPrivate GabbleJingleContentPrivate; @@ -109,10 +112,14 @@ void gabble_jingle_content_produce_node (GabbleJingleContent *c, void gabble_jingle_content_parse_accept (GabbleJingleContent *c, LmMessageNode *content_node, gboolean google_mode, GError **error); +void gabble_jingle_content_parse_info (GabbleJingleContent *c, + LmMessageNode *content_node, GError **error); void gabble_jingle_content_parse_transport_info (GabbleJingleContent *self, LmMessageNode *trans_node, GError **error); void gabble_jingle_content_parse_description_info (GabbleJingleContent *self, LmMessageNode *trans_node, GError **error); +guint gabble_jingle_content_create_share_channel (GabbleJingleContent *self, + const gchar *name); void gabble_jingle_content_add_candidates (GabbleJingleContent *self, GList *li); void _gabble_jingle_content_set_media_ready (GabbleJingleContent *self); gboolean gabble_jingle_content_is_ready (GabbleJingleContent *self); @@ -144,5 +151,7 @@ gboolean gabble_jingle_content_receiving (GabbleJingleContent *self); void gabble_jingle_content_set_sending (GabbleJingleContent *self, gboolean send); +void gabble_jingle_content_send_complete (GabbleJingleContent *self); + #endif /* __JINGLE_CONTENT_H__ */ diff --git a/src/jingle-factory.c b/src/jingle-factory.c index 8128e4c22..0ad4d6b0a 100644 --- a/src/jingle-factory.c +++ b/src/jingle-factory.c @@ -32,6 +32,7 @@ #include "connection.h" #include "debug.h" +#include "jingle-share.h" #include "jingle-media-rtp.h" #include "jingle-session.h" #include "jingle-transport-google.h" @@ -560,6 +561,7 @@ gabble_jingle_factory_constructor (GType type, gabble_signal_connect_weak (priv->conn, "status-changed", (GCallback) connection_status_changed_cb, G_OBJECT (self)); + jingle_share_register (self); jingle_media_rtp_register (self); jingle_transport_google_register (self); jingle_transport_rawudp_register (self); @@ -702,13 +704,14 @@ get_unique_sid_for (GabbleJingleFactory *factory, { guint32 val; gchar *sid = NULL; - gchar *key_; + gchar *key_ = NULL; do { val = g_random_int_range (1000000, G_MAXINT); g_free (sid); + g_free (key_); sid = g_strdup_printf ("%u", val); key_ = make_session_map_key (peer, jid, sid); } diff --git a/src/jingle-factory.h b/src/jingle-factory.h index 04dce6d5a..1c0865ec5 100644 --- a/src/jingle-factory.h +++ b/src/jingle-factory.h @@ -66,7 +66,8 @@ typedef enum { JINGLE_ACTION_SESSION_TERMINATE, JINGLE_ACTION_TRANSPORT_INFO, JINGLE_ACTION_TRANSPORT_ACCEPT, - JINGLE_ACTION_DESCRIPTION_INFO + JINGLE_ACTION_DESCRIPTION_INFO, + JINGLE_ACTION_INFO } JingleAction; typedef enum { diff --git a/src/jingle-media-rtp.c b/src/jingle-media-rtp.c index 62bbd7588..0bf894be7 100644 --- a/src/jingle-media-rtp.c +++ b/src/jingle-media-rtp.c @@ -40,6 +40,7 @@ #include "jingle-session.h" #include "namespaces.h" #include "util.h" +#include "jingle-transport-google.h" G_DEFINE_TYPE (GabbleJingleMediaRtp, gabble_jingle_media_rtp, GABBLE_TYPE_JINGLE_CONTENT); @@ -244,6 +245,8 @@ static void parse_description (GabbleJingleContent *content, LmMessageNode *desc_node, GError **error); static void produce_description (GabbleJingleContent *obj, LmMessageNode *content_node); +static void transport_created (GabbleJingleContent *obj, + GabbleJingleTransportIface *transport); static void gabble_jingle_media_rtp_class_init (GabbleJingleMediaRtpClass *cls) @@ -260,6 +263,7 @@ gabble_jingle_media_rtp_class_init (GabbleJingleMediaRtpClass *cls) content_class->parse_description = parse_description; content_class->produce_description = produce_description; + content_class->transport_created = transport_created; param_spec = g_param_spec_uint ("media-type", "RTP media type", "Media type.", @@ -280,6 +284,34 @@ gabble_jingle_media_rtp_class_init (GabbleJingleMediaRtpClass *cls) G_TYPE_NONE, 1, G_TYPE_POINTER); } +static void transport_created (GabbleJingleContent *content, + GabbleJingleTransportIface *transport) +{ + GabbleJingleMediaRtp *self = GABBLE_JINGLE_MEDIA_RTP (content); + GabbleJingleMediaRtpPrivate *priv = self->priv; + GabbleJingleTransportGoogle *gtrans = NULL; + JingleDialect dialect; + + if (GABBLE_IS_JINGLE_TRANSPORT_GOOGLE (transport)) + { + gtrans = GABBLE_JINGLE_TRANSPORT_GOOGLE (transport); + dialect = gabble_jingle_session_get_dialect (content->session); + + if (priv->media_type == JINGLE_MEDIA_TYPE_VIDEO && + JINGLE_IS_GOOGLE_DIALECT (dialect)) + { + jingle_transport_google_set_component_name (gtrans, "video_rtp", 1); + jingle_transport_google_set_component_name (gtrans, "video_rtcp", 2); + } + else + { + jingle_transport_google_set_component_name (gtrans, "rtp", 1); + jingle_transport_google_set_component_name (gtrans, "rtcp", 2); + } + } +} + + static JingleMediaType extract_media_type (LmMessageNode *desc_node, GError **error) diff --git a/src/jingle-session.c b/src/jingle-session.c index 5aa51bfc3..9a9ba7dbd 100644 --- a/src/jingle-session.c +++ b/src/jingle-session.c @@ -108,7 +108,7 @@ typedef struct { } JingleStateActions; /* gcc should be able to figure this out from the table below, but.. */ -#define MAX_ACTIONS_PER_STATE 11 +#define MAX_ACTIONS_PER_STATE 12 /* NB: JINGLE_ACTION_UNKNOWN is used as a terminator here. */ static JingleAction allowed_actions[MAX_JINGLE_STATES][MAX_ACTIONS_PER_STATE] = { @@ -118,24 +118,27 @@ static JingleAction allowed_actions[MAX_JINGLE_STATES][MAX_ACTIONS_PER_STATE] = { JINGLE_ACTION_SESSION_TERMINATE, JINGLE_ACTION_SESSION_ACCEPT, JINGLE_ACTION_TRANSPORT_ACCEPT, /* required for GTalk4 */ JINGLE_ACTION_DESCRIPTION_INFO, JINGLE_ACTION_SESSION_INFO, - JINGLE_ACTION_TRANSPORT_INFO, JINGLE_ACTION_UNKNOWN }, + JINGLE_ACTION_TRANSPORT_INFO, JINGLE_ACTION_INFO, + JINGLE_ACTION_UNKNOWN }, /* JS_STATE_PENDING_INITIATED */ { JINGLE_ACTION_SESSION_ACCEPT, JINGLE_ACTION_SESSION_TERMINATE, JINGLE_ACTION_TRANSPORT_INFO, JINGLE_ACTION_CONTENT_REJECT, JINGLE_ACTION_CONTENT_MODIFY, JINGLE_ACTION_CONTENT_ACCEPT, JINGLE_ACTION_CONTENT_REMOVE, JINGLE_ACTION_DESCRIPTION_INFO, JINGLE_ACTION_TRANSPORT_ACCEPT, JINGLE_ACTION_SESSION_INFO, + JINGLE_ACTION_INFO, JINGLE_ACTION_UNKNOWN }, /* JS_STATE_PENDING_ACCEPT_SENT */ { JINGLE_ACTION_TRANSPORT_INFO, JINGLE_ACTION_DESCRIPTION_INFO, JINGLE_ACTION_SESSION_TERMINATE, JINGLE_ACTION_SESSION_INFO, + JINGLE_ACTION_INFO, JINGLE_ACTION_UNKNOWN }, /* JS_STATE_ACTIVE */ { JINGLE_ACTION_CONTENT_MODIFY, JINGLE_ACTION_CONTENT_ADD, JINGLE_ACTION_CONTENT_REMOVE, JINGLE_ACTION_CONTENT_REPLACE, JINGLE_ACTION_CONTENT_ACCEPT, JINGLE_ACTION_CONTENT_REJECT, JINGLE_ACTION_SESSION_INFO, JINGLE_ACTION_TRANSPORT_INFO, - JINGLE_ACTION_DESCRIPTION_INFO, + JINGLE_ACTION_DESCRIPTION_INFO, JINGLE_ACTION_INFO, JINGLE_ACTION_SESSION_TERMINATE, JINGLE_ACTION_UNKNOWN }, /* JS_STATE_ENDED */ { JINGLE_ACTION_UNKNOWN } @@ -158,13 +161,15 @@ gabble_jingle_session_defines_action (GabbleJingleSession *sess, return (a != JINGLE_ACTION_DESCRIPTION_INFO && a != JINGLE_ACTION_SESSION_INFO); case JINGLE_DIALECT_GTALK4: - if (a == JINGLE_ACTION_TRANSPORT_ACCEPT) + if (a == JINGLE_ACTION_TRANSPORT_ACCEPT || + a == JINGLE_ACTION_INFO ) return TRUE; case JINGLE_DIALECT_GTALK3: return (a == JINGLE_ACTION_SESSION_ACCEPT || a == JINGLE_ACTION_SESSION_INITIATE || a == JINGLE_ACTION_SESSION_TERMINATE || - a == JINGLE_ACTION_TRANSPORT_INFO); + a == JINGLE_ACTION_TRANSPORT_INFO || + a == JINGLE_ACTION_INFO); default: return FALSE; } @@ -519,6 +524,8 @@ parse_action (const gchar *txt) return JINGLE_ACTION_TRANSPORT_ACCEPT; else if (!tp_strdiff (txt, "description-info")) return JINGLE_ACTION_DESCRIPTION_INFO; + else if (!tp_strdiff (txt, "info")) + return JINGLE_ACTION_INFO; return JINGLE_ACTION_UNKNOWN; } @@ -559,6 +566,8 @@ produce_action (JingleAction action, JingleDialect dialect) return "transport-accept"; case JINGLE_ACTION_DESCRIPTION_INFO: return "description-info"; + case JINGLE_ACTION_INFO: + return "info"; default: /* only reached if g_return_val_if_fail is disabled */ DEBUG ("unknown action %u", action); @@ -1332,6 +1341,7 @@ on_transport_info (GabbleJingleSession *sess, LmMessageNode *node, if (JINGLE_IS_GOOGLE_DIALECT (priv->dialect)) { GHashTableIter iter; + gpointer value; if (priv->dialect == JINGLE_DIALECT_GTALK4) { @@ -1363,8 +1373,9 @@ on_transport_info (GabbleJingleSession *sess, LmMessageNode *node, } g_hash_table_iter_init (&iter, priv->initiator_contents); - while (g_hash_table_iter_next (&iter, NULL, (gpointer) &c)) + while (g_hash_table_iter_next (&iter, NULL, &value)) { + c = value; gabble_jingle_content_parse_transport_info (c, node, error); if (error != NULL && *error != NULL) break; @@ -1401,6 +1412,26 @@ on_description_info (GabbleJingleSession *sess, LmMessageNode *node, _foreach_content (sess, node, TRUE, _each_description_info, error); } +static void +on_info (GabbleJingleSession *sess, LmMessageNode *node, + GError **error) +{ + GabbleJingleSessionPrivate *priv = sess->priv; + GabbleJingleContent *c = NULL; + + DEBUG ("received info "); + if (JINGLE_IS_GOOGLE_DIALECT (priv->dialect)) + { + GHashTableIter iter; + g_hash_table_iter_init (&iter, priv->initiator_contents); + while (g_hash_table_iter_next (&iter, NULL, (gpointer) &c)) + { + gabble_jingle_content_parse_info (c, node, error); + if (error != NULL && *error != NULL) + break; + } + } +} static HandlerFunc handlers[] = { NULL, /* for unknown action */ @@ -1416,7 +1447,8 @@ static HandlerFunc handlers[] = { on_session_terminate, /* jingle_on_session_terminate */ on_transport_info, /* jingle_on_transport_info */ on_transport_accept, - on_description_info + on_description_info, + on_info }; static void @@ -1875,13 +1907,17 @@ try_session_initiate_or_accept (GabbleJingleSession *sess) DEBUG ("Contents are ready: %s", contents_ready ? "yes" : "no"); if (!contents_ready) + { + DEBUG ("Contents not yet ready, not initiating/accepting now.."); return; + } msg = gabble_jingle_session_new_message (sess, action, &sess_node); if (priv->dialect == JINGLE_DIALECT_GTALK3) { gboolean has_video = FALSE; + gboolean has_audio = FALSE; GHashTableIter iter; gpointer value; @@ -1895,19 +1931,25 @@ try_session_initiate_or_accept (GabbleJingleSession *sess) if (type == JINGLE_MEDIA_TYPE_VIDEO) { has_video = TRUE; - break; + } + else if (type == JINGLE_MEDIA_TYPE_AUDIO) + { + has_audio = TRUE; } } - sess_node = lm_message_node_add_child (sess_node, "description", - NULL); + if (has_video || has_audio) + { + sess_node = lm_message_node_add_child (sess_node, "description", + NULL); - if (has_video) - lm_message_node_set_attribute (sess_node, "xmlns", - NS_GOOGLE_SESSION_VIDEO); - else - lm_message_node_set_attribute (sess_node, "xmlns", - NS_GOOGLE_SESSION_PHONE); + if (has_video) + lm_message_node_set_attribute (sess_node, "xmlns", + NS_GOOGLE_SESSION_VIDEO); + else + lm_message_node_set_attribute (sess_node, "xmlns", + NS_GOOGLE_SESSION_PHONE); + } } @@ -2219,6 +2261,12 @@ gabble_jingle_session_get_peer_resource (GabbleJingleSession *sess) } const gchar * +gabble_jingle_session_get_initiator (GabbleJingleSession *sess) +{ + return sess->priv->initiator; +} + +const gchar * gabble_jingle_session_get_sid (GabbleJingleSession *sess) { return sess->priv->sid; diff --git a/src/jingle-session.h b/src/jingle-session.h index 123c3432d..a71bcdaa0 100644 --- a/src/jingle-session.h +++ b/src/jingle-session.h @@ -111,6 +111,8 @@ GType gabble_jingle_session_get_content_type (GabbleJingleSession *); GList *gabble_jingle_session_get_contents (GabbleJingleSession *sess); const gchar *gabble_jingle_session_get_peer_resource ( GabbleJingleSession *sess); +const gchar *gabble_jingle_session_get_initiator ( + GabbleJingleSession *sess); const gchar *gabble_jingle_session_get_sid (GabbleJingleSession *sess); JingleDialect gabble_jingle_session_get_dialect (GabbleJingleSession *sess); diff --git a/src/jingle-share.c b/src/jingle-share.c new file mode 100644 index 000000000..918235843 --- /dev/null +++ b/src/jingle-share.c @@ -0,0 +1,528 @@ +/* + * jingle-share.c - Source for GabbleJingleShare + * + * Copyright (C) 2010 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* Share content type deals with file sharing content, ie. file transfers. It + * Google's jingle variants (libjingle 0.3/0.4). */ + +#include "jingle-share.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <glib.h> + +#include <loudmouth/loudmouth.h> + +#define DEBUG_FLAG GABBLE_DEBUG_SHARE + +#include "connection.h" +#include "debug.h" +#include "jingle-content.h" +#include "jingle-factory.h" +#include "jingle-session.h" +#include "namespaces.h" +#include "util.h" + +/****************************************************************** + * Example description XML: + * + * <description xmlns="http://www.google.com/session/share"> + * <manifest> + * <file size='341'> + * <name>foo.txt</name> + * </file> + * <file size='51321'> + * <name>foo.jpg</name> + * <image width='480' height='320'/> + * </file> + * <folder> + * <name>stuff</name> + * </folder> + * </manifest> + * <protocol> + * <http> + * <url name='source-path'>/temporary/23A53F01/</url> + * <url name='preview-path'>/temporary/90266EA1/</url> + * </http> + * </protocol> + * </description> + * + *******************************************************************/ + + +G_DEFINE_TYPE (GabbleJingleShare, + gabble_jingle_share, GABBLE_TYPE_JINGLE_CONTENT); + +/* properties */ +enum +{ + PROP_MEDIA_TYPE = 1, + PROP_FILENAME, + PROP_FILESIZE, + LAST_PROPERTY +}; + +struct _GabbleJingleSharePrivate +{ + gboolean dispose_has_run; + + GabbleJingleShareManifest *manifest; + gchar *filename; + guint64 filesize; +}; + + +static gchar * +generate_temp_url (void) +{ + gchar *uuid = gabble_generate_id (); + gchar *url = NULL; + + url = g_strdup_printf ("/temporary/%s/", uuid); + g_free (uuid); + + return url; +} + +static void +free_manifest (GabbleJingleShare *self) +{ + GList * i; + + if (self->priv->manifest) + { + for (i = self->priv->manifest->entries; i; i = i->next) + { + GabbleJingleShareManifestEntry *item = i->data; + + g_free (item->name); + g_slice_free (GabbleJingleShareManifestEntry, item); + } + g_list_free (self->priv->manifest->entries); + + g_free (self->priv->manifest->source_url); + g_free (self->priv->manifest->preview_url); + + g_slice_free (GabbleJingleShareManifest, self->priv->manifest); + self->priv->manifest = NULL; + } +} + +static void +ensure_manifest (GabbleJingleShare *self) +{ + if (self->priv->manifest == NULL) + { + GabbleJingleShareManifestEntry *m = NULL; + + self->priv->manifest = g_slice_new0 (GabbleJingleShareManifest); + self->priv->manifest->source_url = generate_temp_url (); + self->priv->manifest->preview_url = generate_temp_url (); + + if (self->priv->filename != NULL) + { + m = g_slice_new0 (GabbleJingleShareManifestEntry); + m->name = g_strdup (self->priv->filename); + m->size = self->priv->filesize; + self->priv->manifest->entries = g_list_prepend (NULL, m); + } + } +} + +static void +gabble_jingle_share_init (GabbleJingleShare *obj) +{ + GabbleJingleSharePrivate *priv = + G_TYPE_INSTANCE_GET_PRIVATE (obj, GABBLE_TYPE_JINGLE_SHARE, + GabbleJingleSharePrivate); + + DEBUG ("jingle share init called"); + obj->priv = priv; + + priv->dispose_has_run = FALSE; +} + + +static void +gabble_jingle_share_dispose (GObject *object) +{ + GabbleJingleShare *self = GABBLE_JINGLE_SHARE (object); + GabbleJingleSharePrivate *priv = self->priv; + + if (priv->dispose_has_run) + return; + + DEBUG ("dispose called"); + priv->dispose_has_run = TRUE; + + g_free (priv->filename); + priv->filename = NULL; + + free_manifest (self); + + if (G_OBJECT_CLASS (gabble_jingle_share_parent_class)->dispose) + G_OBJECT_CLASS (gabble_jingle_share_parent_class)->dispose (object); +} + + +static void parse_description (GabbleJingleContent *content, + LmMessageNode *desc_node, GError **error); +static void produce_description (GabbleJingleContent *obj, + LmMessageNode *content_node); + + +static void +gabble_jingle_share_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GabbleJingleShare *self = GABBLE_JINGLE_SHARE (object); + GabbleJingleSharePrivate *priv = self->priv; + + switch (property_id) + { + case PROP_MEDIA_TYPE: + g_value_set_uint (value, JINGLE_MEDIA_TYPE_NONE); + break; + case PROP_FILENAME: + g_value_set_string (value, priv->filename); + break; + case PROP_FILESIZE: + g_value_set_uint64 (value, priv->filesize); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gabble_jingle_share_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GabbleJingleShare *self = GABBLE_JINGLE_SHARE (object); + GabbleJingleSharePrivate *priv = self->priv; + + switch (property_id) + { + case PROP_MEDIA_TYPE: + break; + case PROP_FILENAME: + g_free (priv->filename); + priv->filename = g_value_dup_string (value); + free_manifest (self); + /* simulate a media_ready when we know our own filename */ + _gabble_jingle_content_set_media_ready (GABBLE_JINGLE_CONTENT (self)); + break; + case PROP_FILESIZE: + priv->filesize = g_value_get_uint64 (value); + free_manifest (self); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static JingleContentSenders +get_default_senders (GabbleJingleContent *c) +{ + return JINGLE_CONTENT_SENDERS_INITIATOR; +} + +static void +gabble_jingle_share_class_init (GabbleJingleShareClass *cls) +{ + GObjectClass *object_class = G_OBJECT_CLASS (cls); + GabbleJingleContentClass *content_class = GABBLE_JINGLE_CONTENT_CLASS (cls); + + g_type_class_add_private (cls, sizeof (GabbleJingleSharePrivate)); + + object_class->get_property = gabble_jingle_share_get_property; + object_class->set_property = gabble_jingle_share_set_property; + object_class->dispose = gabble_jingle_share_dispose; + + content_class->parse_description = parse_description; + content_class->produce_description = produce_description; + content_class->get_default_senders = get_default_senders; + + /* This property is here only because jingle-session sets the media-type + when constructing the object.. */ + g_object_class_install_property (object_class, PROP_MEDIA_TYPE, + g_param_spec_uint ("media-type", "media type", + "irrelevant media type. Will always be NONE.", + JINGLE_MEDIA_TYPE_NONE, JINGLE_MEDIA_TYPE_NONE, + JINGLE_MEDIA_TYPE_NONE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, PROP_FILENAME, + g_param_spec_string ("filename", "file name", + "The name of the file", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, PROP_FILESIZE, + g_param_spec_uint64 ("filesize", "file size", + "The size of the file", + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + +} + +static void +parse_description (GabbleJingleContent *content, + LmMessageNode *desc_node, GError **error) +{ + GabbleJingleShare *self = GABBLE_JINGLE_SHARE (content); + GabbleJingleSharePrivate *priv = self->priv; + NodeIter i; + LmMessageNode *manifest_node = NULL; + LmMessageNode *protocol_node = NULL; + LmMessageNode *http_node = NULL; + + DEBUG ("parse description called"); + + if (priv->manifest != NULL) + { + DEBUG ("Not parsing description, we already have a manifest"); + return; + } + + manifest_node = lm_message_node_get_child (desc_node, "manifest"); + protocol_node = lm_message_node_get_child (desc_node, "protocol"); + if (protocol_node != NULL) + http_node = lm_message_node_get_child (protocol_node, "http"); + + free_manifest (self); + priv->manifest = g_slice_new0 (GabbleJingleShareManifest); + + /* Build the manifest */ + for (i = node_iter (manifest_node); i; i = node_iter_next (i)) + { + LmMessageNode *node = node_iter_data (i); + LmMessageNode *name = NULL; + LmMessageNode *image = NULL; + gboolean folder; + const gchar *size; + GabbleJingleShareManifestEntry *m = NULL; + + if (!tp_strdiff (lm_message_node_get_name (node), "folder")) + folder = TRUE; + else if (!tp_strdiff (lm_message_node_get_name (node), "file")) + folder = FALSE; + else + continue; + + name = lm_message_node_get_child (node, "name"); + if (name == NULL) + continue; + + m = g_slice_new0 (GabbleJingleShareManifestEntry); + m->folder = folder; + m->name = g_strdup (lm_message_node_get_value (name)); + + size = lm_message_node_get_attribute (node, "size"); + if (size) + m->size = strtoull (size, NULL, 10); + + image = lm_message_node_get_child (node, "image"); + if (image) + { + const gchar *width; + const gchar *height; + + m->image = TRUE; + + width = lm_message_node_get_attribute (image, "width"); + if (width) + m->image_width = g_ascii_strtoull (width, NULL, 10); + + height =lm_message_node_get_attribute (image, "height"); + if (height) + m->image_height = g_ascii_strtoull (height, NULL, 10); + } + priv->manifest->entries = g_list_prepend (priv->manifest->entries, m); + } + + /* Get the source and preview url paths from the protocol/http node */ + if (http_node != NULL) + { + /* clear the previously set values */ + for (i = node_iter (http_node); i; i = node_iter_next (i)) + { + LmMessageNode *node = node_iter_data (i); + const gchar *name; + + if (tp_strdiff (lm_message_node_get_name (node), "url")) + continue; + + name = lm_message_node_get_attribute (node, "name"); + if (name == NULL) + continue; + + if (!tp_strdiff (name, "source-path")) + { + const gchar *url = lm_message_node_get_value (node); + priv->manifest->source_url = g_strdup (url); + } + + if (!tp_strdiff (name, "preview-path")) + { + const gchar *url = lm_message_node_get_value (node); + priv->manifest->preview_url = g_strdup (url); + } + } + } + + /* Build the filename/filesize property values based on the new manifest */ + g_free (priv->filename); + priv->filename = NULL; + priv->filesize = 0; + + if (g_list_length (priv->manifest->entries) > 0) + { + if (g_list_length (priv->manifest->entries) == 1) + { + GabbleJingleShareManifestEntry *m = priv->manifest->entries->data; + + if (m->folder) + priv->filename = g_strdup_printf ("%s.tar", m->name); + else + priv->filename = g_strdup (m->name); + + priv->filesize = m->size; + } + else + { + GList *li; + gchar *temp; + + priv->filename = g_strdup (""); + for (li = priv->manifest->entries; li; li = li->next) + { + GabbleJingleShareManifestEntry *m = li->data; + + temp = priv->filename; + priv->filename = g_strdup_printf ("%s%s%s%s", temp, m->name, + m->folder? ".tar":"", li->next == NULL? "": "-"); + g_free (temp); + + priv->filesize += m->size; + } + temp = priv->filename; + priv->filename = g_strdup_printf ("%s.tar", temp); + g_free (temp); + } + } + _gabble_jingle_content_set_media_ready (content); +} + +static void +produce_description (GabbleJingleContent *content, LmMessageNode *content_node) +{ + GabbleJingleShare *self = GABBLE_JINGLE_SHARE (content); + GabbleJingleSharePrivate *priv = self->priv; + GList *i; + + LmMessageNode *desc_node; + LmMessageNode *manifest_node; + LmMessageNode *protocol_node; + LmMessageNode *http_node; + LmMessageNode *url_node; + + DEBUG ("produce description called"); + + ensure_manifest (self); + + desc_node = lm_message_node_add_child (content_node, "description", NULL); + + lm_message_node_set_attribute (desc_node, "xmlns", NS_GOOGLE_SESSION_SHARE); + + manifest_node = lm_message_node_add_child (desc_node, "manifest", NULL); + + for (i = priv->manifest->entries; i; i = i->next) + { + GabbleJingleShareManifestEntry *m = i->data; + LmMessageNode *file_node; + LmMessageNode *image_node; + gchar *size_str, *width_str, *height_str; + + if (m->folder) + file_node = lm_message_node_add_child (manifest_node, "folder", NULL); + else + file_node = lm_message_node_add_child (manifest_node, "file", NULL); + + if (m->size > 0) + { + size_str = g_strdup_printf ("%" G_GUINT64_FORMAT, m->size); + lm_message_node_set_attribute (file_node, "size", size_str); + g_free (size_str); + } + lm_message_node_add_child (file_node, "name", m->name); + + if (m->image && + (m->image_width > 0 || m->image_height > 0)) + { + image_node = lm_message_node_add_child (file_node, "image", NULL); + if (m->image_width > 0) + { + width_str = g_strdup_printf ("%d", m->image_width); + lm_message_node_set_attribute (image_node, "width", width_str); + g_free (width_str); + } + + if (m->image_height > 0) + { + height_str = g_strdup_printf ("%d", m->image_height); + lm_message_node_set_attribute (image_node, "height", height_str); + g_free (height_str); + } + } + } + + protocol_node = lm_message_node_add_child (desc_node, "protocol", NULL); + http_node = lm_message_node_add_child (protocol_node, "http", NULL); + url_node = lm_message_node_add_child (http_node, "url", + priv->manifest->source_url); + lm_message_node_set_attribute (url_node, "name", "source-path"); + url_node = lm_message_node_add_child (http_node, "url", + priv->manifest->preview_url); + lm_message_node_set_attribute (url_node, "name", "preview-path"); + +} + +GabbleJingleShareManifest * +gabble_jingle_share_get_manifest (GabbleJingleShare *self) +{ + ensure_manifest (self); + return self->priv->manifest; +} + +void +jingle_share_register (GabbleJingleFactory *factory) +{ + /* GTalk video call namespace */ + gabble_jingle_factory_register_content_type (factory, + NS_GOOGLE_SESSION_SHARE, + GABBLE_TYPE_JINGLE_SHARE); +} diff --git a/src/jingle-share.h b/src/jingle-share.h new file mode 100644 index 000000000..5444743c0 --- /dev/null +++ b/src/jingle-share.h @@ -0,0 +1,87 @@ +/* + * jingle-share.h - Header for GabbleJingleShare + * Copyright (C) 2008 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __JINGLE_SHARE_H__ +#define __JINGLE_SHARE_H__ + +#include <glib-object.h> +#include <loudmouth/loudmouth.h> +#include "types.h" + +#include "jingle-content.h" + +G_BEGIN_DECLS + +typedef struct _GabbleJingleShareClass GabbleJingleShareClass; + +GType gabble_jingle_share_get_type (void); + +/* TYPE MACROS */ +#define GABBLE_TYPE_JINGLE_SHARE \ + (gabble_jingle_share_get_type ()) +#define GABBLE_JINGLE_SHARE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), GABBLE_TYPE_JINGLE_SHARE, \ + GabbleJingleShare)) +#define GABBLE_JINGLE_SHARE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), GABBLE_TYPE_JINGLE_SHARE, \ + GabbleJingleShareClass)) +#define GABBLE_IS_JINGLE_SHARE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), GABBLE_TYPE_JINGLE_SHARE)) +#define GABBLE_IS_JINGLE_SHARE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), GABBLE_TYPE_JINGLE_SHARE)) +#define GABBLE_JINGLE_SHARE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), GABBLE_TYPE_JINGLE_SHARE, \ + GabbleJingleShareClass)) + +struct _GabbleJingleShareClass { + GabbleJingleContentClass parent_class; +}; + +typedef struct _GabbleJingleSharePrivate GabbleJingleSharePrivate; + +struct _GabbleJingleShare { + GabbleJingleContent parent; + GabbleJingleSharePrivate *priv; +}; + +typedef struct { + gboolean folder; + gboolean image; + guint64 size; + gchar *name; + guint image_width; + guint image_height; +} GabbleJingleShareManifestEntry; + +typedef struct { + gchar *source_url; + gchar *preview_url; + /* a list of g_slice_new (GabbleJingleShareManifestEntry) */ + GList *entries; +} GabbleJingleShareManifest; + +void jingle_share_register (GabbleJingleFactory *factory); + +gchar *gabble_jingle_share_get_source_url (GabbleJingleShare *content); +gchar *gabble_jingle_share_get_preview_url (GabbleJingleShare *content); +GabbleJingleShareManifest *gabble_jingle_share_get_manifest ( + GabbleJingleShare *content); + +#endif /* __JINGLE_SHARE_H__ */ + diff --git a/src/jingle-transport-google.c b/src/jingle-transport-google.c index 5087494bf..8b7ed9461 100644 --- a/src/jingle-transport-google.c +++ b/src/jingle-transport-google.c @@ -69,6 +69,10 @@ struct _GabbleJingleTransportGooglePrivate JingleTransportState state; gchar *transport_ns; + /* Component names or jingle-share transport 'channels' + g_strdup'd component name => GINT_TO_POINTER (component id) */ + GHashTable *component_names; + GList *local_candidates; /* A pointer into "local_candidates" list to mark the @@ -88,6 +92,9 @@ gabble_jingle_transport_google_init (GabbleJingleTransportGoogle *obj) GabbleJingleTransportGooglePrivate); obj->priv = priv; + priv->component_names = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + priv->dispose_has_run = FALSE; } @@ -103,6 +110,9 @@ gabble_jingle_transport_google_dispose (GObject *object) DEBUG ("dispose called"); priv->dispose_has_run = TRUE; + g_hash_table_destroy (priv->component_names); + priv->component_names = NULL; + jingle_transport_free_candidates (priv->remote_candidates); priv->remote_candidates = NULL; @@ -231,13 +241,8 @@ parse_candidates (GabbleJingleTransportIface *obj, GabbleJingleTransportGoogle *t = GABBLE_JINGLE_TRANSPORT_GOOGLE (obj); GabbleJingleTransportGooglePrivate *priv = t->priv; GList *candidates = NULL; - JingleMediaType media_type; - JingleDialect dialect; NodeIter i; - g_object_get (priv->content, "media-type", &media_type, NULL); - dialect = gabble_jingle_session_get_dialect (priv->content->session); - for (i = node_iter (transport_node); i; i = node_iter_next (i)) { LmMessageNode *node = node_iter_data (i); @@ -255,32 +260,15 @@ parse_candidates (GabbleJingleTransportIface *obj, if (name == NULL) break; - if (g_str_has_prefix (name, "video_")) - { - if (media_type != JINGLE_MEDIA_TYPE_VIDEO) - continue; - - if (!tp_strdiff (name, "video_rtp")) - component = 1; - else if (!tp_strdiff (name, "video_rtcp")) - component = 2; - else - break; - } - else + if (!g_hash_table_lookup_extended (priv->component_names, name, + NULL, NULL)) { - if (media_type != JINGLE_MEDIA_TYPE_AUDIO - && JINGLE_IS_GOOGLE_DIALECT (dialect)) - continue; - - if (!tp_strdiff (name, "rtp")) - component = 1; - else if (!tp_strdiff (name, "rtcp")) - component = 2; - else - break; + DEBUG ("component name %s unknown to this transport", name); + continue; } + component = GPOINTER_TO_INT (g_hash_table_lookup (priv->component_names, + name)); address = lm_message_node_get_attribute (node, "address"); if (address == NULL) break; @@ -484,40 +472,63 @@ group_and_transmit_candidates (GabbleJingleTransportGoogle *transport, GList *candidates) { GabbleJingleTransportGooglePrivate *priv = transport->priv; - GList *rtp_candidates = NULL; - GList *rtcp_candidates = NULL; - JingleDialect dialect; + GList *all_candidates = NULL; JingleMediaType media; GList *li; + GList *cands; for (li = candidates; li != NULL; li = g_list_next (li)) { JingleCandidate *c = li->data; - if (c->component == 1) - rtp_candidates = g_list_prepend (rtp_candidates, c); - else if (c->component == 2) - rtcp_candidates = g_list_prepend (rtcp_candidates, c); - else - DEBUG ("Ignoring unknown component %d", c->component); + for (cands = all_candidates; cands != NULL; cands = g_list_next (cands)) + { + JingleCandidate *c2 = ((GList *) cands->data)->data; + + if (c->component == c2->component) + { + break; + } + } + if (cands == NULL) + { + all_candidates = g_list_prepend (all_candidates, NULL); + cands = all_candidates; + } + + cands->data = g_list_prepend (cands->data, c); } - dialect = gabble_jingle_session_get_dialect (priv->content->session); g_object_get (priv->content, "media-type", &media, NULL); - if (media == JINGLE_MEDIA_TYPE_VIDEO && JINGLE_IS_GOOGLE_DIALECT (dialect)) + for (cands = all_candidates; cands != NULL; cands = g_list_next (cands)) { - transmit_candidates (transport, "video_rtp", rtp_candidates); - transmit_candidates (transport, "video_rtcp", rtcp_candidates); - } - else - { - transmit_candidates (transport, "rtp", rtp_candidates); - transmit_candidates (transport, "rtcp", rtcp_candidates); + GHashTableIter iter; + gpointer key, value; + gchar *name = NULL; + JingleCandidate *c = ((GList *) cands->data)->data; + + g_hash_table_iter_init (&iter, priv->component_names); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + if (GPOINTER_TO_INT (value) == c->component) + { + name = key; + break; + } + } + if (name) + { + transmit_candidates (transport, name, cands->data); + } + else + { + DEBUG ("Ignoring unknown component %d", c->component); + } + g_list_free (cands->data); } - g_list_free (rtp_candidates); - g_list_free (rtcp_candidates); + g_list_free (all_candidates); } /* Takes in a list of slice-allocated JingleCandidate structs */ @@ -607,6 +618,23 @@ transport_iface_init (gpointer g_iface, gpointer iface_data) klass->get_transport_type = get_transport_type; } +/* Returns FALSE if the component name already exists */ +gboolean +jingle_transport_google_set_component_name ( + GabbleJingleTransportGoogle *transport, + const gchar *name, guint component_id) +{ + GabbleJingleTransportGooglePrivate *priv = transport->priv; + + if (g_hash_table_lookup_extended (priv->component_names, name, NULL, NULL)) + return FALSE; + + g_hash_table_insert (priv->component_names, g_strdup (name), + GINT_TO_POINTER (component_id)); + + return TRUE; +} + void jingle_transport_google_register (GabbleJingleFactory *factory) { diff --git a/src/jingle-transport-google.h b/src/jingle-transport-google.h index 5cb7d029d..32c7ea76a 100644 --- a/src/jingle-transport-google.h +++ b/src/jingle-transport-google.h @@ -61,5 +61,9 @@ struct _GabbleJingleTransportGoogle { void jingle_transport_google_register (GabbleJingleFactory *factory); +gboolean jingle_transport_google_set_component_name ( + GabbleJingleTransportGoogle *transport, + const gchar *name, guint component_id); + #endif /* __JINGLE_TRANSPORT_GOOGLE_H__ */ diff --git a/src/namespaces.h b/src/namespaces.h index b1d666433..04445fac5 100644 --- a/src/namespaces.h +++ b/src/namespaces.h @@ -32,6 +32,7 @@ #define NS_FILE_TRANSFER "http://jabber.org/protocol/si/profile/file-transfer" #define NS_GOOGLE_CAPS "http://www.google.com/xmpp/client/caps" #define NS_GOOGLE_FEAT_SESSION "http://www.google.com/xmpp/protocol/session" +#define NS_GOOGLE_FEAT_SHARE "http://google.com/xmpp/protocol/share/v1" #define NS_GOOGLE_FEAT_VOICE "http://www.google.com/xmpp/protocol/voice/v1" #define NS_GOOGLE_FEAT_VIDEO "http://www.google.com/xmpp/protocol/video/v1" #define NS_GOOGLE_JINGLE_INFO "google:jingleinfo" @@ -69,6 +70,8 @@ #define NS_GOOGLE_SESSION_PHONE "http://www.google.com/session/phone" /* Video capability in Google's Jingle dialect */ #define NS_GOOGLE_SESSION_VIDEO "http://www.google.com/session/video" +/* File transfer capability in Google's Jingle dialect */ +#define NS_GOOGLE_SESSION_SHARE "http://www.google.com/session/share" /* google-p2p transport */ #define NS_GOOGLE_TRANSPORT_P2P "http://www.google.com/transport/p2p" diff --git a/src/types.h b/src/types.h index 2cbac15e8..95a6a4377 100644 --- a/src/types.h +++ b/src/types.h @@ -52,6 +52,7 @@ typedef struct _GabbleJingleTransportGoogle GabbleJingleTransportGoogle; typedef struct _GabbleJingleTransportRawUdp GabbleJingleTransportRawUdp; typedef struct _GabbleJingleTransportIceUdp GabbleJingleTransportIceUdp; typedef struct _GabbleJingleMediaRtp GabbleJingleMediaRtp; +typedef struct _GabbleJingleShare GabbleJingleShare; typedef struct _GabbleCallMember GabbleCallMember; typedef struct _GabbleCallMemberContent GabbleCallMemberContent; diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index dab481522..1b151220f 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -148,6 +148,17 @@ TWISTED_TESTS = \ jingle/test-content-complex.py \ jingle/test-wait-for-caps.py \ jingle/test-wait-for-caps-incomplete.py \ + jingle-share/test-caps-file-transfer.py \ + jingle-share/test-send-file.py \ + jingle-share/test-send-file-send-before-accept.py \ + jingle-share/test-receive-file-and-close-socket-while-receiving.py \ + jingle-share/test-receive-file-and-disconnect.py \ + jingle-share/test-receive-file-and-sender-disconnect-while-pending.py \ + jingle-share/test-receive-file-and-sender-disconnect-while-transfering.py \ + jingle-share/test-receive-file-decline.py \ + jingle-share/test-send-file-and-cancel-immediately.py \ + jingle-share/test-send-file-wait-to-provide.py \ + jingle-share/test-multift.py \ file-transfer/test-caps-file-transfer.py \ file-transfer/test-ibb-too-early.py \ file-transfer/test-receive-file-and-close-socket-while-receiving.py \ diff --git a/tests/twisted/constants.py b/tests/twisted/constants.py index 69b0b736b..25f9aacb8 100644 --- a/tests/twisted/constants.py +++ b/tests/twisted/constants.py @@ -224,6 +224,7 @@ FT_DATE = CHANNEL_TYPE_FILE_TRANSFER + '.Date' FT_AVAILABLE_SOCKET_TYPES = CHANNEL_TYPE_FILE_TRANSFER + '.AvailableSocketTypes' FT_TRANSFERRED_BYTES = CHANNEL_TYPE_FILE_TRANSFER + '.TransferredBytes' FT_INITIAL_OFFSET = CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset' +FT_FILE_COLLECTION = CHANNEL_TYPE_FILE_TRANSFER + '.FUTURE.FileCollection' GF_CAN_ADD = 1 GF_CAN_REMOVE = 2 diff --git a/tests/twisted/jingle-share/file_transfer_helper.py b/tests/twisted/jingle-share/file_transfer_helper.py new file mode 100644 index 000000000..e677d0745 --- /dev/null +++ b/tests/twisted/jingle-share/file_transfer_helper.py @@ -0,0 +1,578 @@ +import dbus +import socket +import hashlib +import time +import datetime + +from servicetest import EventPattern, TimeoutError, assertEquals, assertLength +from gabbletest import exec_test, sync_stream, make_result_iq, elem_iq, elem +import ns + +from caps_helper import text_fixed_properties, text_allowed_properties, \ + stream_tube_fixed_properties, stream_tube_allowed_properties, \ + dbus_tube_fixed_properties, dbus_tube_allowed_properties, \ + ft_fixed_properties, ft_allowed_properties, compute_caps_hash + +from twisted.words.xish import domish, xpath + +import constants as cs +import sys + + +class File(object): + DEFAULT_DATA = "What a nice file" + DEFAULT_NAME = "The foo.txt" + DEFAULT_CONTENT_TYPE = 'text/plain' + DEFAULT_DESCRIPTION = "A nice file to test" + + def __init__(self, data=DEFAULT_DATA, name=DEFAULT_NAME, + content_type=DEFAULT_CONTENT_TYPE, description=DEFAULT_DESCRIPTION, + hash_type=cs.FILE_HASH_TYPE_MD5): + self.data = data + self.size = len(self.data) + self.name = name + + self.content_type = content_type + self.description = description + self.date = int(time.time()) + + self.compute_hash(hash_type) + + self.offset = 0 + + def compute_hash(self, hash_type): + assert hash_type == cs.FILE_HASH_TYPE_MD5 + self.hash_type = hash_type + self.hash = hashlib.md5(self.data).hexdigest() + +generic_ft_caps = [(text_fixed_properties, text_allowed_properties), + (stream_tube_fixed_properties, \ + stream_tube_allowed_properties), + (dbus_tube_fixed_properties, dbus_tube_allowed_properties), + (ft_fixed_properties, ft_allowed_properties)] + +generic_caps = [(text_fixed_properties, text_allowed_properties), + (stream_tube_fixed_properties, \ + stream_tube_allowed_properties), + (dbus_tube_fixed_properties, dbus_tube_allowed_properties)] + +class FileTransferTest(object): + caps_identities = None + caps_features = None + caps_ft = None + + def __init__(self, file, address_type, access_control, access_control_param): + self.file = file + self.address_type = address_type + self.access_control = access_control + self.access_control_param = access_control_param + self.closed = True + + def connect(self): + self.conn.Connect() + + self.q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED], + path=self.conn.object.object_path) + + self.self_handle = self.conn.GetSelfHandle() + self.self_handle_name = self.conn.InspectHandles(cs.HT_CONTACT, [self.self_handle])[0] + + def set_target(self, jid): + self.target = jid + self.handle = self.conn.RequestHandles(cs.HT_CONTACT, [jid])[0] + + def set_ft_caps(self): + caps_iface = dbus.Interface(self.conn, cs.CONN_IFACE_CONTACT_CAPS) + caps_iface.UpdateCapabilities([("self", + [ft_fixed_properties], + dbus.Array([], signature="s"))]) + + self.q.expect('dbus-signal', signal='ContactCapabilitiesChanged', + path=self.conn.object.object_path, + args=[{self.self_handle:generic_ft_caps}]) + + def wait_for_ft_caps(self): + conn_caps_iface = dbus.Interface(self.conn, cs.CONN_IFACE_CONTACT_CAPS) + + caps = conn_caps_iface.GetContactCapabilities([self.handle]) + if caps != dbus.Dictionary({self.handle:generic_ft_caps}): + self.q.expect('dbus-signal', + signal='ContactCapabilitiesChanged', + path=self.conn.object.object_path, + args=[{self.handle:generic_ft_caps}]) + caps = conn_caps_iface.GetContactCapabilities([self.handle]) + assert caps == dbus.Dictionary({self.handle:generic_ft_caps}), caps + + def create_ft_channel(self): + ft_chan = self.bus.get_object(self.conn.object.bus_name, self.ft_path) + self.channel = dbus.Interface(ft_chan, cs.CHANNEL) + self.ft_channel = dbus.Interface(ft_chan, cs.CHANNEL_TYPE_FILE_TRANSFER) + self.ft_props = dbus.Interface(ft_chan, cs.PROPERTIES_IFACE) + + self.closed = False + def channel_closed_cb(): + self.closed = True + self.channel.connect_to_signal('Closed', channel_closed_cb) + + def close_channel(self): + if self.closed is False: + self.channel.Close() + self.q.expect('dbus-signal', signal='Closed', + path=self.channel.object_path) + + def done(self): + pass + + def test(self, q, bus, conn, stream): + self.q = q + self.bus = bus + self.conn = conn + self.stream = stream + + self.stream.addObserver( + "//presence", self._cb_presence_iq, priority=1) + self.stream.addObserver( + "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", + self._cb_disco_iq, priority=1) + + def _cb_presence_iq(self, stanza): + nodes = xpath.queryForNodes("/presence/c", stanza) + c = nodes[0] + if 'share-v1' in c.getAttribute('ext'): + assert FileTransferTest.caps_identities is not None and \ + FileTransferTest.caps_features is not None + + new_hash = compute_caps_hash(FileTransferTest.caps_identities, + FileTransferTest.caps_features + \ + [ns.GOOGLE_FEAT_SHARE], + {}) + # Replace ver hash from one with file-transfer ns to one without + FileTransferTest.caps_ft = c.attributes['ver'] + c.attributes['ver'] = new_hash + else: + node = c.attributes['node'] + ver = c.attributes['ver'] + # ask for raw caps + request = elem_iq(self.stream, 'get', + from_='fake_contact@jabber.org/resource')( + elem(ns.DISCO_INFO, 'query', node=(node + '#' + ver))) + self.stream.send(request) + + + def _cb_disco_iq(self, iq): + nodes = xpath.queryForNodes("/iq/query", iq) + query = nodes[0] + + if query.getAttribute('node') is None: + return + + node = query.attributes['node'] + ver = node.replace("http://telepathy.freedesktop.org/caps#", "") + + if iq.getAttribute('type') == 'result': + + if FileTransferTest.caps_identities is None or \ + FileTransferTest.caps_features is None: + identity_nodes = xpath.queryForNodes('/iq/query/identity', iq) + assertLength(1, identity_nodes) + identity_node = identity_nodes[0] + + identity_category = identity_node['category'] + identity_type = identity_node['type'] + identity_name = identity_node['name'] + identity = '%s/%s//%s' % (identity_category, identity_type, + identity_name) + FileTransferTest.caps_identities = [identity] + + FileTransferTest.caps_features = [] + for feature in xpath.queryForNodes('/iq/query/feature', iq): + FileTransferTest.caps_features.append(feature['var']) + + # Check if the hash matches the announced capabilities + assertEquals(compute_caps_hash(FileTransferTest.caps_identities, + FileTransferTest.caps_features, + {}), ver) + + if ver == FileTransferTest.caps_ft: + caps_share = compute_caps_hash(FileTransferTest.caps_identities, + FileTransferTest.caps_features + \ + [ns.GOOGLE_FEAT_SHARE], + {}) + n = query.attributes['node'].replace(ver, caps_share) + query.attributes['node'] = n + + for feature in xpath.queryForNodes('/iq/query/feature', iq): + query.children.remove(feature) + + for f in FileTransferTest.caps_features + [ns.GOOGLE_FEAT_SHARE]: + el = domish.Element((None, 'feature')) + el['var'] = f + query.addChild(el) + + elif iq.getAttribute('type') == 'get': + caps_share = compute_caps_hash(FileTransferTest.caps_identities, + FileTransferTest.caps_features + \ + [ns.GOOGLE_FEAT_SHARE], + {}) + + if ver == caps_share: + n = query.attributes['node'].replace(ver, + FileTransferTest.caps_ft) + query.attributes['node'] = n + + def create_socket(self): + if self.address_type == cs.SOCKET_ADDRESS_TYPE_UNIX: + return socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + elif self.address_type == cs.SOCKET_ADDRESS_TYPE_IPV4: + return socket.socket(socket.AF_INET, socket.SOCK_STREAM) + elif self.address_type == cs.SOCKET_ADDRESS_TYPE_IPV6: + return socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + else: + assert False + +class ReceiveFileTest(FileTransferTest): + def __init__(self, file, address_type, access_control, access_control_param): + FileTransferTest.__init__(self, file, address_type, access_control, + access_control_param) + + self._actions = [self.connect, self.set_ft_caps, None, + + self.wait_for_ft_caps, None, + + self.check_new_channel, + self.accept_file, None, + + self.receive_file, None, + + self.close_channel, self.done] + + def check_new_channel(self): + e = self.q.expect('dbus-signal', signal='NewChannels', + path=self.conn.object.object_path) + channels = e.args[0] + assert len(channels) == 1 + path, props = channels[0] + + # check channel properties + # org.freedesktop.Telepathy.Channel D-Bus properties + assert props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_FILE_TRANSFER, props + assert props[cs.INTERFACES] == [], props + assert props[cs.TARGET_HANDLE] == self.handle, props + assert props[cs.TARGET_ID] == self.target, props + assert props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT, props + assert props[cs.REQUESTED] == False, props + assert props[cs.INITIATOR_HANDLE] == self.handle, props + assert props[cs.INITIATOR_ID] == self.target, props + + # org.freedesktop.Telepathy.Channel.Type.FileTransfer D-Bus properties + assert props[cs.FT_STATE] == cs.FT_STATE_PENDING, props + assert props[cs.FT_CONTENT_TYPE] == '', props + assert props[cs.FT_FILENAME].encode('utf-8') == self.file.name, props + assert props[cs.FT_SIZE] == self.file.size, props + # FT's protocol doesn't allow us the send the hash info + assert props[cs.FT_CONTENT_HASH_TYPE] == cs.FILE_HASH_TYPE_NONE, props + assert props[cs.FT_CONTENT_HASH] == '', props + assert props[cs.FT_DESCRIPTION] == '', props + assert props[cs.FT_DATE] == 0, props + assert props[cs.FT_AVAILABLE_SOCKET_TYPES] == \ + {cs.SOCKET_ADDRESS_TYPE_UNIX: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST], + cs.SOCKET_ADDRESS_TYPE_IPV4: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST], + cs.SOCKET_ADDRESS_TYPE_IPV6: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST]}, \ + props[cs.FT_AVAILABLE_SOCKET_TYPES] + assert props[cs.FT_TRANSFERRED_BYTES] == 0, props + assert props[cs.FT_INITIAL_OFFSET] == 0, props + + self.ft_path = path + + self.create_ft_channel() + + def accept_file(self): + self.address = self.ft_channel.AcceptFile(self.address_type, + self.access_control, self.access_control_param, self.file.offset, + byte_arrays=True) + + state_event = self.q.expect('dbus-signal', + signal='FileTransferStateChanged', + path=self.channel.object_path) + + state, reason = state_event.args + assert state == cs.FT_STATE_ACCEPTED + assert reason == cs.FT_STATE_CHANGE_REASON_REQUESTED + + + state_event, offset_event = self.q.expect_many( + EventPattern ('dbus-signal', + signal='FileTransferStateChanged', + path=self.channel.object_path), + EventPattern ('dbus-signal', + signal='InitialOffsetDefined', + path=self.channel.object_path)) + + offset = offset_event.args[0] + assert offset == 0 + + state, reason = state_event.args + assert state == cs.FT_STATE_OPEN + assert reason == cs.FT_STATE_CHANGE_REASON_NONE + + def receive_file(self): + # Connect to Gabble's socket + s = self.create_socket() + s.connect(self.address) + + self._read_file_from_socket(s) + + def _read_file_from_socket(self, s): + # Read the file from Gabble's socket + data = '' + read = 0 + to_receive = self.file.size + + e = self.q.expect('dbus-signal', signal='TransferredBytesChanged', + path=self.channel.object_path) + count = e.args[0] + + while True: + received = s.recv(1024) + if len(received) == 0: + break + data += received + assert data == self.file.data + + while count < to_receive: + # Catch TransferredBytesChanged until we transfered all the data + e = self.q.expect('dbus-signal', signal='TransferredBytesChanged', + path=self.channel.object_path) + count = e.args[0] + + e = self.q.expect('dbus-signal', signal='FileTransferStateChanged', + path=self.channel.object_path) + state, reason = e.args + assert state == cs.FT_STATE_COMPLETED + assert reason == cs.FT_STATE_CHANGE_REASON_NONE + +class SendFileTest(FileTransferTest): + def __init__(self, file, address_type, + access_control, acces_control_param): + FileTransferTest.__init__(self, file, address_type, + access_control, acces_control_param) + + self._actions = [self.connect, self.set_ft_caps, + self.check_ft_available, None, + + self.wait_for_ft_caps, None, + + self.request_ft_channel, self.provide_file, None, + + self.send_file, self.wait_for_completion, None, + + self.close_channel, self.done] + + def check_ft_available(self): + properties = self.conn.GetAll(cs.CONN_IFACE_REQUESTS, + dbus_interface=cs.PROPERTIES_IFACE) + + # general FT class + assert ({cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT}, + [cs.FT_CONTENT_HASH_TYPE, cs.TARGET_HANDLE, cs.TARGET_ID, cs.FT_CONTENT_TYPE, + cs.FT_FILENAME, cs.FT_SIZE, cs.FT_CONTENT_HASH, cs.FT_DESCRIPTION, cs.FT_DATE] + ) in properties.get('RequestableChannelClasses'),\ + properties['RequestableChannelClasses'] + + # FT class with MD5 as HashType + assert ({cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.FT_CONTENT_HASH_TYPE: cs.FILE_HASH_TYPE_MD5}, + [cs.TARGET_HANDLE, cs.TARGET_ID, cs.FT_CONTENT_TYPE, cs.FT_FILENAME, + cs.FT_SIZE, cs.FT_CONTENT_HASH, cs.FT_DESCRIPTION, cs.FT_DATE] + ) in properties.get('RequestableChannelClasses'),\ + properties['RequestableChannelClasses'] + + def request_ft_channel(self): + requests_iface = dbus.Interface(self.conn, cs.CONN_IFACE_REQUESTS) + + self.ft_path, props = requests_iface.CreateChannel({ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: self.handle, + cs.FT_CONTENT_TYPE: self.file.content_type, + cs.FT_FILENAME: self.file.name, + cs.FT_SIZE: self.file.size, + cs.FT_CONTENT_HASH_TYPE: self.file.hash_type, + cs.FT_CONTENT_HASH: self.file.hash, + cs.FT_DESCRIPTION: self.file.description, + cs.FT_DATE: self.file.date, + cs.FT_INITIAL_OFFSET: 0, + }) + + # org.freedesktop.Telepathy.Channel D-Bus properties + assert props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_FILE_TRANSFER + assert props[cs.INTERFACES] == [] + assert props[cs.TARGET_HANDLE] == self.handle + assert props[cs.TARGET_ID] == self.target + assert props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT + assert props[cs.REQUESTED] == True + assert props[cs.INITIATOR_HANDLE] == self.self_handle + assert props[cs.INITIATOR_ID] == self.self_handle_name + + # org.freedesktop.Telepathy.Channel.Type.FileTransfer D-Bus properties + assert props[cs.FT_STATE] == cs.FT_STATE_PENDING + assert props[cs.FT_CONTENT_TYPE] == self.file.content_type + assert props[cs.FT_FILENAME].encode('utf-8') == self.file.name, props + assert props[cs.FT_SIZE] == self.file.size + assert props[cs.FT_CONTENT_HASH_TYPE] == self.file.hash_type + assert props[cs.FT_CONTENT_HASH] == self.file.hash + assert props[cs.FT_DESCRIPTION] == self.file.description + assert props[cs.FT_DATE] == self.file.date + assert props[cs.FT_AVAILABLE_SOCKET_TYPES] == \ + {cs.SOCKET_ADDRESS_TYPE_UNIX: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST], + cs.SOCKET_ADDRESS_TYPE_IPV4: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST], + cs.SOCKET_ADDRESS_TYPE_IPV6: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST]}, \ + props[cs.FT_AVAILABLE_SOCKET_TYPES] + assert props[cs.FT_TRANSFERRED_BYTES] == 0 + assert props[cs.FT_INITIAL_OFFSET] == 0 + + self.create_ft_channel() + + self.open = False + self.offset_defined = False + + def initial_offset_defined_cb(offset): + self.offset_defined = True + assert offset == 0, offset + + self.ft_channel.connect_to_signal('InitialOffsetDefined', + initial_offset_defined_cb) + + + # Make sure the file transfer is of type jingle-share + event = self.q.expect('stream-iq', stream=self.stream, + query_name = 'session', + query_ns = ns.GOOGLE_SESSION) + description_node = xpath.queryForNodes('/iq/session/description', + event.stanza)[0] + assert description_node.uri == ns.GOOGLE_SESSION_SHARE, \ + description_node.uri + + def provide_file(self): + # try to accept our outgoing file transfer + try: + self.ft_channel.AcceptFile(self.address_type, + self.access_control, self.access_control_param, self.file.offset, + byte_arrays=True) + except dbus.DBusException, e: + assert e.get_dbus_name() == cs.NOT_AVAILABLE + else: + assert False + + # In case a unit test accepts the FT before we ProvideFile + # then the ProvideFile will result in an OPEN state with reason + state = self.ft_props.Get(cs.CHANNEL_TYPE_FILE_TRANSFER, 'State') + if state == cs.FT_STATE_ACCEPTED: + self.open_reason = cs.FT_STATE_CHANGE_REASON_REQUESTED + else: + self.open_reason = cs.FT_STATE_CHANGE_REASON_NONE + + self.address = self.ft_channel.ProvideFile(self.address_type, + self.access_control, self.access_control_param, + byte_arrays=True) + + + def send_file(self): + + if self.open is False: + self.q.expect('dbus-signal', + signal='FileTransferStateChanged', + path=self.channel.object_path, + args=[cs.FT_STATE_OPEN, self.open_reason]) + + assert self.offset_defined == True + + s = self.create_socket() + s.connect(self.address) + s.send(self.file.data) + + def wait_for_completion(self): + to_send = self.file.size + self.count = 0 + + def bytes_changed_cb(bytes): + self.count = bytes + + self.ft_channel.connect_to_signal('TransferredBytesChanged', bytes_changed_cb) + + + # FileTransferStateChanged can be fired while we are receiving data + self.completed = False + def ft_state_changed_cb(state, reason): + if state == cs.FT_STATE_COMPLETED: + self.completed = True + self.ft_channel.connect_to_signal('FileTransferStateChanged', ft_state_changed_cb) + + + # If not all the bytes transferred have been announced using + # TransferredBytesChanged, wait for them + while self.count < to_send: + self.q.expect('dbus-signal', signal='TransferredBytesChanged', + path=self.channel.object_path) + + assert self.count == to_send + + +def exec_file_transfer_test(send_cls, recv_cls, file = None): + addr_type = cs.SOCKET_ADDRESS_TYPE_IPV4 + access_control = cs.SOCKET_ACCESS_CONTROL_LOCALHOST + access_control_param = "" + + if file is None: + file = File() + + def test(q, bus, conns, streams): + q.timeout = 15 + conn1, conn2 = conns + stream1, stream2 = streams + send = send_cls(file, addr_type, access_control, + access_control_param) + recv = recv_cls(file, addr_type, access_control, + access_control_param) + send.test(q, bus, conn1, stream1) + recv.test(q, bus, conn2, stream2) + + send_action = 0 + recv_action = 0 + target_set = False + done = False + while send_action < len(send._actions) or \ + recv_action < len(recv._actions): + for i in range(send_action, len(send._actions)): + action = send._actions[i] + if action is None: + break + done = action() + if done is True: + break + send_action = i + 1 + + if done is True: + break + + for i in range(recv_action, len(recv._actions)): + action = recv._actions[i] + if action is None: + break + done = action() + if done is True: + break + recv_action = i + 1 + + if done is True: + break + + if target_set == False: + send.set_target(recv.self_handle_name) + recv.set_target(send.self_handle_name) + target_set = True + + exec_test(test, num_instances=2) diff --git a/tests/twisted/jingle-share/jingleshareutils.py b/tests/twisted/jingle-share/jingleshareutils.py new file mode 100644 index 000000000..16c75fbda --- /dev/null +++ b/tests/twisted/jingle-share/jingleshareutils.py @@ -0,0 +1,103 @@ +import dbus + +from twisted.words.xish import xpath + +from servicetest import (assertEquals, EventPattern) +from gabbletest import exec_test, make_result_iq, sync_stream, make_presence +import constants as cs + +from caps_helper import compute_caps_hash, \ + text_fixed_properties, text_allowed_properties, \ + ft_fixed_properties, ft_allowed_properties + +import ns + +run = 0 + +def test_ft_caps_from_contact(q, bus, conn, stream, contact, contact_handle, client): + global run + run += 1 + + conn_caps_iface = dbus.Interface(conn, cs.CONN_IFACE_CONTACT_CAPS) + conn_contacts_iface = dbus.Interface(conn, cs.CONN_IFACE_CONTACTS) + + # send presence with no FT cap + presence = make_presence(contact, status='hello') + c = presence.addElement((ns.CAPS, 'c')) + c['node'] = client + c['ver'] = compute_caps_hash(['client/pc//jingleshareutils-%d' % run], [], {}) + c['ext'] = "" + stream.send(presence) + + # Gabble looks up our capabilities + event = q.expect('stream-iq', to=contact, query_ns=ns.DISCO_INFO) + query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] + assert query_node.attributes['node'] == \ + client + '#' + c['ver'] + + # send good reply + result = make_result_iq(stream, event.stanza) + query = result.firstChildElement() + query['node'] = client + '#' + c['ver'] + stream.send(result) + + # no change in ContactCapabilities, so no signal ContactCapabilitiesChanged + sync_stream(q, stream) + + # no special capabilities + basic_caps = dbus.Dictionary({contact_handle: + [(text_fixed_properties, text_allowed_properties)]}) + caps = conn_caps_iface.GetContactCapabilities([contact_handle]) + assert caps == basic_caps, caps + # test again, to check GetContactCapabilities does not have side effect + caps = conn_caps_iface.GetContactCapabilities([contact_handle]) + assert caps == basic_caps, caps + # check the Contacts interface give the same caps + caps_via_contacts_iface = conn_contacts_iface.GetContactAttributes( + [contact_handle], [cs.CONN_IFACE_CONTACT_CAPS], False) \ + [contact_handle][cs.ATTR_CONTACT_CAPABILITIES] + assert caps_via_contacts_iface == caps[contact_handle], \ + caps_via_contacts_iface + + # send presence with ft capa + presence = make_presence(contact, status='hello') + c = presence.addElement((ns.CAPS, 'c')) + c['node'] = client + c['ext'] = "share-v1" + c['ver'] = compute_caps_hash([], [], {}) + stream.send(presence) + + # Gabble looks up our capabilities + event = q.expect('stream-iq', to=contact, query_ns=ns.DISCO_INFO) + query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] + assert query_node.attributes['node'] == \ + client + '#' + c['ext'] + + # send good reply + result = make_result_iq(stream, event.stanza) + query = result.firstChildElement() + query['node'] = client + '#' + c['ext'] + feature = query.addElement('feature') + feature['var'] = ns.GOOGLE_FEAT_SHARE + stream.send(result) + + + generic_ft_caps = dbus.Dictionary({contact_handle: + [(text_fixed_properties, text_allowed_properties), + (ft_fixed_properties, ft_allowed_properties)]}) + + event = q.expect('dbus-signal', signal='ContactCapabilitiesChanged') + assert len(event.args) == 1 + assert event.args[0] == generic_ft_caps + + caps = conn_caps_iface.GetContactCapabilities([contact_handle]) + assert caps == generic_ft_caps, caps + # test again, to check GetContactCapabilities does not have side effect + caps = conn_caps_iface.GetContactCapabilities([contact_handle]) + assert caps == generic_ft_caps, caps + # check the Contacts interface give the same caps + caps_via_contacts_iface = conn_contacts_iface.GetContactAttributes( + [contact_handle], [cs.CONN_IFACE_CONTACT_CAPS], False) \ + [contact_handle][cs.ATTR_CONTACT_CAPABILITIES] + assert caps_via_contacts_iface == caps[contact_handle], \ + caps_via_contacts_iface diff --git a/tests/twisted/jingle-share/test-caps-file-transfer.py b/tests/twisted/jingle-share/test-caps-file-transfer.py new file mode 100644 index 000000000..21a19ab8e --- /dev/null +++ b/tests/twisted/jingle-share/test-caps-file-transfer.py @@ -0,0 +1,156 @@ +import dbus + +from twisted.words.xish import xpath + +from servicetest import (assertEquals, EventPattern) +from gabbletest import exec_test, make_result_iq, sync_stream, make_presence +import constants as cs + +from caps_helper import compute_caps_hash, \ + text_fixed_properties, text_allowed_properties, \ + stream_tube_fixed_properties, stream_tube_allowed_properties, \ + dbus_tube_fixed_properties, dbus_tube_allowed_properties, \ + ft_fixed_properties, ft_allowed_properties + +import ns +from jingleshareutils import test_ft_caps_from_contact + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + client = 'http://telepathy.freedesktop.org/fake-client' + + test_ft_caps_from_contact(q, bus, conn, stream, 'bilbo1@foo.com/Foo', + 2L, client) + + # our own capabilities, formerly tested here, are now in + # tests/twisted/caps/advertise-contact-capabilities.py + + +generic_ft_caps = [(text_fixed_properties, text_allowed_properties), + (stream_tube_fixed_properties, \ + stream_tube_allowed_properties), + (dbus_tube_fixed_properties, dbus_tube_allowed_properties), + (ft_fixed_properties, ft_allowed_properties)] + +generic_caps = [(text_fixed_properties, text_allowed_properties), + (stream_tube_fixed_properties, \ + stream_tube_allowed_properties), + (dbus_tube_fixed_properties, dbus_tube_allowed_properties)] + +def check_contact_caps(conn, handle, with_ft): + conn_caps_iface = dbus.Interface(conn, cs.CONN_IFACE_CONTACT_CAPS) + conn_contacts_iface = dbus.Interface(conn, cs.CONN_IFACE_CONTACTS) + + if with_ft: + expected_caps = dbus.Dictionary({handle: generic_ft_caps}) + else: + expected_caps = dbus.Dictionary({handle: generic_caps}) + + caps = conn_caps_iface.GetContactCapabilities([handle]) + assert caps == expected_caps, caps + # check the Contacts interface give the same caps + caps_via_contacts_iface = conn_contacts_iface.GetContactAttributes( + [handle], [cs.CONN_IFACE_CONTACT_CAPS], False) \ + [handle][cs.ATTR_CONTACT_CAPABILITIES] + assert caps_via_contacts_iface == caps[handle], \ + caps_via_contacts_iface + + +def test2(q, bus, connections, streams): + + for i, conn in enumerate(connections): + path = conn.object.object_path + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', path=path, + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + conn1, conn2 = connections + stream1, stream2 = streams + conn1_handle = conn1.Properties.Get(cs.CONN, 'SelfHandle') + conn1_jid = conn1.InspectHandles(cs.HT_CONTACT, [conn1_handle])[0] + conn2_handle = conn2.Properties.Get(cs.CONN, 'SelfHandle') + conn2_jid = conn2.InspectHandles(cs.HT_CONTACT, [conn2_handle])[0] + handle1 = conn2.RequestHandles(cs.HT_CONTACT, [conn1_jid])[0] + handle2 = conn1.RequestHandles(cs.HT_CONTACT, [conn2_jid])[0] + + q.expect_many(EventPattern('dbus-signal', + signal='ContactCapabilitiesChanged', + path=conn1.object.object_path), + EventPattern('dbus-signal', + signal='ContactCapabilitiesChanged', + path=conn2.object.object_path)) + + check_contact_caps (conn1, handle2, False) + check_contact_caps (conn2, handle1, False) + + caps_iface = dbus.Interface(conn1, cs.CONN_IFACE_CONTACT_CAPS) + caps_iface.UpdateCapabilities([("self", + [ft_fixed_properties], + dbus.Array([], signature="s"))]) + + _, presence, disco, _ = \ + q.expect_many(EventPattern('dbus-signal', + signal='ContactCapabilitiesChanged', + path=conn1.object.object_path, + args=[{conn1_handle:generic_ft_caps}]), + EventPattern('stream-presence', stream=stream1), + EventPattern('stream-iq', stream=stream1, + query_ns=ns.DISCO_INFO, + iq_type = 'result'), + EventPattern('dbus-signal', + signal='ContactCapabilitiesChanged', + path=conn2.object.object_path, + args=[{handle1:generic_ft_caps}])) + + presence_c = xpath.queryForNodes('/presence/c', presence.stanza)[0] + assert "share-v1" in presence_c.attributes['ext'] + + conn1_ver = presence_c.attributes['ver'] + + found_share = False + for feature in xpath.queryForNodes('/iq/query/feature', disco.stanza): + if feature.attributes['var'] == ns.GOOGLE_FEAT_SHARE: + found_share = True + assert found_share + + check_contact_caps (conn2, handle1, True) + + caps_iface = dbus.Interface(conn2, cs.CONN_IFACE_CONTACT_CAPS) + caps_iface.UpdateCapabilities([("self", + [ft_fixed_properties], + dbus.Array([], signature="s"))]) + + _, presence, _ = \ + q.expect_many(EventPattern('dbus-signal', + signal='ContactCapabilitiesChanged', + path=conn2.object.object_path, + args=[{conn2_handle:generic_ft_caps}]), + EventPattern('stream-presence', stream=stream2), + EventPattern('dbus-signal', + signal='ContactCapabilitiesChanged', + path=conn1.object.object_path, + args=[{handle2:generic_ft_caps}])) + + presence_c = xpath.queryForNodes('/presence/c', presence.stanza)[0] + assert "share-v1" in presence_c.attributes['ext'] + + # We will have the same capabilities on both sides, so we can't check for + # a cap disco since the hash will be the same, so we need to make sure the + # hash is indeed the same + assert presence_c.attributes['ver'] == conn1_ver + + found_share = False + for feature in xpath.queryForNodes('/iq/query/feature', disco.stanza): + if feature.attributes['var'] == ns.GOOGLE_FEAT_SHARE: + found_share = True + assert found_share + + check_contact_caps (conn1, handle2, True) + + +if __name__ == '__main__': + exec_test(test) + exec_test(test2, num_instances=2) diff --git a/tests/twisted/jingle-share/test-multift.py b/tests/twisted/jingle-share/test-multift.py new file mode 100644 index 000000000..cf1a6818c --- /dev/null +++ b/tests/twisted/jingle-share/test-multift.py @@ -0,0 +1,159 @@ +import dbus + +from twisted.words.xish import xpath +from twisted.words.protocols.jabber.client import IQ + +from servicetest import (assertEquals, EventPattern, TimeoutError) +from gabbletest import exec_test, make_result_iq, sync_stream, make_presence +import constants as cs + +from caps_helper import compute_caps_hash, \ + text_fixed_properties, text_allowed_properties, \ + stream_tube_fixed_properties, stream_tube_allowed_properties, \ + dbus_tube_fixed_properties, dbus_tube_allowed_properties, \ + ft_fixed_properties, ft_allowed_properties + +from jingleshareutils import test_ft_caps_from_contact + +import ns + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + client = 'http://telepathy.freedesktop.org/fake-client' + contact = 'bilbo1@foo.com/Resource' + files = [("file", "File.txt", 12345, False), + ("file", "Image.txt", 54321, True), + ("folder", "Folder", 123, False), + ("folder", "Folder no size", None, True)] + + test_ft_caps_from_contact(q, bus, conn, stream, contact, + 2L, client) + + self_handle = conn.GetSelfHandle() + jid = conn.InspectHandles(cs.HT_CONTACT, [self_handle])[0] + + iq = IQ(stream, "set") + iq['to'] = jid + iq['from'] = contact + session = iq.addElement("session", "http://www.google.com/session") + session['type'] = "initiate" + session['id'] = "2156517633" + session['initiator'] = contact + session.addElement("transport", "http://www.google.com/transport/p2p") + description = session.addElement("description", + "http://www.google.com/session/share") + + manifest = description.addElement("manifest") + for f in files: + type, name, size, image = f + file = manifest.addElement(type) + if size is not None: + file['size'] = str(size) + file.addElement("name", None, name) + if image: + image = file.addElement("image") + image['width'] = '1200' + image['height'] = '1024' + + protocol = description.addElement("protocol") + http = protocol.addElement("http") + url = http.addElement("url", None, "/temporary/ade15194140cf7b7bceafe/") + url['name'] = 'source-path' + url = http.addElement("url", None, "/temporary/578d715be25ddc28870d3f/") + url['name'] = 'preview-path' + + stream.send(iq) + event = q.expect('dbus-signal', signal="NewChannels") + channels = event.args[0] + + # Make sure we get the right amout of channels + assert len(channels) == len(files) + + # Make sure every file transfer has a channel associated with it + found = [False for i in files] + file_collection = None + for channel in channels: + path, props = channel + + # Get the FileCollection and make sure it exists + if file_collection is None: + file_collection = props[cs.FT_FILE_COLLECTION] + assert file_collection != '' + assert file_collection is not None + + # FileCollection must be the same for every channel + assert props[cs.FT_FILE_COLLECTION] == file_collection, props + + for i, f in enumerate(files): + type, name, size, image = f + if type == "folder": + name = "%s.tar" % name + if size is None: + size = 0 + + if props[cs.FT_FILENAME].encode('utf=8') == name: + assert found[i] == False + found[i] = True + assert props[cs.FT_SIZE] == size, props + + assert props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_FILE_TRANSFER, props + assert props[cs.INTERFACES] == [], props + assert props[cs.TARGET_HANDLE] == 2L, props + assert props[cs.TARGET_ID] == contact.replace("/Resource", ""), props + assert props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT, props + assert props[cs.REQUESTED] == False, props + assert props[cs.INITIATOR_HANDLE] == 2L, props + assert props[cs.INITIATOR_ID] == contact.replace("/Resource", ""), props + assert props[cs.FT_STATE] == cs.FT_STATE_PENDING, props + assert props[cs.FT_CONTENT_TYPE] == '', props + # FT's protocol doesn't allow us the send the hash info + assert props[cs.FT_CONTENT_HASH_TYPE] == cs.FILE_HASH_TYPE_NONE, props + assert props[cs.FT_CONTENT_HASH] == '', props + assert props[cs.FT_DESCRIPTION] == '', props + assert props[cs.FT_DATE] == 0, props + assert props[cs.FT_AVAILABLE_SOCKET_TYPES] == \ + {cs.SOCKET_ADDRESS_TYPE_UNIX: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST], + cs.SOCKET_ADDRESS_TYPE_IPV4: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST], + cs.SOCKET_ADDRESS_TYPE_IPV6: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST]}, \ + props[cs.FT_AVAILABLE_SOCKET_TYPES] + assert props[cs.FT_TRANSFERRED_BYTES] == 0, props + assert props[cs.FT_INITIAL_OFFSET] == 0, props + + assert False not in found + + event = q.expect('stream-iq', to=contact, + iq_type='set', query_name='session') + stanza = event.stanza + session_node = xpath.queryForNodes('/iq/session', event.stanza)[0] + assert session_node.attributes['type'] == 'transport-accept' + + # Lower the timeout because we will do a q.expect where we expect it to + # timeout since we check for the *not* reception of the terminate + q.timeout = 2 + + # Cancel all the channels and make sure gabble cancels the multiFT only + # once the last channel has been closed + last_path, props = channels[-1] + for i in range(len(channels)): + path, props = channels[i] + ft_chan = bus.get_object(conn.object.bus_name, path) + channel = dbus.Interface(ft_chan, cs.CHANNEL) + channel.Close() + try: + event = q.expect('stream-iq', to=contact, + iq_type='set', query_name='session') + # If the iq is received, it must be for the last channel closed + assert path == last_path, event + # Make sure it's a terminate message + stanza = event.stanza + session_node = xpath.queryForNodes('/iq/session', event.stanza)[0] + assert session_node.attributes['type'] == 'terminate' + except TimeoutError, e: + # Timeout only for the non last channel getting closed + assert path != last_path + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/jingle-share/test-receive-file-and-close-socket-while-receiving.py b/tests/twisted/jingle-share/test-receive-file-and-close-socket-while-receiving.py new file mode 100644 index 000000000..298fedc7d --- /dev/null +++ b/tests/twisted/jingle-share/test-receive-file-and-close-socket-while-receiving.py @@ -0,0 +1,19 @@ + +import constants as cs +from file_transfer_helper import SendFileTest, ReceiveFileTest, \ + exec_file_transfer_test + +class ReceiveFileAndCancelWhileReceiving(ReceiveFileTest): + def receive_file(self): + # Connect to Gabble's socket + s = self.create_socket() + s.connect(self.address) + + # for some reason the socket is closed + s.close() + + self.q.expect('dbus-signal', signal='FileTransferStateChanged', + args=[cs.FT_STATE_CANCELLED, cs.FT_STATE_CHANGE_REASON_LOCAL_ERROR]) + +if __name__ == '__main__': + exec_file_transfer_test(SendFileTest, ReceiveFileAndCancelWhileReceiving) diff --git a/tests/twisted/jingle-share/test-receive-file-and-disconnect.py b/tests/twisted/jingle-share/test-receive-file-and-disconnect.py new file mode 100644 index 000000000..f7f958788 --- /dev/null +++ b/tests/twisted/jingle-share/test-receive-file-and-disconnect.py @@ -0,0 +1,16 @@ + +from file_transfer_helper import SendFileTest, ReceiveFileTest, \ + exec_file_transfer_test + +class ReceiveFileAndDisconnectTest(ReceiveFileTest): + def receive_file(self): + s = self.create_socket() + s.connect(self.address) + + # return True so the test will be ended and the connection + # disconnected + return True + +if __name__ == '__main__': + exec_file_transfer_test(SendFileTest, ReceiveFileAndDisconnectTest) + diff --git a/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-pending.py b/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-pending.py new file mode 100644 index 000000000..2437483ad --- /dev/null +++ b/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-pending.py @@ -0,0 +1,52 @@ +import dbus + +import constants as cs +from file_transfer_helper import SendFileTest, ReceiveFileTest, \ + FileTransferTest, exec_file_transfer_test + +class ReceiveFileAndSenderDisconnectWhilePendingTest(ReceiveFileTest): + def accept_file(self): + + e = self.q.expect('dbus-signal', signal='FileTransferStateChanged', + path = self.channel.object_path, + args=[cs.FT_STATE_CANCELLED, \ + cs.FT_STATE_CHANGE_REASON_REMOTE_STOPPED]) + + # We can't accept the transfer now + try: + self.ft_channel.AcceptFile(cs.SOCKET_ADDRESS_TYPE_UNIX, + cs.SOCKET_ACCESS_CONTROL_LOCALHOST, "", 0) + except dbus.DBusException, e: + assert e.get_dbus_name() == cs.NOT_AVAILABLE + else: + assert False + + self.close_channel() + + # stop the test + return True + + +class SendFileAndDisconnect (SendFileTest): + def __init__(self, file, address_type, + access_control, acces_control_param): + FileTransferTest.__init__(self, file, address_type, + access_control, acces_control_param) + + self._actions = [self.connect, self.set_ft_caps, + self.check_ft_available, None, + + self.wait_for_ft_caps, None, + + self.request_ft_channel, self.provide_file, + self.disconnect, None, + + self.close_channel, self.done] + + def disconnect(self): + self.conn.Disconnect() + + +if __name__ == '__main__': + exec_file_transfer_test(SendFileAndDisconnect, \ + ReceiveFileAndSenderDisconnectWhilePendingTest) diff --git a/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-transfering.py b/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-transfering.py new file mode 100644 index 000000000..c7b5b92a9 --- /dev/null +++ b/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-transfering.py @@ -0,0 +1,45 @@ +import dbus + +import constants as cs +from file_transfer_helper import SendFileTest, ReceiveFileTest, \ + FileTransferTest, exec_file_transfer_test + +class ReceiveFileAndSenderDisconnectWhileTransfering(ReceiveFileTest): + def receive_file(self): + self.q.expect('dbus-signal', signal='FileTransferStateChanged', + path = self.channel.object_path, + args=[cs.FT_STATE_CANCELLED, \ + cs.FT_STATE_CHANGE_REASON_REMOTE_STOPPED]) + + self.close_channel() + + # stop the test + return True + + +class SendFileAndDisconnect (SendFileTest): + def __init__(self, file, address_type, + access_control, acces_control_param): + FileTransferTest.__init__(self, file, address_type, + access_control, acces_control_param) + + self._actions = [self.connect, self.set_ft_caps, + self.check_ft_available, None, + + self.wait_for_ft_caps, None, + + self.request_ft_channel, self.provide_file, None, + + self.send_file, self.wait_for_completion, + self.disconnect, None, + + self.close_channel, self.done] + + + def disconnect(self): + self.conn.Disconnect() + + +if __name__ == '__main__': + exec_file_transfer_test(SendFileAndDisconnect, \ + ReceiveFileAndSenderDisconnectWhileTransfering) diff --git a/tests/twisted/jingle-share/test-receive-file-decline.py b/tests/twisted/jingle-share/test-receive-file-decline.py new file mode 100644 index 000000000..e7950f3f4 --- /dev/null +++ b/tests/twisted/jingle-share/test-receive-file-decline.py @@ -0,0 +1,79 @@ +import dbus +import constants as cs + +from servicetest import EventPattern +from file_transfer_helper import SendFileTest, ReceiveFileTest, \ + FileTransferTest, exec_file_transfer_test + +class ReceiveFileDecline(ReceiveFileTest): + + def __init__(self, file, address_type, access_control, access_control_param): + FileTransferTest.__init__(self, file, address_type, access_control, + access_control_param) + self._actions = [self.connect, self.set_ft_caps, None, + + self.wait_for_ft_caps, None, + + self.check_new_channel, self.close_and_check, self.done] + + def close_and_check(self): + self.channel.Close() + + state_event, event, _ = self.q.expect_many( + EventPattern('dbus-signal', signal='FileTransferStateChanged', + path=self.channel.object_path), + EventPattern('stream-iq', stream=self.stream, + iq_type='set', query_name='session'), + EventPattern('dbus-signal', signal='Closed', + path=self.channel.object_path)) + + state, reason = state_event.args + assert state == cs.FT_STATE_CANCELLED + assert reason == cs.FT_STATE_CHANGE_REASON_LOCAL_STOPPED + + while event.query.getAttribute('type') != 'terminate': + event = self.q.expect('stream-iq', stream=self.stream, + iq_type='set', query_name='session') + + + +class SendFileDeclined (SendFileTest): + def __init__(self, file, address_type, + access_control, acces_control_param): + FileTransferTest.__init__(self, file, address_type, + access_control, acces_control_param) + + self._actions = [self.connect, self.set_ft_caps, + self.check_ft_available, None, + + self.wait_for_ft_caps, None, + + self.request_ft_channel, self.provide_file, None, + + self.check_declined, self.close_channel, self.done] + + def check_declined(self): + state_event = self.q.expect('dbus-signal', + signal='FileTransferStateChanged', + path=self.channel.object_path) + + state, reason = state_event.args + assert state == cs.FT_STATE_CANCELLED + assert reason == cs.FT_STATE_CHANGE_REASON_REMOTE_STOPPED + + transferred = self.ft_props.Get(cs.CHANNEL_TYPE_FILE_TRANSFER, + 'TransferredBytes') + # no byte has been transferred as the file was declined + assert transferred == 0 + + # try to provide the file + try: + self.provide_file() + except dbus.DBusException, e: + assert e.get_dbus_name() == cs.NOT_AVAILABLE + else: + assert False + + +if __name__ == '__main__': + exec_file_transfer_test(SendFileDeclined, ReceiveFileDecline) diff --git a/tests/twisted/jingle-share/test-send-file-and-cancel-immediately.py b/tests/twisted/jingle-share/test-send-file-and-cancel-immediately.py new file mode 100644 index 000000000..2375e849c --- /dev/null +++ b/tests/twisted/jingle-share/test-send-file-and-cancel-immediately.py @@ -0,0 +1,74 @@ +import dbus +import constants as cs + +from servicetest import EventPattern +from file_transfer_helper import SendFileTest, ReceiveFileTest, \ + FileTransferTest, exec_file_transfer_test + +class ReceiveFileStopped(ReceiveFileTest): + + def __init__(self, file, address_type, access_control, access_control_param): + FileTransferTest.__init__(self, file, address_type, access_control, + access_control_param) + self._actions = [self.connect, self.set_ft_caps, None, + + self.wait_for_ft_caps, None, + + self.check_new_channel, None, + + self.check_stopped, None, + + self.close_channel, self.done] + + def check_stopped(self): + state_event = self.q.expect ('dbus-signal', + signal='FileTransferStateChanged', + path=self.channel.object_path) + + state, reason = state_event.args + assert state == cs.FT_STATE_CANCELLED + assert reason == cs.FT_STATE_CHANGE_REASON_REMOTE_STOPPED + + # try to provide the file + try: + self.accept_file() + except dbus.DBusException, e: + assert e.get_dbus_name() == cs.NOT_AVAILABLE + else: + assert False + + + + +class SendFileAndClose (SendFileTest): + def __init__(self, file, address_type, + access_control, acces_control_param): + FileTransferTest.__init__(self, file, address_type, + access_control, acces_control_param) + + self._actions = [self.connect, self.set_ft_caps, + self.check_ft_available, None, + + self.wait_for_ft_caps, None, + + self.request_ft_channel, self.provide_file, None, + + self.close_and_check, None, + + self.close_channel, self.done] + + def close_and_check(self): + self.channel.Close() + + state_event, _ = self.q.expect_many( + EventPattern('dbus-signal', signal='FileTransferStateChanged', + path=self.channel.object_path), + EventPattern('dbus-signal', signal='Closed', + path=self.channel.object_path)) + + state, reason = state_event.args + assert state == cs.FT_STATE_CANCELLED + assert reason == cs.FT_STATE_CHANGE_REASON_LOCAL_STOPPED + +if __name__ == '__main__': + exec_file_transfer_test(SendFileAndClose, ReceiveFileStopped) diff --git a/tests/twisted/jingle-share/test-send-file-send-before-accept.py b/tests/twisted/jingle-share/test-send-file-send-before-accept.py new file mode 100644 index 000000000..d67991a91 --- /dev/null +++ b/tests/twisted/jingle-share/test-send-file-send-before-accept.py @@ -0,0 +1,28 @@ +from file_transfer_helper import SendFileTest, FileTransferTest, \ + ReceiveFileTest, exec_file_transfer_test + + +class SendFileBeforeAccept(SendFileTest): + def __init__(self, file, address_type, + access_control, acces_control_param): + FileTransferTest.__init__(self, file, address_type, + access_control, acces_control_param) + + self._actions = [self.connect, self.set_ft_caps, + self.check_ft_available, None, + + self.wait_for_ft_caps, None, + + self.request_ft_channel, self.provide_file, + self.set_open, self.send_file, None, + + self.wait_for_completion, None, + + self.close_channel, self.done] + + def set_open(self): + self.open = True + self.offset_defined = True + +if __name__ == '__main__': + exec_file_transfer_test(SendFileBeforeAccept, ReceiveFileTest) diff --git a/tests/twisted/jingle-share/test-send-file-wait-to-provide.py b/tests/twisted/jingle-share/test-send-file-wait-to-provide.py new file mode 100644 index 000000000..0a1ba653e --- /dev/null +++ b/tests/twisted/jingle-share/test-send-file-wait-to-provide.py @@ -0,0 +1,37 @@ +import dbus +import constants as cs + +from servicetest import EventPattern +from file_transfer_helper import SendFileTest, ReceiveFileTest, \ + FileTransferTest, exec_file_transfer_test + +class SendFileAndWaitToProvide (SendFileTest): + def __init__(self, file, address_type, + access_control, acces_control_param): + FileTransferTest.__init__(self, file, address_type, + access_control, acces_control_param) + + self._actions = [self.connect, self.set_ft_caps, + self.check_ft_available, None, + + self.wait_for_ft_caps, None, + + self.request_ft_channel, self.check_pending_state, None, + + self.check_accepted_state, self.provide_file, + self.send_file, self.wait_for_completion, None, + + self.close_channel, self.done] + + def check_pending_state(self): + # state is still Pending as remote didn't accept the transfer yet + state = self.ft_props.Get(cs.CHANNEL_TYPE_FILE_TRANSFER, 'State') + assert state == cs.FT_STATE_PENDING + + def check_accepted_state(self): + # Remote accepted the transfer + state = self.ft_props.Get(cs.CHANNEL_TYPE_FILE_TRANSFER, 'State') + assert state == cs.FT_STATE_ACCEPTED, state + +if __name__ == '__main__': + exec_file_transfer_test(SendFileAndWaitToProvide, ReceiveFileTest) diff --git a/tests/twisted/jingle-share/test-send-file.py b/tests/twisted/jingle-share/test-send-file.py new file mode 100644 index 000000000..0f8ae7611 --- /dev/null +++ b/tests/twisted/jingle-share/test-send-file.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from file_transfer_helper import SendFileTest, ReceiveFileTest, \ + exec_file_transfer_test, File + +if __name__ == '__main__': + file = File() + file.offset = 5 + file.name = "The greek foo δοκιμή.txt" + exec_file_transfer_test(SendFileTest, ReceiveFileTest, file) diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py index c9008022d..d9b290fff 100644 --- a/tests/twisted/ns.py +++ b/tests/twisted/ns.py @@ -8,6 +8,7 @@ FEATURE_NEG = 'http://jabber.org/protocol/feature-neg' FILE_TRANSFER = 'http://jabber.org/protocol/si/profile/file-transfer' GEOLOC = 'http://jabber.org/protocol/geoloc' GOOGLE_FEAT_SESSION = 'http://www.google.com/xmpp/protocol/session' +GOOGLE_FEAT_SHARE = 'http://google.com/xmpp/protocol/share/v1' GOOGLE_FEAT_VOICE = 'http://www.google.com/xmpp/protocol/voice/v1' GOOGLE_FEAT_VIDEO = 'http://www.google.com/xmpp/protocol/video/v1' GOOGLE_JINGLE_INFO = 'google:jingleinfo' @@ -15,6 +16,7 @@ GOOGLE_P2P = "http://www.google.com/transport/p2p" GOOGLE_QUEUE = 'google:queue' GOOGLE_ROSTER = 'google:roster' GOOGLE_SESSION = "http://www.google.com/session" +GOOGLE_SESSION_SHARE = "http://www.google.com/session/share" GOOGLE_SESSION_PHONE = "http://www.google.com/session/phone" GOOGLE_SESSION_VIDEO = "http://www.google.com/session/video" GOOGLE_MAIL_NOTIFY = "google:mail:notify" |