Files
cypress/packages/proxy/lib/http/index.ts
Zach Bloomquist 6960f7cd78 Rewrite JS/HTML using AST-based approach (#5273)
* Add winPropAccessor to security.js, remove other replacers

* Add start of Cypress.resolveWindowReference

* Add regexes for dot and bracket access

* Some security_spec tests pass with new injection

* Add resolveWindowReference unit tests

* Old security_spec now passes with resolveWindowReference

* Inject stub resolveWindowReference so proxy still works outside of Cypress

* wip: rewrite HTML + JS with tokenizer

* Move to using esprima + hyntax to rewrite JS + HTML

* remove comment; oneLine makes the whole thing commented

* Fix tests, apple.com edge case

* wip: add getOrSet

* Revert "wip: add getOrSet"

This reverts commit a5c647c00f.

* release 3.5.0 [skip ci]

* use recast to replace window property accesses

* replace assignments to top properly

* fix yarn.lock

* bump deps

* update integration tests

* remove old security ts?

* fix integration spec

* always ignore js interception failure

* use globalThis instead of window

* add experimentalSourceRewriting flag

* restore regex-writer spec

* fix types

* update config_spec

* add source rewriting spec

* cleanup

* simplify rewriting logic, move rules into rewriter package

* create threaded rewriting tool for non-streaming use

* update @packages/rewriter to use threads for async

* use async rewriting where convenient

* add worker-shim.js

* add performance info to debug logs

* properly handle +=, -=, ...

* add proxy, rewriter to unit-tests stage

* cleanup

* use parse5 to rewrite HTML, strip SRI

* update tests

* reorganization, cleanup

* rewrite ALL parent, top identifiers except in a few cases

* handle many JS edge cases

* ensure parse5@5.1.1 is installed

* update yarn.lock

* update tests

* add debugging, add tests

* add attempted repro for .href issue

* implement source maps + extending inline source maps

* update opts passing in proxy layer

* fix sourcemap naming structure

* update tests to account for sourcemaps

* sourcemap tests

* remote source maps work

* comment

* update rewriter tests

* clean up TODOs in resolveWindowReference

* remove @types/nock

* clean up todos in deferred-source-map-cache

* fix rewriter build script

* fix concatStream import

* bump expectedresultcount

* clean up js-rules

* threading improvements, workaround for Electron segfault

* no visit_spec for now

* fix 6_visit_spec

* update MAX_WORKER_THREADS

* add repro for #3975

* cleanup

* cleanup

* make better use of namedTypes and builders

* get rid of the horrific closureDetectionTernary

ast-types keeps track of scope, so it is unneeded

* fix #3975, #3994

* add x-sourcemap, sourcemap header support

* snap-shot-it 7.9.3

* add deferred-source-map-cache-spec

* add tests

* Throw error in driver if AST rewriting fails

* Fix "location = 'relative-url'"

* fix max recursion depth

* slim down some fixtures

* fix window.location usage

* don't mess with `frames` at all

* no integration tests

* skip testing apple.com for now

* update wording: regex-based vs. ast-based

* skip real-world tests for now

* add some padding to process.exit workaround

* fix resolvers_spec

* fix html-spec

* cleanup

* Update packages/rewriter/lib/js-rules.ts

* Update packages/driver/src/cypress/resolvers.ts

* just import find by itself

* privatize typedefs for Cypress.state, remove .gitignore, remove dead code

Co-authored-by: Ben Kucera <14625260+Bkucera@users.noreply.github.com>
2020-05-11 12:54:14 -04:00

262 lines
6.0 KiB
TypeScript

import _ from 'lodash'
import debugModule from 'debug'
import ErrorMiddleware from './error-middleware'
import { HttpBuffers } from './util/buffers'
import { IncomingMessage } from 'http'
import Bluebird from 'bluebird'
import { Readable } from 'stream'
import { Request, Response } from 'express'
import RequestMiddleware from './request-middleware'
import ResponseMiddleware from './response-middleware'
import { DeferredSourceMapCache } from '@packages/rewriter'
const debug = debugModule('cypress:proxy:http')
export enum HttpStages {
IncomingRequest,
IncomingResponse,
Error
}
export type HttpMiddleware<T> = (this: HttpMiddlewareThis<T>) => void
export type CypressRequest = Request & {
// TODO: what's this difference from req.url? is it only for non-proxied requests?
proxiedUrl: string
abort: () => void
}
type MiddlewareStacks = {
[stage in HttpStages]: {
[name: string]: HttpMiddleware<any>
}
}
export type CypressResponse = Response & {
isInitial: null | boolean
wantsInjection: 'full' | 'partial' | false
wantsSecurityRemoved: null | boolean
}
type HttpMiddlewareCtx<T> = {
req: CypressRequest
res: CypressResponse
middleware: MiddlewareStacks
deferSourceMapRewrite: (opts: { js: string, url: string }) => string
} & T
const READONLY_MIDDLEWARE_KEYS: (keyof HttpMiddlewareThis<{}>)[] = [
'buffers',
'config',
'getFileServerToken',
'getRemoteState',
'request',
'next',
'end',
'onResponse',
'onError',
'skipMiddleware',
]
type HttpMiddlewareThis<T> = HttpMiddlewareCtx<T> & Readonly<{
buffers: HttpBuffers
config: any
getFileServerToken: () => string
getRemoteState: () => any
request: any
next: () => void
/**
* Call to completely end the stage, bypassing any remaining middleware.
*/
end: () => void
onResponse: (incomingRes: Response, resStream: Readable) => void
onError: (error: Error) => void
skipMiddleware: (name: string) => void
}>
export function _runStage (type: HttpStages, ctx: any) {
const stage = HttpStages[type]
debug('Entering stage %o', { stage })
const runMiddlewareStack = () => {
const middlewares = ctx.middleware[type]
// pop the first pair off the middleware
const middlewareName = _.keys(middlewares)[0]
if (!middlewareName) {
return Bluebird.resolve()
}
const middleware = middlewares[middlewareName]
ctx.middleware[type] = _.omit(middlewares, middlewareName)
return new Bluebird((resolve) => {
let ended = false
function copyChangedCtx () {
_.chain(fullCtx)
.omit(READONLY_MIDDLEWARE_KEYS)
.forEach((value, key) => {
if (ctx[key] !== value) {
ctx[key] = value
}
})
.value()
}
function _end (retval?) {
if (ended) {
return
}
ended = true
copyChangedCtx()
resolve(retval)
}
if (!middleware) {
return resolve()
}
debug('Running middleware %o', { stage, middlewareName })
const fullCtx = {
next: () => {
copyChangedCtx()
_end(runMiddlewareStack())
},
end: () => _end(),
onResponse: (incomingRes: IncomingMessage, resStream: Readable) => {
ctx.incomingRes = incomingRes
ctx.incomingResStream = resStream
_end()
},
onError: (error: Error) => {
debug('Error in middleware %o', { stage, middlewareName, error })
if (type === HttpStages.Error) {
return
}
ctx.error = error
_end(_runStage(HttpStages.Error, ctx))
},
skipMiddleware: (name) => {
ctx.middleware[type] = _.omit(ctx.middleware[type], name)
},
...ctx,
}
try {
middleware.call(fullCtx)
} catch (err) {
fullCtx.onError(err)
}
})
}
return runMiddlewareStack()
.then(() => {
debug('Leaving stage %o', { stage })
})
}
export class Http {
buffers: HttpBuffers
deferredSourceMapCache: DeferredSourceMapCache
config: any
getFileServerToken: () => string
getRemoteState: () => any
middleware: MiddlewareStacks
request: any
constructor (opts: {
config: any
getFileServerToken: () => string
getRemoteState: () => any
middleware?: MiddlewareStacks
request: any
}) {
this.buffers = new HttpBuffers()
this.deferredSourceMapCache = new DeferredSourceMapCache(opts.request)
this.config = opts.config
this.getFileServerToken = opts.getFileServerToken
this.getRemoteState = opts.getRemoteState
this.request = opts.request
if (typeof opts.middleware === 'undefined') {
this.middleware = {
[HttpStages.IncomingRequest]: RequestMiddleware,
[HttpStages.IncomingResponse]: ResponseMiddleware,
[HttpStages.Error]: ErrorMiddleware,
}
} else {
this.middleware = opts.middleware
}
}
handle (req: Request, res: Response) {
const ctx: HttpMiddlewareCtx<any> = {
req,
res,
buffers: this.buffers,
config: this.config,
getFileServerToken: this.getFileServerToken,
getRemoteState: this.getRemoteState,
request: this.request,
middleware: _.cloneDeep(this.middleware),
deferSourceMapRewrite: (opts) => {
this.deferredSourceMapCache.defer({
resHeaders: ctx.incomingRes.headers,
...opts,
})
},
}
return _runStage(HttpStages.IncomingRequest, ctx)
.then(() => {
if (ctx.incomingRes) {
return _runStage(HttpStages.IncomingResponse, ctx)
}
return debug('warning: Request was not fulfilled with a response.')
})
}
async handleSourceMapRequest (req: Request, res: Response) {
try {
const sm = await this.deferredSourceMapCache.resolve(req.params.id, req.headers)
if (!sm) {
throw new Error('no sourcemap found')
}
res.json(sm)
} catch (err) {
res.status(500).json({ err })
}
}
reset () {
this.buffers.reset()
}
setBuffer (buffer) {
return this.buffers.set(buffer)
}
}