diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 01525b2939ff2..4670f6d059135 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -603,7 +603,7 @@ export class WebSocketRouteHandler { } public matches(wsURL: string): boolean { - return urlMatches(this._baseURL, wsURL, this.url); + return urlMatches(this._baseURL, wsURL, this.url, true); } public async handle(webSocketRoute: WebSocketRoute) { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 109990bc88c49..da56445699f0a 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -344,6 +344,14 @@ scheme.LocalUtilsTraceDiscardedParams = tObject({ stacksId: tString, }); scheme.LocalUtilsTraceDiscardedResult = tOptional(tObject({})); +scheme.LocalUtilsGlobToRegexParams = tObject({ + glob: tString, + baseURL: tOptional(tString), + webSocketUrl: tOptional(tBoolean), +}); +scheme.LocalUtilsGlobToRegexResult = tObject({ + regex: tString, +}); scheme.RootInitializer = tOptional(tObject({})); scheme.RootInitializeParams = tObject({ sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']), diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index 1c74b7258ca3f..a681b349d325f 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -24,6 +24,7 @@ import { Progress, ProgressController } from '../progress'; import { SocksInterceptor } from '../socksInterceptor'; import { WebSocketTransport } from '../transport'; import { fetchData } from '../utils/network'; +import { resolveGlobToRegexPattern } from '../../utils/isomorphic/urlMatch'; import type { HarBackend } from '../harBackend'; import type { CallMetadata } from '../instrumentation'; @@ -120,6 +121,11 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. return { pipe, headers: transport.headers }; }, params.timeout || 0); } + + async globToRegex(params: channels.LocalUtilsGlobToRegexParams, metadata?: CallMetadata): Promise { + const regex = resolveGlobToRegexPattern(params.baseURL, params.glob, params.webSocketUrl); + return { regex }; + } } async function urlToWSEndpoint(progress: Progress | undefined, endpointURL: string): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts index f31bf7659d860..b85aae5b3f766 100644 --- a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts @@ -148,7 +148,7 @@ export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, chann function matchesPattern(dispatcher: PageDispatcher | BrowserContextDispatcher, baseURL: string | undefined, url: string) { for (const pattern of dispatcher._webSocketInterceptionPatterns || []) { const urlMatch = pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags) : pattern.glob; - if (urlMatches(baseURL, url, urlMatch)) + if (urlMatches(baseURL, url, urlMatch, true)) return true; } return false; diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index a6afce68dde77..35965191c9ddd 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -19,7 +19,7 @@ import { isString } from './stringUtils'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping const escapedChars = new Set(['$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']']); -export function globToRegex(glob: string): RegExp { +export function globToRegexPattern(glob: string): string { const tokens = ['^']; let inGroup = false; for (let i = 0; i < glob.length; ++i) { @@ -70,7 +70,7 @@ export function globToRegex(glob: string): RegExp { } } tokens.push('$'); - return new RegExp(tokens.join('')); + return tokens.join(''); } function isRegExp(obj: any): obj is RegExp { @@ -85,14 +85,39 @@ export function urlMatchesEqual(match1: URLMatch, match2: URLMatch) { return match1 === match2; } -export function urlMatches(baseURL: string | undefined, urlString: string, match: URLMatch | undefined): boolean { +export function urlMatches(baseURL: string | undefined, urlString: string, match: URLMatch | undefined, webSocketUrl?: boolean): boolean { if (match === undefined || match === '') return true; - if (isString(match) && !match.startsWith('*')) { - // Allow http(s) baseURL to match ws(s) urls. - if (baseURL && /^https?:\/\//.test(baseURL) && /^wss?:\/\//.test(urlString)) - baseURL = baseURL.replace(/^http/, 'ws'); + if (isString(match)) + match = new RegExp(resolveGlobToRegexPattern(baseURL, match, webSocketUrl)); + if (isRegExp(match)) { + const r = match.test(urlString); + return r; + } + const url = parseURL(urlString); + if (!url) + return false; + if (typeof match !== 'function') + throw new Error('url parameter should be string, RegExp or function'); + return match(url); +} + +export function resolveGlobToRegexPattern(baseURL: string | undefined, glob: string, webSocketUrl?: boolean): string { + if (webSocketUrl) + baseURL = toWebSocketBaseUrl(baseURL); + glob = resolveGlobBase(baseURL, glob); + return globToRegexPattern(glob); +} + +function toWebSocketBaseUrl(baseURL: string | undefined) { + // Allow http(s) baseURL to match ws(s) urls. + if (baseURL && /^https?:\/\//.test(baseURL)) + baseURL = baseURL.replace(/^http/, 'ws'); + return baseURL; +} +function resolveGlobBase(baseURL: string | undefined, match: string): string { + if (!match.startsWith('*')) { const tokenMap = new Map(); function mapToken(original: string, replacement: string) { if (original.length === 0) @@ -123,16 +148,7 @@ export function urlMatches(baseURL: string | undefined, urlString: string, match resolved = resolved.replace(token, original); match = resolved; } - if (isString(match)) - match = globToRegex(match); - if (isRegExp(match)) - return match.test(urlString); - const url = parseURL(urlString); - if (!url) - return false; - if (typeof match !== 'function') - throw new Error('url parameter should be string, RegExp or function'); - return match(url); + return match; } function parseURL(url: string): URL | null { diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 582c61e721378..80aca67578622 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -473,6 +473,7 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel { tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise; addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise; traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise; + globToRegex(params: LocalUtilsGlobToRegexParams, metadata?: CallMetadata): Promise; } export type LocalUtilsZipParams = { zipFile: string, @@ -572,6 +573,18 @@ export type LocalUtilsTraceDiscardedOptions = { }; export type LocalUtilsTraceDiscardedResult = void; +export type LocalUtilsGlobToRegexParams = { + glob: string, + baseURL?: string, + webSocketUrl?: boolean, +}; +export type LocalUtilsGlobToRegexOptions = { + baseURL?: string, + webSocketUrl?: boolean, +}; +export type LocalUtilsGlobToRegexResult = { + regex: string, +}; export interface LocalUtilsEvents { } diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index f385b3eb7ad40..3c2366f166558 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -705,6 +705,14 @@ LocalUtils: parameters: stacksId: string + globToRegex: + parameters: + glob: string + baseURL: string? + webSocketUrl: boolean? + returns: + regex: string + Root: type: interface diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index 1c2a8f5f4665a..80c94d0854034 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -16,7 +16,7 @@ */ import { test as it, expect } from './pageTest'; -import { globToRegex, urlMatches } from '../../packages/playwright-core/lib/utils/isomorphic/urlMatch'; +import { globToRegexPattern, urlMatches } from '../../packages/playwright-core/lib/utils/isomorphic/urlMatch'; import vm from 'vm'; it('should work with navigation @smoke', async ({ page, server }) => { @@ -71,6 +71,9 @@ it('should intercept after a service worker', async ({ page, server, browserName }); it('should work with glob', async () => { + function globToRegex(glob: string): RegExp { + return new RegExp(globToRegexPattern(glob)); + } expect(globToRegex('**/*.js').test('https://localhost:8080/foo.js')).toBeTruthy(); expect(globToRegex('**/*.css').test('https://localhost:8080/foo.js')).toBeFalsy(); expect(globToRegex('*.js').test('https://localhost:8080/foo.js')).toBeFalsy();