diff --git a/src/core/ProxyRules.ts b/src/core/ProxyRules.ts index e9ee8692..e7c3d55d 100644 --- a/src/core/ProxyRules.ts +++ b/src/core/ProxyRules.ts @@ -20,6 +20,14 @@ import { Utils } from "../lib/Utils"; import { SettingsOperation } from "./SettingsOperation"; import { api } from "../lib/environment"; +function regexHostMatches(rule: CompiledProxyRule, host: string): boolean { + if (rule.regex.test(host)) + return true; + + let normalizedHost = Utils.normalizeIpForMatching(host); + return normalizedHost != null && normalizedHost !== host && rule.regex.test(normalizedHost); +} + export class ProxyRules { public static compileRules(profile: SmartProfileBase, proxyRules: ProxyRule[]): { @@ -370,7 +378,7 @@ export class ProxyRules { } } - if (rule.regex.test(domainHostLowerCase)) + if (regexHostMatches(rule, domainHostLowerCase)) return rule; break; @@ -448,7 +456,7 @@ export class ProxyRules { case CompiledProxyRuleType.RegexHost: - if (rule.regex.test(domainHostLowerCase)) + if (regexHostMatches(rule, domainHostLowerCase)) return rule; break; diff --git a/src/lib/RuleImporterSwitchy.js b/src/lib/RuleImporterSwitchy.js index 73134910..ae4081ca 100644 --- a/src/lib/RuleImporterSwitchy.js +++ b/src/lib/RuleImporterSwitchy.js @@ -6,6 +6,8 @@ * @source https://github.com/FelisCatus/SwitchyOmega * @license GPL3 */ +const { Utils } = require('./Utils'); + const strStartsWith = function (str, prefix) { return str.substr(0, prefix.length) === prefix; }; @@ -839,146 +841,69 @@ const Conditions = { 'IpCondition': { abbrs: ['Ip'], analyze: function (condition) { - let addr, cache, ip, mask; + let cache, ip, regex; cache = { - addr: null, - normalized: null + regex: null }; - ip = condition.ip; - if (ip.charCodeAt(0) === '['.charCodeAt(0)) { - ip = ip.substr(1, ip.length - 2); + ip = (condition.ip || '').trim(); + if (ip.charCodeAt(0) === '['.charCodeAt(0) && ip.charCodeAt(ip.length - 1) === ']'.charCodeAt(0)) { + ip = ip.substring(1, ip.length - 1).trim(); } - addr = ip + '/' + condition.prefixLength; - cache.addr = this.parseIp(addr); - if (cache.addr == null) { - throw new Error("Invalid IP address " + addr); + regex = Utils.ipCidrNotationToRegExp(ip, String(condition.prefixLength)); + if (regex == null) { + throw new Error("Invalid IP address " + ip + '/' + condition.prefixLength); } - cache.normalized = this.normalizeIp(cache.addr); - mask = cache.addr.v4 ? new IP.v4.Address('255.255.255.255/' + cache.addr.subnetMask) : new IP.v6.Address(this.ipv6Max + '/' + cache.addr.subnetMask); - cache.mask = this.normalizeIp(mask.startAddress()); + cache.regex = regex; return cache; }, match: function (condition, request, cache) { - let addr; - addr = this.parseIp(request.host); - if (addr == null) { + let normalizedHost; + normalizedHost = Utils.normalizeIpForMatching(request.host); + if (normalizedHost == null) { return false; } cache = cache.analyzed; - if (addr.v4 !== cache.addr.v4) { - return false; - } - return addr.isInSubnet(cache.addr); + return cache.regex.test(normalizedHost); }, compile: function (condition, cache) { - let hostIsInNet, hostIsInNetEx, hostLooksLikeIp; cache = cache.analyzed; - hostLooksLikeIp = cache.addr.v4 ? new U2.AST_Binary({ - left: new U2.AST_Sub({ - expression: new U2.AST_SymbolRef({ - name: 'host' - }), - property: new U2.AST_Binary({ - left: new U2.AST_Dot({ - expression: new U2.AST_SymbolRef({ - name: 'host' - }), - property: 'length' - }), - operator: '-', - right: new U2.AST_Number({ - value: 1 - }) - }) - }), - operator: '>=', - right: new U2.AST_Number({ - value: 0 - }) - }) : new U2.AST_Binary({ - left: new U2.AST_Call({ - expression: new U2.AST_Dot({ - expression: new U2.AST_SymbolRef({ - name: 'host' - }), - property: 'indexOf' - }), - args: [ - new U2.AST_String({ - value: ':' - }) - ] - }), - operator: '>=', - right: new U2.AST_Number({ - value: 0 - }) - }); - if (cache.addr.subnetMask === 0) { - return hostLooksLikeIp; - } - hostIsInNet = new U2.AST_Call({ - expression: new U2.AST_SymbolRef({ - name: 'isInNet' - }), - args: [ - new U2.AST_SymbolRef({ - name: 'host' - }), new U2.AST_String({ - value: cache.normalized - }), new U2.AST_String({ - value: cache.mask - }) - ] - }); - if (!cache.addr.v4) { - hostIsInNetEx = new U2.AST_Call({ - expression: new U2.AST_SymbolRef({ - name: 'isInNetEx' - }), - args: [ - new U2.AST_SymbolRef({ - name: 'host' - }), new U2.AST_String({ - value: cache.normalized + cache.addr.subnet - }) - ] - }); - hostIsInNet = new U2.AST_Conditional({ - condition: new U2.AST_Binary({ - left: new U2.AST_UnaryPrefix({ - operator: 'typeof', - expression: new U2.AST_SymbolRef({ - name: 'isInNetEx' - }) - }), - operator: '===', - right: new U2.AST_String({ - value: 'function' - }) - }), - consequent: hostIsInNetEx, - alternative: hostIsInNet - }); - } - return new U2.AST_Binary({ - left: hostLooksLikeIp, - operator: '&&', - right: hostIsInNet - }); + return this.regTest('host', cache.regex); }, str: function (condition) { return condition.ip + '/' + condition.prefixLength; }, fromStr: function (str, condition) { - let addr; - addr = this.parseIp(str); - if (addr != null) { - condition.ip = addr.addressMinusSuffix; - condition.prefixLength = addr.subnetMask; + let ip, slashIndex; + str = str.trim(); + if (str.charCodeAt(0) === '['.charCodeAt(0) && str.charCodeAt(str.length - 1) === ']'.charCodeAt(0)) { + str = str.substring(1, str.length - 1).trim(); + } + slashIndex = str.lastIndexOf('/'); + if (slashIndex > 0 && slashIndex < str.length - 1) { + ip = str.substring(0, slashIndex).trim(); + condition.ip = ip; + condition.prefixLength = parseInt(str.substring(slashIndex + 1).trim(), 10); + } else if (slashIndex < 0) { + // Bare IP with no prefix — default to /32 for IPv4, /128 for IPv6. + // This restores the old parseIp behaviour for bare-IP entries and avoids + // silently producing a /0 match-all. + if (str.indexOf(':') >= 0) { + // IPv6 address — analyze() will validate via ipCidrNotationToRegExp. + condition.ip = str; + condition.prefixLength = 128; + } else if (str.indexOf('.') >= 0) { + // Looks like an IPv4 address. + condition.ip = str; + condition.prefixLength = 32; + } else { + return null; + } } else { - condition.ip = '0.0.0.0'; - condition.prefixLength = 0; + // Trailing slash or leading slash — treat as invalid. + return null; + } + if (isNaN(condition.prefixLength)) { + return null; } return condition; } @@ -1876,4 +1801,4 @@ if (typeof (exports) == 'undefined') exports.RuleImporterSwitchy = { switchy: Switchy, compiler: SwitchyCompiler, -} \ No newline at end of file +} diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts index 64507885..65d85f4b 100644 --- a/src/lib/Utils.ts +++ b/src/lib/Utils.ts @@ -602,9 +602,14 @@ export class Utils { public static normalizeIpForMatching(host: string): string | null { if (!host) return null; let h = host.trim(); - - // remove surrounding brackets if any - h = h.replace(/^\[|\]$/g, ''); + const bracketedHost = h.match(/^\[([^\]]+)\](?::\d+)?$/); + if (bracketedHost) { + h = bracketedHost[1]; + } + else { + // remove surrounding brackets if any + h = h.replace(/^\[|\]$/g, ''); + } // If there's an IPv4 tail (possibly with a port), extract it: ::ffff:192.0.2.1 or 192.0.2.1:8080 const ipv4Tail = h.match(/(\d+\.\d+\.\d+\.\d+)(?::\d+)?$/); @@ -612,15 +617,6 @@ export class Utils { return ipv4Tail[1]; } - // Remove trailing :port for IPv6 hosts that lost brackets earlier (e.g. ::1:8080) - if (/:\d+$/.test(h)) { - // Only strip if it looks like a port (all digits) and the rest contains ':' (likely IPv6) - const withoutPort = h.replace(/:\d+$/, ''); - if (withoutPort.indexOf(':') >= 0) { - h = withoutPort; - } - } - // If it looks like IPv6, try to expand if (h.indexOf(':') >= 0) { const groups = Utils.expandIPv6ToGroups(h); @@ -709,4 +705,4 @@ export class Utils { private static readonly IgnoreDomainExtensions: string[] = [ "000.nl", "999.nl", "aa.no", "ab.ca", "ab.se", "abo.pa", "ac.ae", "ac.at", "ac.be", "ac.be", "ac.cn", "ac.com", "ac.cr", "ac.cy", "ac.fj", "ac.fk", "ac.gn", "ac.id", "ac.il", "ac.im.in", "ac.in", "ac.ir", "ac.jp", "ac.mw", "ac.nz", "ac.pa", "ac.ru", "ac.rw", "ac.se", "ac.th", "ac.tj", "ac.tz", "ac.ug", "ac.uk", "ac.vn", "ac.yu", "ac.zm", "ac.zw", "act.au", "ad.jp", "adm.br", "adult.ht", "adv.br", "aero.mv", "agr.br", "ah.cn", "ah.no", "ak.us", "al.us", "alt.za", "am.br", "ar.us", "army.mil", "arq.br", "art.br", "art.do", "art.dz", "art.ht", "art.pl", "asn.au", "asn.au", "asn.lv", "asso.dz", "asso.fr", "asso.ht", "asso.mc", "ato.br", "av.tr", "az.us", "bbs.tr", "bc.ca", "bd.se", "bel.tr", "bio.br", "biz.az", "biz.cy", "biz.et", "biz.fj", "biz.mv", "biz.nr", "biz.om", "biz.pk", "biz.pl", "biz.pr", "biz.tj", "biz.tr", "biz.tt", "biz.vn", "bj.cn", "bl.uk", "bmd.br", "bu.no", "c.se", "ca.us", "cim.br", "city.za", "ck.ua", "club.tw", "cn.ua", "cng.br", "cnt.br", "co.ag", "co.ao", "co.at", "co.bw", "co.cc.cd", "co.ck", "co.cr", "co.fk", "co.gg", "co.hu", "co.id", "co.il", "co.im", "co.in", "co.ir", "co.je", "co.jp", "co.kr", "co.ls", "co.ma", "co.mu", "co.mw", "co.nz", "co.om", "co.rw", "co.th", "co.tj", "co.tt", "co.ug", "co.uk", "co.us", "co.ve", "co.yu", "co.za", "co.zm", "co.zw", "com.ac", "com.af", "com.ag", "com.ai", "com.al", "com.an", "com.ar", "com.au", "com.aw", "com.ax", "com.az", "com.bb", "com.bd", "com.bm", "com.bn", "com.bo", "com.br", "com.bs", "com.bt", "com.cd", "com.ch", "com.cn", "com.co", "com.cu", "com.cy", "com.dm", "com.do", "com.dz", "com.ec", "com.ee", "com.eg", "com.es", "com.et", "com.fj", "com.fr", "com.ge", "com.gh", "com.gi", "com.gn", "com.gp", "com.gr", "com.hk", "com.hn", "com.hr", "com.ht", "com.jm", "com.jo", "com.kh", "com.kw", "com.ky", "com.kz", "com.lb", "com.lc", "com.li", "com.lk", "com.lr", "com.lv", "com.ly", "com.mg", "com.mk", "com.mo", "com.mt", "com.mu", "com.mv", "com.mw", "com.mx", "com.my", "com.ng", "com.ni", "com.np", "com.nr", "com.om", "com.pa", "com.pe", "com.pf", "com.pg", "com.ph", "com.pk", "com.pl", "com.pr", "com.ps", "com.pt", "com.py", "com.ro", "com.ru", "com.rw", "com.sa", "com.sb", "com.sc", "com.sd", "com.sg", "com.sv", "com.sy", "com.tj", "com.tn", "com.tr", "com.tt", "com.tw", "com.ua", "com.uy", "com.ve", "com.vi", "com.vn", "com.ye", "conf.au", "conf.lv", "coop.br", "coop.ht", "coop.mv", "coop.mw", "cpa.pro", "cq.cn", "cri.nz", "csiro.au", "ct.us", "cv.ua", "d.se", "dc.us", "de.us", "dn.ua", "dni.us", "dp.ua", "dpn.br", "dr.tr", "e.se", "ebiz.tw", "ecn.br", "ed.ao", "ed.cr", "ed.jp", "edu.ac", "edu.af", "edu.al", "edu.an", "edu.au", "edu.az", "edu.bb", "edu.bd", "edu.bm", "edu.bn", "edu.bo", "edu.br", "edu.bt", "edu.cn", "edu.co", "edu.cu", "edu.dm", "edu.do", "edu.dz", "edu.ec", "edu.eg", "edu.es", "edu.et", "edu.ge", "edu.gh", "edu.gi", "edu.gp", "edu.gr", "edu.hk", "edu.hn", "edu.ht", "edu.in", "edu.jm", "edu.jo", "edu.kh", "edu.kw", "edu.ky", "edu.kz", "edu.lb", "edu.lc", "edu.lk", "edu.lr", "edu.lv", "edu.ly", "edu.mg", "edu.mo", "edu.mt", "edu.mv", "edu.mw", "edu.mx", "edu.my", "edu.ng", "edu.ni", "edu.np", "edu.nr", "edu.om", "edu.pa", "edu.pe", "edu.pf", "edu.pk", "edu.pl", "edu.pr", "edu.ps", "edu.pt", "edu.py", "edu.rw", "edu.sa", "edu.sb", "edu.sc", "edu.sd", "edu.sg", "edu.sv", "edu.tj", "edu.tr", "edu.tt", "edu.tw", "edu.ua", "edu.vi", "edu.vn", "edu.za", "eng.br", "ens.tn", "esp.br", "etc.br", "eti.br", "eun.eg", "f.se", "fam.pk", "far.br", "fed.us", "fh.se", "fhs.no", "fhsk.se", "fhv.se", "fi.cr", "fie.ee", "fin.ec", "fin.tn", "firm.ht", "firm.in", "fj.cn", "fl.us", "fm.br", "fm.no", "fnd.br", "fot.br", "from.hr", "fst.br", "g.se", "g12.br", "ga.us", "game.tw", "gd.cn", "geek.nz", "gen.in", "gen.nz", "gen.tr", "ggf.br", "go.cr", "go.id", "go.jp", "go.th", "go.tj", "go.tz", "go.ug", "gob.bo", "gob.do", "gob.es", "gob.hn", "gob.mx", "gob.ni", "gob.pa", "gob.pe", "gob.pk", "gob.sv", "gok.pk", "gon.pk", "gop.pk", "gos.pk", "gouv.fr", "gouv.ht", "gouv.rw", "gov.ac", "gov.ae", "gov.af", "gov.al", "gov.ar", "gov.au", "gov.az", "gov.bb", "gov.bd", "gov.bf", "gov.bm", "gov.bo", "gov.br", "gov.bt", "gov.by", "gov.ch", "gov.cn", "gov.co", "gov.cu", "gov.cx", "gov.dm", "gov.do", "gov.dz", "gov.ec", "gov.eg", "gov.et", "gov.fj", "gov.fk", "gov.ge", "gov.gh", "gov.gi", "gov.gn", "gov.gr", "gov.hk", "gov.ie", "gov.il", "gov.im", "gov.in", "gov.ir", "gov.it", "gov.jm", "gov.jo", "gov.kh", "gov.kw", "gov.ky", "gov.kz", "gov.lb", "gov.lc", "gov.li", "gov.lk", "gov.lr", "gov.lt", "gov.lu", "gov.lv", "gov.ly", "gov.ma", "gov.mg", "gov.mo", "gov.mt", "gov.mv", "gov.mw", "gov.my", "gov.ng", "gov.np", "gov.nr", "gov.om", "gov.ph", "gov.pk", "gov.pl", "gov.pr", "gov.ps", "gov.pt", "gov.py", "gov.rw", "gov.sa", "gov.sb", "gov.sc", "gov.sd", "gov.sg", "gov.sy", "gov.tj", "gov.tn", "gov.to", "gov.tp", "gov.tr", "gov.tt.tv", "gov.tv", "gov.tw", "gov.ua", "gov.uk", "gov.vi", "gov.vn", "gov.za", "gov.zm", "gov.zw", "govt.nz", "gr.jp", "gs.cn", "gub.uy", "gv.ao", "gv.at", "gx.cn", "gz.cn", "h.se", "ha.cn", "hb.cn", "he.cn", "hi.cn", "hi.us", "hl.cn", "hl.no", "hm.no", "hn.cn", "i.se", "ia.us", "id.au", "id.lv", "id.ly", "id.us", "idf.il", "idn.sg", "idv.hk", "idv.tw", "if.ua", "il.us", "imb.br", "in.th", "in.us", "ind.br", "ind.in", "ind.tn", "inf.br", "inf.cu", "info.au", "info.az", "info.cy", "info.ec", "info.et", "info.fj", "info.ht", "info.hu", "info.mv", "info.nr", "info.pl", "info.pr", "info.ro", "info.sd", "info.tn", "info.tr", "info.tt", "info.ve", "info.vn", "ing.pa", "inima.al", "int.ar", "int.az", "int.bo", "int.lk", "int.mv", "int.mw", "int.pt", "int.ru", "int.rw", "int.tj", "int.vn", "intl.tn", "ip6.ar", "ip6.pa", "iris.ar", "iris.pa", "isa.us", "isla.pr", "it.ao", "iwi.nz", "iz.hr", "jet.uk", "jl.cn", "jor.br", "js.cn", "jx.cn", "k.se", "k12.il", "k12.tr", "kh.ua", "kiev.ua", "km.ua", "kr.ua", "ks.ua", "ks.us", "kv.ua", "ky.us", "la.us", "law.pro", "law.za", "lel.br", "lg.jp", "lg.ua", "ln.cn", "ltd.co.im", "ltd.cy", "ltd.gi", "ltd.lk", "ltd.uk", "lviv.ua", "m.se", "ma.us", "mat.br", "mb.ca", "md.us", "me.uk", "me.us", "med.br", "med.ec", "med.ht", "med.ly", "med.om", "med.pa", "med.pro", "med.sa", "med.sd", "mi.th", "mi.us", "mil.ac", "mil.ae", "mil.ar", "mil.az", "mil.bd", "mil.bo", "mil.br", "mil.by", "mil.co", "mil.do", "mil.ec", "mil.eg", "mil.fj", "mil.ge", "mil.gh", "mil.hn", "mil.in", "mil.jo", "mil.kh", "mil.kw", "mil.kz", "mil.lt", "mil.lu", "mil.lv", "mil.mg", "mil.mv", "mil.my", "mil.no", "mil.np", "mil.nz", "mil.om", "mil.pe", "mil.pl", "mil.rw", "mil.se", "mil.tj", "mil.tr", "mil.tw", "mil.uk", "mil.uy", "mil.za", "mk.ua", "mn.us", "mo.us", "mod.gi", "mod.uk", "mr.no", "ms.us", "msk.ru", "mt.us", "muni.il", "mus.br", "n.se", "name.ae", "name.af", "name.az", "name.cy", "name.et", "name.fj", "name.hr", "name.mv", "name.my", "name.pr", "name.ro", "name.tj", "name.tr", "name.tt", "name.vn", "nat.tn", "navy.mil", "nb.ca", "nc.us", "nd.us", "ne.jp", "ne.tz", "ne.ug", "ne.us", "nel.uk", "net.ac", "net.ae", "net.af", "net.ag", "net.ai", "net.al", "net.an", "net.ar", "net.au", "net.az", "net.bb", "net.bd", "net.bm", "net.bn", "net.bo", "net.br", "net.bs", "net.bt", "net.cd", "net.ch", "net.cn", "net.co", "net.cu", "net.cy", "net.dm", "net.do", "net.dz", "net.ec", "net.eg", "net.et", "net.fj", "net.fk", "net.ge", "net.gg", "net.gn", "net.gp", "net.gr", "net.hk", "net.hn", "net.ht", "net.il", "net.im", "net.in", "net.ir", "net.je", "net.jm", "net.jo", "net.kh", "net.kw", "net.ky", "net.kz", "net.lb", "net.li", "net.lk", "net.lr", "net.lu", "net.lv", "net.ly", "net.ma", "net.mo", "net.mt", "net.mv", "net.mw", "net.mx", "net.my", "net.ng", "net.ni", "net.np", "net.nr", "net.nz", "net.om", "net.pa", "net.pe", "net.pg", "net.pk", "net.pl", "net.pr", "net.ps", "net.pt", "net.py", "net.ru", "net.rw", "net.sa", "net.sb", "net.sc", "net.sd", "net.sg", "net.sy", "net.th", "net.tj", "net.tn", "net.tr", "net.tt", "net.tw", "net.ua", "net.uk", "net.uy", "net.ve", "net.vn", "net.ye", "net.za", "nf.ca", "ngo.lk", "ngo.pl", "ngo.za", "nh.us", "nhs.uk", "nic.im", "nic.in", "nic.uk", "nj.us", "nl.ca", "nl.no", "nls.uk", "nm.cn", "nm.us", "nom.adae", "nom.ag", "nom.ai", "nom.br", "nom.co", "nom.es", "nom.fk", "nom.fr", "nom.mg", "nom.ni", "nom.pa", "nom.pe", "nom.ro", "nom.za", "nome.pt", "not.br", "ns.ca", "nsn.us", "nsw.au", "nt.au", "nt.ca", "nt.no", "nt.ro", "ntr.br", "nu.ca", "nv.us", "nx.cn", "ny.us", "o.se", "od.ua", "odo.br", "of.no", "off.ai", "og.ao", "oh.us", "ok.us", "ol.no", "on.ca", "or.at", "or.cr", "or.id", "or.jp", "or.kr", "or.th", "or.tz", "or.ug", "or.us", "org.ac", "org.ae", "org.ag", "org.ai", "org.al", "org.an", "org.au", "org.az", "org.bb", "org.bd", "org.bm", "org.bn", "org.bo", "org.br", "org.bs", "org.bt", "org.bw", "org.cd", "org.ch", "org.cn", "org.co", "org.cu", "org.cy", "org.dm", "org.do", "org.dz", "org.ec", "org.ee", "org.eg", "org.es", "org.et", "org.fj", "org.fk", "org.ge", "org.gg", "org.gh", "org.gi", "org.gn", "org.gp", "org.gr", "org.hk", "org.hn", "org.ht", "org.hu", "org.il", "org.im", "org.in", "org.ir", "org.je", "org.jm", "org.jo", "org.kh", "org.kw", "org.ky", "org.kz", "org.lb", "org.lc", "org.li", "org.lk", "org.lr", "org.ls", "org.lu", "org.lv", "org.ly", "org.ma", "org.mg", "org.mk", "org.mo", "org.mt", "org.mv", "org.mw", "org.mx", "org.my", "org.ng", "org.ni", "org.np", "org.nr", "org.nz", "org.om", "org.pa", "org.pe", "org.pf", "org.pk", "org.pl", "org.pr", "org.ps", "org.pt", "org.py", "org.ro", "org.ru", "org.sa", "org.sc", "org.sd", "org.se", "org.sg", "org.sv", "org.tj", "org.tn", "org.tr", "org.tt", "org.tw", "org.ua", "org.uk", "org.uy", "org.ve", "org.vi", "org.vn", "org.yu", "org.za", "org.zm", "org.zw", "oz.au", "pa.us", "pb.ao", "pe.ca", "per.kh", "per.sg", "perso.ht", "pl.ua", "plc.co.im", "plc.ly", "plc.uk", "plo.ps", "pol.dz", "pol.ht", "pol.tr", "pp.az", "pp.ru", "pp.se", "ppg.br", "prd.fr", "prd.mg", "pri.ee", "priv.at", "priv.hu", "pro.ae", "pro.br", "pro.cy", "pro.ec", "pro.fj", "pro.ht", "pro.mv", "pro.om", "pro.pr", "pro.tt", "pro.vn", "psc.br", "psi.br", "pub.sa", "publ.pt", "pvt.ge", "qc.ca", "qh.cn", "qld.au", "qsl.br", "rec.br", "rec.ro", "red.sv", "rel.ht", "res.in", "ri.us", "rl.no", "rv.ua", "s.se", "sa.au", "sa.cr", "sc.cn", "sc.ug", "sc.us", "sch.ae", "sch.ir", "sch.lk", "sch.ly", "sch.om", "sch.sa", "sch.uk", "sch.zm", "sci.eg", "sd.cn", "sd.us", "sec.ps", "sf.no", "sh.cn", "shop.ht", "sk.ca", "sld.do", "sld.pa", "slg.br", "sn.cn", "soc.lk", "soros.al", "sport.hu", "srv.br", "sshn.se", "st.no", "sumy.ua", "sx.cn", "t.se", "tas.au", "te.ua", "tel.tr", "tirana.al", "tj.cn", "tm.cy", "tm.fr", "tm.hu", "tm.mc", "tm.mg", "tm.no", "tm.ro", "tm.se", "tm.za", "tmp.br", "tn.us", "tr.no", "trd.br", "tur.br", "tv.bo", "tv.br", "tv.sd", "tx.us", "u.se", "uniti.al", "upt.al", "uri.ar", "uri.pa", "us.com", "ut.us", "va.no", "va.us", "vet.br", "vf.no", "vgs.no", "vic.au", "vn.ua", "vt.us", "w.se", "wa.au", "wa.us", "waw.pl", "web.do", "web.lk", "web.pk", "web.tj", "web.tr", "web.ve", "wi.us", "wv.us", "www.ro", "x.se", "xj.cn", "xz.cn", "y.se", "yk.ca", "yn.cn", "z.se", "zj.cn", "zlg.br", "zp.ua", "zt.ua"] -} \ No newline at end of file +} diff --git a/src/tests/ProxyRules.test.ts b/src/tests/ProxyRules.test.ts index 9d5e7b02..62c6d583 100644 --- a/src/tests/ProxyRules.test.ts +++ b/src/tests/ProxyRules.test.ts @@ -2,6 +2,7 @@ import { ProxyRules } from '../core/ProxyRules'; import { GeneralOptions, ProxyRule, ProxyRuleType, CompiledProxyRuleType, SmartProfileBase, SettingsConfig, SmartProfile, SmartProfileType, getSmartProfileTypeConfig } from '../core/definitions'; import { ProfileRules } from '../core/ProfileRules'; import { Settings } from '../core/Settings'; +import { externalAppRuleParser } from '../lib/RuleImporter'; describe('ProxyRules.compileRules', () => { // Create minimal mock profile for testing @@ -84,6 +85,20 @@ describe('ProxyRules.compileRules', () => { expect(result.compiledWhiteList).toHaveLength(1); expect(result.compiledWhiteList[0].search).toBe('allowed.com'); }); + + it('should match imported SwitchyOmega IPv6 CIDR rules after host normalization', () => { + const text = `[SwitchyOmega Conditions] +Ip: 2001:db8::1/128`; + + const parsed = externalAppRuleParser.Switchy.parseAndCompile(text); + const importedRules = externalAppRuleParser.Switchy.convertToProxyRule(parsed.compiled); + const proxyRules = importedRules.map(rule => rule.getProxyRule()); + const result = ProxyRules.compileRules(createMockProfile(), proxyRules); + + expect(result.compiledList).toHaveLength(1); + expect(ProxyRules.findMatchedUrlInRules('http://[2001:db8::1]/', result.compiledList)).toBeDefined(); + expect(ProxyRules.findMatchedUrlInRules('http://[2001:db8::2]/', result.compiledList)).toBeNull(); + }); }); describe('ProfileRules.toggleRule', () => { diff --git a/src/tests/RuleImporter.test.ts b/src/tests/RuleImporter.test.ts index 16e6ae07..1d96fbf7 100644 --- a/src/tests/RuleImporter.test.ts +++ b/src/tests/RuleImporter.test.ts @@ -1,5 +1,6 @@ import { externalAppRuleParser } from '../lib/RuleImporter'; import { CompiledProxyRuleType } from '../core/definitions'; +import { Utils } from '../lib/Utils'; describe('externalAppRuleParser.GFWList', () => { describe('convertLineRegex', () => { @@ -309,16 +310,110 @@ http://example.com/* // Test the 10.*.*.* rule with actual IP addresses (with and without port) const rule10 = rules.find(r => r.name && r.name.includes('10.*.*.*')); expect(rule10).toBeDefined(); - expect(rule10!.regex).toBeTruthy(); - expect(rule10!.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); + expect(rule10?.regex).toBeTruthy(); + expect(rule10?.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); + if (!rule10?.regex) { + throw new Error('Expected 10.*.*.* rule regex'); + } - const regex10 = new RegExp(rule10!.regex); + const regex10 = new RegExp(rule10.regex); // HostWildcardCondition tests against host, not full URL expect(regex10.test('10.19.29.157')).toBe(true); expect(regex10.test('10.0.0.1')).toBe(true); expect(regex10.test('11.0.0.1')).toBe(false); }); + it('should convert SwitchyOmega Ip conditions into regex host rules', () => { + const text = `[SwitchyOmega Conditions] +Ip: 10.0.0.0/8 +Ip: [172.16.0.0/12] +Ip: 192.168.0.0/16 +Ip: 2001:0db8:0000:0000:0000:0000:0000:0001/128 +Ip: 2001:db8::1/128 +*.github.com`; + + const result = externalAppRuleParser.Switchy.parseAndCompile(text); + const rules = externalAppRuleParser.Switchy.convertToProxyRule(result.compiled); + + expect(rules).toHaveLength(6); + + const rule10 = rules.find(r => r.name === 'Ip: 10.0.0.0/8'); + expect(rule10?.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); + expect(rule10?.regex).toBeTruthy(); + if (!rule10?.regex) { + throw new Error('Expected IPv4 CIDR rule regex'); + } + const rule10Regex = new RegExp(rule10.regex); + expect(rule10Regex.test('10.42.0.1')).toBe(true); + expect(rule10Regex.test('11.42.0.1')).toBe(false); + + const rule172 = rules.find(r => r.name === 'Ip: [172.16.0.0/12]'); + expect(rule172?.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); + expect(rule172?.regex).toBeTruthy(); + if (!rule172?.regex) { + throw new Error('Expected bracketed IPv4 CIDR rule regex'); + } + const rule172Regex = new RegExp(rule172.regex); + expect(rule172Regex.test('172.31.255.255')).toBe(true); + expect(rule172Regex.test('172.32.0.0')).toBe(false); + + const ipv6Normalized = Utils.normalizeIpForMatching('2001:db8::1'); + expect(ipv6Normalized).toBeTruthy(); + const ipv6Rule = rules.find(r => r.name === 'Ip: 2001:db8::1/128'); + expect(ipv6Rule?.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); + expect(ipv6Rule?.regex).toBeTruthy(); + if (!ipv6Rule?.regex || !ipv6Normalized) { + throw new Error('Expected IPv6 CIDR rule regex'); + } + const ipv6Regex = new RegExp(ipv6Rule.regex, 'i'); + expect(ipv6Regex.test(ipv6Normalized)).toBe(true); + }); + + it('should treat malformed Ip: rules as invalid (no match-all fallback)', () => { + // A bare IP without a prefix length used to fall back to 0.0.0.0/0, producing a + // regex that matched every IPv4 address. The fixed behaviour is: + // • bare IP (no slash) → treated as a /32 (IPv4) or /128 (IPv6) host rule + // • trailing slash → rule is dropped entirely (returns null from fromStr) + // • non-numeric prefix → rule is dropped entirely + + // ── bare IPv4 ────────────────────────────────────────────────────────── + const bareText = `[SwitchyOmega Conditions] +Ip: 192.168.1.1 +*.example.com`; + const bareResult = externalAppRuleParser.Switchy.parseAndCompile(bareText); + const bareRules = externalAppRuleParser.Switchy.convertToProxyRule(bareResult.compiled); + // The bare IP must become a host rule and must NOT match an unrelated address + const bareIpRule = bareRules.find(r => r.name === 'Ip: 192.168.1.1'); + expect(bareIpRule).toBeDefined(); + expect(bareIpRule?.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); + if (!bareIpRule?.regex) throw new Error('Expected bare IP rule regex'); + const bareRegex = new RegExp(bareIpRule.regex); + expect(bareRegex.test('192.168.1.1')).toBe(true); + // Must NOT match every IPv4 address (old /0 behaviour) + expect(bareRegex.test('10.0.0.1')).toBe(false); + expect(bareRegex.test('1.2.3.4')).toBe(false); + + // ── trailing slash ───────────────────────────────────────────────────── + const trailingText = `[SwitchyOmega Conditions] +Ip: 192.168.0.1/ +*.example.com`; + const trailingResult = externalAppRuleParser.Switchy.parseAndCompile(trailingText); + const trailingRules = externalAppRuleParser.Switchy.convertToProxyRule(trailingResult.compiled); + // The invalid Ip: rule must be dropped; only the wildcard rule survives + expect(trailingRules).toHaveLength(1); + expect(trailingRules[0].name).toBe('*.example.com'); + + // ── non-numeric prefix ───────────────────────────────────────────────── + const nanText = `[SwitchyOmega Conditions] +Ip: 192.168.0.0/abc +*.example.com`; + const nanResult = externalAppRuleParser.Switchy.parseAndCompile(nanText); + const nanRules = externalAppRuleParser.Switchy.convertToProxyRule(nanResult.compiled); + // Same expectation: bad rule is dropped, only wildcard survives + expect(nanRules).toHaveLength(1); + expect(nanRules[0].name).toBe('*.example.com'); + }); + it('should handle patterns without prefix as HostWildcard (fromStr fix)', () => { // Test that patterns without explicit type prefix default to HostWildcardCondition const text = `[SwitchyOmega Conditions] @@ -342,14 +437,22 @@ example.com // Test example.com rule - pattern without wildcards matches exact host const exampleRule = rules.find(r => r.name === 'example.com'); expect(exampleRule).toBeDefined(); - const exampleRegex = new RegExp(exampleRule!.regex); + expect(exampleRule?.regex).toBeTruthy(); + if (!exampleRule?.regex) { + throw new Error('Expected example.com regex'); + } + const exampleRegex = new RegExp(exampleRule.regex); expect(exampleRegex.test('example.com')).toBe(true); // Without explicit wildcards, it won't match subdomains // Test *.google.com - should match subdomains but not google.com itself const googleRule = rules.find(r => r.name === '*.google.com'); expect(googleRule).toBeDefined(); - const googleRegex = new RegExp(googleRule!.regex); + expect(googleRule?.regex).toBeTruthy(); + if (!googleRule?.regex) { + throw new Error('Expected *.google.com regex'); + } + const googleRegex = new RegExp(googleRule.regex); expect(googleRegex.test('www.google.com')).toBe(true); expect(googleRegex.test('mail.google.com')).toBe(true); }); @@ -399,16 +502,16 @@ H: google.com`; // Check each rule type const rule1 = rules.find(r => r.name.includes('10.0.0')); - expect(rule1!.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); + expect(rule1?.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); const rule2 = rules.find(r => r.name.includes('192.168')); - expect(rule2!.importedRuleType).toBe(CompiledProxyRuleType.RegexUrl); + expect(rule2?.importedRuleType).toBe(CompiledProxyRuleType.RegexUrl); const rule3 = rules.find(r => r.name === 'example.com'); - expect(rule3!.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); + expect(rule3?.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); const rule4 = rules.find(r => r.name === 'H: google.com'); - expect(rule4!.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); + expect(rule4?.importedRuleType).toBe(CompiledProxyRuleType.RegexHost); }); it('should handle domain patterns with wildcards', () => { @@ -427,7 +530,11 @@ H: google.com`; // Test *.example.com pattern - matches subdomains only (not the domain itself) const wildcardRule = rules.find(r => r.name.includes('*.example')); expect(wildcardRule).toBeDefined(); - const wildcardRegex = new RegExp(wildcardRule!.regex); + expect(wildcardRule?.regex).toBeTruthy(); + if (!wildcardRule?.regex) { + throw new Error('Expected wildcard regex'); + } + const wildcardRegex = new RegExp(wildcardRule.regex); // *.example.com uses (.*\.)? which means "any characters followed by dot" is optional // This actually matches example.com (when optional part is omitted) expect(wildcardRegex.test('www.example.com')).toBe(true); @@ -464,4 +571,4 @@ H: google.com`; expect(rule.name).toBe('*.example.com'); }); }); -}); \ No newline at end of file +}); diff --git a/src/tests/Utils.ipCidr.test.ts b/src/tests/Utils.ipCidr.test.ts index d8d4c9e2..3b2470bd 100644 --- a/src/tests/Utils.ipCidr.test.ts +++ b/src/tests/Utils.ipCidr.test.ts @@ -142,5 +142,9 @@ describe('Utils.ipCidrNotationToRegExp edge cases', () => { expect(normalized).not.toBeNull(); expect(r.test(normalized)).toBe(true); }); -}); + test('normalizeIpForMatching preserves trailing IPv6 hextet and strips bracketed port', () => { + expect(Utils.normalizeIpForMatching('2001:db8::1')).toBe('2001:0db8:0000:0000:0000:0000:0000:0001'); + expect(Utils.normalizeIpForMatching('[2001:db8::1]:443')).toBe('2001:0db8:0000:0000:0000:0000:0000:0001'); + }); +}); diff --git a/src/tests/__mocks__/webdav.ts b/src/tests/__mocks__/webdav.ts new file mode 100644 index 00000000..7beb527c --- /dev/null +++ b/src/tests/__mocks__/webdav.ts @@ -0,0 +1 @@ +export const createClient = jest.fn();