1using Gee; 2using Xmpp; 3using Xmpp.Xep; 4 5namespace Xmpp.Xep.JingleFileTransfer { 6 7private const string NS_URI = "urn:xmpp:jingle:apps:file-transfer:5"; 8 9public class Module : Jingle.ContentType, XmppStreamModule { 10 public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0234_jingle_file_transfer"); 11 12 public override void attach(XmppStream stream) { 13 stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); 14 stream.get_module(Jingle.Module.IDENTITY).register_content_type(this); 15 } 16 public override void detach(XmppStream stream) { 17 stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); 18 } 19 20 public string content_type_ns_uri() { 21 return NS_URI; 22 } 23 public Jingle.TransportType content_type_transport_type() { 24 return Jingle.TransportType.STREAMING; 25 } 26 public Jingle.ContentParameters parse_content_parameters(StanzaNode description) throws Jingle.IqError { 27 return Parameters.parse(this, description); 28 } 29 public void handle_content_session_info(XmppStream stream, Jingle.Session session, StanzaNode info, Iq.Stanza iq) throws Jingle.IqError { 30 switch (info.name) { 31 case "received": 32 stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); 33 break; 34 case "checksum": 35 // TODO(hrxi): handle hash 36 stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); 37 break; 38 default: 39 throw new Jingle.IqError.UNSUPPORTED_INFO(@"unsupported file transfer info $(info.name)"); 40 } 41 } 42 43 public signal void file_incoming(XmppStream stream, FileTransfer file_transfer); 44 45 public async bool is_available(XmppStream stream, Jid full_jid) { 46 bool? has_feature = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); 47 if (has_feature == null || !(!)has_feature) { 48 return false; 49 } 50 return yield stream.get_module(Jingle.Module.IDENTITY).is_available(stream, Jingle.TransportType.STREAMING, full_jid); 51 } 52 53 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 { 54 StanzaNode file_node; 55 StanzaNode description = new StanzaNode.build("description", NS_URI) 56 .add_self_xmlns() 57 .put_node(file_node = new StanzaNode.build("file", NS_URI) 58 .put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(basename)))); 59 // TODO(hrxi): Add the mandatory hash field 60 61 if (size > 0) { 62 file_node.put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(size.to_string()))); 63 } else { 64 warning("Sending file %s without size, likely going to cause problems down the road...", basename); 65 } 66 67 Jingle.Session session; 68 try { 69 session = yield stream.get_module(Jingle.Module.IDENTITY) 70 .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"? 71 } catch (Jingle.Error e) { 72 throw new IOError.FAILED(@"couldn't create Jingle session: $(e.message)"); 73 } 74 session.terminate_on_connection_close = false; 75 76 yield session.conn.input_stream.close_async(); 77 78 // TODO(hrxi): catch errors 79 yield session.conn.output_stream.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); 80 } 81 82 public override string get_ns() { return NS_URI; } 83 public override string get_id() { return IDENTITY.id; } 84} 85 86public class Parameters : Jingle.ContentParameters, Object { 87 88 Module parent; 89 string? media_type; 90 public string? name { get; private set; } 91 public int64 size { get; private set; } 92 public StanzaNode original_description { get; private set; } 93 94 public Parameters(Module parent, StanzaNode original_description, string? media_type, string? name, int64 size) { 95 this.parent = parent; 96 this.original_description = original_description; 97 this.media_type = media_type; 98 this.name = name; 99 this.size = size; 100 } 101 102 public static Parameters parse(Module parent, StanzaNode description) throws Jingle.IqError { 103 Gee.List<StanzaNode> files = description.get_subnodes("file", NS_URI); 104 if (files.size != 1) { 105 throw new Jingle.IqError.BAD_REQUEST("there needs to be exactly one file node"); 106 } 107 StanzaNode file = files[0]; 108 StanzaNode? media_type_node = file.get_subnode("media-type", NS_URI); 109 StanzaNode? name_node = file.get_subnode("name", NS_URI); 110 StanzaNode? size_node = file.get_subnode("size", NS_URI); 111 string? media_type = media_type_node != null ? media_type_node.get_string_content() : null; 112 string? name = name_node != null ? name_node.get_string_content() : null; 113 string? size_raw = size_node != null ? size_node.get_string_content() : null; 114 // TODO(hrxi): For some reason, the ?:-expression does not work due to a type error. 115 //int64? size = size_raw != null ? int64.parse(size_raw) : null; // TODO(hrxi): this has no error handling 116 if (size_raw == null) { 117 // Jingle file transfers (XEP-0234) theoretically SHOULD send a 118 // file size, however, we do require it in order to reliably find 119 // the end of the file transfer. 120 throw new Jingle.IqError.BAD_REQUEST("file offer without file size"); 121 } 122 int64 size = int64.parse(size_raw); 123 if (size < 0) { 124 throw new Jingle.IqError.BAD_REQUEST("negative file size is invalid"); 125 } 126 127 return new Parameters(parent, description, media_type, name, size); 128 } 129 130 public void on_session_initiate(XmppStream stream, Jingle.Session session) { 131 parent.file_incoming(stream, new FileTransfer(session, this)); 132 } 133} 134 135// Does nothing except wrapping an input stream to signal EOF after reading 136// `max_size` bytes. 137private class FileTransferInputStream : InputStream { 138 InputStream inner; 139 int64 remaining_size; 140 public FileTransferInputStream(InputStream inner, int64 max_size) { 141 this.inner = inner; 142 this.remaining_size = max_size; 143 } 144 private ssize_t update_remaining(ssize_t read) { 145 this.remaining_size -= read; 146 return read; 147 } 148 public override ssize_t read(uint8[] buffer_, Cancellable? cancellable = null) throws IOError { 149 unowned uint8[] buffer = buffer_; 150 if (remaining_size <= 0) { 151 return 0; 152 } 153 if (buffer.length > remaining_size) { 154 buffer = buffer[0:remaining_size]; 155 } 156 return update_remaining(inner.read(buffer, cancellable)); 157 } 158 public override async ssize_t read_async(uint8[]? buffer_, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { 159 unowned uint8[] buffer = buffer_; 160 if (remaining_size <= 0) { 161 return 0; 162 } 163 if (buffer.length > remaining_size) { 164 buffer = buffer[0:remaining_size]; 165 } 166 return update_remaining(yield inner.read_async(buffer, io_priority, cancellable)); 167 } 168 public override bool close(Cancellable? cancellable = null) throws IOError { 169 return inner.close(cancellable); 170 } 171 public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { 172 return yield inner.close_async(io_priority, cancellable); 173 } 174} 175 176public class FileTransfer : Object { 177 Jingle.Session session; 178 Parameters parameters; 179 180 public Jid peer { get { return session.peer_full_jid; } } 181 public string? file_name { get { return parameters.name; } } 182 public int64 size { get { return parameters.size; } } 183 public Jingle.SecurityParameters? security { get { return session.security; } } 184 185 public InputStream? stream { get; private set; } 186 187 public FileTransfer(Jingle.Session session, Parameters parameters) { 188 this.session = session; 189 this.parameters = parameters; 190 this.stream = new FileTransferInputStream(session.conn.input_stream, size); 191 } 192 193 public void accept(XmppStream stream) throws IOError { 194 session.accept(stream, parameters.original_description); 195 session.conn.output_stream.close(); 196 } 197 198 public void reject(XmppStream stream) { 199 session.reject(stream); 200 } 201} 202 203} 204