493 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			493 lines
		
	
	
		
			14 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
 | 
						|
      // 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 === 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 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);
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
}());
 |