mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
feat(socks): support pattern similar to proxy bypass rules (#19387)
- `<loopback>` for local interfaces; - `123.123.123.123` for IPv4; - `[1:2:3:4:5:6:7:8]` for IPv6; - `*.example.com` and `.example.com` for subdomains; - `example.com` for domains; - `anything:3000` for port matching.
This commit is contained in:
parent
59118b83f9
commit
539893402e
@ -285,6 +285,61 @@ function parseIP(address: string): number[] {
|
||||
return address.split('.', 4).map(t => +t);
|
||||
}
|
||||
|
||||
type PatternMatcher = (host: string, port: number) => boolean;
|
||||
|
||||
function starMatchToRegex(pattern: string) {
|
||||
const source = pattern.split('*').map(s => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}).join('.*');
|
||||
return new RegExp('^' + source + '$');
|
||||
}
|
||||
|
||||
// This follows "Proxy bypass rules" syntax without implicit and negative rules.
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:net/docs/proxy.md;l=331
|
||||
export function parsePattern(pattern: string | undefined): PatternMatcher {
|
||||
if (!pattern)
|
||||
return () => false;
|
||||
|
||||
const matchers: PatternMatcher[] = pattern.split(',').map(token => {
|
||||
const match = token.match(/^(.*?)(?::(\d+))?$/);
|
||||
if (!match)
|
||||
throw new Error(`Unsupported token "${token}" in pattern "${pattern}"`);
|
||||
const tokenPort = match[2] ? +match[2] : undefined;
|
||||
const portMatches = (port: number) => tokenPort === undefined || tokenPort === port;
|
||||
let tokenHost = match[1];
|
||||
|
||||
if (tokenHost === '<loopback>') {
|
||||
return (host, port) => {
|
||||
if (!portMatches(port))
|
||||
return false;
|
||||
return host === 'localhost'
|
||||
|| host.endsWith('.localhost')
|
||||
|| host === '127.0.0.1'
|
||||
|| host === '[::1]';
|
||||
};
|
||||
}
|
||||
|
||||
if (tokenHost === '*')
|
||||
return (host, port) => portMatches(port);
|
||||
|
||||
if (net.isIPv4(tokenHost) || net.isIPv6(tokenHost))
|
||||
return (host, port) => host === tokenHost && portMatches(port);
|
||||
|
||||
if (tokenHost[0] === '.')
|
||||
tokenHost = '*' + tokenHost;
|
||||
const tokenRegex = starMatchToRegex(tokenHost);
|
||||
return (host, port) => {
|
||||
if (!portMatches(port))
|
||||
return false;
|
||||
if (net.isIPv4(host) || net.isIPv6(host))
|
||||
return false;
|
||||
return !!host.match(tokenRegex);
|
||||
};
|
||||
});
|
||||
return (host, port) => matchers.some(matcher => matcher(host, port));
|
||||
}
|
||||
|
||||
export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
||||
static Events = {
|
||||
SocksRequested: 'socksRequested',
|
||||
@ -297,7 +352,7 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
||||
private _sockets = new Set<net.Socket>();
|
||||
private _closed = false;
|
||||
private _port: number | undefined;
|
||||
private _pattern: string | undefined;
|
||||
private _patternMatcher: PatternMatcher = () => false;
|
||||
private _directSockets = new Map<string, net.Socket>();
|
||||
|
||||
constructor() {
|
||||
@ -318,11 +373,11 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
||||
}
|
||||
|
||||
setPattern(pattern: string | undefined) {
|
||||
this._pattern = pattern;
|
||||
}
|
||||
|
||||
private _matchesPattern(request: SocksSocketRequestedPayload) {
|
||||
return this._pattern === '*' || (this._pattern === 'localhost' && request.host === 'localhost');
|
||||
try {
|
||||
this._patternMatcher = parsePattern(pattern);
|
||||
} catch (e) {
|
||||
this._patternMatcher = () => false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleDirect(request: SocksSocketRequestedPayload) {
|
||||
@ -374,7 +429,7 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
||||
}
|
||||
|
||||
onSocketRequested(payload: SocksSocketRequestedPayload) {
|
||||
if (!this._matchesPattern(payload)) {
|
||||
if (!this._patternMatcher(payload.host, payload.port)) {
|
||||
this._handleDirect(payload);
|
||||
return;
|
||||
}
|
||||
@ -431,26 +486,22 @@ export class SocksProxyHandler extends EventEmitter {
|
||||
};
|
||||
|
||||
private _sockets = new Map<string, net.Socket>();
|
||||
private _pattern: string | undefined;
|
||||
private _patternMatcher: PatternMatcher = () => false;
|
||||
private _redirectPortForTest: number | undefined;
|
||||
|
||||
constructor(pattern: string | undefined, redirectPortForTest?: number) {
|
||||
super();
|
||||
this._pattern = pattern;
|
||||
this._patternMatcher = parsePattern(pattern);
|
||||
this._redirectPortForTest = redirectPortForTest;
|
||||
}
|
||||
|
||||
private _matchesPattern(host: string, port: number) {
|
||||
return this._pattern === '*' || (this._pattern === 'localhost' && host === 'localhost');
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
for (const uid of this._sockets.keys())
|
||||
this.socketClosed({ uid });
|
||||
}
|
||||
|
||||
async socketRequested({ uid, host, port }: SocksSocketRequestedPayload): Promise<void> {
|
||||
if (!this._matchesPattern(host, port)) {
|
||||
if (!this._patternMatcher(host, port)) {
|
||||
const payload: SocksSocketFailedPayload = { uid, errorCode: 'ECONNREFUSED' };
|
||||
this.emit(SocksProxyHandler.Events.SocksFailed, payload);
|
||||
return;
|
||||
|
117
tests/library/proxy-pattern.spec.ts
Normal file
117
tests/library/proxy-pattern.spec.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { parsePattern } from '../../packages/playwright-core/lib/common/socksProxy';
|
||||
import { playwrightTest as test, expect } from '../config/browserTest';
|
||||
|
||||
test('socks proxy patter matcher', async ({}) => {
|
||||
const m1 = parsePattern('*');
|
||||
expect.soft(m1('example.com', 80)).toBe(true);
|
||||
expect.soft(m1('some.long.example.com', 80)).toBe(true);
|
||||
expect.soft(m1('localhost', 3000)).toBe(true);
|
||||
expect.soft(m1('foo.localhost', 3000)).toBe(true);
|
||||
expect.soft(m1('127.0.0.1', 9222)).toBe(true);
|
||||
expect.soft(m1('123.123.123.123', 9222)).toBe(true);
|
||||
expect.soft(m1('[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]', 8080)).toBe(true);
|
||||
expect.soft(m1('[::1]', 5000)).toBe(true);
|
||||
|
||||
const m2 = parsePattern('<loopback>');
|
||||
expect.soft(m2('example.com', 80)).toBe(false);
|
||||
expect.soft(m2('some.long.example.com', 80)).toBe(false);
|
||||
expect.soft(m2('localhost', 3000)).toBe(true);
|
||||
expect.soft(m2('foo.localhost', 3000)).toBe(true);
|
||||
expect.soft(m2('127.0.0.1', 9222)).toBe(true);
|
||||
expect.soft(m2('123.123.123.123', 9222)).toBe(false);
|
||||
expect.soft(m2('[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]', 8080)).toBe(false);
|
||||
expect.soft(m2('[::1]', 5000)).toBe(true);
|
||||
|
||||
const m3 = parsePattern('<loopback>:3000');
|
||||
expect.soft(m3('example.com', 80)).toBe(false);
|
||||
expect.soft(m3('some.long.example.com', 80)).toBe(false);
|
||||
expect.soft(m3('localhost', 3000)).toBe(true);
|
||||
expect.soft(m3('foo.localhost', 3000)).toBe(true);
|
||||
expect.soft(m3('127.0.0.1', 9222)).toBe(false);
|
||||
expect.soft(m3('123.123.123.123', 9222)).toBe(false);
|
||||
expect.soft(m3('[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]', 8080)).toBe(false);
|
||||
expect.soft(m3('[::1]', 5000)).toBe(false);
|
||||
|
||||
const m4 = parsePattern('.com:80');
|
||||
expect.soft(m4('example.com', 80)).toBe(true);
|
||||
expect.soft(m4('some.long.example.com', 80)).toBe(true);
|
||||
expect.soft(m4('localhost', 3000)).toBe(false);
|
||||
expect.soft(m4('foo.localhost', 3000)).toBe(false);
|
||||
expect.soft(m4('127.0.0.1', 9222)).toBe(false);
|
||||
expect.soft(m4('123.123.123.123', 9222)).toBe(false);
|
||||
expect.soft(m4('[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]', 8080)).toBe(false);
|
||||
expect.soft(m4('[::1]', 5000)).toBe(false);
|
||||
|
||||
const m5 = parsePattern('example.com');
|
||||
expect.soft(m5('example.com', 80)).toBe(true);
|
||||
expect.soft(m5('some.long.example.com', 80)).toBe(false);
|
||||
expect.soft(m5('localhost', 3000)).toBe(false);
|
||||
expect.soft(m5('foo.localhost', 3000)).toBe(false);
|
||||
expect.soft(m5('127.0.0.1', 9222)).toBe(false);
|
||||
expect.soft(m5('123.123.123.123', 9222)).toBe(false);
|
||||
expect.soft(m5('[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]', 8080)).toBe(false);
|
||||
expect.soft(m5('[::1]', 5000)).toBe(false);
|
||||
|
||||
const m6 = parsePattern('*.com');
|
||||
expect.soft(m6('example.com', 80)).toBe(true);
|
||||
expect.soft(m6('some.long.example.com', 80)).toBe(true);
|
||||
expect.soft(m6('localhost', 3000)).toBe(false);
|
||||
expect.soft(m6('foo.localhost', 3000)).toBe(false);
|
||||
expect.soft(m6('127.0.0.1', 9222)).toBe(false);
|
||||
expect.soft(m6('123.123.123.123', 9222)).toBe(false);
|
||||
expect.soft(m6('[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]', 8080)).toBe(false);
|
||||
expect.soft(m6('[::1]', 5000)).toBe(false);
|
||||
|
||||
const m7 = parsePattern('123.123.123.123:9222');
|
||||
expect.soft(m7('example.com', 80)).toBe(false);
|
||||
expect.soft(m7('some.long.example.com', 80)).toBe(false);
|
||||
expect.soft(m7('localhost', 3000)).toBe(false);
|
||||
expect.soft(m7('foo.localhost', 3000)).toBe(false);
|
||||
expect.soft(m7('127.0.0.1', 9222)).toBe(false);
|
||||
expect.soft(m7('123.123.123.123', 9222)).toBe(true);
|
||||
expect.soft(m7('[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]', 8080)).toBe(false);
|
||||
expect.soft(m7('[::1]', 5000)).toBe(false);
|
||||
|
||||
const m8 = parsePattern('example.com:80,localhost,*:9222');
|
||||
expect.soft(m8('example.com', 80)).toBe(true);
|
||||
expect.soft(m8('some.long.example.com', 80)).toBe(false);
|
||||
expect.soft(m8('localhost', 3000)).toBe(true);
|
||||
expect.soft(m8('foo.localhost', 3000)).toBe(false);
|
||||
expect.soft(m8('127.0.0.1', 9222)).toBe(true);
|
||||
expect.soft(m8('123.123.123.123', 9222)).toBe(true);
|
||||
expect.soft(m8('[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]', 8080)).toBe(false);
|
||||
expect.soft(m8('[::1]', 5000)).toBe(false);
|
||||
|
||||
const m9 = parsePattern('127.*.*.1');
|
||||
expect.soft(m9('example.com', 80)).toBe(false);
|
||||
expect.soft(m9('some.long.example.com', 80)).toBe(false);
|
||||
expect.soft(m9('localhost', 3000)).toBe(false);
|
||||
expect.soft(m9('foo.localhost', 3000)).toBe(false);
|
||||
expect.soft(m9('127.0.0.1', 9222)).toBe(false);
|
||||
expect.soft(m9('123.123.123.123', 9222)).toBe(false);
|
||||
expect.soft(m9('[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]', 8080)).toBe(false);
|
||||
expect.soft(m9('[::1]', 5000)).toBe(false);
|
||||
|
||||
const m10 = parsePattern('foo?/bar.*.com');
|
||||
expect.soft(m10('foo?/bar.X.com', 80)).toBe(true);
|
||||
expect.soft(m10('foo?/bar.Y.com', 80)).toBe(true);
|
||||
expect.soft(m10('foo?/bar.com', 80)).toBe(false);
|
||||
expect.soft(m10('fo/bar.X.com', 80)).toBe(false);
|
||||
expect.soft(m10('fo?/bar.X.com', 80)).toBe(false);
|
||||
});
|
Loading…
Reference in New Issue
Block a user