diff --git a/src/helper.js b/src/helper.js index c12fde10..27352b53 100644 --- a/src/helper.js +++ b/src/helper.js @@ -43,6 +43,7 @@ const Helper = { getDefaultNick, parseHostmask, compareHostmask, + compareWithWildcard, password: { hash: passwordHash, @@ -314,8 +315,27 @@ function parseHostmask(hostmask) { function compareHostmask(a, b) { return ( - (a.nick.toLowerCase() === b.nick.toLowerCase() || a.nick === "*") && - (a.ident.toLowerCase() === b.ident.toLowerCase() || a.ident === "*") && - (a.hostname.toLowerCase() === b.hostname.toLowerCase() || a.hostname === "*") + compareWithWildcard(a.nick, b.nick) && + compareWithWildcard(a.ident, b.ident) && + compareWithWildcard(a.hostname, b.hostname) ); } + +function compareWithWildcard(a, b) { + // we allow '*' and '?' wildcards in our comparison. + // this is mostly aligned with https://modern.ircdocs.horse/#wildcard-expressions + // but we do not support the escaping. The ABNF does not seem to be clear as to + // how to escape the escape char '\', which is valid in a nick, + // whereas the wildcards tend not to be (as per RFC1459). + + // The "*" wildcard is ".*" in regex, "?" is "." + // so we tokenize and join with the proper char back together, + // escaping any other regex modifier + const wildmany_split = a.split("*").map((sub) => { + const wildone_split = sub.split("?").map((p) => _.escapeRegExp(p)); + return wildone_split.join("."); + }); + const user_regex = wildmany_split.join(".*"); + const re = new RegExp(`^${user_regex}$`, "i"); // case insensitive + return re.test(b); +} diff --git a/test/tests/hostmask.js b/test/tests/hostmask.js index 406238c2..b1e84c5e 100644 --- a/test/tests/hostmask.js +++ b/test/tests/hostmask.js @@ -48,7 +48,14 @@ describe("Hostmask", function () { it(".compareHostmask (wildcard)", function () { const a = Helper.parseHostmask("nick!user@host"); - const b = Helper.parseHostmask("nick!*@*"); + const b = Helper.parseHostmask("n?ck!*@*"); + expect(Helper.compareHostmask(b, a)).to.be.true; + expect(Helper.compareHostmask(a, b)).to.be.false; + }); + + it(".compareHostmask (wildcard - partial)", function () { + const a = Helper.parseHostmask("nicky!user@host"); + const b = Helper.parseHostmask("nick*!*e?@?os*"); expect(Helper.compareHostmask(b, a)).to.be.true; expect(Helper.compareHostmask(a, b)).to.be.false; }); @@ -60,3 +67,47 @@ describe("Hostmask", function () { expect(Helper.compareHostmask(a, b)).to.be.true; }); }); + +describe("compareWithWildcard", function () { + const goodPairs = [ + ["asdf", "asdf"], + ["AsDf", "asdf"], + ["a?df*", "asdf"], + ["*asdf*", "asdf"], + ["*asdf", "asdf"], + ["asd?", "asdf"], + ["asd?*", "asdf"], + ["a??f", "asdf"], + ["a*", "asdf"], + ["*f", "asdf"], + ["*s*", "asdf"], + ["*", ""], + ["**", ""], + ]; + + for (const t of goodPairs) { + it(`("${t[0]}", "${t[1]}")`, function () { + expect(Helper.compareWithWildcard(t[0], t[1])).to.be.true; + }); + } + + const badPairs = [ + ["asdf", "fdsa"], + ["a?df*", "adfg"], + ["?", ""], + ["?asdf", "asdf"], + ["?*", ""], + ["*?*", ""], + ["*?", ""], + ["asd", "asdf"], + ["sdf", "asdf"], + ["sd", "asdf"], + ["", "asdf"], + ]; + + for (const t of badPairs) { + it(`("${t[0]}", "${t[1]}")`, function () { + expect(Helper.compareWithWildcard(t[0], t[1])).to.be.false; + }); + } +});