diff options
Diffstat (limited to 'libdino/src')
-rw-r--r-- | libdino/src/application.vala | 1 | ||||
-rw-r--r-- | libdino/src/entity/file_transfer.vala | 156 | ||||
-rw-r--r-- | libdino/src/entity/message.vala | 1 | ||||
-rw-r--r-- | libdino/src/service/content_item_store.vala | 8 | ||||
-rw-r--r-- | libdino/src/service/database.vala | 23 | ||||
-rw-r--r-- | libdino/src/service/file_manager.vala | 301 | ||||
-rw-r--r-- | libdino/src/service/file_transfer_storage.vala | 47 | ||||
-rw-r--r-- | libdino/src/service/jingle_file_transfers.vala | 12 | ||||
-rw-r--r-- | libdino/src/service/message_processor.vala | 7 | ||||
-rw-r--r-- | libdino/src/service/message_storage.vala | 18 | ||||
-rw-r--r-- | libdino/src/service/module_manager.vala | 1 | ||||
-rw-r--r-- | libdino/src/service/sfs_metadata.vala | 81 | ||||
-rw-r--r-- | libdino/src/service/stateless_file_sharing.vala | 162 | ||||
-rw-r--r-- | libdino/src/util/limit_input_stream.vala | 73 | ||||
-rw-r--r-- | libdino/src/util/util.vala | 31 |
15 files changed, 578 insertions, 344 deletions
diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 0fcee731..4aa4b8ad 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -58,6 +58,7 @@ public interface Application : GLib.Application { Replies.start(stream_interactor, db); FallbackBody.start(stream_interactor, db); ContactModels.start(stream_interactor); + StatelessFileSharing.start(stream_interactor, db); create_actions(); diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala index c9c7916e..49ff2aa3 100644 --- a/libdino/src/entity/file_transfer.vala +++ b/libdino/src/entity/file_transfer.vala @@ -4,6 +4,8 @@ namespace Dino.Entities { public class FileTransfer : Object { + public signal void sources_changed(); + public const bool DIRECTION_SENT = true; public const bool DIRECTION_RECEIVED = false; @@ -14,23 +16,8 @@ public class FileTransfer : Object { FAILED } - public class SerializedSfsSource: Object { - public string type; - public string data; - - public SerializedSfsSource.from_sfs_source(Xep.StatelessFileSharing.SfsSource source) { - this.type = source.type(); - this.data = source.serialize(); - } - - public async Xep.StatelessFileSharing.SfsSource to_sfs_source() { - assert(this.type == Xep.StatelessFileSharing.HttpSource.SOURCE_TYPE); - Xep.StatelessFileSharing.HttpSource http_source = yield Xep.StatelessFileSharing.HttpSource.deserialize(this.data); - return http_source; - } - } - public int id { get; set; default=-1; } + public string? file_sharing_id { get; set; } public Account account { get; set; } public Jid counterpart { get; set; } public Jid ourpart { get; set; } @@ -85,13 +72,45 @@ public class FileTransfer : Object { public int provider { get; set; } public string info { get; set; } public Cancellable cancellable { get; default=new Cancellable(); } + + // This value is not persisted + public int64 transferred_bytes { get; set; } + + public Xep.FileMetadataElement.FileMetadata file_metadata { + owned get { + return new Xep.FileMetadataElement.FileMetadata() { + name = this.file_name, + mime_type = this.mime_type, + size = this.size, + desc = this.desc, + date = this.modification_date, + width = this.width, + height = this.height, + length = this.length, + hashes = this.hashes, + thumbnails = this.thumbnails + }; + } + set { + this.file_name = value.name; + this.mime_type = value.mime_type; + this.size = value.size; + this.desc = value.desc; + this.modification_date = value.date; + this.width = value.width; + this.height = value.height; + this.length = value.length; + this.hashes = value.hashes; + this.thumbnails = value.thumbnails; + } + } public string? desc { get; set; } public DateTime? modification_date { get; set; } public int width { get; set; default=-1; } public int height { get; set; default=-1; } public int64 length { get; set; default=-1; } - public Xep.CryptographicHashes.Hashes hashes { get; set; default=new Xep.CryptographicHashes.Hashes();} - public ListStore sfs_sources { get; set; default=new ListStore(typeof(SerializedSfsSource)); } + public Gee.List<Xep.CryptographicHashes.Hash> hashes = new Gee.ArrayList<Xep.CryptographicHashes.Hash>(); + public Gee.List<Xep.StatelessFileSharing.Source> sfs_sources = new Gee.ArrayList<Xep.StatelessFileSharing.Source>(Xep.StatelessFileSharing.Source.equals_func); public Gee.List<Xep.JingleContentThumbnails.Thumbnail> thumbnails = new Gee.ArrayList<Xep.JingleContentThumbnails.Thumbnail>(); private Database? db; @@ -102,6 +121,7 @@ public class FileTransfer : Object { this.storage_dir = storage_dir; id = row[db.file_transfer.id]; + file_sharing_id = row[db.file_transfer.file_sharing_id]; account = db.get_account_by_id(row[db.file_transfer.account_id]); // TODO don’t have to generate acc new counterpart = db.get_jid_by_id(row[db.file_transfer.counterpart_id]); @@ -130,11 +150,12 @@ public class FileTransfer : Object { height = row[db.file_transfer.height]; length = (int64) row[db.file_transfer.length]; + // TODO put those into the initial query foreach(var hash_row in db.file_hashes.select().with(db.file_hashes.id, "=", id)) { Xep.CryptographicHashes.Hash hash = new Xep.CryptographicHashes.Hash(); hash.algo = hash_row[db.file_hashes.algo]; hash.val = hash_row[db.file_hashes.value]; - hashes.hashes.add(hash); + hashes.add(hash); } foreach(var thumbnail_row in db.file_thumbnails.select().with(db.file_thumbnails.id, "=", id)) { @@ -146,11 +167,10 @@ public class FileTransfer : Object { thumbnails.add(thumbnail); } - foreach(Qlite.Row source_row in db.sfs_sources.select().with(db.sfs_sources.id, "=", id)) { - SerializedSfsSource source = new SerializedSfsSource(); - source.type = source_row[db.sfs_sources.type]; - source.data = source_row[db.sfs_sources.data]; - sfs_sources.append(source as Object); + foreach(Qlite.Row source_row in db.sfs_sources.select().with(db.sfs_sources.file_transfer_id, "=", id)) { + if (source_row[db.sfs_sources.type] == "http") { + sfs_sources.add(new Xep.StatelessFileSharing.HttpSource() { url=source_row[db.sfs_sources.data] }); + } } notify.connect(on_update); @@ -175,6 +195,7 @@ public class FileTransfer : Object { .value(db.file_transfer.provider, provider) .value(db.file_transfer.info, info); + if (file_sharing_id != null) builder.value(db.file_transfer.file_sharing_id, file_sharing_id); if (path != null) builder.value(db.file_transfer.path, path); if (mime_type != null) builder.value(db.file_transfer.mime_type, mime_type); if (path != null) builder.value(db.file_transfer.path, path); @@ -185,7 +206,7 @@ public class FileTransfer : Object { id = (int) builder.perform(); - foreach (Xep.CryptographicHashes.Hash hash in hashes.hashes) { + foreach (Xep.CryptographicHashes.Hash hash in hashes) { db.file_hashes.insert() .value(db.file_hashes.id, id) .value(db.file_hashes.algo, hash.algo) @@ -202,29 +223,40 @@ public class FileTransfer : Object { .perform(); } - for(int i = 0; i < sfs_sources.get_n_items(); i++) { - Object source_object = sfs_sources.get_item(i); - SerializedSfsSource source = source_object as SerializedSfsSource; + foreach (Xep.StatelessFileSharing.Source source in sfs_sources) { + add_sfs_source(source); + } + + notify.connect(on_update); + } + + public void add_sfs_source(Xep.StatelessFileSharing.Source source) { + if (sfs_sources.contains(source)) return; // Don't add the same source twice. Might happen due to MAM and lacking deduplication. + + sfs_sources.add(source); + + Xep.StatelessFileSharing.HttpSource? http_source = source as Xep.StatelessFileSharing.HttpSource; + if (http_source != null) { db.sfs_sources.insert() - .value(db.sfs_sources.id, id) - .value(db.sfs_sources.type, source.type) - .value(db.sfs_sources.data, source.data) + .value(db.sfs_sources.file_transfer_id, id) + .value(db.sfs_sources.type, "http") + .value(db.sfs_sources.data, http_source.url) .perform(); } - notify.connect(on_update); - sfs_sources.items_changed.connect((position, removed, added) => { - on_update_sources_items(this, position, removed, added); - }); + sources_changed(); } - public File get_file() { + public File? get_file() { + if (path == null) return null; return File.new_for_path(Path.build_filename(Dino.get_storage_dir(), "files", path)); } private void on_update(Object o, ParamSpec sp) { Qlite.UpdateBuilder update_builder = db.file_transfer.update().with(db.file_transfer.id, "=", id); switch (sp.name) { + case "file-sharing-id": + update_builder.set(db.file_transfer.file_sharing_id, file_sharing_id); break; case "counterpart": update_builder.set(db.file_transfer.counterpart_id, db.get_jid_id(counterpart)); update_builder.set(db.file_transfer.counterpart_resource, counterpart.resourcepart); break; @@ -264,58 +296,6 @@ public class FileTransfer : Object { } update_builder.perform(); } - - private void on_update_sources_items(FileTransfer file_transfer, uint position, uint removed, uint added) { - for(uint i = position; i < position + added; i++) { - Object source_object = file_transfer.sfs_sources.get_item(i); - SerializedSfsSource source = source_object as SerializedSfsSource; - db.sfs_sources.insert() - .value(db.sfs_sources.id, id) - .value(db.sfs_sources.type, source.type) - .value(db.sfs_sources.data, source.data) - .perform(); - } - } - - public Xep.FileMetadataElement.FileMetadata to_metadata_element() { - Xep.FileMetadataElement.FileMetadata metadata = new Xep.FileMetadataElement.FileMetadata(); - metadata.name = this.file_name; - metadata.mime_type = this.mime_type; - metadata.size = this.size; - metadata.desc = this.desc; - metadata.date = this.modification_date; - metadata.width = this.width; - metadata.height = this.height; - metadata.length = this.length; - metadata.hashes = this.hashes; - metadata.thumbnails = this.thumbnails; - return metadata; - } - - public async Xep.StatelessFileSharing.SfsElement to_sfs_element() { - Xep.StatelessFileSharing.SfsElement sfs_element = new Xep.StatelessFileSharing.SfsElement(); - sfs_element.metadata = this.to_metadata_element(); - for(int i = 0; i < sfs_sources.get_n_items(); i++) { - Object source_object = sfs_sources.get_item(i); - SerializedSfsSource source = source_object as SerializedSfsSource; - sfs_element.sources.add(yield source.to_sfs_source()); - } - - return sfs_element; - } - - public void with_metadata_element(Xep.FileMetadataElement.FileMetadata metadata) { - this.file_name = metadata.name; - this.mime_type = metadata.mime_type; - this.size = metadata.size; - this.desc = metadata.desc; - this.modification_date = metadata.date; - this.width = metadata.width; - this.height = metadata.height; - this.length = metadata.length; - this.hashes = metadata.hashes; - this.thumbnails = metadata.thumbnails; - } } } diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala index d48f999b..e5aad25f 100644 --- a/libdino/src/entity/message.vala +++ b/libdino/src/entity/message.vala @@ -1,6 +1,5 @@ using Gee; using Xmpp; -using Xmpp.Xep; namespace Dino.Entities { diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index 740dc2a9..7d7ed1fb 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -121,13 +121,7 @@ public class ContentItemStore : StreamInteractionModule, Object { Message? message = get_message_for_content_item(conversation, content_item); if (message == null) return null; - if (message.edit_to != null) return message.edit_to; - - if (conversation.type_ == Conversation.Type.CHAT) { - return message.stanza_id; - } else { - return message.server_id; - } + return MessageStorage.get_reference_id(message); } public Jid? get_message_sender_for_content_item(Conversation conversation, ContentItem content_item) { diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 30d6d55f..bfa76890 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -180,6 +180,7 @@ public class Database : Qlite.Database { public class FileTransferTable : Table { public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column<string> file_sharing_id = new Column.Text("file_sharing_id") { min_version=28 }; public Column<int> account_id = new Column.Integer("account_id") { not_null = true }; public Column<int> counterpart_id = new Column.Integer("counterpart_id") { not_null = true }; public Column<string> counterpart_resource = new Column.Text("counterpart_resource"); @@ -191,7 +192,7 @@ public class Database : Qlite.Database { public Column<string> file_name = new Column.Text("file_name"); public Column<string> path = new Column.Text("path"); public Column<string> mime_type = new Column.Text("mime_type"); - public Column<long> size = new Column.Long("size") { default = "-1", min_version=28 }; + public Column<long> size = new Column.Long("size"); public Column<int> state = new Column.Integer("state"); public Column<int> provider = new Column.Integer("provider"); public Column<string> info = new Column.Text("info"); @@ -202,9 +203,9 @@ public class Database : Qlite.Database { internal FileTransferTable(Database db) { base(db, "file_transfer"); - init({id, account_id, counterpart_id, counterpart_resource, our_resource, direction, time, local_time, - encryption, file_name, path, mime_type, size, state, provider, info, modification_date, width, height, - length}); + init({id, file_sharing_id, account_id, counterpart_id, counterpart_resource, our_resource, direction, + time, local_time, encryption, file_name, path, mime_type, size, state, provider, info, modification_date, + width, height, length}); } } @@ -233,15 +234,15 @@ public class Database : Qlite.Database { } } - public class SfsSourcesTable : Table { - public Column<int> id = new Column.Integer("id"); + public class SourcesTable : Table { + public Column<int> file_transfer_id = new Column.Integer("file_transfer_id"); public Column<string> type = new Column.Text("type") { not_null = true }; public Column<string> data = new Column.Text("data") { not_null = true }; - internal SfsSourcesTable(Database db) { + internal SourcesTable(Database db) { base(db, "sfs_sources"); - init({id, type, data}); - unique({id, type, data}, "REPLACE"); + init({file_transfer_id, type, data}); + index("sfs_sources_file_transfer_id_idx", {file_transfer_id}); } } @@ -445,7 +446,7 @@ public class Database : Qlite.Database { public FileTransferTable file_transfer { get; private set; } public FileHashesTable file_hashes { get; private set; } public FileThumbnailsTable file_thumbnails { get; private set; } - public SfsSourcesTable sfs_sources { get; private set; } + public SourcesTable sfs_sources { get; private set; } public CallTable call { get; private set; } public CallCounterpartTable call_counterpart { get; private set; } public ConversationTable conversation { get; private set; } @@ -478,7 +479,7 @@ public class Database : Qlite.Database { file_transfer = new FileTransferTable(this); file_hashes = new FileHashesTable(this); file_thumbnails = new FileThumbnailsTable(this); - sfs_sources = new SfsSourcesTable(this); + sfs_sources = new SourcesTable(this); call = new CallTable(this); call_counterpart = new CallCounterpartTable(this); conversation = new ConversationTable(this); diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index 32cf23c4..2a665e1e 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -22,6 +22,11 @@ public class FileManager : StreamInteractionModule, Object { private Gee.List<FileProvider> file_providers = new ArrayList<FileProvider>(); private Gee.List<FileMetadataProvider> file_metadata_providers = new ArrayList<FileMetadataProvider>(); + public StatelessFileSharing sfs { + owned get { return stream_interactor.get_module(StatelessFileSharing.IDENTITY); } + private set { } + } + public static void start(StreamInteractor stream_interactor, Database db) { FileManager m = new FileManager(stream_interactor, db); stream_interactor.add_module(m); @@ -40,15 +45,12 @@ public class FileManager : StreamInteractionModule, Object { this.add_sender(new JingleFileSender(stream_interactor)); this.add_metadata_provider(new GenericFileMetadataProvider()); this.add_metadata_provider(new ImageFileMetadataProvider()); - this.stream_interactor.account_added.connect((account) => { - on_account_added(account); - }); } public const int HTTP_PROVIDER_ID = 0; public const int SFS_PROVIDER_ID = 2; - private FileProvider? select_file_provider(FileTransfer file_transfer) { + public FileProvider? select_file_provider(FileTransfer file_transfer) { bool http_usable = file_transfer.provider == SFS_PROVIDER_ID; foreach (FileProvider file_provider in this.file_providers) { if (file_transfer.provider == file_provider.get_id()) { @@ -61,98 +63,7 @@ public class FileManager : StreamInteractionModule, Object { return null; } - // For receiving out of band data as sfs - private async void on_backwards_compatible_sfs(FileProvider file_provider, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) { - Xep.StatelessFileSharing.SfsElement sfs_element = new Xep.StatelessFileSharing.SfsElement(); - - Xep.StatelessFileSharing.HttpSource source = new Xep.StatelessFileSharing.HttpSource(); - HttpFileReceiveData http_receive_data = receive_data as HttpFileReceiveData; - source.url = http_receive_data.url; - sfs_element.sources.add(source); - - FileTransfer file_transfer = new FileTransfer(); - - if (is_jid_trustworthy(from, conversation)) { - try { - file_meta = yield file_provider.get_meta_info(file_transfer, http_receive_data, file_meta); - } catch (Error e) { - warning("Can't accept oob data as stateless file sharing due to failed http request\n"); - } - } - - sfs_element.metadata.size = file_meta.size; - sfs_element.metadata.name = file_meta.file_name; - sfs_element.metadata.mime_type = file_meta.mime_type; - // Encryption unused in http file transfers - - yield on_receive_sfs(from, conversation, sfs_element, null); - } - - private async void on_receive_sfs(Jid from, Conversation conversation, Xep.StatelessFileSharing.SfsElement sfs_element, string? id) { - FileTransfer file_transfer = new FileTransfer(); - file_transfer.account = conversation.account; - file_transfer.counterpart = file_transfer.direction == FileTransfer.DIRECTION_RECEIVED ? from : conversation.counterpart; - if (conversation.type_.is_muc_semantic()) { - file_transfer.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.bare_jid; - file_transfer.direction = from.equals(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; - } else { - file_transfer.ourpart = conversation.account.full_jid; - file_transfer.direction = from.equals_bare(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; - } - file_transfer.time = new DateTime.now_utc(); - // TODO: get time from message - file_transfer.local_time = new DateTime.now_utc(); - file_transfer.provider = SFS_PROVIDER_ID; - file_transfer.with_metadata_element(sfs_element.metadata); - foreach (Xep.StatelessFileSharing.SfsSource source in sfs_element.sources) { - file_transfer.sfs_sources.append(new FileTransfer.SerializedSfsSource.from_sfs_source(source) as Object); - } - // FileTransfer.info stores the id of the MessageStanza for future SfsSourceAttachments - // Prior to sfs, info stored the id of the Message entity for oob - file_transfer.info = id; - - stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); - - if (is_sender_trustworthy(file_transfer, conversation)) { - if (file_transfer.size >= 0 && file_transfer.size < 500) { - FileProvider? file_provider = this.select_file_provider(file_transfer); - download_file_internal.begin(file_provider, file_transfer, conversation, (_, res) => { - download_file_internal.end(res); - }); - } - } - - conversation.last_active = file_transfer.time; - received_file(file_transfer, conversation); - } - - private void on_receive_sfs_attachment(Jid from, Conversation conversation, Xep.StatelessFileSharing.SfsSourceAttachment attachment) { - foreach (Qlite.Row file_transfer_row in this.db.file_transfer.select() - .with(db.file_transfer.info, "=", attachment.sfs_id)) { - FileTransfer file_transfer = new FileTransfer.from_row(this.db, file_transfer_row, FileManager.get_storage_dir()); - if (file_transfer.hashes.supported_hashes().is_empty) { - return; - } - foreach (StatelessFileSharing.SfsSource source in attachment.sources) { - file_transfer.sfs_sources.append(new FileTransfer.SerializedSfsSource.from_sfs_source(source) as Object); - } - } - - } - - private void on_account_added(Account account) { - Xep.StatelessFileSharing.Module fsf_module = stream_interactor.module_manager.get_module(account, Xep.StatelessFileSharing.Module.IDENTITY); - fsf_module.received_sfs.connect((from, to, sfs_element, message) => { - Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from, to, account, message.type_); - on_receive_sfs(from, conversation, sfs_element, message.id); - }); - fsf_module.received_sfs_attachment.connect((from, to, sfs_attachment, message) => { - Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from, to, account, message.type_); - on_receive_sfs_attachment(from, conversation, sfs_attachment); - }); - } - - public async HashMap<int, long> get_file_size_limits(Conversation conversation) { + public async HashMap<int, long> get_file_size_limits(Conversation conversation) { HashMap<int, long> ret = new HashMap<int, long>(); foreach (FileSender sender in file_senders) { ret[sender.get_id()] = yield sender.get_file_size_limit(conversation); @@ -180,7 +91,7 @@ public class FileManager : StreamInteractionModule, Object { yield file_metadata_provider.fill_metadata(file, metadata); } } - file_transfer.with_metadata_element(metadata); + file_transfer.file_metadata = metadata; try { file_transfer.input_stream = yield file.read_async(); @@ -237,7 +148,16 @@ public class FileManager : StreamInteractionModule, Object { file_send_data = file_encryptor.preprocess_send_file(conversation, file_transfer, file_send_data, file_meta); } + file_transfer.state = FileTransfer.State.IN_PROGRESS; + + // Update current download progress in the FileTransfer + LimitInputStream? limit_stream = file_transfer.input_stream as LimitInputStream; + if (limit_stream != null) { + limit_stream.bind_property("retrieved-bytes", file_transfer, "transferred-bytes", BindingFlags.SYNC_CREATE); + } + yield file_sender.send_file(conversation, file_transfer, file_send_data, file_meta); + file_transfer.state = FileTransfer.State.COMPLETE; } catch (Error e) { warning("Send file error: %s", e.message); @@ -265,12 +185,7 @@ public class FileManager : StreamInteractionModule, Object { public void add_provider(FileProvider file_provider) { file_providers.add(file_provider); file_provider.file_incoming.connect((info, from, time, local_time, conversation, receive_data, file_meta) => { - if (receive_data is HttpFileReceiveData) { - printerr("Handling oob data as stateless file sharing"); - this.on_backwards_compatible_sfs.begin(file_provider, from, time, local_time, conversation, receive_data, file_meta); - } else { - handle_incoming_file.begin(file_provider, info, from, time, local_time, conversation, receive_data, file_meta); - } + handle_incoming_file.begin(file_provider, info, from, time, local_time, conversation, receive_data, file_meta); }); } @@ -296,10 +211,12 @@ public class FileManager : StreamInteractionModule, Object { file_metadata_providers.add(file_metadata_provider); } - private bool is_jid_trustworthy(Jid from, Conversation conversation) { + public bool is_sender_trustworthy(FileTransfer file_transfer, Conversation conversation) { + if (file_transfer.direction == FileTransfer.DIRECTION_SENT) return true; + Jid relevant_jid = conversation.counterpart; if (conversation.type_ == Conversation.Type.GROUPCHAT) { - relevant_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(from, conversation.account); + relevant_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(file_transfer.from, conversation.account); } if (relevant_jid == null) return false; @@ -307,12 +224,6 @@ public class FileManager : StreamInteractionModule, Object { return in_roster; } - public bool is_sender_trustworthy(FileTransfer file_transfer, Conversation conversation) { - if (file_transfer.direction == FileTransfer.DIRECTION_SENT) return true; - - return is_jid_trustworthy(file_transfer.from, conversation); - } - private async FileMeta get_file_meta(FileProvider file_provider, FileTransfer file_transfer, Conversation conversation, FileReceiveData receive_data_) throws FileReceiveError { FileReceiveData receive_data = receive_data_; FileMeta file_meta = file_provider.get_file_meta(file_transfer); @@ -337,7 +248,11 @@ public class FileManager : StreamInteractionModule, Object { private async void download_file_internal(FileProvider file_provider, FileTransfer file_transfer, Conversation conversation) { try { // Get meta info - FileReceiveData receive_data = yield file_provider.get_file_receive_data(file_transfer); + FileReceiveData? receive_data = file_provider.get_file_receive_data(file_transfer); + if (receive_data == null) { + warning("Don't have download data (yet)"); + return; + } FileDecryptor? file_decryptor = null; foreach (FileDecryptor decryptor in file_decryptors) { if (decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) { @@ -367,10 +282,17 @@ public class FileManager : StreamInteractionModule, Object { input_stream = yield file_decryptor.decrypt_file(input_stream, conversation, file_transfer, receive_data); } + // Update current download progress in the FileTransfer + LimitInputStream? limit_stream = input_stream as LimitInputStream; + if (limit_stream != null) { + limit_stream.bind_property("retrieved-bytes", file_transfer, "transferred-bytes", BindingFlags.SYNC_CREATE); + } + // Save file string filename = Random.next_int().to_string("%x") + "_" + file_transfer.file_name; File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename)); + // libsoup doesn't properly support splicing OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION); uint8[] buffer = new uint8[1024]; ssize_t read; @@ -381,20 +303,49 @@ public class FileManager : StreamInteractionModule, Object { } yield input_stream.close_async(Priority.LOW, file_transfer.cancellable); yield os.close_async(Priority.LOW, file_transfer.cancellable); + + // Verify the hash of the downloaded file, if it is known + var supported_hashes = Xep.CryptographicHashes.get_supported_hashes(file_transfer.hashes); + if (!supported_hashes.is_empty) { + var checksum_types = new ArrayList<ChecksumType>(); + var hashes = new HashMap<ChecksumType, string>(); + foreach (var hash in supported_hashes) { + var checksum_type = Xep.CryptographicHashes.hash_string_to_type(hash.algo); + checksum_types.add(checksum_type); + hashes[checksum_type] = hash.val; + } + + var computed_hashes = yield compute_file_hashes(file, checksum_types); + foreach (var checksum_type in hashes.keys) { + if (hashes[checksum_type] != computed_hashes[checksum_type]) { + warning("Hash of downloaded file does not equal advertised hash, discarding: %s. %s should be %s, was %s", + file_transfer.file_name, checksum_type.to_string(), hashes[checksum_type], computed_hashes[checksum_type]); + FileUtils.remove(file.get_path()); + file_transfer.state = FileTransfer.State.FAILED; + return; + } + } + } + file_transfer.path = file.get_basename(); - file_transfer.input_stream = yield file.read_async(); FileInfo file_info = file_transfer.get_file().query_info("*", FileQueryInfoFlags.NONE); file_transfer.mime_type = file_info.get_content_type(); file_transfer.state = FileTransfer.State.COMPLETE; + } catch (IOError.CANCELLED e) { + print("cancelled\n"); } catch (Error e) { warning("Error downloading file: %s", e.message); - file_transfer.state = FileTransfer.State.FAILED; + if (file_transfer.provider == 0 || file_transfer.provider == FileManager.SFS_PROVIDER_ID) { + file_transfer.state = FileTransfer.State.NOT_STARTED; + } else { + file_transfer.state = FileTransfer.State.FAILED; + } } } - private async void handle_incoming_file(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) { + public FileTransfer create_file_transfer_from_provider_incoming(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) { FileTransfer file_transfer = new FileTransfer(); file_transfer.account = conversation.account; file_transfer.counterpart = file_transfer.direction == FileTransfer.DIRECTION_RECEIVED ? from : conversation.counterpart; @@ -426,6 +377,11 @@ public class FileManager : StreamInteractionModule, Object { } } + return file_transfer; + } + + private async void handle_incoming_file(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) { + FileTransfer file_transfer = create_file_transfer_from_provider_incoming(file_provider, info, from, time, local_time, conversation, receive_data, file_meta); stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); if (is_sender_trustworthy(file_transfer, conversation)) { @@ -451,10 +407,10 @@ public class FileManager : StreamInteractionModule, Object { string filename = Random.next_int().to_string("%x") + "_" + file_transfer.file_name; File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename)); OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION); - yield os.splice_async(file_transfer.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); + yield os.splice_async(file_transfer.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET); file_transfer.state = FileTransfer.State.COMPLETE; file_transfer.path = filename; - file_transfer.input_stream = yield file.read_async(); + file_transfer.input_stream = new LimitInputStream(yield file.read_async(), file_transfer.size); } catch (Error e) { throw new FileSendError.SAVE_FAILED("Saving file error: %s".printf(e.message)); } @@ -467,10 +423,10 @@ public errordomain FileSendError { SAVE_FAILED } +// Get rid of this Error and pass IoErrors instead - DOWNLOAD_FAILED already removed public errordomain FileReceiveError { GET_METADATA_FAILED, - DECRYPTION_FAILED, - DOWNLOAD_FAILED + DECRYPTION_FAILED } public class FileMeta { @@ -505,10 +461,10 @@ public interface FileProvider : Object { public abstract Encryption get_encryption(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta); public abstract FileMeta get_file_meta(FileTransfer file_transfer) throws FileReceiveError; - public abstract async FileReceiveData? get_file_receive_data(FileTransfer file_transfer); + public abstract FileReceiveData? get_file_receive_data(FileTransfer file_transfer); public abstract async FileMeta get_meta_info(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError; - public abstract async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError; + public abstract async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws IOError; public abstract int get_id(); } @@ -541,107 +497,4 @@ public interface FileDecryptor : Object { public abstract async InputStream decrypt_file(InputStream encrypted_stream, Conversation conversation, FileTransfer file_transfer, FileReceiveData receive_data) throws FileReceiveError; } -public interface FileMetadataProvider : Object { - public abstract bool supports_file(File file); - - public abstract async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata); -} - -class GenericFileMetadataProvider: Dino.FileMetadataProvider, Object { - public bool supports_file(File file) { - return true; - } - - public async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata) { - FileInfo info = file.query_info("*", FileQueryInfoFlags.NONE); - - metadata.name = info.get_display_name(); - metadata.mime_type = info.get_content_type(); - metadata.size = info.get_size(); - metadata.date = info.get_modification_date_time(); - - Bytes file_data = file.load_bytes(); - metadata.hashes.hashes.add(new CryptographicHashes.Hash.from_data(GLib.ChecksumType.SHA256, file_data.get_data())); - metadata.hashes.hashes.add(new CryptographicHashes.Hash.from_data(GLib.ChecksumType.SHA512, file_data.get_data())); - } -} - -public class ImageFileMetadataProvider: Dino.FileMetadataProvider, Object { - public bool supports_file(File file) { - return file.query_info("*", FileQueryInfoFlags.NONE).get_content_type().has_prefix("image"); - } - - private const int[] THUMBNAIL_DIMS = { 1, 2, 3, 4, 8 }; - private const string IMAGE_TYPE = "png"; - private const string MIME_TYPE = "image/png"; - - public async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata) { - Pixbuf pixbuf = new Pixbuf.from_stream(yield file.read_async()); - metadata.width = pixbuf.get_width(); - metadata.height = pixbuf.get_height(); - float ratio = (float)metadata.width / (float) metadata.height; - - int thumbnail_width = -1; - int thumbnail_height = -1; - float diff = float.INFINITY; - for (int i = 0; i < THUMBNAIL_DIMS.length; i++) { - int test_width = THUMBNAIL_DIMS[i]; - int test_height = THUMBNAIL_DIMS[THUMBNAIL_DIMS.length - 1 - i]; - float test_ratio = (float)test_width / (float)test_height; - float test_diff = (test_ratio - ratio).abs(); - if (test_diff < diff) { - thumbnail_width = test_width; - thumbnail_height = test_height; - diff = test_diff; - } - } - - Pixbuf thumbnail_pixbuf = pixbuf.scale_simple(thumbnail_width, thumbnail_height, InterpType.BILINEAR); - uint8[] buffer; - thumbnail_pixbuf.save_to_buffer(out buffer, IMAGE_TYPE); - string base_64 = GLib.Base64.encode(buffer); - string uri = @"data:$MIME_TYPE;base64,$base_64"; - Xep.JingleContentThumbnails.Thumbnail thumbnail = new Xep.JingleContentThumbnails.Thumbnail(); - thumbnail.uri = uri; - thumbnail.media_type = MIME_TYPE; - thumbnail.width = thumbnail_width; - thumbnail.height = thumbnail_height; - metadata.thumbnails.add(thumbnail); - } - - public static Pixbuf? parse_thumbnail(Xep.JingleContentThumbnails.Thumbnail thumbnail) { - string[] splits = thumbnail.uri.split(":", 2); - if (splits.length != 2) { - printerr("Thumbnail parsing error: ':' not found"); - return null; - } - if (splits[0] != "data") { - printerr("Unsupported thumbnail: unimplemented uri type\n"); - return null; - } - splits = splits[1].split(";", 2); - if (splits.length != 2) { - printerr("Thumbnail parsing error: ';' not found"); - return null; - } - if (splits[0] != MIME_TYPE) { - printerr("Unsupported thumbnail: unsupported mime-type\n"); - return null; - } - splits = splits[1].split(",", 2); - if (splits.length != 2) { - printerr("Thumbnail parsing error: ',' not found"); - return null; - } - if (splits[0] != "base64") { - printerr("Unsupported thumbnail: data is not base64 encoded\n"); - return null; - } - uint8[] data = Base64.decode(splits[1]); - MemoryInputStream input_stream = new MemoryInputStream.from_data(data); - Pixbuf pixbuf = new Pixbuf.from_stream(input_stream); - return pixbuf; - } -} - } diff --git a/libdino/src/service/file_transfer_storage.vala b/libdino/src/service/file_transfer_storage.vala index 1cc62403..64bb6b81 100644 --- a/libdino/src/service/file_transfer_storage.vala +++ b/libdino/src/service/file_transfer_storage.vala @@ -14,6 +14,8 @@ namespace Dino { private Database db; private WeakMap<int, FileTransfer> files_by_db_id = new WeakMap<int, FileTransfer>(); + private WeakMap<int, FileTransfer> files_by_message_id = new WeakMap<int, FileTransfer>(); + private WeakMap<string, FileTransfer> files_by_message_and_file_id = new WeakMap<string, FileTransfer>(); public static void start(StreamInteractor stream_interactor, Database db) { FileTransferStorage m = new FileTransferStorage(stream_interactor, db); @@ -41,6 +43,42 @@ namespace Dino { return create_file_from_row_opt(row_option, conversation); } + // Http file transfers store the corresponding message id in the `info` field + public FileTransfer? get_file_by_message_id(int id, Conversation conversation) { + FileTransfer? file_transfer = files_by_message_id[id]; + if (file_transfer != null) { + return file_transfer; + } + + RowOption row_option = db.file_transfer.select() + .with(db.file_transfer.info, "=", id.to_string()) + .single() + .row(); + + return create_file_from_row_opt(row_option, conversation); + } + + public FileTransfer get_files_by_message_and_file_id(int message_id, string file_sharing_id, Conversation conversation) { + string combined_identifier = message_id.to_string() + file_sharing_id; + FileTransfer? file_transfer = files_by_message_and_file_id[combined_identifier]; + + if (file_transfer == null) { + RowOption row_option = db.file_transfer.select() + .with(db.file_transfer.info, "=", message_id.to_string()) + .with(db.file_transfer.file_sharing_id, "=", file_sharing_id) + .single() + .row(); + + file_transfer = create_file_from_row_opt(row_option, conversation); + } + + // There can be collisions in the combined identifier, check it's the correct FileTransfer + if (file_transfer != null && file_transfer.info == message_id.to_string() && file_transfer.file_sharing_id == file_sharing_id) { + return file_transfer; + } + return null; + } + private FileTransfer? create_file_from_row_opt(RowOption row_opt, Conversation conversation) { if (!row_opt.is_present()) return null; @@ -61,6 +99,15 @@ namespace Dino { private void cache_file(FileTransfer file_transfer) { files_by_db_id[file_transfer.id] = file_transfer; + + if (file_transfer.info != null && file_transfer.info != "") { + files_by_message_id[int.parse(file_transfer.info)] = file_transfer; + + if (file_transfer.file_sharing_id != null && file_transfer.info != null) { + string combined_identifier = file_transfer.info + file_transfer.file_sharing_id; + files_by_message_and_file_id[combined_identifier] = file_transfer; + } + } } } }
\ No newline at end of file diff --git a/libdino/src/service/jingle_file_transfers.vala b/libdino/src/service/jingle_file_transfers.vala index daccd309..e0d3fce1 100644 --- a/libdino/src/service/jingle_file_transfers.vala +++ b/libdino/src/service/jingle_file_transfers.vala @@ -74,7 +74,7 @@ public class JingleFileProvider : FileProvider, Object { return file_meta; } - public async FileReceiveData? get_file_receive_data(FileTransfer file_transfer) { + public FileReceiveData? get_file_receive_data(FileTransfer file_transfer) { return new FileReceiveData(); } @@ -95,18 +95,14 @@ public class JingleFileProvider : FileProvider, Object { return Encryption.NONE; } - public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError { + public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws IOError { // TODO(hrxi) What should happen if `stream == null`? XmppStream? stream = stream_interactor.get_stream(file_transfer.account); Xmpp.Xep.JingleFileTransfer.FileTransfer? jingle_file_transfer = file_transfers[file_transfer.info]; if (jingle_file_transfer == null) { - throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore"); - } - try { - yield jingle_file_transfer.accept(stream); - } catch (IOError e) { - throw new FileReceiveError.DOWNLOAD_FAILED("Establishing connection did not work"); + throw new IOError.FAILED("Transfer data not available anymore"); } + yield jingle_file_transfer.accept(stream); return jingle_file_transfer.stream; } diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index d8ea3e2d..e8a43b05 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -307,7 +307,8 @@ public class MessageProcessor : StreamInteractionModule, Object { public override string[] after_actions { get { return after_actions_const; } } public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - return (message.body == null); + return message.body == null && + Xep.StatelessFileSharing.get_file_shares(stanza) == null; } } @@ -326,8 +327,6 @@ public class MessageProcessor : StreamInteractionModule, Object { } public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - if (message.body == null || outer.is_duplicate(message, stanza, conversation)) return true; - stream_interactor.get_module(MessageStorage.IDENTITY).add_message(message, conversation); return false; } @@ -371,7 +370,7 @@ public class MessageProcessor : StreamInteractionModule, Object { } } - public Entities.Message create_out_message(string text, Conversation conversation) { + public Entities.Message create_out_message(string? text, Conversation conversation) { Entities.Message message = new Entities.Message(text); message.type_ = Util.get_message_type_for_conversation(conversation); message.stanza_id = random_uuid(); diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala index 3dadab7b..81df46d5 100644 --- a/libdino/src/service/message_storage.vala +++ b/libdino/src/service/message_storage.vala @@ -99,6 +99,14 @@ public class MessageStorage : StreamInteractionModule, Object { return create_message_from_row_opt(row_option, conversation); } + public Message? get_message_by_referencing_id(string id, Conversation conversation) { + if (conversation.type_ == Conversation.Type.CHAT) { + return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(id, conversation); + } else { + return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(id, conversation); + } + } + public Message? get_message_by_stanza_id(string stanza_id, Conversation conversation) { if (messages_by_stanza_id.has_key(conversation)) { Message? message = messages_by_stanza_id[conversation][stanza_id]; @@ -191,6 +199,16 @@ public class MessageStorage : StreamInteractionModule, Object { message_refs.remove_at(message_refs.size - 1); } } + + public static string? get_reference_id(Message message) { + if (message.edit_to != null) return message.edit_to; + + if (message.type_ == Message.Type.CHAT) { + return message.stanza_id; + } else { + return message.server_id; + } + } } } diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala index 28ccf8ef..eb9e4fbc 100644 --- a/libdino/src/service/module_manager.vala +++ b/libdino/src/service/module_manager.vala @@ -86,7 +86,6 @@ public class ModuleManager { module_map[account].add(new Xep.Muji.Module()); module_map[account].add(new Xep.CallInvites.Module()); module_map[account].add(new Xep.Coin.Module()); - module_map[account].add(new Xep.StatelessFileSharing.Module()); initialize_account_modules(account, module_map[account]); } } diff --git a/libdino/src/service/sfs_metadata.vala b/libdino/src/service/sfs_metadata.vala new file mode 100644 index 00000000..ff2a57b4 --- /dev/null +++ b/libdino/src/service/sfs_metadata.vala @@ -0,0 +1,81 @@ +using Gdk; +using GLib; +using Gee; + +using Xmpp; +using Xmpp.Xep; +using Dino.Entities; + + +namespace Dino { + public interface FileMetadataProvider : Object { + public abstract bool supports_file(File file); + public abstract async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata); + } + + class GenericFileMetadataProvider: Dino.FileMetadataProvider, Object { + public bool supports_file(File file) { + return true; + } + + public async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata) { + FileInfo info = file.query_info("*", FileQueryInfoFlags.NONE); + + metadata.name = info.get_display_name(); + metadata.mime_type = info.get_content_type(); + metadata.size = info.get_size(); + metadata.date = info.get_modification_date_time(); + + var checksum_types = new ArrayList<ChecksumType>.wrap(new ChecksumType[] { ChecksumType.SHA256, ChecksumType.SHA512 }); + var file_hashes = yield compute_file_hashes(file, checksum_types); + + metadata.hashes.add(new CryptographicHashes.Hash.with_checksum(ChecksumType.SHA256, file_hashes[ChecksumType.SHA256])); + metadata.hashes.add(new CryptographicHashes.Hash.with_checksum(ChecksumType.SHA512, file_hashes[ChecksumType.SHA512])); + } + } + + public class ImageFileMetadataProvider: Dino.FileMetadataProvider, Object { + public bool supports_file(File file) { + return file.query_info("*", FileQueryInfoFlags.NONE).get_content_type().has_prefix("image"); + } + + private const int[] THUMBNAIL_DIMS = { 1, 2, 3, 4, 8 }; + private const string IMAGE_TYPE = "png"; + private const string MIME_TYPE = "image/png"; + + public async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata) { + Pixbuf pixbuf = new Pixbuf.from_stream(yield file.read_async()); + metadata.width = pixbuf.get_width(); + metadata.height = pixbuf.get_height(); + float ratio = (float)metadata.width / (float) metadata.height; + + int thumbnail_width = -1; + int thumbnail_height = -1; + float diff = float.INFINITY; + for (int i = 0; i < THUMBNAIL_DIMS.length; i++) { + int test_width = THUMBNAIL_DIMS[i]; + int test_height = THUMBNAIL_DIMS[THUMBNAIL_DIMS.length - 1 - i]; + float test_ratio = (float)test_width / (float)test_height; + float test_diff = (test_ratio - ratio).abs(); + if (test_diff < diff) { + thumbnail_width = test_width; + thumbnail_height = test_height; + diff = test_diff; + } + } + + Pixbuf thumbnail_pixbuf = pixbuf.scale_simple(thumbnail_width, thumbnail_height, InterpType.BILINEAR); + uint8[] buffer; + thumbnail_pixbuf.save_to_buffer(out buffer, IMAGE_TYPE); + string base_64 = GLib.Base64.encode(buffer); + string uri = @"data:$MIME_TYPE;base64,$base_64"; + Xep.JingleContentThumbnails.Thumbnail thumbnail = new Xep.JingleContentThumbnails.Thumbnail(); + thumbnail.uri = uri; + thumbnail.media_type = MIME_TYPE; + thumbnail.width = thumbnail_width; + thumbnail.height = thumbnail_height; + metadata.thumbnails.add(thumbnail); + } + } +} + diff --git a/libdino/src/service/stateless_file_sharing.vala b/libdino/src/service/stateless_file_sharing.vala new file mode 100644 index 00000000..9d3c53ab --- /dev/null +++ b/libdino/src/service/stateless_file_sharing.vala @@ -0,0 +1,162 @@ +using Gdk; +using Gee; + +using Xmpp; +using Xmpp.Xep; +using Dino.Entities; + +public class Dino.StatelessFileSharing : StreamInteractionModule, Object { + public static ModuleIdentity<StatelessFileSharing> IDENTITY = new ModuleIdentity<StatelessFileSharing>("sfs"); + public string id { get { return IDENTITY.id; } } + + public const int SFS_PROVIDER_ID = 2; + + public StreamInteractor stream_interactor { + owned get { return Application.get_default().stream_interactor; } + private set { } + } + + public FileManager file_manager { + owned get { return stream_interactor.get_module(FileManager.IDENTITY); } + private set { } + } + + public Database db { + owned get { return Application.get_default().db; } + private set { } + } + + private StatelessFileSharing(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this)); + } + + public static void start(StreamInteractor stream_interactor, Database db) { + StatelessFileSharing m = new StatelessFileSharing(stream_interactor, db); + stream_interactor.add_module(m); + } + + public async void create_file_transfer(Conversation conversation, Message message, string? file_sharing_id, Xep.FileMetadataElement.FileMetadata metadata, Gee.List<Xep.StatelessFileSharing.Source>? sources) { + FileTransfer file_transfer = new FileTransfer(); + file_transfer.file_sharing_id = file_sharing_id; + file_transfer.account = message.account; + file_transfer.counterpart = message.counterpart; + file_transfer.ourpart = message.ourpart; + file_transfer.direction = message.direction; + file_transfer.time = message.time; + file_transfer.local_time = message.local_time; + file_transfer.provider = SFS_PROVIDER_ID; + file_transfer.file_metadata = metadata; + file_transfer.info = message.id.to_string(); + if (sources != null) { + file_transfer.sfs_sources = sources; + } + + stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); + + conversation.last_active = file_transfer.time; + file_manager.received_file(file_transfer, conversation); + } + + public void on_received_sources(Jid from, Conversation conversation, string attach_to_message_id, string? attach_to_file_id, Gee.List<Xep.StatelessFileSharing.Source> sources) { + Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_referencing_id(attach_to_message_id, conversation); + if (message == null) return; + + FileTransfer? file_transfer = null; + if (attach_to_file_id != null) { + file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_files_by_message_and_file_id(message.id, attach_to_file_id, conversation); + } else { + file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_message_id(message.id, conversation); + } + if (file_transfer == null) return; + + // "If no <hash/> is provided or the <hash/> elements provided use unsupported algorithms, receiving clients MUST ignore + // any attached sources from other senders and only obtain the file from the sources announced by the original sender." + // For now we only allow the original sender + if (from.equals(file_transfer.from) && Xep.CryptographicHashes.get_supported_hashes(file_transfer.hashes).is_empty) { + warning("Ignoring sfs source: Not from original sender or no known file hashes"); + return; + } + + foreach (var source in sources) { + file_transfer.add_sfs_source(source); + } + + if (file_manager.is_sender_trustworthy(file_transfer, conversation) && file_transfer.state == FileTransfer.State.NOT_STARTED && file_transfer.size >= 0 && file_transfer.size < 5000000) { + file_manager.download_file(file_transfer); + } + } + + /* + public async void create_sfs_for_legacy_transfer(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) { + FileTransfer file_transfer = file_manager.create_file_transfer_from_provider_incoming(file_provider, info, from, time, local_time, conversation, receive_data, file_meta); + + HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData; + if (http_receive_data == null) return; + + var sources = new ArrayList<Xep.StatelessFileSharing.Source>(); + Xep.StatelessFileSharing.HttpSource source = new Xep.StatelessFileSharing.HttpSource(); + source.url = http_receive_data.url; + sources.add(source); + + if (file_manager.is_jid_trustworthy(from, conversation)) { + try { + file_meta = yield file_provider.get_meta_info(file_transfer, http_receive_data, file_meta); + } catch (Error e) { + warning("Http meta request failed: %s", e.message); + } + } + + var metadata = new Xep.FileMetadataElement.FileMetadata(); + metadata.size = file_meta.size; + metadata.name = file_meta.file_name; + metadata.mime_type = file_meta.mime_type; + + file_transfer.provider = SFS_PROVIDER_ID; + file_transfer.file_metadata = metadata; + file_transfer.sfs_sources = sources; + } + */ + + private class ReceivedMessageListener : MessageListener { + + public string[] after_actions_const = new string[]{ "STORE" }; + public override string action_group { get { return "MESSAGE_REINTERPRETING"; } } + public override string[] after_actions { get { return after_actions_const; } } + + private StatelessFileSharing outer; + private StreamInteractor stream_interactor; + + public ReceivedMessageListener(StatelessFileSharing outer) { + this.outer = outer; + this.stream_interactor = outer.stream_interactor; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + Gee.List<Xep.StatelessFileSharing.FileShare> file_shares = Xep.StatelessFileSharing.get_file_shares(stanza); + if (file_shares != null) { + // For now, only accept file shares that have at least one supported hash + foreach (Xep.StatelessFileSharing.FileShare file_share in file_shares) { + if (!Xep.CryptographicHashes.has_supported_hashes(file_share.metadata.hashes)) { + return false; + } + } + foreach (Xep.StatelessFileSharing.FileShare file_share in file_shares) { + outer.create_file_transfer(conversation, message, file_share.id, file_share.metadata, file_share.sources); + } + return true; + } + + var source_attachments = Xep.StatelessFileSharing.get_source_attachments(stanza); + if (source_attachments != null) { + foreach (var source_attachment in source_attachments) { + outer.on_received_sources(stanza.from, conversation, source_attachment.to_message_id, source_attachment.to_file_transfer_id, source_attachment.sources); + return true; + } + } + return false; + } + } +}
\ No newline at end of file diff --git a/libdino/src/util/limit_input_stream.vala b/libdino/src/util/limit_input_stream.vala new file mode 100644 index 00000000..5569d778 --- /dev/null +++ b/libdino/src/util/limit_input_stream.vala @@ -0,0 +1,73 @@ +public class Dino.LimitInputStream : InputStream, PollableInputStream { + private InputStream inner; + public int64 max_bytes { public get; private set; } + public int64 retrieved_bytes { public get; private set; } + + public int64 remaining_bytes { get { + return max_bytes < 0 ? -1 : max_bytes - retrieved_bytes; + }} + + public LimitInputStream(InputStream inner, int64 max_bytes) { + this.inner = inner; + this.max_bytes = max_bytes; + } + + public bool can_poll() { + return inner is PollableInputStream && ((PollableInputStream)inner).can_poll(); + } + + public PollableSource create_source(Cancellable? cancellable = null) { + if (!can_poll()) throw new IOError.NOT_SUPPORTED("Stream is not pollable"); + return ((PollableInputStream)inner).create_source(cancellable); + } + + public bool is_readable() { + if (!can_poll()) throw new IOError.NOT_SUPPORTED("Stream is not pollable"); + return remaining_bytes == 0 || ((PollableInputStream)inner).is_readable(); + } + + private ssize_t check_limit(ssize_t read) throws IOError { + if (remaining_bytes - (int64) read < 0) throw new IOError.FAILED("Stream length exceeded limit"); + this.retrieved_bytes += read; + return read; + } + + public override ssize_t read(uint8[] buffer, Cancellable? cancellable = null) throws IOError { + if (remaining_bytes == 0) return 0; + int original_buffer_length = buffer.length; + if (remaining_bytes != -1 && (int64) buffer.length > remaining_bytes) { + // Never read more than remaining_bytes by limiting the buffer length + buffer.length = (int) remaining_bytes; + } + ssize_t read_bytes = inner.read(buffer, cancellable); + this.retrieved_bytes += read_bytes; + buffer.length = original_buffer_length; + return read_bytes; + } + + public override async ssize_t read_async(uint8[]? buffer, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { + if (remaining_bytes == 0) return 0; + int original_buffer_length = buffer.length; + if (remaining_bytes != -1 && (int64) buffer.length > remaining_bytes) { + // Never read more than remaining_bytes by limiting the buffer length + buffer.length = (int) remaining_bytes; + } + ssize_t read_bytes = yield inner.read_async(buffer, io_priority, cancellable); + this.retrieved_bytes += read_bytes; + buffer.length = original_buffer_length; + return read_bytes; + } + + public ssize_t read_nonblocking_fn(uint8[] buffer) throws Error { + if (!is_readable()) throw new IOError.WOULD_BLOCK("Stream is not readable"); + return read(buffer); + } + + public override bool close(Cancellable? cancellable = null) throws IOError { + return inner.close(cancellable); + } + + public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { + return yield inner.close_async(io_priority, cancellable); + } +}
\ No newline at end of file diff --git a/libdino/src/util/util.vala b/libdino/src/util/util.vala index 9f7ae45f..31f4c105 100644 --- a/libdino/src/util/util.vala +++ b/libdino/src/util/util.vala @@ -1,3 +1,6 @@ +using Gee; +using Xmpp; + namespace Dino { private extern const string SYSTEM_LIBDIR_NAME; @@ -90,4 +93,32 @@ public static void internationalize(string gettext_package, string locales_dir) Intl.bindtextdomain(gettext_package, locales_dir); } +public static async HashMap<ChecksumType, string> compute_file_hashes(File file, Gee.List<ChecksumType> checksum_types) { + var checksums = new Checksum[checksum_types.size]; + + for (int i = 0; i < checksum_types.size; i++) { + checksums[i] = new Checksum(checksum_types.get(i)); + } + + FileInputStream stream = yield file.read_async(); + uint8 fbuf[1024]; + size_t size; + while ((size = yield stream.read_async(fbuf)) > 0) { + for (int i = 0; i < checksum_types.size; i++) { + checksums[i].update(fbuf, size); + } + } + + var ret = new HashMap<ChecksumType, string>(); + for (int i = 0; i < checksum_types.size; i++) { + var checksum_type = checksum_types.get(i); + uint8[] digest = new uint8[64]; + size_t length = digest.length; + checksums[i].get_digest(digest, ref length); + string computed_hash = GLib.Base64.encode(digest[0:length]); + ret[checksum_type] = computed_hash; + } + return ret; +} + } |