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(preload): only allow same origin (domain + subdomains) #5065

Merged
merged 7 commits into from
Jun 6, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
19 changes: 17 additions & 2 deletions lighthouse-core/audits/uses-rel-preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
'use strict';

const URL = require('../lib/url-shim');
const Audit = require('./audit');
const UnusedBytes = require('./byte-efficiency/byte-efficiency-audit');
const THRESHOLD_IN_MS = 100;
Expand Down Expand Up @@ -57,6 +58,20 @@ class UsesRelPreloadAudit extends Audit {
return requests;
}

/**
*
* @param {LH.WebInspector.NetworkRequest} request
* @param {LH.WebInspector.NetworkRequest} mainResource
* @return {boolean}
*/
static shouldPreload(request, mainResource) {
if (request._isLinkPreload || request.protocol === 'data') {
return false;
}

return URL.rootDomainsMatch(request.url, mainResource.url);
}

/**
* Computes the estimated effect of preloading all the resources.
* @param {Set<string>} urls The array of byte savings results per resource
Expand Down Expand Up @@ -159,8 +174,8 @@ class UsesRelPreloadAudit extends Audit {
/** @type {Set<string>} */
const urls = new Set();
for (const networkRecord of criticalRequests) {
if (!networkRecord._isLinkPreload && networkRecord.protocol !== 'data') {
urls.add(networkRecord._url);
if (UsesRelPreloadAudit.shouldPreload(networkRecord, mainResource)) {
urls.add(networkRecord.url);
}
}

Expand Down
50 changes: 50 additions & 0 deletions lighthouse-core/lib/url-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const Util = require('../report/html/renderer/util.js');
const URL = /** @type {!Window["URL"]} */ (typeof self !== 'undefined' && self.URL) ||
require('url').URL;

const listOfTlds = [
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add a comment that somewhat documents what happened here re: coverage and whatnot? maybe just link to the best comment thread on this PR :)

'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv',
'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on',
];
/**
* There is fancy URL rewriting logic for the chrome://settings page that we need to work around.
* Why? Special handling was added by Chrome team to allow a pushState transition between chrome:// pages.
Expand Down Expand Up @@ -87,6 +91,52 @@ class URLShim extends URL {
}
}

/**
* Gets the tld of a domain
*
* @param {string} hostname
* @return {string} tld
*/
static getTld(hostname) {
const tlds = hostname.split('.').slice(-2);

if (!listOfTlds.includes(tlds[0])) {
return `.${tlds[tlds.length - 1]}`;
}

return `.${tlds.join('.')}`;
}

/**
* Check if rootDomains matches
*
* @param {string} urlA
* @param {string} urlB
*/
static rootDomainsMatch(urlA, urlB) {
Copy link
Member

Choose a reason for hiding this comment

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

can you add some tests in url-shim-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.

yeah forgot about that one :)

let urlAInfo;
let urlBInfo;
try {
urlAInfo = new URL(urlA);
urlBInfo = new URL(urlB);
} catch (err) {
return false;
}

if (!urlAInfo.hostname || !urlBInfo.hostname) {
return false;
}

const tldA = URLShim.getTld(urlAInfo.hostname);
const tldB = URLShim.getTld(urlBInfo.hostname);

// get the string before the tld
const urlARootDomain = urlAInfo.hostname.replace(new RegExp(`${tldA}$`), '').split('.').splice(-1)[0];
const urlBRootDomain = urlBInfo.hostname.replace(new RegExp(`${tldB}$`), '').split('.').splice(-1)[0];

return urlARootDomain === urlBRootDomain;
}

