Skip to content

Commit c56e35c

Browse files
authored
server, driver: fixes #1288, handle basic auth at the network level, attach authorization headers to requests matching remote origin (#1290)
* server, driver: fixes #1288, handle basic auth at the network level, attach authorization headers to requests matching remote origin * server, driver: fixes failing tests * driver: fix tests, move around order of ops * driver: try cypress promise * server, driver: increase timeouts, don't disable background networking
1 parent d6c944e commit c56e35c

File tree

12 files changed

+276
-18
lines changed

12 files changed

+276
-18
lines changed

packages/driver/src/cy/commands/navigation.coffee

+16-2
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,11 @@ module.exports = (Commands, Cypress, cy, state, config) ->
257257
fn()
258258

259259
requestUrl = (url, options = {}) ->
260-
Cypress.backend("resolve:url", url, _.pick(options, "failOnStatusCode"))
260+
Cypress.backend(
261+
"resolve:url",
262+
url,
263+
_.pick(options, "failOnStatusCode", "auth")
264+
)
261265
.then (resp = {}) ->
262266
switch
263267
## if we didn't even get an OK response
@@ -456,6 +460,7 @@ module.exports = (Commands, Cypress, cy, state, config) ->
456460
$utils.throwErrByPath("visit.invalid_1st_arg")
457461

