442 lines
12 KiB
JavaScript
442 lines
12 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
/*
|
|
module.exports.ask = function (query, cb) {
|
|
};
|
|
*/
|
|
|
|
var NOERROR = 0;
|
|
var NXDOMAIN = 3;
|
|
var REFUSED = 5;
|
|
|
|
function getRecords(engine, qname, cb) {
|
|
var delMe = {};
|
|
var dns = require('dns');
|
|
// SECURITY XXX TODO var dig = require('dig.js/dns-request');
|
|
var count;
|
|
|
|
return engine.records.get({ name: qname }, function (err, myRecords) {
|
|
if (err) { cb(err); return; }
|
|
|
|
function checkCount() {
|
|
var ready;
|
|
|
|
count -= 1;
|
|
ready = count <= 0;
|
|
|
|
if (!ready) {
|
|
return;
|
|
}
|
|
|
|
myRecords = myRecords.filter(function (r) {
|
|
return !delMe[r.id];
|
|
});
|
|
|
|
// There are a number of ways to interpret the wildcard rules
|
|
var hasWild = false;
|
|
var hasMatch = false;
|
|
myRecords.some(function (r) {
|
|
if (qname === r.name) {
|
|
hasMatch = true;
|
|
return true;
|
|
}
|
|
if ('*' === r.name[0]) {
|
|
hasWild = true;
|
|
}
|
|
});
|
|
|
|
if (hasMatch) {
|
|
myRecords = myRecords.filter(function (r) {
|
|
if ('*' !== r.name[0]) { return true; }
|
|
});
|
|
}
|
|
/*
|
|
// no need to filter out records if wildcard is used
|
|
else {
|
|
records = records.filter(function (r) {
|
|
if ('*' === r.name[0]) { return true; }
|
|
});
|
|
}
|
|
*/
|
|
|
|
cb(null, myRecords);
|
|
}
|
|
|
|
function getRecord(r) {
|
|
// TODO allow multiple records to be returned(?)
|
|
return function (err, addresses) {
|
|
if (err || !addresses.length) {
|
|
delMe[r.id] = true;
|
|
} else if (addresses.length > 1) {
|
|
r._address = addresses[Math.floor(Math.random() * addresses.length)];
|
|
} else {
|
|
r._address = addresses[0];
|
|
}
|
|
checkCount();
|
|
};
|
|
}
|
|
|
|
count = myRecords.length;
|
|
myRecords.forEach(function (r) {
|
|
if (r.aname && !r.address) {
|
|
if ('A' === r.type) {
|
|
// SECURITY XXX TODO dig.resolveJson(query, opts);
|
|
dns.resolve4(r.aname, getRecord(r));
|
|
return;
|
|
}
|
|
if ('AAAA' === r.type) {
|
|
// SECURITY XXX TODO dig.resolveJson(query, opts);
|
|
dns.resolve6(r.aname, getRecord(r));
|
|
return;
|
|
}
|
|
}
|
|
|
|
checkCount();
|
|
});
|
|
|
|
if (!myRecords.length) {
|
|
checkCount();
|
|
}
|
|
});
|
|
}
|
|
|
|
function dbToResourceRecord(r) {
|
|
return {
|
|
name: r.name
|
|
, typeName: r.typeName || r.type // NS
|
|
, className: 'IN'
|
|
, ttl: r.ttl || 300
|
|
|
|
// SOA
|
|
/*
|
|
, "primary": "ns1.yahoo.com"
|
|
, "admin": "hostmaster.yahoo-inc.com"
|
|
, "serial": 2017092539
|
|
, "refresh": 3600
|
|
, "retry": 300
|
|
, "expiration": 1814400
|
|
, "minimum": 600
|
|
*/
|
|
|
|
// A, AAAA
|
|
, address: -1 !== [ 'A', 'AAAA' ].indexOf(r.type) ? (r._address || r.address || r.value) : undefined
|
|
|
|
// CNAME, NS, PTR || TXT
|
|
, data: -1 !== [ 'CNAME', 'NS', 'PTR', 'TXT' ].indexOf(r.type) ? (r.data || r.value || r.values) : undefined
|
|
|
|
// MX, SRV
|
|
, priority: r.priority
|
|
|
|
// MX
|
|
, exchange: r.exchange
|
|
|
|
// SRV
|
|
, weight: r.weight
|
|
, port: r.port
|
|
, target: r.target
|
|
};
|
|
}
|
|
|
|
function getNs(engine, zs, results, cb) {
|
|
console.log('[DEV] getNs entered with domains', zs);
|
|
|
|
var z = zs.shift();
|
|
console.log('[DEV] trying another one', z);
|
|
|
|
if (!z) {
|
|
results.header.rcode = NXDOMAIN;
|
|
cb(null, results);
|
|
return;
|
|
}
|
|
|
|
var qn = z.name.toLowerCase();
|
|
|
|
return getRecords(engine, qn, function (err, records) {
|
|
if (err) { cb(err); return; }
|
|
|
|
records.forEach(function (r) {
|
|
if ('NS' !== r.type) {
|
|
return;
|
|
}
|
|
|
|
var ns = {
|
|
name: r.name
|
|
, typeName: r.type // NS
|
|
, className: r.class || 'IN'
|
|
, ttl: r.ttl || 300
|
|
, data: r.data || r.value || r.address
|
|
};
|
|
|
|
console.log('got NS record:');
|
|
console.log(r);
|
|
console.log(ns);
|
|
|
|
// TODO what if this NS is one of the NS?
|
|
// return SOA record instead
|
|
results.authority.push(ns);
|
|
});
|
|
|
|
if (!results.authority.length) {
|
|
return getNs(engine, zs, results, cb);
|
|
}
|
|
|
|
// d.vanityNs should only be vanity nameservers (pointing to this same server)
|
|
if (z.vanityNs || results.authority.some(function (ns) {
|
|
console.log('[debug] ns', ns);
|
|
return -1 !== engine.primaryNameservers.indexOf(ns.data.toLowerCase());
|
|
})) {
|
|
results.authority.length = 0;
|
|
results.authority.push(engine.zones._toSoa(z));
|
|
results.header.rcode = NXDOMAIN;
|
|
}
|
|
cb(null, results);
|
|
return;
|
|
});
|
|
}
|
|
|
|
function getSoa(engine, domain, results, cb, answerSoa) {
|
|
console.log('[DEV] getSoa entered');
|
|
|
|
if (!answerSoa) {
|
|
results.authority.push(engine.zones._toSoa(domain));
|
|
} else {
|
|
results.answer.push(engine.zones._toSoa(domain));
|
|
}
|
|
|
|
cb(null, results);
|
|
return;
|
|
}
|
|
|
|
module.exports.query = function (engine, query, cb) {
|
|
/*
|
|
var fs = require('fs');
|
|
|
|
fs.readFile(input, 'utf8', function (err, text) {
|
|
if (err) { cb(err); return; }
|
|
var records;
|
|
try {
|
|
records = JSON.parse(text);
|
|
} catch(e) { cb(e); return; }
|
|
});
|
|
*/
|
|
|
|
var qname;
|
|
|
|
if (!Array.isArray(query.question) || query.question.length < 1) {
|
|
cb(new Error("query is missing question section"));
|
|
return;
|
|
}
|
|
|
|
if (1 !== query.question.length) {
|
|
cb(new Error("query should have exactly one question (for now)"));
|
|
return;
|
|
}
|
|
|
|
if (!query.question[0] || 'string' !== typeof query.question[0].name) {
|
|
cb(new Error("query's question section should exist and have a String name property"));
|
|
return;
|
|
}
|
|
|
|
qname = query.question[0].name.toLowerCase();
|
|
|
|
var results = {
|
|
header: {
|
|
id: query.header.id // same as request
|
|
, qr: 1
|
|
, opcode: 0 // pretty much always 0 QUERY
|
|
, aa: 1 // TODO right now we assume that if we have the record, we're authoritative
|
|
// but in reality we could be hitting a cache and then recursing on a cache miss
|
|
, tc: 0
|
|
, rd: query.header.rd // duh
|
|
, ra: 0 // will be changed by cli.norecurse
|
|
, rcode: NOERROR // 0 NOERROR, 3 NXDOMAIN, 5 REFUSED
|
|
}
|
|
, question: [ query.question[0] ], answer: [], authority: [], additional: []
|
|
};
|
|
|
|
function getNsAndSoa(getNsAlso, answerSoa) {
|
|
// If the query is www.foo.delegated.example.com
|
|
// and we have been delegated delegated.example.com
|
|
// and delegated.example.com exists
|
|
// but foo.delegated.example.com does not exist
|
|
// what's the best strategy for returning the record?
|
|
//
|
|
// What does PowerDNS do in these situations?
|
|
// https://doc.powerdns.com/md/authoritative/backend-generic-mysql/
|
|
|
|
// How to optimize:
|
|
// Assume that if a record is being requested, it probably exists
|
|
// (someone has probably published it somewhere)
|
|
// If the record doesn't exist, then see if any of the domains are managed
|
|
// [ 'www.john.smithfam.net', 'john.smithfam.net', 'smithfam.net', 'net' ]
|
|
// Then if one of those exists, return the SOA record with NXDOMAIN
|
|
|
|
var qarr = qname.split('.');
|
|
var qnames = [];
|
|
while (qarr.length) {
|
|
qnames.push({ name: qarr.join('.').toLowerCase() });
|
|
qarr.shift(); // first
|
|
}
|
|
|
|
console.log('[DEV] getNsAlso?', getNsAlso);
|
|
console.log('[DEV] answerSoa?', answerSoa);
|
|
console.log('[DEV] qnames');
|
|
console.log(qnames);
|
|
|
|
// getSoas
|
|
return engine.zones.get(qnames, function (err, myDomains) {
|
|
console.log('[SOA] looking for', qnames, 'and proudly serving', err, myDomains);
|
|
if (err) { cb(err); return; }
|
|
|
|
// this should result in a REFUSED status
|
|
if (!myDomains.length) {
|
|
// REFUSED will have no records, so we could still recursion, if enabled
|
|
results.header.rcode = REFUSED;
|
|
cb(null, results);
|
|
return;
|
|
}
|
|
|
|
myDomains.sort(function (d1, d2) {
|
|
if (d1.id.length > d2.id.length) {
|
|
return -1;
|
|
}
|
|
if (d1.id.length < d2.id.length) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
});
|
|
//console.log('sorted domains', myDomains);
|
|
|
|
if (!getNsAlso) {
|
|
return getSoa(engine, myDomains[0], results, cb, answerSoa);
|
|
}
|
|
|
|
return getNs(engine, /*myDomains.slice(0)*/qnames, results, function (err, results) {
|
|
//console.log('[DEV] getNs complete');
|
|
|
|
if (err) { cb(err, results); return; }
|
|
|
|
// has NS records (or SOA record if NS records match the server itself)
|
|
if (results.authority.length) {
|
|
console.log(results); cb(null, results); return;
|
|
}
|
|
|
|
// myDomains was sorted such that the longest was first
|
|
return getSoa(engine, myDomains[0], results, cb);
|
|
|
|
});
|
|
});
|
|
}
|
|
|
|
if ('SOA' === query.question[0].typeName) {
|
|
return getNsAndSoa(false, true);
|
|
}
|
|
|
|
//console.log('[DEV] QUERY NAME', qname);
|
|
return getRecords(engine, qname, function (err, someRecords) {
|
|
var myRecords;
|
|
var nsRecords = [];
|
|
|
|
if (err) { cb(err); return; }
|
|
|
|
// There are two special cases
|
|
// NS records are returned as ANSWER for NS and ANY, and as AUTHORITY when an externally-delegated domain would return an SOA (no records)
|
|
// SOA records are returned as ANSWER for SOA and ANY, and as AUTHORITY when no records are found, but the domain is controlled here
|
|
|
|
console.log("[DEV] has", someRecords.length, "records");
|
|
|
|
// filter out NS (delegation) records, unless that is what is intended
|
|
someRecords = someRecords.filter(function (r) {
|
|
// If it's not an NS record, it's a potential result
|
|
if ('NS' !== r.type && 'NS' !== r.typeName) {
|
|
return true;
|
|
}
|
|
|
|
console.log("It's NS");
|
|
|
|
// If it's a vanity NS, it's not a valid NS for lookup
|
|
// NOTE: I think that the issue here is EXTERNAL vs INTERNAL vanity NS
|
|
// We _should_ reply for EXTERNAL vanity NS... but not when it's listed on the SOA internally?
|
|
// It's surrounding the problem of what if I do sub domain delegation to the same server.
|
|
if (-1 === engine.primaryNameservers.indexOf(r.data.toLowerCase())) {
|
|
console.log("It's a vanity NS");
|
|
return false;
|
|
}
|
|
|
|
// If the query was for NS, it's a potential result
|
|
if ('ANY' === query.question[0].typeName || 'NS' === query.question[0].typeName) {
|
|
return true;
|
|
}
|
|
|
|
nsRecords.push(r);
|
|
return false;
|
|
});
|
|
|
|
myRecords = someRecords;
|
|
|
|
// If we had an ANY query then we don't need to filter out results
|
|
if (255 !== query.question[0].type && 'ANY' !== query.question[0].typeName) {
|
|
var hasA = false;
|
|
var hasCname = false;
|
|
|
|
// We should only return the records that match the query,
|
|
// except in the case of A/AAAA in which case we should also collect the CNAME
|
|
myRecords = myRecords.filter(function (r) {
|
|
var passCnames = false;
|
|
|
|
if (!hasA && ('A' === query.question[0].typeName || 'AAAA' === query.question[0].typeName)) {
|
|
passCnames = ('CNAME' === r.type ||'CNAME' === r.typeName);
|
|
hasCname = hasCname || passCnames;
|
|
}
|
|
|
|
hasA = hasA || ('A' === r.type || 'A' === r.typeName || 'AAAA' === r.type || 'AAAA' === r.typeName);
|
|
|
|
return passCnames || ((r.type && r.type === query.question[0].type)
|
|
|| (r.type && r.type === query.question[0].typeName)
|
|
|| (r.typeName && r.typeName === query.question[0].typeName)
|
|
);
|
|
});
|
|
|
|
// A and AAAA will also return CNAME records
|
|
// but we filter out the CNAME records unless there are no A / AAAA records
|
|
if (hasA && hasCname && ('A' === query.question[0].typeName || 'AAAA' === query.question[0].typeName)) {
|
|
myRecords = myRecords.forEach(function (r) {
|
|
return 'CNAME' !== r.type && 'CNAME' !== r.typeName;
|
|
});
|
|
}
|
|
}
|
|
|
|
if (myRecords.length) {
|
|
myRecords.forEach(function (r) {
|
|
results.answer.push(dbToResourceRecord(r));
|
|
});
|
|
results.header.rcode = NOERROR;
|
|
//console.log('[DEV] ANSWER results', results);
|
|
|
|
if (255 === query.question[0].type || 'ANY' === query.question[0].typeName) {
|
|
getNsAndSoa(false, true);
|
|
return;
|
|
}
|
|
cb(null, results);
|
|
return;
|
|
}
|
|
else if (nsRecords.length) {
|
|
nsRecords.forEach(function (r) {
|
|
results.authority.push(dbToResourceRecord(r));
|
|
});
|
|
results.header.rcode = NOERROR;
|
|
//console.log('[DEV] AUTHORITY results', results);
|
|
cb(null, results);
|
|
return;
|
|
}
|
|
|
|
console.log("[DEV] Gonna get NS and SOA");
|
|
|
|
// !myRecords.length
|
|
getNsAndSoa(true);
|
|
});
|
|
};
|
|
|
|
}());
|