-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
core: bail if encounter insecure ssl cert, to avoid hanging forever #6300
Changes from 5 commits
104e399
2eafa44
8790ebd
089147a
b2d7c42
e7c463d
25a9853
9f14eff
3a3015c
bb2ce13
ef5ac10
f3bb0bd
157e84a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -849,6 +849,24 @@ class Driver { | |
}); | ||
} | ||
|
||
/** | ||
* @param {number} [timeout] | ||
* @return {Promise<LH.Crdp.Security.SecurityStateChangedEvent>} | ||
*/ | ||
getSecurityState(timeout = 1000) { | ||
return new Promise((resolve, reject) => { | ||
const err = new LHError(LHError.errors.SECURITY_STATE_TIMEOUT); | ||
const asyncTimeout = setTimeout((_ => reject(err)), timeout); | ||
|
||
this.once('Security.securityStateChanged', state => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aside: does this check break if someone else has been listening to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I enabled just before calling this function, and yes, it does break. It times out + rejects with Besides other users enabling the Security domain via code, could this fail if a developer opens the Security tab (this enables the domain) before running an audit? |
||
clearTimeout(asyncTimeout); | ||
resolve(state); | ||
this.sendCommand('Security.disable'); | ||
}); | ||
this.sendCommand('Security.enable'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want this to be a general utility function, it will need to disable the domain and remove the event listener in the rejection case. Otherwise we'll need to think how to make rejections always lead to a program exit, as the state of things might get weird (we'll have to deal with the same thing in #6296) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe this is overkill. If it's timing out (unexpectedly), something is pretty wrong and LH probably won't recover. Disabling the domain also might not even work. OTOH, not cleaning up feels wrong :) |
||
}); | ||
} | ||
|
||
/** | ||
* @param {string} name The name of API whose permission you wish to query | ||
* @return {Promise<string>} The state of permissions, resolved in a promise. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -148,9 +148,10 @@ class GatherRunner { | |
* Returns an error if the original network request failed or wasn't found. | ||
* @param {string} url The URL of the original requested page. | ||
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords | ||
* @param {LH.Crdp.Security.SecurityStateChangedEvent} securityState | ||
* @return {LHError|undefined} | ||
*/ | ||
static getPageLoadError(url, networkRecords) { | ||
static getPageLoadError(url, networkRecords, {securityState, explanations}) { | ||
const mainRecord = networkRecords.find(record => { | ||
// record.url is actual request url, so needs to be compared without any URL fragment. | ||
return URL.equalWithExcludedFragments(record.url, url); | ||
|
@@ -165,6 +166,12 @@ class GatherRunner { | |
} else if (mainRecord.hasErrorStatusCode()) { | ||
errorDef = {...LHError.errors.ERRORED_DOCUMENT_REQUEST}; | ||
errorDef.message += ` Status code: ${mainRecord.statusCode}.`; | ||
} else if (securityState === 'insecure') { | ||
errorDef = {...LHError.errors.INSECURE_DOCUMENT_REQUEST}; | ||
const insecureDescriptions = explanations | ||
.filter(exp => exp.securityState === 'insecure') | ||
.map(exp => exp.description); | ||
errorDef.message += ` ${insecureDescriptions.join(' ')}`; | ||
} | ||
|
||
if (errorDef) { | ||
|
@@ -271,7 +278,9 @@ class GatherRunner { | |
const networkRecords = NetworkRecorder.recordsFromLogs(devtoolsLog); | ||
log.verbose('statusEnd', status); | ||
|
||
let pageLoadError = GatherRunner.getPageLoadError(passContext.url, networkRecords); | ||
const securityState = await driver.getSecurityState(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we're making this harder than it needs to be by trying to unify with with the other page load errors.
Maybe this should just be a separate function, This also gets around extending WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be great to utilize error throwing to simplify this. But, I can't quite figure out how to still display a LHR in
|
||
let pageLoadError = GatherRunner.getPageLoadError(passContext.url, | ||
networkRecords, securityState); | ||
// If the driver was offline, a page load error is expected, so do not save it. | ||
if (!driver.online) pageLoadError = undefined; | ||
|
||
|
@@ -286,10 +295,15 @@ class GatherRunner { | |
networkRecords, | ||
devtoolsLog, | ||
trace, | ||
pageLoadError, | ||
}; | ||
|
||
// Disable throttling so the afterPass analysis isn't throttled | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. restore? |
||
await driver.setThrottling(passContext.settings, {useThrottling: false}); | ||
if (!pageLoadError) { | ||
// Disable throttling so the afterPass analysis isn't throttled | ||
// This will hang if there was a security error. But, there is no | ||
// need to throttle if there is such an error. See #6287 | ||
await driver.setThrottling(passContext.settings, {useThrottling: false}); | ||
} | ||
|
||
for (const gathererDefn of gatherers) { | ||
const gatherer = gathererDefn.instance; | ||
|
@@ -435,6 +449,12 @@ class GatherRunner { | |
baseArtifacts.URL.finalUrl = passContext.url; | ||
firstPass = false; | ||
} | ||
|
||
const pageLoadError = passData.pageLoadError; | ||
if (pageLoadError && pageLoadError.code === LHError.errors.INSECURE_DOCUMENT_REQUEST.code) { | ||
// Some protocol commands will hang, so let's just bail. See #6287 | ||
break; | ||
} | ||
} | ||
const resetStorage = !options.settings.disableStorageReset; | ||
if (resetStorage) await driver.clearDataForOrigin(options.requestedUrl); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -142,6 +142,12 @@ const ERRORS = { | |
message: strings.pageLoadFailed, | ||
lhrRuntimeError: true, | ||
}, | ||
/* Used when security error prevents page load. */ | ||
INSECURE_DOCUMENT_REQUEST: { | ||
code: 'INSECURE_DOCUMENT_REQUEST', | ||
message: strings.pageLoadFailedInsecure, | ||
lhrRuntimeError: true, | ||
}, | ||
|
||
// Protocol internal failures | ||
TRACING_ALREADY_STARTED: { | ||
|
@@ -169,6 +175,12 @@ const ERRORS = { | |
message: strings.requestContentTimeout, | ||
}, | ||
|
||
// Protocol timeout failures | ||
SECURITY_STATE_TIMEOUT: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in the usual case for the security state check, it should just reject on an insecure security state, right? If so, we probably want to make this a more general protocol communication timeout error (anticipating #6296) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My plan was to modify that bit in the referenced issue. but now that we have this concrete proto definition it makes sense to make it good sooner rather than later There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I actually removed the timeout stuff for security checking. I'll remove this too. |
||
code: 'SECURITY_STATE_TIMEOUT', | ||
message: strings.securityStateTimeout, | ||
}, | ||
|
||
// URL parsing failures | ||
INVALID_URL: { | ||
code: 'INVALID_URL', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,10 @@ class TestGathererNoArtifact extends Gatherer { | |
|
||
const fakeDriver = require('./fake-driver'); | ||
|
||
const secureSecurityState = { | ||
securityState: 'secure', | ||
}; | ||
|
||
function getMockedEmulationDriver(emulationFn, netThrottleFn, cpuThrottleFn, | ||
blockUrlFn, extraHeadersFn) { | ||
const Driver = require('../../gather/driver'); | ||
|
@@ -543,9 +547,9 @@ describe('GatherRunner', function() { | |
], | ||
}; | ||
|
||
return GatherRunner.afterPass({url, driver, passConfig}, {TestGatherer: []}).then(vals => { | ||
return GatherRunner.afterPass({url, driver, passConfig}, {TestGatherer: []}).then(passData => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok to revert these lines now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
assert.equal(calledDevtoolsLogCollect, true); | ||
assert.strictEqual(vals.devtoolsLog[0], fakeDevtoolsMessage); | ||
assert.strictEqual(passData.devtoolsLog[0], fakeDevtoolsMessage); | ||
}); | ||
}); | ||
|
||
|
@@ -629,14 +633,14 @@ describe('GatherRunner', function() { | |
const url = 'http://the-page.com'; | ||
const mainRecord = new NetworkRequest(); | ||
mainRecord.url = url; | ||
assert.ok(!GatherRunner.getPageLoadError(url, [mainRecord])); | ||
assert.ok(!GatherRunner.getPageLoadError(url, [mainRecord], secureSecurityState)); | ||
}); | ||
|
||
it('passes when the page is loaded, ignoring any fragment', () => { | ||
const url = 'http://example.com/#/page/list'; | ||
const mainRecord = new NetworkRequest(); | ||
mainRecord.url = 'http://example.com'; | ||
assert.ok(!GatherRunner.getPageLoadError(url, [mainRecord])); | ||
assert.ok(!GatherRunner.getPageLoadError(url, [mainRecord], secureSecurityState)); | ||
}); | ||
|
||
it('fails when page fails to load', () => { | ||
|
@@ -645,15 +649,15 @@ describe('GatherRunner', function() { | |
mainRecord.url = url; | ||
mainRecord.failed = true; | ||
mainRecord.localizedFailDescription = 'foobar'; | ||
const error = GatherRunner.getPageLoadError(url, [mainRecord]); | ||
const error = GatherRunner.getPageLoadError(url, [mainRecord], secureSecurityState); | ||
assert.equal(error.message, 'FAILED_DOCUMENT_REQUEST'); | ||
assert.ok(/^Lighthouse was unable to reliably load/.test(error.friendlyMessage)); | ||
}); | ||
|
||
it('fails when page times out', () => { | ||
const url = 'http://the-page.com'; | ||
const records = []; | ||
const error = GatherRunner.getPageLoadError(url, records); | ||
const error = GatherRunner.getPageLoadError(url, records, secureSecurityState); | ||
assert.equal(error.message, 'NO_DOCUMENT_REQUEST'); | ||
assert.ok(/^Lighthouse was unable to reliably load/.test(error.friendlyMessage)); | ||
}); | ||
|
@@ -663,7 +667,7 @@ describe('GatherRunner', function() { | |
const mainRecord = new NetworkRequest(); | ||
mainRecord.url = url; | ||
mainRecord.statusCode = 404; | ||
const error = GatherRunner.getPageLoadError(url, [mainRecord]); | ||
const error = GatherRunner.getPageLoadError(url, [mainRecord], secureSecurityState); | ||
assert.equal(error.message, 'ERRORED_DOCUMENT_REQUEST'); | ||
assert.ok(/^Lighthouse was unable to reliably load/.test(error.friendlyMessage)); | ||
}); | ||
|
@@ -673,10 +677,37 @@ describe('GatherRunner', function() { | |
const mainRecord = new NetworkRequest(); | ||
mainRecord.url = url; | ||
mainRecord.statusCode = 500; | ||
const error = GatherRunner.getPageLoadError(url, [mainRecord]); | ||
const error = GatherRunner.getPageLoadError(url, [mainRecord], secureSecurityState); | ||
assert.equal(error.message, 'ERRORED_DOCUMENT_REQUEST'); | ||
assert.ok(/^Lighthouse was unable to reliably load/.test(error.friendlyMessage)); | ||
}); | ||
|
||
it('fails when page is insecure', () => { | ||
const insecureSecurityState = { | ||
explanations: [ | ||
{ | ||
description: 'reason 1.', | ||
securityState: 'insecure', | ||
}, | ||
{ | ||
description: 'blah.', | ||
securityState: 'info', | ||
}, | ||
{ | ||
description: 'reason 2.', | ||
securityState: 'insecure', | ||
}, | ||
], | ||
securityState: 'insecure', | ||
}; | ||
const url = 'http://the-page.com'; | ||
const mainRecord = new NetworkRequest(); | ||
mainRecord.url = url; | ||
const error = GatherRunner.getPageLoadError(url, [mainRecord], insecureSecurityState); | ||
assert.equal(error.message, 'INSECURE_DOCUMENT_REQUEST'); | ||
/* eslint-disable-next-line max-len */ | ||
assert.equal(error.friendlyMessage, 'The URL you have provided does not have valid security credentials. reason 1. reason 2.'); | ||
}); | ||
}); | ||
|
||
describe('artifact collection', () => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will eventually be replaced by #6296