diff --git a/configure.ac b/configure.ac index 80267dc..8bb7423 100644 --- a/configure.ac +++ b/configure.ac @@ -207,6 +207,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.10.1) +AC_SUBST(NICE_CFLAGS) +AC_SUBST(NICE_LIBS) + PKG_CHECK_MODULES([UUID], [uuid], [HAVE_UUID=yes], [HAVE_UUID=no]) AC_SUBST([UUID_CFLAGS]) AC_SUBST([UUID_LIBS]) diff --git a/src/Makefile.am b/src/Makefile.am index e0d656a..fb81014 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -77,6 +77,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 \ @@ -186,14 +188,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@ \ -I $(top_srcdir)/lib -I $(top_builddir)/lib \ -I $(top_srcdir)/gabble \ -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@ + @SOUP_LIBS@ @NICE_LIBS@ @UUID_LIBS@ @GMODULE_LIBS@ # build gibber first all: gibber diff --git a/src/capabilities.c b/src/capabilities.c index ce2d622..8df6002 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; @@ -246,6 +254,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); @@ -311,6 +322,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); @@ -324,6 +336,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; @@ -364,8 +377,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. */ @@ -387,6 +402,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 027faa6..9434129 100644 --- a/src/capabilities.h +++ b/src/capabilities.h @@ -108,10 +108,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 72a9bbc..d03b18c 100644 --- a/src/connection.c +++ b/src/connection.c @@ -2031,7 +2031,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 */ @@ -2054,9 +2054,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); @@ -2405,6 +2409,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 ad180b5..4f5e1cf 100644 --- a/src/debug.c +++ b/src/debug.c @@ -39,6 +39,7 @@ static GDebugKey keys[] = { { "search", GABBLE_DEBUG_SEARCH }, { "base-channel", GABBLE_DEBUG_BASE_CHANNEL }, { "plugins", GABBLE_DEBUG_PLUGINS }, + { "share", GABBLE_DEBUG_SHARE }, { 0, }, }; diff --git a/src/debug.h b/src/debug.h index 5eacff6..3bfcd45 100644 --- a/src/debug.h +++ b/src/debug.h @@ -32,7 +32,8 @@ typedef enum GABBLE_DEBUG_FT = 1 << 18, GABBLE_DEBUG_SEARCH = 1 << 19, GABBLE_DEBUG_BASE_CHANNEL = 1 << 20, - GABBLE_DEBUG_PLUGINS = 1 << 21 + GABBLE_DEBUG_PLUGINS = 1 << 21, + GABBLE_DEBUG_SHARE = 1 << 22 } GabbleDebugFlags; void gabble_debug_set_flags_from_env (void); diff --git a/src/ft-channel.c b/src/ft-channel.c index eff2a88..5ba0306 100644 --- a/src/ft-channel.c +++ b/src/ft-channel.c @@ -18,6 +18,8 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ +#include "config.h" + #include #include #include @@ -37,6 +39,9 @@ #include #include +#include "jingle-factory.h" +#include "jingle-session.h" +#include "jingle-share.h" #include "bytestream-factory.h" #include "connection.h" #include "ft-channel.h" @@ -53,8 +58,15 @@ #include #include +#include + + 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 nice_data_received_cb (NiceAgent *agent, guint stream_id, + guint component_id, guint len, gchar *buffer, gpointer user_data); +static void transferred_chunk (GabbleFileTransferChannel *self, guint64 count); + G_DEFINE_TYPE_WITH_CODE (GabbleFileTransferChannel, gabble_file_transfer_channel, G_TYPE_OBJECT, @@ -106,10 +118,42 @@ enum PROP_RESUME_SUPPORTED, PROP_CONNECTION, - PROP_BYTESTREAM, 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; + gint channel_id; + HttpStatus http_status; + gboolean is_chunked; + guint64 content_length; + gchar *write_buffer; + guint write_len; + gchar *read_buffer; + guint read_len; + gint manifest_entry; +} JingleChannel; + /* private structure */ struct _GabbleFileTransferChannelPrivate { gboolean dispose_has_run; @@ -125,6 +169,9 @@ struct _GabbleFileTransferChannelPrivate { gboolean remote_accepted; gboolean resume_supported; + GabbleJingleSession *jingle; + GHashTable *jingle_channels; + GabbleBytestreamIface *bytestream; GibberListener *listener; GibberTransport *transport; @@ -143,8 +190,6 @@ struct _GabbleFileTransferChannelPrivate { guint64 date; }; -static void set_bytestream (GabbleFileTransferChannel *self, - GabbleBytestreamIface *bytestream); static void gabble_file_transfer_channel_do_close (GabbleFileTransferChannel *self) @@ -291,9 +336,6 @@ gabble_file_transfer_channel_get_property (GObject *object, TP_IFACE_CHANNEL_TYPE_FILE_TRANSFER, "InitialOffset", NULL)); break; - case PROP_BYTESTREAM: - g_value_set_object (value, self->priv->bytestream); - break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -366,10 +408,6 @@ gabble_file_transfer_channel_set_property (GObject *object, case PROP_INITIAL_OFFSET: self->priv->initial_offset = g_value_get_uint64 (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); break; @@ -494,16 +532,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); @@ -735,15 +767,6 @@ gabble_file_transfer_channel_class_init ( g_object_class_install_property (object_class, PROP_DATE, param_spec); - param_spec = g_param_spec_object ( - "bytestream", - "Object implementing the GabbleBytestreamIface interface", - "Bytestream object used to send the file", - G_TYPE_OBJECT, - G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); - g_object_class_install_property (object_class, PROP_BYTESTREAM, - param_spec); - param_spec = g_param_spec_boolean ( "resume-supported", "resume is supported", @@ -770,6 +793,7 @@ gabble_file_transfer_channel_dispose (GObject *object) if (self->priv->dispose_has_run) return; + DEBUG ("dispose called"); self->priv->dispose_has_run = TRUE; tp_handle_unref (handle_repo, self->priv->handle); @@ -783,23 +807,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 */ @@ -846,8 +855,26 @@ gabble_file_transfer_channel_finalize (GObject *object) } static void -close_bytestrean_and_transport (GabbleFileTransferChannel *self) +close_session_and_transport (GabbleFileTransferChannel *self) { + 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; + } + } + + if (self->priv->jingle_channels != NULL) + { + g_hash_table_destroy (self->priv->jingle_channels); + self->priv->jingle_channels = NULL; + } + if (self->priv->bytestream != NULL) { gabble_bytestream_iface_close (self->priv->bytestream, NULL); @@ -855,6 +882,12 @@ close_bytestrean_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); @@ -882,7 +915,7 @@ gabble_file_transfer_channel_close (TpSvcChannel *iface, TP_FILE_TRANSFER_STATE_CANCELLED, TP_FILE_TRANSFER_STATE_CHANGE_REASON_LOCAL_STOPPED); - close_bytestrean_and_transport (self); + close_session_and_transport (self); } gabble_file_transfer_channel_do_close (GABBLE_FILE_TRANSFER_CHANNEL (iface)); @@ -1005,7 +1038,7 @@ check_address_and_access_control (GabbleFileTransferChannel *self, } static void -bytestream_open (GabbleFileTransferChannel *self) +channel_open (GabbleFileTransferChannel *self) { if (self->priv->socket_address != NULL) { @@ -1029,7 +1062,7 @@ bytestream_open (GabbleFileTransferChannel *self) } static void -bytestream_closed (GabbleFileTransferChannel *self) +channel_closed (GabbleFileTransferChannel *self) { if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED) { @@ -1058,25 +1091,652 @@ 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) { - bytestream_closed (self); + channel_closed (self); } } -static void -set_bytestream (GabbleFileTransferChannel *self, - GabbleBytestreamIface *bytestream) +gboolean +gabble_file_transfer_channel_set_bytestream (GabbleFileTransferChannel *self, + GabbleBytestreamIface *bytestream) + { if (bytestream == NULL) - return; + return FALSE; + + if (self->priv->bytestream || self->priv->jingle) + return FALSE; self->priv->bytestream = g_object_ref (bytestream); gabble_signal_connect_weak (bytestream, "state-changed", G_CALLBACK (bytestream_state_changed_cb), G_OBJECT (self)); + + return TRUE; +} + +static JingleChannel * +get_jingle_channel (GabbleFileTransferChannel *self, NiceAgent *agent) +{ + GHashTableIter iter; + gpointer key, value; + JingleChannel *ret = NULL; + + g_hash_table_iter_init (&iter, self->priv->jingle_channels); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + JingleChannel *channel = (JingleChannel *) value; + if (channel->agent == agent) + { + ret = channel; + break; + } + } + + return ret; +} + + +static void +jingle_session_state_changed_cb (GabbleJingleSession *session, + GParamSpec *arg1, + GabbleFileTransferChannel *self) +{ + JingleSessionState state; + + 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: + 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 JS_STATE_PENDING_ACCEPT_SENT: + 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 JS_STATE_ACTIVE: + channel_open (self); + break; + case JS_STATE_ENDED: + default: + channel_closed (self); + break; + } +} + +static void +jingle_session_terminated_cb (GabbleJingleSession *session, + gboolean local_terminator, + TpChannelGroupChangeReason reason, + const gchar *text, + gpointer user_data) +{ + GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); + + g_assert (session == self->priv->jingle); + + 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); + } + + gabble_file_transfer_channel_do_close (self); + +} + +static void +content_new_remote_candidates_cb (GabbleJingleContent *content, + GList *clist, gpointer user_data) +{ + GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); + GList *li; + + DEBUG ("Got new remote candidates"); + + for (li = clist; li; li = li->next) + { + JingleCandidate *candidate = li->data; + NiceCandidate *cand = NULL; + JingleChannel *channel = NULL; + GSList *candidates = NULL; + + if (candidate->type != JINGLE_TRANSPORT_PROTOCOL_UDP) + continue; + + channel = g_hash_table_lookup (self->priv->jingle_channels, + GINT_TO_POINTER (candidate->component)); + if (channel == NULL) + 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 = channel->stream_id; + cand->component_id = 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) + strncpy (cand->foundation, candidate->id, + 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 (channel->agent, + channel->stream_id, 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) +{ + GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); + JingleChannel *channel = get_jingle_channel (self, agent); + GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (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, + 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 */ + channel->channel_id, + /* address */ + ip, + /* port */ + nice_address_get_port (&cand->addr), + /* generation */ + 0, + /* preference */ + cand->priority / 1000, + /* 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) +{ + GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); + JingleChannel *channel = get_jingle_channel (self, agent); + GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (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: + + 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); + /* 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 (GabbleFileTransferChannel *self, + JingleChannel *channel) +{ + GabbleJingleShareManifest *manifest = NULL; + GabbleJingleShareManifestEntry *entry = NULL; + + manifest = gabble_jingle_share_get_manifest (channel->content); + channel->manifest_entry++; + entry = g_list_nth_data (manifest->entries, channel->manifest_entry); + + /* FIXME: We only support one file per manifest for now! */ + if (channel->manifest_entry > 0) + entry = NULL; + + if (entry == NULL) + { + GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (channel->content); + DEBUG ("Received all the file. Transfer is complete"); + gabble_jingle_content_send_complete (content); + + 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 (gibber_transport_buffer_is_empty (self->priv->transport)) + gibber_transport_disconnect (self->priv->transport); + + } + else + { + gchar *buffer = NULL; + + /* 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" + "User-Agent: %s\r\n\r\n", + manifest->source_url, + manifest->source_url[strlen (manifest->source_url) - 1] == '/'?"":"/", + entry->name, + gabble_jingle_session_get_initiator (self->priv->jingle), + PACKAGE_STRING); + + /* FIXME: check for success */ + nice_agent_send (channel->agent, channel->stream_id, + channel->component_id, strlen (buffer), buffer); + g_free (buffer); + + channel->http_status = HTTP_CLIENT_RECEIVE; + /* Do not receive anything until the local socket is connected */ + if (self->priv->transport == NULL) + { + nice_agent_attach_recv (channel->agent, channel->stream_id, + channel->component_id, NULL, NULL, NULL); + channel->agent_attached = FALSE; + } + } +} + +static void +nice_component_writable (NiceAgent *agent, guint stream_id, guint component_id, + gpointer user_data) +{ + GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); + JingleChannel *channel = get_jingle_channel (self, agent); + + if (channel->http_status == HTTP_CLIENT_IDLE) + { + get_next_manifest_entry (self, channel); + } + else if (channel->http_status == HTTP_SERVER_SEND) + { + gibber_transport_block_receiving (self->priv->transport, FALSE); + if (channel->write_buffer) + { + gint ret = nice_agent_send (agent, stream_id, component_id, + channel->write_len, channel->write_buffer); + if (ret < 0 || (guint) ret < channel->write_len) + { + gchar *to_free = channel->write_buffer; + if (ret < 0) + ret = 0; + + channel->write_buffer = g_memdup (channel->write_buffer + ret, + channel->write_len - ret); + channel->write_len = channel->write_len - ret; + g_free (to_free); + + gibber_transport_block_receiving (self->priv->transport, TRUE); + } + else + { + g_free (channel->write_buffer); + channel->write_buffer = NULL; + channel->write_len = 0; + } + transferred_chunk (self, (guint64) channel->write_len); + } + } + +} + +typedef struct +{ + GabbleFileTransferChannel *self; + JingleChannel *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) + server_ip = g_value_get_string (value); + else + return; + + value = g_hash_table_lookup (relay, "port"); + if (value) + server_port = g_value_get_uint (value); + else + return; + + value = g_hash_table_lookup (relay, "username"); + if (value) + username = g_value_get_string (value); + else + return; + + value = g_hash_table_lookup (relay, "password"); + if (value) + password = g_value_get_string (value); + else + return; + + value = g_hash_table_lookup (relay, "type"); + if (value) + 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->channel->agent, + data->channel->stream_id, data->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->self == NULL) + { + DEBUG ("Received relay session callback but self got destroyed"); + g_slice_free (GoogleRelaySessionData, data); + return; + } + + if (relays) + g_ptr_array_foreach (relays, set_relay_info, user_data); + + nice_agent_gather_candidates (data->channel->agent, data->channel->stream_id); + + g_object_remove_weak_pointer (G_OBJECT (data->self), (gpointer *)&data->self); + g_slice_free (GoogleRelaySessionData, data); +} + + +static void +content_new_channel_cb (GabbleJingleContent *content, const gchar *name, + gint channel_id, gpointer user_data) +{ + GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); + TpBaseConnection *base_conn = (TpBaseConnection *) self->priv->connection; + gboolean requested = (self->priv->initiator == base_conn->self_handle); + JingleChannel *channel = g_slice_new0 (JingleChannel); + 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 channel %s was created and linked to id %d", name, channel_id); + + channel->agent = agent; + channel->stream_id = stream_id; + channel->component_id = NICE_COMPONENT_TYPE_RTP; + channel->content = GABBLE_JINGLE_SHARE (content); + channel->channel_id = channel_id; + + /* We increment the manifest_entry before fetching the entry, so we + initialize it at -1, in order to start the index at 0 */ + channel->manifest_entry = -1; + + if (requested) + channel->http_status = HTTP_SERVER_IDLE; + else + 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 channel id.. */ + g_hash_table_insert (self->priv->jingle_channels, + GINT_TO_POINTER (channel_id), channel); + + channel->agent_attached = TRUE; + nice_agent_attach_recv (agent, stream_id, channel->component_id, + g_main_context_default (), nice_data_received_cb, self); + + if (gabble_jingle_factory_get_stun_server ( + self->priv->connection->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->self = self; + relay_data->channel = channel; + g_object_add_weak_pointer (G_OBJECT (relay_data->self), + (gpointer *)&relay_data->self); + gabble_jingle_factory_create_google_relay_session ( + self->priv->connection->jingle_factory, 1, + google_relay_session_cb, relay_data); +} + +static void +content_completed (GabbleJingleContent *content, gpointer user_data) +{ + GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); + 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); +} + +static void +free_jingle_channel (gpointer data) +{ + JingleChannel *channel = (JingleChannel *) data; + + DEBUG ("Freeing jingle channel"); + + if (channel->write_buffer) + { + g_free (channel->write_buffer); + channel->write_buffer = NULL; + } + if (channel->read_buffer) + { + g_free (channel->read_buffer); + channel->read_buffer = NULL; + } + g_object_unref (channel->agent); + g_slice_free (JingleChannel, channel); +} + + +gboolean +gabble_file_transfer_channel_set_jingle_session ( + GabbleFileTransferChannel *self, GabbleJingleSession *session) +{ + TpBaseConnection *base_conn = (TpBaseConnection *) self->priv->connection; + gboolean requested = (self->priv->initiator == base_conn->self_handle); + GList *cs; + + if (session == NULL) + return FALSE; + + if (self->priv->bytestream || self->priv->jingle) + return FALSE; + + if (self->priv->jingle_channels != NULL) + { + g_hash_table_destroy (self->priv->jingle_channels); + self->priv->jingle_channels = NULL; + } + + self->priv->jingle_channels = g_hash_table_new_full (NULL, NULL, + NULL, free_jingle_channel); + + /* FIXME: we should start creating a nice agent already and have it start + the candidate gathering.. but we don't know which channel name to + assign it to... */ + + 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)); + + cs = gabble_jingle_session_get_contents (session); + + if (cs != NULL) + { + GabbleJingleShare *content = GABBLE_JINGLE_SHARE (cs->data); + + if (!requested) + { + GabbleJingleShareManifest *manifest = NULL; + GabbleJingleShareManifestEntry *entry = NULL; + + /* FIXME: We only support one file per manifest for now! */ + manifest = gabble_jingle_share_get_manifest (content); + entry = g_list_nth_data (manifest->entries, 0); + if (entry != NULL) + { + g_free (self->priv->filename); + if (entry->folder) + self->priv->filename = g_strdup_printf ("%s.tar", entry->name); + else + self->priv->filename = g_strdup (entry->name); + + self->priv->size = entry->size; + } + } + + gabble_signal_connect_weak (content, "new-channel", + (GCallback) content_new_channel_cb, G_OBJECT (self)); + gabble_signal_connect_weak (content, "completed", + (GCallback) content_completed, G_OBJECT (self)); + } + + return TRUE; } static void @@ -1121,75 +1781,29 @@ bytestream_negotiate_cb (GabbleBytestreamIface *bytestream, DEBUG ("receiver accepted file offer (offset: %" G_GUINT64_FORMAT ")", self->priv->initial_offset); - set_bytestream (self, bytestream); + gabble_file_transfer_channel_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); + gchar *stream_id, *size_str, *full_jid; - presence = gabble_presence_cache_get (self->priv->connection->presence_cache, - self->priv->handle); - if (presence == NULL) - { - DEBUG ("can't find contact's presence"); - if (error != NULL) - 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 */ - const gchar *resource; - - /* 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, - gabble_capability_set_predicate_has, NS_FILE_TRANSFER); - - if (resource == NULL) - { - DEBUG ("contact doesn't have file transfer capabilities"); - if (error != NULL) - 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); - } + if (resource) + full_jid = g_strdup_printf ("%s/%s", jid, resource); else - { - /* MUC jid, we already have the full jid */ - full_jid = g_strdup (jid); - } + full_jid = g_strdup (jid); + + /* Outgoing FT , we'll need SOCK5 proxies */ + gabble_bytestream_factory_query_socks5_proxies ( + self->priv->connection->bytestream_factory); - DEBUG ("Offering file transfer to %s", full_jid); stream_id = gabble_bytestream_factory_generate_stream_id (); @@ -1237,8 +1851,132 @@ 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; +} + + +static gboolean +offer_jingle (GabbleFileTransferChannel *self, const gchar *jid, + const gchar *resource, GError **error) +{ + + GabbleJingleSession *jingle; + GabbleJingleContent *content; + + jingle = gabble_jingle_factory_create_session ( + self->priv->connection->jingle_factory, + self->priv->handle, resource, FALSE); + + if (!jingle) + return FALSE; + + g_object_set (jingle, "dialect", JINGLE_DIALECT_GTALK4, NULL); + + content = gabble_jingle_session_add_content (jingle, + JINGLE_MEDIA_TYPE_FILE, "share", NS_GOOGLE_SESSION_SHARE, + NS_GOOGLE_TRANSPORT_P2P); + + if (content) + { + g_object_set (content, + "filename", self->priv->filename, + "filesize", self->priv->size, + NULL); + } + else + { + g_object_unref (jingle); + return FALSE; + } + + gabble_file_transfer_channel_set_jingle_session (self, jingle); + + gabble_jingle_session_accept (self->priv->jingle); + + 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; + const gchar *resource = NULL; + + g_assert (!CHECK_STR_EMPTY (self->priv->filename)); + g_assert (self->priv->size != GABBLE_UNDEFINED_FILE_SIZE); + g_assert (self->priv->bytestream == NULL && self->priv->jingle == NULL); + + presence = gabble_presence_cache_get (self->priv->connection->presence_cache, + self->priv->handle); + if (presence == NULL) + { + DEBUG ("can't find contact's presence"); + if (error != NULL) + 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 */ + resource = gabble_presence_pick_resource_by_caps (presence, + gabble_capability_set_predicate_has, NS_FILE_TRANSFER); + + if (resource == NULL) + { + resource = gabble_presence_pick_resource_by_caps (presence, + gabble_capability_set_predicate_has, NS_GOOGLE_FEAT_SHARE); + if (resource == NULL) + { + DEBUG ("contact doesn't have file transfer capabilities"); + if (error != NULL) + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_CAPABLE, + "contact doesn't have file transfer capabilities"); + + return FALSE; + } + else + { + si = FALSE; + } + } + else + { + si = TRUE; + } + } + else + { + /* MUC jid, we already have the full jid */ + } + + DEBUG ("Offering file transfer to %s%s%s", jid, + resource? "/":"", resource? resource:""); + + if (si) + result = offer_bytestream (self, jid, resource, error); + else + result = offer_jingle (self, jid, resource, error); return result; } @@ -1327,6 +2065,290 @@ transferred_chunk (GabbleFileTransferChannel *self, emit_progress_update_cb, self); } +static void +send_transport_data (GabbleFileTransferChannel *self, + JingleChannel *channel, const guint8 *data, guint data_len) +{ + GError *error = NULL; + if (!gibber_transport_send (self->priv->transport, data, data_len, &error)) + { + DEBUG ("sending to transport failed: %s", error->message); + g_error_free (error); + + 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); + } + else + { + transferred_chunk (self, (guint64) data_len); + + if (!gibber_transport_buffer_is_empty (self->priv->transport)) + { + /* We don't want to send more data while the buffer isn't empty */ + /* FIXME: what if we miss TCP syn/ack messages and cause longer + timeouts because rto becomes big ? */ + nice_agent_attach_recv (channel->agent, channel->stream_id, + channel->component_id, NULL, NULL, NULL); + channel->agent_attached = FALSE; + } + } +} + +/* Return the pointer at the end of the line or NULL if not \n found */ +static gchar * +http_read_line (gchar *buffer, guint len) +{ + gchar *p = buffer; + + while ((guint) (p - buffer) < len && *p != '\n') + p++; + + if ((guint) (p - buffer) >= len) + return NULL; + + *p = 0; + if (p > buffer && *(p-1) == '\r') + *(p-1) = 0; + p++; + + return p; +} + +static guint +http_data_received (GabbleFileTransferChannel *self, JingleChannel *channel, + gchar *buffer, guint len) +{ + + switch (channel->http_status) + { + case HTTP_SERVER_IDLE: + { + gchar *headers = http_read_line (buffer, len); + if (headers == NULL) + return 0; + + channel->http_status = HTTP_SERVER_HEADERS; + + 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 (%d) : %s", strlen (line), line); + if (*line == 0) + { + gchar *response = NULL; + /* FIXME: how about content-length and an actual body ? */ + channel->http_status = HTTP_SERVER_SEND; + DEBUG ("Found empty line, now sending our response"); + + /* FIXME: actually check for the requested file's uri */ + /* FIXME: check for success of nice_agent_send */ + response = g_strdup_printf ("HTTP/1.1 200\r\n" + "Connection: Keep-Alive\r\n" + "Content-Length: %llu\r\n" + "Content-Type: application/octet-stream\r\n\r\n", + self->priv->size - self->priv->initial_offset); + nice_agent_send (channel->agent, channel->stream_id, + channel->component_id, strlen (response), response); + g_free (response); + gibber_transport_block_receiving (self->priv->transport, + FALSE); + } + + 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; + + channel->http_status = HTTP_CLIENT_HEADERS; + + 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 (%d) : %s", strlen (line), line); + if (*line == 0) + { + DEBUG ("Found empty line, now receiving file data"); + if (channel->is_chunked) + { + channel->http_status = HTTP_CLIENT_CHUNK_SIZE; + } + else + { + channel->http_status = HTTP_CLIENT_BODY; + if (channel->content_length == 0) + get_next_manifest_entry (self, channel); + } + } + else if (!g_ascii_strncasecmp (line, "Content-Length: ", 16)) + { + channel->is_chunked = FALSE; + /* Check strtoull read all the length */ + channel->content_length = g_ascii_strtoull (line + 16, + NULL, 10); + DEBUG ("Found data length : %llu", channel->content_length); + } + else if (!g_ascii_strncasecmp (line, + "Transfer-Encoding: chunked", 26)) + { + channel->is_chunked = TRUE; + 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 */ + channel->content_length = strtoul (line, NULL, 16); + if (channel->content_length > 0) + channel->http_status = HTTP_CLIENT_BODY; + else + channel->http_status = HTTP_CLIENT_CHUNK_FINAL; + + /* Reset the size of the file as we receive more data than expected */ + if (self->priv->transferred_bytes + self->priv->initial_offset + + channel->content_length >= self->priv->size) + self->priv->size = self->priv->transferred_bytes + \ + self->priv->initial_offset + channel->content_length; + + return next_line - buffer; + } + break; + case HTTP_CLIENT_BODY: + { + guint consumed = 0; + + if (len >= channel->content_length) + { + consumed = channel->content_length; + send_transport_data (self, channel, (const guint8 *) buffer, + channel->content_length); + channel->content_length = 0; + if (channel->is_chunked) + channel->http_status = HTTP_CLIENT_CHUNK_END; + else + get_next_manifest_entry (self, channel); + } + else + { + consumed = len; + channel->content_length -= len; + send_transport_data (self, channel, (const guint8 *) buffer, len); + } + + return consumed; + } + break; + case HTTP_CLIENT_CHUNK_END: + { + gchar *chunk = http_read_line (buffer, len); + if (chunk == NULL) + return 0; + + 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; + + channel->http_status = HTTP_CLIENT_IDLE; + get_next_manifest_entry (self, channel); + + 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) +{ + GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data); + JingleChannel *channel = get_jingle_channel (self, agent); + gchar *free_buffer = NULL; + + + if (channel->read_buffer != NULL) + { + gchar *tmp = g_malloc (channel->read_len + len); + memcpy (tmp, channel->read_buffer, channel->read_len); + memcpy (tmp + channel->read_len, buffer, len); + + free_buffer = buffer = tmp; + len += channel->read_len; + + g_free (channel->read_buffer); + channel->read_buffer = NULL; + channel->read_len = 0; + } + while (len > 0) + { + guint consumed = http_data_received (self, channel, buffer, len); + + if (consumed == 0) + { + channel->read_buffer = g_memdup (buffer, len); + channel->read_len = len; + break; + } + else + { + /* we assume http_data_received never returns consumed > len */ + len -= consumed; + buffer += consumed; + } + } + + if (free_buffer != NULL) + g_free (free_buffer); + +} static void data_received_cb (GabbleBytestreamIface *stream, @@ -1475,17 +2497,49 @@ 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) + { + gabble_signal_connect_weak (self->priv->bytestream, "data-received", + G_CALLBACK (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); + + /* 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->jingle) + { + GList *cs = gabble_jingle_session_get_contents (self->priv->jingle); + + if (cs != NULL) + { + GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (cs->data); + guint initial_id = 0; + gint channel_id; + /* The new-channel signal will take care of the rest.. */ + do + { + gchar *channel_name = NULL; - /* Block the bytestream while the user is not connected to the socket */ - gabble_bytestream_iface_block_reading (self->priv->bytestream, TRUE); + channel_name = g_strdup_printf ("gabble-%d", ++initial_id); + channel_id = gabble_jingle_content_create_channel (content, + channel_name); + g_free (channel_name); + } while (channel_id <= 0 && initial_id < 10); - /* channel state will change to open once the bytestream is open */ - gabble_bytestream_iface_accept (self->priv->bytestream, augment_si_reply, - self); + /* FIXME: not assert but actually cancel the FT? */ + g_assert (channel_id > 0); + } + gabble_jingle_session_accept (self->priv->jingle); + } + else + { + g_assert_not_reached (); + } } /** @@ -1619,28 +2673,53 @@ 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) { - DEBUG ("Sending failed. Closing the bytestream"); - close_bytestrean_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->jingle) + { + JingleChannel *channel = g_hash_table_lookup (self->priv->jingle_channels, + GINT_TO_POINTER (1)); + gint ret = nice_agent_send (channel->agent, channel->stream_id, + channel->component_id, data->length, (const gchar *) data->data); + + if (ret < 0 || (guint) ret < data->length) + { + if (ret < 0) + ret = 0; + channel->write_buffer = g_memdup (data->data + ret, + data->length - ret); + channel->write_len = data->length - ret; + gibber_transport_block_receiving (self->priv->transport, TRUE); + } + } transferred_chunk (self, (guint64) data->length); - if (self->priv->transferred_bytes + self->priv->initial_offset >= - self->priv->size) + /* nothing to do here for jingle.. wait until the other side requests + another file (hypothetically) or sends the info before setting + out state to COMPLETED */ + if (self->priv->bytestream) { - DEBUG ("All the file has been sent. Closing the bytestream"); + 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_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; + gabble_bytestream_iface_close (self->priv->bytestream, NULL); + } } } @@ -1655,17 +2734,51 @@ bytestream_write_blocked_cb (GabbleBytestreamIface *bytestream, static void file_transfer_send (GabbleFileTransferChannel *self) { - gabble_signal_connect_weak (self->priv->bytestream, "write-blocked", - G_CALLBACK (bytestream_write_blocked_cb), G_OBJECT (self)); - gibber_transport_set_handler (self->priv->transport, transport_handler, self); + if (self->priv->bytestream) + { + gabble_signal_connect_weak (self->priv->bytestream, "write-blocked", + G_CALLBACK (bytestream_write_blocked_cb), G_OBJECT (self)); + gibber_transport_set_handler (self->priv->transport, transport_handler, + self); + } + else + { + JingleChannel *channel = g_hash_table_lookup (self->priv->jingle_channels, + GINT_TO_POINTER (1)); + gibber_transport_set_handler (self->priv->transport, transport_handler, + self); + if (channel == NULL || channel->http_status != HTTP_SERVER_SEND) + { + gibber_transport_block_receiving (self->priv->transport, TRUE); + } + else + { + gibber_transport_block_receiving (self->priv->transport, FALSE); + } + } } 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) + { + g_assert (self->priv->bytestream != NULL); + gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE); + } + else if (self->priv->jingle) + { + JingleChannel *channel = g_hash_table_lookup (self->priv->jingle_channels, + GINT_TO_POINTER (1)); + if (channel && !channel->agent_attached) + { + channel->agent_attached = TRUE; + nice_agent_attach_recv (channel->agent, channel->stream_id, + channel->component_id, g_main_context_default (), + nice_data_received_cb, self); + } + } } static void @@ -1674,14 +2787,16 @@ transport_disconnected_cb (GibberTransport *transport, { DEBUG ("transport to local socket has been disconnected"); - if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED) + if (self->priv->transferred_bytes + self->priv->initial_offset < + self->priv->size) { - close_bytestrean_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); } } @@ -1690,7 +2805,21 @@ 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) + gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE); + + if (self->priv->jingle) + { + JingleChannel *channel = g_hash_table_lookup (self->priv->jingle_channels, + GINT_TO_POINTER (1)); + if (channel && !channel->agent_attached) + { + channel->agent_attached = TRUE; + nice_agent_attach_recv (channel->agent, channel->stream_id, + channel->component_id, g_main_context_default (), + nice_data_received_cb, self); + } + } if (self->priv->state > TP_FILE_TRANSFER_STATE_OPEN) gibber_transport_disconnect (transport); @@ -1836,8 +2965,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; } @@ -1855,7 +2984,6 @@ gabble_file_transfer_channel_new (GabbleConnection *conn, const gchar *description, guint64 date, guint64 initial_offset, - GabbleBytestreamIface *bytestream, gboolean resume_supported) { @@ -1872,7 +3000,6 @@ gabble_file_transfer_channel_new (GabbleConnection *conn, "description", description, "date", date, "initial-offset", initial_offset, - "bytestream", bytestream, "resume-supported", resume_supported, NULL); } diff --git a/src/ft-channel.h b/src/ft-channel.h index 2bd339d..eb38cb2 100644 --- a/src/ft-channel.h +++ b/src/ft-channel.h @@ -68,7 +68,16 @@ 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); + +gboolean gabble_file_transfer_channel_set_bytestream ( + GabbleFileTransferChannel *self, + GabbleBytestreamIface *bytestream); + +gboolean gabble_file_transfer_channel_set_jingle_session ( + GabbleFileTransferChannel *self, + GabbleJingleSession *session); + gboolean gabble_file_transfer_channel_offer_file ( GabbleFileTransferChannel *self, GError **error); diff --git a/src/ft-manager.c b/src/ft-manager.c index 5ab7002..2fa6814 100644 --- a/src/ft-manager.c +++ b/src/ft-manager.c @@ -28,6 +28,8 @@ #include #include +#include "jingle-session.h" +#include "jingle-share.h" #include "caps-channel-manager.h" #include "connection.h" #include "ft-manager.h" @@ -80,6 +82,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 +96,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 +151,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,6 +167,22 @@ 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); +} + + void gabble_ft_manager_dispose (GObject *object) { @@ -277,6 +302,73 @@ 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); + GList *cs; + + if (gabble_jingle_session_get_content_type (sess) != + GABBLE_TYPE_JINGLE_SHARE) + return; + + cs = gabble_jingle_session_get_contents (sess); + + if (cs != NULL) + { + GabbleJingleShare *c = GABBLE_JINGLE_SHARE (cs->data); + const gchar *filename; + guint64 size; + GabbleFileTransferChannel *chan; + + g_object_get (c, + "filename", &filename, + "filesize", &size, + NULL); + + chan = gabble_file_transfer_channel_new (self->priv->connection, + sess->peer, sess->peer, TP_FILE_TRANSFER_STATE_PENDING, + NULL, filename, size, TP_FILE_HASH_TYPE_NONE, NULL, + NULL, 0, 0, FALSE); + + gabble_file_transfer_channel_set_jingle_session (chan, sess); + + gabble_ft_manager_channel_created (self, chan, NULL); + + g_list_free (cs); + } + +} + + +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: + 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 +491,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); if (!gabble_file_transfer_channel_offer_file (chan, &error)) { @@ -555,7 +647,9 @@ 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); + + gabble_file_transfer_channel_set_bytestream (chan, bytestream); gabble_ft_manager_channel_created (self, chan, NULL); } @@ -672,6 +766,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/jingle-content.c b/src/jingle-content.c index e3e306c..88251d9 100644 --- a/src/jingle-content.c +++ b/src/jingle-content.c @@ -33,8 +33,10 @@ #include "jingle-factory.h" #include "jingle-session.h" #include "jingle-transport-iface.h" +#include "jingle-transport-google.h" #include "namespaces.h" #include "util.h" +#include "gabble-signals-marshal.h" /* signal enum */ enum @@ -42,6 +44,8 @@ enum READY, NEW_CANDIDATES, REMOVED, + NEW_CHANNEL, + COMPLETED, LAST_SIGNAL }; @@ -83,6 +87,7 @@ struct _GabbleJingleContentPrivate gboolean have_local_candidates; guint gtalk4_event_id; + guint last_channel_component_id; gboolean dispose_has_run; }; @@ -96,6 +101,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 +243,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: @@ -346,6 +354,27 @@ gabble_jingle_content_class_init (GabbleJingleContentClass *cls) NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); + signals[NEW_CHANNEL] = g_signal_new ( + "new-channel", + G_TYPE_FROM_CLASS (cls), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + gabble_marshal_VOID__STRING_INT, + G_TYPE_NONE, + 2, + G_TYPE_STRING, G_TYPE_INT); + + 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'. @@ -408,6 +437,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) { @@ -446,17 +486,24 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c, GType transport_type = 0; GabbleJingleTransportIface *trans = NULL; JingleDialect dialect = gabble_jingle_session_get_dialect (c->session); + JingleMediaType media_type; desc_node = lm_message_node_get_child_any_ns (content_node, "description"); trans_node = lm_message_node_get_child_any_ns (content_node, "transport"); creator = lm_message_node_get_attribute (content_node, "creator"); name = lm_message_node_get_attribute (content_node, "name"); senders = lm_message_node_get_attribute (content_node, "senders"); + g_object_get (c, "media-type", &media_type, NULL); g_assert (priv->transport_ns == NULL); if (senders == NULL) - senders = "both"; + { + if (media_type == JINGLE_MEDIA_TYPE_FILE) + senders = "initiator"; + else + senders = "both"; + } if (google_mode) { @@ -563,6 +610,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); @@ -579,6 +627,98 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c, return; } +static guint +new_channel (GabbleJingleContent *c, const gchar *name) +{ + GabbleJingleContentPrivate *priv = c->priv; + GabbleJingleTransportGoogle *gtrans = NULL; + + if (priv->transport && + GABBLE_IS_JINGLE_TRANSPORT_GOOGLE (priv->transport)) + { + gtrans = GABBLE_JINGLE_TRANSPORT_GOOGLE (priv->transport); + if (jingle_transport_google_set_component_name (gtrans, name, + priv->last_channel_component_id + 1) == FALSE) + return 0; + + priv->last_channel_component_id++; + + g_signal_emit (c, signals[NEW_CHANNEL], 0, + name, priv->last_channel_component_id); + + return priv->last_channel_component_id; + } + return 0; +} + +guint +gabble_jingle_content_create_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 Gtalk4 '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_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 Gtalk4 '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"); + DEBUG ("Channel name is %s", name); + if (name) + new_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) @@ -588,12 +728,15 @@ gabble_jingle_content_parse_accept (GabbleJingleContent *c, LmMessageNode *trans_node, *desc_node; JingleDialect dialect = gabble_jingle_session_get_dialect (c->session); JingleContentSenders newsenders; + JingleMediaType media_type; desc_node = lm_message_node_get_child_any_ns (content_node, "description"); trans_node = lm_message_node_get_child_any_ns (content_node, "transport"); senders = lm_message_node_get_attribute (content_node, "senders"); + g_object_get (c, "media-type", &media_type, NULL); - if (JINGLE_IS_GOOGLE_DIALECT (dialect) && trans_node == NULL) + if (media_type != JINGLE_MEDIA_TYPE_FILE && + JINGLE_IS_GOOGLE_DIALECT (dialect) && trans_node == NULL) { DEBUG ("no transport node, assuming GTalk3 dialect"); /* gtalk lj0.3 assumes google-p2p transport */ @@ -601,7 +744,12 @@ gabble_jingle_content_parse_accept (GabbleJingleContent *c, } if (senders == NULL) - senders = "both"; + { + if (media_type == JINGLE_MEDIA_TYPE_FILE) + senders = "initiator"; + else + senders = "both"; + } newsenders = parse_senders (senders); if (newsenders == JINGLE_CONTENT_SENDERS_NONE) @@ -612,7 +760,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), + senders); priv->senders = newsenders; g_object_notify ((GObject *) c, "senders"); } @@ -771,22 +920,24 @@ gboolean gabble_jingle_content_is_ready (GabbleJingleContent *self) { GabbleJingleContentPrivate *priv = self->priv; + JingleMediaType media_type; + g_object_get (self, "media-type", &media_type, NULL); if (priv->created_by_us) { /* 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 && + (media_type == JINGLE_MEDIA_TYPE_FILE || 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 && + (media_type == JINGLE_MEDIA_TYPE_FILE || + gabble_jingle_transport_iface_can_accept (priv->transport))) return TRUE; } diff --git a/src/jingle-content.h b/src/jingle-content.h index 2efe47a..8ec9b94 100644 --- a/src/jingle-content.h +++ b/src/jingle-content.h @@ -31,7 +31,8 @@ G_BEGIN_DECLS typedef enum { JINGLE_MEDIA_TYPE_NONE = 0, JINGLE_MEDIA_TYPE_AUDIO, - JINGLE_MEDIA_TYPE_VIDEO + JINGLE_MEDIA_TYPE_VIDEO, + JINGLE_MEDIA_TYPE_FILE } JingleMediaType; typedef enum { @@ -85,6 +86,8 @@ struct _GabbleJingleContentClass { void (*parse_description) (GabbleJingleContent *, LmMessageNode *, GError **); void (*produce_description) (GabbleJingleContent *, LmMessageNode *); + void (*transport_created) (GabbleJingleContent *, + GabbleJingleTransportIface *); }; 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_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 a32c472..6046fec 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" @@ -558,6 +559,7 @@ gabble_jingle_factory_constructor (GType type, g_signal_connect (priv->conn, "status-changed", (GCallback) connection_status_changed_cb, self); + jingle_share_register (self); jingle_media_rtp_register (self); jingle_transport_google_register (self); jingle_transport_rawudp_register (self); @@ -704,13 +706,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, resource, sid); } diff --git a/src/jingle-factory.h b/src/jingle-factory.h index 2ca3854..0c7deae 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 378ba63..86c8f8f 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); @@ -229,6 +230,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) @@ -245,6 +248,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.", @@ -265,6 +269,31 @@ 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; + + if (GABBLE_IS_JINGLE_TRANSPORT_GOOGLE (transport)) + { + gtrans = GABBLE_JINGLE_TRANSPORT_GOOGLE (transport); + + if (priv->media_type == JINGLE_MEDIA_TYPE_AUDIO) + { + jingle_transport_google_set_component_name (gtrans, "rtp", 1); + jingle_transport_google_set_component_name (gtrans, "rtcp", 2); + } + else if (priv->media_type == JINGLE_MEDIA_TYPE_VIDEO) + { + jingle_transport_google_set_component_name (gtrans, "video_rtp", 1); + jingle_transport_google_set_component_name (gtrans, "video_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 d67ca67..d7b0b54 100644 --- a/src/jingle-session.c +++ b/src/jingle-session.c @@ -106,7 +106,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] = { @@ -116,24 +116,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 } @@ -156,7 +159,8 @@ 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 || @@ -529,6 +533,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; } @@ -569,6 +575,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); @@ -1411,6 +1419,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 */ @@ -1426,7 +1454,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 @@ -1881,7 +1910,10 @@ try_session_initiate_or_accept (GabbleJingleSession *sess) _map_initial_contents (sess, _check_content_ready, &contents_ready); if (!contents_ready) + { + DEBUG ("Contents not yet ready, not initiating/accepting now.."); return; + } msg = gabble_jingle_session_new_message (sess, action, &sess_node); @@ -2225,6 +2257,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 628a7b9..766a465 100644 --- a/src/jingle-session.h +++ b/src/jingle-session.h @@ -112,6 +112,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 0000000..43ba39b --- /dev/null +++ b/src/jingle-share.c @@ -0,0 +1,483 @@ +/* + * jingle-share.c - Source 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 + */ + +/* 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 +#include +#include +#include + +#include + +#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" + +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 buf[sizeof (guint32) * 2]; + guint32 *uint_buf = (guint32 *) buf; + guint i; + + for (i = 0; i < sizeof (buf); i++) + buf[i] = g_random_int_range (0, 256); + + return g_strdup_printf ("/temporary/%x%x/", uint_buf[0], uint_buf[1]); +} + +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 +gen_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_FILE); + 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 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; + + /* 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", + "Media type.", + JINGLE_MEDIA_TYPE_NONE, G_MAXUINT32, 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; + + 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 = atoi (width); + + height =lm_message_node_get_attribute (image, "height"); + if (height) + m->image_height = atoi (height); + } + 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"); + + gen_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 ("%llu", 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) +{ + gen_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 0000000..7f4414c --- /dev/null +++ b/src/jingle-share.h @@ -0,0 +1,88 @@ +/* + * 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 +#include +#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; + GList *entries; +} GabbleJingleShareManifest; + +const gchar *gabble_jingle_share_parse (GabbleJingleShare *sess, + LmMessage *message, GError **error); +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 4472e8c..d563278 100644 --- a/src/jingle-transport-google.c +++ b/src/jingle-transport-google.c @@ -68,6 +68,7 @@ struct _GabbleJingleTransportGooglePrivate GabbleJingleContent *content; JingleTransportState state; gchar *transport_ns; + GHashTable *channels; /* Google transport 'channels', not TP channels */ GList *local_candidates; @@ -88,6 +89,8 @@ gabble_jingle_transport_google_init (GabbleJingleTransportGoogle *obj) GabbleJingleTransportGooglePrivate); obj->priv = priv; + priv->channels = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + priv->dispose_has_run = FALSE; } @@ -103,6 +106,9 @@ gabble_jingle_transport_google_dispose (GObject *object) DEBUG ("dispose called"); priv->dispose_has_run = TRUE; + g_hash_table_destroy (priv->channels); + priv->channels = NULL; + jingle_transport_free_candidates (priv->remote_candidates); priv->remote_candidates = NULL; @@ -233,11 +239,9 @@ parse_candidates (GabbleJingleTransportIface *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)) { @@ -256,32 +260,14 @@ parse_candidates (GabbleJingleTransportIface *obj, if (name == NULL) break; - if (g_str_has_prefix (name, "video_")) + if (g_hash_table_lookup_extended (priv->channels, name, + NULL, NULL) == FALSE) { - 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 (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 ("Couldn't find name %s in channels", name); + break; } + component = GPOINTER_TO_INT (g_hash_table_lookup (priv->channels, name)); address = lm_message_node_get_attribute (node, "address"); if (address == NULL) break; @@ -485,40 +471,62 @@ 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->channels); + 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 */ @@ -608,6 +616,24 @@ 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, gint component_id) +{ + GabbleJingleTransportGooglePrivate *priv = transport->priv; + + if (g_hash_table_lookup_extended (priv->channels, name, + NULL, NULL) == TRUE) + return FALSE; + + g_hash_table_insert (priv->channels, 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 5cb7d02..3fc748f 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, gint component_id); + #endif /* __JINGLE_TRANSPORT_GOOGLE_H__ */ diff --git a/src/namespaces.h b/src/namespaces.h index 50c6e32..4759274 100644 --- a/src/namespaces.h +++ b/src/namespaces.h @@ -32,6 +32,7 @@ #define NS_GABBLE_CAPS "http://telepathy.freedesktop.org/caps" #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://www.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" @@ -68,6 +69,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 bb7b8c8..5b71cfa 100644 --- a/src/types.h +++ b/src/types.h @@ -51,6 +51,7 @@ typedef struct _GabbleJingleTransportGoogle GabbleJingleTransportGoogle; typedef struct _GabbleJingleTransportRawUdp GabbleJingleTransportRawUdp; typedef struct _GabbleJingleTransportIceUdp GabbleJingleTransportIceUdp; typedef struct _GabbleJingleMediaRtp GabbleJingleMediaRtp; +typedef struct _GabbleJingleShare GabbleJingleShare; typedef struct _JingleCandidate JingleCandidate;