Skip to content
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

Merged
merged 28 commits into from
Jan 11, 2019
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4c658fb
INSECURE_DOCUMENT_REQUEST errors still return lhr (#6595)
connorjclark Nov 19, 2018
eb7f5a3
fix comment. remove dead code
connorjclark Nov 19, 2018
62787ee
Merge branch 'master' into issue-6595-insecure
connorjclark Dec 18, 2018
3577e0c
move security check to wait for page load
connorjclark Dec 19, 2018
2c0b251
remove finally for timing
connorjclark Dec 19, 2018
e22d9ca
set cancel
connorjclark Dec 20, 2018
9e54f1b
update test name
connorjclark Dec 20, 2018
059d95e
tweak security logic
connorjclark Dec 20, 2018
51e4f05
fix test
connorjclark Dec 20, 2018
1ce9144
add smoke test, other stuff
connorjclark Dec 20, 2018
4bd59e4
fix offline case for security check
connorjclark Dec 20, 2018
53e00a7
skip security check when offline
connorjclark Dec 20, 2018
1b6e678
skip security check if localhost
connorjclark Dec 20, 2018
dde5b95
Merge branch 'master' into issue-6595-insecure
connorjclark Jan 8, 2019
4807cf0
only do security check if online and https
connorjclark Jan 8, 2019
2d505e8
consider all secure schemes
connorjclark Jan 8, 2019
1e1fc8a
move security check to promise race. only rejects now.
connorjclark Jan 8, 2019
0a0e8e0
fix typo
connorjclark Jan 8, 2019
b863fbd
Merge remote-tracking branch 'origin/master' into issue-6595-insecure
connorjclark Jan 8, 2019
da6fd9c
Merge remote-tracking branch 'origin/master' into issue-6595-insecure
connorjclark Jan 8, 2019
3c54326
fix typo, add comment
connorjclark Jan 9, 2019
ab0582b
make security check cancelable promise again
connorjclark Jan 9, 2019
f272978
simplify cancels
connorjclark Jan 9, 2019
e6a659a
fix cancel bug
connorjclark Jan 10, 2019
271a80a
move security error creation
connorjclark Jan 10, 2019
8bbcbc4
cr changes
connorjclark Jan 10, 2019
04b1f64
create driver for each test
connorjclark Jan 10, 2019
1da3b01
cr changes
connorjclark Jan 11, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lighthouse-cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const opn = require('opn');
const _RUNTIME_ERROR_CODE = 1;
const _PROTOCOL_TIMEOUT_EXIT_CODE = 67;
const _PAGE_HUNG_EXIT_CODE = 68;
const _INSECRE_DOCUMENT_REQUEST_EXIT_CODE = 69;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still INSECRE ;)


/**
* exported for testing
Expand Down Expand Up @@ -77,6 +78,12 @@ function showPageHungError(err) {
process.exit(_PAGE_HUNG_EXIT_CODE);
}

/** @param {LH.LighthouseError} err */
function showInsecureDocumentRequestError(err) {
console.error('Insecure document request:', err.friendlyMessage);
process.exit(_INSECRE_DOCUMENT_REQUEST_EXIT_CODE);
}

/**
* @param {LH.LighthouseError} err
*/
Expand All @@ -98,6 +105,8 @@ function handleError(err) {
showProtocolTimeoutError();
} else if (err.code === 'PAGE_HUNG') {
showPageHungError(err);
} else if (err.code === 'INSECURE_DOCUMENT_REQUEST') {
showInsecureDocumentRequestError(err);
} else {
showRuntimeError(err);
}
Expand Down
6 changes: 6 additions & 0 deletions lighthouse-cli/test/smokehouse/error-expectations.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ module.exports = [
errorCode: 'PAGE_HUNG',
audits: {},
},
{
requestedUrl: 'https://expired.badssl.com',
finalUrl: 'https://expired.badssl.com',
errorCode: 'INSECURE_DOCUMENT_REQUEST',
audits: {},
},
];
9 changes: 8 additions & 1 deletion lighthouse-cli/test/smokehouse/smokehouse.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const log = require('lighthouse-logger');

const PROTOCOL_TIMEOUT_EXIT_CODE = 67;
const PAGE_HUNG_EXIT_CODE = 68;
const INSECURE_DOCUMENT_REQUEST_EXIT_CODE = 69;
const RETRIES = 3;
const NUMERICAL_EXPECTATION_REGEXP = /^(<=?|>=?)((\d|\.)+)$/;

Expand Down Expand Up @@ -103,7 +104,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 !== INSECURE_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);
Expand All @@ -118,6 +121,10 @@ function runLighthouse(url, configPath, isDebug) {
return {requestedUrl: url, finalUrl: url, errorCode: 'PAGE_HUNG', audits: {}};
}

