522 lines
13 KiB
JavaScript
522 lines
13 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
|
|
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
|
pprint: "chrome://remote/content/shared/Format.sys.mjs",
|
|
});
|
|
|
|
/**
|
|
* Parsed pattern to use for URL matching.
|
|
*
|
|
* @typedef {object} ParsedURLPattern
|
|
* @property {string|null} protocol
|
|
* The protocol, for instance "https".
|
|
* @property {string|null} hostname
|
|
* The hostname, for instance "example.com".
|
|
* @property {string|null} port
|
|
* The serialized port. Empty string for default ports of special schemes.
|
|
* @property {string|null} path
|
|
* The path, starting with "/".
|
|
* @property {string|null} search
|
|
* The search query string, without the leading "?"
|
|
*/
|
|
|
|
/**
|
|
* Subset of properties extracted from a parsed URL.
|
|
*
|
|
* @typedef {object} ParsedURL
|
|
* @property {string=} host
|
|
* @property {string|Array<string>} path
|
|
* Either a string if the path is an opaque path, or an array of strings
|
|
* (path segments).
|
|
* @property {number=} port
|
|
* @property {string=} query
|
|
* @property {string=} scheme
|
|
*/
|
|
|
|
/**
|
|
* Enum of URLPattern types.
|
|
*
|
|
* @readonly
|
|
* @enum {URLPatternType}
|
|
*/
|
|
const URLPatternType = {
|
|
Pattern: "pattern",
|
|
String: "string",
|
|
};
|
|
|
|
const supportedURLPatternTypes = Object.values(URLPatternType);
|
|
|
|
const SPECIAL_SCHEMES = ["file", "http", "https", "ws", "wss"];
|
|
const DEFAULT_PORTS = {
|
|
file: null,
|
|
http: 80,
|
|
https: 443,
|
|
ws: 80,
|
|
wss: 443,
|
|
};
|
|
|
|
/**
|
|
* Check if a given URL pattern is compatible with the provided URL.
|
|
*
|
|
* Implements https://w3c.github.io/webdriver-bidi/#match-url-pattern
|
|
*
|
|
* @param {ParsedURLPattern} urlPattern
|
|
* The URL pattern to match.
|
|
* @param {string} url
|
|
* The string representation of a URL to test against the pattern.
|
|
*
|
|
* @returns {boolean}
|
|
* True if the pattern is compatible with the provided URL, false otherwise.
|
|
*/
|
|
export function matchURLPattern(urlPattern, url) {
|
|
const parsedURL = parseURL(url);
|
|
|
|
if (urlPattern.protocol !== null && urlPattern.protocol != parsedURL.scheme) {
|
|
return false;
|
|
}
|
|
|
|
if (urlPattern.hostname !== null && urlPattern.hostname != parsedURL.host) {
|
|
return false;
|
|
}
|
|
|
|
if (urlPattern.port !== null && urlPattern.port != serializePort(parsedURL)) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
urlPattern.pathname !== null &&
|
|
urlPattern.pathname != serializePath(parsedURL)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (urlPattern.search !== null) {
|
|
const urlQuery = parsedURL.query === null ? "" : parsedURL.query;
|
|
if (urlPattern.search != urlQuery) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse a URLPattern into a parsed pattern object which can be used to match
|
|
* URLs using `matchURLPattern`.
|
|
*
|
|
* Implements https://w3c.github.io/webdriver-bidi/#parse-url-pattern
|
|
*
|
|
* @param {URLPattern} pattern
|
|
* The pattern to parse.
|
|
*
|
|
* @returns {ParsedURLPattern}
|
|
* The parsed URL pattern.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
* @throws {UnsupportedOperationError}
|
|
* Raised if the pattern uses a protocol not supported by Firefox.
|
|
*/
|
|
export function parseURLPattern(pattern) {
|
|
lazy.assert.object(
|
|
pattern,
|
|
lazy.pprint`Expected URL pattern to be an object, got ${pattern}`
|
|
);
|
|
|
|
let hasProtocol = true;
|
|
let hasHostname = true;
|
|
let hasPort = true;
|
|
let hasPathname = true;
|
|
let hasSearch = true;
|
|
|
|
let patternUrl;
|
|
switch (pattern.type) {
|
|
case URLPatternType.Pattern:
|
|
patternUrl = "";
|
|
if ("protocol" in pattern) {
|
|
patternUrl += parseProtocol(pattern.protocol);
|
|
} else {
|
|
hasProtocol = false;
|
|
patternUrl += "http";
|
|
}
|
|
|
|
const scheme = patternUrl.toLowerCase();
|
|
patternUrl += ":";
|
|
if (SPECIAL_SCHEMES.includes(scheme)) {
|
|
patternUrl += "//";
|
|
}
|
|
|
|
if ("hostname" in pattern) {
|
|
patternUrl += parseHostname(pattern.hostname, scheme);
|
|
} else {
|
|
if (scheme != "file") {
|
|
patternUrl += "placeholder";
|
|
}
|
|
hasHostname = false;
|
|
}
|
|
|
|
if ("port" in pattern) {
|
|
patternUrl += parsePort(pattern.port);
|
|
} else {
|
|
hasPort = false;
|
|
}
|
|
|
|
if ("pathname" in pattern) {
|
|
patternUrl += parsePathname(pattern.pathname);
|
|
} else {
|
|
hasPathname = false;
|
|
}
|
|
|
|
if ("search" in pattern) {
|
|
patternUrl += parseSearch(pattern.search);
|
|
} else {
|
|
hasSearch = false;
|
|
}
|
|
break;
|
|
case URLPatternType.String:
|
|
lazy.assert.string(
|
|
pattern.pattern,
|
|
lazy.pprint`Expected URL pattern "pattern" to be a string, got ${pattern.pattern}`
|
|
);
|
|
patternUrl = unescapeUrlPattern(pattern.pattern);
|
|
break;
|
|
default:
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected "urlPattern" type to be one of ${supportedURLPatternTypes}, got ${pattern.type}`
|
|
);
|
|
}
|
|
|
|
if (!URL.canParse(patternUrl)) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Unable to parse URL "${patternUrl}"`
|
|
);
|
|
}
|
|
|
|
let parsedURL;
|
|
try {
|
|
parsedURL = parseURL(patternUrl);
|
|
} catch (e) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Failed to parse URL "${patternUrl}"`
|
|
);
|
|
}
|
|
|
|
if (hasProtocol && !SPECIAL_SCHEMES.includes(parsedURL.scheme)) {
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`URL pattern did not specify a supported protocol (one of ${SPECIAL_SCHEMES}), got ${parsedURL.scheme}`
|
|
);
|
|
}
|
|
|
|
return {
|
|
protocol: hasProtocol ? parsedURL.scheme : null,
|
|
hostname: hasHostname ? parsedURL.host : null,
|
|
port: hasPort ? serializePort(parsedURL) : null,
|
|
pathname:
|
|
hasPathname && parsedURL.path.length ? serializePath(parsedURL) : null,
|
|
search: hasSearch ? parsedURL.query || "" : null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parse the hostname property of a URLPatternPattern.
|
|
*
|
|
* @param {string} hostname
|
|
* A hostname property.
|
|
* @param {string} scheme
|
|
* The scheme for the URLPatternPattern.
|
|
*
|
|
* @returns {string}
|
|
* The parsed property.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
*/
|
|
function parseHostname(hostname, scheme) {
|
|
if (typeof hostname != "string" || hostname == "") {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected URLPattern "hostname" to be a non-empty string, got ${hostname}`
|
|
);
|
|
}
|
|
|
|
if (scheme == "file") {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`URLPattern with "file" scheme cannot specify a hostname, got ${hostname}`
|
|
);
|
|
}
|
|
|
|
hostname = unescapeUrlPattern(hostname);
|
|
|
|
const forbiddenHostnameCharacters = ["/", "?", "#"];
|
|
let insideBrackets = false;
|
|
for (const codepoint of hostname) {
|
|
if (
|
|
forbiddenHostnameCharacters.includes(codepoint) ||
|
|
(!insideBrackets && codepoint == ":")
|
|
) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`URL pattern "hostname" contained a forbidden character, got "${hostname}"`
|
|
);
|
|
}
|
|
|
|
if (codepoint == "[") {
|
|
insideBrackets = true;
|
|
} else if (codepoint == "]") {
|
|
insideBrackets = false;
|
|
}
|
|
}
|
|
|
|
return hostname;
|
|
}
|
|
|
|
/**
|
|
* Parse the pathname property of a URLPatternPattern.
|
|
*
|
|
* @param {string} pathname
|
|
* A pathname property.
|
|
*
|
|
* @returns {string}
|
|
* The parsed property.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
*/
|
|
function parsePathname(pathname) {
|
|
lazy.assert.string(
|
|
pathname,
|
|
lazy.pprint`Expected URL pattern "pathname" to be a string, got ${pathname}`
|
|
);
|
|
|
|
pathname = unescapeUrlPattern(pathname);
|
|
if (!pathname.startsWith("/")) {
|
|
pathname = `/${pathname}`;
|
|
}
|
|
|
|
if (pathname.includes("?") || pathname.includes("#")) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`URL pattern "pathname" contained a forbidden character, got "${pathname}"`
|
|
);
|
|
}
|
|
|
|
return pathname;
|
|
}
|
|
|
|
/**
|
|
* Parse the port property of a URLPatternPattern.
|
|
*
|
|
* @param {string} port
|
|
* A port property.
|
|
*
|
|
* @returns {string}
|
|
* The parsed property.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
*/
|
|
function parsePort(port) {
|
|
if (typeof port != "string" || port == "") {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected URLPattern "port" to be a non-empty string, got ${port}`
|
|
);
|
|
}
|
|
|
|
port = unescapeUrlPattern(port);
|
|
|
|
const isNumber = /^\d*$/.test(port);
|
|
if (!isNumber) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`URL pattern "port" is not a valid number, got "${port}"`
|
|
);
|
|
}
|
|
|
|
return `:${port}`;
|
|
}
|
|
|
|
/**
|
|
* Parse the protocol property of a URLPatternPattern.
|
|
*
|
|
* @param {string} protocol
|
|
* A protocol property.
|
|
*
|
|
* @returns {string}
|
|
* The parsed property.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
*/
|
|
function parseProtocol(protocol) {
|
|
if (typeof protocol != "string" || protocol == "") {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected URLPattern "protocol" to be a non-empty string, got ${protocol}`
|
|
);
|
|
}
|
|
|
|
protocol = unescapeUrlPattern(protocol);
|
|
if (!/^[a-zA-Z0-9+-.]*$/.test(protocol)) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`URL pattern "protocol" contained a forbidden character, got "${protocol}"`
|
|
);
|
|
}
|
|
|
|
return protocol;
|
|
}
|
|
|
|
/**
|
|
* Parse the search property of a URLPatternPattern.
|
|
*
|
|
* @param {string} search
|
|
* A search property.
|
|
*
|
|
* @returns {string}
|
|
* The parsed property.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
*/
|
|
function parseSearch(search) {
|
|
lazy.assert.string(
|
|
search,
|
|
lazy.pprint`Expected URL pattern "search" to be a string, got ${search}`
|
|
);
|
|
|
|
search = unescapeUrlPattern(search);
|
|
if (!search.startsWith("?")) {
|
|
search = `?${search}`;
|
|
}
|
|
|
|
if (search.includes("#")) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected URLPattern "search" to never contain "#", got ${search}`
|
|
);
|
|
}
|
|
|
|
return search;
|
|
}
|
|
|
|
/**
|
|
* Parse a string URL. This tries to be close to Basic URL Parser, however since
|
|
* this is not currently implemented in Firefox and URL parsing has many edge
|
|
* cases, it does not try to be a faithful implementation.
|
|
*
|
|
* Edge cases which are not supported are mostly about non-special URLs, which
|
|
* in practice should not be observable in automation.
|
|
*
|
|
* @param {string} url
|
|
* The string based URL to parse.
|
|
* @returns {ParsedURL}
|
|
* The parsed URL.
|
|
*/
|
|
function parseURL(url) {
|
|
const urlObj = new URL(url);
|
|
const uri = urlObj.URI;
|
|
|
|
return {
|
|
scheme: uri.scheme,
|
|
// Note: Use urlObj instead of uri for hostname:
|
|
// nsIURI removes brackets from ipv6 hostnames (eg [::1] becomes ::1).
|
|
host: urlObj.hostname,
|
|
path: uri.filePath,
|
|
// Note: Use urlObj instead of uri for port:
|
|
// nsIURI throws on the port getter for non-special schemes.
|
|
port: urlObj.port != "" ? Number(uri.port) : null,
|
|
query: uri.hasQuery ? uri.query : null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Serialize the path of a parsed URL.
|
|
*
|
|
* @see https://pr-preview.s3.amazonaws.com/w3c/webdriver-bidi/pull/429.html#parse-url-pattern
|
|
*
|
|
* @param {ParsedURL} url
|
|
* A parsed url.
|
|
*
|
|
* @returns {string}
|
|
* The serialized path
|
|
*/
|
|
function serializePath(url) {
|
|
// Check for opaque path
|
|
if (typeof url.path == "string") {
|
|
return url.path;
|
|
}
|
|
|
|
let serialized = "";
|
|
for (const segment of url.path) {
|
|
serialized += `/${segment}`;
|
|
}
|
|
|
|
return serialized;
|
|
}
|
|
|
|
/**
|
|
* Serialize the port of a parsed URL.
|
|
*
|
|
* @see https://pr-preview.s3.amazonaws.com/w3c/webdriver-bidi/pull/429.html#parse-url-pattern
|
|
*
|
|
* @param {ParsedURL} url
|
|
* A parsed url.
|
|
*
|
|
* @returns {string}
|
|
* The serialized port
|
|
*/
|
|
function serializePort(url) {
|
|
let port = null;
|
|
if (
|
|
SPECIAL_SCHEMES.includes(url.scheme) &&
|
|
DEFAULT_PORTS[url.scheme] !== null &&
|
|
(url.port === null || url.port == DEFAULT_PORTS[url.scheme])
|
|
) {
|
|
port = "";
|
|
} else if (url.port !== null) {
|
|
port = `${url.port}`;
|
|
}
|
|
|
|
return port;
|
|
}
|
|
|
|
/**
|
|
* Unescape and check a pattern string against common forbidden characters.
|
|
*
|
|
* @see https://pr-preview.s3.amazonaws.com/w3c/webdriver-bidi/pull/429.html#unescape-url-pattern
|
|
*
|
|
* @param {string} pattern
|
|
* Either a full URLPatternString pattern or a property of a URLPatternPattern.
|
|
*
|
|
* @returns {string}
|
|
* The unescaped pattern
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
*/
|
|
function unescapeUrlPattern(pattern) {
|
|
const forbiddenCharacters = ["(", ")", "*", "{", "}"];
|
|
const escapeCharacter = "\\";
|
|
|
|
let isEscaped = false;
|
|
let result = "";
|
|
|
|
for (const codepoint of Array.from(pattern)) {
|
|
if (!isEscaped) {
|
|
if (forbiddenCharacters.includes(codepoint)) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`URL pattern contained an unescaped forbidden character ${codepoint}`
|
|
);
|
|
}
|
|
|
|
if (codepoint == escapeCharacter) {
|
|
isEscaped = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
result += codepoint;
|
|
isEscaped = false;
|
|
}
|
|
|
|
return result;
|
|
}
|