"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;