458462
_.defaults(options, {
463+
auth: null
459464
failOnStatusCode: true
460465
log: true
461466
timeout: config("pageLoadTimeout")
@@ -538,9 +543,14 @@ module.exports = (Commands, Cypress, cy, state, config) ->
538543

539544
remote = $Location.create(remoteUrl ? url)
540545

546+
## reset auth options if we have them
547+
if a = remote.authObj
548+
options.auth = a
549+
541550
## store the existing hash now since
542551
## we'll need to apply it later
543552
existingHash = remote.hash ? ""
553+
existingAuth = remote.auth ? ""
544554

545555
if previousDomainVisited and remote.originPolicy isnt existing.originPolicy
546556
## if we've already visited a new superDomain
@@ -562,7 +572,11 @@ module.exports = (Commands, Cypress, cy, state, config) ->
562572
## before telling our backend to resolve this url
563573
url = url.replace(existingHash, "")
564574

565-
requestUrl(url, _.pick(options, "failOnStatusCode"))
575+
if existingAuth
576+
## strip out the existing url if we have one
577+
url = url.replace(existingAuth + "@", "")
578+
579+
requestUrl(url, options)
566580
.then (resp = {}) =>
567581
{url, originalUrl, cookies, redirects, filePath} = resp
568582

packages/driver/src/cypress/location.coffee

+14
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ class $Location
2727
constructor: (remote) ->
2828
@remote = new UrlParse remote
2929

30+
getAuth: ->
31+
@remote.auth
32+
33+
getAuthObj: ->
34+
if a = @remote.auth
35+
[ username, password ] = a.split(":")
36+
return {
37+
username
38+
39+
password
40+
}
41+
3042
getHash: ->
3143
@remote.hash
3244

@@ -103,6 +115,8 @@ class $Location
103115

104116
getObject: ->
105117
{
118+
auth: @getAuth()
119+
authObj: @getAuthObj()
106120
hash: @getHash()
107121
href: @getHref()
108122
host: @getHost()

packages/driver/test/cypress/integration/commands/location_spec.coffee

+2-3
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,7 @@ describe "src/cy/commands/location", ->
245245
context "#location", ->
246246
it "returns the location object", ->
247247
cy.location().then (loc) ->
248-
keys = _.keys loc
249-
expect(keys).to.deep.eq ["hash", "href", "host", "hostname", "origin", "pathname", "port", "protocol", "search", "originPolicy", "superDomain", "toString"]
248+
expect(loc).to.have.keys ["auth", "authObj", "hash", "href", "host", "hostname", "origin", "pathname", "port", "protocol", "search", "originPolicy", "superDomain", "toString"]
250249

251250
it "returns a specific key from location object", ->
252251
cy.location("href").then (href) ->
@@ -380,4 +379,4 @@ describe "src/cy/commands/location", ->
380379

381380
expect(_.keys(consoleProps)).to.deep.eq ["Command", "Yielded"]
382381
expect(consoleProps.Command).to.eq "location"
383-
expect(_.keys(consoleProps.Yielded)).to.deep.eq ["hash", "href", "host", "hostname", "origin", "pathname", "port", "protocol", "search", "originPolicy", "superDomain", "toString"]
382+
expect(_.keys(consoleProps.Yielded)).to.deep.eq ["auth", "authObj", "hash", "href", "host", "hostname", "origin", "pathname", "port", "protocol", "search", "originPolicy", "superDomain", "toString"]

packages/driver/test/cypress/integration/commands/navigation_spec.coffee

+21
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,27 @@ describe "src/cy/commands/navigation", ->
513513
.visit("localhost:3500/status-404", { failOnStatusCode: false })
514514
.visit("localhost:3500/status-500", { failOnStatusCode: false })
515515

516+
it "strips username + password out of the url when provided", ->
517+
backend = cy.spy(Cypress, "backend")
518+
519+
cy
520+
.visit("http://cypress:password123@localhost:3500/timeout")
521+
.then ->
522+
expect(backend).to.be.calledWith("resolve:url", "http://localhost:3500/timeout")
523+
524+
it "passes auth options", ->
525+
backend = cy.spy(Cypress, "backend")
526+
527+
auth = {
528+
username: "cypress"
529+
password: "password123"
530+
}
531+
532+
cy
533+
.visit("http://localhost:3500/timeout", { auth })
534+
.then ->
535+
expect(backend).to.be.calledWithMatch("resolve:url", "http://localhost:3500/timeout", { auth })
536+
516537
describe "when only hashes are changing", ->
517538
it "short circuits the visit if the page will not refresh", ->
518539
count = 0

packages/driver/test/cypress/integration/cypress/location_spec.coffee

+18-1
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,30 @@ urls = {
1717
heroku: "https://example.herokuapp.com"
1818
herokuSub:"https://foo.example.herokuapp.com"
1919
unknown: "http://what.is.so.unknown"
20+
auth: "http://cypress:password123@localhost:8080/foo"
2021
}
2122

2223
describe "src/cypress/location", ->
2324
beforeEach ->
2425
@setup = (remote) =>
2526
new Location(urls[remote])
2627

28+
context "#getAuth", ->
29+
it "returns string with username + password", ->
30+
str = @setup("auth").getAuth()
31+
expect(str).to.eq("cypress:password123")
32+
33+
context "#getAuthObj", ->
34+
it "returns an object with username and password", ->
35+
obj = @setup("auth").getAuthObj()
36+
expect(obj).to.deep.eq({
37+
username: "cypress"
38+
password: "password123"
39+
})
40+
41+
it "returns undefined when no username or password", ->
42+
expect(@setup("app").getAuthObj()).to.be.undefined
43+
2744
context "#getHash", ->
2845
it "returns the hash fragment prepended with #", ->
2946
str = @setup("app").getHash()
@@ -159,7 +176,7 @@ describe "src/cypress/location", ->
159176
context ".create", ->
160177
it "returns an object literal", ->
161178
obj = Location.create(urls.cypress, urls.signin)
162-
keys = ["hash", "href", "host", "hostname", "origin", "pathname", "port", "protocol", "search", "toString", "originPolicy", "superDomain"]
179+
keys = ["auth", "authObj", "hash", "href", "host", "hostname", "origin", "pathname", "port", "protocol", "search", "toString", "originPolicy", "superDomain"]
163180
expect(obj).to.have.keys(keys)
164181

165182
it "can invoke toString function", ->
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,49 @@
1+
run = ->
2+
cy.window()
3+
.then { timeout: 60000 }, (win) ->
4+
new Cypress.Promise (resolve) ->
5+
i = win.document.createElement("iframe")
6+
i.onload = resolve
7+
i.src = "/basic_auth"
8+
win.document.body.appendChild(i)
9+
.get("iframe").should ($iframe) ->
10+
expect($iframe.contents().text()).to.include("basic auth worked")
11+
.window().then { timeout: 60000 }, (win) ->
12+
new Cypress.Promise (resolve, reject) ->
13+
xhr = new win.XMLHttpRequest()
14+
xhr.open("GET", "/basic_auth")
15+
xhr.onload = ->
16+
try
17+
expect(@responseText).to.include("basic auth worked")
18+
resolve(win)
19+
catch err
20+
reject(err)
21+
xhr.send()
22+
.then { timeout: 60000 }, (win) ->
23+
new Cypress.Promise (resolve, reject) ->
24+
## ensure other origins do not have auth headers attached
25+
xhr = new win.XMLHttpRequest()
26+
xhr.open("GET", "http://localhost:3501/basic_auth")
27+
xhr.onload = ->
28+
try
29+
expect(@status).to.eq(401)
30+
resolve(win)
31+
catch err
32+
reject(err)
33+
xhr.send()
34+
35+
# cy.visit("http://admin:[email protected]/basic_auth")
36+
137
describe "basic auth", ->
2-
it "can visit", ->
38+
it "can visit with username/pw in url", ->
339
cy.visit("http://cypress:password123@localhost:3500/basic_auth")
4-
# cy.visit("http://admin:[email protected]/basic_auth")
40+
run()
41+
42+
it "can visit with auth options", ->
43+
cy.visit("http://localhost:3500/basic_auth", {
44+
auth: {
45+
username: "cypress"
46+
password: "password123"
47+
}
48+
})
49+
run()

packages/server/lib/browsers/chrome.coffee

+5-5
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,17 @@ defaultArgs = [
4141
"--enable-automation"
4242
"--disable-infobars"
4343

44-
## needed for https://github.com/cypress-io/cypress/issues/573
45-
## list of flags here: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/runtime_enabled_features.json5
46-
"--disable-blink-features=BlockCredentialedSubresources"
47-
4844
## the following come frome chromedriver
4945
## https://code.google.com/p/chromium/codesearch#chromium/src/chrome/test/chromedriver/chrome_launcher.cc&sq=package:chromium&l=70
5046
"--metrics-recording-only"
5147
"--disable-prompt-on-repost"
5248
"--disable-hang-monitor"
5349
"--disable-sync"
54-
"--disable-background-networking"
50+
## this flag is causing throttling of XHR callbacks for
51+
## as much as 30 seconds. If you VNC in and open dev tools or
52+
## click on a button, it'll "instantly" work. with this
53+
## option enabled, it will time out some of our tests in circle
54+
# "--disable-background-networking"
5555
"--disable-web-resources"
5656
"--safebrowsing-disable-auto-update"
5757
"--safebrowsing-disable-download-protection"

packages/server/lib/controllers/proxy.coffee

+7
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,13 @@ module.exports = {
266266
else
267267
opts.url = remoteUrl
268268

269+
## if we have auth headers and this request matches our origin policy
270+
if (a = remoteState.auth) and resMatchesOriginPolicy()
271+
## and no existing Authentication headers
272+
if not req.headers["authorization"]
273+
base64 = new Buffer(a.username + ":" + a.password).toString("base64")
274+
req.headers["authorization"] = "Basic #{base64}"
275+
269276
rq = request.create(opts)
270277

271278
rq.on("error", endWithResponseErr)

packages/server/lib/server.coffee

+18-5
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ class Server
278278
# }
279279

280280
props = _.extend({}, {
281+
auth: @_remoteAuth
281282
props: @_remoteProps
282283
origin: @_remoteOrigin
283284
strategy: @_remoteStrategy
@@ -294,6 +295,12 @@ class Server
294295
@_request.send(headers, automationRequest, options)
295296

296297
_onResolveUrl: (urlStr, headers, automationRequest, options = {}) ->
298+
debug("resolving visit", {
299+
url: urlStr
300+
headers
301+
options
302+
})
303+
297304
request = @_request
298305

299306
handlingLocalFile = false
@@ -327,7 +334,7 @@ class Server
327334

328335
@_remoteVisitingUrl = true
329336

330-
@_onDomainSet(urlStr)
337+
@_onDomainSet(urlStr, options)
331338

332339
## TODO: instead of joining remoteOrigin here
333340
## we can simply join our fileServer origin
@@ -396,7 +403,7 @@ class Server
396403
if isOk and isHtml
397404
## reset the domain to the new url if we're not
398405
## handling a local file
399-
@_onDomainSet(newUrl) if not handlingLocalFile
406+
@_onDomainSet(newUrl, options) if not handlingLocalFile
400407

401408
buffers.set({
402409
url: newUrl
@@ -415,6 +422,7 @@ class Server
415422
.pipe(stream.PassThrough())
416423

417424
restorePreviousState = =>
425+
@_remoteAuth = previousState.auth
418426
@_remoteProps = previousState.props
419427
@_remoteOrigin = previousState.origin
420428
@_remoteStrategy = previousState.strategy
@@ -425,6 +433,7 @@ class Server
425433
request.sendStream(headers, automationRequest, {
426434
## turn off gzip since we need to eventually
427435
## rewrite these contents
436+
auth: options.auth
428437
gzip: false
429438
url: urlFile ? urlStr
430439
headers: {
@@ -445,9 +454,13 @@ class Server
445454
.then(handleReqStream)
446455
.catch(error)
447456

448-
_onDomainSet: (fullyQualifiedUrl) ->
449-
l = (type, url) ->
450-
debug("Setting %s %s", type, url)
457+
_onDomainSet: (fullyQualifiedUrl, options = {}) ->
458+
l = (type, val) ->
459+
debug("Setting", type, val)
460+
461+
@_remoteAuth = options.auth
462+
463+
l("remoteAuth", @_remoteAuth)
451464

452465
## if this isn't a fully qualified url
453466
## or if this came to us as <root> in our tests

packages/server/test/integration/http_requests_spec.coffee

+53
Original file line numberDiff line numberDiff line change
@@ -1673,6 +1673,59 @@ describe "Routes", ->
16731673
"Content-Type": "text/css"
16741674
})
16751675

1676+
it "attaches auth headers when matches origin", ->
1677+
username = "u"
1678+
password = "p"
1679+
1680+
base64 = new Buffer(username + ":" + password).toString("base64")
1681+
1682+
@server._remoteAuth = {
1683+
username
1684+
password
1685+
}
1686+
1687+
nock("http://localhost:8080")
1688+
.get("/index")
1689+
.matchHeader("authorization", "Basic #{base64}")
1690+
.reply(200, "")
1691+
1692+
@rp("http://localhost:8080/index")
1693+
.then (res) ->
1694+
expect(res.statusCode).to.eq(200)
1695+
1696+
it "does not attach auth headers when not matching origin", ->
1697+
nock("http://localhost:8080", {
1698+
badheaders: ['authorization']
1699+
})
1700+
.get("/index")
1701+
.reply(200, "")
1702+
1703+
@rp("http://localhost:8080/index")
1704+
.then (res) ->
1705+
expect(res.statusCode).to.eq(200)
1706+
1707+
it "does not modify existing auth headers when matching origin", ->
1708+
existing = "Basic asdf"
1709+
1710+
@server._remoteAuth = {
1711+
username: "u"
1712+
password: "p"
1713+
}
1714+
1715+
nock("http://localhost:8080")
1716+
.get("/index")
1717+
.matchHeader("authorization", existing)
1718+
.reply(200, "")
1719+
1720+
@rp({
1721+
url: "http://localhost:8080/index"
1722+
headers: {
1723+
"Authorization": existing
1724+
}
1725+
})
1726+
.then (res) ->
1727+
expect(res.statusCode).to.eq(200)
1728+
16761729
context "images", ->
16771730
beforeEach ->
16781731
Fixtures.scaffold()

0 commit comments

Comments
 (0)