Skip to content

Commit

Permalink
core(preload): only allow same origin (domain + subdomains) (#5065)
Browse files Browse the repository at this point in the history
  • Loading branch information
wardpeet authored and patrickhulce committed Jun 6, 2018
1 parent ac264c0 commit d385645
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 9 deletions.
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
54 changes: 54 additions & 0 deletions lighthouse-core/lib/url-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const Util = require('../report/html/renderer/util.js');
const URL = /** @type {!Window["URL"]} */ (typeof self !== 'undefined' && self.URL) ||
require('url').URL;

// 25 most used tld plus one domains from http archive.
// @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212
const listOfTlds = [
'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 +93,54 @@ 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) {
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
50 changes: 43 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');
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',
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,9 @@ 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);
assert.equal(output.details.items[0].url, 'http://www.example.com/script-added.js');
assert.equal(output.details.items[1].url, 'http://sub.example.com/script-sub.js');
}
);
});
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

0 comments on commit d385645

Please sign in to comment.