From a0a956ee0878d24bd06be7f5d75dc4ccd4e7901d Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sun, 22 Dec 2019 04:10:53 +0100 Subject: Properly check Jids everywhere --- 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 | 155 ++++++++++++++++----- xmpp-vala/src/module/roster/item.vala | 11 +- xmpp-vala/src/module/stanza.vala | 15 +- .../xep/0030_service_discovery/items_result.vala | 10 +- xmpp-vala/src/module/xep/0045_muc/module.vala | 146 +++++++++++-------- xmpp-vala/src/module/xep/0048_conference.vala | 10 +- .../src/module/xep/0065_socks5_bytestreams.vala | 6 +- xmpp-vala/src/module/xep/0166_jingle.vala | 12 +- .../module/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 ++++++++ 17 files changed, 464 insertions(+), 124 deletions(-) create mode 100644 xmpp-vala/tests/jid.vala create mode 100644 xmpp-vala/vapi/icu-uc.vapi (limited to 'xmpp-vala') 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.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); + } + + 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)"); } - this.jid = jid; - this.localpart = (owned) localpart; - this.domainpart = (owned) domainpart; - this.resourcepart = (owned) resourcepart; } - 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; + 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)"); + } + } - return new Jid.intern(jid, (owned) localpart, (owned) domainpart, (owned) resourcepart); + 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) { + 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); + try { + Presence.Stanza presence = new Presence.Stanza(); + presence.to = bare_jid.with_resource(nick); - stream.get_flag(Flag.IDENTITY).start_muc_enter(bare_jid, presence.id); + 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); - query_room_info(stream, bare_jid); - stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence); + stream.get_flag(Flag.IDENTITY).start_muc_enter(bare_jid, presence.id); - 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; + 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 +} + +} -- cgit v1.2.3-54-g00ecf