diff --git a/lighthouse-cli/cli-flags.js b/lighthouse-cli/cli-flags.js index 61bcdccfcc6f..f1d2ee963fee 100644 --- a/lighthouse-cli/cli-flags.js +++ b/lighthouse-cli/cli-flags.js @@ -105,6 +105,7 @@ function getFlags(manualArgv) { 'skip-audits': 'Run everything except these audits', 'plugins': 'Run the specified plugins', 'print-config': 'Print the normalized config for the given config and options, then exit.', + 'ignore-https-errors': 'Whether to ignore HTTPS errors during navigation. Defaults to false', }) // set aliases .alias({'gather-mode': 'G', 'audit-mode': 'A'}) @@ -123,7 +124,7 @@ function getFlags(manualArgv) { // boolean values .boolean([ 'disable-storage-reset', 'save-assets', 'list-all-audits', - 'list-trace-categories', 'view', 'verbose', 'quiet', 'help', 'print-config', + 'list-trace-categories', 'view', 'verbose', 'quiet', 'help', 'print-config', 'ignore-https-errors', ]) .choices('output', printer.getValidOutputOptions()) .choices('emulated-form-factor', ['mobile', 'desktop', 'none']) diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap index 3e2964fdfbe9..e7ecf36d61ff 100644 --- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap +++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap @@ -1203,6 +1203,7 @@ Object { "emulatedFormFactor": "mobile", "extraHeaders": null, "gatherMode": false, + "ignoreHttpsErrors": false, "locale": "en-US", "maxWaitForFcp": 15000, "maxWaitForLoad": 45000, @@ -1332,6 +1333,7 @@ Object { "emulatedFormFactor": "mobile", "extraHeaders": null, "gatherMode": false, + "ignoreHttpsErrors": false, "locale": "en-US", "maxWaitForFcp": 15000, "maxWaitForLoad": 45000, diff --git a/lighthouse-core/config/constants.js b/lighthouse-core/config/constants.js index 331975751ece..14da70deef3d 100644 --- a/lighthouse-core/config/constants.js +++ b/lighthouse-core/config/constants.js @@ -63,6 +63,7 @@ const defaultSettings = { onlyAudits: null, onlyCategories: null, skipAudits: null, + ignoreHttpsErrors: false, }; /** @type {LH.Config.Pass} */ diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js index 61e97ff8ea0e..f08e0a0824fa 100644 --- a/lighthouse-core/gather/driver.js +++ b/lighthouse-core/gather/driver.js @@ -658,6 +658,22 @@ class Driver { return {promise: Promise.resolve(), cancel() {}}; } + /** + * Returns a promise that never resolves. + * Used for placeholder conditions that we don't want race with, but still want to satisfy the same + * interface. + * @return {{promise: Promise, cancel: function(): void}} + */ + _waitForever() { + let cancel = () => {}; + return { + promise: new Promise(resolve => { + cancel = resolve; + }), + cancel, + }; + } + /** * Returns a promise that resolve when a frame has been navigated. * Used for detecting that our about:blank reset has been completed. @@ -977,11 +993,12 @@ class Driver { * @param {number} cpuQuietThresholdMs * @param {number} maxWaitForLoadedMs * @param {number=} maxWaitForFCPMs + * @param {boolean} ignoreHttpsErrors * @return {Promise} * @private */ async _waitForFullyLoaded(pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs, - maxWaitForLoadedMs, maxWaitForFCPMs) { + maxWaitForLoadedMs, maxWaitForFCPMs, ignoreHttpsErrors) { /** @type {NodeJS.Timer|undefined} */ let maxTimeoutHandle; @@ -994,10 +1011,15 @@ class Driver { // CPU listener. Resolves when the CPU has been idle for cpuQuietThresholdMs after network idle. let waitForCPUIdle = this._waitForNothing(); - const monitorForInsecureState = this._monitorForInsecureState(); + /** @type {{promise: Promise, cancel: () => void}} */ + const monitorForInsecureState = ignoreHttpsErrors + ? this._waitForever() + : this._monitorForInsecureState(); const securityCheckPromise = monitorForInsecureState.promise.then(securityMessages => { return function() { - throw new LHError(LHError.errors.INSECURE_DOCUMENT_REQUEST, {securityMessages}); + throw new LHError(LHError.errors.INSECURE_DOCUMENT_REQUEST, { + securityMessages: securityMessages || '', + }); }; }); @@ -1136,7 +1158,7 @@ class Driver { * possible workaround. * Resolves on the url of the loaded page, taking into account any redirects. * @param {string} url - * @param {{waitForFCP?: boolean, waitForLoad?: boolean, waitForNavigated?: boolean, passContext?: LH.Gatherer.PassContext}} options + * @param {{waitForFCP?: boolean, waitForLoad?: boolean, waitForNavigated?: boolean, passContext?: LH.Gatherer.PassContext, ignoreHttpsErrors?: boolean}} options * @return {Promise} */ async gotoURL(url, options = {}) { @@ -1145,6 +1167,7 @@ class Driver { const waitForLoad = options.waitForLoad || false; const passContext = /** @type {Partial} */ (options.passContext || {}); const disableJS = passContext.disableJavaScript || false; + const ignoreHttpsErrors = options.ignoreHttpsErrors || false; if (waitForNavigated && (waitForFCP || waitForLoad)) { throw new Error('Cannot use both waitForNavigated and another event, pick just one'); @@ -1184,7 +1207,7 @@ class Driver { if (!waitForFCP) maxFCPMs = undefined; await this._waitForFullyLoaded(pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs, - maxWaitMs, maxFCPMs); + maxWaitMs, maxFCPMs, ignoreHttpsErrors); } // Bring `Page.navigate` errors back into the promise chain. See https://github.com/GoogleChrome/lighthouse/pull/6739. @@ -1565,6 +1588,24 @@ class Driver { await this.sendCommand('Page.enable'); } + + /** + * Set chrome security settings to ignore certificate errors. + * @return {Promise} + */ + async setIgnoreHttpsErrors() { + // the devtools protocol also has an experimental Security.setIgnoreCertificateErrors + // https://chromedevtools.github.io/devtools-protocol/tot/Security#method-setIgnoreCertificateErrors + // It can be used instead of the following when it becomes stable. + this.on('Security.certificateError', event => { + this.sendCommand('Security.handleCertificateError', { + eventId: event.eventId, + action: 'continue', + }).catch(err => log.warn('Driver', err)); + }); + await this.sendCommand('Security.enable'); + await this.sendCommand('Security.setOverrideCertificateErrors', {override: true}); + } } module.exports = Driver; diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index 033a9a8bf317..8af369db32a9 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -92,6 +92,7 @@ class GatherRunner { const finalUrl = await driver.gotoURL(passContext.url, { waitForFCP: passContext.passConfig.recordTrace, waitForLoad: true, + ignoreHttpsErrors: passContext.settings.ignoreHttpsErrors, passContext, }); passContext.url = finalUrl; @@ -106,12 +107,14 @@ class GatherRunner { const status = {msg: 'Initializing…', id: 'lh:gather:setupDriver'}; log.time(status); const resetStorage = !options.settings.disableStorageReset; + const ignoreHttpsErrors = options.settings.ignoreHttpsErrors; await driver.assertNoSameOriginServiceWorkerClients(options.requestedUrl); await driver.beginEmulation(options.settings); await driver.enableRuntimeEvents(); await driver.cacheNatives(); await driver.registerPerformanceObserver(); await driver.dismissJavaScriptDialogs(); + if (ignoreHttpsErrors) await driver.setIgnoreHttpsErrors(); if (resetStorage) await driver.clearDataForOrigin(options.requestedUrl); log.timeEnd(status); } diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json index b9e493af335c..e7c9117c0392 100644 --- a/lighthouse-core/test/results/sample_v2.json +++ b/lighthouse-core/test/results/sample_v2.json @@ -2970,7 +2970,8 @@ "precomputedLanternData": null, "onlyAudits": null, "onlyCategories": null, - "skipAudits": null + "skipAudits": null, + "ignoreHttpsErrors": false }, "categories": { "performance": { diff --git a/types/externs.d.ts b/types/externs.d.ts index 8de82bbb0252..f2832090055a 100644 --- a/types/externs.d.ts +++ b/types/externs.d.ts @@ -119,6 +119,8 @@ declare global { channel?: string /** Precomputed lantern estimates to use instead of observed analysis. */ precomputedLanternData?: PrecomputedLanternData | null; + /** Ignore HTTPS errors during navigation */ + ignoreHttpsErrors?: boolean; } /**