digd.js/lib/dns-store.js

266 lines
6.7 KiB
JavaScript

(function () {
'use strict';
/*
module.exports.ask = function (query, cb) {
};
*/
var SUCCESS = 0;
var NXDOMAIN = 3;
var REFUSED = 5;
function getRecords(db, qname) {
var myRecords = db.records.filter(function (r) {
if ('string' !== typeof r.domain) {
return false;
}
// TODO use IN in masterquest (or implement OR)
// Only return single-level wildcard?
if (qname === r.domain || ('*.' + qname.split('.').slice(1).join('.')) === r.domain) {
return true;
}
});
return myRecords;
}
function dbToResourceRecord(r) {
return {
name: r.domain
, 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.value) : undefined
// CNAME, NS, PTR || TXT
, data: -1 !== [ 'CNAME', 'NS', 'PTR', 'TXT' ].indexOf(r.type) ? (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) {
var d = ds.shift();
if (!d) {
results.rcode = NXDOMAIN;
cb(null, results);
return;
}
var qn = d.id.toLowerCase();
getRecords(db, qn).forEach(function (r) {
if ('NS' !== r.type) {
return;
}
results.authority.push({
name: r.domain
, typeName: r.type // NS
, className: 'IN'
, ttl: r.ttl || 300
, data: r.value
});
});
if (results.authority.length) {
cb(null, results);
return;
}
}
function getSoa(db, domain, results, cb) {
var index = Math.floor(Math.random() * domain.nameservers.length) % domain.nameservers.length;
var nameserver = domain.nameservers[index];
results.authority.push({
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
, email_addr: domain.admin
// serial -- the version, for cache-busting of secondary nameservers. suggested format: YYYYMMDDnn
, serial: domain.serial || Math.round((domain.updatedAt || domain.createdAt) / 1000)
, sn: domain.serial || Math.round((domain.updatedAt || domain.createdAt) / 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
});
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: SUCCESS // 0 SUCCESS, 3 NXDOMAIN, 5 REFUSED
}
, question: [], answer: [], authority: [], additional: []
};
var myRecords = getRecords(db, qname);
if (myRecords.length) {
myRecords.forEach(function (r) {
results.answer.push(dbToResourceRecord(r));
});
return;
}
if (!myRecords.length) {
// 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
}
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);
return getNs(db, myDomains.slice(0), results, function (err, results) {
if (err) { cb(err, results); return; }
// has NS records
if (NXDOMAIN !== results.header.rcode) { cb(null, results); return; }
// myDomains was sorted such that the longest was first
getSoa(db, myDomains[0], results, cb);
});
}
/*
query.question.forEach(function (q) {
module.exports.ask(q);
});
*/
};
}());