if (runResults.status === INSECURE_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);
Expand Down
69 changes: 38 additions & 31 deletions lighthouse-core/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,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
Expand Down Expand Up @@ -765,6 +758,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).
Expand Down Expand Up @@ -792,6 +786,40 @@ class Driver {
const waitForNetworkIdle = this._waitForNetworkIdle(networkQuietThresholdMs);
// CPU listener. Resolves when the CPU has been idle for cpuQuietThresholdMs after network idle.
let waitForCPUIdle = this._waitForNothing();
const cancelAll = () => {
waitForFCP.cancel();
waitForLoadEvent.cancel();
waitForNetworkIdle.cancel();
waitForCPUIdle.cancel();
};

// Promise that only rejects when an insecure security state is encountered
let securityCheckCleanup = () => {};
const securityCheckPromise = new Promise((_, reject) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a lot to unpack in this function that needs longstanding cleanup I'll take a whack at as a followup to #6944, for now WDYT about sticking this in a _waitForSecurityError or something following the {promise, cancel} paradigm we have currently?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cancelAll and clearing the maxTimeoutHandle complicates that a lot. It's not clear to me how to extract into a function without using multiple parameter callbacks (or is that the cleanup you had in mind?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this goes hand-in-hand with the resolve idea. I'm not sure we could easily pull it out with it still rejecting immediately

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think I see how that'd work. I'll give it a shot

/**
* @param {LH.Crdp.Security.SecurityStateChangedEvent} event
*/
const securityStateChangedListener = ({securityState, explanations}) => {
if (securityState === 'insecure') {
maxTimeoutHandle && clearTimeout(maxTimeoutHandle);
securityCheckCleanup();
cancelAll();
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);
}
};
securityCheckCleanup = () => {
this.off('Security.securityStateChanged', securityStateChangedListener);
this.sendCommand('Security.disable').catch(() => {});
};
this.on('Security.securityStateChanged', securityStateChangedListener);
this.sendCommand('Security.enable').catch(() => {});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha, I think I disagree with @patrickhulce. The internals of SecurityStateExplanation seem like a distraction from the actual purpose of _waitForFullyLoaded, since either way we're using waitForSecurityCheck to alert us of an error, the question is if we should do more work in there or out here.

If that's not convincing, maybe we could split the difference? Filter and map within _waitForSecurityCheck and then it only needs to be

const securityCheckPromise = waitForSecurityCheck.promise.then(securityMessages => {
  return function() {
    throw new new LHError(LHError.errors.INSECURE_DOCUMENT_REQUEST, {securityMessages});
  }
});

(using the cleanupFn to reject the promise is a little weird but w/e :P)


// Wait for both load promises. Resolves on cleanup function the clears load
// timeout timer.
Expand All @@ -816,10 +844,7 @@ class Driver {
}).then(_ => {
return async () => {
log.warn('Driver', 'Timed out waiting for page load. Checking if page is hung...');
waitForFCP.cancel();
waitForLoadEvent.cancel();
waitForNetworkIdle.cancel();
waitForCPUIdle.cancel();
cancelAll();

if (await this.isPageHung()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW for future refactoring, @paulirish mentioned he doesn't think this check is necessary anymore (errors are successfully being detected elsewhere, I guess)

log.warn('Driver', 'Page appears to be hung, killing JavaScript...');
Expand All @@ -832,9 +857,11 @@ class Driver {

// Wait for load or timeout and run the cleanup function the winner returns.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edit this comment to note that securityCheckPromise will only ever reject?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OR crazy idea, the promise resolves with a cleanupFn that throws an error :D then the three cases are a little more parallel

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to your second comment, so securityCheckPromise would be changed to resolve instead of reject?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to your second comment, so securityCheckPromise would be changed to resolve instead of reject?

right it would go hand-in-hand with #6608 (comment) to enable to refactor

const cleanupFn = await Promise.race([
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having this cleanupFn setup now is weird if they all can (and should) be cancelled regardless, but that can wait for the refactor too

securityCheckPromise,
loadPromise,
maxTimeoutPromise,
]);
securityCheckCleanup();
await cleanupFn();
}

Expand Down Expand Up @@ -1007,26 +1034,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.
Expand Down
20 changes: 0 additions & 20 deletions lighthouse-core/gather/gather-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ class GatherRunner {
await driver.cacheNatives();
await driver.registerPerformanceObserver();
await driver.dismissJavaScriptDialogs();
await driver.listenForSecurityStateChanges();
if (resetStorage) await driver.clearDataForOrigin(options.requestedUrl);
log.timeEnd(status);
}
Expand Down Expand Up @@ -173,23 +172,6 @@ class GatherRunner {
}
}

/**
* Throws an error if the security state is insecure.
* @param {LH.Crdp.Security.SecurityStateChangedEvent} securityState
* @throws {LHError}
*/
static assertNoSecurityIssues({securityState, explanations}) {
if (securityState === 'insecure') {
const insecureDescriptions = explanations
.filter(exp => exp.securityState === 'insecure')
.map(exp => exp.description);
throw new LHError(
LHError.errors.INSECURE_DOCUMENT_REQUEST,
{securityMessages: insecureDescriptions.join(' ')}
);
}
}

/**
* Calls beforePass() on gatherers before tracing
* has started and before navigation to the target page.
Expand Down Expand Up @@ -312,8 +294,6 @@ class GatherRunner {
const networkRecords = NetworkRecorder.recordsFromLogs(devtoolsLog);
log.timeEnd(status);

this.assertNoSecurityIssues(driver.getSecurityState());

let pageLoadError = GatherRunner.getPageLoadError(passContext.url, networkRecords);
// If the driver was offline, a page load error is expected, so do not save it.
if (!driver.online) pageLoadError = undefined;
Expand Down
2 changes: 1 addition & 1 deletion lighthouse-core/lib/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,7 @@
"description": "Error message explaining that Lighthouse couldn't complete because the page has stopped responding to its instructions."
},
"lighthouse-core/lib/lh-error.js | pageLoadFailedInsecure": {
"message": "The URL you have provided does not have valid security credentials. ({securityMessages})",
"message": "The URL you have provided does not have valid security credentials. {securityMessages}",
"description": "Error message explaining that the credentials included in the Lighthouse run were invalid, so the URL cannot be accessed."
},
"lighthouse-core/lib/lh-error.js | pageLoadFailedWithDetails": {
Expand Down
2 changes: 1 addition & 1 deletion lighthouse-core/lib/lh-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
Copy link
Member

Choose a reason for hiding this comment

The 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)

Copy link
Member

@exterkamp exterkamp Dec 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A realistic error message reads like this:

The URL you have provided does not have valid security credentials. This site is missing a valid, trusted certificate (net::ERR_CERT_DATE_INVALID).

Which is a valid sentence. So...ignore this maybe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct-o, the sentences should make sense as - is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at least, in english ..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to add something about securityMessages to the description for the translators. "securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load." or whatever

/** 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. */
Expand Down
81 changes: 80 additions & 1 deletion lighthouse-core/test/gather/driver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a weird use of async/await (and depending on driver.once calls you'll end up with an unhandled promise rejection for the throw below).

Can we just

// wait a tick b/c real events never fire immediately
setTimeout(_ => cb(events[eventName]), 0);
return;

like normal people? :P

Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

await Promise.resolve();

if (events[eventName]) {
return cb(events[eventName]);
}
Expand Down Expand Up @@ -95,13 +98,18 @@ connection.sendCommand = function(command, params) {
case 'Network.getResponseBody':
return new Promise(res => setTimeout(res, MAX_WAIT_FOR_PROTOCOL + 20));
case 'Page.enable':
case 'Page.navigate':
case 'Page.setLifecycleEventsEnabled':
case 'Network.enable':
case 'Tracing.start':
case 'ServiceWorker.enable':
case 'ServiceWorker.disable':
case 'Security.enable':
case 'Security.disable':
case 'Network.setExtraHTTPHeaders':
case 'Network.emulateNetworkConditions':
case 'Emulation.setCPUThrottlingRate':
case 'Emulation.setScriptExecutionDisabled':
return Promise.resolve({});
case 'Tracing.end':
return Promise.reject(new Error('tracing not started'));
Expand Down Expand Up @@ -535,4 +543,75 @@ describe('Multiple tab check', () => {
});
});
});

describe('Security check', () => {
it('does not reject when page is secure', async () => {
const secureSecurityState = {
explanations: [],
securityState: 'secure',
};
driverStub.on = driverStub.once = createOnceStub({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this poor driverStub was never meant to be used repeatedly :)

Can you also break the cycle and make a new Driver(connection) within each of these tests? We can deal with having a fresh connection later

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I'm also rather fond of keeping all the mocking close to the test like I did over in manifest PR with jest.spyOn. Given the way the past few reviews have gone I'm guessing @brendankenny feels the opposite though so take it with a grain of salt 😂

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the way the past few reviews have gone I'm guessing @brendankenny feels the opposite

Ha, I'm not entirely sure how to interpret that. I also like maximizing locality so you can see what's actually been done to the mock you have to use. But I am generally against mocks if at all possible, if that's what that means :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also like maximizing locality

phew! :)

Ha, I'm not entirely sure how to interpret that.

only a joke no worries, just I feel like I've lead @hoten chasing too many wild geese lately 😆

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only a joke no worries

lol I was trying to read too deeply :) :)

'Security.securityStateChanged': secureSecurityState,
'Page.loadEventFired': {},
'Page.domContentEventFired': {},
});

const startUrl = 'https://www.example.com';
const loadOptions = {
waitForLoad: true,
passContext: {
passConfig: {
networkQuietThresholdMs: 1,
},
settings: {
maxWaitForLoad: 1,
},
},
};
await driverStub.gotoURL(startUrl, loadOptions);
});

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,
});

const startUrl = 'https://www.example.com';
const loadOptions = {
waitForLoad: true,
passContext: {
passConfig: {
networkQuietThresholdMs: 1,
},
},
};

try {
await driverStub.gotoURL(startUrl, loadOptions);
assert.fail('security check 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.');
}
});
});
});
8 changes: 0 additions & 8 deletions lighthouse-core/test/gather/fake-driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,6 @@ const fakeDriver = {
setExtraHTTPHeaders() {
return Promise.resolve();
},
listenForSecurityStateChanges() {
return Promise.resolve();
},
getSecurityState() {
return Promise.resolve({
securityState: 'secure',
});
},
};

module.exports = fakeDriver;
Expand Down
Loading