diff options
author | Patiga <dev@patiga.eu> | 2022-06-28 12:11:17 +0200 |
---|---|---|
committer | fiaxh <git@lightrise.org> | 2024-11-14 10:20:12 -0600 |
commit | aaf4542e6208460c305db4be36b15dc832ddc95a (patch) | |
tree | ec7b60b0f0ea74e21403788e8345336bd0f3939b /xmpp-vala | |
parent | 909f569318835d11703c49fba7dbe49996759f38 (diff) | |
download | dino-aaf4542e6208460c305db4be36b15dc832ddc95a.tar.gz dino-aaf4542e6208460c305db4be36b15dc832ddc95a.zip |
Implement XEP-0447: Stateless file sharing
Diffstat (limited to 'xmpp-vala')
-rw-r--r-- | xmpp-vala/CMakeLists.txt | 4 | ||||
-rw-r--r-- | xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala | 49 | ||||
-rw-r--r-- | xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala | 136 | ||||
-rw-r--r-- | xmpp-vala/src/module/xep/0446_file_metadata_element.vala | 120 | ||||
-rw-r--r-- | xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala | 225 |
5 files changed, 534 insertions, 0 deletions
diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index 4a213fda..824b667e 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -125,10 +125,12 @@ SOURCES "src/module/xep/0249_direct_muc_invitations.vala" "src/module/xep/0260_jingle_socks5_bytestreams.vala" "src/module/xep/0261_jingle_in_band_bytestreams.vala" + "src/module/xep/0264_jingle_content_thumbnails.vala" "src/module/xep/0272_muji.vala" "src/module/xep/0280_message_carbons.vala" "src/module/xep/0297_stanza_forwarding.vala" "src/module/xep/0298_coin.vala" + "src/module/xep/0300_cryptographic_hashes.vala" "src/module/xep/0308_last_message_correction.vala" "src/module/xep/0313_message_archive_management.vala" "src/module/xep/0313_2_message_archive_management.vala" @@ -143,6 +145,8 @@ SOURCES "src/module/xep/0421_occupant_ids.vala" "src/module/xep/0428_fallback_indication.vala" "src/module/xep/0444_reactions.vala" + "src/module/xep/0446_file_metadata_element.vala" + "src/module/xep/0447_stateless_file_sharing.vala" "src/module/xep/0461_replies.vala" "src/module/xep/0482_call_invites.vala" "src/module/xep/pixbuf_storage.vala" diff --git a/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala b/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala new file mode 100644 index 00000000..cb281f80 --- /dev/null +++ b/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala @@ -0,0 +1,49 @@ +namespace Xmpp.Xep.JingleContentThumbnails { + public const string NS_URI = "urn:xmpp:thumbs:1"; + public const string STANZA_NAME = "thumbnail"; + + public class Thumbnail { + public string uri; + public string? media_type; + public int width; + public int height; + + const string URI_ATTRIBUTE = "uri"; + const string MIME_ATTRIBUTE = "media-type"; + const string WIDTH_ATTRIBUTE = "width"; + const string HEIGHT_ATTRIBUTE = "height"; + + public StanzaNode to_stanza_node() { + StanzaNode node = new StanzaNode.build(STANZA_NAME, NS_URI).add_self_xmlns() + .put_attribute(URI_ATTRIBUTE, this.uri); + if (this.media_type != null) { + node.put_attribute(MIME_ATTRIBUTE, this.media_type); + } + if (this.width != -1) { + node.put_attribute(WIDTH_ATTRIBUTE, this.width.to_string()); + } + if (this.height != -1) { + node.put_attribute(HEIGHT_ATTRIBUTE, this.height.to_string()); + } + return node; + } + + public static Thumbnail? from_stanza_node(StanzaNode node) { + Thumbnail thumbnail = new Thumbnail(); + thumbnail.uri = node.get_attribute(URI_ATTRIBUTE); + if (thumbnail.uri == null) { + return null; + } + thumbnail.media_type = node.get_attribute(MIME_ATTRIBUTE); + string? width = node.get_attribute(WIDTH_ATTRIBUTE); + if (width != null) { + thumbnail.width = int.parse(width); + } + string? height = node.get_attribute(HEIGHT_ATTRIBUTE); + if (height != null) { + thumbnail.height = int.parse(height); + } + return thumbnail; + } + } +}
\ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala b/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala new file mode 100644 index 00000000..00f9e2ee --- /dev/null +++ b/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala @@ -0,0 +1,136 @@ +using GLib; +using Gee; + +namespace Xmpp.Xep.CryptographicHashes { + public const string NS_URI = "urn:xmpp:hashes:2"; + + public enum HashCmp { + Match, + Mismatch, + None, + } + + public class Hash { + public string algo; + // hash encoded in Base64 + public string val; + + public static string hash_name(ChecksumType type) { + switch(type) { + case ChecksumType.MD5: + return "md5"; + case ChecksumType.SHA1: + return "sha-1"; + case ChecksumType.SHA256: + return "sha-256"; + case ChecksumType.SHA384: + return "sha-384"; + case ChecksumType.SHA512: + return "sha-512"; + } + return "(null)"; + } + + public static ChecksumType? supported_hash(string hash) { + switch (hash) { + case "sha-1": + return ChecksumType.SHA1; + case "sha-256": + return ChecksumType.SHA256; + case "sha-384": + return ChecksumType.SHA384; + case "sha-512": + return ChecksumType.SHA512; + } + return null; + } + + public Hash.from_data(GLib.ChecksumType type, uint8[] data) { + GLib.Checksum checksum = new GLib.Checksum(type); + checksum.update(data, data.length); + // 64 * 8 = 512 (sha-512 is the longest hash variant) + uint8[] digest = new uint8[64]; + size_t length = digest.length; + checksum.get_digest(digest, ref length); + this.algo = hash_name(type); + this.val = GLib.Base64.encode(digest[0:length]); + } + + public HashCmp compare(Hash other) { + if (this.algo != other.algo) { + return HashCmp.None; + } + if (this.val == other.val) { + return HashCmp.Match; + } else { + return HashCmp.Mismatch; + } + } + + public StanzaNode to_stanza_node() { + return new StanzaNode.build("hash", NS_URI).add_self_xmlns() + .put_attribute("algo", this.algo) + .put_node(new StanzaNode.text(this.val)); + } + + public Hash.from_stanza_node(StanzaNode node) { + this.algo = node.get_attribute("algo"); + this.val = node.get_string_content(); + } + } + + public class Hashes { + public Gee.List<Hash> hashes = new ArrayList<Hash>(); + + public Gee.List<ChecksumType> supported_hashes() { + Gee.List<ChecksumType> supported = new ArrayList<ChecksumType>(); + foreach (Hash hash in this.hashes) { + ChecksumType? hash_type = Hash.supported_hash(hash.algo); + if (hash_type != null) { + supported.add(hash_type); + } + } + return supported; + } + + public Hashes.from_data(Gee.List<ChecksumType> types, uint8[] data) { + foreach (ChecksumType type in types) { + this.hashes.add(new Hash.from_data(type, data)); + } + } + + public HashCmp compare(Hashes other) { + HashCmp cmp = HashCmp.None; + foreach (Hash this_hash in this.hashes) { + foreach (Hash other_hash in other.hashes) { + switch (this_hash.compare(other_hash)) { + case HashCmp.Mismatch: + return HashCmp.Mismatch; + case HashCmp.Match: + cmp = HashCmp.Match; + break; + case HashCmp.None: + continue; + } + } + } + return cmp; + } + + public Gee.List<StanzaNode> to_stanza_nodes() { + Gee.List<StanzaNode> nodes = new ArrayList<StanzaNode>(); + foreach (Hash hash in this.hashes) { + nodes.add(hash.to_stanza_node()); + } + return nodes; + } + + public Hashes.from_stanza_subnodes(StanzaNode node) { + Gee.List<StanzaNode> subnodes = node.get_subnodes("hash", NS_URI); + this.hashes = new ArrayList<Hash>(); + foreach (StanzaNode subnode in subnodes) { + this.hashes.add(new Hash.from_stanza_node(subnode)); + } + } + } +} diff --git a/xmpp-vala/src/module/xep/0446_file_metadata_element.vala b/xmpp-vala/src/module/xep/0446_file_metadata_element.vala new file mode 100644 index 00000000..d7fbb06f --- /dev/null +++ b/xmpp-vala/src/module/xep/0446_file_metadata_element.vala @@ -0,0 +1,120 @@ +using Xmpp.Xep.CryptographicHashes; + +namespace Xmpp.Xep.FileMetadataElement { + public const string NS_URI = "urn:xmpp:file:metadata:0"; + + public class FileMetadata { + public string name { get; set; } + public string? mime_type { get; set; } + public int64 size { get; set; default=-1; } + public string? desc { get; set; } + public DateTime? date { get; set; } + public int width { get; set; default=-1; } // Width of image in pixels + public int height { get; set; default=-1; } // Height of image in pixels + public CryptographicHashes.Hashes hashes = new CryptographicHashes.Hashes(); + public int64 length { get; set; default=-1; } // Length of audio/video in milliseconds + public Gee.List<Xep.JingleContentThumbnails.Thumbnail> thumbnails = new Gee.ArrayList<Xep.JingleContentThumbnails.Thumbnail>(); + + public StanzaNode to_stanza_node() { + StanzaNode node = new StanzaNode.build("file", NS_URI).add_self_xmlns() + .put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(this.name))); + if (this.mime_type != null) { + node.put_node(new StanzaNode.build("media_type", NS_URI).put_node(new StanzaNode.text(this.mime_type))); + } + if (this.size != -1) { + node.put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(this.size.to_string()))); + } + if (this.date != null) { + node.put_node(new StanzaNode.build("date", NS_URI).put_node(new StanzaNode.text(this.date.to_string()))); + } + if (this.desc != null) { + node.put_node(new StanzaNode.build("desc", NS_URI).put_node(new StanzaNode.text(this.desc))); + } + if (this.width != -1) { + node.put_node(new StanzaNode.build("width", NS_URI).put_node(new StanzaNode.text(this.width.to_string()))); + } + if (this.height != -1) { + node.put_node(new StanzaNode.build("height", NS_URI).put_node(new StanzaNode.text(this.height.to_string()))); + } + if (this.length != -1) { + node.put_node(new StanzaNode.build("length", NS_URI).put_node(new StanzaNode.text(this.length.to_string()))); + } + node.sub_nodes.add_all(this.hashes.to_stanza_nodes()); + foreach (Xep.JingleContentThumbnails.Thumbnail thumbnail in this.thumbnails) { + node.put_node(thumbnail.to_stanza_node()); + } + return node; + } + + public void add_to_message(MessageStanza message) { + StanzaNode node = this.to_stanza_node(); + printerr("Attaching file metadata:\n"); + printerr("%s\n", node.to_ansi_string(true)); + message.stanza.put_node(node); + } + + public static FileMetadata? from_stanza_node(StanzaNode node) { + FileMetadata metadata = new FileMetadata(); + // TODO: null checks on final values + StanzaNode? name_node = node.get_subnode("name"); + if (name_node == null || name_node.get_string_content() == null) { + return null; + } else { + metadata.name = name_node.get_string_content(); + } + StanzaNode? desc_node = node.get_subnode("desc"); + if (desc_node != null && desc_node.get_string_content() != null) { + metadata.desc = desc_node.get_string_content(); + } + StanzaNode? mime_node = node.get_subnode("media_type"); + if (mime_node != null && mime_node.get_string_content() != null) { + metadata.mime_type = mime_node.get_string_content(); + } + StanzaNode? size_node = node.get_subnode("size"); + if (size_node != null && size_node.get_string_content() != null) { + metadata.size = int64.parse(size_node.get_string_content()); + } + StanzaNode? date_node = node.get_subnode("date"); + if (date_node != null && date_node.get_string_content() != null) { + metadata.date = new DateTime.from_iso8601(date_node.get_string_content(), null); + } + StanzaNode? width_node = node.get_subnode("width"); + if (width_node != null && width_node.get_string_content() != null) { + metadata.width = int.parse(width_node.get_string_content()); + } + StanzaNode? height_node = node.get_subnode("height"); + if (height_node != null && height_node.get_string_content() != null) { + metadata.height = int.parse(height_node.get_string_content()); + } + StanzaNode? length_node = node.get_subnode("length"); + if (length_node != null && length_node.get_string_content() != null) { + metadata.length = int.parse(length_node.get_string_content()); + } + foreach (StanzaNode thumbnail_node in node.get_subnodes(Xep.JingleContentThumbnails.STANZA_NAME, Xep.JingleContentThumbnails.NS_URI)) { + Xep.JingleContentThumbnails.Thumbnail? thumbnail = Xep.JingleContentThumbnails.Thumbnail.from_stanza_node(thumbnail_node); + if (thumbnail != null) { + metadata.thumbnails.add(thumbnail); + } + } + metadata.hashes = new CryptographicHashes.Hashes.from_stanza_subnodes(node); + return metadata; + } + + public static FileMetadata? from_message(MessageStanza message) { + StanzaNode? node = message.stanza.get_subnode("file", NS_URI); + if (node == null) { + return null; + } + printerr("Parsing metadata from message:\n"); + printerr("%s\n", node.to_xml()); + FileMetadata metadata = FileMetadata.from_stanza_node(node); + if (metadata != null) { + printerr("Parsed metadata:\n"); + printerr("%s\n", metadata.to_stanza_node().to_ansi_string(true)); + } else { + printerr("Failed to parse metadata!\n"); + } + return FileMetadata.from_stanza_node(node); + } + } +} diff --git a/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala b/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala new file mode 100644 index 00000000..285bfe78 --- /dev/null +++ b/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala @@ -0,0 +1,225 @@ +using Xmpp; + +namespace Xmpp.Xep.StatelessFileSharing { + + + public const string STANZA_NAME = "file-transfer"; + + public interface SfsSource: Object { + public abstract string type(); + public abstract string serialize(); + + public abstract StanzaNode to_stanza_node(); + } + + public class HttpSource: Object, SfsSource { + public string url; + + public const string HTTP_NS_URI = "http://jabber.org/protocol/url-data"; + public const string HTTP_STANZA_NAME = "url-data"; + public const string HTTP_URL_ATTRIBUTE = "target"; + public const string SOURCE_TYPE = "http"; + + public string type() { + return SOURCE_TYPE; + } + + public string serialize() { + return this.to_stanza_node().to_xml(); + } + + public StanzaNode to_stanza_node() { + StanzaNode node = new StanzaNode.build(HTTP_STANZA_NAME, HTTP_NS_URI).add_self_xmlns(); + node.put_attribute(HTTP_URL_ATTRIBUTE, this.url); + return node; + } + + public static async HttpSource deserialize(string data) { + StanzaNode node = yield new StanzaReader.for_string(data).read_stanza_node(); + HttpSource source = HttpSource.from_stanza_node(node); + assert(source != null); + return source; + } + + public static HttpSource? from_stanza_node(StanzaNode node) { + string? url = node.get_attribute(HTTP_URL_ATTRIBUTE); + if (url == null) { + return null; + } + HttpSource source = new HttpSource(); + source.url = url; + return source; + } + + public static Gee.List<HttpSource> extract_sources(StanzaNode node) { + Gee.List<HttpSource> sources = new Gee.ArrayList<HttpSource>(); + foreach (StanzaNode http_node in node.get_subnodes(HTTP_STANZA_NAME, HTTP_NS_URI)) { + HttpSource? source = HttpSource.from_stanza_node(http_node); + if (source != null) { + sources.add(source); + } + } + return sources; + } + } + + public class SfsElement { + public Xep.FileMetadataElement.FileMetadata metadata = new Xep.FileMetadataElement.FileMetadata(); + public Gee.List<SfsSource> sources = new Gee.ArrayList<SfsSource>(); + + public static SfsElement? from_stanza_node(StanzaNode node) { + SfsElement element = new SfsElement(); + StanzaNode? metadata_node = node.get_subnode("file", Xep.FileMetadataElement.NS_URI); + if (metadata_node == null) { + return null; + } + Xep.FileMetadataElement.FileMetadata metadata = Xep.FileMetadataElement.FileMetadata.from_stanza_node(metadata_node); + if (metadata == null) { + return null; + } + element.metadata = metadata; + StanzaNode? sources_node = node.get_subnode("sources"); + if (sources_node == null) { + return null; + } + Gee.List<HttpSource> sources = HttpSource.extract_sources(sources_node); + if (sources.is_empty) { + return null; + } + element.sources = sources; + return element; + } + + public StanzaNode to_stanza_node() { + StanzaNode node = new StanzaNode.build(STANZA_NAME, NS_URI).add_self_xmlns(); + node.put_node(this.metadata.to_stanza_node()); + StanzaNode sources_node = new StanzaNode.build("sources", NS_URI); + Gee.List<StanzaNode> sources = new Gee.ArrayList<StanzaNode>(); + foreach (SfsSource source in this.sources) { + sources.add(source.to_stanza_node()); + } + sources_node.sub_nodes = sources; + node.put_node(sources_node); + return node; + } + } + + public class SfsSourceAttachment { + public string sfs_id; + public Gee.List<SfsSource> sources = new Gee.ArrayList<SfsSource>(); + + public const string ATTACHMENT_NS_URI = "urn:xmpp:message-attaching:1"; + public const string ATTACH_TO_STANZA_NAME = "attach-to"; + public const string SOURCES_STANZA_NAME = "sources"; + public const string ID_ATTRIBUTE_NAME = "id"; + + + public static SfsSourceAttachment? from_message_stanza(MessageStanza stanza) { + StanzaNode? attach_to = stanza.stanza.get_subnode(ATTACH_TO_STANZA_NAME, ATTACHMENT_NS_URI); + StanzaNode? sources = stanza.stanza.get_subnode(SOURCES_STANZA_NAME, NS_URI); + if (attach_to == null || sources == null) { + return null; + } + string? id = attach_to.get_attribute(ID_ATTRIBUTE_NAME, ATTACHMENT_NS_URI); + if (id == null) { + return null; + } + SfsSourceAttachment attachment = new SfsSourceAttachment(); + attachment.sfs_id = id; + Gee.List<HttpSource> http_sources = HttpSource.extract_sources(sources); + if (http_sources.is_empty) { + return null; + } + attachment.sources = http_sources; + return attachment; + } + + public MessageStanza to_message_stanza(Jid to, string message_type) { + MessageStanza stanza = new MessageStanza() { to=to, type_=message_type }; + Xep.MessageProcessingHints.set_message_hint(stanza, Xep.MessageProcessingHints.HINT_STORE); + + StanzaNode attach_to = new StanzaNode.build(ATTACH_TO_STANZA_NAME, ATTACHMENT_NS_URI); + attach_to.add_attribute(new StanzaAttribute.build(ATTACHMENT_NS_URI, "id", this.sfs_id)); + stanza.stanza.put_node(attach_to); + + StanzaNode sources = new StanzaNode.build(SOURCES_STANZA_NAME, NS_URI); + Gee.List<StanzaNode> sources_nodes = new Gee.ArrayList<StanzaNode>(); + foreach (SfsSource source in this.sources) { + sources_nodes.add(source.to_stanza_node()); + } + sources.sub_nodes = sources_nodes; + stanza.stanza.put_node(sources); + + return stanza; + } + } + + public class MessageFlag : Xmpp.MessageFlag { + public const string ID = "stateless_file_sharing"; + + public static MessageFlag? get_flag(MessageStanza message) { + return (MessageFlag) message.get_flag(NS_URI, ID); + } + + public override string get_ns() { + return NS_URI; + } + + public override string get_id() { + return ID; + } + } + + public class Module : XmppStreamModule { + public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "stateless_file_sharing"); + + public signal void received_sfs(Jid from, Jid to, SfsElement sfs_element, MessageStanza message); + public signal void received_sfs_attachment(Jid from, Jid to, SfsSourceAttachment attachment, MessageStanza message); + + public void send_stateless_file_transfer(XmppStream stream, MessageStanza sfs_message, SfsElement sfs_element) { + StanzaNode sfs_node = sfs_element.to_stanza_node(); + printerr(sfs_node.to_ansi_string(true)); + + sfs_message.stanza.put_node(sfs_node); + printerr("Sending message:\n"); + printerr(sfs_message.stanza.to_ansi_string(true)); + stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, sfs_message); + } + + public void send_stateless_file_transfer_attachment(XmppStream stream, Jid to, string message_type, SfsSourceAttachment attachment) { + MessageStanza message = attachment.to_message_stanza(to, message_type); + + printerr("Sending message:\n"); + printerr(message.stanza.to_ansi_string(true)); + stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message); + } + + private void on_received_message(XmppStream stream, MessageStanza message) { + StanzaNode? sfs_node = message.stanza.get_subnode(STANZA_NAME, NS_URI); + if (sfs_node != null) { + SfsElement? sfs_element = SfsElement.from_stanza_node(sfs_node); + if (sfs_element == null) { + return; + } + message.add_flag(new MessageFlag()); + received_sfs(message.from, message.to, sfs_element, message); + } + SfsSourceAttachment? attachment = SfsSourceAttachment.from_message_stanza(message); + if (attachment != null) { + received_sfs_attachment(message.from, message.to, attachment, message); + } + + } + + public override void attach(XmppStream stream) { + stream.get_module(MessageModule.IDENTITY).received_message.connect(on_received_message); + } + + public override void detach(XmppStream stream) { + stream.get_module(MessageModule.IDENTITY).received_message.disconnect(on_received_message); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } + } +} |