Files
cypress/packages/network/lib/cors.ts
T

247 lines
7.6 KiB
TypeScript

import _ from 'lodash'
import * as uri from './uri'
import debugModule from 'debug'
import _parseDomain from '@cypress/parse-domain'
import type { ParsedHost, ParsedHostWithProtocolAndHost } from './types'
type Policy = 'same-origin' | 'same-super-domain-origin' | 'schemeful-same-site'
const debug = debugModule('cypress:network:cors')
// match IP addresses or anything following the last .
const customTldsRe = /(^[\d\.]+$|\.[^\.]+$)/
const strictSameOriginDomains = Object.freeze(['google.com'])
export function getSuperDomain (url) {
const parsed = parseUrlIntoHostProtocolDomainTldPort(url)
return _.compact([parsed.domain, parsed.tld]).join('.')
}
export function parseDomain (domain: string, options = {}) {
return _parseDomain(domain, _.defaults(options, {
privateTlds: true,
customTlds: customTldsRe,
}))
}
export function parseUrlIntoHostProtocolDomainTldPort (str) {
let { hostname, port, protocol } = uri.parse(str)
if (!hostname) {
hostname = ''
}
if (!port) {
port = protocol === 'https:' ? '443' : '80'
}
let parsed: Partial<ParsedHostWithProtocolAndHost> | null = parseDomain(hostname)
// if we couldn't get a parsed domain
if (!parsed) {
// then just fall back to a dumb check
// based on assumptions that the tld
// is the last segment after the final
// '.' and that the domain is the segment
// before that
const segments = hostname.split('.')
parsed = {
subdomain: segments[segments.length - 3] || '',
tld: segments[segments.length - 1] || '',
domain: segments[segments.length - 2] || '',
}
}
const obj: ParsedHostWithProtocolAndHost = {
port,
protocol,
subdomain: parsed.subdomain || null,
domain: parsed.domain,
tld: parsed.tld,
}
debug('Parsed URL %o', obj)
return obj
}
export function getDomainNameFromUrl (url: string) {
const parsedHost = parseUrlIntoHostProtocolDomainTldPort(url)
return getDomainNameFromParsedHost(parsedHost)
}
export function getDomainNameFromParsedHost (parsedHost: ParsedHost) {
return _.compact([parsedHost.domain, parsedHost.tld]).join('.')
}
/**
* same-origin: Whether or not a a urls scheme, port, and host match. @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
* same-super-domain-origin: Whether or not a url's scheme, domain, top-level domain, and port match
* same-site: Whether or not a url's scheme, domain, and top-level domain match. @see https://developer.mozilla.org/en-US/docs/Glossary/Site
* @param {Policy} policy - the policy being used
* @param {string} frameUrl - the url being compared
* @param {ParsedHostWithProtocolAndHost} topProps - the props being compared against the url
* @returns {boolean} whether or not the props and url fit the policy
*/
function urlMatchesPolicyProps ({ policy, frameUrl, topProps }: {
policy: Policy
frameUrl: string
topProps: ParsedHostWithProtocolAndHost
}): boolean {
if (!policy || !frameUrl || !topProps) {
return false
}
const urlProps = parseUrlIntoHostProtocolDomainTldPort(frameUrl)
switch (policy) {
case 'same-origin': {
// if same origin, all parts of the props needs to match, including subdomain and scheme
return _.isEqual(urlProps, topProps)
}
case 'same-super-domain-origin':
case 'schemeful-same-site': {
const { port: port1, subdomain: _unused1, ...parsedUrl } = urlProps
const { port: port2, subdomain: _unused2, ...relevantProps } = topProps
let doPortsPassSameSchemeCheck: boolean
if (policy === 'same-super-domain-origin') {
// if a super domain origin comparison, the ports MUST be strictly equal
doPortsPassSameSchemeCheck = port1 === port2
} else {
// otherwise, this is a same-site comparison
// If HTTPS, ports NEED to match. Otherwise, HTTP ports can be different and are same origin
doPortsPassSameSchemeCheck = port1 !== port2 ? (port1 !== '443' && port2 !== '443') : true
}
return doPortsPassSameSchemeCheck && _.isEqual(parsedUrl, relevantProps)
}
default:
return false
}
}
function urlMatchesPolicy ({ policy, frameUrl, topUrl }: {
policy: Policy
frameUrl: string
topUrl: string
}): boolean {
if (!policy || !frameUrl || !topUrl) {
return false
}
return urlMatchesPolicyProps({
policy,
frameUrl,
topProps: parseUrlIntoHostProtocolDomainTldPort(topUrl),
})
}
export function urlOriginsMatch (frameUrl: string, topUrl: string): boolean {
return urlMatchesPolicy({
policy: 'same-origin',
frameUrl,
topUrl,
})
}
export const urlSameSiteMatch = (frameUrl: string, topUrl: string): boolean => {
return urlMatchesPolicy({
policy: 'schemeful-same-site',
frameUrl,
topUrl,
})
}
/**
* Returns the policy that will be used for the specified url.
* @param url - the url to check the policy against.
* @returns a Policy string.
*/
export const policyForDomain = (url: string): Policy => {
const obj = parseUrlIntoHostProtocolDomainTldPort(url)
return strictSameOriginDomains.includes(`${obj.domain}.${obj.tld}`) ? 'same-origin' : 'same-super-domain-origin'
}
/**
* Checks the supplied url's against the determined policy.
* The policy is same-super-domain-origin unless the domain is in the list of strict same origin domains,
* in which case the policy is 'same-origin'
* @param frameUrl - The url you are testing the policy for.
* @param topUrl - The url you are testing the policy in context of.
* @returns boolean, true if matching, false if not.
*/
export const urlMatchesPolicyBasedOnDomain = (frameUrl: string, topUrl: string): boolean => {
return urlMatchesPolicy({
policy: policyForDomain(frameUrl),
frameUrl,
topUrl,
})
}
/**
* Checks the supplied url and props against the determined policy.
* The policy is same-super-domain-origin unless the domain is in the list of strict same origin domains,
* in which case the policy is 'same-origin'
* @param frameUrl - The url you are testing the policy for.
* @param topProps - The props of the url you are testing the policy in context of.
* @returns boolean, true if matching, false if not.
*/
export const urlMatchesPolicyBasedOnDomainProps = (frameUrl: string, topProps: ParsedHostWithProtocolAndHost): boolean => {
const obj = parseUrlIntoHostProtocolDomainTldPort(frameUrl)
const policy = strictSameOriginDomains.includes(`${obj.domain}.${obj.tld}`) ? 'same-origin' : 'same-super-domain-origin'
return urlMatchesPolicyProps({
policy,
frameUrl,
topProps,
})
}
declare module 'url' {
interface UrlWithStringQuery {
format(): string
}
}
export function urlMatchesOriginProtectionSpace (urlStr, origin) {
const normalizedUrl = uri.addDefaultPort(urlStr).format()
const normalizedOrigin = uri.addDefaultPort(origin).format()
return _.startsWith(normalizedUrl, normalizedOrigin)
}
export function getOrigin (url: string) {
// @ts-ignore
const { origin } = new URL(url)
// origin is comprised of:
// protocol + subdomain + superdomain + port
return origin
}
/**
* Returns the super-domain of a URL
*
* The primary driver uses the super-domain origin to allow tests to
* navigate between subdomains of the same super-domain by setting
* document.domain to the super-domain
* @param url - the full absolute url
* @returns the super domain origin
* ex: http://www.example.com:8081/my/path -> http://example.com:8081
*/
export function getSuperDomainOrigin (url: string) {
// @ts-ignore
const { port, protocol } = new URL(url)
// super domain origin is comprised of:
// protocol + superdomain + port (subdomain is not factored in)
return _.compact([`${protocol}//${getSuperDomain(url)}`, port]).join(':')
}