mirror of
https://github.com/cypress-io/cypress.git
synced 2026-03-20 19:32:16 -05:00
* fix specs * use debugger protocol for cookie handling in electron * use latest gulp * use rimraf instead of gulp-clean * use electron 3.1.8 and node 10.2.1 * use gulp 4 in packages/static * fix sendCommandAsync, log Schema.getDomains on CDP connect * autofill e2e test name [skip ci] * electron@5.0.7, see what new failures exist * --no-sandbox for launching Electron * update cookies logic for electron * node 12 * update snapshot for new node * update error message for new node * stub sendCommandAsync * only connect to socket if path has been replaced, fixes #4776 * update node-sass to support node 12 * skip wacky socket tests for now * snapshot * fix run_plugins_spec snapshot, don't include stack trace * use --no-sandbox on linux to run as root * allow sendCommandAsync to resolve * use euid for root check * log domains even if undefined * don't worry about ending 1xx responses immediately anymore * use --max-http-header-size, change max size from 8kb to 1mb, fix #76 * do not send 502 on failed websocket, just send back ECONNRESET * update websocket spec port to not collide with other test * update outdated expect * Revert "only connect to socket if path has been replaced, fixes #4776" This reverts commitf179eda5ca. * update gulp in root * update https-proxy unit tests * update network spec to properly close server * update reporter spec * update https-proxy-agent to fix node 10.10.0 change discussion: https://github.com/nodejs/node/issues/24474\#issuecomment-511963799 * only pass --max-http-header-size on node >=12 * use own server-destroy implementation that supports secureConnect events * oops * update socket_spec * electron 6.0.0 * console.table introduced in node 10 * change browserify entry to init.js * handle edge case when no response body * console.table added in node 10 * do not exit app when all BrowserWindows are closed * update e2e snapshots * value may not be null * update plugins spec * correct cookie expiry, use browser.getversion for CDP version check * fix snapshotting for require stacks * reorder cookies in spec * warn when depreated electron callback apis are used * only report 1 plugin error per process * cleanup * node 12.4.0, cypress/browsers:node12.4.0-chrome76 docker image * update shell.openExternal to promisified * update dialog.showOpenDialog to promisified * update webContents.session.setProxy to promisified * updating native dependencies since we don't need ancient node ABI support anymore * WIP: switch cookies to simpler, jar-less approach * WIP: switch cookies to simpler, jar-less approach * making tests pass * improve cookie filtering logic * Remove unneeded Promise.try * filter what makes it to the extension * properly re-set superdomain cookies on cross-origin cy.visit * allow comma-separated list of e2e tests * sort cookies in order of expiration date, ascending * updating tests, cleanup * update tests * version electron as a devDependency, electron@6.0.1 * cleanup, remove old automation * cleanup, remove old automation * bump chokidar to fix win10 + node12 issue was seeing this on windows: https://github.com/nuxt/nuxt.js/issues/6035 fixed with version bump * enable now-supported quit role, re-enable old tests * don't need that arg there * remove last deprecated callback electron invocations * Delete cypress.json * responding to PR feedback * cleanup * invoke * use 'quit' role * Use new appMenu role for Cypress menu on mac * electron@6.0.2 * electron@6.0.3 * remove domain: cookie.domain and see what happens * remove setErrorHandler * Revert "remove domain: cookie.domain and see what happens" This reverts commit49e916896d. * add unit tests for cookies * ci * fix project-content css * electron@6.0.4 * fix specs_list test * electron@6.0.7 * some cleanup * electron@6.0.9 * Update 8_reporters_spec.coffee.js * electron@5.0.10 - Chromium 73, Node 12 * cli: fix the STDIN pipe on Windows (#5045) * cli: pipe stdin * uggh, here is the actual change * update cli unit tests * add unit test * more permissive check for json to include application/vnd.api+j… (#5166) * more permissive check for json to include * add json test for content-type application/vnd.api+json * cruder solution passes e2e tests locally, so let's go with that * Remove 'charset' from content-type before checking if JSON * fix eslint for fixture specs (#5176) * update eslint to lint files within 'fixtures' in support files - ignore some edge cases like jquery, jsx and obvious js files we wrote with broken code * Fixes from eslint to 'fixtures' files * Catch env variable with reserved name CYPRESS_ENV 1621 (#1626) * server: check CYPRESS_ENV variable when merging configs * catch invalid CYPRESS_ENV value in CLI, close #1621 * linting * sanitize platform in test snapshot * linting * update error message text * add missing comma * fix finally merge in JS code * pass CLI linter * fix log reference, should be debug * use correct sinon reference * update message, show first part in red * update error message text * Addresses #2953 (#5174) * Addresses #2953 * Added proper test for new error message * Didn't realize it ran this test as well, whoops * Implementing changes as suggested by @jennifer-shehane * Fixing tests and error output. Moved the checks to the start of the get command to ensure we always catch improper options * Removing issue test since the querying spec covers it * Using coffescript isArray check * depromisify things that were promisified b/t electron 5 <=> 6 Revert "update shell.openExternal to promisified" This reverts commit8b6460d015. Revert "update dialog.showOpenDialog to promisified" This reverts commit5f178b075b. Revert "update webContents.session.setProxy to promisified" This reverts commit727df3a4e5. * node12.4.0-chrome76 => node12.0.0-chrome75 * fix tests for electron downgrade * node12.0.0-chrome75 => node12.0.0-chrome73 Co-authored-by: Zach Bloomquist <github@chary.us> Co-authored-by: Brian Mann <brian.mann86@gmail.com>
412 lines
13 KiB
CoffeeScript
412 lines
13 KiB
CoffeeScript
_ = require("lodash")
|
|
zlib = require("zlib")
|
|
charset = require("charset")
|
|
concat = require("concat-stream")
|
|
iconv = require("iconv-lite")
|
|
Promise = require("bluebird")
|
|
accept = require("http-accept")
|
|
debug = require("debug")("cypress:server:proxy")
|
|
cwd = require("../cwd")
|
|
cors = require("../util/cors")
|
|
buffers = require("../util/buffers")
|
|
rewriter = require("../util/rewriter")
|
|
blacklist = require("../util/blacklist")
|
|
conditional = require("../util/conditional_stream")
|
|
{ passthruStream } = require("../util/passthru_stream")
|
|
|
|
REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]
|
|
NO_BODY_STATUS_CODES = [204, 304]
|
|
|
|
zlib = Promise.promisifyAll(zlib)
|
|
|
|
zlibOptions = {
|
|
flush: zlib.Z_SYNC_FLUSH
|
|
finishFlush: zlib.Z_SYNC_FLUSH
|
|
}
|
|
|
|
## https://github.com/cypress-io/cypress/issues/1543
|
|
getNodeCharsetFromResponse = (headers, body) ->
|
|
httpCharset = (charset(headers, body, 1024) || '').toLowerCase()
|
|
|
|
debug("inferred charset from response %o", { httpCharset })
|
|
|
|
if iconv.encodingExists(httpCharset)
|
|
return httpCharset
|
|
|
|
## browsers default to latin1
|
|
return "latin1"
|
|
|
|
isGzipError = (err) ->
|
|
Object.prototype.hasOwnProperty.call(zlib.constants, err.code)
|
|
|
|
## https://github.com/cypress-io/cypress/issues/4298
|
|
## https://tools.ietf.org/html/rfc7230#section-3.3.3
|
|
## HEAD, 1xx, 204, and 304 responses should never contain anything after headers
|
|
responseMustHaveEmptyBody = (method, statusCode) ->
|
|
_.some([
|
|
_.includes(NO_BODY_STATUS_CODES, statusCode),
|
|
_.invoke(method, 'toLowerCase') == 'head',
|
|
])
|
|
|
|
setCookie = (res, key, val, domainName) ->
|
|
## cannot use res.clearCookie because domain
|
|
## is not sent correctly
|
|
options = {
|
|
domain: domainName
|
|
}
|
|
|
|
if not val
|
|
val = ""
|
|
|
|
## force expires to be the epoch
|
|
options.expires = new Date(0)
|
|
|
|
res.cookie(key, val, options)
|
|
|
|
reqNeedsBasicAuthHeaders = (req, remoteState) ->
|
|
{ auth, origin } = remoteState
|
|
|
|
auth &&
|
|
not req.headers["authorization"] &&
|
|
cors.urlMatchesOriginProtectionSpace(req.proxiedUrl, origin)
|
|
|
|
module.exports = {
|
|
handle: (req, res, config, getRemoteState, request, nodeProxy) ->
|
|
remoteState = getRemoteState()
|
|
|
|
debug("handling proxied request %o", {
|
|
url: req.url
|
|
proxiedUrl: req.proxiedUrl
|
|
headers: req.headers
|
|
remoteState
|
|
})
|
|
|
|
## if we have an unload header it means
|
|
## our parent app has been navigated away
|
|
## directly and we need to automatically redirect
|
|
## to the clientRoute
|
|
if req.cookies["__cypress.unload"]
|
|
return res.redirect config.clientRoute
|
|
|
|
## when you access cypress from a browser which has not
|
|
## had its proxy setup then req.url will match req.proxiedUrl
|
|
## and we'll know to instantly redirect them to the correct
|
|
## client route
|
|
if req.url is req.proxiedUrl and not remoteState.visiting
|
|
## if we dont have a remoteState.origin that means we're initially
|
|
## requesting the cypress app and we need to redirect to the
|
|
## root path that serves the app
|
|
return res.redirect(config.clientRoute)
|
|
|
|
## if we have black listed hosts
|
|
if blh = config.blacklistHosts
|
|
## and url matches any of our blacklisted hosts
|
|
if matched = blacklist.matches(req.proxiedUrl, blh)
|
|
## then bail and return with 503
|
|
## and set a custom header
|
|
res.set("x-cypress-matched-blacklisted-host", matched)
|
|
|
|
debug("blacklisting request %o", {
|
|
url: req.proxiedUrl
|
|
matched: matched
|
|
})
|
|
|
|
return res.status(503).end()
|
|
|
|
thr = passthruStream()
|
|
|
|
@getHttpContent(thr, req, res, remoteState, config, request)
|
|
.pipe(res)
|
|
|
|
getHttpContent: (thr, req, res, remoteState, config, request) ->
|
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
|
|
|
|
isInitial = req.cookies["__cypress.initial"] is "true"
|
|
|
|
wantsInjection = null
|
|
wantsSecurityRemoved = null
|
|
|
|
resContentTypeIs = (respHeaders, str) ->
|
|
contentType = respHeaders["content-type"]
|
|
|
|
## make sure the response includes string type
|
|
contentType and contentType.includes(str)
|
|
|
|
resContentTypeIsJavaScript = (respHeaders) ->
|
|
_.some [
|
|
'application/javascript',
|
|
'application/x-javascript',
|
|
'text/javascript'
|
|
].map(_.partial(resContentTypeIs, respHeaders))
|
|
|
|
reqAcceptsHtml = ->
|
|
## don't inject if this is an XHR from jquery
|
|
return if req.headers["x-requested-with"]
|
|
|
|
types = accept.parser(req.headers.accept) ? []
|
|
|
|
find = (type) ->
|
|
type in types
|
|
|
|
## bail if we didn't find both text/html and application/xhtml+xml
|
|
## https://github.com/cypress-io/cypress/issues/288
|
|
find("text/html") and find("application/xhtml+xml")
|
|
|
|
resMatchesOriginPolicy = (respHeaders) ->
|
|
switch remoteState.strategy
|
|
when "http"
|
|
cors.urlMatchesOriginPolicyProps(req.proxiedUrl, remoteState.props)
|
|
when "file"
|
|
req.proxiedUrl.startsWith(remoteState.origin)
|
|
|
|
setCookies = (value) ->
|
|
## dont modify any cookies if we're trying to clear
|
|
## the initial cookie and we're not injecting anything
|
|
return if (not value) and (not wantsInjection)
|
|
|
|
## dont set the cookies if we're not on the initial request
|
|
return if not isInitial
|
|
|
|
setCookie(res, "__cypress.initial", value, remoteState.domainName)
|
|
|
|
setBody = (str, statusCode, headers) ->
|
|
## set the status to whatever the incomingRes statusCode is
|
|
res.status(statusCode)
|
|
|
|
## turn off __cypress.initial by setting false here
|
|
setCookies(false, wantsInjection)
|
|
|
|
encoding = headers["content-encoding"]
|
|
|
|
isGzipped = encoding and encoding.includes("gzip")
|
|
|
|
debug("received response for %o", {
|
|
url: req.proxiedUrl
|
|
headers,
|
|
statusCode,
|
|
isGzipped
|
|
wantsInjection,
|
|
wantsSecurityRemoved,
|
|
})
|
|
|
|
if responseMustHaveEmptyBody(req.method, statusCode)
|
|
return res.end()
|
|
|
|
## if there is nothing to inject then just
|
|
## bypass the stream buffer and pipe this back
|
|
if wantsInjection
|
|
rewrite = (body) ->
|
|
## transparently decode their body to a node string and then re-encode
|
|
nodeCharset = getNodeCharsetFromResponse(headers, body)
|
|
decodedBody = iconv.decode(body, nodeCharset)
|
|
rewrittenBody = rewriter.html(decodedBody, remoteState.domainName, wantsInjection, wantsSecurityRemoved)
|
|
iconv.encode(rewrittenBody, nodeCharset)
|
|
|
|
## TODO: we can probably move this to the new
|
|
## replacestream rewriter instead of using
|
|
## a buffer
|
|
injection = concat (body) ->
|
|
## concat-stream yields an empty array if nothing is written
|
|
if _.isEqual(body, [])
|
|
body = Buffer.from('')
|
|
## if we're gzipped that means we need to unzip
|
|
## this content first, inject, and the rezip
|
|
if isGzipped
|
|
zlib.gunzipAsync(body, zlibOptions)
|
|
.then(rewrite)
|
|
.then(zlib.gzipAsync)
|
|
.then(thr.end)
|
|
## if we have an error here there's nothing
|
|
## to do but log it out and end the socket
|
|
## because we cannot inject into content
|
|
## that failed rewriting gzip
|
|
## which is the same thing we do below
|
|
## on regular proxied network requests
|
|
.catch(endWithNetworkErr)
|
|
else
|
|
thr.end rewrite(body)
|
|
|
|
str.pipe(injection)
|
|
else
|
|
## only rewrite if we should
|
|
if wantsSecurityRemoved
|
|
gunzip = zlib.createGunzip(zlibOptions)
|
|
gunzip.setEncoding("utf8")
|
|
|
|
onError = (err) ->
|
|
gzipError = isGzipError(err)
|
|
|
|
debug("failed to proxy response %o", {
|
|
url: req.proxiedUrl
|
|
headers
|
|
statusCode
|
|
isGzipped
|
|
gzipError
|
|
wantsInjection
|
|
wantsSecurityRemoved
|
|
err
|
|
})
|
|
|
|
endWithNetworkErr(err)
|
|
|
|
## only unzip when it is already gzipped
|
|
return str
|
|
.pipe(conditional(isGzipped, gunzip))
|
|
.on("error", onError)
|
|
.pipe(rewriter.security())
|
|
.on("error", onError)
|
|
.pipe(conditional(isGzipped, zlib.createGzip()))
|
|
.on("error", onError)
|
|
.pipe(thr)
|
|
.on("error", onError)
|
|
|
|
return str.pipe(thr)
|
|
|
|
endWithNetworkErr = (err) ->
|
|
debug('request failed in proxy layer %o', {
|
|
res: _.pick(res, 'headersSent', 'statusCode', 'headers')
|
|
req: _.pick(req, 'url', 'proxiedUrl', 'headers', 'method')
|
|
err
|
|
})
|
|
|
|
req.socket.destroy()
|
|
|
|
onResponse = (str, incomingRes) =>
|
|
{headers, statusCode} = incomingRes
|
|
|
|
originalSetHeader = res.setHeader
|
|
|
|
## express does all kinds of silly/nasty stuff to the content-type...
|
|
## but we don't want to change it at all!
|
|
res.setHeader = (k, v) ->
|
|
if k == 'content-type'
|
|
v = incomingRes.headers['content-type']
|
|
|
|
originalSetHeader.call(res, k, v)
|
|
|
|
wantsInjection ?= do ->
|
|
return false if not resContentTypeIs(headers, "text/html")
|
|
|
|
return false if not resMatchesOriginPolicy(headers)
|
|
|
|
return "full" if isInitial
|
|
|
|
return false if not reqAcceptsHtml()
|
|
|
|
return "partial"
|
|
|
|
wantsSecurityRemoved = do ->
|
|
## we want to remove security IF we're doing a full injection
|
|
## on the response or its a request for any javascript script tag
|
|
config.modifyObstructiveCode and (
|
|
(wantsInjection is "full") or
|
|
resContentTypeIsJavaScript(headers)
|
|
)
|
|
|
|
@setResHeaders(req, res, incomingRes, wantsInjection)
|
|
|
|
## always proxy the cookies coming from the incomingRes
|
|
if cookies = headers["set-cookie"]
|
|
## normalize into array
|
|
for c in [].concat(cookies)
|
|
try
|
|
res.append("Set-Cookie", c)
|
|
catch err
|
|
## noop
|
|
|
|
if REDIRECT_STATUS_CODES.includes(statusCode)
|
|
newUrl = headers.location
|
|
|
|
## set cookies to initial=true
|
|
setCookies(true)
|
|
|
|
debug("redirecting to new url %o", { status: statusCode, url: newUrl })
|
|
|
|
## finally redirect our user agent back to our domain
|
|
## by making this an absolute-path-relative redirect
|
|
return res.redirect(statusCode, newUrl)
|
|
|
|
if headers["x-cypress-file-server-error"]
|
|
wantsInjection or= "partial"
|
|
|
|
setBody(str, statusCode, headers)
|
|
|
|
if obj = buffers.take(req.proxiedUrl)
|
|
wantsInjection = "full"
|
|
|
|
onResponse(obj.stream, obj.response)
|
|
else
|
|
opts = {
|
|
timeout: null
|
|
strictSSL: false
|
|
followRedirect: false
|
|
retryIntervals: [0, 100, 200, 200]
|
|
}
|
|
|
|
## strip unsupported accept-encoding headers
|
|
encodings = accept.parser(req.headers["accept-encoding"]) ? []
|
|
|
|
if "gzip" in encodings
|
|
## we only want to support gzip right now
|
|
req.headers["accept-encoding"] = "gzip"
|
|
else
|
|
## else just delete them since we cannot
|
|
## properly decode them
|
|
delete req.headers["accept-encoding"]
|
|
|
|
if remoteState.strategy is "file" and req.proxiedUrl.startsWith(remoteState.origin)
|
|
opts.url = req.proxiedUrl.replace(remoteState.origin, remoteState.fileServer)
|
|
else
|
|
opts.url = req.proxiedUrl
|
|
|
|
## if we have auth headers and this request matches our origin
|
|
## protection space and the user has not supplied auth headers
|
|
if reqNeedsBasicAuthHeaders(req, remoteState)
|
|
{ auth } = remoteState
|
|
|
|
base64 = Buffer
|
|
.from(auth.username + ":" + auth.password)
|
|
.toString("base64")
|
|
|
|
req.headers["authorization"] = "Basic #{base64}"
|
|
|
|
rq = request.create(opts)
|
|
|
|
rq.on("error", endWithNetworkErr)
|
|
|
|
rq.on "response", (incomingRes) ->
|
|
onResponse(rq, incomingRes)
|
|
|
|
## if our original request has been
|
|
## aborted, then ensure we forward
|
|
## this onto the proxied request
|
|
## https://github.com/cypress-io/cypress/issues/2612
|
|
## this can happen on permanent connections
|
|
## like SSE, but also on any regular ol'
|
|
## http request
|
|
req.on "aborted", ->
|
|
rq.abort()
|
|
|
|
## proxy the request body, content-type, headers
|
|
## to the new rq
|
|
req.pipe(rq)
|
|
|
|
return thr
|
|
|
|
setResHeaders: (req, res, incomingRes, wantsInjection) ->
|
|
return if res.headersSent
|
|
|
|
## omit problematic headers
|
|
headers = _.omit incomingRes.headers, "set-cookie", "x-frame-options", "content-length", "content-security-policy", "connection"
|
|
|
|
## do not cache when we inject content into responses
|
|
## later on we should switch to an etag system so we dont
|
|
## have to download the remote http responses if the etag
|
|
## hasnt changed
|
|
if wantsInjection
|
|
headers["cache-control"] = "no-cache, no-store, must-revalidate"
|
|
|
|
## proxy the headers
|
|
res.set(headers)
|
|
}
|