'use strict'; var crypto = require('crypto'); var dns = require('dns'); var os = require('os'); var path = require('path'); var pathResolvers = { '.': function fromCwd(relPath) { return path.join(process.cwd(), relPath); }, '~': function fromHomedir(relPath) { if (!os.homedir) { throw new Error( 'Resolving home directory relative paths is not supported in this version of node.' ); } return path.join(os.homedir(), relPath); }, noop: function (p) { return p; } } module.exports.create = function (opts) { // opts = { filepath }; // `opts.filepath` is a module id or path to a module that contains a store plugin or file var pathFn = pathResolvers[opts.filepath[0]] || pathResolvers.noop; var storeId = pathFn(opts.filepath); var pathToStore = require.resolve(storeId); var engine = { db: null }; function notDeleted(r) { return !r.revokedAt && !r.deletedAt; } // instantiate the DB module var db = (pathToStore.slice(-5) === '.json') ? // JSON files should be loaded using our built in store.json.js require('./store.json.js')(pathToStore) : // everything else should be loaded as a module and passed our opts object require(storeId)(opts); // TODO: examine usage of engine.primaryNameservers to see if we are supporting it right engine.primaryNameservers = db.primaryNameservers.list; engine.peers = { all: function (cb) { var pNS = db.primaryNameservers.list(); function getRecord(ns, done) { dns.resolve4(ns.name, function (err, addresses) { console.log('ns addresses:'); console.log(addresses); if (err) { console.error(err); done(); return; } ns.type = 'A'; ns.address = addresses[0]; done(); }); } // resolve addreses for all of the primary nameservers in parallel pNS.forEach(function (ns) { var status = { pending: true }; function done() { status.pending = false; // TODO: determine if the locally stored records should get updated var incomplete = tasks.filter(function (s) { return s.pending; }); if (incomplete.length < 1) { cb(null, pNS); } } getRecord(ns, done); return status; }); } }; engine.zones = { _immutableKeys: [ 'id', 'name', 'primary', 'serial', 'revokedAt', 'changedAt', 'insertedAt', 'updatedAt', 'deletedAt' ] , _mutableKeys: [ 'admin', 'expiration', 'minimum', 'refresh', 'retry', 'ttl', 'vanity' ] , _dateToSerial: function (date) { // conventionally the format is YYYYMMDDxx, // but since it's an integer and I don't want to keep track of incrementing xx, // epoch in seconds will do return parseInt(Math.round(date/1000).toString().slice(-10), 10); } // NOTE/TODO: despite the _, _toSoa is used outside this file (in lib/digd.js and lib/httpd.js) , _toSoa: function (domain) { var nameservers = domain.vanityNs || engine.primaryNameservers().map(function (n) { return n.name; }); var index = Math.floor(Math.random() * nameservers.length) % nameservers.length; var nameserver = nameservers[index]; return { id: domain.id , name: domain.name , 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 // default is effectively admin@{domain name} , admin: domain.admin || ('admin.' + domain.name) , email_addr: domain.admin || ('admin.' + domain.name) // serial -- the version, for cache-busting of secondary nameservers. suggested format: YYYYMMDDnn , serial: domain.serial || engine.zones._dateToSerial(domain.updatedAt || domain.createdAt || Date.now()) , sn: domain.serial || engine.zones._dateToSerial(domain.updatedAt || domain.createdAt || Date.now()) // 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 // 4 weeks , ex: domain.expiration || 2419200 // 4 weeks // minimum -- how long to cache a non-existent domain (also the default ttl for BIND) , minimum: domain.minimum || 5 , nx: domain.minimum || 5 }; } , all: function (cb) { process.nextTick(function () { cb(null, db.zones().filter(notDeleted)); }); } , get: function (queries, cb) { if (!Array.isArray(queries)) { queries = queries.names.map(function (n) { return { name: n }; }); } var myDomains = db.zones().filter(function (d) { return queries.some(function (q) { return (d.name.toLowerCase() === q.name) && notDeleted(d); }); }); process.nextTick(function () { cb(null, myDomains); }); } , touch: function (zone, cb) { db.zones.get(zone, function (err, existing) { if (err || !existing) { cb(err, null); return; } existing.updatedAt = new Date().valueOf(); // toISOString(); console.log('touch saving...'); db.zone.update(existing, function (err) { cb(err, !err && existing || null); }); return; }); } , save: function (zone, cb) { if (zone.id) { console.log('update zone!'); engine.zones.update(zone, cb); } else { engine.zones.create(zone, cb); } } , update: function (zone, cb) { db.zones.get({ id: zone.id }, function (err, found) { var dirty; if (err) { console.log('error finding zone'); cb(new Error("Error finding zone for '" + zone.id + "'"), null); return; } if (!found) { console.log('no existing zone'); cb(new Error("zone for '" + zone.id + "' does not exist"), null); return; } console.log('found existing zone'); console.log(found); console.log(zone); Object.keys(zone).forEach(function (key) { if (-1 !== engine.zones._immutableKeys.indexOf(key)) { return; } if (found[key] !== zone[key]) { dirty = true; console.log('existing key', key, found[key], zone[key]); found[key] = zone[key]; } }); found.updatedAt = new Date().valueOf(); // toISOString(); // Math.round(Date.now() / 1000); if (dirty) { found.changedAt = found.updatedAt; } console.log('saving...'); db.zones.update(found, function (err) { cb(err, !err && found || null); }); }); } , create: function (zone, cb) { var zoneName = (zone.name||'').toLowerCase(); db.zones.get({ name: zoneName }, function (err, found) { if (err) { console.error(err); cb(new Error("error attempting to create new zone '" + zoneName + "'")); return; } if (found) { cb(new Error("tried to create new zone, but '" + found.name + "' already exists")); return; } var newZone = { id: crypto.randomBytes(16).toString('hex'), name: zoneName }; var nss = []; newZone.createdAt = Date.now(); newZone.updatedAt = newZone.createdAt; /* Set only the mutable keys in the new zone from the proposed zone object */ Object.keys(zone).forEach(function (key) { //if (-1 !== engine.zones._immutableKeys.indexOf(key)) { return; } if (-1 === engine.zones._mutableKeys.indexOf(key)) { return; } newZone[key] = zone[key]; }); // TODO create NS and A records for normal and vanity nameservers if (zone.vanity) { newZone.vanity = true; } else { newZone.vanity = false; } // TODO: distinguish between primary and secondary zones // TODO: determine if we need to do anything special for delegation // create records for the primary nameservers (or vanity name servers) db.primaryNameservers.list().forEach(function (ns, i) { var nsx = 'ns' + (i + 1); var nsZone; var ttl = 43200; // 12h // TODO pick a well-reasoned number var now = Date.now(); if (zone.vanity) { nsZone = nsx + '.' + newZone.name; } else { nsZone = ns.name; } // NS example.com ns1.example.com 43200 nss.push({ id: crypto.randomBytes(16).toString('hex') , createdAt: Date.now() , updatedAt: Date.now() , changedAt: Date.now() , zone: newZone.name , soa: true , type: 'NS' , data: nsZone , name: newZone.name , ttl: ttl }); // A ns1.example.com 127.0.0.1 43200 nss.push({ id: crypto.randomBytes(16).toString('hex') , createdAt: now , updatedAt: now , changedAt: now , zone: newZone.name , soa: true , type: ns.type , name: nsZone , address: ns.address , ttl: 43200 // 12h // TODO pick a good number }); }); db.zones.create(newZone, function (err) { // WIP: going to need to figure out how to manage this as a transaction // Significant benefit to having records owned by the zone is we won't have // records for zones that don't otherwise exist - at least at the engine level. // every line below this one is not yet modified... }); nss.forEach(function (ns) { db.records.push(ns); }); console.log('[zone] [create] saving...'); db.save(function (err) { cb(err, !err && newZone || null); }); } , destroy: function (zoneId, cb) { var zone; var records; var now = Date.now(); db.zones.filter(notDeleted).some(function (z) { if (zoneId === z.id) { zone = z; z.deletedAt = now; return true; } }); if (!zone) { process.nextTick(function () { cb(null, null); }); return; } records = []; db.records.filter(notDeleted).forEach(function (r) { if (zone.name === r.zone) { r.deletedAt = now; records.push(r); } }); console.log('[zone] [destroy] saving...'); db.save(function (err) { zone.records = records; cb(err, !err && zone || null); }); } }; engine.records = { all: function (cb) { process.nextTick(function () { cb(null, db.records.slice(0).filter(notDeleted)); }); } , one: function (id, cb) { var myRecord; db.records.slice(0).some(function (r) { if (id && id === r.id) { if (notDeleted(r)) { myRecord = r; return true; } return false; } }); process.nextTick(function () { cb(null, myRecord); }); } , get: function (query, cb) { 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 (query.name === r.name || ('*.' + query.name.split('.').slice(1).join('.')) === r.name) { if (notDeleted(r)) { return true; } } }); process.nextTick(function () { cb(null, myRecords); }); } , save: function (record, cb) { function touchZone(err, r) { if (err) { cb(err); } if (!r) { cb(null, null); } engine.zones.touch({ name: r.zone }, cb); } if (record.id) { console.log('update record!'); engine.records.update(record, touchZone); } else { engine.records.create(record, touchZone); } } , update: function (record, cb) { var existing; var dirty; db.records.some(function (r) { if (r.id === record.id) { existing = r; return true; } }); if (!existing) { console.log('no existing record'); cb(new Error("record for '" + record.id + "' does not exist"), null); return; } console.log('found existing record'); console.log(existing); console.log(record); Object.keys(record).forEach(function (key) { var keys = [ 'name', 'id', 'zone', 'revokedAt', 'changedAt', 'insertedAt', 'updatedAt', 'deletedAt' ]; if (-1 !== keys.indexOf(key)) { return; } if (existing[key] !== record[key]) { dirty = true; console.log(existing[key], record[key]); existing[key] = record[key]; } }); record.updatedAt = new Date().valueOf(); // toISOString(); // Math.round(Date.now() / 1000); if (dirty) { record.changedAt = record.updatedAt; } console.log('saving...'); db.save(function (err) { cb(err, !err && existing || null); }); } , create: function (record, cb) { var obj = { id: crypto.randomBytes(16).toString('hex') }; console.log('found existing record'); console.log(record); //var keys = [ 'name', 'id', 'zone', 'revokedAt', 'changedAt', 'insertedAt', 'updatedAt', 'deletedAt' ]; //var okeys = [ 'name', 'zone', 'admin', 'data', 'expiration', 'minimum', 'serial', 'retry', 'refresh', 'ttl', 'type' ]; // primary var okeys = [ 'name', 'zone', 'type', 'data', 'class', 'ttl', 'address' , 'exchange', 'priority', 'port', 'value', 'tag', 'flag', 'aname' ]; okeys.forEach(function (key) { if ('undefined' !== typeof record[key]) { obj[key] = record[key]; } }); record.updatedAt = new Date().valueOf(); // toISOString(); // Math.round(Date.now() / 1000); //record.changedAt = record.updatedAt; record.insertedAt = record.updatedAt; record.createdAt = record.updatedAt; console.log('saving new...'); db.records.push(record); db.save(function (err) { cb(err, record); }); } , destroy: function (id, cb) { var record; db.records.some(function (r/*, i*/) { if (id === r.id) { record = r; r.deletedAt = Date.now(); //record = db.records.splice(i, 1); return true; } }); process.nextTick(function () { db.save(function (err) { cb(err, record); }); }); } }; return engine; };