From 2b90fcc39a1079346d6c5e2bfff8987104da737a Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 19 Mar 2021 22:46:39 +0100 Subject: Improve & refactor Jingle base implementation Co-authored-by: Marvin W --- .../src/module/xep/0234_jingle_file_transfer.vala | 162 ++++++++++++++++----- 1 file changed, 122 insertions(+), 40 deletions(-) (limited to 'xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala') diff --git a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala index 1c0323be..07b158bc 100644 --- a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala +++ b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala @@ -7,50 +7,42 @@ namespace Xmpp.Xep.JingleFileTransfer { private const string NS_URI = "urn:xmpp:jingle:apps:file-transfer:5"; public class Module : Jingle.ContentType, XmppStreamModule { + + public signal void file_incoming(XmppStream stream, FileTransfer file_transfer); + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0234_jingle_file_transfer"); + public SessionInfoType session_info_type = new SessionInfoType(); public override void attach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); stream.get_module(Jingle.Module.IDENTITY).register_content_type(this); + stream.get_module(Jingle.Module.IDENTITY).register_session_info_type(session_info_type); } public override void detach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); } - public string content_type_ns_uri() { - return NS_URI; - } - public Jingle.TransportType content_type_transport_type() { - return Jingle.TransportType.STREAMING; - } + public string ns_uri { get { return NS_URI; } } + public Jingle.TransportType required_transport_type { get { return Jingle.TransportType.STREAMING; } } + public uint8 required_components { get { return 1; } } + public Jingle.ContentParameters parse_content_parameters(StanzaNode description) throws Jingle.IqError { return Parameters.parse(this, description); } - public void handle_content_session_info(XmppStream stream, Jingle.Session session, StanzaNode info, Iq.Stanza iq) throws Jingle.IqError { - switch (info.name) { - case "received": - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - break; - case "checksum": - // TODO(hrxi): handle hash - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - break; - default: - throw new Jingle.IqError.UNSUPPORTED_INFO(@"unsupported file transfer info $(info.name)"); - } - } - public signal void file_incoming(XmppStream stream, FileTransfer file_transfer); + public Jingle.ContentParameters create_content_parameters(Object object) throws Jingle.IqError { + assert_not_reached(); + } public async bool is_available(XmppStream stream, Jid full_jid) { bool? has_feature = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); if (has_feature == null || !(!)has_feature) { return false; } - return yield stream.get_module(Jingle.Module.IDENTITY).is_available(stream, Jingle.TransportType.STREAMING, full_jid); + return yield stream.get_module(Jingle.Module.IDENTITY).is_available(stream, required_transport_type, required_components, full_jid); } - public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size, string? precondition_name = null, Object? precondition_options = null) throws IOError { + public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size, string? precondition_name = null, Object? precondition_options = null) throws Jingle.Error { StanzaNode file_node; StanzaNode description = new StanzaNode.build("description", NS_URI) .add_self_xmlns() @@ -64,25 +56,83 @@ public class Module : Jingle.ContentType, XmppStreamModule { warning("Sending file %s without size, likely going to cause problems down the road...", basename); } - Jingle.Session session; - try { - session = yield stream.get_module(Jingle.Module.IDENTITY) - .create_session(stream, Jingle.TransportType.STREAMING, receiver_full_jid, Jingle.Senders.INITIATOR, "a-file-offer", description, precondition_name, precondition_options); // TODO(hrxi): Why "a-file-offer"? - } catch (Jingle.Error e) { - throw new IOError.FAILED(@"couldn't create Jingle session: $(e.message)"); + Parameters parameters = Parameters.parse(this, description); + + Jingle.Module jingle_module = stream.get_module(Jingle.Module.IDENTITY); + + Jingle.Transport? transport = yield jingle_module.select_transport(stream, required_transport_type, required_components, receiver_full_jid, Set.empty()); + if (transport == null) { + throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable transports"); + } + Jingle.SecurityPrecondition? precondition = jingle_module.get_security_precondition(precondition_name); + if (precondition_name != null && precondition == null) { + throw new Jingle.Error.UNSUPPORTED_SECURITY("No suitable security precondiiton found"); } - session.terminate_on_connection_close = false; + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) { + throw new Jingle.Error.GENERAL("Couldn't determine own JID"); + } + Jingle.TransportParameters transport_params = transport.create_transport_parameters(stream, required_components, my_jid, receiver_full_jid); + Jingle.SecurityParameters? security_params = precondition != null ? precondition.create_security_parameters(stream, my_jid, receiver_full_jid, precondition_options) : null; + + Jingle.Content content = new Jingle.Content.initiate_sent("a-file-offer", Jingle.Senders.INITIATOR, + this, parameters, + transport, transport_params, + precondition, security_params, + my_jid, receiver_full_jid); + + ArrayList contents = new ArrayList(); + contents.add(content); + - yield session.conn.input_stream.close_async(); + Jingle.Session? session = null; + try { + session = yield jingle_module.create_session(stream, contents, receiver_full_jid); + + // Wait for the counterpart to accept our offer + ulong content_notify_id = 0; + content_notify_id = content.notify["state"].connect(() => { + if (content.state == Jingle.Content.State.ACCEPTED) { + Idle.add(offer_file_stream.callback); + content.disconnect(content_notify_id); + } + }); + yield; - // TODO(hrxi): catch errors - yield session.conn.output_stream.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); + // Send the file data + Jingle.StreamingConnection connection = content.component_connections.values.to_array()[0] as Jingle.StreamingConnection; + IOStream io_stream = yield connection.stream.wait_async(); + yield io_stream.input_stream.close_async(); + yield io_stream.output_stream.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); + yield connection.terminate(true); + } catch (Jingle.Error e) { + session.terminate(Jingle.ReasonElement.FAILED_TRANSPORT, e.message, e.message); + throw new Jingle.Error.GENERAL(@"couldn't create Jingle session: $(e.message)"); + } } public override string get_ns() { return NS_URI; } public override string get_id() { return IDENTITY.id; } } +public class SessionInfoType : Jingle.SessionInfoNs, Object { + + public string ns_uri { get { return NS_URI; } } + + public void handle_content_session_info(XmppStream stream, Jingle.Session session, StanzaNode info, Iq.Stanza iq) throws Jingle.IqError { + switch (info.name) { + case "received": + break; + case "checksum": + // TODO(hrxi): handle hash + break; + default: + throw new Jingle.IqError.UNSUPPORTED_INFO(@"unsupported file transfer info $(info.name)"); + } + } + +} + public class Parameters : Jingle.ContentParameters, Object { Module parent; @@ -127,24 +177,42 @@ public class Parameters : Jingle.ContentParameters, Object { return new Parameters(parent, description, media_type, name, size); } - public void on_session_initiate(XmppStream stream, Jingle.Session session) { - parent.file_incoming(stream, new FileTransfer(session, this)); + public StanzaNode get_description_node() { + return original_description; } + + public async void handle_proposed_content(XmppStream stream, Jingle.Session session, Jingle.Content content) { + parent.file_incoming(stream, new FileTransfer(session, content, this)); + } + + public void modify(XmppStream stream, Jingle.Session session, Jingle.Content content, Jingle.Senders senders) { } + + public void accept(XmppStream stream, Jingle.Session session, Jingle.Content content) { } + + public void handle_accept(XmppStream stream, Jingle.Session session, Jingle.Content content, StanzaNode description_node) { } + + public void terminate(bool we_terminated, string? reason_name, string? reason_text) { } } // Does nothing except wrapping an input stream to signal EOF after reading // `max_size` bytes. private class FileTransferInputStream : InputStream { + + public signal void closed(); + InputStream inner; int64 remaining_size; + public FileTransferInputStream(InputStream inner, int64 max_size) { this.inner = inner; this.remaining_size = max_size; } + private ssize_t update_remaining(ssize_t read) { this.remaining_size -= read; return read; } + public override ssize_t read(uint8[] buffer_, Cancellable? cancellable = null) throws IOError { unowned uint8[] buffer = buffer_; if (remaining_size <= 0) { @@ -155,6 +223,7 @@ private class FileTransferInputStream : InputStream { } return update_remaining(inner.read(buffer, cancellable)); } + public override async ssize_t read_async(uint8[]? buffer_, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { unowned uint8[] buffer = buffer_; if (remaining_size <= 0) { @@ -165,16 +234,21 @@ private class FileTransferInputStream : InputStream { } return update_remaining(yield inner.read_async(buffer, io_priority, cancellable)); } + public override bool close(Cancellable? cancellable = null) throws IOError { + closed(); return inner.close(cancellable); } + public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { + closed(); return yield inner.close_async(io_priority, cancellable); } } public class FileTransfer : Object { Jingle.Session session; + Jingle.Content content; Parameters parameters; public Jid peer { get { return session.peer_full_jid; } } @@ -184,19 +258,27 @@ public class FileTransfer : Object { public InputStream? stream { get; private set; } - public FileTransfer(Jingle.Session session, Parameters parameters) { + public FileTransfer(Jingle.Session session, Jingle.Content content, Parameters parameters) { this.session = session; + this.content = content; this.parameters = parameters; - this.stream = new FileTransferInputStream(session.conn.input_stream, size); } - public void accept(XmppStream stream) throws IOError { - session.accept(stream, parameters.original_description); - session.conn.output_stream.close(); + public async void accept(XmppStream stream) throws IOError { + content.accept(); + + Jingle.StreamingConnection connection = content.component_connections.values.to_array()[0] as Jingle.StreamingConnection; + IOStream? io_stream = yield connection.stream.wait_async(); + FileTransferInputStream ft_stream = new FileTransferInputStream(io_stream.input_stream, size); + io_stream.output_stream.close(); + ft_stream.closed.connect(() => { + session.terminate(Jingle.ReasonElement.SUCCESS, null, null); + }); + this.stream = ft_stream; } public void reject(XmppStream stream) { - session.reject(stream); + content.reject(); } } -- cgit v1.2.3-70-g09d2