From a0a956ee0878d24bd06be7f5d75dc4ccd4e7901d Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sun, 22 Dec 2019 04:10:53 +0100 Subject: [PATCH] Properly check Jids everywhere --- CMakeLists.txt | 1 + libdino/src/application.vala | 8 +- libdino/src/entity/account.vala | 30 +++- libdino/src/entity/conversation.vala | 2 +- libdino/src/entity/file_transfer.vala | 2 +- libdino/src/entity/message.vala | 2 +- libdino/src/service/content_item_store.vala | 24 ++- libdino/src/service/database.vala | 54 ++++-- libdino/src/service/file_manager.vala | 10 +- libdino/src/service/message_processor.vala | 2 +- libdino/src/service/muc_manager.vala | 12 +- libdino/src/service/roster_manager.vala | 14 +- libdino/src/service/search_processor.vala | 44 +++-- main/CMakeLists.txt | 6 +- .../add_conference_dialog.vala | 2 +- .../add_conversation/add_contact_dialog.vala | 23 ++- .../add_groupchat_dialog.vala | 38 +++-- .../conference_details_fragment.vala | 82 ++++----- .../add_conversation/select_jid_fragment.vala | 22 ++- main/src/ui/application.vala | 18 +- main/src/ui/chat_input_controller.vala | 6 +- .../content_item_widget_factory.vala | 1 - .../manage_accounts/add_account_dialog.vala | 117 ++++++++----- main/src/ui/util/helper.vala | 6 +- main/vapi/icu-uc.vapi | 16 +- plugins/omemo/src/logic/manager.vala | 8 +- plugins/omemo/src/logic/trust_manager.vala | 12 +- .../omemo/src/ui/contact_details_dialog.vala | 6 +- xmpp-vala/CMakeLists.txt | 7 + xmpp-vala/src/core/xmpp_stream.vala | 6 +- xmpp-vala/src/module/bind.vala | 18 +- xmpp-vala/src/module/conference.vala | 4 +- xmpp-vala/src/module/jid.vala | 159 ++++++++++++++---- xmpp-vala/src/module/roster/item.vala | 11 +- xmpp-vala/src/module/stanza.vala | 15 +- .../0030_service_discovery/items_result.vala | 10 +- xmpp-vala/src/module/xep/0045_muc/module.vala | 150 ++++++++++------- xmpp-vala/src/module/xep/0048_conference.vala | 10 +- .../module/xep/0065_socks5_bytestreams.vala | 6 +- xmpp-vala/src/module/xep/0166_jingle.vala | 12 +- .../xep/0260_jingle_socks5_bytestreams.vala | 6 +- xmpp-vala/src/module/xep/0402_bookmarks2.vala | 25 ++- xmpp-vala/tests/common.vala | 8 + xmpp-vala/tests/jid.vala | 93 ++++++++++ xmpp-vala/vapi/icu-uc.vapi | 56 ++++++ 45 files changed, 844 insertions(+), 320 deletions(-) create mode 100644 xmpp-vala/tests/jid.vala create mode 100644 xmpp-vala/vapi/icu-uc.vapi diff --git a/CMakeLists.txt b/CMakeLists.txt index 509a5169..13c84af7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,7 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) set(GTK3_GLOBAL_VERSION 3.22) set(GLib_GLOBAL_VERSION 2.38) +set(ICU_GLOBAL_VERSION 57) if (NOT VALA_EXECUTABLE) unset(VALA_EXECUTABLE CACHE) diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 86283823..82d392f6 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -78,6 +78,12 @@ public interface Application : GLib.Application { while (jid[0] == '/') { jid = jid.substring(1); } + jid = Uri.unescape_string(jid); + try { + jid = new Xmpp.Jid(jid).to_string(); + } catch (Xmpp.InvalidJidError e) { + warning("Received invalid jid in xmpp:-URI: %s", e.message); + } string query = "message"; Gee.Map options = new Gee.HashMap(); if (m.length == 2) { @@ -85,7 +91,7 @@ public interface Application : GLib.Application { query = cmds[0]; for (int i = 1; i < cmds.length; ++i) { string[] opt = cmds[i].split("=", 2); - options[opt[0]] = opt.length == 2 ? opt[1] : ""; + options[Uri.unescape_string(opt[0])] = opt.length == 2 ? Uri.unescape_string(opt[1]) : ""; } } activate(); diff --git a/libdino/src/entity/account.vala b/libdino/src/entity/account.vala index f7257b5a..3eb75505 100644 --- a/libdino/src/entity/account.vala +++ b/libdino/src/entity/account.vala @@ -6,10 +6,11 @@ namespace Dino.Entities { public class Account : Object { public int id { get; set; } - public string localpart { get { return bare_jid.localpart; } } - public string domainpart { get { return bare_jid.domainpart; } } - public string resourcepart { get; set; } - public Jid bare_jid { get; private set; } + public string localpart { get { return full_jid.localpart; } } + public string domainpart { get { return full_jid.domainpart; } } + public string resourcepart { get { return full_jid.resourcepart;} } + public Jid bare_jid { owned get { return full_jid.bare_jid; } } + public Jid full_jid { get; private set; } public string? password { get; set; } public string display_name { owned get { return alias ?? bare_jid.to_string(); } @@ -23,17 +24,28 @@ public class Account : Object { public Account(Jid bare_jid, string? resourcepart, string? password, string? alias) { this.id = -1; - this.resourcepart = resourcepart ?? "dino." + Random.next_int().to_string("%x"); - this.bare_jid = bare_jid; + if (resourcepart != null) { + try { + this.full_jid = bare_jid.with_resource(resourcepart); + } catch (InvalidJidError e) { + warning("Tried to create account with invalid resource (%s), defaulting to auto generated", e.message); + } + } + if (this.full_jid == null) { + try { + this.full_jid = bare_jid.with_resource("dino." + Random.next_int().to_string("%x")); + } catch (InvalidJidError e) { + error("Auto-generated resource was invalid (%s)", e.message); + } + } this.password = password; this.alias = alias; } - public Account.from_row(Database db, Qlite.Row row) { + public Account.from_row(Database db, Qlite.Row row) throws InvalidJidError { this.db = db; id = row[db.account.id]; - resourcepart = row[db.account.resourcepart]; - bare_jid = new Jid(row[db.account.bare_jid]); + full_jid = new Jid(row[db.account.bare_jid]).with_resource(row[db.account.resourcepart]); password = row[db.account.password]; alias = row[db.account.alias]; enabled = row[db.account.enabled]; diff --git a/libdino/src/entity/conversation.vala b/libdino/src/entity/conversation.vala index 53c59bd2..d0ba4920 100644 --- a/libdino/src/entity/conversation.vala +++ b/libdino/src/entity/conversation.vala @@ -47,7 +47,7 @@ public class Conversation : Object { this.type_ = type; } - public Conversation.from_row(Database db, Qlite.Row row) { + public Conversation.from_row(Database db, Qlite.Row row) throws InvalidJidError { this.db = db; id = row[db.conversation.id]; diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala index 9c0099d0..a8e386bf 100644 --- a/libdino/src/entity/file_transfer.vala +++ b/libdino/src/entity/file_transfer.vala @@ -63,7 +63,7 @@ public class FileTransfer : Object { private Database? db; private string storage_dir; - public FileTransfer.from_row(Database db, Qlite.Row row, string storage_dir) { + public FileTransfer.from_row(Database db, Qlite.Row row, string storage_dir) throws InvalidJidError { this.db = db; this.storage_dir = storage_dir; diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala index 4b16d12c..d5697c72 100644 --- a/libdino/src/entity/message.vala +++ b/libdino/src/entity/message.vala @@ -60,7 +60,7 @@ public class Message : Object { this.body = body; } - public Message.from_row(Database db, Qlite.Row row) { + public Message.from_row(Database db, Qlite.Row row) throws InvalidJidError { this.db = db; id = row[db.message.id]; diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index 7baba1fd..03278b4b 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -49,19 +49,29 @@ public class ContentItemStore : StreamInteractionModule, Object { case 1: RowOption row_option = db.message.select().with(db.message.id, "=", foreign_id).row(); if (row_option.is_present()) { - Message message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation); + Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation); if (message == null) { - message = new Message.from_row(db, row_option.inner); + try { + message = new Message.from_row(db, row_option.inner); + } catch (InvalidJidError e) { + warning("Ignoring message with invalid Jid: %s", e.message); + } + } + if (message != null) { + items.add(new MessageItem(message, conversation, row[db.content_item.id])); } - items.add(new MessageItem(message, conversation, row[db.content_item.id])); } break; case 2: RowOption row_option = db.file_transfer.select().with(db.file_transfer.id, "=", foreign_id).row(); if (row_option.is_present()) { - string storage_dir = FileManager.get_storage_dir(); - FileTransfer file_transfer = new FileTransfer.from_row(db, row_option.inner, storage_dir); - items.add(new FileItem(file_transfer, row[db.content_item.id])); + try { + string storage_dir = FileManager.get_storage_dir(); + FileTransfer file_transfer = new FileTransfer.from_row(db, row_option.inner, storage_dir); + items.add(new FileItem(file_transfer, row[db.content_item.id])); + } catch (InvalidJidError e) { + warning("Ignoring file transfer with invalid Jid: %s", e.message); + } } break; } @@ -256,7 +266,7 @@ public class FileItem : ContentItem { public Conversation conversation; public FileItem(FileTransfer file_transfer, int id) { - Jid jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart; + Jid jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.full_jid : file_transfer.counterpart; Entities.Message.Marked mark = Entities.Message.Marked.NONE; if (file_transfer.direction == FileTransfer.DIRECTION_SENT) { mark = file_to_message_state(file_transfer.state); diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index e230c3af..792b4b5f 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -311,9 +311,13 @@ public class Database : Qlite.Database { public ArrayList get_accounts() { ArrayList ret = new ArrayList(Account.equals_func); foreach(Row row in account.select()) { - Account account = new Account.from_row(this, row); - ret.add(account); - account_table_cache[account.id] = account; + try { + Account account = new Account.from_row(this, row); + ret.add(account); + account_table_cache[account.id] = account; + } catch (InvalidJidError e) { + warning("Ignoring account with invalid Jid: %s", e.message); + } } return ret; } @@ -324,9 +328,13 @@ public class Database : Qlite.Database { } else { Row? row = account.row_with(account.id, id).inner; if (row != null) { - Account a = new Account.from_row(this, row); - account_table_cache[a.id] = a; - return a; + try { + Account a = new Account.from_row(this, row); + account_table_cache[a.id] = a; + return a; + } catch (InvalidJidError e) { + warning("Ignoring account with invalid Jid: %s", e.message); + } } return null; } @@ -380,7 +388,11 @@ public class Database : Qlite.Database { LinkedList ret = new LinkedList(); foreach (Row row in select) { - ret.insert(0, new Message.from_row(this, row)); + try { + ret.insert(0, new Message.from_row(this, row)); + } catch (InvalidJidError e) { + warning("Ignoring message with invalid Jid: %s", e.message); + } } return ret; } @@ -394,7 +406,11 @@ public class Database : Qlite.Database { select.with(message.counterpart_id, "=", get_jid_id(jid)); } foreach (Row row in select) { - ret.add(new Message.from_row(this, row)); + try { + ret.add(new Message.from_row(this, row)); + } catch (InvalidJidError e) { + warning("Ignoring message with invalid Jid: %s", e.message); + } } return ret; } @@ -402,7 +418,11 @@ public class Database : Qlite.Database { public Message? get_message_by_id(int id) { Row? row = message.row_with(message.id, id).inner; if (row != null) { - return new Message.from_row(this, row); + try { + return new Message.from_row(this, row); + } catch (InvalidJidError e) { + warning("Ignoring message with invalid Jid: %s", e.message); + } } return null; } @@ -410,7 +430,11 @@ public class Database : Qlite.Database { public ArrayList get_conversations(Account account) { ArrayList ret = new ArrayList(); foreach (Row row in conversation.select().with(conversation.account_id, "=", account.id)) { - ret.add(new Conversation.from_row(this, row)); + try { + ret.add(new Conversation.from_row(this, row)); + } catch (InvalidJidError e) { + warning("Ignoring conversation with invalid Jid: %s", e.message); + } } return ret; } @@ -426,7 +450,11 @@ public class Database : Qlite.Database { public HashMap get_avatar_hashes(int type) { HashMap ret = new HashMap(Jid.hash_func, Jid.equals_func); foreach (Row row in avatar.select({avatar.jid, avatar.hash}).with(avatar.type_, "=", type)) { - ret[Jid.parse(row[avatar.jid])] = row[avatar.hash]; + try { + ret[new Jid(row[avatar.jid])] = row[avatar.hash]; + } catch (InvalidJidError e) { + warning("Ignoring avatar of invalid Jid: %s", e.message); + } } return ret; } @@ -466,13 +494,13 @@ public class Database : Qlite.Database { } } - public Jid? get_jid_by_id(int id) { + public Jid? get_jid_by_id(int id) throws InvalidJidError { if (jid_table_cache.has_key(id)) { return jid_table_cache[id]; } else { string? bare_jid = jid.select({jid.bare_jid}).with(jid.id, "=", id)[jid.bare_jid]; if (bare_jid != null) { - Jid jid_parsed = Jid.parse(bare_jid); + Jid jid_parsed = new Jid(bare_jid); jid_table_cache[id] = jid_parsed; jid_table_reverse[jid_parsed] = id; return jid_parsed; diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index a7d7b94a..5f3db040 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -169,8 +169,12 @@ public class FileManager : StreamInteractionModule, Object { private Gee.List get_transfers_from_qry(Qlite.QueryBuilder select) { Gee.List ret = new ArrayList(); foreach (Qlite.Row row in select) { - FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir()); - ret.insert(0, file_transfer); + try { + FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir()); + ret.insert(0, file_transfer); + } catch (InvalidJidError e) { + warning("Ignoring file transfer with invalid Jid: %s", e.message); + } } return ret; } @@ -287,7 +291,7 @@ public class FileManager : StreamInteractionModule, Object { if (conversation.type_ in new Conversation.Type[]{Conversation.Type.GROUPCHAT, Conversation.Type.GROUPCHAT_PM}) { file_transfer.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.bare_jid; } else { - file_transfer.ourpart = conversation.account.bare_jid.with_resource(conversation.account.resourcepart); + file_transfer.ourpart = conversation.account.full_jid; } file_transfer.time = time; file_transfer.local_time = local_time; diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index a7d186ef..a0000936 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -534,7 +534,7 @@ public class MessageProcessor : StreamInteractionModule, Object { message.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.bare_jid; message.real_jid = conversation.account.bare_jid; } else { - message.ourpart = conversation.account.bare_jid.with_resource(conversation.account.resourcepart); + message.ourpart = conversation.account.full_jid; } message.marked = Entities.Message.Marked.UNSENT; message.encryption = conversation.encryption; diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala index 01185731..018f78b3 100644 --- a/libdino/src/service/muc_manager.vala +++ b/libdino/src/service/muc_manager.vala @@ -239,10 +239,14 @@ public class MucManager : StreamInteractionModule, Object { } public Jid? get_own_jid(Jid muc_jid, Account account) { - Xep.Muc.Flag? flag = get_muc_flag(account); - if (flag != null) { - string? nick = flag.get_muc_nick(muc_jid); - if (nick != null) return muc_jid.with_resource(nick); + try { + Xep.Muc.Flag? flag = get_muc_flag(account); + if (flag != null) { + string? nick = flag.get_muc_nick(muc_jid); + if (nick != null) return muc_jid.with_resource(nick); + } + } catch (InvalidJidError e) { + warning("Joined MUC with invalid Jid: %s", e.message); } return null; } diff --git a/libdino/src/service/roster_manager.vala b/libdino/src/service/roster_manager.vala index 6f004c01..62428882 100644 --- a/libdino/src/service/roster_manager.vala +++ b/libdino/src/service/roster_manager.vala @@ -91,11 +91,15 @@ public class RosterStoreImpl : Roster.Storage, Object { this.db = db; foreach (Qlite.Row row in db.roster.select().with(db.roster.account_id, "=", account.id)) { - Roster.Item item = new Roster.Item(); - item.jid = Jid.parse(row[db.roster.jid]); - item.name = row[db.roster.handle]; - item.subscription = row[db.roster.subscription]; - items[item.jid] = item; + try { + Roster.Item item = new Roster.Item(); + item.jid = new Jid(row[db.roster.jid]); + item.name = row[db.roster.handle]; + item.subscription = row[db.roster.subscription]; + items[item.jid] = item; + } catch (InvalidJidError e) { + warning("Ignoring roster entry with invalid Jid: %s", e.message); + } } } diff --git a/libdino/src/service/search_processor.vala b/libdino/src/service/search_processor.vala index 37f524ac..9a41ad98 100644 --- a/libdino/src/service/search_processor.vala +++ b/libdino/src/service/search_processor.vala @@ -132,10 +132,14 @@ public class SearchProcessor : StreamInteractionModule, Object { .with(db.conversation.type_, "=", Conversation.Type.CHAT) .order_by(db.conversation.last_active, "DESC"); foreach(Row chat in chats) { - if (suggestions.size == 0) { - suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "from:"+chat[db.jid.bare_jid], after_prev_space, next_space)); + try { + if (suggestions.size == 0) { + suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "from:"+chat[db.jid.bare_jid], after_prev_space, next_space)); + } + suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.account.bare_jid]), "from:"+chat[db.account.bare_jid], after_prev_space, next_space)); + } catch (InvalidJidError e) { + warning("Ignoring search suggestion with invalid Jid: %s", e.message); } - suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.account.bare_jid]), "from:"+chat[db.account.bare_jid], after_prev_space, next_space)); } return suggestions; } @@ -155,7 +159,11 @@ public class SearchProcessor : StreamInteractionModule, Object { .order_by_name(@"MAX($(db.message.time))", "DESC") .limit(5); foreach(Row msg in msgs) { - suggestions.add(new SearchSuggestion(new Conversation.from_row(db, msg), new Jid(current_in).with_resource(msg[db.message.counterpart_resource]), "from:"+msg[db.message.counterpart_resource], after_prev_space, next_space)); + try { + suggestions.add(new SearchSuggestion(new Conversation.from_row(db, msg), new Jid(current_in).with_resource(msg[db.message.counterpart_resource]), "from:"+msg[db.message.counterpart_resource], after_prev_space, next_space)); + } catch (InvalidJidError e) { + warning("Ignoring search suggestion with invalid Jid: %s", e.message); + } } } // TODO: auto complete from @@ -181,7 +189,11 @@ public class SearchProcessor : StreamInteractionModule, Object { .order_by(db.conversation.last_active, "DESC") .limit(limit); foreach(Row chat in chats) { - suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "with:"+chat[db.jid.bare_jid], after_prev_space, next_space) { order = chat[db.conversation.last_active]}); + try { + suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "with:"+chat[db.jid.bare_jid], after_prev_space, next_space) { order = chat[db.conversation.last_active]}); + } catch (InvalidJidError e) { + warning("Ignoring search suggestion with invalid Jid: %s", e.message); + } } // Groupchat PM @@ -195,7 +207,11 @@ public class SearchProcessor : StreamInteractionModule, Object { .order_by(db.conversation.last_active, "DESC") .limit(limit - suggestions.size); foreach(Row chat in chats) { - suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.jid.bare_jid]).with_resource(chat[db.conversation.resource]), "with:"+chat[db.jid.bare_jid]+"/"+chat[db.conversation.resource], after_prev_space, next_space) { order = chat[db.conversation.last_active]}); + try { + suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.jid.bare_jid]).with_resource(chat[db.conversation.resource]), "with:"+chat[db.jid.bare_jid]+"/"+chat[db.conversation.resource], after_prev_space, next_space) { order = chat[db.conversation.last_active]}); + } catch (InvalidJidError e) { + warning("Ignoring search suggestion with invalid Jid: %s", e.message); + } } suggestions.sort((a, b) => (int)(b.order - a.order)); } @@ -218,7 +234,11 @@ public class SearchProcessor : StreamInteractionModule, Object { .order_by(db.conversation.last_active, "DESC") .limit(limit); foreach(Row chat in groupchats) { - suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "in:"+chat[db.jid.bare_jid], after_prev_space, next_space)); + try { + suggestions.add(new SearchSuggestion(new Conversation.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "in:"+chat[db.jid.bare_jid], after_prev_space, next_space)); + } catch (InvalidJidError e) { + warning("Ignoring search suggestion with invalid Jid: %s", e.message); + } } } else { // Other auto complete? @@ -233,9 +253,13 @@ public class SearchProcessor : StreamInteractionModule, Object { rows.offset(offset); } foreach (Row row in rows) { - Message message = new Message.from_row(db, row); - Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(message); - ret.add(new MessageItem(message, conversation, row[db.content_item.id])); + try { + Message message = new Message.from_row(db, row); + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(message); + ret.add(new MessageItem(message, conversation, row[db.content_item.id])); + } catch (InvalidJidError e) { + warning("Ignoring search result with invalid Jid: %s", e.message); + } } return ret; } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 893137ee..d5414084 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -5,11 +5,11 @@ gettext_compile(${GETTEXT_PACKAGE} SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/po TAR find_packages(MAIN_PACKAGES REQUIRED Gee - GLib>=2.38 + GLib GModule GObject - GTK3>=3.22 - ICU>=57 + GTK3 + ICU ) set(RESOURCE_LIST diff --git a/main/src/ui/add_conversation/add_conference_dialog.vala b/main/src/ui/add_conversation/add_conference_dialog.vala index ecafc9cc..a03f3db6 100644 --- a/main/src/ui/add_conversation/add_conference_dialog.vala +++ b/main/src/ui/add_conversation/add_conference_dialog.vala @@ -159,7 +159,7 @@ public class AddConferenceDialog : Gtk.Dialog { } private void set_ok_sensitive_from_details() { - ok_button.sensitive = select_fragment.done; + ok_button.sensitive = details_fragment.done; } private void on_next_button_clicked() { diff --git a/main/src/ui/add_conversation/add_contact_dialog.vala b/main/src/ui/add_conversation/add_contact_dialog.vala index 33f6fa72..7e9b5185 100644 --- a/main/src/ui/add_conversation/add_contact_dialog.vala +++ b/main/src/ui/add_conversation/add_contact_dialog.vala @@ -39,17 +39,24 @@ protected class AddContactDialog : Gtk.Dialog { private void on_ok_button_clicked() { string? alias = alias_entry.text == "" ? null : alias_entry.text; - Jid jid = new Jid(jid_entry.text); - stream_interactor.get_module(RosterManager.IDENTITY).add_jid(account, jid, alias); - stream_interactor.get_module(PresenceManager.IDENTITY).request_subscription(account, jid); - close(); + try { + Jid jid = new Jid(jid_entry.text); + stream_interactor.get_module(RosterManager.IDENTITY).add_jid(account, jid, alias); + stream_interactor.get_module(PresenceManager.IDENTITY).request_subscription(account, jid); + close(); + } catch (InvalidJidError e) { + warning("Tried to add contact with invalid Jid: %s", e.message); + } } private void on_jid_entry_changed() { - Jid parsed_jid = Jid.parse(jid_entry.text); - bool sensitive = parsed_jid != null && parsed_jid.resourcepart == null && - stream_interactor.get_module(RosterManager.IDENTITY).get_roster_item(account, parsed_jid) == null; - ok_button.set_sensitive(sensitive); + try { + Jid parsed_jid = new Jid(jid_entry.text); + ok_button. sensitive = parsed_jid != null && parsed_jid.resourcepart == null && + stream_interactor.get_module(RosterManager.IDENTITY).get_roster_item(account, parsed_jid) == null; + } catch (InvalidJidError e) { + ok_button.sensitive = false; + } } } diff --git a/main/src/ui/add_conversation/add_groupchat_dialog.vala b/main/src/ui/add_conversation/add_groupchat_dialog.vala index 33a3a455..da6b10b2 100644 --- a/main/src/ui/add_conversation/add_groupchat_dialog.vala +++ b/main/src/ui/add_conversation/add_groupchat_dialog.vala @@ -52,29 +52,41 @@ protected class AddGroupchatDialog : Gtk.Dialog { private bool on_jid_key_release() { check_ok(); if (!alias_entry_changed) { - Jid? parsed_jid = Jid.parse(jid_entry.text); - alias_entry.text = parsed_jid != null && parsed_jid.localpart != null ? parsed_jid.localpart : jid_entry.text; + try { + Jid parsed_jid = new Jid(jid_entry.text); + alias_entry.text = parsed_jid != null && parsed_jid.localpart != null ? parsed_jid.localpart : jid_entry.text; + } catch (InvalidJidError e) { + alias_entry.text = jid_entry.text; + } } return false; } private bool check_ok() { - Jid? parsed_jid = Jid.parse(jid_entry.text); - ok_button.sensitive = parsed_jid != null && parsed_jid.localpart != null && parsed_jid.resourcepart == null; + try { + Jid parsed_jid = new Jid(jid_entry.text); + ok_button.sensitive = parsed_jid != null && parsed_jid.localpart != null && parsed_jid.resourcepart == null; + } catch (InvalidJidError e) { + ok_button.sensitive = false; + } return false; } private void on_ok_button_clicked() { - Conference conference = new Conference(); - conference.jid = Jid.parse(jid_entry.text); - conference.nick = nick_entry.text != "" ? nick_entry.text : null; - conference.name = alias_entry.text; - if (edit_conference == null) { - stream_interactor.get_module(MucManager.IDENTITY).add_bookmark(account_combobox.selected, conference); - } else { - stream_interactor.get_module(MucManager.IDENTITY).replace_bookmark(account_combobox.selected, edit_conference, conference); + try { + Conference conference = new Conference(); + conference.jid = new Jid(jid_entry.text); + conference.nick = nick_entry.text != "" ? nick_entry.text : null; + conference.name = alias_entry.text; + if (edit_conference == null) { + stream_interactor.get_module(MucManager.IDENTITY).add_bookmark(account_combobox.selected, conference); + } else { + stream_interactor.get_module(MucManager.IDENTITY).replace_bookmark(account_combobox.selected, edit_conference, conference); + } + close(); + } catch (InvalidJidError e) { + warning("Ignoring invalid conference Jid: %s", e.message); } - close(); } } diff --git a/main/src/ui/add_conversation/conference_details_fragment.vala b/main/src/ui/add_conversation/conference_details_fragment.vala index 38232633..d0a4c42e 100644 --- a/main/src/ui/add_conversation/conference_details_fragment.vala +++ b/main/src/ui/add_conversation/conference_details_fragment.vala @@ -14,9 +14,12 @@ protected class ConferenceDetailsFragment : Box { public bool done { get { - Jid? parsed_jid = Jid.parse(jid); - return parsed_jid != null && parsed_jid.localpart != null && - parsed_jid.resourcepart == null && nick != ""; + try { + Jid parsed_jid = new Jid(jid); + return parsed_jid.localpart != null && parsed_jid.resourcepart == null && nick != null; + } catch (InvalidJidError e) { + return false; + } } private set {} } @@ -146,43 +149,46 @@ protected class ConferenceDetailsFragment : Box { ok_button.label = _("Joiningโ€ฆ"); ok_button.sensitive = false; - Jid parsed_jid = new Jid(jid); - Muc.JoinResult? join_result = yield stream_interactor.get_module(MucManager.IDENTITY).join(account, parsed_jid, nick, password); - - ok_button.label = _("Join"); - ok_button.sensitive = true; - if (join_result == null || join_result.nick != null) { - Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(parsed_jid, account, Conversation.Type.GROUPCHAT); - Application app = GLib.Application.get_default() as Application; - app.controller.select_conversation(conversation); - joined(); - return; - } - string label_text = ""; - if (join_result.muc_error != null) { - switch (join_result.muc_error) { - case Muc.MucEnterError.PASSWORD_REQUIRED: - label_text = _("Password required to enter room"); - password_text_label.visible = true; - password_stack.visible = true; - break; - case Muc.MucEnterError.BANNED: - label_text = _("Banned from joining or creating conference"); break; - case Muc.MucEnterError.ROOM_DOESNT_EXIST: - label_text = _("Room does not exist"); break; - case Muc.MucEnterError.CREATION_RESTRICTED: - label_text = _("Not allowed to create room"); break; - case Muc.MucEnterError.NOT_IN_MEMBER_LIST: - label_text = _("Members-only room"); break; - case Muc.MucEnterError.USE_RESERVED_ROOMNICK: - case Muc.MucEnterError.NICK_CONFLICT: - label_text = _("Choose a different nick"); break; - case Muc.MucEnterError.OCCUPANT_LIMIT_REACHED: - label_text = _("Too many occupants in room"); break; + try { + Jid parsed_jid = new Jid(jid); + Muc.JoinResult? join_result = yield stream_interactor.get_module(MucManager.IDENTITY).join(account, parsed_jid, nick, password); + + ok_button.label = _("Join"); + ok_button.sensitive = true; + if (join_result == null || join_result.nick != null) { + Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(parsed_jid, account, Conversation.Type.GROUPCHAT); + Application app = GLib.Application.get_default() as Application; + app.controller.select_conversation(conversation);joined(); + return; } - } else if (join_result.stanza_error != null) { - label_text = _("Could not connect to %s").printf((new Jid(jid)).domainpart); + + if (join_result.muc_error != null) { + switch (join_result.muc_error) { + case Muc.MucEnterError.PASSWORD_REQUIRED: + label_text = _("Password required to enter room"); + password_text_label.visible = true; + password_stack.visible = true; + break; + case Muc.MucEnterError.BANNED: + label_text = _("Banned from joining or creating conference"); break; + case Muc.MucEnterError.ROOM_DOESNT_EXIST: + label_text = _("Room does not exist"); break; + case Muc.MucEnterError.CREATION_RESTRICTED: + label_text = _("Not allowed to create room"); break; + case Muc.MucEnterError.NOT_IN_MEMBER_LIST: + label_text = _("Members-only room"); break; + case Muc.MucEnterError.USE_RESERVED_ROOMNICK: + case Muc.MucEnterError.NICK_CONFLICT: + label_text = _("Choose a different nick"); break; + case Muc.MucEnterError.OCCUPANT_LIMIT_REACHED: + label_text = _("Too many occupants in room"); break; + } + } else if (join_result.stanza_error != null) { + label_text = _("Could not connect to %s").printf((new Jid(jid)).domainpart); + } + } catch (InvalidJidError e) { + label_text = _("Invalid address"); } notification_label.label = label_text; notification_revealer.set_reveal_child(true); diff --git a/main/src/ui/add_conversation/select_jid_fragment.vala b/main/src/ui/add_conversation/select_jid_fragment.vala index 09792c75..f0170cfb 100644 --- a/main/src/ui/add_conversation/select_jid_fragment.vala +++ b/main/src/ui/add_conversation/select_jid_fragment.vala @@ -53,13 +53,17 @@ public class SelectJidFragment : Gtk.Box { string[] ? values = str == "" ? null : str.split(" "); filterable_list.set_filter_values(values); - Jid? parsed_jid = Jid.parse(str); - if (parsed_jid != null && parsed_jid.localpart != null) { - foreach (Account account in accounts) { - AddListRow row = new AddListRow(stream_interactor, str, account); - filterable_list.add(row); - added_rows.add(row); + try { + Jid parsed_jid = new Jid(str); + if (parsed_jid != null && parsed_jid.localpart != null) { + foreach (Account account in accounts) { + AddListRow row = new AddListRow(stream_interactor, parsed_jid, account); + filterable_list.add(row); + added_rows.add(row); + } } + } catch (InvalidJidError ignored) { + // Ignore } } @@ -82,11 +86,11 @@ public class SelectJidFragment : Gtk.Box { private class AddListRow : ListRow { - public AddListRow(StreamInteractor stream_interactor, string jid, Account account) { + public AddListRow(StreamInteractor stream_interactor, Jid jid, Account account) { this.account = account; - this.jid = new Jid(jid); + this.jid = jid; - name_label.label = jid; + name_label.label = jid.to_string(); if (stream_interactor.get_accounts().size > 1) { via_label.label = account.bare_jid.to_string(); } else { diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index c8db09da..2b4b2d3e 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -50,12 +50,18 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application { public void handle_uri(string jid, string query, Gee.Map options) { switch (query) { case "join": - show_join_muc_dialog(null, new Jid(jid)); + show_join_muc_dialog(null, jid); break; case "message": Gee.List accounts = stream_interactor.get_accounts(); - if (accounts.size == 1) { - Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(new Jid(jid), accounts[0], Conversation.Type.CHAT); + Jid parsed_jid = null; + try { + parsed_jid = new Jid(jid); + } catch (InvalidJidError ignored) { + // Ignored + } + if (accounts.size == 1 && parsed_jid != null) { + Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(parsed_jid, accounts[0], Conversation.Type.CHAT); stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(conversation); controller.select_conversation(conversation); } else { @@ -128,7 +134,7 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application { accept_muc_invite_action.activate.connect((variant) => { Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_by_id(variant.get_int32()); if (conversation == null) return; - show_join_muc_dialog(conversation.account, conversation.counterpart); + show_join_muc_dialog(conversation.account, conversation.counterpart.to_string()); }); add_action(accept_muc_invite_action); @@ -182,13 +188,13 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application { license_type: License.GPL_3_0); } - private void show_join_muc_dialog(Account? account, Jid jid) { + private void show_join_muc_dialog(Account? account, string jid) { Dialog dialog = new Dialog.with_buttons(_("Join Channel"), window, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.USE_HEADER_BAR, _("Join"), ResponseType.OK, _("Cancel"), ResponseType.CANCEL); dialog.modal = true; Button ok_button = dialog.get_widget_for_response(ResponseType.OK) as Button; ok_button.get_style_context().add_class("suggested-action"); ConferenceDetailsFragment conference_fragment = new ConferenceDetailsFragment(stream_interactor) { ok_button=ok_button }; - conference_fragment.jid = jid.to_string(); + conference_fragment.jid = jid; if (account != null) { conference_fragment.account = account; } diff --git a/main/src/ui/chat_input_controller.vala b/main/src/ui/chat_input_controller.vala index c386f55e..aafb40a3 100644 --- a/main/src/ui/chat_input_controller.vala +++ b/main/src/ui/chat_input_controller.vala @@ -108,7 +108,11 @@ public class ChatInputController : Object { return; case "/ping": Xmpp.XmppStream? stream = stream_interactor.get_stream(conversation.account); - stream.get_module(Xmpp.Xep.Ping.Module.IDENTITY).send_ping(stream, conversation.counterpart.with_resource(token[1]), null); + try { + stream.get_module(Xmpp.Xep.Ping.Module.IDENTITY).send_ping(stream, conversation.counterpart.with_resource(token[1]), null); + } catch (Xmpp.InvalidJidError e) { + warning("Could not ping invalid Jid: %s", e.message); + } return; case "/topic": stream_interactor.get_module(MucManager.IDENTITY).change_subject(conversation.account, conversation.counterpart, token[1]); diff --git a/main/src/ui/conversation_summary/content_item_widget_factory.vala b/main/src/ui/conversation_summary/content_item_widget_factory.vala index 00972371..54283e75 100644 --- a/main/src/ui/conversation_summary/content_item_widget_factory.vala +++ b/main/src/ui/conversation_summary/content_item_widget_factory.vala @@ -3,7 +3,6 @@ using Gdk; using Gtk; using Pango; using Xmpp; -using Unicode; using Dino.Entities; diff --git a/main/src/ui/manage_accounts/add_account_dialog.vala b/main/src/ui/manage_accounts/add_account_dialog.vala index 459e91d3..b1727c5d 100644 --- a/main/src/ui/manage_accounts/add_account_dialog.vala +++ b/main/src/ui/manage_accounts/add_account_dialog.vala @@ -73,6 +73,7 @@ public class AddAccountDialog : Gtk.Dialog { private Database db; private HashMap list_box_jids = new HashMap(); private Jid? server_jid = null; + private Jid? login_jid = null; private Xep.InBandRegistration.Form? form = null; public AddAccountDialog(StreamInteractor stream_interactor, Database db) { @@ -97,8 +98,12 @@ public class AddAccountDialog : Gtk.Dialog { // Select Server server_entry.changed.connect(() => { - Jid? jid = Jid.parse(server_entry.text); - select_server_continue.sensitive = jid != null && jid.localpart == null && jid.resourcepart == null; + try { + Jid jid = new Jid(server_entry.text); + select_server_continue.sensitive = jid != null && jid.localpart == null && jid.resourcepart == null; + } catch (InvalidJidError e) { + select_server_continue.sensitive = false; + } }); select_server_continue.clicked.connect(on_select_server_continue); login_button.clicked.connect(show_sign_in_jid); @@ -131,6 +136,7 @@ public class AddAccountDialog : Gtk.Dialog { set_default(sign_in_jid_continue_button); sign_in_jid_error_label.label = ""; + jid_entry.sensitive = true; animate_window_resize(sign_in_jid_box); } @@ -155,7 +161,7 @@ public class AddAccountDialog : Gtk.Dialog { set_default(sign_in_password_continue_button); sign_in_password_error_label.label = ""; - sign_in_password_title.label = _("Sign in to %s").printf(jid_entry.text); + sign_in_password_title.label = _("Sign in to %s").printf(login_jid.to_string()); animate_window_resize(sign_in_password_box); } @@ -205,46 +211,60 @@ public class AddAccountDialog : Gtk.Dialog { } private void on_jid_entry_changed() { - Jid? jid = Jid.parse(jid_entry.text); - if (jid != null && jid.localpart != null && jid.resourcepart == null) { - sign_in_jid_continue_button.set_sensitive(true); - jid_entry.secondary_icon_name = null; - } else { - sign_in_jid_continue_button.set_sensitive(false); + try { + login_jid = new Jid(jid_entry.text); + if (login_jid.localpart != null && login_jid.resourcepart == null) { + sign_in_jid_continue_button.sensitive = true; + jid_entry.secondary_icon_name = null; + } else { + sign_in_jid_continue_button.sensitive = false; + } + } catch (InvalidJidError e) { + sign_in_jid_continue_button.sensitive = false; } } private async void on_sign_in_jid_continue_button_clicked() { - Jid jid = new Jid(jid_entry.get_text()); - sign_in_jid_continue_stack.visible_child_name = "spinner"; - Register.ServerAvailabilityReturn server_status = yield Register.check_server_availability(jid); - sign_in_jid_continue_stack.visible_child_name = "label"; - if (server_status.available) { - show_sign_in_password(); - } else { - if (server_status.error_flags != null) { - string error_desc = "The server could not prove that it is %s.".printf("" + jid.domainpart + ""); - if (TlsCertificateFlags.UNKNOWN_CA in server_status.error_flags) { - error_desc += " " + "Its security certificate is not trusted by your computer's operating system."; - } else if (TlsCertificateFlags.BAD_IDENTITY in server_status.error_flags) { - error_desc += " " + "Its security certificate is issued to another domain."; - } else if (TlsCertificateFlags.NOT_ACTIVATED in server_status.error_flags) { - error_desc += " " + "Its security certificate will only become valid in the future."; - } else if (TlsCertificateFlags.EXPIRED in server_status.error_flags) { - error_desc += " " + "Its security certificate is expired."; - } - sign_in_tls_label.label = error_desc; - show_tls_error(); + try { + login_jid = new Jid(jid_entry.text); + jid_entry.sensitive = false; + sign_in_tls_label.label = ""; + sign_in_jid_error_label.label = ""; + sign_in_jid_continue_button.sensitive = false; + sign_in_jid_continue_stack.visible_child_name = "spinner"; + Register.ServerAvailabilityReturn server_status = yield Register.check_server_availability(login_jid); + sign_in_jid_continue_stack.visible_child_name = "label"; + sign_in_jid_continue_button.sensitive = true; + if (server_status.available) { + show_sign_in_password(); } else { - sign_in_jid_error_label.label = _("Could not connect to %s").printf(jid.domainpart); + jid_entry.sensitive = true; + if (server_status.error_flags != null) { + string error_desc = "The server could not prove that it is %s.".printf("" + login_jid.domainpart + ""); + if (TlsCertificateFlags.UNKNOWN_CA in server_status.error_flags) { + error_desc += " " + "Its security certificate is not trusted by your computer's operating system."; + } else if (TlsCertificateFlags.BAD_IDENTITY in server_status.error_flags) { + error_desc += " " + "Its security certificate is issued to another domain."; + } else if (TlsCertificateFlags.NOT_ACTIVATED in server_status.error_flags) { + error_desc += " " + "Its security certificate will only become valid in the future."; + } else if (TlsCertificateFlags.EXPIRED in server_status.error_flags) { + error_desc += " " + "Its security certificate is expired."; + } + sign_in_tls_label.label = error_desc; + show_tls_error(); + } else { + sign_in_jid_error_label.label = _("Could not connect to %s").printf(login_jid.domainpart); + } } + } catch (InvalidJidError e) { + warning("Invalid address from interface allowed login: %s", e.message); + sign_in_jid_error_label.label = _("Invalid address"); } } private async void on_sign_in_password_continue_button_clicked() { - Jid jid = new Jid(jid_entry.get_text()); - string password = password_entry.get_text(); - Account account = new Account(jid, null, password, null); + string password = password_entry.text; + Account account = new Account(login_jid, null, password, null); sign_in_password_continue_stack.visible_child_name = "spinner"; ConnectionManager.ConnectionError.Source? error = yield stream_interactor.get_module(Register.IDENTITY).add_check_account(account); @@ -256,7 +276,7 @@ public class AddAccountDialog : Gtk.Dialog { sign_in_password_error_label.label = _("Wrong username or password"); break; default: - sign_in_password_error_label.label = "Something went wrong"; + sign_in_password_error_label.label = _("Something went wrong"); break; } } else { @@ -266,13 +286,23 @@ public class AddAccountDialog : Gtk.Dialog { } private void on_select_server_continue() { - server_jid = new Jid(server_entry.text); - request_show_register_form.begin(); + try { + server_jid = new Jid(server_entry.text); + request_show_register_form.begin(); + } catch (InvalidJidError e) { + warning("Invalid address from interface allowed server: %s", e.message); + display_notification(_("Invalid address")); + } } private void on_server_list_row_selected(ListBox box, ListBoxRow? row) { - server_jid = new Jid(list_box_jids[row]); - request_show_register_form.begin(); + try { + server_jid = new Jid(list_box_jids[row]); + request_show_register_form.begin(); + } catch (InvalidJidError e) { + warning("Invalid address from selected server: %s", e.message); + display_notification(_("Invalid address")); + } } private async void request_show_register_form() { @@ -341,9 +371,14 @@ public class AddAccountDialog : Gtk.Dialog { case "password": password = field.get_value_string(); break; } } - Account account = new Account(new Jid.components(username, server_jid.domainpart, null), null, password, null); - add_activate_account(account); - show_success(account); + try { + Account account = new Account(new Jid.components(username, server_jid.domainpart, null), null, password, null); + add_activate_account(account); + show_success(account); + } catch (InvalidJidError e) { + warning("Invalid address from components of registration: %s", e.message); + display_notification(_("Invalid address")); + } } else { display_notification(error); } diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index db44103a..b48fdb22 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -331,11 +331,11 @@ public int get_only_emoji_count(string markup_text) { emoji_no--; } - if (last_was_emoji && last_was_modifier_base && Unicode.has_binary_property(curchar, Unicode.Property.EMOJI_MODIFIER)) { + if (last_was_emoji && last_was_modifier_base && ICU.has_binary_property(curchar, ICU.Property.EMOJI_MODIFIER)) { // still an emoji, but no longer a modifier base last_was_modifier_base = false; - } else if (Unicode.has_binary_property(curchar, Unicode.Property.EMOJI_PRESENTATION)) { - if (Unicode.has_binary_property(curchar, Unicode.Property.EMOJI_MODIFIER_BASE)) { + } else if (ICU.has_binary_property(curchar, ICU.Property.EMOJI_PRESENTATION)) { + if (ICU.has_binary_property(curchar, ICU.Property.EMOJI_MODIFIER_BASE)) { last_was_modifier_base = true; } emoji_no++; diff --git a/main/vapi/icu-uc.vapi b/main/vapi/icu-uc.vapi index 23732a15..db1609ad 100644 --- a/main/vapi/icu-uc.vapi +++ b/main/vapi/icu-uc.vapi @@ -1,12 +1,14 @@ -namespace Unicode { - [CCode (cprefix = "UCHAR_", cheader_filename = "unicode/uchar.h")] - public enum Property { +namespace ICU { + +[CCode (cprefix = "UCHAR_", cheader_filename = "unicode/uchar.h")] +public enum Property { EMOJI, EMOJI_PRESENTATION, EMOJI_MODIFIER, EMOJI_MODIFIER_BASE, - } - - [CCode (cname = "u_hasBinaryProperty", cheader_filename = "unicode/uchar.h")] - public bool has_binary_property(unichar c, Property p); +} + +[CCode (cname = "u_hasBinaryProperty", cheader_filename = "unicode/uchar.h")] +public bool has_binary_property(unichar c, Property p); + } diff --git a/plugins/omemo/src/logic/manager.vala b/plugins/omemo/src/logic/manager.vala index 2bbd918c..98280a8b 100644 --- a/plugins/omemo/src/logic/manager.vala +++ b/plugins/omemo/src/logic/manager.vala @@ -202,8 +202,12 @@ public class Manager : StreamInteractionModule, Object { //Fetch the bundle for each new device int inc = 0; foreach (Row row in db.identity_meta.get_unknown_devices(identity_id, jid.bare_jid.to_string())) { - module.fetch_bundle(stream, Jid.parse(row[db.identity_meta.address_name]), row[db.identity_meta.device_id], false); - inc++; + try { + module.fetch_bundle(stream, new Jid(row[db.identity_meta.address_name]), row[db.identity_meta.device_id], false); + inc++; + } catch (InvalidJidError e) { + warning("Ignoring device with invalid Jid: %s", e.message); + } } if (inc > 0) { debug("new bundles %i/%i for %s", inc, device_list.size, jid.to_string()); diff --git a/plugins/omemo/src/logic/trust_manager.vala b/plugins/omemo/src/logic/trust_manager.vala index 1b8a9436..5a169a82 100644 --- a/plugins/omemo/src/logic/trust_manager.vala +++ b/plugins/omemo/src/logic/trust_manager.vala @@ -303,7 +303,11 @@ public class TrustManager { PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); string identity_key = Base64.encode(msg.identity_key.serialize()); foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) { - possible_jids.add(new Jid(row[db.identity_meta.address_name])); + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("Ignoring invalid jid from database: %s", e.message); + } } if (possible_jids.size != 1) { continue; @@ -311,7 +315,11 @@ public class TrustManager { } else { // If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid)) { - possible_jids.add(new Jid(row[db.identity_meta.address_name])); + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("Ignoring invalid jid from database: %s", e.message); + } } } } diff --git a/plugins/omemo/src/ui/contact_details_dialog.vala b/plugins/omemo/src/ui/contact_details_dialog.vala index 90bd0d3e..ed185685 100644 --- a/plugins/omemo/src/ui/contact_details_dialog.vala +++ b/plugins/omemo/src/ui/contact_details_dialog.vala @@ -148,7 +148,11 @@ public class ContactDetailsDialog : Gtk.Dialog { } }); foreach (Row device in plugin.db.identity_meta.get_unknown_devices(identity_id, jid.to_string())) { - module.fetch_bundle(stream, Jid.parse(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false); + try { + module.fetch_bundle(stream, new Jid(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false); + } catch (InvalidJidError e) { + warning("Ignoring device with invalid Jid: %s", e.message); + } } } diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index e059b068..246f0108 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -4,6 +4,7 @@ find_packages(ENGINE_PACKAGES REQUIRED GIO GLib GObject + ICU ) set(ENGINE_DEFINITIONS "") @@ -15,6 +16,7 @@ if(GIO_VERSION VERSION_GREATER "2.60") else() message(STATUS "No ALPN support, needs GIO >= 2.60") endif() +set(ENGINE_EXTRA_OPTIONS ${MAIN_EXTRA_OPTIONS} --vapidir=${CMAKE_CURRENT_SOURCE_DIR}/vapi) vala_precompile(ENGINE_VALA_C SOURCES @@ -104,6 +106,8 @@ CUSTOM_VAPIS "${CMAKE_CURRENT_SOURCE_DIR}/src/glib_fixes.vapi" DEFINITIONS ${ENGINE_DEFINITIONS} +OPTIONS + ${ENGINE_EXTRA_OPTIONS} ) add_custom_target(xmpp-vala-vapi @@ -128,12 +132,15 @@ if(BUILD_TESTS) "tests/common.vala" "tests/testcase.vala" + "tests/jid.vala" "tests/stanza.vala" "tests/util.vala" CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/xmpp-vala_internal.vapi PACKAGES ${ENGINE_PACKAGES} + OPTIONS + ${ENGINE_EXTRA_OPTIONS} ) add_definitions(${VALA_CFLAGS}) diff --git a/xmpp-vala/src/core/xmpp_stream.vala b/xmpp-vala/src/core/xmpp_stream.vala index 97d00004..39754ba1 100644 --- a/xmpp-vala/src/core/xmpp_stream.vala +++ b/xmpp-vala/src/core/xmpp_stream.vala @@ -44,7 +44,11 @@ public class XmppStream { } public async void connect(string? remote_name = null) throws IOStreamError { - if (remote_name != null) this.remote_name = Jid.parse(remote_name); + try { + if (remote_name != null) this.remote_name = new Jid(remote_name); + } catch (InvalidJidError e) { + throw new IOStreamError.CONNECT(@"Invalid remote name \"$remote_name\": $(e.message)"); + } attach_negotation_modules(); try { int min_priority = -1; diff --git a/xmpp-vala/src/module/bind.vala b/xmpp-vala/src/module/bind.vala index a3b0762b..89398bfb 100644 --- a/xmpp-vala/src/module/bind.vala +++ b/xmpp-vala/src/module/bind.vala @@ -5,11 +5,11 @@ namespace Xmpp.Bind { public class Module : XmppStreamNegotiationModule { public static ModuleIdentity IDENTITY = new ModuleIdentity(NS_URI, "bind_module"); - public string requested_resource { get; set; } + public string? requested_resource { get; set; } public signal void bound_to_resource(XmppStream stream, Jid my_jid); - public Module(string requested_resource) { + public Module(string? requested_resource) { this.requested_resource = requested_resource; } @@ -18,9 +18,13 @@ namespace Xmpp.Bind { if (flag == null || flag.finished) return; if (iq.type_ == Iq.Stanza.TYPE_RESULT) { - flag.my_jid = Jid.parse(iq.stanza.get_subnode("jid", NS_URI, true).get_string_content()); - flag.finished = true; - bound_to_resource(stream, flag.my_jid); + try { + flag.my_jid = new Jid(iq.stanza.get_subnode("jid", NS_URI, true).get_string_content()); + flag.finished = true; + bound_to_resource(stream, flag.my_jid); + } catch (InvalidJidError e) { + warning("Received invalid Jid when binding: %s", e.message); + } } } @@ -32,7 +36,9 @@ namespace Xmpp.Bind { if (bind != null) { var flag = new Flag(); StanzaNode bind_node = new StanzaNode.build("bind", NS_URI).add_self_xmlns(); - bind_node.put_node(new StanzaNode.build("resource", NS_URI).put_node(new StanzaNode.text(requested_resource))); + if (requested_resource != null) { + bind_node.put_node(new StanzaNode.build("resource", NS_URI).put_node(new StanzaNode.text(requested_resource))); + } stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.set(bind_node), iq_response_stanza); stream.add_flag(flag); } diff --git a/xmpp-vala/src/module/conference.vala b/xmpp-vala/src/module/conference.vala index ee1be6b6..9b1172d8 100644 --- a/xmpp-vala/src/module/conference.vala +++ b/xmpp-vala/src/module/conference.vala @@ -1,14 +1,14 @@ namespace Xmpp { public class Conference : Object { - public virtual Jid jid { get; set; } + public virtual Jid? jid { get; set; } public virtual bool autojoin { get; set; } public virtual string? nick { get; set; } public virtual string? name { get; set; } public virtual string? password { get; set; } public static bool equal_func(Conference a, Conference b) { - return a.jid.equals(b.jid); + return Jid.equals_func(a.jid, b.jid); } public static uint hash_func(Conference a) { diff --git a/xmpp-vala/src/module/jid.vala b/xmpp-vala/src/module/jid.vala index c20e0202..569be54f 100644 --- a/xmpp-vala/src/module/jid.vala +++ b/xmpp-vala/src/module/jid.vala @@ -6,60 +6,126 @@ public class Jid { public string? resourcepart; public Jid bare_jid { - owned get { return is_bare() ? this : new Jid.components(localpart, domainpart, null); } + owned get { return is_bare() ? this : new Jid.intern(null, localpart, domainpart, null); } } public Jid domain_jid { - owned get { return is_domain() ? this : new Jid.components(null, domainpart, null); } + owned get { return is_domain() ? this : new Jid.intern(domainpart, null, domainpart, null); } } private string jid; - public Jid(string jid) { - Jid? parsed = Jid.parse(jid); - string? localpart = parsed != null ? (owned) parsed.localpart : null; - string domainpart = parsed != null ? (owned) parsed.domainpart : jid; - string? resourcepart = parsed != null ? (owned) parsed.resourcepart : null; - this.intern(jid, (owned) localpart, (owned) domainpart, (owned) resourcepart); + public Jid(string jid) throws InvalidJidError { + int slash_index = jid.index_of("/"); + int at_index = jid.index_of("@"); + if (at_index > slash_index && slash_index != -1) at_index = -1; + string resourcepart = slash_index < 0 ? null : jid.slice(slash_index + 1, jid.length); + string localpart = at_index < 0 ? null : jid.slice(0, at_index); + string domainpart; + if (at_index < 0) { + if (slash_index < 0) { + domainpart = jid; + } else { + domainpart = jid.slice(0, slash_index); + } + } else { + if (slash_index < 0) { + domainpart = jid.slice(at_index + 1, jid.length); + } else { + domainpart = jid.slice(at_index + 1, slash_index); + } + } + + this.components(localpart, domainpart, resourcepart); } - private Jid.intern(owned string jid, owned string? localpart, owned string domainpart, owned string? resourcepart) { + private Jid.intern(owned string? jid, owned string? localpart, owned string domainpart, owned string? resourcepart) { this.jid = (owned) jid; this.localpart = (owned) localpart; this.domainpart = (owned) domainpart; this.resourcepart = (owned) resourcepart; } - public Jid.components(owned string? localpart, owned string domainpart, owned string? resourcepart) { - string jid = domainpart; - if (localpart != null) { - jid = @"$localpart@$jid"; + public Jid.components(string? localpart, string domainpart, string? resourcepart) throws InvalidJidError { + // TODO verify and normalize all parts + if (domainpart.length == 0) throw new InvalidJidError.EMPTY_DOMAIN("Domain is empty"); + if (localpart != null && localpart.length == 0) throw new InvalidJidError.EMPTY_LOCAL("Localpart is empty but non-null"); + if (resourcepart != null && resourcepart.length == 0) throw new InvalidJidError.EMPTY_RESOURCE("Resource is empty but non-null"); + string domain = domainpart[domainpart.length - 1] == '.' ? domainpart.substring(0, domainpart.length - 1) : domainpart; + if (domain.contains("xn--")) { + domain = idna_decode(domain); } - if (resourcepart != null) { - jid = @"$jid/$resourcepart"; - } - this.jid = jid; - this.localpart = (owned) localpart; - this.domainpart = (owned) domainpart; - this.resourcepart = (owned) resourcepart; + this.localpart = prepare(localpart, ICU.PrepType.RFC3920_NODEPREP); + this.domainpart = prepare(domain, ICU.PrepType.RFC3491_NAMEPREP); + this.resourcepart = prepare(resourcepart, ICU.PrepType.RFC3920_RESOURCEPREP); + idna_verify(this.domainpart); } - public static Jid? parse(string jid) { - int slash_index = jid.index_of("/"); - string resourcepart = slash_index == -1 ? null : jid.slice(slash_index + 1, jid.length); - string bare_jid = slash_index == -1 ? jid : jid.slice(0, slash_index); - int at_index = bare_jid.index_of("@"); - string localpart = at_index == -1 ? null : bare_jid.slice(0, at_index); - string domainpart = at_index == -1 ? bare_jid : bare_jid.slice(at_index + 1, bare_jid.length); - - if (domainpart == "") return null; - if (slash_index != -1 && resourcepart == "") return null; - if (at_index != -1 && localpart == "") return null; - - return new Jid.intern(jid, (owned) localpart, (owned) domainpart, (owned) resourcepart); + private static string idna_decode(string src) throws InvalidJidError { + try { + ICU.ErrorCode status = ICU.ErrorCode.ZERO_ERROR; + long src16_length = 0; + string16 src16 = src.to_utf16(-1, null, out src16_length); + ICU.Char[] dest16 = new ICU.Char[src16_length]; + ICU.ParseError error; + long dest16_length = ICU.IDNA.IDNToUnicode(src16, (int32) src16_length, dest16, dest16.length, ICU.IDNAOptions.DEFAULT, out error, ref status); + if (status == ICU.ErrorCode.INVALID_CHAR_FOUND) { + throw new InvalidJidError.INVALID_CHAR("Found invalid character"); + } else if (status != ICU.ErrorCode.ZERO_ERROR) { + throw new InvalidJidError.UNKNOWN(@"Unknown error: $(status.errorName())"); + } else if (dest16_length < 0) { + throw new InvalidJidError.UNKNOWN("Unknown error"); + } + return ((string16) dest16).to_utf8(dest16_length, null, null); + } catch (ConvertError e) { + throw new InvalidJidError.INVALID_CHAR(@"Conversion error: $(e.message)"); + } } - public Jid with_resource(string? resourcepart) { + private static void idna_verify(string src) throws InvalidJidError { + try { + ICU.ErrorCode status = ICU.ErrorCode.ZERO_ERROR; + long src16_length = 0; + string16 src16 = src.to_utf16(-1, null, out src16_length); + ICU.Char[] dest16 = new ICU.Char[256]; + ICU.ParseError error; + long dest16_length = ICU.IDNA.IDNToASCII(src16, (int32) src16_length, dest16, dest16.length, ICU.IDNAOptions.DEFAULT, out error, ref status); + if (status == ICU.ErrorCode.INVALID_CHAR_FOUND) { + throw new InvalidJidError.INVALID_CHAR("Found invalid character"); + } else if (status != ICU.ErrorCode.ZERO_ERROR) { + throw new InvalidJidError.UNKNOWN(@"Unknown error: $(status.errorName())"); + } else if (dest16_length < 0) { + throw new InvalidJidError.UNKNOWN("Unknown error"); + } + } catch (ConvertError e) { + throw new InvalidJidError.INVALID_CHAR(@"Conversion error: $(e.message)"); + } + } + + private static string? prepare(string? src, ICU.PrepType type) throws InvalidJidError { + if (src == null) return src; + try { + ICU.ErrorCode status = ICU.ErrorCode.ZERO_ERROR; + ICU.PrepProfile profile = ICU.PrepProfile.openByType(type, ref status); + long src16_length = 0; + string16 src16 = src.to_utf16(-1, null, out src16_length); + ICU.Char[] dest16 = new ICU.Char[src16_length * 2]; + ICU.ParseError error; + long dest16_length = profile.prepare((ICU.Char*) src16, (int32) src16_length, dest16, dest16.length, ICU.PrepOptions.ALLOW_UNASSIGNED, out error, ref status); + if (status == ICU.ErrorCode.INVALID_CHAR_FOUND) { + throw new InvalidJidError.INVALID_CHAR("Found invalid character"); + } else if (status != ICU.ErrorCode.ZERO_ERROR) { + throw new InvalidJidError.UNKNOWN(@"Unknown error: $(status.errorName())"); + } else if (dest16_length < 0) { + throw new InvalidJidError.UNKNOWN("Unknown error"); + } + return ((string16) dest16).to_utf8(dest16_length, null, null); + } catch (ConvertError e) { + throw new InvalidJidError.INVALID_CHAR(@"Conversion error: $(e.message)"); + } + } + + public Jid with_resource(string? resourcepart) throws InvalidJidError { return new Jid.components(localpart, domainpart, resourcepart); } @@ -68,7 +134,7 @@ public class Jid { } public bool is_bare() { - return localpart != null && resourcepart == null; + return resourcepart == null; } public bool is_full() { @@ -76,6 +142,17 @@ public class Jid { } public string to_string() { + if (jid == null) { + if (localpart != null && resourcepart != null) { + jid = @"$localpart@$domainpart/$resourcepart"; + } else if (localpart != null) { + jid = @"$localpart@$domainpart"; + } else if (resourcepart != null) { + jid = @"$domainpart/$resourcepart"; + } else { + jid = domainpart; + } + } return jid; } @@ -88,11 +165,11 @@ public class Jid { } public static new bool equals_bare_func(Jid jid1, Jid jid2) { - return jid1.bare_jid.to_string() == jid2.bare_jid.to_string(); + return jid1.localpart == jid2.localpart && jid1.domainpart == jid2.domainpart; } public static bool equals_func(Jid jid1, Jid jid2) { - return jid1.to_string() == jid2.to_string(); + return equals_bare_func(jid1, jid2) && jid1.resourcepart == jid2.resourcepart; } public static new uint hash_bare_func(Jid jid) { @@ -104,4 +181,12 @@ public class Jid { } } +public errordomain InvalidJidError { + EMPTY_DOMAIN, + EMPTY_RESOURCE, + EMPTY_LOCAL, + INVALID_CHAR, + UNKNOWN +} + } diff --git a/xmpp-vala/src/module/roster/item.vala b/xmpp-vala/src/module/roster/item.vala index 2fbf03ef..78974a35 100644 --- a/xmpp-vala/src/module/roster/item.vala +++ b/xmpp-vala/src/module/roster/item.vala @@ -17,8 +17,15 @@ public class Item { public StanzaNode stanza_node; private Jid jid_; - public Jid jid { - get { return jid_ ?? (jid_ = Jid.parse(stanza_node.get_attribute(NODE_JID))); } + public Jid? jid { + get { + try { + return jid_ ?? (jid_ = new Jid(stanza_node.get_attribute(NODE_JID))); + } catch (InvalidJidError e) { + warning("Ignoring invalid Jid in roster entry: %s", e.message); + return null; + } + } set { stanza_node.set_attribute(NODE_JID, value.to_string()); } } diff --git a/xmpp-vala/src/module/stanza.vala b/xmpp-vala/src/module/stanza.vala index abdc33e9..f4e40f84 100644 --- a/xmpp-vala/src/module/stanza.vala +++ b/xmpp-vala/src/module/stanza.vala @@ -18,7 +18,13 @@ public class Stanza : Object { string? from_attribute = stanza.get_attribute(ATTRIBUTE_FROM); // "when a client receives a stanza that does not include a 'from' attribute, it MUST assume that the stanza // is from the user's account on the server." (RFC6120 8.1.2.1) - if (from_attribute != null) return from_ = Jid.parse(from_attribute); + if (from_attribute != null) { + try { + return from_ = new Jid(from_attribute); + } catch (InvalidJidError e) { + warning("Ignoring invalid from Jid: %s", e.message); + } + } if (my_jid != null) { return my_jid.bare_jid; } @@ -37,7 +43,12 @@ public class Stanza : Object { string? to_attribute = stanza.get_attribute(ATTRIBUTE_TO); // "if the stanza does not include a 'to' address then the client MUST treat it as if the 'to' address were // included with a value of the client's full JID." (RFC6120 8.1.1.1) - return to_attribute == null ? my_jid : to_ = Jid.parse(to_attribute); + try { + return to_attribute == null ? my_jid : to_ = new Jid(to_attribute); + } catch (InvalidJidError e) { + warning("Ignoring invalid to Jid: %s", e.message); + } + return my_jid; } set { stanza.set_attribute(ATTRIBUTE_TO, value.to_string()); } } diff --git a/xmpp-vala/src/module/xep/0030_service_discovery/items_result.vala b/xmpp-vala/src/module/xep/0030_service_discovery/items_result.vala index 16a9f5ec..233a0c06 100644 --- a/xmpp-vala/src/module/xep/0030_service_discovery/items_result.vala +++ b/xmpp-vala/src/module/xep/0030_service_discovery/items_result.vala @@ -9,9 +9,13 @@ public class ItemsResult { owned get { ArrayList ret = new ArrayList(); foreach (StanzaNode feature_node in iq.stanza.get_subnode("query", NS_URI_ITEMS).get_subnodes("item", NS_URI_ITEMS)) { - ret.add(new Item(Jid.parse(feature_node.get_attribute("jid", NS_URI_ITEMS)), - feature_node.get_attribute("name", NS_URI_ITEMS), - feature_node.get_attribute("node", NS_URI_ITEMS))); + try { + ret.add(new Item(new Jid(feature_node.get_attribute("jid", NS_URI_ITEMS)), + feature_node.get_attribute("name", NS_URI_ITEMS), + feature_node.get_attribute("node", NS_URI_ITEMS))); + } catch (InvalidJidError e) { + warning("Ignoring service at invalid Jid: %s", e.message); + } } return ret; } diff --git a/xmpp-vala/src/module/xep/0045_muc/module.vala b/xmpp-vala/src/module/xep/0045_muc/module.vala index 59d61f3d..ec10d500 100644 --- a/xmpp-vala/src/module/xep/0045_muc/module.vala +++ b/xmpp-vala/src/module/xep/0045_muc/module.vala @@ -82,41 +82,50 @@ public class Module : XmppStreamModule { } public async JoinResult? enter(XmppStream stream, Jid bare_jid, string nick, string? password, DateTime? history_since) { - Presence.Stanza presence = new Presence.Stanza(); - presence.to = bare_jid.with_resource(nick); - StanzaNode x_node = new StanzaNode.build("x", NS_URI).add_self_xmlns(); - if (password != null) { - x_node.put_node(new StanzaNode.build("password", NS_URI).put_node(new StanzaNode.text(password))); - } - if (history_since != null) { - StanzaNode history_node = new StanzaNode.build("history", NS_URI); - history_node.set_attribute("since", DateTimeProfiles.to_datetime(history_since)); - x_node.put_node(history_node); - } - presence.stanza.put_node(x_node); - - stream.get_flag(Flag.IDENTITY).start_muc_enter(bare_jid, presence.id); - - query_room_info(stream, bare_jid); - stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence); - - var promise = new Promise(); - stream.get_flag(Flag.IDENTITY).enter_futures[bare_jid] = promise; try { - JoinResult? enter_result = yield promise.future.wait_async(); - stream.get_flag(Flag.IDENTITY).enter_futures.unset(bare_jid); - return enter_result; - } catch (Gee.FutureError e) { - return null; + Presence.Stanza presence = new Presence.Stanza(); + presence.to = bare_jid.with_resource(nick); + + StanzaNode x_node = new StanzaNode.build("x", NS_URI).add_self_xmlns(); + if (password != null) { + x_node.put_node(new StanzaNode.build("password", NS_URI).put_node(new StanzaNode.text(password))); + } + if (history_since != null) { + StanzaNode history_node = new StanzaNode.build("history", NS_URI); + history_node.set_attribute("since", DateTimeProfiles.to_datetime(history_since)); + x_node.put_node(history_node); + } + presence.stanza.put_node(x_node); + + stream.get_flag(Flag.IDENTITY).start_muc_enter(bare_jid, presence.id); + + query_room_info(stream, bare_jid); + stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence); + + var promise = new Promise(); + stream.get_flag(Flag.IDENTITY).enter_futures[bare_jid] = promise; + try { + JoinResult? enter_result = yield promise.future.wait_async(); + stream.get_flag(Flag.IDENTITY).enter_futures.unset(bare_jid); + return enter_result; + } catch (Gee.FutureError e) { + return null; + } + } catch (InvalidJidError e) { + return new JoinResult() { muc_error = MucEnterError.NICK_CONFLICT }; } } public void exit(XmppStream stream, Jid jid) { - string nick = stream.get_flag(Flag.IDENTITY).get_muc_nick(jid); - Presence.Stanza presence = new Presence.Stanza(); - presence.to = jid.with_resource(nick); - presence.type_ = Presence.Stanza.TYPE_UNAVAILABLE; - stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence); + try { + string nick = stream.get_flag(Flag.IDENTITY).get_muc_nick(jid); + Presence.Stanza presence = new Presence.Stanza(); + presence.to = jid.with_resource(nick); + presence.type_ = Presence.Stanza.TYPE_UNAVAILABLE; + stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence); + } catch (InvalidJidError e) { + warning("Tried to leave room with invalid nick: %s", e.message); + } } public void change_subject(XmppStream stream, Jid jid, string subject) { @@ -128,9 +137,14 @@ public class Module : XmppStreamModule { } public void change_nick(XmppStream stream, Jid jid, string new_nick) { - Presence.Stanza presence = new Presence.Stanza(); - presence.to = jid.with_resource(new_nick); - stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence); + // TODO: Return if successful + try { + Presence.Stanza presence = new Presence.Stanza(); + presence.to = jid.with_resource(new_nick); + stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence); + } catch (InvalidJidError e) { + warning("Tried to change nick to invalid nick: %s", e.message); + } } public void invite(XmppStream stream, Jid to_muc, Jid jid) { @@ -148,20 +162,25 @@ public class Module : XmppStreamModule { /* XEP 0046: "A user cannot be kicked by a moderator with a lower affiliation." (XEP 0045 8.2) */ public bool kick_possible(XmppStream stream, Jid occupant) { - Jid muc_jid = occupant.bare_jid; - Flag flag = stream.get_flag(Flag.IDENTITY); - string own_nick = flag.get_muc_nick(muc_jid); - Affiliation my_affiliation = flag.get_affiliation(muc_jid, muc_jid.with_resource(own_nick)); - Affiliation other_affiliation = flag.get_affiliation(muc_jid, occupant); - switch (my_affiliation) { - case Affiliation.MEMBER: - if (other_affiliation == Affiliation.ADMIN || other_affiliation == Affiliation.OWNER) return false; - break; - case Affiliation.ADMIN: - if (other_affiliation == Affiliation.OWNER) return false; - break; + try { + Jid muc_jid = occupant.bare_jid; + Flag flag = stream.get_flag(Flag.IDENTITY); + string own_nick = flag.get_muc_nick(muc_jid); + Affiliation my_affiliation = flag.get_affiliation(muc_jid, muc_jid.with_resource(own_nick)); + Affiliation other_affiliation = flag.get_affiliation(muc_jid, occupant); + switch (my_affiliation) { + case Affiliation.MEMBER: + if (other_affiliation == Affiliation.ADMIN || other_affiliation == Affiliation.OWNER) return false; + break; + case Affiliation.ADMIN: + if (other_affiliation == Affiliation.OWNER) return false; + break; + } + return true; + } catch (InvalidJidError e) { + warning("Tried to kick with invalid nick: %s", e.message); + return false; } - return true; } public void change_role(XmppStream stream, Jid jid, string nick, string new_role) { @@ -314,12 +333,16 @@ public class Module : XmppStreamModule { } string? jid_ = x_node.get_deep_attribute("item", "jid"); if (jid_ != null) { - Jid? jid = Jid.parse(jid_); - flag.set_real_jid(presence.from, jid); - if (affiliation != null) { - stream.get_flag(Flag.IDENTITY).set_offline_member(presence.from, jid, affiliation); + try { + Jid jid = new Jid(jid_); + flag.set_real_jid(presence.from, jid); + if (affiliation != null) { + stream.get_flag(Flag.IDENTITY).set_offline_member(presence.from, jid, affiliation); + } + received_occupant_jid(stream, presence.from, jid); + } catch (InvalidJidError e) { + warning("Received invalid occupant jid: %s", e.message); } - received_occupant_jid(stream, presence.from, jid); } string? role_str = x_node.get_deep_attribute("item", "role"); if (role_str != null) { @@ -414,12 +437,17 @@ public class Module : XmppStreamModule { Gee.List item_nodes = query_node.get_subnodes("item", NS_URI_ADMIN); Gee.List ret_jids = new ArrayList(Jid.equals_func); foreach (StanzaNode item in item_nodes) { - Jid? jid_ = Jid.parse(item.get_attribute("jid")); + string jid__ = item.get_attribute("jid"); string? affiliation_ = item.get_attribute("affiliation"); - if (jid_ != null && affiliation_ != null) { - stream.get_flag(Flag.IDENTITY).set_offline_member(iq.from, jid_, parse_affiliation(affiliation_)); - ret_jids.add(jid_); - received_occupant_jid(stream, iq.from, jid_); + if (jid__ != null && affiliation_ != null) { + try { + Jid jid_ = new Jid(jid__); + stream.get_flag(Flag.IDENTITY).set_offline_member(iq.from, jid_, parse_affiliation(affiliation_)); + ret_jids.add(jid_); + received_occupant_jid(stream, iq.from, jid_); + } catch (InvalidJidError e) { + warning("Received invalid occupant jid: %s", e.message); + } } } if (listener != null) listener(stream, ret_jids); @@ -489,13 +517,19 @@ public class ReceivedPipelineListener : StanzaListener { StanzaNode? password_node = x_node.get_subnode("password", NS_URI_USER); if (password_node != null) password = password_node.get_string_content(); if (invite_node != null) { - string? from_jid = invite_node.get_attribute("from"); + Jid? from_jid = null; + try { + string from = invite_node.get_attribute("from"); + if (from != null) from_jid = new Jid(from); + } catch (InvalidJidError e) { + warning("Received invite from invalid jid: %s", e.message); + } if (from_jid != null) { StanzaNode? reason_node = invite_node.get_subnode("reason", NS_URI_USER); string? reason = null; if (reason_node != null) reason = reason_node.get_string_content(); bool is_mam_message = Xep.MessageArchiveManagement.MessageFlag.get_flag(message) != null; // TODO - if (!is_mam_message) outer.invite_received(stream, message.from, new Jid(from_jid), password, reason); + if (!is_mam_message) outer.invite_received(stream, message.from, from_jid, password, reason); return true; } } diff --git a/xmpp-vala/src/module/xep/0048_conference.vala b/xmpp-vala/src/module/xep/0048_conference.vala index d78a2685..fdc26152 100644 --- a/xmpp-vala/src/module/xep/0048_conference.vala +++ b/xmpp-vala/src/module/xep/0048_conference.vala @@ -20,8 +20,14 @@ public class Bookmarks1Conference : Conference { } private Jid jid_; - public override Jid jid { - get { return jid_ ?? (jid_ = Jid.parse(stanza_node.get_attribute(ATTRIBUTE_JID))); } + public override Jid? jid { + get { + try { + return jid_ ?? (jid_ = new Jid(stanza_node.get_attribute(ATTRIBUTE_JID))); + } catch (InvalidJidError e) { + return null; + } + } set { stanza_node.set_attribute(ATTRIBUTE_JID, value.to_string()); } } diff --git a/xmpp-vala/src/module/xep/0065_socks5_bytestreams.vala b/xmpp-vala/src/module/xep/0065_socks5_bytestreams.vala index 1890aac3..a1be00d4 100644 --- a/xmpp-vala/src/module/xep/0065_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0065_socks5_bytestreams.vala @@ -52,7 +52,11 @@ public class Module : XmppStreamModule, Iq.Handler { } string? host = stream_host.get_attribute("host"); string? jid_str = stream_host.get_attribute("jid"); - Jid? jid = jid_str != null ? Jid.parse(jid_str) : null; + Jid? jid = null; + try { + jid = jid_str != null ? new Jid(jid_str) : null; + } catch (InvalidJidError ignored) { + } int port = stream_host.get_attribute_int("port"); if (host == null || jid == null || port <= 0 || port > 65535) { return; diff --git a/xmpp-vala/src/module/xep/0166_jingle.vala b/xmpp-vala/src/module/xep/0166_jingle.vala index 7fc6c929..ca368f00 100644 --- a/xmpp-vala/src/module/xep/0166_jingle.vala +++ b/xmpp-vala/src/module/xep/0166_jingle.vala @@ -572,13 +572,15 @@ public class Session { } void handle_session_accept(XmppStream stream, ContentNode content, StanzaNode jingle, Iq.Stanza iq) throws IqError { string? responder_str = jingle.get_attribute("responder"); - Jid responder; + Jid responder = iq.from; if (responder_str != null) { - responder = Jid.parse(responder_str) ?? iq.from; - } else { - responder = iq.from; // TODO(hrxi): and above, can we assume iq.from != null - // TODO(hrxi): more sanity checking, perhaps replace who we're talking to + try { + responder = new Jid(responder_str); + } catch (InvalidJidError e) { + warning("Received invalid session accept: %s", e.message); + } } + // TODO(hrxi): more sanity checking, perhaps replace who we're talking to if (!responder.is_full()) { throw new IqError.BAD_REQUEST("invalid responder JID"); } diff --git a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala index 991ea141..c6556eb7 100644 --- a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala @@ -116,7 +116,11 @@ public class Candidate : Socks5Bytestreams.Proxy { string? cid = candidate.get_attribute("cid"); string? host = candidate.get_attribute("host"); string? jid_str = candidate.get_attribute("jid"); - Jid? jid = jid_str != null ? Jid.parse(jid_str) : null; + Jid? jid = null; + try { + jid = new Jid(jid_str); + } catch (InvalidJidError ignored) { + } int port = candidate.get_attribute("port") != null ? candidate.get_attribute_int("port") : 1080; int priority = candidate.get_attribute_int("priority"); string? type_str = candidate.get_attribute("type"); diff --git a/xmpp-vala/src/module/xep/0402_bookmarks2.vala b/xmpp-vala/src/module/xep/0402_bookmarks2.vala index 15386398..50c52ac2 100644 --- a/xmpp-vala/src/module/xep/0402_bookmarks2.vala +++ b/xmpp-vala/src/module/xep/0402_bookmarks2.vala @@ -66,19 +66,28 @@ public class Module : BookmarksProvider, XmppStreamModule { } private void on_pupsub_retract(XmppStream stream, Jid jid, string id) { - Jid jid_parsed = Jid.parse(id); - Flag? flag = stream.get_flag(Flag.IDENTITY); - if (flag != null) { - flag.conferences.unset(jid_parsed); + try { + Jid jid_parsed = new Jid(id); + Flag? flag = stream.get_flag(Flag.IDENTITY); + if (flag != null) { + flag.conferences.unset(jid_parsed); + } + conference_removed(stream, jid_parsed); + } catch (InvalidJidError e) { + warning("Ignoring conference bookmark update with invalid Jid: %s", e.message); } - conference_removed(stream, jid_parsed); } private Conference? parse_item_node(StanzaNode conference_node, string id) { Conference conference = new Conference(); - Jid? jid_parsed = Jid.parse(id); - if (jid_parsed == null || jid_parsed.resourcepart != null) return null; - conference.jid = jid_parsed; + try { + Jid jid_parsed = new Jid(id); + if (jid_parsed.resourcepart != null) return null; + conference.jid = jid_parsed; + } catch (InvalidJidError e) { + warning("Ignoring conference bookmark update with invalid Jid: %s", e.message); + return null; + } if (conference_node.name != "conference" || conference_node.ns_uri != NS_URI) return null; diff --git a/xmpp-vala/tests/common.vala b/xmpp-vala/tests/common.vala index b91bbf7c..c616d2e7 100644 --- a/xmpp-vala/tests/common.vala +++ b/xmpp-vala/tests/common.vala @@ -5,6 +5,7 @@ int main(string[] args) { GLib.Test.set_nonfatal_assertions(); TestSuite.get_root().add_suite(new Xmpp.Test.StanzaTest().get_suite()); TestSuite.get_root().add_suite(new Xmpp.Test.UtilTest().get_suite()); + TestSuite.get_root().add_suite(new Xmpp.Test.JidTest().get_suite()); return GLib.Test.run(); } @@ -74,6 +75,13 @@ bool fail_if_not_eq_str(string? left, string? right, string? reason = null) { return fail_if_not(!nullcheck && left == right, @"$(reason + ": " ?? "")$left != $right"); } +bool fail_if_eq_str(string? left, string? right, string? reason = null) { + bool nullcheck = (left == null && right != null) || (left != null && right == null); + if (left == null) left = "(null)"; + if (right == null) right = "(null)"; + return fail_if(!nullcheck && left == right, @"$(reason + ": " ?? "")$left == $right"); +} + bool fail_if_not_eq_uint8_arr(uint8[] left, uint8[] right, string? reason = null) { if (fail_if_not_eq_int(left.length, right.length, @"$(reason + ": " ?? "")array length not equal")) return true; return fail_if_not_eq_str(Base64.encode(left), Base64.encode(right), reason); diff --git a/xmpp-vala/tests/jid.vala b/xmpp-vala/tests/jid.vala new file mode 100644 index 00000000..8928dc97 --- /dev/null +++ b/xmpp-vala/tests/jid.vala @@ -0,0 +1,93 @@ +namespace Xmpp.Test { + +class JidTest : Gee.TestCase { + public JidTest() { + base("Jid"); + + add_test("jid_valid_domain_only", () => { test_jid_valid("example.com"); }); + add_test("jid_valid_bare", () => { test_jid_valid("test@example.com"); }); + add_test("jid_valid_domain_with_resource", () => { test_jid_valid("example.com/test"); }); + add_test("jid_valid_full", () => { test_jid_valid("test@example.com/test"); }); + + // Should those actually be valid? + add_test("jid_valid_emoji_local", () => { test_jid_valid("๐Ÿ˜…@example.com"); }); + add_test("jid_valid_emoji_resource", () => { test_jid_valid("test@example.com/๐Ÿ˜…"); }); + + add_test("jid_invalid_emoji_domain", () => { test_jid_invalid("test@๐Ÿ˜….com"); }); + add_test("jid_invalid_bidi_local", () => { test_jid_invalid("teโ€st@example.com"); }); + add_test("jid_invalid_bidi_resource", () => { test_jid_invalid("test@example.com/teโ€st"); }); + add_test("jid_invalid_bidi_domain", () => { test_jid_invalid("test@exaโ€mple.com"); }); + add_test("jid_invalid_overlong_idn", () => { test_jid_invalid("test@รงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรงรง.com"); }); + + add_test("jid_equal_end_domain", () => { test_jids_equal("test@example.com", "test@example.com."); }); + add_test("jid_equal_case_domain", () => { test_jids_equal("test@example.com", "test@eXample.com"); }); + add_test("jid_equal_norm_domain", () => { test_jids_equal("test@garรงon.com", "test@garcฬงon.com"); }); + add_test("jid_equal_puny_domain", () => { test_jids_equal("test@garรงon.com", "test@xn--garon-0ra.com"); }); + add_test("jid_equal_case_local", () => { test_jids_equal("test@example.com", "tEst@example.com"); }); + add_test("jid_equal_norm_local", () => { test_jids_equal("garรงon@example.com", "garcฬงon@example.com"); }); + add_test("jid_equal_norm_resource", () => { test_jids_equal("test@example.com/garรงon", "test@example.com/garcฬงon"); }); + + add_test("jid_non_equal_case_resource", () => { test_jids_unequal("example.com/test", "example.com/tEst"); }); + + add_test("jid_to_string_end_domain", () => { test_jid_to_string("test@example.com.", "test@example.com"); }); + add_test("jid_to_string_case_domain", () => { test_jid_to_string("test@eXample.com", "test@example.com"); }); + add_test("jid_to_string_norm_domain", () => { test_jid_to_string("test@garcฬงon.com", "test@garรงon.com"); }); + add_test("jid_to_string_puny_domain", () => { test_jid_to_string("test@xn--garon-0ra.com", "test@garรงon.com"); }); + add_test("jid_to_string_case_local", () => { test_jid_to_string("tEst@example.com", "test@example.com"); }); + add_test("jid_to_string_norm_local", () => { test_jid_to_string("garcฬงon@example.com", "garรงon@example.com"); }); + add_test("jid_to_string_case_resource", () => { test_jid_to_string("example.com/tEst", "example.com/tEst"); }); + add_test("jid_to_string_norm_resource", () => { test_jid_to_string("test@example.com/garcฬงon", "test@example.com/garรงon"); }); + } + + private void test_jid_valid(string jid) { + try { + new Jid(jid); + } catch (Error e) { + fail_if_reached(); + } + } + + private void test_jid_invalid(string jid) { + try { + new Jid(jid); + fail_if_reached(); + } catch (Error e) { +// try { +// fail_if_not_eq_str(Jid.parse(jid).to_string(), jid); +// } catch (Error e) { +// fail_if_reached(); +// } + } + } + + private void test_jids_equal(string jid1, string jid2) { + try { + var t1 = new Jid(jid1); + var t2 = new Jid(jid2); + fail_if_not_eq_str(t1.to_string(), t2.to_string()); + } catch (Error e) { + fail_if_reached(); + } + } + + private void test_jid_to_string(string jid1, string jid2) { + try { + var t1 = new Jid(jid1); + fail_if_not_eq_str(t1.to_string(), jid2); + } catch (Error e) { + fail_if_reached(); + } + } + + private void test_jids_unequal(string jid1, string jid2) { + try { + var t1 = new Jid(jid1); + var t2 = new Jid(jid2); + fail_if_eq_str(t1.to_string(), t2.to_string()); + } catch (Error e) { + fail_if_reached(); + } + } +} + +} \ No newline at end of file diff --git a/xmpp-vala/vapi/icu-uc.vapi b/xmpp-vala/vapi/icu-uc.vapi new file mode 100644 index 00000000..14764440 --- /dev/null +++ b/xmpp-vala/vapi/icu-uc.vapi @@ -0,0 +1,56 @@ +namespace ICU { + +[CCode (cname = "UChar")] +[IntegerType (rank = 5, min = 0, max = 65535)] +struct Char {} + +[CCode (cname = "UErrorCode", cprefix = "U_", cheader_filename = "unicode/utypes.h")] +enum ErrorCode { + ZERO_ERROR, + INVALID_CHAR_FOUND, + INDEX_OUTOFBOUNDS_ERROR, + BUFFER_OVERFLOW_ERROR, + UNASSIGNED_CODE_POINT_FOUND, + IDNA_STD3_ASCII_RULES_ERROR + ; + [CCode (cname = "u_errorName")] + public unowned string errorName(); +} + +[CCode (cname = "UErrorCode", cprefix = "U_", cheader_filename = "unicode/parseerr.h")] +struct ParseError {} + +[CCode (cname = "UStringPrepProfile", cprefix = "usprep_", free_function = "usprep_close", cheader_filename = "unicode/usprep.h")] +[Compact] +class PrepProfile { + public static PrepProfile open(string path, string file_name, ref ErrorCode status); + public static PrepProfile openByType(PrepType type, ref ErrorCode status); + public int32 prepare(Char* src, int32 src_length, Char* dest, int32 dest_capacity, PrepOptions options, out ParseError parse_error, ref ErrorCode status); +} +[CCode (cname = "UStringPrepProfileType", cprefix = "USPREP_")] +enum PrepType { + RFC3491_NAMEPREP, + RFC3920_NODEPREP, + RFC3920_RESOURCEPREP +} +[CCode (cname = "int32_t", cprefix = "USPREP_")] +enum PrepOptions { + DEFAULT, + ALLOW_UNASSIGNED +} + +[CCode (cname = "UIDNA", cprefix = "uidna_", free_function = "uidna_close", cheader_filename = "unicode/uidna.h")] +[Compact] +class IDNA { + public static int32 IDNToUnicode(Char* src, int32 src_length, Char* dest, int32 dest_capacity, IDNAOptions options, out ParseError parse_error, ref ErrorCode status); + public static int32 IDNToASCII(Char* src, int32 src_length, Char* dest, int32 dest_capacity, IDNAOptions options, out ParseError parse_error, ref ErrorCode status); +} + +[CCode (cname = "uint32_t", cprefix = "UIDNA_")] +enum IDNAOptions { + DEFAULT, + ALLOW_UNASSIGNED, + USE_STD3_RULES +} + +}