(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.getRecords({ 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) { 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.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, 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(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, 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 !== engine.primaryNameservers.indexOf(ns.data.toLowerCase()); })) { results.authority.length = 0; results.authority.push(domainToSoa(engine.primaryNameservers, d)); results.header.rcode = NXDOMAIN; } cb(null, results); return; }); } function domainToSoa(primaryNameservers, domain) { var nameservers = domain.vanityNs || 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(primaryNameservers, domain, results, cb, answerSoa) { console.log('[DEV] getSoa entered'); if (!answerSoa) { results.authority.push(domainToSoa(primaryNameservers, domain)); } else { results.answer.push(domainToSoa(primaryNameservers, 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(qarr.join('.').toLowerCase()); qarr.shift(); // first } console.log('[DEV] getNsAlso?', getNsAlso); console.log('[DEV] answerSoa?', answerSoa); console.log('[DEV] qnames'); console.log(qnames); return engine.getSoas({ names: 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.primaryNameservers, myDomains[0], results, cb, answerSoa); } return getNs(engine, /*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(engine.primaryNameservers, 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); }); }; }());