cdn-cache-check
Version:
Makes HTTP requests to URLs and parses response headers to determine caching behaviour
244 lines (194 loc) • 12.7 kB
JavaScript
const debug = require('debug')('cdn-cache-check-dns');
debug('Entry: [%s]', __filename);
// Initialise wildcard string parser
const matcher = require('multimatch');
// Initialise Domain validation object
const isValidDomain = require('is-valid-domain');
// Import DNS library
const dns = require('native-dns-multisocket');
function getDNSResolver() {
debug('getDNSResolver()::entry');
try {
// Check if the static variable is already defined
if (typeof (this.getDNSResolver.primaryDNSResolver) === 'undefined') {
// Import dns module
let dns = require('dns');
// Get array of local machine's DNS resolvers
let resolvers = dns.getServers();
debug('Obtained resolvers list: %O', resolvers);
// Pick the first one
this.getDNSResolver.primaryDNSResolver = resolvers[0];
}
debug('Returning resolver: %s', this.getDNSResolver.primaryDNSResolver);
// Return resolver IP Address
return (this.getDNSResolver.primaryDNSResolver);
} catch (error) {
// An error occurred getting the locally configured resolver, so return default
debug('getDNSResolvers caught an error: %O', error);
debug('Returning default resolver: %s', global.CCC_DNS.DEFAULT_RESOLVER);
return (global.CCC_DNS.DEFAULT_RESOLVER);
}
}
function getUniqueDomains(urls) {
try {
// Using a Set() as it can only contain unique values
let uniqueDomains = new Set();
// Initialise the JSON return object
let returnObject = {};
// Initialise length of the longest FQDN in the list
let domainNameLength = 0;
// loop through array, extracting hostname from each URL
for (let i = 0; i < urls.length; i++) {
// Load the URL into a parsed object
let currentUrl = new URL(urls[i]);
// Extract the hostname from the URL
let hostname = currentUrl.hostname;
// Add the hostname to the Set
uniqueDomains.add(hostname);
// Record the length of the hostname if it's the largest yet
domainNameLength = (hostname.length > domainNameLength) ? hostname.length : domainNameLength;
}
returnObject.domains = Array.from(uniqueDomains);
returnObject.domainNameLength = domainNameLength;
returnObject.count = uniqueDomains.size;
return (returnObject);
} catch (error) {
debug('Exception caught in getUniqueDomains(): %O', error);
return (null);
}
}
function parseAnswer(answer, options) {
debug('parseAnswer(answer, options) called with answer: %O ---> options: %O', answer, options);
// Validate the answer object has something to parse
if (Array.isArray(answer) && answer.length === 0) {
debug('parseAnswer() answer[] is an empty array. Nothing to parse; returning "no_address"');
// No IP addresses, `answer` is an empty array
return ('no_address');
} else {
// Initialise the array we're going to return
let response = [];
// Add the hostname that was resolved to the response[] array (so we have a complete end-to-end chain in the recursive response)
if (Object.prototype.hasOwnProperty.call(answer[0], 'name')) {
response.push(answer[0].name);
}
switch (options.operation) {
case 'getRecursion': { // Get full recursive hostnames
// Get the whole nested recursion
for (let i = 0; i < answer.length; i++) {
if (Object.prototype.hasOwnProperty.call(answer[i], 'data')) { // Check if the answer element has a "data" property (which a CNAME record will have)
response.push(answer[i].data); // Extract CNAME record data
} else if ((options.includeIpAddresses) && (Object.prototype.hasOwnProperty.call(answer[i], 'address'))) { // Check if the answer element has an "address" property (which an A record will have)
response.push(answer[i].address); // Extract A record data
}
}
break;
}
case 'getTTL': {
// Get the record's time-to-live value
response = answer[0].ttl;
debug('TTL: %s', answer[0].ttl);
break;
}
default: // Extract the IP address by default
for (let i = 0; i < answer.length; i++) { // Iterate through recursive answer
if (Object.prototype.hasOwnProperty.call(answer[i], 'address')) {
response = answer[i].address;
}
}
}
return (response);
}
}
let inspectDNS = (domain, settings) => {
debug('inspectDNS(%s)', domain);
return new Promise(function (resolve, reject) {
let response = global.CCC_SERVICE_DETECTION_DEFAULT_RESPONSE; // Initialise response object
response.fqdn = domain; // Set the Fully Qualified Domain Name
if (typeof (domain) === 'string' && domain.trim().length > 0) { // Check if the fqdn is a non-empty string
if (isValidDomain(domain, { subdomain: true, wildcard: false })) { // Verify that the fqdn conforms to DNS specifications
let question = dns.Question({ // Create DNS Question object
name: domain,
type: global.CCC_DNS_REQUEST_RECORD_TYPE,
});
let req = dns.Request({ // Create DNS Request object
question: question,
server: { address: getDNSResolver(), port: 53, type: 'udp' },
timeout: 5000
});
// DNS 'timeout' event
req.on('timeout', () => { // Handle DNS timeout event
debug('DNS timeout occurred resolving [%s]', domain);
response.message = 'DNS Timeout'; // Record Timeout message
response.messages.push(response.message); // Add message to the messages[] array
response.reason = `DNS timeout after ${req.timeout} ms`;
response.status = global.CCC_SERVICE_DETECTION_STATUS_LABEL.ERROR;
reject(response); // reject the promise
});
// DNS 'message' event
req.on('message', (error, answer) => { // handle DNS message event
if (error) { // DNS returned an error
debug('Received DNS error for %s: %O', domain, error);
response.message = `DNS Error flagged in message event: ${error}`;
response.messages.push(response.message); // Add message to the messages[] array
response.reason = 'DNS Error';
response.status = global.CCC_SERVICE_DETECTION_STATUS_LABEL.ERROR;
debug('inspectDNS() rejecting Promise with response: %O', response);
reject(response); // reject the promise
} else { // Process DNS Answer
debug('Received DNS answer to the lookup for [%s]: %O', domain, answer);
// Expand the answer into an array of all nested addresses in the full DNS recursion
response.dnsAnswer = parseAnswer(answer.answer, { operation: 'getRecursion' });
// Get the IP address from the DNS answer
debug('Extracting the IP address from the DNS answer');
response.ipAddress = parseAnswer(answer.answer, {});
// Iterate through each nested address in the DNS answer to check if matches a known service's domain
for (let i = 0; i < response.dnsAnswer.length; i++) {
for (let service in settings.apexDomains) {
debug('Evaluating FQDN [%s] against the service [%s] which uses the domains: %O', response.dnsAnswer[i], service, settings.apexDomains[service].domains);
// Generate an array of service apex domains which match the FQDN's CNAME chain entries
let matchingDomains = matcher(response.dnsAnswer[i], settings.apexDomains[service].domains);
if (matchingDomains.length > 0) { // We've found 6y7/a match. Record the details
debug('%s is served by %s due to nested domain %s', domain, settings.apexDomains[service].title, matchingDomains[0]);
// Populate response object properties
response.reason = `${domain} resolves to ${matchingDomains[0]} which matches a ${service} domain pattern`;
response.matchingDomains = matchingDomains[0];
response.service = settings.apexDomains[service].service;
response.message = settings.apexDomains[service].title;
response.messages.push(response.message);
if (settings.apexDomains[service].service.toUpperCase() === 'CDN') {
response.status = global.CCC_SERVICE_DETECTION_STATUS_LABEL.CDN;
} else {
response.status = global.CCC_SERVICE_DETECTION_STATUS_LABEL.OTHER;
}
}
}
}
// Check if the DNS inspection didn't identify the service provider
if (response.status === global.CCC_SERVICE_DETECTION_STATUS_LABEL.UNKNOWN) {
// We didn't identify the service behind the domain name
console.log('Just checking if it is worth setting response.status to UNKNOWN here as it is currently: %s', response.status);
response.message = [global.CCC_SERVICE_DETECTION_STATUS_LABEL.UNKNOWN]; // add the "Unknown" message
debug('%s\'s DNS recursion didn\'t match a known provider\'s domain (response.status: %s)', domain, response.status);
}
debug('inspectDNS(%s) returning: %O', domain, response);
// Return response object as we found a known service behind the fqdn
resolve(response);
}
});
debug('Sending DNS Request: %O', req);
req.send(); // Issue the DNS lookup request
} else {
response.message = `DNS Inspection failed. The "fqdn" [${domain}] did not pass DNS name validation.`
response.messages.push(response.message); // Add message to the messages[] array
debug('inspectDNS() rejecting Promise with response: %O', response);
reject(response); // reject the promise
}
} else {
response.message = `DNS Inspection failed. The "fqdn" parameter [${domain}] is either empty or not a string.`
response.messages.push(response.message); // Add message to the messages[] array
debug('inspectDNS() rejecting Promise with response: %O', response);
reject(response); // reject the promise
}
});
};
module.exports = { getDNSResolver, getUniqueDomains, inspectDNS };