| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type * as Platform from '../../core/platform/platform.js'; |
| |
| function parseScheme(pattern: string): {hostPattern: string, scheme: string}|undefined { |
| const SCHEME_SEPARATOR = '://'; |
| const schemeEnd = pattern.indexOf(SCHEME_SEPARATOR); |
| if (schemeEnd < 0) { |
| return undefined; |
| } |
| const scheme = pattern.substr(0, schemeEnd).toLowerCase(); |
| |
| // Keep in sync with //extensions/common/url_pattern.cc in chromium |
| const validSchemes = [ |
| '*', 'http', 'https', 'ftp', 'chrome', 'chrome-extension', |
| // Chromium additionally defines the following schemes, but these aren't relevant for host url patterns: |
| /* 'file', 'filesystem', 'ws', 'wss', 'data', 'uuid-in-package'*/ |
| ]; |
| |
| if (!validSchemes.includes(scheme)) { |
| return undefined; |
| } |
| |
| return {scheme, hostPattern: pattern.substr(schemeEnd + SCHEME_SEPARATOR.length)}; |
| } |
| |
| function defaultPort(scheme: string): string|undefined { |
| switch (scheme) { |
| case 'http': |
| return '80'; |
| case 'https': |
| return '443'; |
| case 'ftp': |
| return '25'; |
| } |
| return undefined; |
| } |
| |
| function parseHostAndPort(pattern: string, scheme: string): {host: string, port: string}|undefined { |
| const pathnameStart = pattern.indexOf('/'); |
| if (pathnameStart >= 0) { |
| const path = pattern.substr(pathnameStart); |
| if (path !== '/*' && path !== '/') { |
| // Host patterns don't allow for paths to be specified |
| return undefined; |
| } |
| // Strip off path part |
| pattern = pattern.substr(0, pathnameStart); |
| } |
| |
| const PORT_WILDCARD = ':*'; |
| if (pattern.endsWith(PORT_WILDCARD)) { |
| // Strip off wildcard port to not upset url parsing |
| pattern = pattern.substr(0, pattern.length - PORT_WILDCARD.length); |
| } |
| |
| if (pattern.endsWith(':')) { |
| return undefined; |
| } |
| |
| const SUBDOMAIN_WILDCARD = '*.'; |
| let asUrl: URL; |
| try { |
| asUrl = new URL( |
| pattern.startsWith(SUBDOMAIN_WILDCARD) ? `http://${pattern.substr(SUBDOMAIN_WILDCARD.length)}` : |
| `http://${pattern}`); |
| } catch { |
| return undefined; |
| } |
| if (asUrl.pathname !== '/') { |
| return undefined; |
| } |
| |
| if (asUrl.hostname.endsWith('.')) { |
| asUrl.hostname = asUrl.hostname.substr(0, asUrl.hostname.length - 1); |
| } |
| |
| // The URL constructor is happy to accept '*', but it gets replaced with %2A |
| if (asUrl.hostname !== '%2A' && asUrl.hostname.includes('%2A')) { |
| return undefined; |
| } |
| |
| // The URL constructor strips off the default port for the scheme, even if it was given explicitly |
| const httpPort = defaultPort('http'); |
| if (!httpPort) { |
| return undefined; |
| } |
| const port = pattern.endsWith(`:${httpPort}`) ? httpPort : (asUrl.port === '' ? '*' : asUrl.port); |
| const schemesWithPort = ['http', 'https', 'ftp']; |
| if (port !== '*' && !schemesWithPort.includes(scheme)) { |
| return undefined; |
| } |
| |
| const host = asUrl.hostname !== '%2A' ? (pattern.startsWith('*.') ? `*.${asUrl.hostname}` : asUrl.hostname) : '*'; |
| return { |
| host, |
| port, |
| }; |
| } |
| |
| /** |
| * HostUrlPatterns define permissions in for extensions in the form of "<protocol>://<sub-domain>.example.com:<port>/". |
| * Where the respected parts can be patters like "*". |
| * Since these aren't valid {@link Common.ParsedURL.ParsedURL} |
| * can't handle them and we need a separate implementation. |
| * |
| * More information in the Chromium code base - |
| * {@link https://crsrc.org/c/chrome/browser/extensions/extension_management_internal.h;l=137 | here}. |
| */ |
| export class HostUrlPattern { |
| static parse(pattern: string): HostUrlPattern|undefined { |
| if (pattern === '<all_urls>') { |
| return new HostUrlPattern({matchesAll: true}); |
| } |
| const parsedScheme = parseScheme(pattern); |
| if (!parsedScheme) { |
| return undefined; |
| } |
| const {scheme, hostPattern} = parsedScheme; |
| |
| const parsedHost = parseHostAndPort(hostPattern, scheme); |
| if (!parsedHost) { |
| return undefined; |
| } |
| const {host, port} = parsedHost; |
| |
| return new HostUrlPattern({scheme, host, port, matchesAll: false}); |
| } |
| |
| private constructor(readonly pattern: { |
| matchesAll: true, |
| }|{readonly scheme: string, readonly host: string, readonly port: string, matchesAll: false}) { |
| } |
| |
| get scheme(): string { |
| return this.pattern.matchesAll ? '*' : this.pattern.scheme; |
| } |
| get host(): string { |
| return this.pattern.matchesAll ? '*' : this.pattern.host; |
| } |
| get port(): string { |
| return this.pattern.matchesAll ? '*' : this.pattern.port; |
| } |
| |
| matchesAllUrls(): boolean { |
| return this.pattern.matchesAll; |
| } |
| |
| matchesUrl(url: Platform.DevToolsPath.UrlString): boolean { |
| let parsedUrl; |
| try { |
| parsedUrl = new URL(url); |
| } catch { |
| return false; |
| } |
| // Try to parse the input url before checking for <all_urls> because <all_urls> doesn't match invalid urls |
| if (this.matchesAllUrls()) { |
| return true; |
| } |
| const scheme = parsedUrl.protocol.substr(0, parsedUrl.protocol.length - 1); |
| const port = parsedUrl.port || defaultPort(scheme); |
| return this.matchesScheme(scheme) && this.matchesHost(parsedUrl.hostname) && (!port || this.matchesPort(port)); |
| } |
| |
| matchesScheme(scheme: string): boolean { |
| if (this.pattern.matchesAll) { |
| return true; |
| } |
| if (this.pattern.scheme === '*') { |
| return scheme === 'http' || scheme === 'https'; |
| } |
| return this.pattern.scheme === scheme; |
| } |
| |
| matchesHost(host: string): boolean { |
| if (this.pattern.matchesAll) { |
| return true; |
| } |
| if (this.pattern.host === '*') { |
| return true; |
| } |
| let normalizedHost = new URL(`http://${host}`).hostname; |
| if (normalizedHost.endsWith('.')) { |
| normalizedHost = normalizedHost.substr(0, normalizedHost.length - 1); |
| } |
| if (this.pattern.host.startsWith('*.')) { |
| return normalizedHost === this.pattern.host.substr(2) || normalizedHost.endsWith(this.pattern.host.substr(1)); |
| } |
| return this.pattern.host === normalizedHost; |
| } |
| |
| matchesPort(port: string): boolean { |
| if (this.pattern.matchesAll) { |
| return true; |
| } |
| return this.pattern.port === '*' || this.pattern.port === port; |
| } |
| } |