(function () { 'use strict'; /* module.exports.ask = function (query, cb) { }; */ var NOERROR = 0; var NXDOMAIN = 3; var REFUSED = 5; function getRecords(db, qname, cb) { 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; } }); process.nextTick(function () { cb(null, 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) { console.log('[DEV] getNs entered'); var d = ds.shift(); 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.domain , typeName: r.type // NS , className: 'IN' , ttl: r.ttl || 300 , data: r.address || r.value || r.data }; 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) { console.log('[DEV] getSoa entered'); results.authority.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: [] }; return getRecords(db, qname, function (err, myRecords) { if (err) { cb(err); return; } if (myRecords.length) { myRecords.forEach(function (r) { results.answer.push(dbToResourceRecord(r)); }); results.header.rcode = NOERROR; console.log('[DEV] results', results); cb(null, results); 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) { 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 getSoa(db, myDomains[0], results, cb); }); } }); }; }());