/**
* @param {string} url
* @param {{numPathParts: number, preserveQuery: boolean, preserveHost: boolean}=} options
Expand Down
48 changes: 41 additions & 7 deletions lighthouse-core/test/audits/uses-rel-preload-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,18 @@ describe('Performance: uses-rel-preload audit', () => {

it('should suggest preload resource', () => {
const rootNode = buildNode(1, 'http://example.com');
const mainDocumentNode = buildNode(2, 'http://www.example.com');
const mainDocumentNode = buildNode(2, 'http://www.example.com:3000');
Copy link
Collaborator

Choose a reason for hiding this comment

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

we're gonna need some graph creation helpers soon to make all this stuff :)

const scriptNode = buildNode(3, 'http://www.example.com/script.js');
const scriptAddedNode = buildNode(4, 'http://www.example.com/script-added.js');
const scriptSubNode = buildNode(5, 'http://sub.example.com/script-sub.js');
const scriptOtherNode = buildNode(6, 'http://otherdomain.com/script-other.js');

mainDocumentNode.setIsMainDocument(true);
mainDocumentNode.addDependency(rootNode);
scriptNode.addDependency(mainDocumentNode);
scriptAddedNode.addDependency(scriptNode);
scriptSubNode.addDependency(scriptNode);
scriptOtherNode.addDependency(scriptNode);

mockGraph = rootNode;
mockSimulator = {
Expand All @@ -69,41 +73,63 @@ describe('Performance: uses-rel-preload audit', () => {
const mainDocumentNodeLocal = nodesByUrl.get(mainDocumentNode.record.url);
const scriptNodeLocal = nodesByUrl.get(scriptNode.record.url);
const scriptAddedNodeLocal = nodesByUrl.get(scriptAddedNode.record.url);
const scriptSubNodeLocal = nodesByUrl.get(scriptSubNode.record.url);
const scriptOtherNodeLocal = nodesByUrl.get(scriptOtherNode.record.url);

const nodeTimings = new Map([
[rootNodeLocal, {starTime: 0, endTime: 500}],
[mainDocumentNodeLocal, {startTime: 500, endTime: 1000}],
[scriptNodeLocal, {startTime: 1000, endTime: 2000}],
[scriptAddedNodeLocal, {startTime: 2000, endTime: 3250}],
[scriptSubNodeLocal, {startTime: 2000, endTime: 3000}],
[scriptOtherNodeLocal, {startTime: 2000, endTime: 3500}],
]);

if (scriptAddedNodeLocal.getDependencies()[0] === mainDocumentNodeLocal) {
nodeTimings.set(scriptAddedNodeLocal, {startTime: 1000, endTime: 2000});
}

return {timeInMs: 3250, nodeTimings};
if (scriptSubNodeLocal.getDependencies()[0] === mainDocumentNodeLocal) {
nodeTimings.set(scriptSubNodeLocal, {startTime: 1000, endTime: 2000});
}

if (scriptOtherNodeLocal.getDependencies()[0] === mainDocumentNodeLocal) {
nodeTimings.set(scriptOtherNodeLocal, {startTime: 1000, endTime: 2500});
}

return {timeInMs: 3500, nodeTimings};
},
};

const mainResource = Object.assign({}, defaultMainResource, {
url: 'http://www.example.com',
url: 'http://www.example.com:3000',
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this just to exercise extra hostname checks?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes it's to make sure that origins with a port number are handled correct by the rootDomainMatch

redirects: [''],
});
const networkRecords = [
{
requestId: '2',
_isLinkPreload: false,
_url: 'http://www.example.com',
url: 'http://www.example.com:3000',
},
{
requestId: '3',
_isLinkPreload: false,
_url: 'http://www.example.com/script.js',
url: 'http://www.example.com/script.js',
},
{
requestId: '4',
_isLinkPreload: false,
_url: 'http://www.example.com/script-added.js',
url: 'http://www.example.com/script-added.js',
},
{
requestId: '5',
_isLinkPreload: false,
url: 'http://sub.example.com/script-sub.js',
},
{
requestId: '6',
_isLinkPreload: false,
url: 'http://otherdomain.com/script-other.js',
},
];

Expand All @@ -120,6 +146,14 @@ describe('Performance: uses-rel-preload audit', () => {
request: networkRecords[2],
children: {},
},
'5': {
request: networkRecords[3],
children: {},
},
'6': {
request: networkRecords[4],
children: {},
},
},
},
},
Expand All @@ -131,7 +165,7 @@ describe('Performance: uses-rel-preload audit', () => {
return UsesRelPreload.audit(mockArtifacts(networkRecords, chains, mainResource), {}).then(
output => {
assert.equal(output.rawValue, 1250);
assert.equal(output.details.items.length, 1);
assert.equal(output.details.items.length, 2);
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe overkill but assert the URL matches too?

}
);
});
Expand Down
55 changes: 55 additions & 0 deletions lighthouse-core/test/lib/url-shim-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,61 @@ describe('URL Shim', () => {
assert.equal(URL.getOrigin(urlD), null);
});

describe('getTld', () => {
it ('returns the correct tld', () => {
assert.equal(URL.getTld('example.com'), '.com');
assert.equal(URL.getTld('example.co.uk'), '.co.uk');
assert.equal(URL.getTld('example.com.br'), '.com.br');
assert.equal(URL.getTld('example.tokyo.jp'), '.jp');
});
});

describe('rootDomainsMatch', () => {
it('matches a subdomain and a root domain', () => {
const urlA = 'http://example.com/js/test.js';
const urlB = 'http://example.com/';
const urlC = 'http://sub.example.com/js/test.js';
const urlD = 'http://sub.otherdomain.com/js/test.js';

assert.ok(URL.rootDomainsMatch(urlA, urlB));
assert.ok(URL.rootDomainsMatch(urlA, urlC));
assert.ok(!URL.rootDomainsMatch(urlA, urlD));
assert.ok(!URL.rootDomainsMatch(urlB, urlD));
});

it(`doesn't break on urls without a valid host`, () => {
const urlA = 'http://example.com/js/test.js';
const urlB = 'data:image/jpeg;base64,foobar';
const urlC = 'anonymous:90';
const urlD = '!!garbage';
const urlE = 'file:///opt/lighthouse/index.js';

assert.ok(!URL.rootDomainsMatch(urlA, urlB));
assert.ok(!URL.rootDomainsMatch(urlA, urlC));
assert.ok(!URL.rootDomainsMatch(urlA, urlD));
assert.ok(!URL.rootDomainsMatch(urlA, urlE));
assert.ok(!URL.rootDomainsMatch(urlB, urlC));
assert.ok(!URL.rootDomainsMatch(urlB, urlD));
assert.ok(!URL.rootDomainsMatch(urlB, urlE));
});

it(`matches tld plus domains`, () => {
const coUkA = 'http://example.co.uk/js/test.js';
const coUkB = 'http://sub.example.co.uk/js/test.js';
const testUkA = 'http://example.test.uk/js/test.js';
const testUkB = 'http://sub.example.test.uk/js/test.js';
const ltdBrA = 'http://example.ltd.br/js/test.js';
const ltdBrB = 'http://sub.example.ltd.br/js/test.js';
const privAtA = 'http://examplepriv.at/js/test.js';
const privAtB = 'http://sub.examplepriv.at/js/test.js';

assert.ok(URL.rootDomainsMatch(coUkA, coUkB));
assert.ok(URL.rootDomainsMatch(testUkA, testUkB));
assert.ok(URL.rootDomainsMatch(ltdBrA, ltdBrB));
assert.ok(URL.rootDomainsMatch(privAtA, privAtB));
});
});

describe('getURLDisplayName', () => {
it('respects numPathParts option', () => {
const url = 'http://example.com/a/deep/nested/file.css';
Expand Down