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 5 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
79 changes: 52 additions & 27 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 @@ -512,6 +505,53 @@ class Driver {
});
}

/**
* Rejects if the security state is insecure.
* @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) => {
Copy link
Member

Choose a reason for hiding this comment

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

no async here

/**
* @param {LH.Crdp.Security.SecurityStateChangedEvent} event
*/
const securityStateChangedListener = ({securityState, explanations}) => {
this.sendCommand('Security.disable');
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we need this in a promise chain like @exterkamp's cleanup?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do you mean adding an await here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

or an empty .catch if we don't care?

if (securityState === 'insecure') {
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 {
resolve();
}
};

// wait for Security.enable to resolve, so we skip whatever state change
// events are in the pipeline
await this.sendCommand('Security.enable');
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I can't really explain it, but this only works when awaiting the Security.enable ...

Copy link
Collaborator

Choose a reason for hiding this comment

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

do we have some tests/a test that can capture the cases that fail without this await but succeed with?

Copy link
Collaborator

Choose a reason for hiding this comment

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

this also seems a bit like it has the potential to race :/

Copy link
Collaborator Author

@connorjclark connorjclark Dec 20, 2018

Choose a reason for hiding this comment

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

The test would basically be 1) go to https://expired.badssl.com/ (or load page with bad cert) 2) assert LH does not hang / exercise the full page load time out (so, within 5s??)

would that be a good candidate for a smoke test?

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, it looks like it starts with the "neutral" security state (about: has no meaningful value for security) and then either goes to "secure" or "insecure", depending on the target page. So I can tweak the logic to listen for state changes until "not neutral" comes up

Copy link
Collaborator

Choose a reason for hiding this comment

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

would that be a good candidate for a smoke test?

sounds great to me! who maintains badssl? do we expect it to be roughly up most of the time? oh we do, awesome yeah let's do it :D https://github.com/chromium/badssl.com

Copy link
Member

Choose a reason for hiding this comment

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

Just https://localhost:10200 could also work

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If in offline mode, the security state is always "neutral" ...

This is me running the pwa smoke test. First two security states are the online pass (neutral -> secure), last three(?) are the offline pass (neutral -> neutral... but with explanations)
image

But in each case, the first state we care about is the first one with non-empty explanations. So maybe use that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Which makes sense... if there's no explanation for a security state, why value it?

Copy link
Collaborator Author

@connorjclark connorjclark Dec 20, 2018

Choose a reason for hiding this comment

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

For some reason, some offline urls gives three empty, neutral states in the offline pass. Perhaps better to just disable this check for the offline pass.

this.once('Security.securityStateChanged', securityStateChangedListener);

