mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-19 21:51:37 -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>
712 lines
21 KiB
CoffeeScript
712 lines
21 KiB
CoffeeScript
_ = require("lodash")
|
|
exphbs = require("express-handlebars")
|
|
url = require("url")
|
|
http = require("http")
|
|
concatStream = require("concat-stream")
|
|
stream = require("stream")
|
|
express = require("express")
|
|
Promise = require("bluebird")
|
|
evilDns = require("evil-dns")
|
|
isHtml = require("is-html")
|
|
httpProxy = require("http-proxy")
|
|
la = require("lazy-ass")
|
|
check = require("check-more-types")
|
|
httpsProxy = require("@packages/https-proxy")
|
|
compression = require("compression")
|
|
debug = require("debug")("cypress:server:server")
|
|
agent = require("@packages/network").agent
|
|
cors = require("./util/cors")
|
|
uri = require("./util/uri")
|
|
origin = require("./util/origin")
|
|
ensureUrl = require("./util/ensure-url")
|
|
appData = require("./util/app_data")
|
|
buffers = require("./util/buffers")
|
|
blacklist = require("./util/blacklist")
|
|
statusCode = require("./util/status_code")
|
|
headersUtil = require("./util/headers")
|
|
allowDestroy = require("./util/server_destroy")
|
|
cwd = require("./cwd")
|
|
errors = require("./errors")
|
|
logger = require("./logger")
|
|
Socket = require("./socket")
|
|
Request = require("./request")
|
|
fileServer = require("./file_server")
|
|
|
|
DEFAULT_DOMAIN_NAME = "localhost"
|
|
fullyQualifiedRe = /^https?:\/\//
|
|
|
|
isResponseHtml = (contentType, responseBuffer) ->
|
|
if contentType
|
|
return contentType is "text/html"
|
|
|
|
if body = _.invoke(responseBuffer, 'toString')
|
|
return isHtml(body)
|
|
|
|
return false
|
|
|
|
setProxiedUrl = (req) ->
|
|
## bail if we've already proxied the url
|
|
return if req.proxiedUrl
|
|
|
|
## backup the original proxied url
|
|
## and slice out the host/origin
|
|
## and only leave the path which is
|
|
## how browsers would normally send
|
|
## use their url
|
|
req.proxiedUrl = uri.removeDefaultPort(req.url).format()
|
|
|
|
req.url = uri.getPath(req.url)
|
|
|
|
notSSE = (req, res) ->
|
|
req.headers.accept isnt "text/event-stream" and compression.filter(req, res)
|
|
|
|
## currently not making use of event emitter
|
|
## but may do so soon
|
|
class Server
|
|
constructor: ->
|
|
if not (@ instanceof Server)
|
|
return new Server()
|
|
|
|
@_request = null
|
|
@_middleware = null
|
|
@_server = null
|
|
@_socket = null
|
|
@_baseUrl = null
|
|
@_nodeProxy = null
|
|
@_fileServer = null
|
|
@_httpsProxy = null
|
|
@_urlResolver = null
|
|
|
|
createExpressApp: (morgan) ->
|
|
app = express()
|
|
|
|
## set the cypress config from the cypress.json file
|
|
app.set("view engine", "html")
|
|
|
|
## since we use absolute paths, configure express-handlebars to not automatically find layouts
|
|
## https://github.com/cypress-io/cypress/issues/2891
|
|
app.engine("html", exphbs({
|
|
defaultLayout: false
|
|
layoutsDir: []
|
|
partialsDir: []
|
|
}))
|
|
|
|
## handle the proxied url in case
|
|
## we have not yet started our websocket server
|
|
app.use (req, res, next) =>
|
|
setProxiedUrl(req)
|
|
|
|
## if we've defined some middlware
|
|
## then call this. useful in tests
|
|
if m = @_middleware
|
|
m(req, res)
|
|
|
|
## always continue on
|
|
|
|
next()
|
|
|
|
app.use require("cookie-parser")()
|
|
app.use compression({filter: notSSE})
|
|
app.use require("morgan")("dev") if morgan
|
|
|
|
## errorhandler
|
|
app.use require("errorhandler")()
|
|
|
|
## remove the express powered-by header
|
|
app.disable("x-powered-by")
|
|
|
|
return app
|
|
|
|
createRoutes: ->
|
|
require("./routes").apply(null, arguments)
|
|
|
|
getHttpServer: -> @_server
|
|
|
|
portInUseErr: (port) ->
|
|
e = errors.get("PORT_IN_USE_SHORT", port)
|
|
e.port = port
|
|
e.portInUse = true
|
|
e
|
|
|
|
open: (config = {}, project, onWarning) ->
|
|
la(_.isPlainObject(config), "expected plain config object", config)
|
|
|
|
Promise.try =>
|
|
## always reset any buffers
|
|
## TODO: change buffers to be an instance
|
|
## here and pass this dependency around
|
|
buffers.reset()
|
|
|
|
app = @createExpressApp(config.morgan)
|
|
|
|
logger.setSettings(config)
|
|
|
|
## generate our request instance
|
|
## and set the responseTimeout
|
|
@_request = Request({timeout: config.responseTimeout})
|
|
@_nodeProxy = httpProxy.createProxyServer()
|
|
|
|
getRemoteState = => @_getRemoteState()
|
|
|
|
@createHosts(config.hosts)
|
|
|
|
@createRoutes(app, config, @_request, getRemoteState, project, @_nodeProxy)
|
|
|
|
@createServer(app, config, project, @_request, onWarning)
|
|
|
|
createHosts: (hosts = {}) ->
|
|
_.each hosts, (ip, host) ->
|
|
evilDns.add(host, ip)
|
|
|
|
createServer: (app, config, project, request, onWarning) ->
|
|
new Promise (resolve, reject) =>
|
|
{port, fileServerFolder, socketIoRoute, baseUrl, blacklistHosts} = config
|
|
|
|
@_server = http.createServer(app)
|
|
|
|
allowDestroy(@_server)
|
|
|
|
onError = (err) =>
|
|
## if the server bombs before starting
|
|
## and the err no is EADDRINUSE
|
|
## then we know to display the custom err message
|
|
if err.code is "EADDRINUSE"
|
|
reject @portInUseErr(port)
|
|
|
|
onUpgrade = (req, socket, head) =>
|
|
debug("Got UPGRADE request from %s", req.url)
|
|
|
|
@proxyWebsockets(@_nodeProxy, socketIoRoute, req, socket, head)
|
|
|
|
callListeners = (req, res) =>
|
|
listeners = @_server.listeners("request").slice(0)
|
|
|
|
@_callRequestListeners(@_server, listeners, req, res)
|
|
|
|
onSniUpgrade = (req, socket, head) =>
|
|
upgrades = @_server.listeners("upgrade").slice(0)
|
|
for upgrade in upgrades
|
|
upgrade.call(@_server, req, socket, head)
|
|
|
|
@_server.on "connect", (req, socket, head) =>
|
|
debug("Got CONNECT request from %s", req.url)
|
|
|
|
@_httpsProxy.connect(req, socket, head, {
|
|
onDirectConnection: (req) =>
|
|
urlToCheck = "https://" + req.url
|
|
|
|
isMatching = cors.urlMatchesOriginPolicyProps(urlToCheck, @_remoteProps)
|
|
|
|
word = if isMatching then "does" else "does not"
|
|
|
|
debug("HTTPS request #{word} match URL: #{urlToCheck} with props: %o", @_remoteProps)
|
|
|
|
## if we are currently matching then we're
|
|
## not making a direct connection anyway
|
|
## so we only need to check this if we
|
|
## have blacklist hosts and are not matching.
|
|
##
|
|
## if we have blacklisted hosts lets
|
|
## see if this matches - if so then
|
|
## we cannot allow it to make a direct
|
|
## connection
|
|
if blacklistHosts and not isMatching
|
|
isMatching = blacklist.matches(urlToCheck, blacklistHosts)
|
|
|
|
debug("HTTPS request #{urlToCheck} matches blacklist?", isMatching)
|
|
|
|
## make a direct connection only if
|
|
## our req url does not match the origin policy
|
|
## which is the superDomain + port
|
|
return not isMatching
|
|
})
|
|
|
|
@_server.on "upgrade", onUpgrade
|
|
|
|
@_server.once "error", onError
|
|
|
|
@_listen(port, onError)
|
|
.then (port) =>
|
|
Promise.all([
|
|
httpsProxy.create(appData.path("proxy"), port, {
|
|
onRequest: callListeners
|
|
onUpgrade: onSniUpgrade
|
|
}),
|
|
|
|
fileServer.create(fileServerFolder)
|
|
])
|
|
.spread (httpsProxy, fileServer) =>
|
|
@_httpsProxy = httpsProxy
|
|
@_fileServer = fileServer
|
|
|
|
## if we have a baseUrl let's go ahead
|
|
## and make sure the server is connectable!
|
|
if baseUrl
|
|
@_baseUrl = baseUrl
|
|
|
|
if config.isTextTerminal
|
|
return @_retryBaseUrlCheck(baseUrl, onWarning)
|
|
.return(null)
|
|
.catch (e) ->
|
|
debug(e)
|
|
reject(errors.get("CANNOT_CONNECT_BASE_URL", baseUrl))
|
|
|
|
ensureUrl.isListening(baseUrl)
|
|
.return(null)
|
|
.catch (err) ->
|
|
errors.get("CANNOT_CONNECT_BASE_URL_WARNING", baseUrl)
|
|
|
|
.then (warning) =>
|
|
## once we open set the domain
|
|
## to root by default
|
|
## which prevents a situation where navigating
|
|
## to http sites redirects to /__/ cypress
|
|
@_onDomainSet(baseUrl ? "<root>")
|
|
|
|
resolve([port, warning])
|
|
|
|
_port: ->
|
|
_.chain(@_server).invoke("address").get("port").value()
|
|
|
|
_listen: (port, onError) ->
|
|
new Promise (resolve) =>
|
|
listener = =>
|
|
address = @_server.address()
|
|
|
|
@isListening = true
|
|
|
|
debug("Server listening on ", address)
|
|
|
|
@_server.removeListener "error", onError
|
|
|
|
resolve(address.port)
|
|
|
|
@_server.listen(port || 0, '127.0.0.1', listener)
|
|
|
|
_getRemoteState: ->
|
|
# {
|
|
# origin: "http://localhost:2020"
|
|
# fileServer:
|
|
# strategy: "file"
|
|
# domainName: "localhost"
|
|
# props: null
|
|
# }
|
|
|
|
# {
|
|
# origin: "https://foo.google.com"
|
|
# strategy: "http"
|
|
# domainName: "google.com"
|
|
# props: {
|
|
# port: 443
|
|
# tld: "com"
|
|
# domain: "google"
|
|
# }
|
|
# }
|
|
|
|
props = _.extend({}, {
|
|
auth: @_remoteAuth
|
|
props: @_remoteProps
|
|
origin: @_remoteOrigin
|
|
strategy: @_remoteStrategy
|
|
visiting: @_remoteVisitingUrl
|
|
domainName: @_remoteDomainName
|
|
fileServer: @_remoteFileServer
|
|
})
|
|
|
|
debug("Getting remote state: %o", props)
|
|
|
|
return props
|
|
|
|
_onRequest: (headers, automationRequest, options) ->
|
|
@_request.sendPromise(headers, automationRequest, options)
|
|
|
|
_onResolveUrl: (urlStr, headers, automationRequest, options = {}) ->
|
|
debug("resolving visit %o", {
|
|
url: urlStr
|
|
headers
|
|
options
|
|
})
|
|
|
|
startTime = new Date()
|
|
|
|
## if we have an existing url resolver
|
|
## in flight then cancel it
|
|
if @_urlResolver
|
|
@_urlResolver.cancel()
|
|
|
|
request = @_request
|
|
|
|
handlingLocalFile = false
|
|
previousState = _.clone @_getRemoteState()
|
|
|
|
## nuke any hashes from our url since
|
|
## those those are client only and do
|
|
## not apply to http requests
|
|
urlStr = url.parse(urlStr)
|
|
urlStr.hash = null
|
|
urlStr = urlStr.format()
|
|
|
|
originalUrl = urlStr
|
|
|
|
reqStream = null
|
|
currentPromisePhase = null
|
|
|
|
runPhase = (fn) ->
|
|
return currentPromisePhase = fn()
|
|
|
|
return @_urlResolver = p = new Promise (resolve, reject, onCancel) =>
|
|
onCancel ->
|
|
p.currentPromisePhase = currentPromisePhase
|
|
p.reqStream = reqStream
|
|
|
|
_.invoke(reqStream, "abort")
|
|
_.invoke(currentPromisePhase, "cancel")
|
|
|
|
## if we have a buffer for this url
|
|
## then just respond with its details
|
|
## so we are idempotant and do not make
|
|
## another request
|
|
if obj = buffers.getByOriginalUrl(urlStr)
|
|
debug("got previous request buffer for url:", urlStr)
|
|
|
|
## reset the cookies from the buffer on the browser
|
|
return runPhase ->
|
|
resolve(
|
|
Promise.map obj.details.cookies, _.partial(automationRequest, 'set:cookie')
|
|
.return(obj.details)
|
|
)
|
|
|
|
redirects = []
|
|
newUrl = null
|
|
|
|
if not fullyQualifiedRe.test(urlStr)
|
|
handlingLocalFile = true
|
|
|
|
@_remoteVisitingUrl = true
|
|
|
|
@_onDomainSet(urlStr, options)
|
|
|
|
## TODO: instead of joining remoteOrigin here
|
|
## we can simply join our fileServer origin
|
|
## and bypass all the remoteState.visiting shit
|
|
urlFile = url.resolve(@_remoteFileServer, urlStr)
|
|
urlStr = url.resolve(@_remoteOrigin, urlStr)
|
|
|
|
onReqError = (err) =>
|
|
## only restore the previous state
|
|
## if our promise is still pending
|
|
if p.isPending()
|
|
restorePreviousState()
|
|
|
|
reject(err)
|
|
|
|
onReqStreamReady = (str) =>
|
|
reqStream = str
|
|
|
|
str
|
|
.on("error", onReqError)
|
|
.on "response", (incomingRes) =>
|
|
debug(
|
|
"resolve:url headers received, buffering response %o",
|
|
_.pick(incomingRes, "headers", "statusCode")
|
|
)
|
|
|
|
newUrl ?= urlStr
|
|
|
|
runPhase =>
|
|
## get the cookies that would be sent with this request so they can be rehydrated
|
|
automationRequest("get:cookies", {
|
|
domain: cors.getSuperDomain(newUrl)
|
|
})
|
|
.then (cookies) =>
|
|
@_remoteVisitingUrl = false
|
|
|
|
statusIs2xxOrAllowedFailure = ->
|
|
## is our status code in the 2xx range, or have we disabled failing
|
|
## on status code?
|
|
statusCode.isOk(incomingRes.statusCode) or (options.failOnStatusCode is false)
|
|
|
|
isOk = statusIs2xxOrAllowedFailure()
|
|
contentType = headersUtil.getContentType(incomingRes)
|
|
|
|
details = {
|
|
isOkStatusCode: isOk
|
|
contentType
|
|
url: newUrl
|
|
status: incomingRes.statusCode
|
|
cookies
|
|
statusText: statusCode.getText(incomingRes.statusCode)
|
|
redirects
|
|
originalUrl
|
|
}
|
|
|
|
## does this response have this cypress header?
|
|
if fp = incomingRes.headers["x-cypress-file-path"]
|
|
## if so we know this is a local file request
|
|
details.filePath = fp
|
|
|
|
debug("setting details resolving url %o", details)
|
|
|
|
concatStr = concatStream (responseBuffer) =>
|
|
## buffer the entire response before resolving.
|
|
## this allows us to detect & reject ETIMEDOUT errors
|
|
## where the headers have been sent but the
|
|
## connection hangs before receiving a body.
|
|
|
|
if !_.get(responseBuffer, 'length')
|
|
## concatStream can yield an empty array, which is
|
|
## not a valid chunk
|
|
responseBuffer = undefined
|
|
|
|
## if there is not a content-type, try to determine
|
|
## if the response content is HTML-like
|
|
## https://github.com/cypress-io/cypress/issues/1727
|
|
details.isHtml = isResponseHtml(contentType, responseBuffer)
|
|
|
|
debug("resolve:url response ended, setting buffer %o", { newUrl, details })
|
|
|
|
details.totalTime = new Date() - startTime
|
|
|
|
## TODO: think about moving this logic back into the
|
|
## frontend so that the driver can be in control of
|
|
## when the server should cache the request buffer
|
|
## and set the domain vs not
|
|
if isOk and details.isHtml
|
|
## reset the domain to the new url if we're not
|
|
## handling a local file
|
|
@_onDomainSet(newUrl, options) if not handlingLocalFile
|
|
|
|
responseBufferStream = new stream.PassThrough({
|
|
highWaterMark: Number.MAX_SAFE_INTEGER
|
|
})
|
|
|
|
responseBufferStream.end(responseBuffer)
|
|
|
|
buffers.set({
|
|
url: newUrl
|
|
stream: responseBufferStream
|
|
details
|
|
originalUrl: originalUrl
|
|
response: incomingRes
|
|
})
|
|
else
|
|
## TODO: move this logic to the driver too for
|
|
## the same reasons listed above
|
|
restorePreviousState()
|
|
|
|
resolve(details)
|
|
|
|
str.pipe(concatStr)
|
|
.catch(onReqError)
|
|
|
|
restorePreviousState = =>
|
|
@_remoteAuth = previousState.auth
|
|
@_remoteProps = previousState.props
|
|
@_remoteOrigin = previousState.origin
|
|
@_remoteStrategy = previousState.strategy
|
|
@_remoteFileServer = previousState.fileServer
|
|
@_remoteDomainName = previousState.domainName
|
|
@_remoteVisitingUrl = previousState.visiting
|
|
|
|
# if they're POSTing an object, querystringify their POST body
|
|
if options.method == 'POST' and _.isObject(options.body)
|
|
options.form = options.body
|
|
delete options.body
|
|
|
|
_.assign(options, {
|
|
## turn off gzip since we need to eventually
|
|
## rewrite these contents
|
|
gzip: false
|
|
url: urlFile ? urlStr
|
|
headers: _.assign({
|
|
accept: "text/html,*/*"
|
|
}, options.headers)
|
|
onBeforeReqInit: runPhase
|
|
followRedirect: (incomingRes) ->
|
|
status = incomingRes.statusCode
|
|
next = incomingRes.headers.location
|
|
|
|
curr = newUrl ? urlStr
|
|
|
|
newUrl = url.resolve(curr, next)
|
|
|
|
redirects.push([status, newUrl].join(": "))
|
|
|
|
return true
|
|
})
|
|
|
|
debug('sending request with options %o', options)
|
|
|
|
runPhase ->
|
|
request.sendStream(headers, automationRequest, options)
|
|
.then (createReqStream) ->
|
|
onReqStreamReady(createReqStream())
|
|
.catch(onReqError)
|
|
|
|
_onDomainSet: (fullyQualifiedUrl, options = {}) ->
|
|
l = (type, val) ->
|
|
debug("Setting", type, val)
|
|
|
|
@_remoteAuth = options.auth
|
|
|
|
l("remoteAuth", @_remoteAuth)
|
|
|
|
## if this isn't a fully qualified url
|
|
## or if this came to us as <root> in our tests
|
|
## then we know to go back to our default domain
|
|
## which is the localhost server
|
|
if fullyQualifiedUrl is "<root>" or not fullyQualifiedRe.test(fullyQualifiedUrl)
|
|
@_remoteOrigin = "http://#{DEFAULT_DOMAIN_NAME}:#{@_port()}"
|
|
@_remoteStrategy = "file"
|
|
@_remoteFileServer = "http://#{DEFAULT_DOMAIN_NAME}:#{@_fileServer?.port()}"
|
|
@_remoteDomainName = DEFAULT_DOMAIN_NAME
|
|
@_remoteProps = null
|
|
|
|
l("remoteOrigin", @_remoteOrigin)
|
|
l("remoteStrategy", @_remoteStrategy)
|
|
l("remoteHostAndPort", @_remoteProps)
|
|
l("remoteDocDomain", @_remoteDomainName)
|
|
l("remoteFileServer", @_remoteFileServer)
|
|
|
|
else
|
|
@_remoteOrigin = origin(fullyQualifiedUrl)
|
|
|
|
@_remoteStrategy = "http"
|
|
|
|
@_remoteFileServer = null
|
|
|
|
## set an object with port, tld, and domain properties
|
|
## as the remoteHostAndPort
|
|
@_remoteProps = cors.parseUrlIntoDomainTldPort(@_remoteOrigin)
|
|
|
|
@_remoteDomainName = _.compact([@_remoteProps.domain, @_remoteProps.tld]).join(".")
|
|
|
|
l("remoteOrigin", @_remoteOrigin)
|
|
l("remoteHostAndPort", @_remoteProps)
|
|
l("remoteDocDomain", @_remoteDomainName)
|
|
|
|
return @_getRemoteState()
|
|
|
|
_callRequestListeners: (server, listeners, req, res) ->
|
|
for listener in listeners
|
|
listener.call(server, req, res)
|
|
|
|
_normalizeReqUrl: (server) ->
|
|
## because socket.io removes all of our request
|
|
## events, it forces the socket.io traffic to be
|
|
## handled first.
|
|
## however we need to basically do the same thing
|
|
## it does and after we call into socket.io go
|
|
## through and remove all request listeners
|
|
## and change the req.url by slicing out the host
|
|
## because the browser is in proxy mode
|
|
listeners = server.listeners("request").slice(0)
|
|
server.removeAllListeners("request")
|
|
server.on "request", (req, res) =>
|
|
setProxiedUrl(req)
|
|
|
|
@_callRequestListeners(server, listeners, req, res)
|
|
|
|
_retryBaseUrlCheck: (baseUrl, onWarning) ->
|
|
ensureUrl.retryIsListening(baseUrl, {
|
|
retryIntervals: [3000, 3000, 4000],
|
|
onRetry: ({ attempt, delay, remaining }) ->
|
|
warning = errors.get("CANNOT_CONNECT_BASE_URL_RETRYING", {
|
|
remaining
|
|
attempt
|
|
delay
|
|
baseUrl
|
|
})
|
|
|
|
onWarning(warning)
|
|
})
|
|
|
|
proxyWebsockets: (proxy, socketIoRoute, req, socket, head) ->
|
|
## bail if this is our own namespaced socket.io request
|
|
return if req.url.startsWith(socketIoRoute)
|
|
|
|
if (host = req.headers.host) and @_remoteProps and (remoteOrigin = @_remoteOrigin)
|
|
## get the port from @_remoteProps
|
|
## get the protocol from remoteOrigin
|
|
## get the hostname from host header
|
|
{port} = @_remoteProps
|
|
{protocol} = url.parse(remoteOrigin)
|
|
{hostname} = url.parse("http://#{host}")
|
|
|
|
onProxyErr = (err, req, res) ->
|
|
debug("Got ERROR proxying websocket connection", { err, port, protocol, hostname, req })
|
|
|
|
proxy.ws(req, socket, head, {
|
|
secure: false
|
|
target: {
|
|
host: hostname
|
|
port: port
|
|
protocol: protocol
|
|
}
|
|
agent: agent
|
|
}, onProxyErr)
|
|
else
|
|
## we can't do anything with this socket
|
|
## since we don't know how to proxy it!
|
|
socket.end() if socket.writable
|
|
|
|
reset: ->
|
|
buffers.reset()
|
|
|
|
@_onDomainSet(@_baseUrl ? "<root>")
|
|
|
|
_close: ->
|
|
@reset()
|
|
|
|
logger.unsetSettings()
|
|
|
|
evilDns.clear()
|
|
|
|
## bail early we dont have a server or we're not
|
|
## currently listening
|
|
return Promise.resolve() if not @_server or not @isListening
|
|
|
|
@_server.destroyAsync()
|
|
.then =>
|
|
@isListening = false
|
|
|
|
close: ->
|
|
Promise.join(
|
|
@_close()
|
|
@_socket?.close()
|
|
@_fileServer?.close()
|
|
@_httpsProxy?.close()
|
|
)
|
|
.then =>
|
|
## reset any middleware
|
|
@_middleware = null
|
|
|
|
end: ->
|
|
@_socket and @_socket.end()
|
|
|
|
changeToUrl: (url) ->
|
|
@_socket and @_socket.changeToUrl(url)
|
|
|
|
onTestFileChange: (filePath) ->
|
|
@_socket and @_socket.onTestFileChange(filePath)
|
|
|
|
onRequest: (fn) ->
|
|
@_middleware = fn
|
|
|
|
onNextRequest: (fn) ->
|
|
@onRequest =>
|
|
fn.apply(@, arguments)
|
|
|
|
@_middleware = null
|
|
|
|
startWebsockets: (automation, config, options = {}) ->
|
|
options.onResolveUrl = @_onResolveUrl.bind(@)
|
|
options.onRequest = @_onRequest.bind(@)
|
|
|
|
@_socket = Socket(config)
|
|
@_socket.startListening(@_server, automation, config, options)
|
|
@_normalizeReqUrl(@_server)
|
|
# handleListeners(@_server)
|
|
|
|
module.exports = Server
|