'use strict'; module.exports.create = function (opts) { // opts = { filepath }; var engine = { db: null }; function notDeleted(r) { return !r.revokedAt && !r.deletedAt; } var db = require(opts.filepath); var stat = require('fs').statSync(opts.filepath); var crypto = require('crypto'); // // Manual Migration // db.primaryNameservers.forEach(function (ns, i, arr) { if ('string' === typeof ns) { ns = { name: ns }; arr[i] = ns; } if (!ns.id) { ns.id = crypto.randomBytes(16).toString('hex'); } }); db.zones = db.zones || []; if (db.domains) { db.zones = db.zones.concat(db.domains); } db.zones.forEach(function (zone) { if (!zone.name) { zone.name = zone.id; zone.id = null; } if (!zone.id) { zone.id = crypto.randomBytes(16).toString('hex'); } if (!zone.createdAt) { zone.createdAt = stat.mtime.valueOf(); } if (!zone.updatedAt) { zone.updatedAt = stat.mtime.valueOf(); } }); db.records.forEach(function (record) { if (!record.id) { record.id = crypto.randomBytes(16).toString('hex'); } }); require('fs').writeFileSync(opts.filepath, JSON.stringify(db, null, 2)); // // End Migration // db.save = function (cb) { if (db.save._saving) { console.log('make pending'); db.save._pending.push(cb); return; } db.save._saving = true; require('fs').writeFile(opts.filepath, JSON.stringify(db, null, 2), function (err) { console.log('done writing'); var pending = db.save._pending.splice(0); db.save._saving = false; cb(err); if (!pending.length) { return; } db.save(function (err) { console.log('double save'); pending.forEach(function (cb) { cb(err); }); }); }); }; db.save._pending = []; engine.primaryNameservers = db.primaryNameservers; engine.zoneToSoa = 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 , 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 || 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 }; }; engine.peers = { all: function (cb) { var dns = require('dns'); var count = db.primaryNameservers.length; function gotRecord() { count -= 1; if (!count) { cb(null, db.primaryNameservers); } } function getRecord(ns) { dns.resolve4(ns.name, function (err, addresses) { console.log('ns addresses:'); console.log(addresses); if (err) { console.error(err); gotRecord(); return; } ns.type = 'A'; ns.address = addresses[0]; gotRecord(); }); } db.primaryNameservers.forEach(getRecord); } }; engine.zones = { all: function (cb) { process.nextTick(function () { cb(null, db.zones.slice(0).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) { var existing; db.zones.some(function (z) { if (z.id && zone.id === z.id) { existing = z; return true; } if (z.name && zone.name === z.name) { existing = z; return true; } }); if (!existing) { cb(null, null); return; } existing.updatedAt = new Date().valueOf(); // toISOString(); console.log('touch saving...'); db.save(function (err) { cb(err, !err && existing || null); }); } , 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) { var existing; var dirty; db.zones.some(function (z) { if (z.id === zone.id) { existing = z; return true; } }); if (!existing) { console.log('no existing zone'); cb(new Error("zone for '" + zone.id + "' does not exist"), null); return; } console.log('found existing zone'); console.log(existing); console.log(zone); Object.keys(zone).forEach(function (key) { var keys = [ 'name', 'id', 'revokedAt', 'changedAt', 'insertedAt', 'updatedAt', 'deletedAt' ]; if (-1 !== keys.indexOf(key)) { return; } if (existing[key] !== zone[key]) { dirty = true; console.log('existing key', key, existing[key], zone[key]); existing[key] = zone[key]; } }); zone.updatedAt = new Date().valueOf(); // toISOString(); // Math.round(Date.now() / 1000); if (dirty) { zone.changedAt = zone.updatedAt; } console.log('saving...'); db.save(function (err) { cb(err, !err && existing || 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; };