397 lines
17 KiB
JavaScript
397 lines
17 KiB
JavaScript
"use strict";
|
|
// -------------------------------------------------------------
|
|
// WARNING: this file is used by both the client and the server.
|
|
// Do not use any browser or node-specific API!
|
|
// -------------------------------------------------------------
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.processMetaRefreshContent = exports.updateScriptImportUrls = exports.prepareUrl = exports.omitDefaultPort = exports.ensureOriginTrailingSlash = exports.isValidUrl = exports.isRelativeUrl = exports.isSpecialPage = exports.ensureTrailingSlash = exports.processSpecialChars = exports.correctMultipleSlashes = exports.handleUrlsSet = exports.formatUrl = exports.resolveUrlAsDest = exports.isSupportedProtocol = exports.parseUrl = exports.getPathname = exports.parseProxyUrl = exports.getDomain = exports.getProxyUrl = exports.getURLString = exports.sameOriginCheck = exports.isSubDomain = exports.restoreShortOrigin = exports.getResourceTypeString = exports.parseResourceType = exports.Credentials = exports.HTTPS_DEFAULT_PORT = exports.HTTP_DEFAULT_PORT = exports.SPECIAL_PAGES = exports.SPECIAL_ERROR_PAGE = exports.SPECIAL_BLANK_PAGE = exports.TRAILING_SLASH_RE = exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR = exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR = exports.HASH_RE = exports.SUPPORTED_PROTOCOL_RE = void 0;
|
|
const string_trim_1 = __importDefault(require("./string-trim"));
|
|
const URL_RE = /^\s*([\w-]+?:)?(?:\/\/(?:([^/]+)@)?(([^/%?;#: ]*)(?::(\d+))?))?(.*?)\s*$/;
|
|
const PROTOCOL_RE = /^([\w-]+?:)(\/\/|[^\\/]|$)/;
|
|
const QUERY_AND_HASH_RE = /(\?.+|#[^#]*)$/;
|
|
const PATH_AFTER_HOST_RE = /^\/([^/]+?)\/([\S\s]+)$/;
|
|
const HTTP_RE = /^https?:/;
|
|
const FILE_RE = /^file:/i;
|
|
const SHORT_ORIGIN_RE = /^http(s)?:\/\//;
|
|
const IS_SECURE_ORIGIN_RE = /^s\*/;
|
|
const META_REFRESH_RE = /^(.+?[;,]\s*(?:url\s*=\s*)?(['"])?)(.+?)?(\2)?$/i;
|
|
exports.SUPPORTED_PROTOCOL_RE = /^(?:https?|file):/i;
|
|
exports.HASH_RE = /^#/;
|
|
exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR = '!';
|
|
exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR = '*';
|
|
exports.TRAILING_SLASH_RE = /\/$/;
|
|
exports.SPECIAL_BLANK_PAGE = 'about:blank';
|
|
exports.SPECIAL_ERROR_PAGE = 'about:error';
|
|
exports.SPECIAL_PAGES = [exports.SPECIAL_BLANK_PAGE, exports.SPECIAL_ERROR_PAGE];
|
|
exports.HTTP_DEFAULT_PORT = '80';
|
|
exports.HTTPS_DEFAULT_PORT = '443';
|
|
var Credentials;
|
|
(function (Credentials) {
|
|
Credentials[Credentials["include"] = 0] = "include";
|
|
Credentials[Credentials["sameOrigin"] = 1] = "sameOrigin";
|
|
Credentials[Credentials["omit"] = 2] = "omit";
|
|
Credentials[Credentials["unknown"] = 3] = "unknown";
|
|
})(Credentials = exports.Credentials || (exports.Credentials = {})); // eslint-disable-line no-shadow
|
|
const SPECIAL_PAGE_DEST_RESOURCE_INFO = {
|
|
protocol: 'about:',
|
|
host: '',
|
|
hostname: '',
|
|
port: '',
|
|
partAfterHost: '',
|
|
};
|
|
const RESOURCE_TYPES = [
|
|
{ name: 'isIframe', flag: 'i' },
|
|
{ name: 'isForm', flag: 'f' },
|
|
{ name: 'isScript', flag: 's' },
|
|
{ name: 'isEventSource', flag: 'e' },
|
|
{ name: 'isHtmlImport', flag: 'h' },
|
|
{ name: 'isWebSocket', flag: 'w' },
|
|
{ name: 'isServiceWorker', flag: 'c' },
|
|
{ name: 'isAjax', flag: 'a' },
|
|
{ name: 'isObject', flag: 'o' },
|
|
];
|
|
function parseResourceType(resourceType) {
|
|
const parsedResourceType = {};
|
|
if (!resourceType)
|
|
return parsedResourceType;
|
|
for (const { name, flag } of RESOURCE_TYPES) {
|
|
if (resourceType.indexOf(flag) > -1)
|
|
parsedResourceType[name] = true;
|
|
}
|
|
return parsedResourceType;
|
|
}
|
|
exports.parseResourceType = parseResourceType;
|
|
function getResourceTypeString(parsedResourceType) {
|
|
if (!parsedResourceType)
|
|
return null;
|
|
let resourceType = '';
|
|
for (const { name, flag } of RESOURCE_TYPES) {
|
|
if (parsedResourceType[name])
|
|
resourceType += flag;
|
|
}
|
|
return resourceType || null;
|
|
}
|
|
exports.getResourceTypeString = getResourceTypeString;
|
|
function makeShortOrigin(origin) {
|
|
return origin === 'null' ? '' : origin.replace(SHORT_ORIGIN_RE, (_, secure) => secure ? 's*' : '');
|
|
}
|
|
function restoreShortOrigin(origin) {
|
|
if (!origin)
|
|
return 'null';
|
|
return IS_SECURE_ORIGIN_RE.test(origin) ? origin.replace(IS_SECURE_ORIGIN_RE, 'https://') : 'http://' + origin;
|
|
}
|
|
exports.restoreShortOrigin = restoreShortOrigin;
|
|
function isSubDomain(domain, subDomain) {
|
|
domain = domain.replace(/^www./i, '');
|
|
subDomain = subDomain.replace(/^www./i, '');
|
|
if (domain === subDomain)
|
|
return true;
|
|
const index = subDomain.lastIndexOf(domain);
|
|
return subDomain[index - 1] === '.' && subDomain.length === index + domain.length;
|
|
}
|
|
exports.isSubDomain = isSubDomain;
|
|
function sameOriginCheck(location, checkedUrl) {
|
|
if (!checkedUrl)
|
|
return true;
|
|
const parsedCheckedUrl = parseUrl(checkedUrl);
|
|
const isRelative = !parsedCheckedUrl.host;
|
|
if (isRelative)
|
|
return true;
|
|
const parsedLocation = parseUrl(location);
|
|
const parsedProxyLocation = parseProxyUrl(location);
|
|
if (parsedCheckedUrl.host === parsedLocation.host && parsedCheckedUrl.protocol === parsedLocation.protocol)
|
|
return true;
|
|
const parsedDestUrl = parsedProxyLocation ? parsedProxyLocation.destResourceInfo : parsedLocation;
|
|
if (!parsedDestUrl)
|
|
return false;
|
|
const isSameProtocol = !parsedCheckedUrl.protocol || parsedCheckedUrl.protocol === parsedDestUrl.protocol;
|
|
const portsEq = !parsedDestUrl.port && !parsedCheckedUrl.port ||
|
|
parsedDestUrl.port && parsedDestUrl.port.toString() === parsedCheckedUrl.port;
|
|
return isSameProtocol && !!portsEq && parsedDestUrl.hostname === parsedCheckedUrl.hostname;
|
|
}
|
|
exports.sameOriginCheck = sameOriginCheck;
|
|
// NOTE: Convert the destination protocol and hostname to the lower case. (GH-1)
|
|
function convertHostToLowerCase(url) {
|
|
const parsedUrl = parseUrl(url);
|
|
parsedUrl.protocol = parsedUrl.protocol && parsedUrl.protocol.toLowerCase();
|
|
parsedUrl.host = parsedUrl.host && parsedUrl.host.toLowerCase();
|
|
return formatUrl(parsedUrl);
|
|
}
|
|
function getURLString(url) {
|
|
// TODO: fix it
|
|
// eslint-disable-next-line no-undef
|
|
if (url === null && /iPad|iPhone/i.test(window.navigator.userAgent))
|
|
return '';
|
|
return String(url).replace(/[\n\t]/g, '');
|
|
}
|
|
exports.getURLString = getURLString;
|
|
function getProxyUrl(url, opts) {
|
|
const sessionInfo = [opts.sessionId];
|
|
if (opts.windowId)
|
|
sessionInfo.push(opts.windowId);
|
|
const params = [sessionInfo.join(exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR)];
|
|
if (opts.resourceType)
|
|
params.push(opts.resourceType);
|
|
if (opts.charset)
|
|
params.push(opts.charset.toLowerCase());
|
|
if (typeof opts.credentials === 'number')
|
|
params.push(opts.credentials.toString());
|
|
if (opts.reqOrigin)
|
|
params.push(encodeURIComponent(makeShortOrigin(opts.reqOrigin)));
|
|
const descriptor = params.join(exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR);
|
|
const proxyProtocol = opts.proxyProtocol || 'http:';
|
|
return `${proxyProtocol}//${opts.proxyHostname}:${opts.proxyPort}/${descriptor}/${convertHostToLowerCase(url)}`;
|
|
}
|
|
exports.getProxyUrl = getProxyUrl;
|
|
function getDomain(parsed) {
|
|
if (parsed.protocol === 'file:')
|
|
return 'null';
|
|
return formatUrl({
|
|
protocol: parsed.protocol,
|
|
host: parsed.host,
|
|
hostname: parsed.hostname,
|
|
port: String(parsed.port || ''),
|
|
});
|
|
}
|
|
exports.getDomain = getDomain;
|
|
function parseRequestDescriptor(desc) {
|
|
const [sessionInfo, resourceType, ...resourceData] = desc.split(exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR);
|
|
if (!sessionInfo)
|
|
return null;
|
|
const [sessionId, windowId] = sessionInfo.split(exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR);
|
|
const parsedDesc = { sessionId, resourceType: resourceType || null };
|
|
if (windowId)
|
|
parsedDesc.windowId = windowId;
|
|
if (resourceType && resourceData.length) {
|
|
const parsedResourceType = parseResourceType(resourceType);
|
|
if (parsedResourceType.isScript || parsedResourceType.isServiceWorker)
|
|
parsedDesc.charset = resourceData[0];
|
|
else if (parsedResourceType.isWebSocket)
|
|
parsedDesc.reqOrigin = decodeURIComponent(restoreShortOrigin(resourceData[0]));
|
|
else if (parsedResourceType.isIframe && resourceData[0])
|
|
parsedDesc.reqOrigin = decodeURIComponent(restoreShortOrigin(resourceData[0]));
|
|
else if (parsedResourceType.isAjax) {
|
|
parsedDesc.credentials = parseInt(resourceData[0], 10);
|
|
if (resourceData.length === 2)
|
|
parsedDesc.reqOrigin = decodeURIComponent(restoreShortOrigin(resourceData[1]));
|
|
}
|
|
}
|
|
return parsedDesc;
|
|
}
|
|
function parseProxyUrl(proxyUrl) {
|
|
// TODO: Remove it.
|
|
const parsedUrl = parseUrl(proxyUrl);
|
|
if (!parsedUrl.partAfterHost)
|
|
return null;
|
|
const match = parsedUrl.partAfterHost.match(PATH_AFTER_HOST_RE);
|
|
if (!match)
|
|
return null;
|
|
const parsedDesc = parseRequestDescriptor(match[1]);
|
|
// NOTE: We should have, at least, the job uid and the owner token.
|
|
if (!parsedDesc)
|
|
return null;
|
|
let destUrl = match[2];
|
|
// Browser can redirect to a special page with hash (GH-1671)
|
|
const destUrlWithoutHash = destUrl.replace(/#[\S\s]*$/, '');
|
|
if (!isSpecialPage(destUrlWithoutHash) && !exports.SUPPORTED_PROTOCOL_RE.test(destUrl))
|
|
return null;
|
|
let destResourceInfo;
|
|
if (isSpecialPage(destUrlWithoutHash))
|
|
destResourceInfo = SPECIAL_PAGE_DEST_RESOURCE_INFO;
|
|
else {
|
|
destUrl = omitDefaultPort(destUrl);
|
|
destResourceInfo = parseUrl(destUrl);
|
|
}
|
|
return {
|
|
destUrl,
|
|
destResourceInfo,
|
|
partAfterHost: parsedUrl.partAfterHost,
|
|
proxy: {
|
|
hostname: parsedUrl.hostname || '',
|
|
port: parsedUrl.port || '',
|
|
},
|
|
sessionId: parsedDesc.sessionId,
|
|
resourceType: parsedDesc.resourceType,
|
|
charset: parsedDesc.charset,
|
|
reqOrigin: parsedDesc.reqOrigin,
|
|
windowId: parsedDesc.windowId,
|
|
credentials: parsedDesc.credentials,
|
|
};
|
|
}
|
|
exports.parseProxyUrl = parseProxyUrl;
|
|
function getPathname(path) {
|
|
return path.replace(QUERY_AND_HASH_RE, '');
|
|
}
|
|
exports.getPathname = getPathname;
|
|
function parseUrl(url) {
|
|
url = processSpecialChars(url);
|
|
if (!url)
|
|
return {};
|
|
const urlMatch = url.match(URL_RE);
|
|
return urlMatch ? {
|
|
protocol: urlMatch[1],
|
|
auth: urlMatch[2],
|
|
host: urlMatch[3],
|
|
hostname: urlMatch[4],
|
|
port: urlMatch[5],
|
|
partAfterHost: urlMatch[6],
|
|
} : {};
|
|
}
|
|
exports.parseUrl = parseUrl;
|
|
function isSupportedProtocol(url) {
|
|
url = (0, string_trim_1.default)(url || '');
|
|
const isHash = exports.HASH_RE.test(url);
|
|
if (isHash)
|
|
return false;
|
|
const protocol = url.match(PROTOCOL_RE);
|
|
if (!protocol)
|
|
return true;
|
|
return exports.SUPPORTED_PROTOCOL_RE.test(protocol[0]);
|
|
}
|
|
exports.isSupportedProtocol = isSupportedProtocol;
|
|
function resolveUrlAsDest(url, getProxyUrlMeth, isUrlsSet = false) {
|
|
if (isUrlsSet)
|
|
return handleUrlsSet(resolveUrlAsDest, url, getProxyUrlMeth);
|
|
getProxyUrlMeth = getProxyUrlMeth || getProxyUrl;
|
|
if (isSupportedProtocol(url)) {
|
|
const proxyUrl = getProxyUrlMeth(url);
|
|
const parsedProxyUrl = parseProxyUrl(proxyUrl);
|
|
return parsedProxyUrl ? formatUrl(parsedProxyUrl.destResourceInfo) : url;
|
|
}
|
|
return url;
|
|
}
|
|
exports.resolveUrlAsDest = resolveUrlAsDest;
|
|
function formatUrl(parsedUrl) {
|
|
// NOTE: the URL is relative.
|
|
if (parsedUrl.protocol !== 'file:' && parsedUrl.protocol !== 'about:' &&
|
|
!parsedUrl.host && (!parsedUrl.hostname || !parsedUrl.port))
|
|
return parsedUrl.partAfterHost || '';
|
|
let url = parsedUrl.protocol || '';
|
|
if (parsedUrl.protocol !== 'about:')
|
|
url += '//';
|
|
if (parsedUrl.auth)
|
|
url += parsedUrl.auth + '@';
|
|
if (parsedUrl.host)
|
|
url += parsedUrl.host;
|
|
else if (parsedUrl.hostname) {
|
|
url += parsedUrl.hostname;
|
|
if (parsedUrl.port)
|
|
url += ':' + parsedUrl.port;
|
|
}
|
|
if (parsedUrl.partAfterHost)
|
|
url += parsedUrl.partAfterHost;
|
|
return url;
|
|
}
|
|
exports.formatUrl = formatUrl;
|
|
function handleUrlsSet(handler, url, ...args) {
|
|
const resourceUrls = url.split(',');
|
|
const replacedUrls = [];
|
|
for (const fullUrlStr of resourceUrls) {
|
|
const [urlStr, postUrlStr] = fullUrlStr.replace(/ +/g, ' ').trim().split(' ');
|
|
if (urlStr) {
|
|
const replacedUrl = handler(urlStr, ...args);
|
|
replacedUrls.push(replacedUrl + (postUrlStr ? ` ${postUrlStr}` : ''));
|
|
}
|
|
}
|
|
return replacedUrls.join(',');
|
|
}
|
|
exports.handleUrlsSet = handleUrlsSet;
|
|
function correctMultipleSlashes(url, pageProtocol = '') {
|
|
// NOTE: Remove unnecessary slashes from the beginning of the url and after scheme.
|
|
// For example:
|
|
// "//////example.com" -> "//example.com" (scheme-less HTTP(S) URL)
|
|
// "////home/testcafe/documents" -> "///home/testcafe/documents" (scheme-less unix file URL)
|
|
// "http:///example.com" -> "http://example.com"
|
|
//
|
|
// And add missing slashes after the file scheme.
|
|
// "file://C:/document.txt" -> "file:///C:/document.txt"
|
|
if (url.match(FILE_RE) || pageProtocol.match(FILE_RE)) {
|
|
return url
|
|
.replace(/^(file:)?\/+(\/\/\/.*$)/i, '$1$2')
|
|
.replace(/^(file:)?\/*([A-Za-z]):/i, '$1///$2:');
|
|
}
|
|
return url.replace(/^(https?:)?\/+(\/\/.*$)/i, '$1$2');
|
|
}
|
|
exports.correctMultipleSlashes = correctMultipleSlashes;
|
|
function processSpecialChars(url) {
|
|
return correctMultipleSlashes(getURLString(url));
|
|
}
|
|
exports.processSpecialChars = processSpecialChars;
|
|
function ensureTrailingSlash(srcUrl, processedUrl) {
|
|
if (!isValidUrl(processedUrl))
|
|
return processedUrl;
|
|
const srcUrlEndsWithTrailingSlash = exports.TRAILING_SLASH_RE.test(srcUrl);
|
|
const processedUrlEndsWithTrailingSlash = exports.TRAILING_SLASH_RE.test(processedUrl);
|
|
if (srcUrlEndsWithTrailingSlash && !processedUrlEndsWithTrailingSlash)
|
|
processedUrl += '/';
|
|
else if (srcUrl && !srcUrlEndsWithTrailingSlash && processedUrlEndsWithTrailingSlash)
|
|
processedUrl = processedUrl.replace(exports.TRAILING_SLASH_RE, '');
|
|
return processedUrl;
|
|
}
|
|
exports.ensureTrailingSlash = ensureTrailingSlash;
|
|
function isSpecialPage(url) {
|
|
return exports.SPECIAL_PAGES.indexOf(url) !== -1;
|
|
}
|
|
exports.isSpecialPage = isSpecialPage;
|
|
function isRelativeUrl(url) {
|
|
const parsedUrl = parseUrl(url);
|
|
return parsedUrl.protocol !== 'file:' && !parsedUrl.host;
|
|
}
|
|
exports.isRelativeUrl = isRelativeUrl;
|
|
function isValidPort(port) {
|
|
const parsedPort = parseInt(port, 10);
|
|
return parsedPort > 0 && parsedPort <= 65535;
|
|
}
|
|
function isValidUrl(url) {
|
|
const parsedUrl = parseUrl(url);
|
|
return parsedUrl.protocol === 'file:' || parsedUrl.protocol === 'about:' ||
|
|
!!parsedUrl.hostname && (!parsedUrl.port || isValidPort(parsedUrl.port));
|
|
}
|
|
exports.isValidUrl = isValidUrl;
|
|
function ensureOriginTrailingSlash(url) {
|
|
// NOTE: If you request an url containing only port, host and protocol
|
|
// then browser adds the trailing slash itself.
|
|
const parsedUrl = parseUrl(url);
|
|
if (!parsedUrl.partAfterHost && parsedUrl.protocol && HTTP_RE.test(parsedUrl.protocol))
|
|
return url + '/';
|
|
return url;
|
|
}
|
|
exports.ensureOriginTrailingSlash = ensureOriginTrailingSlash;
|
|
function omitDefaultPort(url) {
|
|
// NOTE: If you request an url containing default port
|
|
// then browser remove this one itself.
|
|
const parsedUrl = parseUrl(url);
|
|
const hasDefaultPort = parsedUrl.protocol === 'https:' && parsedUrl.port === exports.HTTPS_DEFAULT_PORT ||
|
|
parsedUrl.protocol === 'http:' && parsedUrl.port === exports.HTTP_DEFAULT_PORT;
|
|
if (hasDefaultPort) {
|
|
parsedUrl.host = parsedUrl.hostname;
|
|
parsedUrl.port = '';
|
|
return formatUrl(parsedUrl);
|
|
}
|
|
return url;
|
|
}
|
|
exports.omitDefaultPort = omitDefaultPort;
|
|
function prepareUrl(url) {
|
|
url = omitDefaultPort(url);
|
|
url = ensureOriginTrailingSlash(url);
|
|
return url;
|
|
}
|
|
exports.prepareUrl = prepareUrl;
|
|
function updateScriptImportUrls(cachedScript, serverInfo, sessionId, windowId) {
|
|
const regExp = new RegExp('(' + serverInfo.protocol + '//' + serverInfo.hostname + ':(?:' + serverInfo.port + '|' +
|
|
serverInfo.crossDomainPort + ')/)[^/' + exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR + ']+', 'g');
|
|
const pattern = '$1' + sessionId + (windowId ? exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR + windowId : '');
|
|
return cachedScript.replace(regExp, pattern);
|
|
}
|
|
exports.updateScriptImportUrls = updateScriptImportUrls;
|
|
function processMetaRefreshContent(content, urlReplacer) {
|
|
const match = content.match(META_REFRESH_RE);
|
|
if (!match || !match[3])
|
|
return content;
|
|
return match[1] + urlReplacer(match[3]) + (match[4] || '');
|
|
}
|
|
exports.processMetaRefreshContent = processMetaRefreshContent;
|