Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/ProxyEngineChrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ function FindProxyForURL(url, host, noDiagnostics) {
// subscription skip bypass rules/ don't apply proxy
let subMatchedRule = findMatchedUrlInRules(url, host, hostAndPort, compiledRules.SubscriptionRules);
if (subMatchedRule) {
return makeResultForAlwaysEnabledForced(userMatchedRule)
return makeResultForAlwaysEnabledForced(subMatchedRule)
}

// subscription bypass rules/ apply proxy by force
Expand Down
177 changes: 57 additions & 120 deletions src/lib/RuleImporterSwitchy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const strStartsWith = function (str, prefix) {
return str.substr(0, prefix.length) === prefix;
};
const hasProp = {}.hasOwnProperty;
const Utils = require('./Utils').Utils;

const shExpUtils = {
regExpMetaChars: (function () {
Expand Down Expand Up @@ -420,24 +421,53 @@ const Conditions = {
});
},
parseIp: function (ip) {
let addr;
if (ip.charCodeAt(0) === '['.charCodeAt(0)) {
ip = ip.substr(1, ip.length - 2);
}
addr = new IP.v4.Address(ip);
if (!addr.isValid()) {
addr = new IP.v6.Address(ip);
if (!addr.isValid()) {
let address, addressIsBracketed, normalized, prefixMatch, subnetMask;
if (!ip) {
return null;
}
address = ip.trim();
prefixMatch = address.match(/\/(\d+)$/);
if (prefixMatch) {
subnetMask = parseInt(prefixMatch[1], 10);
address = address.substring(0, address.length - prefixMatch[0].length);
}
addressIsBracketed = address.charCodeAt(0) === '['.charCodeAt(0);
if (addressIsBracketed) {
if (!/^\[[^\]]+\]$/.test(address)) {
return null;
}
address = address.substring(1, address.length - 1);
} else if (address.indexOf('[') >= 0 || address.indexOf(']') >= 0) {
return null;
}
Comment on lines +429 to 442

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an imported ip is 'bracketed', like [172.16.0.0/12], the prefixMatch regex //(\d+)$/ will return null because the string ends with a bracket ], not a digit.

As a result, the code skips stripping the subnet suffix here, later strips the brackets, and eventually passes "172.16.0.0/12" with a wrong default subnetMask to Utils.ipCidrNotationToRegExp.

To fix this, the bracket-stripping logic should execute before the prefixMatch slicing.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah thanks I got this PR is kinda messed up, i'll try another one

return addr;
if (/^\d+\.\d+\.\d+\.\d+:\d+$/.test(address)) {
return null;
}
if (subnetMask == null) {
subnetMask = address.indexOf(':') >= 0 ? 128 : 32;
}
if (Utils.ipCidrNotationToRegExp(address, subnetMask.toString()) == null) {
return null;
Comment thread
salarcode marked this conversation as resolved.
}
normalized = Utils.normalizeIpForMatching(address);
if (!normalized) {
return null;
}
return {
address: ip,
addressMinusSuffix: address,
normalized: normalized,
subnet: '/' + subnetMask,
subnetMask: subnetMask,
v4: normalized.indexOf(':') < 0,
isValid: function () {
return true;
}
};
},
normalizeIp: function (addr) {
let ref1;
return ((ref1 = addr.correctForm) != null ? ref1 : addr.canonicalForm).call(addr);
return addr != null ? addr.normalized : void 0;
},
//ipv6Max: new IP.v6.Address('::/0').endAddress().canonicalForm(),
localHosts: ["127.0.0.1", "[::1]", "localhost"],
getWeekdayList: function (condition) {
let i, j, k, results, results1;
Expand Down Expand Up @@ -839,133 +869,40 @@ const Conditions = {
'IpCondition': {
abbrs: ['Ip'],
analyze: function (condition) {
let addr, cache, ip, mask;
let addr, cache, regex;
cache = {
addr: null,
normalized: null
normalized: null,
regex: null
};
ip = condition.ip;
if (ip.charCodeAt(0) === '['.charCodeAt(0)) {
ip = ip.substr(1, ip.length - 2);
}
addr = ip + '/' + condition.prefixLength;
addr = condition.ip + '/' + condition.prefixLength;
cache.addr = this.parseIp(addr);
if (cache.addr == null) {
throw new Error("Invalid IP address " + addr);
}
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());
regex = Utils.ipCidrNotationToRegExp(cache.addr.addressMinusSuffix, cache.addr.subnetMask.toString());
if (regex == null) {
throw new Error("Invalid IP address " + addr);
}
cache.regex = regex;
Comment on lines +884 to +888
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) {
return false;
}
cache = cache.analyzed;
if (addr.v4 !== cache.addr.v4) {
if ((normalizedHost.indexOf(':') < 0) !== 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);
Comment thread
salarcode marked this conversation as resolved.
},
str: function (condition) {
return condition.ip + '/' + condition.prefixLength;
Expand Down
18 changes: 7 additions & 11 deletions src/lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,25 +602,21 @@ export class Utils {
public static normalizeIpForMatching(host: string): string | null {
if (!host) return null;
let h = host.trim();
const bracketedHostMatch = h.match(/^\[([^\]]+)\](?::\d+)?$/);

// remove surrounding brackets if any
h = h.replace(/^\[|\]$/g, '');
if (bracketedHostMatch) {
h = bracketedHostMatch[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+)?$/);
if (ipv4Tail) {
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);
Expand Down
35 changes: 34 additions & 1 deletion src/tests/RuleImporter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { externalAppRuleParser } from '../lib/RuleImporter';
import { CompiledProxyRuleType } from '../core/definitions';
import { Utils } from '../lib/Utils';

describe('externalAppRuleParser.GFWList', () => {
describe('convertLineRegex', () => {
Expand Down Expand Up @@ -311,14 +312,46 @@ http://example.com/*
expect(rule10).toBeDefined();
expect(rule10!.regex).toBeTruthy();
expect(rule10!.importedRuleType).toBe(CompiledProxyRuleType.RegexHost);

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 internal RegexHost rules', () => {
const text = `[SwitchyOmega Conditions]
; Require: https://github.com/salarcode/SmartProxy

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`;

const result = externalAppRuleParser.Switchy.parseAndCompile(text);
const rules = externalAppRuleParser.Switchy.convertToProxyRule(result.compiled);

expect(rules).toBeDefined();
expect(rules.length).toBe(5);
rules.forEach(rule => {
expect(rule.importedRuleType).toBe(CompiledProxyRuleType.RegexHost);
expect(rule.regex).toBeTruthy();
});

const ipv4Rule = rules.find(r => r.name === 'Ip: 10.0.0.0/8');
expect(ipv4Rule).toBeDefined();
const ipv4Regex = new RegExp(ipv4Rule!.regex, 'i');
expect(ipv4Regex.test('10.19.29.150')).toBe(true);
expect(ipv4Regex.test('11.0.0.1')).toBe(false);

const ipv6ExactRule = rules.find(r => r.name === 'Ip: 2001:db8::1/128');
expect(ipv6ExactRule).toBeDefined();
const ipv6Regex = new RegExp(ipv6ExactRule!.regex, 'i');
expect(ipv6Regex.test(Utils.normalizeIpForMatching('2001:db8::1')!)).toBe(true);
expect(ipv6Regex.test(Utils.normalizeIpForMatching('2001:db8::2')!)).toBe(false);
});

it('should handle patterns without prefix as HostWildcard (fromStr fix)', () => {
// Test that patterns without explicit type prefix default to HostWildcardCondition
const text = `[SwitchyOmega Conditions]
Expand Down
8 changes: 8 additions & 0 deletions src/tests/Utils.ipCidr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,13 @@ describe('Utils.ipCidrNotationToRegExp edge cases', () => {
expect(normalized).not.toBeNull();
expect(r.test(normalized)).toBe(true);
});

test('normalizeIpForMatching keeps the last IPv6 group on exact addresses', () => {
const normalized = Utils.normalizeIpForMatching('2001:db8::1');
expect(normalized).toBe('2001:0db8:0000:0000:0000:0000:0000:0001');

const bracketed = Utils.normalizeIpForMatching('[2001:db8::1]:8080');
expect(bracketed).toBe('2001:0db8:0000:0000:0000:0000:0000:0001');
});
});