cancel = () => {
this.off('Security.securityStateChanged', securityStateChangedListener);
this.sendCommand('Security.disable');
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here re: floating promise

};
});

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.
Expand Down Expand Up @@ -741,6 +781,9 @@ class Driver {
/** @type {NodeJS.Timer|undefined} */
let maxTimeoutHandle;


// Listener for security state change. Rejects if security issue is found.
const waitForSecurityCheck = this._waitForSecurityCheck();
// Listener for onload. Resolves pauseAfterLoadMs ms after load.
const waitForLoadEvent = this._waitForLoadEvent(pauseAfterLoadMs);
// Network listener. Resolves when the network has been idle for networkQuietThresholdMs.
Expand All @@ -752,6 +795,7 @@ class Driver {
// Wait for both load promises. Resolves on cleanup function the clears load
// timeout timer.
const loadPromise = Promise.all([
waitForSecurityCheck.promise,
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 securityStateChanged to be guaranteed to fire early on every single page load, i.e. why would it fire if I go from http to http or https to https or about: to about:, etc

putting it directly in the chain of waiting for load just makes it a bit high stakes

Copy link
Collaborator Author

@connorjclark connorjclark Dec 20, 2018

Choose a reason for hiding this comment

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

I think it'd only work if the Security domain is disabled before _waitForFullyLoaded runs. If a gatherer were to enable Security and forget to disable it, this may break?

Also, isn't this method always called from "about:" -> "target url"?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, isn't this method always called from "about:" -> "target url"?

That's kinda what I'm talking about, just explaining why this is always going to be OK :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, OK I misunderstood. How's the comment I added?

Copy link
Collaborator

Choose a reason for hiding this comment

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

nice! maybe we can add a sentence about how it resolves too not just reject?

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 _waitForSecurityCheck jsdoc :)

waitForLoadEvent.promise,
waitForNetworkIdle.promise,
]).then(() => {
Expand All @@ -771,6 +815,7 @@ class Driver {
}).then(_ => {
return async () => {
log.warn('Driver', 'Timed out waiting for page load. Checking if page is hung...');
waitForSecurityCheck.cancel();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 cancelAll in one place, after the Promise race.

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 is ripe for cleanup, but this new parallel style will make it so much easier now, thank you!!

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 don't think anything would be harmed today if you threw this into cancelAll right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

would also have to remove the self-cancel that _waitForSecurityCheck does, otherwise it would cancel itself twice.

I'll try the cancel state thing first

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 cancel twice, we're just extra disabling the security domain and removing a listener that might've been removed already.

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, LMK if that is better or worse

waitForLoadEvent.cancel();
waitForNetworkIdle.cancel();
waitForCPUIdle && waitForCPUIdle.cancel();
Expand Down Expand Up @@ -959,26 +1004,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 @@ -109,7 +109,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 @@ -172,23 +171,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 @@ -311,8 +293,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 @@ -952,7 +952,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
52 changes: 52 additions & 0 deletions lighthouse-core/test/gather/driver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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':
Expand Down Expand Up @@ -221,6 +223,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/';
Expand Down Expand Up @@ -535,4 +543,48 @@ describe('Multiple tab check', () => {
});
});
});

describe('._waitForSecurityCheck', () => {
it('returns nothing when page is secure', async () => {
const secureSecurityState = {
securityState: 'secure',
};
driverStub.once = createOnceStub({
'Security.securityStateChanged': secureSecurityState,
});
await driverStub._waitForSecurityCheck().promise;
});

it('returns an error 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.once = 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.');
}
});
});
});
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
40 changes: 0 additions & 40 deletions lighthouse-core/test/gather/gather-runner-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,6 @@ describe('GatherRunner', function() {
clearDataForOrigin: createCheck('calledClearStorage'),
blockUrlPatterns: asyncFunc,
setExtraHTTPHeaders: asyncFunc,
listenForSecurityStateChanges: asyncFunc,
};

return GatherRunner.setupDriver(driver, {settings: {}}).then(_ => {
Expand Down Expand Up @@ -382,7 +381,6 @@ describe('GatherRunner', function() {
clearDataForOrigin: createCheck('calledClearStorage'),
blockUrlPatterns: asyncFunc,
setExtraHTTPHeaders: asyncFunc,
listenForSecurityStateChanges: asyncFunc,
};

return GatherRunner.setupDriver(driver, {
Expand Down Expand Up @@ -696,44 +694,6 @@ describe('GatherRunner', function() {
});
});

describe('#assertNoSecurityIssues', () => {
it('succeeds when page is secure', () => {
const secureSecurityState = {
securityState: 'secure',
};
GatherRunner.assertNoSecurityIssues(secureSecurityState);
});

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',
};
try {
GatherRunner.assertNoSecurityIssues(insecureSecurityState);
assert.fail('expected INSECURE_DOCUMENT_REQUEST LHError');
} catch (err) {
assert.equal(err.message, 'INSECURE_DOCUMENT_REQUEST');
assert.equal(err.code, 'INSECURE_DOCUMENT_REQUEST');
expect(err.friendlyMessage)
.toBeDisplayString(/The URL.*security credentials.*reason 1 reason 2/);
}
});
});

describe('artifact collection', () => {
// Make sure our gatherers never execute in parallel
it('runs gatherer lifecycle methods strictly in sequence', async () => {
Expand Down