467 lines
12 KiB
JavaScript
467 lines
12 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
/*
|
|
module.exports.ask = function (query, cb) {
|
|
};
|
|
*/
|
|
|
|
var NOERROR = 0;
|
|
var NXDOMAIN = 3;
|
|
var REFUSED = 5;
|
|
|
|
function getRecords(db, qname, cb) {
|
|
var delMe = {};
|
|
var dns = require('dns');
|
|
// SECURITY XXX TODO var dig = require('dig.js/dns-request');
|
|
var count;
|
|
var myRecords = db.records.slice(0).filter(function (r) {
|
|
|
|
if ('string' !== typeof r.name) {
|
|
return false;
|
|
}
|
|
|
|
// TODO use IN in masterquest (or implement OR)
|
|
// Only return single-level wildcard?
|
|
if (qname === r.name || ('*.' + qname.split('.').slice(1).join('.')) === r.name) {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
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) {
|
|
r.id = r.id || Math.random();
|
|
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.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(db, ds, results, cb) {
|
|
console.log('[DEV] getNs entered with domains', ds);
|
|
|
|
var d = ds.shift();
|
|
console.log('[DEV] trying another one', d);
|
|
|
|
if (!d) {
|
|
results.header.rcode = NXDOMAIN;
|
|
cb(null, results);
|
|
return;
|
|
}
|
|
|
|
var qn = d.id.toLowerCase();
|
|
|
|
return getRecords(db, 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(db, ds, results, cb);
|
|
}
|
|
|
|
// d.vanityNs should only be vanity nameservers (pointing to this same server)
|
|
if (d.vanityNs || results.authority.some(function (ns) {
|
|
console.log('[debug] ns', ns);
|
|
return -1 !== db.primaryNameservers.indexOf(ns.data.toLowerCase());
|
|
})) {
|
|
results.authority.length = 0;
|
|
results.authority.push(domainToSoa(db, d));
|
|
results.header.rcode = NXDOMAIN;
|
|
}
|
|
cb(null, results);
|
|
return;
|
|
});
|
|
}
|
|
|
|
function domainToSoa(db, domain) {
|
|
var nameservers = domain.vanityNs || db.primaryNameservers;
|
|
|
|
var index = Math.floor(Math.random() * nameservers.length) % nameservers.length;
|
|
var nameserver = nameservers[index];
|
|
return {
|
|
name: domain.id
|
|
, typeName: 'SOA'
|
|
, className: 'IN'
|
|
, ttl: domain.ttl || 60
|
|
|
|
// nameserver -- select an NS at random if they're all in sync
|
|
, primary: nameserver
|
|
, name_server: nameserver
|
|
|
|
// admin -- email address or domain for admin
|
|
, admin: domain.admin || ('admin.' + domain.id)
|
|
, email_addr: domain.admin || ('admin.' + domain.id)
|
|
|
|
// serial -- the version, for cache-busting of secondary nameservers. suggested format: YYYYMMDDnn
|
|
, serial: domain.serial || Math.round((domain.updatedAt || domain.createdAt || 0) / 1000)
|
|
, sn: domain.serial || Math.round((domain.updatedAt || domain.createdAt || 0) / 1000)
|
|
|
|
// refresh -- only used when nameservers following the DNS NOTIFY spec talk
|
|
, refresh: domain.refresh || 1800
|
|
, ref: domain.refresh || 1800
|
|
|
|
// retry -- only used when nameservers following the DNS NOTIFY spec talk
|
|
, retry: domain.retry || 600
|
|
, ret: domain.retry || 600
|
|
|
|
// expiration -- how long other nameservers should continue when the primary goes down
|
|
, expiration: domain.expiration || 2419200
|
|
, ex: domain.expiration || 2419200
|
|
|
|
// minimum -- how long to cache a non-existent domain (also the default ttl for BIND)
|
|
, minimum: domain.minimum || 5
|
|
, nx: domain.minimum || 5
|
|
};
|
|
}
|
|
|
|
function getSoa(db, domain, results, cb, answerSoa) {
|
|
console.log('[DEV] getSoa entered');
|
|
|
|
if (!answerSoa) {
|
|
results.authority.push(domainToSoa(db, domain));
|
|
} else {
|
|
results.answer.push(domainToSoa(db, domain));
|
|
}
|
|
|
|
cb(null, results);
|
|
return;
|
|
}
|
|
|
|
module.exports.query = function (input, 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 db;
|
|
var qname;
|
|
try {
|
|
db = require(input);
|
|
} catch(e) { cb(e); return; }
|
|
|
|
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(qarr.join('.').toLowerCase());
|
|
qarr.shift(); // first
|
|
}
|
|
|
|
console.log('[DEV] getNsAlso?', getNsAlso);
|
|
console.log('[DEV] answerSoa?', answerSoa);
|
|
console.log('[DEV] qnames');
|
|
console.log(qnames);
|
|
var myDomains = db.domains.filter(function (d) {
|
|
return -1 !== qnames.indexOf(d.id.toLowerCase());
|
|
});
|
|
|
|
// 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(db, myDomains[0], results, cb, answerSoa);
|
|
}
|
|
|
|
return getNs(db, /*myDomains.slice(0)*/qnames.map(function (qn) { return { id: qn }; }), 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(db, myDomains[0], results, cb);
|
|
|
|
});
|
|
}
|
|
|
|
if ('SOA' === query.question[0].typeName) {
|
|
return getNsAndSoa(false, true);
|
|
}
|
|
|
|
//console.log('[DEV] QUERY NAME', qname);
|
|
return getRecords(db, 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
|
|
if (-1 !== db.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 (255 !== query.question[0].type && 'ANY' !== query.question[0].typeName) {
|
|
myRecords = myRecords.filter(function (r) {
|
|
|
|
return ((r.type && r.type === query.question[0].type)
|
|
|| (r.type && r.type === query.question[0].typeName)
|
|
|| (r.typeName && r.typeName === query.question[0].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);
|
|
});
|
|
};
|
|
|
|
}());
|