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 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);
 | |
|   });
 | |
| };
 | |
| 
 | |
| }());
 |