Skip to content

Commit

Permalink
fix: Handle large payloads sent with privileged commands (#27122)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbreiding authored Jun 23, 2023
1 parent 0055214 commit f5815b7
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 11 deletions.
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ _Released 07/05/2023 (PENDING)_
**Bugfixes:**

- Fixed an issue where some internal file locations consumed by the Cypress Angular Handler moved as a result of [this commit](https://github.com/angular/angular-cli/commit/466d86dc8d3398695055f9eced7402804848a381) from Angular. Addressed in [#27030](https://github.com/cypress-io/cypress/pull/27030).
- Fixed an issue where certain commands would fail with the error `must only be invoked from the spec file or support file` when invoked with a large argument. Fixes [#27099](https://github.com/cypress-io/cypress/issues/27099).

## 12.15.0

Expand Down
32 changes: 32 additions & 0 deletions packages/driver/cypress/e2e/e2e/privileged_commands.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ describe('privileged commands', () => {
cy.task('return:arg', 'arg')
})

// https://github.com/cypress-io/cypress/issues/27099
it('passes with large payloads', () => {
const hugeJson = Cypress._.times(3000).map(() => {
return {
key1: 'value 1',
key2: {
key3: 'value 3',
key4: {
key5: 'value 5',
},
},
}
})

cy.task('return:arg', hugeJson)
cy.writeFile('cypress/_test-output/huge-out.json', hugeJson)
})

it('handles undefined argument(s)', () => {
cy.task('arg:is:undefined')
cy.task('arg:is:undefined', undefined)
cy.task('arg:is:undefined', undefined, undefined)
cy.task('arg:is:undefined', undefined, { timeout: 9999 })
})

it('handles null argument(s)', () => {
cy.task('return:arg', null)
// @ts-ignore
cy.task('return:arg', null, null)
cy.task('return:arg', null, { timeout: 9999 })
})

it('passes in test body .then() callback', () => {
cy.then(() => {
cy.exec('echo "hello"')
Expand Down
52 changes: 48 additions & 4 deletions packages/server/lib/privileged-commands/privileged-channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
const filter = win.Array.prototype.filter
const arrayIncludes = win.Array.prototype.includes
const map = win.Array.prototype.map
const slice = win.Array.prototype.slice
const isArray = win.Array.isArray
const stringIncludes = win.String.prototype.includes
const replace = win.String.prototype.replace
const split = win.String.prototype.split
const functionToString = win.Function.prototype.toString
const fetch = win.fetch
const parse = win.JSON.parse
const stringify = win.JSON.stringify
const charCodeAt = win.String.prototype.charCodeAt
const imul = Math.imul

const queryStringRegex = /\?.*$/

Expand Down Expand Up @@ -104,6 +108,37 @@
return hasStackLinesFromSpecOrSupportFile(err)
}

// source: https://github.com/bryc/code/blob/d0dac1c607a005679799024ff66166e13601d397/jshash/experimental/cyrb53.js
function hash (str) {
const seed = 0
let h1 = 0xdeadbeef ^ seed
let h2 = 0x41c6ce57 ^ seed

for (let i = 0, ch; i < str.length; i++) {
ch = charCodeAt.call(str, i)
h1 = imul(h1 ^ ch, 2654435761)
h2 = imul(h2 ^ ch, 1597334677)
}
h1 = imul(h1 ^ (h1 >>> 16), 2246822507)
h1 ^= imul(h2 ^ (h2 >>> 13), 3266489909)
h2 = imul(h2 ^ (h2 >>> 16), 2246822507)
h2 ^= imul(h1 ^ (h1 >>> 13), 3266489909)

return `${4294967296 * (2097151 & h2) + (h1 >>> 0)}`
}

function dropRightUndefined (array) {
if (!isArray(array)) return []

let index = array.length

// find index of last non-undefined arg
// eslint-disable-next-line no-empty
while (index-- && array[index] === undefined) {}

return slice.call(array, 0, index + 1)
}

async function onCommandInvocation (command) {
if (!arrayIncludes.call(privilegedCommands, command.name)) return

Expand All @@ -120,13 +155,21 @@
// it as a verified command
if (!stackIsFromSpecFrame(err)) return

const args = map.call([...command.args], (arg) => {
// hash the args to avoid `413 Request Entity Too Large` error from express.
// see https://github.com/cypress-io/cypress/issues/27099 and
// https://github.com/cypress-io/cypress/issues/27097
const args = dropRightUndefined(map.call([...command.args], (arg) => {
// undefined can't be JSON-stringified
if (arg === undefined) {
arg = null
}

if (typeof arg === 'function') {
return functionToString.call(arg)
arg = functionToString.call(arg)
}

return arg
})
return hash(stringify(arg))
}))

// if we verify a privileged command was invoked from the spec frame, we
// send it to the server, where it's stored in state. when the command is
Expand Down Expand Up @@ -154,6 +197,7 @@

// returned for testing purposes only
return {
dropRightUndefined,
onCommandInvocation,
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,36 @@ export interface SpecChannelOptions {

interface SpecOriginatedCommand {
name: string
args: any[]
args: string[]
}

type NonSpecError = Error & { isNonSpec: boolean | undefined }
type ChannelUrl = string
type ChannelKey = string

// hashes a string in the same manner as is in the privileged channel.
// unfortunately this can't be shared because we want to reduce the surface
// area in the privileged channel, which uses closured references to
// globally-accessible functions
// source: https://github.com/bryc/code/blob/d0dac1c607a005679799024ff66166e13601d397/jshash/experimental/cyrb53.js
function hash (str) {
const seed = 0
let h1 = 0xdeadbeef ^ seed
let h2 = 0x41c6ce57 ^ seed

for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i)
h1 = Math.imul(h1 ^ ch, 2654435761)
h2 = Math.imul(h2 ^ ch, 1597334677)
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)

return `${4294967296 * (2097151 & h2) + (h1 >>> 0)}`
}

class PrivilegedCommandsManager {
channelKeys: Record<ChannelUrl, ChannelKey> = {}
verifiedCommands: SpecOriginatedCommand[] = []
Expand Down Expand Up @@ -72,7 +95,13 @@ class PrivilegedCommandsManager {
// also removes that command from the verified commands array
hasVerifiedCommand (command) {
const matchingCommand = _.find(this.verifiedCommands, ({ name, args }) => {
return command.name === name && _.isEqual(command.args, _.dropRightWhile(args, _.isUndefined))
// the args added to `this.verifiedCommands` come hashed (see
// server/lib/privileged-commands/privileged-channel.js for more info)
// hash the ones sent with the command we're running for an
// apples-to-apples comparison
const hashedArgs = command.args.map((arg) => hash(JSON.stringify(arg)))

return command.name === name && _.isEqual(args, hashedArgs)
})

return !!matchingCommand
Expand Down
50 changes: 45 additions & 5 deletions packages/server/test/unit/browsers/privileged-channel_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ describe('privileged channel', () => {
// in the tests, they don't mess up the actual globals since the test
// runner itself relies on them
win = {
Array: { prototype: {
filter: Array.prototype.filter,
includes: Array.prototype.includes,
map: Array.prototype.map,
} },
Array: {
isArray: Array.isArray,
prototype: {
filter: Array.prototype.filter,
includes: Array.prototype.includes,
map: Array.prototype.map,
slice: Array.prototype.slice,
},
},
Error: ErrorStub,
Cypress: {
on () {},
Expand All @@ -32,11 +36,15 @@ describe('privileged channel', () => {
Function: { prototype: {
toString: Function.prototype.toString,
} },
Math: {
imul: Math.imul,
},
JSON: {
parse: JSON.parse,
stringify: JSON.stringify,
},
String: { prototype: {
charCodeAt: String.prototype.charCodeAt,
includes: String.prototype.includes,
replace: String.prototype.replace,
split: String.prototype.split,
Expand Down Expand Up @@ -177,4 +185,36 @@ describe('privileged channel', () => {
expect(win.JSON.parse).not.to.be.called
})
})

describe('#dropRightUndefined', () => {
it('removes undefined values from the right side of the array', () => {
const { dropRightUndefined } = runPrivilegedChannel()

expect(dropRightUndefined(['one', 'two'])).to.deep.equal(['one', 'two'])
expect(dropRightUndefined(['one', 'two', undefined])).to.deep.equal(['one', 'two'])
expect(dropRightUndefined(['one', 'two', undefined, undefined])).to.deep.equal(['one', 'two'])
expect(dropRightUndefined(['one', 'two', undefined, null])).to.deep.equal(['one', 'two', undefined, null])
expect(dropRightUndefined(['one', 'two', null, undefined])).to.deep.equal(['one', 'two', null])
})

it('does not remove undefined values from the beginning or middle of the array', () => {
const { dropRightUndefined } = runPrivilegedChannel()

expect(dropRightUndefined([undefined, 'one', 'two'])).to.deep.equal([undefined, 'one', 'two'])
expect(dropRightUndefined([undefined, 'one', undefined, 'two'])).to.deep.equal([undefined, 'one', undefined, 'two'])
expect(dropRightUndefined([undefined, 'one', undefined, 'two', undefined])).to.deep.equal([undefined, 'one', undefined, 'two'])
expect(dropRightUndefined(['one', undefined, 'two'])).to.deep.equal(['one', undefined, 'two'])
expect(dropRightUndefined(['one', undefined, 'two', undefined])).to.deep.equal(['one', undefined, 'two'])
})

it('returns empty array if not passed an array', () => {
const { dropRightUndefined } = runPrivilegedChannel()

expect(dropRightUndefined()).to.deep.equal([])
expect(dropRightUndefined({})).to.deep.equal([])
expect(dropRightUndefined(true)).to.deep.equal([])
expect(dropRightUndefined(123)).to.deep.equal([])
expect(dropRightUndefined('string')).to.deep.equal([])
})
})
})

8 comments on commit f5815b7

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f5815b7 Jun 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.16.0/linux-arm64/develop-f5815b776f71ff9d5de9465fa00384030ce78425/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f5815b7 Jun 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.16.0/linux-x64/develop-f5815b776f71ff9d5de9465fa00384030ce78425/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f5815b7 Jun 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.16.0/darwin-x64/develop-f5815b776f71ff9d5de9465fa00384030ce78425/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f5815b7 Jun 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.16.0/win32-x64/develop-f5815b776f71ff9d5de9465fa00384030ce78425/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f5815b7 Jun 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.16.0/linux-x64/develop-f5815b776f71ff9d5de9465fa00384030ce78425/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f5815b7 Jun 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.16.0/darwin-x64/develop-f5815b776f71ff9d5de9465fa00384030ce78425/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f5815b7 Jun 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.16.0/darwin-arm64/develop-f5815b776f71ff9d5de9465fa00384030ce78425/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f5815b7 Jun 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.16.0/win32-x64/develop-f5815b776f71ff9d5de9465fa00384030ce78425/cypress.tgz

Please sign in to comment.