-
Notifications
You must be signed in to change notification settings - Fork 9.3k
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: INSECURE_DOCUMENT_REQUEST errors still return lhr #6608
Changes from 16 commits
4c658fb
eb7f5a3
62787ee
3577e0c
2c0b251
e22d9ca
9e54f1b
059d95e
51e4f05
1ce9144
4bd59e4
53e00a7
1b6e678
dde5b95
4807cf0
2d505e8
1e1fc8a
0a0e8e0
b863fbd
da6fd9c
3c54326
ab0582b
f272978
e6a659a
271a80a
8bbcbc4
04b1f64
1da3b01
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 |
---|---|---|
|
@@ -16,6 +16,7 @@ const log = require('lighthouse-logger'); | |
|
||
const PROTOCOL_TIMEOUT_EXIT_CODE = 67; | ||
const PAGE_HUNG_EXIT_CODE = 68; | ||
const INSECRE_DOCUMENT_REQUEST_EXIT_CODE = 69; | ||
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. typo fyi 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. s/INSECRE/INSECURE |
||
const RETRIES = 3; | ||
const NUMERICAL_EXPECTATION_REGEXP = /^(<=?|>=?)((\d|\.)+)$/; | ||
|
||
|
@@ -88,7 +89,9 @@ function runLighthouse(url, configPath, isDebug) { | |
if (runResults.status === PROTOCOL_TIMEOUT_EXIT_CODE) { | ||
console.error(`Lighthouse debugger connection timed out ${RETRIES} times. Giving up.`); | ||
process.exit(1); | ||
} else if (runResults.status !== 0 && runResults.status !== PAGE_HUNG_EXIT_CODE) { | ||
} else if (runResults.status !== 0 | ||
&& runResults.status !== PAGE_HUNG_EXIT_CODE | ||
&& runResults.status !== INSECRE_DOCUMENT_REQUEST_EXIT_CODE) { | ||
console.error(`Lighthouse run failed with exit code ${runResults.status}. stderr to follow:`); | ||
console.error(runResults.stderr); | ||
process.exit(runResults.status); | ||
|
@@ -103,6 +106,10 @@ function runLighthouse(url, configPath, isDebug) { | |
return {requestedUrl: url, finalUrl: url, errorCode: 'PAGE_HUNG', audits: {}}; | ||
} | ||
|
||
if (runResults.status === INSECRE_DOCUMENT_REQUEST_EXIT_CODE) { | ||
return {requestedUrl: url, finalUrl: url, errorCode: 'INSECURE_DOCUMENT_REQUEST', audits: {}}; | ||
} | ||
|
||
const lhr = fs.readFileSync(outputPath, 'utf8'); | ||
if (isDebug) { | ||
console.log('LHR output available at: ', outputPath); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,8 @@ const DEFAULT_CPU_QUIET_THRESHOLD = 0; | |
// Controls how long to wait for a response after sending a DevTools protocol command. | ||
const DEFAULT_PROTOCOL_TIMEOUT = 30000; | ||
|
||
const SECURE_SCHEMES = ['data', 'https', 'wss', 'blob', 'chrome', 'chrome-extension', 'about']; | ||
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. remains to be seen if we need this here, but if we do, i'd much rather this array live in a common place than be duplicated here and in the is-on-https audit. update: yup, this stuff is unnecessary now, but FYI on the feedback. |
||
|
||
/** | ||
* @typedef {LH.Protocol.StrictEventEmitter<LH.CrdpEvents>} CrdpEventEmitter | ||
*/ | ||
|
@@ -81,13 +83,6 @@ class Driver { | |
this._eventEmitter.emit(event.method, event.params); | ||
}); | ||
|
||
/** | ||
* Used for monitoring network status events during gotoURL. | ||
* @type {?LH.Crdp.Security.SecurityStateChangedEvent} | ||
* @private | ||
*/ | ||
this._lastSecurityState = null; | ||
|
||
/** | ||
* @type {number} | ||
* @private | ||
|
@@ -512,6 +507,61 @@ class Driver { | |
}); | ||
} | ||
|
||
/** | ||
* Listener that resolves or rejects on the first interesting security state change. | ||
* If the first change is secure, we resolve. | ||
* Otherwise, we reject. | ||
* We can expect the security state to always change because this function | ||
* is only used to move about:blank (neutral) -> the target url (something not neutral). | ||
* @return {{promise: Promise<void>, cancel: function(): void}} | ||
* @private | ||
*/ | ||
_waitForSecurityCheck() { | ||
/** @type {(() => void)} */ | ||
let cancel = () => { | ||
throw new Error('_waitForSecurityCheck.cancel() called before it was defined'); | ||
}; | ||
|
||
const promise = new Promise(async (resolve, reject) => { | ||
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. no |
||
/** | ||
* @param {LH.Crdp.Security.SecurityStateChangedEvent} event | ||
*/ | ||
const securityStateChangedListener = ({securityState, explanations}) => { | ||
// ignore until there is something meaningful to derive security from | ||
if (securityState === 'neutral' && !explanations.length) { | ||
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. hmm, it looks like regular old http sites are |
||
return; | ||
} | ||
|
||
if (securityState === 'insecure') { | ||
cancel(); | ||
const insecureDescriptions = explanations | ||
.filter(exp => exp.securityState === 'insecure') | ||
.map(exp => exp.description); | ||
const err = new LHError(LHError.errors.INSECURE_DOCUMENT_REQUEST, { | ||
securityMessages: insecureDescriptions.join(' '), | ||
}); | ||
reject(err); | ||
} else { | ||
cancel(); | ||
resolve(); | ||
} | ||
}; | ||
|
||
this.on('Security.securityStateChanged', securityStateChangedListener); | ||
this.sendCommand('Security.enable').catch(() => {}); | ||
|
||
cancel = () => { | ||
this.off('Security.securityStateChanged', securityStateChangedListener); | ||
this.sendCommand('Security.disable').catch(() => {}); | ||
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. do we need to resolve/reject the promise in the cancelled case to prevent lingering events? it seems like we irregularly handle this (CPU timeout rejects, load timeout doesn't do either) 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. Sounds reasonable to reject it. Unclear if it would be considered an unhandled rejection.. I think not, because of the |
||
}; | ||
}); | ||
|
||
return { | ||
promise, | ||
cancel, | ||
}; | ||
} | ||
|
||
/** | ||
* Returns a promise that resolve when a frame has been navigated. | ||
* Used for detecting that our about:blank reset has been completed. | ||
|
@@ -723,6 +773,7 @@ class Driver { | |
/** | ||
* Returns a promise that resolves when: | ||
* - All of the following conditions have been met: | ||
* - page has no security issues | ||
* - pauseAfterLoadMs milliseconds have passed since the load event. | ||
* - networkQuietThresholdMs milliseconds have passed since the last network request that exceeded | ||
* 2 inflight requests (network-2-quiet has been reached). | ||
|
@@ -741,6 +792,14 @@ class Driver { | |
/** @type {NodeJS.Timer|undefined} */ | ||
let maxTimeoutHandle; | ||
|
||
// Noop if offline or not https | ||
// https: => https | ||
const protocol = new URL(this._monitoredUrl || '').protocol.replace(/:$/, ''); | ||
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. It's possible we can make this approach work, but I don't think it will work like this (correct me if I'm missing something).
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. Good call, Brendan. Example URL that redirects to insecure (but isn't caught currently): http://i.paul.irish |
||
const isSecureProtocol = SECURE_SCHEMES.includes(protocol); | ||
const waitForSecurityCheck = this.online && isSecureProtocol ? this._waitForSecurityCheck() : { | ||
promise: Promise.resolve(), | ||
cancel: () => {}, | ||
}; | ||
// Listener for onload. Resolves pauseAfterLoadMs ms after load. | ||
const waitForLoadEvent = this._waitForLoadEvent(pauseAfterLoadMs); | ||
// Network listener. Resolves when the network has been idle for networkQuietThresholdMs. | ||
|
@@ -752,6 +811,7 @@ class Driver { | |
// Wait for both load promises. Resolves on cleanup function the clears load | ||
// timeout timer. | ||
const loadPromise = Promise.all([ | ||
waitForSecurityCheck.promise, | ||
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. this makes me a tad more nervous I feel like we need some beefier commenting on why this is OK, intuitively I wouldn't expect a putting it directly in the chain of waiting for load just makes it a bit high stakes 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 it'd only work if the Also, isn't this method always called from "about:" -> "target url"? 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.
That's kinda what I'm talking about, just explaining why this is always going to be OK :) 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. Ah, OK I misunderstood. How's the comment I added? 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. nice! maybe we can add a sentence about how it I'm thinking something like // Listener that resolves or rejects on the first security state change.
// If the first change is secure, we resolve.
// If the first change is insecure, we reject.
// We can expect the security state to always change because this function
// is only used to move about:blank (neutral) -> the target url (something not neutral). Now that I'm thinking about it maybe it belongs in the |
||
waitForLoadEvent.promise, | ||
waitForNetworkIdle.promise, | ||
]).then(() => { | ||
|
@@ -771,6 +831,7 @@ class Driver { | |
}).then(_ => { | ||
return async () => { | ||
log.warn('Driver', 'Timed out waiting for page load. Checking if page is hung...'); | ||
waitForSecurityCheck.cancel(); | ||
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 it'd be easier to reason about all this canceling if each cancelable promise stores state about if it's been canceled yet and avoids doing it more than once. Then we could judiciously use 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. Yeah this is ripe for cleanup, but this new parallel style will make it so much easier now, thank you!! 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. FWIW, I don't think anything would be harmed today if you threw this into 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 also have to remove the self-cancel that I'll try the cancel state thing first 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. hang on before you do that I was saying I don't think it would matter if we called 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, LMK if that is better or worse |
||
waitForLoadEvent.cancel(); | ||
waitForNetworkIdle.cancel(); | ||
waitForCPUIdle && waitForCPUIdle.cancel(); | ||
|
@@ -959,26 +1020,6 @@ class Driver { | |
return result.body; | ||
} | ||
|
||
async listenForSecurityStateChanges() { | ||
this.on('Security.securityStateChanged', state => { | ||
this._lastSecurityState = state; | ||
}); | ||
await this.sendCommand('Security.enable'); | ||
} | ||
|
||
/** | ||
* @return {LH.Crdp.Security.SecurityStateChangedEvent} | ||
*/ | ||
getSecurityState() { | ||
if (!this._lastSecurityState) { | ||
// happens if 'listenForSecurityStateChanges' is not called, | ||
// or if some assumptions about the Security domain are wrong | ||
throw new Error('Expected a security state.'); | ||
} | ||
|
||
return this._lastSecurityState; | ||
} | ||
|
||
/** | ||
* @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 |
---|---|---|
|
@@ -22,7 +22,7 @@ const UIStrings = { | |
/** Error message explaining that Lighthouse could not load the requested URL and the steps that might be taken to fix the unreliability. */ | ||
pageLoadFailedWithDetails: 'Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests. (Details: {errorDetails})', | ||
/** Error message explaining that the credentials included in the Lighthouse run were invalid, so the URL cannot be accessed. */ | ||
pageLoadFailedInsecure: 'The URL you have provided does not have valid security credentials. ({securityMessages})', | ||
pageLoadFailedInsecure: 'The URL you have provided does not have valid security credentials. {securityMessages}', | ||
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. Any reason for removing these? I feel like a list that might make no sense as a sentence like this belongs in parens. e.g. The URL...credentials. (Reason 1 Reason 2) 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. A realistic error message reads like this:
Which is a valid sentence. So...ignore this maybe. 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. correct-o, the sentences should make sense as - is. 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. at least, in english .. 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 we need to add something about |
||
/** Error message explaining that Chrome has encountered an error during the Lighthouse run, and that Chrome should be restarted. */ | ||
internalChromeError: 'An internal Chrome error occurred. Please restart Chrome and try re-running Lighthouse.', | ||
/** Error message explaining that fetching the resources of the webpage has taken longer than the maximum time. */ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,10 @@ const redirectDevtoolsLog = require('../fixtures/wikipedia-redirect.devtoolslog. | |
const MAX_WAIT_FOR_PROTOCOL = 20; | ||
|
||
function createOnceStub(events) { | ||
return (eventName, cb) => { | ||
return async (eventName, cb) => { | ||
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. this is a weird use of async/await (and depending on Can we just // wait a tick b/c real events never fire immediately
setTimeout(_ => cb(events[eventName]), 0);
return; like normal people? :P 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 meant drop the async/await in favor of the setTimeout :) |
||
// wait a tick b/c real events never fire immediately | ||
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. ❤️ |
||
await Promise.resolve(); | ||
|
||
if (events[eventName]) { | ||
return cb(events[eventName]); | ||
} | ||
|
@@ -99,6 +102,8 @@ connection.sendCommand = function(command, params) { | |
case 'Tracing.start': | ||
case 'ServiceWorker.enable': | ||
case 'ServiceWorker.disable': | ||
case 'Security.enable': | ||
case 'Security.disable': | ||
case 'Network.setExtraHTTPHeaders': | ||
case 'Network.emulateNetworkConditions': | ||
case 'Emulation.setCPUThrottlingRate': | ||
|
@@ -221,6 +226,12 @@ describe('Browser Driver', () => { | |
} | ||
const replayConnection = new ReplayConnection(); | ||
const driver = new Driver(replayConnection); | ||
driver._waitForSecurityCheck = () => { | ||
return { | ||
promise: Promise.resolve(), | ||
cancel: () => {}, | ||
}; | ||
}; | ||
|
||
// Redirect in log will go through | ||
const startUrl = 'http://en.wikipedia.org/'; | ||
|
@@ -535,4 +546,48 @@ describe('Multiple tab check', () => { | |
}); | ||
}); | ||
}); | ||
|
||
describe('._waitForSecurityCheck', () => { | ||
it('does not reject when page is secure', async () => { | ||
const secureSecurityState = { | ||
securityState: 'secure', | ||
}; | ||
driverStub.on = createOnceStub({ | ||
'Security.securityStateChanged': secureSecurityState, | ||
}); | ||
await driverStub._waitForSecurityCheck().promise; | ||
}); | ||
|
||
it('rejects when page is insecure', async () => { | ||
const insecureSecurityState = { | ||
explanations: [ | ||
{ | ||
description: 'reason 1.', | ||
securityState: 'insecure', | ||
}, | ||
{ | ||
description: 'blah.', | ||
securityState: 'info', | ||
}, | ||
{ | ||
description: 'reason 2.', | ||
securityState: 'insecure', | ||
}, | ||
], | ||
securityState: 'insecure', | ||
}; | ||
driverStub.on = createOnceStub({ | ||
'Security.securityStateChanged': insecureSecurityState, | ||
}); | ||
try { | ||
await driverStub._waitForSecurityCheck().promise; | ||
assert.fail('_waitForSecurityCheck should have rejected'); | ||
} catch (err) { | ||
assert.equal(err.message, 'INSECURE_DOCUMENT_REQUEST'); | ||
assert.equal(err.code, 'INSECURE_DOCUMENT_REQUEST'); | ||
/* eslint-disable-next-line max-len */ | ||
expect(err.friendlyMessage).toBeDisplayString('The URL you have provided does not have valid security credentials. reason 1. reason 2.'); | ||
} | ||
}); | ||
}); | ||
}); |
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.
still
INSECRE
;)