'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.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 = { _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); } , _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 , 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.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) { if (-1 !== engine.zones._immutableKeys.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); }); } , create: function (zone, cb) { var newZone = { id: crypto.randomBytes(16).toString('hex') }; var existing; var nss = []; zone.name = (zone.name||'').toLowerCase(); db.zones.some(function (z) { if (z.name === zone.name) { existing = z; return true; } }); if (existing) { cb(new Error("tried to create new zone, but '" + existing.name + "' already exists")); return; } newZone.name = zone.name; newZone.createdAt = Date.now(); newZone.updatedAt = newZone.createdAt; 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; } db.primaryNameservers.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.push(newZone); 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; db.zones.filter(notDeleted).some(function (z) { if (zoneId === z.id) { zone = z; return true; } }); if (!zone) { process.nextTick(function () { cb(null, null); }); return; } records = []; db.records.filter(notDeleted).forEach(function (r) { if (zone.name === r.zone) { 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; };