Compare commits

..
1 Commits
Author SHA1 Message Date
Gaël Métais f423c5dc4e Start working on CDN detection 2021-03-07 09:41:22 +00:00
8 changed files with 19633 additions and 12 deletions
+2 -2
View File
@@ -875,7 +875,7 @@ var policies = {
return offenders; return offenders;
} }
}, },
"unusedUnicodeRanges": { /*"unusedUnicodeRanges": {
"tool": "redownload", "tool": "redownload",
"label": "Unused Unicode ranges", "label": "Unused Unicode ranges",
"message": "<p>This metric counts the number of unused Unicode ranges inside each font. For example, one font could include Cyrillic glyphs but none of them are used on the page.</p><p>It also reveals the number of ligatures (letters that are represented differently when close to each other) and hidden chars (glyphs not linked to the unicode system that can't be displayed on the web).</p><p>Because of technical limitations, Yellow Lab Tools checks each font against the glyphs of the entire page. As a result, estimated use is >= to reality. For example, if you read that 10 glyphs are \"possibly used\", it means that these 10 glyphs are used on the page but nothing guaranties that they are displayed using this font.</p><p>Tools such as <a href=\"https://www.fontsquirrel.com/tools/webfont-generator\" target=\"_blank\">Font Squirrel</a> can remove some unicode ranges from a font.</p><p>In the case of an icon font, make sure you only keep the icons that are used on the website and to remove the others. Several tools are able to extract SVG images from a font, then some other tools can generate a font from the SVGs you want to keep.</p>", "message": "<p>This metric counts the number of unused Unicode ranges inside each font. For example, one font could include Cyrillic glyphs but none of them are used on the page.</p><p>It also reveals the number of ligatures (letters that are represented differently when close to each other) and hidden chars (glyphs not linked to the unicode system that can't be displayed on the web).</p><p>Because of technical limitations, Yellow Lab Tools checks each font against the glyphs of the entire page. As a result, estimated use is >= to reality. For example, if you read that 10 glyphs are \"possibly used\", it means that these 10 glyphs are used on the page but nothing guaranties that they are displayed using this font.</p><p>Tools such as <a href=\"https://www.fontsquirrel.com/tools/webfont-generator\" target=\"_blank\">Font Squirrel</a> can remove some unicode ranges from a font.</p><p>In the case of an icon font, make sure you only keep the icons that are used on the website and to remove the others. Several tools are able to extract SVG images from a font, then some other tools can generate a font from the SVGs you want to keep.</p>",
@@ -886,7 +886,7 @@ var policies = {
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
return offenders; return offenders;
} }
}, },*/
"nonWoff2Fonts": { "nonWoff2Fonts": {
"tool": "redownload", "tool": "redownload",
"label": "WOFF 2", "label": "WOFF 2",
-1
View File
@@ -86,7 +86,6 @@
"policies": { "policies": {
"fontsCount": 1, "fontsCount": 1,
"heavyFonts": 0.5, "heavyFonts": 0.5,
"unusedUnicodeRanges": 0.5,
"nonWoff2Fonts": 0.5 "nonWoff2Fonts": 0.5
} }
}, },
-6
View File
@@ -78,12 +78,6 @@ var Runner = function(params) {
milestone: 'redownload' milestone: 'redownload'
}); });
// Fix: don't display Unicode ranges if the module is not present in Phantomas
if (!data.toolsResults.phantomas.metrics.charactersCount) {
delete data.toolsResults.redownload.metrics.unusedUnicodeRanges;
delete data.toolsResults.redownload.offenders.unusedUnicodeRanges;
}
// Rules checker // Rules checker
var policies = require('./metadata/policies'); var policies = require('./metadata/policies');
data.rules = rulesChecker.check(data, policies); data.rules = rulesChecker.check(data, policies);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
var debug = require('debug')('ylt:cdnDetector');
var Q = require('q');
var url = require('url');
var dns = require('dns');
var cdnDetector = require('cdn-detector');
// This module checks my multiple ways if a request was served by a CDN
var CDNDetector = function() {
function detectCDN(entry) {
var urlObject = url.parse(entry.url);
debug('Start checking CDN for %s, host is %s', entry.url, urlObject.host);
// 1st, the fastest check: the domain is directly recognized as a CDN
return checkDomainDirectly(entry)
.fail(function() {
// 2nd, check the response headers
return checkHeaders(entry)
.fail(function() {
// 3rd, check the Name Servers
return checkNameServers(entry)
.fail(function(entry) {
// None of the above worked, we didn't find any trace of a CDN
return entry;
});
})
});
}
return {
detectCDN: detectCDN
};
// Compares the URL's domain to a list of know CDN, just in case
function checkDomainDirectly(entry) {
var deferred = Q.defer();
var result = checkHostname(url.parse(entry.url).host);
if (result === null) {
deferred.reject(entry);
} else {
entry.weightCheck.cdn = result;
deferred.resolve(entry);
}
return deferred.promise;
}
// Compares a list of HTTP response headers to a matching list
// For example, if there is an X-Akamai-Request-Id header, we know it is Akamai
function checkHeaders(entry) {
var deferred = Q.defer();
var result = cdnDetector.detectFromHeaders(entry.weightCheck.headers);
debug('Headers of %s were compared to the list. Result: %s', entry.url, JSON.stringify(result));
if (result === null) {
deferred.reject(entry);
} else {
entry.weightCheck.cdn = result.cdn;
deferred.resolve(entry);
}
return deferred.promise;
}
function checkNameServers(entry) {
var deferred = Q.defer();
var hostname = url.parse(entry.url).host;
// Send a request to get the Name Servers list
// https://nodejs.org/api/dns.html#dns_dns_resolvens_hostname_callback
dns.resolveNs(hostname, function(error, nameServers) {
if (error) {
debug(error);
debug('Could not find Name Servers for %s', hostname);
deferred.reject(entry);
} else {
debug('Name Servers for %s are ["%s"]', hostname, nameServers.join('", "'));
var cdn = nameServers.map(checkHostname).find(function(entry) {
return entry !== null;
});
if (cdn) {
entry.weightCheck.cdn = cdn;
deferred.resolve(entry);
} else {
deferred.reject(entry);
}
}
});
return deferred.promise;
}
// Compares the hostname to a matching list of hostnames
function checkHostname(hostname) {
var result = cdnDetector.detectFromHostname(hostname);
debug('Hostname %s was compared to the list. Result: %s', hostname, JSON.stringify(result));
return result ? result.cdn : null;
}
// Find an IP address with a DNS lookup
function getIP(hostname) {
var deferred = Q.defer();
// https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback
dns.lookup(hostname, {family: 4}, function(error, address) {
if (error) {
debug('Could not find IP address for %s', hostname);
} else {
debug('The IP address for %s is %s', hostname, address);
}
deferred.resolve(error ? null : address);
});
return deferred.promise;
}
// Compares the IP address agains a list of IP ranges
function checkIPAddress(address) {
var deferred = Q.defer();
var result = cdnDetector.detectFromHeaders(headers);
debug('Headers were compared to the list. Result: %s', JSON.stringify(result));
if (result === null) {
deferred.reject();
} else {
deferred.resolve(result.cdn);
}
return deferred.promise;
}
function saveCDN(cdnName) {
}
};
module.exports = new CDNDetector();
@@ -0,0 +1,71 @@
var http = require('https');
var zlib = require('zlib');
var fs = require('fs');
var CDN_ASN_LIST = {
13335: 'Cloudflare',
15133: 'Verizon',
16625: 'Akamai',
20446: 'StackPath',
20940: 'Akamai',
22822: 'Limelight',
54113: 'Fastly'
};
// Downloads a file and ungzip ip directly
function getGzipped(url, callback) {
var buffer = [];
http.get(url, function(res) {
var gunzip = zlib.createGunzip();
res.pipe(gunzip);
gunzip.on('data', function(data) {
buffer.push(data.toString())
}).on('end', function() {
callback(null, buffer.join(''));
}).on('error', function(e) {
callback(e);
})
}).on('error', function(e) {
callback(e)
});
}
// File provided here: https://iptoasn.com/
console.log('Start downloading the file...');
getGzipped('https://iptoasn.com/data/ip2asn-v4.tsv.gz', function(err, data) {
if (err) {
console.log(err);
return;
}
console.log('Download complete, let\'s start parsing...');
parseFile(data);
});
// Parse the CSV formated file and grag only the interesting IP ranges
function parseFile(data) {
var results = [];
var allTextLines = data.split(/\r\n|\n/);
for (var i = 0; i < allTextLines.length; i++) {
var lineData = allTextLines[i].split('\t');
var cdnName = CDN_ASN_LIST[lineData[2]];
if (cdnName) {
// We found an ASN number from our list. Let's save it!
results.push({
rangeStart: lineData[0],
rangeEnd: lineData[1],
cdn: cdnName
});
}
}
console.log('%d IP ranges found', results.length);
var outputFile = __dirname + '/cdn-ip-list.json';
fs.writeFileSync(outputFile, JSON.stringify(results, null, 2));
console.log('File saved here: %s', outputFile);
}
+2 -3
View File
@@ -65,8 +65,8 @@ var Redownload = function() {
// Prevent a bug with the font analyzer on empty pages // Prevent a bug with the font analyzer on empty pages
var differentCharacters = ''; var differentCharacters = '';
if (data.toolsResults.phantomas.offenders.charactersCount && data.toolsResults.phantomas.offenders.charactersCount.length > 0) { if (data.toolsResults.phantomas.offenders.differentCharacters && data.toolsResults.phantomas.offenders.differentCharacters.length > 0) {
differentCharacters = data.toolsResults.phantomas.offenders.charactersCount[0]; differentCharacters = data.toolsResults.phantomas.offenders.differentCharacters[0];
} }
// Transform every request into a download function with a callback when done // Transform every request into a download function with a callback when done
@@ -900,7 +900,6 @@ var Redownload = function() {
var result = { var result = {
bodyBuffer: body, bodyBuffer: body,
headersSize: Buffer.byteLength(rawHeaders, 'utf8'), headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
headers: res.headers,
bodySize: bodySize, bodySize: bodySize,
isCompressed: isCompressed, isCompressed: isCompressed,
compressionTool: compressionTool, compressionTool: compressionTool,
+1
View File
@@ -29,6 +29,7 @@
"angular-sanitize": "1.7.7", "angular-sanitize": "1.7.7",
"async": "2.6.1", "async": "2.6.1",
"body-parser": "1.18.3", "body-parser": "1.18.3",
"cdn-detector": "0.1.6",
"chart.js": "2.7.3", "chart.js": "2.7.3",
"clean-css": "4.2.1", "clean-css": "4.2.1",
"color-diff": "1.1.0", "color-diff": "1.1.0",