digd.js/lib/store/index.js

483 lines
15 KiB
JavaScript

'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;
};