diff --git a/npm/grep/README.md b/npm/grep/README.md index 9466d08098de..7114b854bed9 100644 --- a/npm/grep/README.md +++ b/npm/grep/README.md @@ -84,12 +84,12 @@ yarn add -D @cypress/grep ```js // cypress/support/index.js // load and register the grep feature using "require" function -// https://github.com/cypress-io/cypress-grep -const registerCypressGrep = require('cypress-grep') +// https://github.com/cypress-io/cypress/tree/develop/npm/grep +const registerCypressGrep = require('@cypress/grep') registerCypressGrep() // if you want to use the "import" keyword -import registerCypressGrep from 'cypress-grep' +import registerCypressGrep from '@cypress/grep' registerCypressGrep() ``` @@ -102,7 +102,7 @@ registerCypressGrep() { e2e: { setupNodeEvents(on, config) { - require('cypress-grep/src/plugin')(config); + require('@cypress/grep/src/plugin')(config); return config; }, } diff --git a/npm/grep/src/support.js b/npm/grep/src/support.js index 844ef51ecf0e..5fd42b5db588 100644 --- a/npm/grep/src/support.js +++ b/npm/grep/src/support.js @@ -4,7 +4,7 @@ const { parseGrep, shouldTestRun } = require('./utils') // @ts-ignore const { version } = require('../package.json') -const debug = require('debug')('cypress-grep') +const debug = require('debug')('@cypress/grep') debug.log = console.info.bind(console) diff --git a/npm/vite-dev-server/CHANGELOG.md b/npm/vite-dev-server/CHANGELOG.md index 6fcb11e650f6..65687aa877a3 100644 --- a/npm/vite-dev-server/CHANGELOG.md +++ b/npm/vite-dev-server/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/vite-dev-server-v4.0.1](https://github.com/cypress-io/cypress/compare/@cypress/vite-dev-server-v4.0.0...@cypress/vite-dev-server-v4.0.1) (2022-11-08) + + +### Bug Fixes + +* vite-dev-server hoisting issue in binary ([#24599](https://github.com/cypress-io/cypress/issues/24599)) ([2513bea](https://github.com/cypress-io/cypress/commit/2513beac307e95267ab736a93a39cd1cd1280506)) + # [@cypress/vite-dev-server-v4.0.0](https://github.com/cypress-io/cypress/compare/@cypress/vite-dev-server-v3.4.0...@cypress/vite-dev-server-v4.0.0) (2022-11-07) diff --git a/npm/vite-dev-server/package.json b/npm/vite-dev-server/package.json index d102af69b040..ac9f49f2a45e 100644 --- a/npm/vite-dev-server/package.json +++ b/npm/vite-dev-server/package.json @@ -15,7 +15,7 @@ "test-unit": "mocha -r ts-node/register/transpile-only --config ./test/.mocharc.js" }, "dependencies": { - "debug": "4.3.3", + "debug": "^4.3.4", "find-up": "6.3.0", "node-html-parser": "5.3.3" }, diff --git a/npm/webpack-dev-server/CHANGELOG.md b/npm/webpack-dev-server/CHANGELOG.md index f088b3f3df03..5bbe93bd80ea 100644 --- a/npm/webpack-dev-server/CHANGELOG.md +++ b/npm/webpack-dev-server/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/webpack-dev-server-v3.0.1](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v3.0.0...@cypress/webpack-dev-server-v3.0.1) (2022-11-08) + + +### Bug Fixes + +* revert dynamic import of webpack config file ([#24598](https://github.com/cypress-io/cypress/issues/24598)) ([69f7dab](https://github.com/cypress-io/cypress/commit/69f7dabc94a5adb25edc7fda6057e92b8bd2e072)) + # [@cypress/webpack-dev-server-v3.0.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v2.5.0...@cypress/webpack-dev-server-v3.0.0) (2022-11-07) diff --git a/npm/webpack-dev-server/package.json b/npm/webpack-dev-server/package.json index 0e216e896d6e..810ddf973f9e 100644 --- a/npm/webpack-dev-server/package.json +++ b/npm/webpack-dev-server/package.json @@ -21,6 +21,7 @@ "fs-extra": "9.1.0", "html-webpack-plugin-4": "npm:html-webpack-plugin@^4", "html-webpack-plugin-5": "npm:html-webpack-plugin@^5", + "local-pkg": "0.4.1", "speed-measure-webpack-plugin": "1.4.2", "tslib": "^2.3.1", "webpack-dev-server": "^4.7.4", diff --git a/npm/webpack-dev-server/src/makeWebpackConfig.ts b/npm/webpack-dev-server/src/makeWebpackConfig.ts index 9e97472e5970..d428fe68ea25 100644 --- a/npm/webpack-dev-server/src/makeWebpackConfig.ts +++ b/npm/webpack-dev-server/src/makeWebpackConfig.ts @@ -1,11 +1,12 @@ import { debug as debugFn } from 'debug' import * as path from 'path' import { merge } from 'webpack-merge' +import { importModule } from 'local-pkg' import type { Configuration, EntryObject } from 'webpack' import { makeCypressWebpackConfig } from './makeDefaultWebpackConfig' import type { CreateFinalWebpackConfig } from './createWebpackDevServer' import { configFiles } from './constants' -import { dynamicAbsoluteImport, dynamicImport } from './dynamic-import' +import { dynamicImport } from './dynamic-import' const debug = debugFn('cypress:webpack-dev-server:makeWebpackConfig') @@ -90,7 +91,7 @@ export async function makeWebpackConfig ( if (configFile) { debug('found webpack config %s', configFile) - const sourcedConfig = await dynamicAbsoluteImport(configFile) + const sourcedConfig = await importModule(configFile) debug('config contains %o', sourcedConfig) if (sourcedConfig && typeof sourcedConfig === 'object') { diff --git a/package.json b/package.json index c08631d16b83..f364b3b8b21b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "10.11.0", + "version": "11.0.0", "description": "Cypress is a next generation front end testing tool built for the modern web", "private": true, "scripts": { diff --git a/packages/config/src/browser.ts b/packages/config/src/browser.ts index d9b85e52a1e5..75150ea41c97 100644 --- a/packages/config/src/browser.ts +++ b/packages/config/src/browser.ts @@ -156,7 +156,7 @@ export const matchesConfigKey = (key: string) => { } export const validate = (cfg: any, onErr: (property: ErrResult | string) => void, testingType: TestingType | null) => { - debug('validating configuration') + debug('validating configuration', cfg) return _.each(cfg, (value, key) => { const validationFn = validationRules[key] @@ -166,7 +166,7 @@ export const validate = (cfg: any, onErr: (property: ErrResult | string) => void const result = validationFn(key, value, { testingType, // TODO: remove with experimentalSessionAndOrigin. Fixed with: https://github.com/cypress-io/cypress/issues/21471 - experimentalSessionAndOrigin: cfg.experimentalSessionAndOrigin, + experimentalSessionAndOrigin: cfg.e2e?.experimentalSessionAndOrigin || cfg.experimentalSessionAndOrigin, }) if (result !== true) { diff --git a/packages/driver/cypress/e2e/e2e/origin/origin.cy.ts b/packages/driver/cypress/e2e/e2e/origin/origin.cy.ts index c76c2f174dff..88b7220da32f 100644 --- a/packages/driver/cypress/e2e/e2e/origin/origin.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/origin.cy.ts @@ -10,6 +10,44 @@ describe('cy.origin', { browser: '!webkit' }, () => { }) }) + it('creates and injects into google subdomains', () => { + // Intercept google to keep our tests independent from google. + cy.intercept('https://www.google.com', { + body: '
google.com
', + }) + + cy.intercept('https://accounts.google.com', { + body: 'accounts.google.com
', + }) + + cy.visit('https://www.google.com') + cy.visit('https://accounts.google.com') + cy.origin('https://accounts.google.com', () => { + cy.window().then((win) => { + expect(win.Cypress).to.exist + }) + }) + }) + + it('creates and injects into google subdomains when visiting in an origin block', () => { + // Intercept google to keep our tests independent from google. + cy.intercept('https://www.google.com', { + body: 'google.com
', + }) + + cy.intercept('https://accounts.google.com', { + body: 'accounts.google.com
', + }) + + cy.visit('https://www.google.com') + cy.origin('https://accounts.google.com', () => { + cy.visit('https://accounts.google.com') + cy.window().then((win) => { + expect(win.Cypress).to.exist + }) + }) + }) + it('passes viewportWidth/Height state to the secondary origin', () => { const expectedViewport = [320, 480] diff --git a/packages/driver/cypress/e2e/e2e/origin/validation.cy.ts b/packages/driver/cypress/e2e/e2e/origin/validation.cy.ts index ba15a29cb2f4..e726cee2f56c 100644 --- a/packages/driver/cypress/e2e/e2e/origin/validation.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/validation.cy.ts @@ -1,4 +1,8 @@ describe('cy.origin', { browser: '!webkit' }, () => { + beforeEach(() => { + cy.visit('') + }) + describe('successes', () => { it('succeeds on a localhost domain name', () => { cy.origin('localhost', () => undefined) @@ -357,3 +361,83 @@ describe('cy.origin', { browser: '!webkit' }, () => { }) }) }) + +describe('cy.origin - external hosts', { browser: '!webkit' }, () => { + describe('successes', () => { + it('succeeds on a complete origin from https using https', () => { + cy.visit('https://www.foobar.com:3502/fixtures/primary-origin.html') + cy.origin('https://www.idp.com:3502', () => undefined) + cy.then(() => { + const expectedSrc = `https://www.idp.com:3502/__cypress/spec-bridge-iframes` + const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://www.idp.com:3502') as HTMLIFrameElement + + expect(iframe.src).to.equal(expectedSrc) + }) + }) + + it('succeeds if url is the super domain as top but the super domain is excepted and must be strictly same origin', () => { + // Intercept google to keep our tests independent from google. + cy.intercept('https://www.google.com', { + body: '', + }) + + cy.visit('https://www.google.com') + cy.origin('accounts.google.com', () => undefined) + cy.then(() => { + const expectedSrc = `https://accounts.google.com/__cypress/spec-bridge-iframes` + const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://accounts.google.com') as HTMLIFrameElement + + expect(iframe.src).to.equal(expectedSrc) + }) + }) + }) + + describe('errors', () => { + it('errors if the url param is same superDomainOrigin as top', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('`cy.origin()` requires the first argument to be a different domain than top. You passed `http://app.foobar.com` to the origin command, while top is at `http://www.foobar.com`.') + + done() + }) + + cy.intercept('http://www.foobar.com', { + body: '
', + }) + + cy.intercept('http://app.foobar.com', { + body: '
', + }) + + cy.visit('http://www.foobar.com') + + cy.origin('http://app.foobar.com', () => undefined) + }) + + it('errors if the url param is same origin as top', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('`cy.origin()` requires the first argument to be a different origin than top. You passed `https://www.google.com` to the origin command, while top is at `https://www.google.com`.') + + done() + }) + + // Intercept google to keep our tests independent from google. + cy.intercept('https://www.google.com', { + body: '
', + }) + + cy.visit('https://www.google.com') + cy.origin('https://www.google.com', () => undefined) + }) + + it('errors and does not hang when throwing a mixed content error creating the spec bridge', { defaultCommandTimeout: 50 }, (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include(`\`cy.origin()\` failed to create a spec bridge to communicate with the specified origin. This can happen when you attempt to create a spec bridge to an insecure (http) frame from a secure (https) frame.`) + + done() + }) + + cy.visit('https://www.foobar.com:3502/fixtures/primary-origin.html') + cy.origin('http://www.foobar.com:3500', () => {}) + }) + }) +}) diff --git a/packages/driver/src/cy/commands/origin/index.ts b/packages/driver/src/cy/commands/origin/index.ts index b1227e565ae7..5cc24e6ffd73 100644 --- a/packages/driver/src/cy/commands/origin/index.ts +++ b/packages/driver/src/cy/commands/origin/index.ts @@ -38,7 +38,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State communicator.userInvocationStack = userInvocationStack // this command runs for as long as the commands in the secondary - // origin run, so it can't have its own timeout + // origin run, so it can't have its own timeout except in the case where we're creating the spec bridge. cy.clearTimeout() if (!config('experimentalSessionAndOrigin')) { @@ -47,6 +47,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State let options let callbackFn + const timeout = Cypress.config('defaultCommandTimeout') if (fn) { callbackFn = fn @@ -64,7 +65,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State name: 'origin', type: 'parent', message: urlOrDomain, - timeout: 0, + timeout, // @ts-ignore TODO: revisit once log-grouping has more implementations }, (_log) => { log = _log @@ -84,7 +85,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State const url = new URL(normalizeOrigin(urlOrDomain)).toString() const location = $Location.create(url) - validator.validateLocation(location, urlOrDomain) + validator.validateLocation(location, urlOrDomain, window.location.href) const origin = location.origin @@ -92,7 +93,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State cy.state('currentActiveOrigin', origin) return new Bluebird((resolve, reject, onCancel) => { - const cleanup = ({ readyForOriginFailed }: {readyForOriginFailed?: boolean} = {}): void => { + const cleanup = (): void => { cy.state('currentActiveOrigin', undefined) communicator.off('queue:finished', onQueueFinished) @@ -108,8 +109,10 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State resolve(unserializableSubjectType ? createUnserializableSubjectProxy(unserializableSubjectType) : subject) } - const _reject = (err, cleanupOptions: {readyForOriginFailed?: boolean} = {}) => { - cleanup(cleanupOptions) + const _reject = (err) => { + // Prevent cypress from trying to add the function to the error log + err.onFail = () => {} + cleanup() log?.error(err) reject(err) } @@ -141,9 +144,6 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State wrappedErr.name = err.name wrappedErr.stack = $stackUtils.replacedStack(wrappedErr, err.stack) - // Prevent cypress from trying to add the function to the error log - wrappedErr.onFail = () => {} - return _reject(wrappedErr) } @@ -168,9 +168,15 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State }) } + // If the spec bridge isn't created in time, it likely failed and we shouldn't hang the test. + const timeoutId = setTimeout(() => { + _reject($errUtils.errByPath('origin.failed_to_create_spec_bridge')) + }, timeout) + // fired once the spec bridge is set up and ready to receive messages communicator.once('bridge:ready', async (_data, { origin: specBridgeOrigin }) => { if (specBridgeOrigin === origin) { + clearTimeout(timeoutId) // now that the spec bridge is ready, instantiate Cypress with the current app config and environment variables for initial sync when creating the instance communicator.toSpecBridge(origin, 'initialize:cypress', { config: preprocessConfig(Cypress.config()), @@ -229,11 +235,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State // It tries to add a bunch of stuff that's not useful and ends up // messing up the stack that we want on the error wrappedErr.__stackCleaned__ = true - - // Prevent cypress from trying to add the function to the error log - wrappedErr.onFail = () => {} - - _reject(wrappedErr, { readyForOriginFailed: true }) + _reject(wrappedErr) } } }) diff --git a/packages/driver/src/cy/commands/origin/validator.ts b/packages/driver/src/cy/commands/origin/validator.ts index eb9d1ee6a0d7..eead76728ff7 100644 --- a/packages/driver/src/cy/commands/origin/validator.ts +++ b/packages/driver/src/cy/commands/origin/validator.ts @@ -1,6 +1,8 @@ import $utils from '../../../cypress/utils' import $errUtils from '../../../cypress/error_utils' import { difference, isPlainObject, isString } from 'lodash' +import type { LocationObject } from '../../../cypress/location' +import * as cors from '@packages/network/lib/cors' const validOptionKeys = Object.freeze(['args']) @@ -67,13 +69,35 @@ export class Validator { return false } - validateLocation (location, urlOrDomain) { + /** + * Validates the location parameter of the cy.origin call. + * @param originLocation - the location passed into the cy.origin command. + * @param urlOrDomain - the original string param passed in. + * @param specHref - the address of the current spec. + */ + validateLocation (originLocation: LocationObject, urlOrDomain: string, specHref: string): void { // we don't support query params - if (location.search.length > 0) { + if (originLocation.search.length > 0) { $errUtils.throwErrByPath('origin.invalid_url_argument', { onFail: this.log, args: { arg: $utils.stringify(urlOrDomain) }, }) } + + // Users would be better off not using cy.origin if the origin is part of the same super domain. + if (cors.urlMatchesPolicyBasedOnDomain(originLocation.href, specHref)) { + // this._isSameSuperDomainOriginWithExceptions({ originLocation, specLocation })) { + + const policy = cors.policyForDomain(originLocation.href) + + $errUtils.throwErrByPath('origin.invalid_url_argument_same_origin', { + onFail: this.log, + args: { + originUrl: $utils.stringify(urlOrDomain), + topOrigin: (window.location.origin), + policy, + }, + }) + } } } diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 005e1f77b25f..19b8b1b2ccfb 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1191,6 +1191,13 @@ export default { invalid_url_argument: { message: `${cmd('origin')} requires the first argument to be either a url (\`https://www.example.com/path\`) or a domain name (\`example.com\`). Query parameters are not allowed. You passed: \`{{arg}}\``, }, + invalid_url_argument_same_origin ({ originUrl, topOrigin, policy }) { + return stripIndent`\ + ${cmd('origin')} requires the first argument to be a different ${policy === 'same-origin' ? 'origin' : 'domain' } than top. You passed \`${originUrl}\` to the origin command, while top is at \`${topOrigin}\`. + + Either the intended page was not visited prior to running the cy.origin block or the cy.origin block may not be needed at all. + ` + }, invalid_options_argument: { message: `${cmd('origin')} requires the 'options' argument to be an object. You passed: \`{{arg}}\``, }, @@ -1260,6 +1267,13 @@ export default { message: stripIndent`\ ${cmd('origin')} could not serialize the thrown value. Please make sure the value being thrown is supported by the structured clone algorithm.`, }, + failed_to_create_spec_bridge: { + message: stripIndent`\ + ${cmd('origin')} failed to create a spec bridge to communicate with the specified origin. This can happen when you attempt to create a spec bridge to an insecure (http) frame from a secure (https) frame. + + Check your Developer Tools Console for the actual error - it should be printed there. + `, + }, unsupported: { route: { message: `${cmd('route')} has been deprecated and its use is not supported in the ${cmd('origin')} callback. Consider using ${cmd('intercept')} (outside of the callback) instead.`, diff --git a/packages/network/lib/cors.ts b/packages/network/lib/cors.ts index 2e4eab92f248..a88c9e0af734 100644 --- a/packages/network/lib/cors.ts +++ b/packages/network/lib/cors.ts @@ -11,6 +11,8 @@ 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) @@ -81,30 +83,30 @@ export function getDomainNameFromParsedHost (parsedHost: ParsedHost) { * 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} url - the url being compared - * @param {ParsedHostWithProtocolAndHost} props - the props being compared against the url + * @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, url, props }: { +function urlMatchesPolicyProps ({ policy, frameUrl, topProps }: { policy: Policy - url: string - props: ParsedHostWithProtocolAndHost + frameUrl: string + topProps: ParsedHostWithProtocolAndHost }): boolean { - if (!policy || !url || !props) { + if (!policy || !frameUrl || !topProps) { return false } - const urlProps = parseUrlIntoHostProtocolDomainTldPort(url) + 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, props) + 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 } = props + const { port: port2, subdomain: _unused2, ...relevantProps } = topProps let doPortsPassSameSchemeCheck: boolean @@ -124,67 +126,81 @@ function urlMatchesPolicyProps ({ policy, url, props }: { } } -function urlMatchesPolicy ({ policy, url1, url2 }: { +function urlMatchesPolicy ({ policy, frameUrl, topUrl }: { policy: Policy - url1: string - url2: string + frameUrl: string + topUrl: string }): boolean { - if (!policy || !url1 || !url2) { + if (!policy || !frameUrl || !topUrl) { return false } return urlMatchesPolicyProps({ policy, - url: url1, - props: parseUrlIntoHostProtocolDomainTldPort(url2), + frameUrl, + topProps: parseUrlIntoHostProtocolDomainTldPort(topUrl), }) } -export function urlMatchesOriginProps (url, props) { - return urlMatchesPolicyProps({ +export function urlOriginsMatch (frameUrl: string, topUrl: string): boolean { + return urlMatchesPolicy({ policy: 'same-origin', - url, - props, - }) -} - -export function urlMatchesSuperDomainOriginProps (url, props) { - return urlMatchesPolicyProps({ - policy: 'same-super-domain-origin', - url, - props, + frameUrl, + topUrl, }) } -export function urlMatchesSameSiteProps (url: string, props: ParsedHostWithProtocolAndHost) { - return urlMatchesPolicyProps({ +export const urlSameSiteMatch = (frameUrl: string, topUrl: string): boolean => { + return urlMatchesPolicy({ policy: 'schemeful-same-site', - url, - props, + frameUrl, + topUrl, }) } -export function urlOriginsMatch (url1: string, url2: string) { - return urlMatchesPolicy({ - policy: 'same-origin', - url1, - url2, - }) +/** + * 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' } -export function urlsSuperDomainOriginMatch (url1: string, url2: string) { +/** + * 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: 'same-super-domain-origin', - url1, - url2, + policy: policyForDomain(frameUrl), + frameUrl, + topUrl, }) } -export const urlSameSiteMatch = (url1: string, url2: string) => { - return urlMatchesPolicy({ - policy: 'schemeful-same-site', - url1, - url2, +/** + * 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, }) } diff --git a/packages/network/test/unit/cors_spec.ts b/packages/network/test/unit/cors_spec.ts index 8ed22d55cca3..348e65ddd2f3 100644 --- a/packages/network/test/unit/cors_spec.ts +++ b/packages/network/test/unit/cors_spec.ts @@ -109,357 +109,270 @@ describe('lib/cors', () => { }) }) - context('.urlMatchesOriginProps', () => { - const assertOriginsDoNotMatch = (url, props) => { - expect(cors.urlMatchesOriginProps(url, props)).to.be.false + context('.urlOriginsMatch', () => { + const assertOriginsDoNotMatch = (url1, url2) => { + expect(cors.urlOriginsMatch(url1, url2)).to.be.false } - const assertOriginsDoMatch = (url, props) => { - expect(cors.urlMatchesOriginProps(url, props)).to.be.true + const assertOriginsDoMatch = (url1, url2) => { + expect(cors.urlOriginsMatch(url1, url2)).to.be.true } describe('domain + subdomain', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('https://staging.google.com') + const url = 'https://staging.google.com' it('does not match', function () { - assertOriginsDoNotMatch('https://foo.bar:443', props) - assertOriginsDoNotMatch('http://foo.bar:80', props) - assertOriginsDoNotMatch('http://foo.bar', props) - assertOriginsDoNotMatch('http://staging.google.com', props) - assertOriginsDoNotMatch('http://staging.google.com:80', props) - assertOriginsDoNotMatch('https://staging.google2.com:443', props) - assertOriginsDoNotMatch('https://staging.google.net:443', props) - assertOriginsDoNotMatch('https://google.net:443', props) - assertOriginsDoNotMatch('http://google.com', props) - assertOriginsDoNotMatch('https://google.com:443', props) - assertOriginsDoNotMatch('https://foo.google.com:443', props) - assertOriginsDoNotMatch('https://foo.bar.google.com:443', props) + assertOriginsDoNotMatch('https://foo.bar:443', url) + assertOriginsDoNotMatch('http://foo.bar:80', url) + assertOriginsDoNotMatch('http://foo.bar', url) + assertOriginsDoNotMatch('http://staging.google.com', url) + assertOriginsDoNotMatch('http://staging.google.com:80', url) + assertOriginsDoNotMatch('https://staging.google2.com:443', url) + assertOriginsDoNotMatch('https://staging.google.net:443', url) + assertOriginsDoNotMatch('https://google.net:443', url) + assertOriginsDoNotMatch('http://google.com', url) + assertOriginsDoNotMatch('https://google.com:443', url) + assertOriginsDoNotMatch('https://foo.google.com:443', url) + assertOriginsDoNotMatch('https://foo.bar.google.com:443', url) }) it('matches', function () { - assertOriginsDoMatch('https://staging.google.com:443', props) + assertOriginsDoMatch('https://staging.google.com:443', url) }) }) describe('public suffix', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('https://example.gitlab.io') + const url = 'https://example.gitlab.io' it('does not match', function () { - assertOriginsDoNotMatch('http://example.gitlab.io', props) - assertOriginsDoNotMatch('https://foo.gitlab.io:443', props) - assertOriginsDoNotMatch('https://foo.example.gitlab.io:443', props) + assertOriginsDoNotMatch('http://example.gitlab.io', url) + assertOriginsDoNotMatch('https://foo.gitlab.io:443', url) + assertOriginsDoNotMatch('https://foo.example.gitlab.io:443', url) }) it('matches', function () { - assertOriginsDoMatch('https://example.gitlab.io:443', props) + assertOriginsDoMatch('https://example.gitlab.io:443', url) }) }) describe('localhost', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://localhost:4200') + const url = 'http://localhost:4200' it('does not match', function () { - assertOriginsDoNotMatch('http://localhost:4201', props) - assertOriginsDoNotMatch('http://localhoss:4200', props) + assertOriginsDoNotMatch('http://localhoss:4200', url) + assertOriginsDoNotMatch('http://localhost:4201', url) }) it('matches', function () { - assertOriginsDoMatch('http://localhost:4200', props) + assertOriginsDoMatch('http://localhost:4200', url) }) }) describe('app.localhost', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://app.localhost:4200') + const url = 'http://app.localhost:4200' it('does not match', function () { - assertOriginsDoNotMatch('http://app.localhost:4201', props) - assertOriginsDoNotMatch('http://app.localhoss:4200', props) - assertOriginsDoNotMatch('http://name.app.localhost:4200', props) + assertOriginsDoNotMatch('http://app.localhoss:4200', url) + assertOriginsDoNotMatch('http://app.localhost:4201', url) + assertOriginsDoNotMatch('http://name.app.localhost:4200', url) }) it('matches', function () { - assertOriginsDoMatch('http://app.localhost:4200', props) + assertOriginsDoMatch('http://app.localhost:4200', url) }) }) describe('local', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://brian.dev.local') + const url = 'http://brian.dev.local' it('does not match', function () { - assertOriginsDoNotMatch('https://brian.dev.local:443', props) - assertOriginsDoNotMatch('https://brian.dev.local', props) - assertOriginsDoNotMatch('http://brian.dev2.local:81', props) - assertOriginsDoNotMatch('http://jennifer.dev.local:80', props) - assertOriginsDoNotMatch('http://jennifer.dev.local', props) - }) - - it('matches', function () { - assertOriginsDoMatch('http://brian.dev.local:80', props) + assertOriginsDoNotMatch('https://brian.dev.local:443', url) + assertOriginsDoNotMatch('https://brian.dev.local', url) + assertOriginsDoNotMatch('http://brian.dev2.local:81', url) + assertOriginsDoNotMatch('http://jennifer.dev.local:4201', url) + assertOriginsDoNotMatch('http://jennifer.dev.local:80', url) + assertOriginsDoNotMatch('http://jennifer.dev.local', url) }) }) describe('ip address', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://192.168.5.10') + const url = 'http://192.168.5.10' it('does not match', function () { - assertOriginsDoNotMatch('http://192.168.5.10:443', props) - assertOriginsDoNotMatch('https://192.168.5.10', props) - assertOriginsDoNotMatch('http://193.168.5.10', props) - assertOriginsDoNotMatch('http://193.168.5.10:80', props) + assertOriginsDoNotMatch('http://192.168.5.10:443', url) + assertOriginsDoNotMatch('https://192.168.5.10', url) + assertOriginsDoNotMatch('http://193.168.5.10', url) + assertOriginsDoNotMatch('http://193.168.5.10:80', url) + assertOriginsDoNotMatch('http://192.168.5.10:12345', url) }) it('matches', function () { - assertOriginsDoMatch('http://192.168.5.10', props) - assertOriginsDoMatch('http://192.168.5.10:80', props) + assertOriginsDoMatch('http://192.168.5.10', url) + assertOriginsDoMatch('http://192.168.5.10:80', url) }) }) }) - context('.urlMatchesSuperDomainOriginProps', () => { - const assertSuperDomainOriginDoesNotMatch = (url, props) => { - expect(cors.urlMatchesSuperDomainOriginProps(url, props)).to.be.false + context('.urlSameSiteMatch', () => { + const assertsUrlsAreNotSameSite = (url1, url2) => { + expect(cors.urlSameSiteMatch(url1, url2)).to.be.false } - const assertSuperDomainOriginDoesMatch = (url, props) => { - expect(cors.urlMatchesSuperDomainOriginProps(url, props)).to.be.true + const assertsUrlsAreSameSite = (url1, url2) => { + expect(cors.urlSameSiteMatch(url1, url2)).to.be.true } describe('domain + subdomain', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('https://staging.google.com') + const url = 'https://staging.google.com' it('does not match', function () { - assertSuperDomainOriginDoesNotMatch('https://foo.bar:443', props) - assertSuperDomainOriginDoesNotMatch('http://foo.bar:80', props) - assertSuperDomainOriginDoesNotMatch('http://foo.bar', props) - assertSuperDomainOriginDoesNotMatch('http://staging.google.com', props) - assertSuperDomainOriginDoesNotMatch('http://staging.google.com:80', props) - assertSuperDomainOriginDoesNotMatch('https://staging.google2.com:443', props) - assertSuperDomainOriginDoesNotMatch('https://staging.google.net:443', props) - assertSuperDomainOriginDoesNotMatch('https://google.net:443', props) - assertSuperDomainOriginDoesNotMatch('http://google.com', props) + assertsUrlsAreNotSameSite('https://foo.bar:443', url) + assertsUrlsAreNotSameSite('http://foo.bar:80', url) + assertsUrlsAreNotSameSite('http://foo.bar', url) + assertsUrlsAreNotSameSite('http://staging.google.com', url) + assertsUrlsAreNotSameSite('http://staging.google.com:80', url) + assertsUrlsAreNotSameSite('https://staging.google2.com:443', url) + assertsUrlsAreNotSameSite('https://staging.google.net:443', url) + assertsUrlsAreNotSameSite('https://google.net:443', url) + assertsUrlsAreNotSameSite('http://google.com', url) }) it('matches', function () { - assertSuperDomainOriginDoesMatch('https://staging.google.com:443', props) - assertSuperDomainOriginDoesMatch('https://google.com:443', props) - assertSuperDomainOriginDoesMatch('https://foo.google.com:443', props) - assertSuperDomainOriginDoesMatch('https://foo.bar.google.com:443', props) + assertsUrlsAreSameSite('https://staging.google.com:443', url) + assertsUrlsAreSameSite('https://google.com:443', url) + assertsUrlsAreSameSite('https://foo.google.com:443', url) + assertsUrlsAreSameSite('https://foo.bar.google.com:443', url) }) }) describe('public suffix', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('https://example.gitlab.io') + const url = 'https://example.gitlab.io' it('does not match', function () { - assertSuperDomainOriginDoesNotMatch('http://example.gitlab.io', props) - assertSuperDomainOriginDoesNotMatch('https://foo.gitlab.io:443', props) + assertsUrlsAreNotSameSite('http://example.gitlab.io', url) + assertsUrlsAreNotSameSite('https://foo.gitlab.io:443', url) }) it('matches', function () { - assertSuperDomainOriginDoesMatch('https://example.gitlab.io:443', props) - assertSuperDomainOriginDoesMatch('https://foo.example.gitlab.io:443', props) + assertsUrlsAreSameSite('https://example.gitlab.io:443', url) + assertsUrlsAreSameSite('https://foo.example.gitlab.io:443', url) }) }) describe('localhost', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://localhost:4200') + const url = 'http://localhost:4200' it('does not match', function () { - assertSuperDomainOriginDoesNotMatch('http://localhoss:4200', props) - assertSuperDomainOriginDoesNotMatch('http://localhost:4201', props) + assertsUrlsAreNotSameSite('http://localhoss:4200', url) }) it('matches', function () { - assertSuperDomainOriginDoesMatch('http://localhost:4200', props) + assertsUrlsAreSameSite('http://localhost:4200', url) + assertsUrlsAreSameSite('http://localhost:4201', url) }) }) describe('app.localhost', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://app.localhost:4200') + const url = 'http://app.localhost:4200' it('does not match', function () { - assertSuperDomainOriginDoesNotMatch('http://app.localhoss:4200', props) - assertSuperDomainOriginDoesNotMatch('http://app.localhost:4201', props) + assertsUrlsAreNotSameSite('http://app.localhoss:4200', url) }) it('matches', function () { - assertSuperDomainOriginDoesMatch('http://app.localhost:4200', props) - assertSuperDomainOriginDoesMatch('http://name.app.localhost:4200', props) + assertsUrlsAreSameSite('http://app.localhost:4200', url) + assertsUrlsAreSameSite('http://name.app.localhost:4200', url) + assertsUrlsAreSameSite('http://app.localhost:4201', url) }) }) describe('local', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://brian.dev.local') + const url = 'http://brian.dev.local' it('does not match', function () { - assertSuperDomainOriginDoesNotMatch('https://brian.dev.local:443', props) - assertSuperDomainOriginDoesNotMatch('https://brian.dev.local', props) - assertSuperDomainOriginDoesNotMatch('http://brian.dev2.local:81', props) - assertSuperDomainOriginDoesNotMatch('http://brian.dev.local:8081', props) + assertsUrlsAreNotSameSite('https://brian.dev.local:443', url) + assertsUrlsAreNotSameSite('https://brian.dev.local', url) + assertsUrlsAreNotSameSite('http://brian.dev2.local:81', url) }) it('matches', function () { - assertSuperDomainOriginDoesMatch('http://brian.dev.local:80', props) - assertSuperDomainOriginDoesMatch('http://jennifer.dev.local:80', props) - assertSuperDomainOriginDoesMatch('http://jennifer.dev.local', props) + assertsUrlsAreSameSite('http://jennifer.dev.local:4201', url) + assertsUrlsAreSameSite('http://jennifer.dev.local:80', url) + assertsUrlsAreSameSite('http://jennifer.dev.local', url) + assertsUrlsAreSameSite('http://brian.dev.local:8081', url) }) }) describe('ip address', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://192.168.5.10') + const url = 'http://192.168.5.10' it('does not match', function () { - assertSuperDomainOriginDoesNotMatch('http://192.168.5.10:443', props) - assertSuperDomainOriginDoesNotMatch('https://192.168.5.10', props) - assertSuperDomainOriginDoesNotMatch('http://193.168.5.10', props) - assertSuperDomainOriginDoesNotMatch('http://193.168.5.10:80', props) - assertSuperDomainOriginDoesNotMatch('http://192.168.5.10:8081', props) + assertsUrlsAreNotSameSite('http://192.168.5.10:443', url) + assertsUrlsAreNotSameSite('https://192.168.5.10', url) + assertsUrlsAreNotSameSite('http://193.168.5.10', url) + assertsUrlsAreNotSameSite('http://193.168.5.10:80', url) }) it('matches', function () { - assertSuperDomainOriginDoesMatch('http://192.168.5.10', props) - assertSuperDomainOriginDoesMatch('http://192.168.5.10:80', props) + assertsUrlsAreSameSite('http://192.168.5.10', url) + assertsUrlsAreSameSite('http://192.168.5.10:80', url) + assertsUrlsAreSameSite('http://192.168.5.10:12345', url) }) }) }) - context('.urlMatchesSameSiteProps', () => { - const assertSameSiteDoesNotMatch = (url, props) => { - expect(cors.urlMatchesSameSiteProps(url, props)).to.be.false + context('.urlMatchesPolicyBasedOnDomain', () => { + const assertsUrlsAreNotAPolicyMatch = (url1, url2) => { + expect(cors.urlMatchesPolicyBasedOnDomain(url1, url2)).to.be.false } - const assertSameSiteDoesMatch = (url, props) => { - expect(cors.urlMatchesSameSiteProps(url, props)).to.be.true + const assertsUrlsAreAPolicyOriginMatch = (url1, url2) => { + expect(cors.urlMatchesPolicyBasedOnDomain(url1, url2)).to.be.true } describe('domain + subdomain', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('https://staging.google.com') - - it('does not match', function () { - assertSameSiteDoesNotMatch('https://foo.bar:443', props) - assertSameSiteDoesNotMatch('http://foo.bar:80', props) - assertSameSiteDoesNotMatch('http://foo.bar', props) - assertSameSiteDoesNotMatch('http://staging.google.com', props) - assertSameSiteDoesNotMatch('http://staging.google.com:80', props) - assertSameSiteDoesNotMatch('https://staging.google2.com:443', props) - assertSameSiteDoesNotMatch('https://staging.google.net:443', props) - assertSameSiteDoesNotMatch('https://google.net:443', props) - assertSameSiteDoesNotMatch('http://google.com', props) - }) - - it('matches', function () { - assertSameSiteDoesMatch('https://staging.google.com:443', props) - assertSameSiteDoesMatch('https://google.com:443', props) - assertSameSiteDoesMatch('https://foo.google.com:443', props) - assertSameSiteDoesMatch('https://foo.bar.google.com:443', props) - }) - }) - - describe('public suffix', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('https://example.gitlab.io') - - it('does not match', function () { - assertSameSiteDoesNotMatch('http://example.gitlab.io', props) - assertSameSiteDoesNotMatch('https://foo.gitlab.io:443', props) - }) - - it('matches', function () { - assertSameSiteDoesMatch('https://example.gitlab.io:443', props) - assertSameSiteDoesMatch('https://foo.example.gitlab.io:443', props) - }) - }) - - describe('localhost', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://localhost:4200') - - it('does not match', function () { - assertSameSiteDoesNotMatch('http://localhoss:4200', props) - }) - - it('matches', function () { - assertSameSiteDoesMatch('http://localhost:4201', props) - assertSameSiteDoesMatch('http://localhost:4200', props) - }) - }) - - describe('app.localhost', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://app.localhost:4200') - - it('does not match', function () { - assertSameSiteDoesNotMatch('http://app.localhoss:4200', props) - }) - - it('matches', function () { - assertSameSiteDoesMatch('http://app.localhost:4200', props) - assertSameSiteDoesMatch('http://name.app.localhost:4200', props) - assertSameSiteDoesMatch('http://app.localhost:4201', props) - }) - }) - - describe('local', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://brian.dev.local') - - it('does not match', function () { - assertSameSiteDoesNotMatch('https://brian.dev.local:443', props) - assertSameSiteDoesNotMatch('https://brian.dev.local', props) - assertSameSiteDoesNotMatch('http://brian.dev2.local:81', props) - }) - - it('matches', function () { - assertSameSiteDoesMatch('http://brian.dev.local:80', props) - assertSameSiteDoesMatch('http://jennifer.dev.local:80', props) - assertSameSiteDoesMatch('http://jennifer.dev.local', props) - assertSameSiteDoesMatch('http://brian.dev.local:8081', props) - }) - }) - - describe('ip address', () => { - const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://192.168.5.10') + const url = 'https://staging.gurgle.com' it('does not match', function () { - assertSameSiteDoesNotMatch('http://192.168.5.10:443', props) - assertSameSiteDoesNotMatch('https://192.168.5.10', props) - assertSameSiteDoesNotMatch('http://193.168.5.10', props) - assertSameSiteDoesNotMatch('http://193.168.5.10:80', props) + assertsUrlsAreNotAPolicyMatch('https://foo.bar:443', url) + assertsUrlsAreNotAPolicyMatch('http://foo.bar:80', url) + assertsUrlsAreNotAPolicyMatch('http://foo.bar', url) + assertsUrlsAreNotAPolicyMatch('http://staging.gurgle.com', url) + assertsUrlsAreNotAPolicyMatch('http://staging.gurgle.com:80', url) + assertsUrlsAreNotAPolicyMatch('https://staging.gurgle2.com:443', url) + assertsUrlsAreNotAPolicyMatch('https://staging.gurgle.net:443', url) + assertsUrlsAreNotAPolicyMatch('https://gurgle.net:443', url) + assertsUrlsAreNotAPolicyMatch('http://gurgle.com', url) }) it('matches', function () { - assertSameSiteDoesMatch('http://192.168.5.10', props) - assertSameSiteDoesMatch('http://192.168.5.10:80', props) - assertSameSiteDoesMatch('http://192.168.5.10:8081', props) + assertsUrlsAreAPolicyOriginMatch('https://staging.gurgle.com:443', url) + assertsUrlsAreAPolicyOriginMatch('https://gurgle.com:443', url) + assertsUrlsAreAPolicyOriginMatch('https://foo.gurgle.com:443', url) + assertsUrlsAreAPolicyOriginMatch('https://foo.bar.gurgle.com:443', url) }) }) - }) - - context('.urlOriginsMatch', () => { - const assertOriginsDoNotMatch = (url1, url2) => { - expect(cors.urlOriginsMatch(url1, url2)).to.be.false - } - const assertOriginsDoMatch = (url1, url2) => { - expect(cors.urlOriginsMatch(url1, url2)).to.be.true - } - - describe('domain + subdomain', () => { + describe('google (strict same-origin policy)', () => { const url = 'https://staging.google.com' it('does not match', function () { - assertOriginsDoNotMatch('https://foo.bar:443', url) - assertOriginsDoNotMatch('http://foo.bar:80', url) - assertOriginsDoNotMatch('http://foo.bar', url) - assertOriginsDoNotMatch('http://staging.google.com', url) - assertOriginsDoNotMatch('http://staging.google.com:80', url) - assertOriginsDoNotMatch('https://staging.google2.com:443', url) - assertOriginsDoNotMatch('https://staging.google.net:443', url) - assertOriginsDoNotMatch('https://google.net:443', url) - assertOriginsDoNotMatch('http://google.com', url) - assertOriginsDoNotMatch('https://google.com:443', url) - assertOriginsDoNotMatch('https://foo.google.com:443', url) - assertOriginsDoNotMatch('https://foo.bar.google.com:443', url) + assertsUrlsAreNotAPolicyMatch('https://foo.bar:443', url) + assertsUrlsAreNotAPolicyMatch('http://foo.bar:80', url) + assertsUrlsAreNotAPolicyMatch('http://foo.bar', url) + assertsUrlsAreNotAPolicyMatch('http://staging.google.com', url) + assertsUrlsAreNotAPolicyMatch('http://staging.google.com:80', url) + assertsUrlsAreNotAPolicyMatch('https://staging.google2.com:443', url) + assertsUrlsAreNotAPolicyMatch('https://staging.google.net:443', url) + assertsUrlsAreNotAPolicyMatch('https://google.net:443', url) + assertsUrlsAreNotAPolicyMatch('http://google.com', url) + assertsUrlsAreNotAPolicyMatch('https://google.com:443', url) + assertsUrlsAreNotAPolicyMatch('https://foo.google.com:443', url) + assertsUrlsAreNotAPolicyMatch('https://foo.bar.google.com:443', url) }) it('matches', function () { - assertOriginsDoMatch('https://staging.google.com:443', url) + assertsUrlsAreAPolicyOriginMatch('https://staging.google.com:443', url) }) }) @@ -467,13 +380,13 @@ describe('lib/cors', () => { const url = 'https://example.gitlab.io' it('does not match', function () { - assertOriginsDoNotMatch('http://example.gitlab.io', url) - assertOriginsDoNotMatch('https://foo.gitlab.io:443', url) - assertOriginsDoNotMatch('https://foo.example.gitlab.io:443', url) + assertsUrlsAreNotAPolicyMatch('http://example.gitlab.io', url) + assertsUrlsAreNotAPolicyMatch('https://foo.gitlab.io:443', url) }) it('matches', function () { - assertOriginsDoMatch('https://example.gitlab.io:443', url) + assertsUrlsAreAPolicyOriginMatch('https://example.gitlab.io:443', url) + assertsUrlsAreAPolicyOriginMatch('https://foo.example.gitlab.io:443', url) }) }) @@ -481,12 +394,12 @@ describe('lib/cors', () => { const url = 'http://localhost:4200' it('does not match', function () { - assertOriginsDoNotMatch('http://localhoss:4200', url) - assertOriginsDoNotMatch('http://localhost:4201', url) + assertsUrlsAreNotAPolicyMatch('http://localhoss:4200', url) + assertsUrlsAreNotAPolicyMatch('http://localhost:4201', url) }) it('matches', function () { - assertOriginsDoMatch('http://localhost:4200', url) + assertsUrlsAreAPolicyOriginMatch('http://localhost:4200', url) }) }) @@ -494,13 +407,13 @@ describe('lib/cors', () => { const url = 'http://app.localhost:4200' it('does not match', function () { - assertOriginsDoNotMatch('http://app.localhoss:4200', url) - assertOriginsDoNotMatch('http://app.localhost:4201', url) - assertOriginsDoNotMatch('http://name.app.localhost:4200', url) + assertsUrlsAreNotAPolicyMatch('http://app.localhoss:4200', url) + assertsUrlsAreNotAPolicyMatch('http://app.localhost:4201', url) }) it('matches', function () { - assertOriginsDoMatch('http://app.localhost:4200', url) + assertsUrlsAreAPolicyOriginMatch('http://app.localhost:4200', url) + assertsUrlsAreAPolicyOriginMatch('http://name.app.localhost:4200', url) }) }) @@ -508,12 +421,16 @@ describe('lib/cors', () => { const url = 'http://brian.dev.local' it('does not match', function () { - assertOriginsDoNotMatch('https://brian.dev.local:443', url) - assertOriginsDoNotMatch('https://brian.dev.local', url) - assertOriginsDoNotMatch('http://brian.dev2.local:81', url) - assertOriginsDoNotMatch('http://jennifer.dev.local:4201', url) - assertOriginsDoNotMatch('http://jennifer.dev.local:80', url) - assertOriginsDoNotMatch('http://jennifer.dev.local', url) + assertsUrlsAreNotAPolicyMatch('https://brian.dev.local:443', url) + assertsUrlsAreNotAPolicyMatch('https://brian.dev.local', url) + assertsUrlsAreNotAPolicyMatch('http://brian.dev2.local:81', url) + assertsUrlsAreNotAPolicyMatch('http://brian.dev.local:8081', url) + }) + + it('matches', function () { + assertsUrlsAreAPolicyOriginMatch('http://jennifer.dev.local', url) + assertsUrlsAreAPolicyOriginMatch('http://jennifer.dev.local:80', url) + assertsUrlsAreAPolicyOriginMatch('http://jennifer.dev.local', url) }) }) @@ -521,232 +438,147 @@ describe('lib/cors', () => { const url = 'http://192.168.5.10' it('does not match', function () { - assertOriginsDoNotMatch('http://192.168.5.10:443', url) - assertOriginsDoNotMatch('https://192.168.5.10', url) - assertOriginsDoNotMatch('http://193.168.5.10', url) - assertOriginsDoNotMatch('http://193.168.5.10:80', url) - assertOriginsDoNotMatch('http://192.168.5.10:12345', url) + assertsUrlsAreNotAPolicyMatch('http://192.168.5.10:443', url) + assertsUrlsAreNotAPolicyMatch('https://192.168.5.10', url) + assertsUrlsAreNotAPolicyMatch('http://193.168.5.10', url) + assertsUrlsAreNotAPolicyMatch('http://193.168.5.10:80', url) + assertsUrlsAreNotAPolicyMatch('http://192.168.5.10:12345', url) }) it('matches', function () { - assertOriginsDoMatch('http://192.168.5.10', url) - assertOriginsDoMatch('http://192.168.5.10:80', url) + assertsUrlsAreAPolicyOriginMatch('http://192.168.5.10', url) + assertsUrlsAreAPolicyOriginMatch('http://192.168.5.10:80', url) }) }) }) - context('.urlsSuperDomainOriginMatch', () => { - const assertsUrlsAreNotASuperDomainOriginMatch = (url1, url2) => { - expect(cors.urlsSuperDomainOriginMatch(url1, url2)).to.be.false + context('.urlMatchesPolicyBasedOnDomainProps', () => { + const assertsUrlsAreNotAPolicyMatch = (url1, props) => { + expect(cors.urlMatchesPolicyBasedOnDomainProps(url1, props)).to.be.false } - const assertsUrlsAreASuperDomainOriginMatch = (url1, url2) => { - expect(cors.urlsSuperDomainOriginMatch(url1, url2)).to.be.true + const assertsUrlsAreAPolicyOriginMatch = (url1, props) => { + expect(cors.urlMatchesPolicyBasedOnDomainProps(url1, props)).to.be.true } describe('domain + subdomain', () => { - const url = 'https://staging.google.com' - - it('does not match', function () { - assertsUrlsAreNotASuperDomainOriginMatch('https://foo.bar:443', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://foo.bar:80', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://foo.bar', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://staging.google.com', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://staging.google.com:80', url) - assertsUrlsAreNotASuperDomainOriginMatch('https://staging.google2.com:443', url) - assertsUrlsAreNotASuperDomainOriginMatch('https://staging.google.net:443', url) - assertsUrlsAreNotASuperDomainOriginMatch('https://google.net:443', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://google.com', url) - }) - - it('matches', function () { - assertsUrlsAreASuperDomainOriginMatch('https://staging.google.com:443', url) - assertsUrlsAreASuperDomainOriginMatch('https://google.com:443', url) - assertsUrlsAreASuperDomainOriginMatch('https://foo.google.com:443', url) - assertsUrlsAreASuperDomainOriginMatch('https://foo.bar.google.com:443', url) - }) - }) - - describe('public suffix', () => { - const url = 'https://example.gitlab.io' - - it('does not match', function () { - assertsUrlsAreNotASuperDomainOriginMatch('http://example.gitlab.io', url) - assertsUrlsAreNotASuperDomainOriginMatch('https://foo.gitlab.io:443', url) - }) - - it('matches', function () { - assertsUrlsAreASuperDomainOriginMatch('https://example.gitlab.io:443', url) - assertsUrlsAreASuperDomainOriginMatch('https://foo.example.gitlab.io:443', url) - }) - }) - - describe('localhost', () => { - const url = 'http://localhost:4200' - - it('does not match', function () { - assertsUrlsAreNotASuperDomainOriginMatch('http://localhoss:4200', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://localhost:4201', url) - }) - - it('matches', function () { - assertsUrlsAreASuperDomainOriginMatch('http://localhost:4200', url) - }) - }) - - describe('app.localhost', () => { - const url = 'http://app.localhost:4200' + const props = cors.parseUrlIntoHostProtocolDomainTldPort('https://staging.gurgle.com') it('does not match', function () { - assertsUrlsAreNotASuperDomainOriginMatch('http://app.localhoss:4200', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://app.localhost:4201', url) + assertsUrlsAreNotAPolicyMatch('https://foo.bar:443', props) + assertsUrlsAreNotAPolicyMatch('http://foo.bar:80', props) + assertsUrlsAreNotAPolicyMatch('http://foo.bar', props) + assertsUrlsAreNotAPolicyMatch('http://staging.gurgle.com', props) + assertsUrlsAreNotAPolicyMatch('http://staging.gurgle.com:80', props) + assertsUrlsAreNotAPolicyMatch('https://staging.gurgle2.com:443', props) + assertsUrlsAreNotAPolicyMatch('https://staging.gurgle.net:443', props) + assertsUrlsAreNotAPolicyMatch('https://gurgle.net:443', props) + assertsUrlsAreNotAPolicyMatch('http://gurgle.com', props) }) it('matches', function () { - assertsUrlsAreASuperDomainOriginMatch('http://app.localhost:4200', url) - assertsUrlsAreASuperDomainOriginMatch('http://name.app.localhost:4200', url) + assertsUrlsAreAPolicyOriginMatch('https://staging.gurgle.com:443', props) + assertsUrlsAreAPolicyOriginMatch('https://gurgle.com:443', props) + assertsUrlsAreAPolicyOriginMatch('https://foo.gurgle.com:443', props) + assertsUrlsAreAPolicyOriginMatch('https://foo.bar.gurgle.com:443', props) }) }) - describe('local', () => { - const url = 'http://brian.dev.local' - - it('does not match', function () { - assertsUrlsAreNotASuperDomainOriginMatch('https://brian.dev.local:443', url) - assertsUrlsAreNotASuperDomainOriginMatch('https://brian.dev.local', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://brian.dev2.local:81', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://brian.dev.local:8081', url) - }) - - it('matches', function () { - assertsUrlsAreASuperDomainOriginMatch('http://jennifer.dev.local', url) - assertsUrlsAreASuperDomainOriginMatch('http://jennifer.dev.local:80', url) - assertsUrlsAreASuperDomainOriginMatch('http://jennifer.dev.local', url) - }) - }) - - describe('ip address', () => { - const url = 'http://192.168.5.10' + describe('google (strict same-origin policy)', () => { + const props = cors.parseUrlIntoHostProtocolDomainTldPort('https://staging.google.com') it('does not match', function () { - assertsUrlsAreNotASuperDomainOriginMatch('http://192.168.5.10:443', url) - assertsUrlsAreNotASuperDomainOriginMatch('https://192.168.5.10', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://193.168.5.10', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://193.168.5.10:80', url) - assertsUrlsAreNotASuperDomainOriginMatch('http://192.168.5.10:12345', url) + assertsUrlsAreNotAPolicyMatch('https://foo.bar:443', props) + assertsUrlsAreNotAPolicyMatch('http://foo.bar:80', props) + assertsUrlsAreNotAPolicyMatch('http://foo.bar', props) + assertsUrlsAreNotAPolicyMatch('http://staging.google.com', props) + assertsUrlsAreNotAPolicyMatch('http://staging.google.com:80', props) + assertsUrlsAreNotAPolicyMatch('https://staging.google2.com:443', props) + assertsUrlsAreNotAPolicyMatch('https://staging.google.net:443', props) + assertsUrlsAreNotAPolicyMatch('https://google.net:443', props) + assertsUrlsAreNotAPolicyMatch('http://google.com', props) + assertsUrlsAreNotAPolicyMatch('https://google.com:443', props) + assertsUrlsAreNotAPolicyMatch('https://foo.google.com:443', props) + assertsUrlsAreNotAPolicyMatch('https://foo.bar.google.com:443', props) }) it('matches', function () { - assertsUrlsAreASuperDomainOriginMatch('http://192.168.5.10', url) - assertsUrlsAreASuperDomainOriginMatch('http://192.168.5.10:80', url) - }) - }) - }) - - context('.urlSameSiteMatch', () => { - const assertsUrlsAreNotSameSite = (url1, url2) => { - expect(cors.urlSameSiteMatch(url1, url2)).to.be.false - } - - const assertsUrlsAreSameSite = (url1, url2) => { - expect(cors.urlSameSiteMatch(url1, url2)).to.be.true - } - - describe('domain + subdomain', () => { - const url = 'https://staging.google.com' - - it('does not match', function () { - assertsUrlsAreNotSameSite('https://foo.bar:443', url) - assertsUrlsAreNotSameSite('http://foo.bar:80', url) - assertsUrlsAreNotSameSite('http://foo.bar', url) - assertsUrlsAreNotSameSite('http://staging.google.com', url) - assertsUrlsAreNotSameSite('http://staging.google.com:80', url) - assertsUrlsAreNotSameSite('https://staging.google2.com:443', url) - assertsUrlsAreNotSameSite('https://staging.google.net:443', url) - assertsUrlsAreNotSameSite('https://google.net:443', url) - assertsUrlsAreNotSameSite('http://google.com', url) - }) - - it('matches', function () { - assertsUrlsAreSameSite('https://staging.google.com:443', url) - assertsUrlsAreSameSite('https://google.com:443', url) - assertsUrlsAreSameSite('https://foo.google.com:443', url) - assertsUrlsAreSameSite('https://foo.bar.google.com:443', url) + assertsUrlsAreAPolicyOriginMatch('https://staging.google.com:443', props) }) }) describe('public suffix', () => { - const url = 'https://example.gitlab.io' + const props = cors.parseUrlIntoHostProtocolDomainTldPort('https://example.gitlab.io') it('does not match', function () { - assertsUrlsAreNotSameSite('http://example.gitlab.io', url) - assertsUrlsAreNotSameSite('https://foo.gitlab.io:443', url) + assertsUrlsAreNotAPolicyMatch('http://example.gitlab.io', props) + assertsUrlsAreNotAPolicyMatch('https://foo.gitlab.io:443', props) }) it('matches', function () { - assertsUrlsAreSameSite('https://example.gitlab.io:443', url) - assertsUrlsAreSameSite('https://foo.example.gitlab.io:443', url) + assertsUrlsAreAPolicyOriginMatch('https://example.gitlab.io:443', props) + assertsUrlsAreAPolicyOriginMatch('https://foo.example.gitlab.io:443', props) }) }) describe('localhost', () => { - const url = 'http://localhost:4200' + const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://localhost:4200') it('does not match', function () { - assertsUrlsAreNotSameSite('http://localhoss:4200', url) + assertsUrlsAreNotAPolicyMatch('http://localhoss:4200', props) + assertsUrlsAreNotAPolicyMatch('http://localhost:4201', props) }) it('matches', function () { - assertsUrlsAreSameSite('http://localhost:4200', url) - assertsUrlsAreSameSite('http://localhost:4201', url) + assertsUrlsAreAPolicyOriginMatch('http://localhost:4200', props) }) }) describe('app.localhost', () => { - const url = 'http://app.localhost:4200' + const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://app.localhost:4200') it('does not match', function () { - assertsUrlsAreNotSameSite('http://app.localhoss:4200', url) + assertsUrlsAreNotAPolicyMatch('http://app.localhoss:4200', props) + assertsUrlsAreNotAPolicyMatch('http://app.localhost:4201', props) }) it('matches', function () { - assertsUrlsAreSameSite('http://app.localhost:4200', url) - assertsUrlsAreSameSite('http://name.app.localhost:4200', url) - assertsUrlsAreSameSite('http://app.localhost:4201', url) + assertsUrlsAreAPolicyOriginMatch('http://app.localhost:4200', props) + assertsUrlsAreAPolicyOriginMatch('http://name.app.localhost:4200', props) }) }) describe('local', () => { - const url = 'http://brian.dev.local' + const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://brian.dev.local') it('does not match', function () { - assertsUrlsAreNotSameSite('https://brian.dev.local:443', url) - assertsUrlsAreNotSameSite('https://brian.dev.local', url) - assertsUrlsAreNotSameSite('http://brian.dev2.local:81', url) + assertsUrlsAreNotAPolicyMatch('https://brian.dev.local:443', props) + assertsUrlsAreNotAPolicyMatch('https://brian.dev.local', props) + assertsUrlsAreNotAPolicyMatch('http://brian.dev2.local:81', props) + assertsUrlsAreNotAPolicyMatch('http://brian.dev.local:8081', props) }) it('matches', function () { - assertsUrlsAreSameSite('http://jennifer.dev.local:4201', url) - assertsUrlsAreSameSite('http://jennifer.dev.local:80', url) - assertsUrlsAreSameSite('http://jennifer.dev.local', url) - assertsUrlsAreSameSite('http://brian.dev.local:8081', url) + assertsUrlsAreAPolicyOriginMatch('http://jennifer.dev.local', props) + assertsUrlsAreAPolicyOriginMatch('http://jennifer.dev.local:80', props) + assertsUrlsAreAPolicyOriginMatch('http://jennifer.dev.local', props) }) }) describe('ip address', () => { - const url = 'http://192.168.5.10' + const props = cors.parseUrlIntoHostProtocolDomainTldPort('http://192.168.5.10') it('does not match', function () { - assertsUrlsAreNotSameSite('http://192.168.5.10:443', url) - assertsUrlsAreNotSameSite('https://192.168.5.10', url) - assertsUrlsAreNotSameSite('http://193.168.5.10', url) - assertsUrlsAreNotSameSite('http://193.168.5.10:80', url) + assertsUrlsAreNotAPolicyMatch('http://192.168.5.10:443', props) + assertsUrlsAreNotAPolicyMatch('https://192.168.5.10', props) + assertsUrlsAreNotAPolicyMatch('http://193.168.5.10', props) + assertsUrlsAreNotAPolicyMatch('http://193.168.5.10:80', props) + assertsUrlsAreNotAPolicyMatch('http://192.168.5.10:12345', props) }) it('matches', function () { - assertsUrlsAreSameSite('http://192.168.5.10', url) - assertsUrlsAreSameSite('http://192.168.5.10:80', url) - assertsUrlsAreSameSite('http://192.168.5.10:12345', url) + assertsUrlsAreAPolicyOriginMatch('http://192.168.5.10', props) + assertsUrlsAreAPolicyOriginMatch('http://192.168.5.10:80', props) }) }) }) diff --git a/packages/proxy/lib/http/request-middleware.ts b/packages/proxy/lib/http/request-middleware.ts index 63bff1b259af..e2b41509e2ae 100644 --- a/packages/proxy/lib/http/request-middleware.ts +++ b/packages/proxy/lib/http/request-middleware.ts @@ -159,7 +159,7 @@ const MaybeEndRequestWithBufferedResponse: RequestMiddleware = function () { this.debug('ending request with buffered response') // NOTE: Only inject fullCrossOrigin here if experimental is on and // the super domain origins do not match in order to keep parity with cypress application reloads - this.res.wantsInjection = this.config.experimentalSessionAndOrigin && buffer.isCrossSuperDomainOrigin ? 'fullCrossOrigin' : 'full' + this.res.wantsInjection = this.config.experimentalSessionAndOrigin && buffer.urlDoesNotMatchPolicyBasedOnDomain ? 'fullCrossOrigin' : 'full' return this.onResponse(buffer.response, buffer.stream) } diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 6e3029171a86..74de86a93166 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -55,9 +55,9 @@ function getNodeCharsetFromResponse (headers: IncomingHttpHeaders, body: Buffer, return 'latin1' } -function reqMatchesSuperDomainOrigin (req: CypressIncomingRequest, remoteState) { +function reqMatchesPolicyBasedOnDomain (req: CypressIncomingRequest, remoteState) { if (remoteState.strategy === 'http') { - return cors.urlMatchesSuperDomainOriginProps(req.proxiedUrl, remoteState.props) + return cors.urlMatchesPolicyBasedOnDomainProps(req.proxiedUrl, remoteState.props) } if (remoteState.strategy === 'file') { @@ -250,7 +250,7 @@ const SetInjectionLevel: ResponseMiddleware = function () { this.debug('determine injection') - const isReqMatchSuperDomainOrigin = reqMatchesSuperDomainOrigin(this.req, this.remoteStates.current()) + const isReqMatchSuperDomainOrigin = reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.current()) const getInjectionLevel = () => { if (this.incomingRes.headers['x-cypress-file-server-error'] && !this.res.isInitial) { this.debug('- partial injection (x-cypress-file-server-error)') @@ -259,11 +259,11 @@ const SetInjectionLevel: ResponseMiddleware = function () { } // NOTE: Only inject fullCrossOrigin if the super domain origins do not match in order to keep parity with cypress application reloads - const isCrossSuperDomainOrigin = !reqMatchesSuperDomainOrigin(this.req, this.remoteStates.getPrimary()) + const urlDoesNotMatchPolicyBasedOnDomain = !reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.getPrimary()) const isAUTFrame = this.req.isAUTFrame const isHTMLLike = isHTML || isRenderedHTML - if (this.config.experimentalSessionAndOrigin && isCrossSuperDomainOrigin && isAUTFrame && isHTMLLike) { + if (this.config.experimentalSessionAndOrigin && urlDoesNotMatchPolicyBasedOnDomain && isAUTFrame && isHTMLLike) { this.debug('- cross origin injection') return 'fullCrossOrigin' diff --git a/packages/proxy/lib/http/util/buffers.ts b/packages/proxy/lib/http/util/buffers.ts index afaa8f01da03..36a38eba3795 100644 --- a/packages/proxy/lib/http/util/buffers.ts +++ b/packages/proxy/lib/http/util/buffers.ts @@ -12,7 +12,7 @@ export type HttpBuffer = { response: IncomingMessage stream: Readable url: string - isCrossSuperDomainOrigin: boolean + urlDoesNotMatchPolicyBasedOnDomain: boolean } const stripPort = (url) => { diff --git a/packages/proxy/test/unit/http/request-middleware.spec.ts b/packages/proxy/test/unit/http/request-middleware.spec.ts index d3f1faf68095..c2a22b9fa8c9 100644 --- a/packages/proxy/test/unit/http/request-middleware.spec.ts +++ b/packages/proxy/test/unit/http/request-middleware.spec.ts @@ -548,7 +548,7 @@ describe('http/request-middleware', () => { it('sets wantsInjection to full when a request is buffered', async () => { const buffers = new HttpBuffers() - const buffer = { url: 'https://www.cypress.io/', isCrossSuperDomainOrigin: false } as HttpBuffer + const buffer = { url: 'https://www.cypress.io/', urlDoesNotMatchPolicyBasedOnDomain: false } as HttpBuffer buffers.set(buffer) @@ -568,7 +568,7 @@ describe('http/request-middleware', () => { it('sets wantsInjection to fullCrossOrigin when a cross origin request is buffered and experimentalSessionAndOrigin=true', async () => { const buffers = new HttpBuffers() - const buffer = { url: 'https://www.cypress.io/', isCrossSuperDomainOrigin: true } as HttpBuffer + const buffer = { url: 'https://www.cypress.io/', urlDoesNotMatchPolicyBasedOnDomain: true } as HttpBuffer buffers.set(buffer) @@ -591,7 +591,7 @@ describe('http/request-middleware', () => { it('sets wantsInjection to full when a cross origin request is buffered and experimentalSessionAndOrigin=false', async () => { const buffers = new HttpBuffers() - const buffer = { url: 'https://www.cypress.io/', isCrossSuperDomainOrigin: true } as HttpBuffer + const buffer = { url: 'https://www.cypress.io/', urlDoesNotMatchPolicyBasedOnDomain: true } as HttpBuffer buffers.set(buffer) @@ -614,7 +614,7 @@ describe('http/request-middleware', () => { it('wantsInjection is not set when the request is not buffered', async () => { const buffers = new HttpBuffers() - const buffer = { url: 'https://www.cypress.io/', isCrossSuperDomainOrigin: true } as HttpBuffer + const buffer = { url: 'https://www.cypress.io/', urlDoesNotMatchPolicyBasedOnDomain: true } as HttpBuffer buffers.set(buffer) diff --git a/packages/server/lib/controllers/files.js b/packages/server/lib/controllers/files.js index d5516d13a043..48b54f853fe0 100644 --- a/packages/server/lib/controllers/files.js +++ b/packages/server/lib/controllers/files.js @@ -39,13 +39,14 @@ module.exports = { }) }, - handleCrossOriginIframe (req, res) { + handleCrossOriginIframe (req, res, namespace) { const iframePath = cwd('lib', 'html', 'spec-bridge-iframe.html') const domain = cors.getSuperDomain(req.proxiedUrl) const iframeOptions = { domain, title: `Cypress for ${domain}`, + namespace, } debug('cross origin iframe with options %o', iframeOptions) diff --git a/packages/server/lib/html/spec-bridge-iframe.html b/packages/server/lib/html/spec-bridge-iframe.html index 5c75ad416ef2..7c4976525802 100644 --- a/packages/server/lib/html/spec-bridge-iframe.html +++ b/packages/server/lib/html/spec-bridge-iframe.html @@ -8,6 +8,6 @@ - +