2019-06-23 12:53:18 +00:00
|
|
|
using Gee;
|
|
|
|
using Xmpp;
|
|
|
|
using Xmpp.Xep;
|
|
|
|
|
|
|
|
namespace Xmpp.Xep.JingleFileTransfer {
|
|
|
|
|
|
|
|
private const string NS_URI = "urn:xmpp:jingle:apps:file-transfer:5";
|
|
|
|
|
2019-06-23 12:51:33 +00:00
|
|
|
public class Module : Jingle.ContentType, XmppStreamModule {
|
2021-03-19 21:46:39 +00:00
|
|
|
|
|
|
|
public signal void file_incoming(XmppStream stream, FileTransfer file_transfer);
|
|
|
|
|
2019-06-23 12:53:18 +00:00
|
|
|
public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0234_jingle_file_transfer");
|
2021-03-19 21:46:39 +00:00
|
|
|
public SessionInfoType session_info_type = new SessionInfoType();
|
2019-06-23 12:53:18 +00:00
|
|
|
|
|
|
|
public override void attach(XmppStream stream) {
|
|
|
|
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
|
2019-06-23 12:51:33 +00:00
|
|
|
stream.get_module(Jingle.Module.IDENTITY).register_content_type(this);
|
2021-03-19 21:46:39 +00:00
|
|
|
stream.get_module(Jingle.Module.IDENTITY).register_session_info_type(session_info_type);
|
2019-06-23 12:53:18 +00:00
|
|
|
}
|
2020-04-21 14:25:21 +00:00
|
|
|
public override void detach(XmppStream stream) {
|
|
|
|
stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI);
|
|
|
|
}
|
2019-06-23 12:53:18 +00:00
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
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; } }
|
|
|
|
|
2019-06-23 12:51:33 +00:00
|
|
|
public Jingle.ContentParameters parse_content_parameters(StanzaNode description) throws Jingle.IqError {
|
|
|
|
return Parameters.parse(this, description);
|
|
|
|
}
|
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
public Jingle.ContentParameters create_content_parameters(Object object) throws Jingle.IqError {
|
|
|
|
assert_not_reached();
|
|
|
|
}
|
2019-06-23 12:51:33 +00:00
|
|
|
|
2020-07-03 19:14:39 +00:00
|
|
|
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);
|
2019-06-23 12:53:18 +00:00
|
|
|
if (has_feature == null || !(!)has_feature) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-03-19 21:46:39 +00:00
|
|
|
return yield stream.get_module(Jingle.Module.IDENTITY).is_available(stream, required_transport_type, required_components, full_jid);
|
2019-06-23 12:53:18 +00:00
|
|
|
}
|
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
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 {
|
2019-09-10 18:56:00 +00:00
|
|
|
StanzaNode file_node;
|
2019-06-23 12:53:18 +00:00
|
|
|
StanzaNode description = new StanzaNode.build("description", NS_URI)
|
|
|
|
.add_self_xmlns()
|
2019-09-10 18:56:00 +00:00
|
|
|
.put_node(file_node = new StanzaNode.build("file", NS_URI)
|
|
|
|
.put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(basename))));
|
2019-06-23 12:53:18 +00:00
|
|
|
// TODO(hrxi): Add the mandatory hash field
|
|
|
|
|
2019-09-10 18:56:00 +00:00
|
|
|
if (size > 0) {
|
|
|
|
file_node.put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(size.to_string())));
|
|
|
|
} else {
|
|
|
|
warning("Sending file %s without size, likely going to cause problems down the road...", basename);
|
|
|
|
}
|
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
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");
|
2019-09-01 16:18:25 +00:00
|
|
|
}
|
2021-03-19 21:46:39 +00:00
|
|
|
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<Jingle.Content> contents = new ArrayList<Jingle.Content>();
|
|
|
|
contents.add(content);
|
|
|
|
|
2019-06-23 12:53:18 +00:00
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
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;
|
2019-06-23 12:51:33 +00:00
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
// 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)");
|
|
|
|
}
|
2019-06-23 12:53:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public override string get_ns() { return NS_URI; }
|
|
|
|
public override string get_id() { return IDENTITY.id; }
|
|
|
|
}
|
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
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)");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-06-23 12:51:33 +00:00
|
|
|
public class Parameters : Jingle.ContentParameters, Object {
|
2019-07-18 00:03:42 +00:00
|
|
|
|
2019-06-23 12:51:33 +00:00
|
|
|
Module parent;
|
|
|
|
string? media_type;
|
|
|
|
public string? name { get; private set; }
|
|
|
|
public int64 size { get; private set; }
|
|
|
|
public StanzaNode original_description { get; private set; }
|
2019-07-18 00:03:42 +00:00
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
public Parameters(Module parent, StanzaNode original_description, string? media_type, string? name, int64 size) {
|
2019-06-23 12:51:33 +00:00
|
|
|
this.parent = parent;
|
|
|
|
this.original_description = original_description;
|
|
|
|
this.media_type = media_type;
|
|
|
|
this.name = name;
|
|
|
|
this.size = size;
|
2019-06-23 12:53:18 +00:00
|
|
|
}
|
2019-07-18 00:03:42 +00:00
|
|
|
|
2019-06-23 12:51:33 +00:00
|
|
|
public static Parameters parse(Module parent, StanzaNode description) throws Jingle.IqError {
|
|
|
|
Gee.List<StanzaNode> files = description.get_subnodes("file", NS_URI);
|
|
|
|
if (files.size != 1) {
|
|
|
|
throw new Jingle.IqError.BAD_REQUEST("there needs to be exactly one file node");
|
|
|
|
}
|
|
|
|
StanzaNode file = files[0];
|
|
|
|
StanzaNode? media_type_node = file.get_subnode("media-type", NS_URI);
|
|
|
|
StanzaNode? name_node = file.get_subnode("name", NS_URI);
|
|
|
|
StanzaNode? size_node = file.get_subnode("size", NS_URI);
|
|
|
|
string? media_type = media_type_node != null ? media_type_node.get_string_content() : null;
|
|
|
|
string? name = name_node != null ? name_node.get_string_content() : null;
|
|
|
|
string? size_raw = size_node != null ? size_node.get_string_content() : null;
|
|
|
|
// TODO(hrxi): For some reason, the ?:-expression does not work due to a type error.
|
|
|
|
//int64? size = size_raw != null ? int64.parse(size_raw) : null; // TODO(hrxi): this has no error handling
|
2019-07-22 19:35:29 +00:00
|
|
|
if (size_raw == null) {
|
|
|
|
// Jingle file transfers (XEP-0234) theoretically SHOULD send a
|
|
|
|
// file size, however, we do require it in order to reliably find
|
|
|
|
// the end of the file transfer.
|
|
|
|
throw new Jingle.IqError.BAD_REQUEST("file offer without file size");
|
|
|
|
}
|
|
|
|
int64 size = int64.parse(size_raw);
|
|
|
|
if (size < 0) {
|
|
|
|
throw new Jingle.IqError.BAD_REQUEST("negative file size is invalid");
|
2019-06-23 12:53:18 +00:00
|
|
|
}
|
2019-06-23 12:51:33 +00:00
|
|
|
|
|
|
|
return new Parameters(parent, description, media_type, name, size);
|
|
|
|
}
|
2019-07-18 00:03:42 +00:00
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
public StanzaNode get_description_node() {
|
|
|
|
return original_description;
|
2019-06-23 12:53:18 +00:00
|
|
|
}
|
2021-03-19 21:46:39 +00:00
|
|
|
|
|
|
|
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) { }
|
2019-06-23 12:53:18 +00:00
|
|
|
}
|
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
// Does nothing except wrapping an input stream to signal EOF after reading
|
|
|
|
// `max_size` bytes.
|
|
|
|
private class FileTransferInputStream : InputStream {
|
2021-03-19 21:46:39 +00:00
|
|
|
|
|
|
|
public signal void closed();
|
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
InputStream inner;
|
|
|
|
int64 remaining_size;
|
2021-03-19 21:46:39 +00:00
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
public FileTransferInputStream(InputStream inner, int64 max_size) {
|
|
|
|
this.inner = inner;
|
|
|
|
this.remaining_size = max_size;
|
|
|
|
}
|
2021-03-19 21:46:39 +00:00
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
private ssize_t update_remaining(ssize_t read) {
|
|
|
|
this.remaining_size -= read;
|
|
|
|
return read;
|
|
|
|
}
|
2021-03-19 21:46:39 +00:00
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
public override ssize_t read(uint8[] buffer_, Cancellable? cancellable = null) throws IOError {
|
|
|
|
unowned uint8[] buffer = buffer_;
|
|
|
|
if (remaining_size <= 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
if (buffer.length > remaining_size) {
|
|
|
|
buffer = buffer[0:remaining_size];
|
|
|
|
}
|
|
|
|
return update_remaining(inner.read(buffer, cancellable));
|
|
|
|
}
|
2021-03-19 21:46:39 +00:00
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
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) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
if (buffer.length > remaining_size) {
|
|
|
|
buffer = buffer[0:remaining_size];
|
|
|
|
}
|
|
|
|
return update_remaining(yield inner.read_async(buffer, io_priority, cancellable));
|
|
|
|
}
|
2021-03-19 21:46:39 +00:00
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
public override bool close(Cancellable? cancellable = null) throws IOError {
|
2021-03-19 21:46:39 +00:00
|
|
|
closed();
|
2019-07-22 19:35:29 +00:00
|
|
|
return inner.close(cancellable);
|
|
|
|
}
|
2021-03-19 21:46:39 +00:00
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError {
|
2021-03-19 21:46:39 +00:00
|
|
|
closed();
|
2019-07-22 19:35:29 +00:00
|
|
|
return yield inner.close_async(io_priority, cancellable);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-23 12:51:33 +00:00
|
|
|
public class FileTransfer : Object {
|
|
|
|
Jingle.Session session;
|
2021-03-19 21:46:39 +00:00
|
|
|
Jingle.Content content;
|
2019-06-23 12:51:33 +00:00
|
|
|
Parameters parameters;
|
|
|
|
|
|
|
|
public Jid peer { get { return session.peer_full_jid; } }
|
|
|
|
public string? file_name { get { return parameters.name; } }
|
|
|
|
public int64 size { get { return parameters.size; } }
|
2019-09-10 18:56:00 +00:00
|
|
|
public Jingle.SecurityParameters? security { get { return session.security; } }
|
2019-06-23 12:53:18 +00:00
|
|
|
|
2019-07-22 19:35:29 +00:00
|
|
|
public InputStream? stream { get; private set; }
|
2019-06-23 12:53:18 +00:00
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
public FileTransfer(Jingle.Session session, Jingle.Content content, Parameters parameters) {
|
2019-06-23 12:51:33 +00:00
|
|
|
this.session = session;
|
2021-03-19 21:46:39 +00:00
|
|
|
this.content = content;
|
2019-06-23 12:51:33 +00:00
|
|
|
this.parameters = parameters;
|
|
|
|
}
|
2019-06-23 12:53:18 +00:00
|
|
|
|
2021-03-19 21:46:39 +00:00
|
|
|
public async void accept(XmppStream stream) throws IOError {
|
|
|
|
content.accept();
|
|
|
|
|
|
|
|
Jingle.StreamingConnection connection = content.component_connections.values.to_array()[0] as Jingle.StreamingConnection;
|
2021-04-29 13:56:22 +00:00
|
|
|
try {
|
|
|
|
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;
|
|
|
|
} catch (FutureError.EXCEPTION e) {
|
|
|
|
warning("Error accepting Jingle file-transfer: %s", connection.stream.exception.message);
|
|
|
|
} catch (FutureError e) {
|
|
|
|
warning("FutureError accepting Jingle file-transfer: %s", e.message);
|
|
|
|
}
|
2019-06-23 12:51:33 +00:00
|
|
|
}
|
2019-07-18 00:03:42 +00:00
|
|
|
|
2019-06-23 12:51:33 +00:00
|
|
|
public void reject(XmppStream stream) {
|
2021-03-19 21:46:39 +00:00
|
|
|
content.reject();
|
2019-06-23 12:51:33 +00:00
|
|
|
}
|
2019-06-23 12:53:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|