implemented better management of arrays in the config

This commit is contained in:
tigerbot 2017-10-10 11:08:19 -06:00
parent 5761ab9d62
commit 485a223f86
4 changed files with 287 additions and 33 deletions

View File

@ -403,13 +403,108 @@ module.exports.create = function (deps, conf) {
next();
}
};
config.restful.saveConfig = function (req, res) {
console.log('config POST body', req.body);
// Since we are sending the changes to another process we don't really
// have a good way of seeing if it worked, so always report success
deps.storage.config.save(req.body);
res.send({ success: true });
config.restful.saveBaseConfig = function (req, res) {
console.log('config POST body', JSON.stringify(req.body));
deps.PromiseA.resolve().then(function () {
var update;
if (req.params.group) {
update = {};
update[req.params.group] = req.body;
} else {
update = req.body;
}
var changer = new (require('./config').ConfigChanger)(conf);
var errors = changer.update(update);
if (errors.length) {
throw Object.assign(new Error(), errors[0], {statusCode: 400});
}
return deps.storage.config.save(changer);
}).then(function (config) {
if (req.params.group) {
config = config[req.params.group];
}
res.send(deps.recase.snakeCopy(config));
}, function (err) {
res.statusCode = err.statusCode || 500;
err.message = err.message || err.toString();
res.end(JSON.stringify({error: {message: err.message, code: err.code}}));
});
};
config.restful.createModule = function (req, res) {
var group = req.params.group;
var err;
deps.PromiseA.resolve().then(function () {
var changer = new (require('./config').ConfigChanger)(conf);
if (!changer[group] || !changer[group].modules) {
err = new Error("'"+group+"' is not a valid settings group or has not modules");
err.statusCode = 404;
throw err;
}
var modList;
if (req.params.id) {
if (changer[group].domains) {
modList = (changer[group].domains.find(req.params.id) || {}).modules;
}
} else {
modList = changer[group].modules;
}
if (!modList) {
err = new Error("'"+group+"' has no domains list or '"+req.params.id+"' does not exist");
err.statusCode = 404;
throw err;
}
modList.add(req.body);
var errors = changer.validate();
if (errors.length) {
throw Object.assign(new Error(), errors[0], {statusCode: 400});
}
return deps.storage.config.save(changer);
}).then(function (config) {
var base;
if (!req.params.id) {
base = config[group];
} else {
base = config[group].domains.find(function (dom) { return dom.id === req.params.id; });
}
res.send(deps.recase.snakeCopy(base.modules));
}, function (err) {
res.statusCode = err.statusCode || 500;
err.message = err.message || err.toString();
res.end(JSON.stringify({error: {message: err.message, code: err.code}}));
});
};
config.restful.createDomain = function (req, res) {
var group = req.params.group;
var err;
deps.PromiseA.resolve().then(function () {
var changer = new (require('./config').ConfigChanger)(conf);
if (!changer[group] || !changer[group].domains) {
err = new Error("'"+group+"' is not a valid settings group or has no domains list");
err.statusCode = 404;
throw err;
}
changer[group].domains.add(req.body);
var errors = changer.validate();
if (errors.length) {
throw Object.assign(new Error(), errors[0], {statusCode: 400});
}
return deps.storage.config.save(changer);
}).then(function (config) {
res.send(deps.recase.snakeCopy(config[group].domains));
}, function (err) {
res.statusCode = err.statusCode || 500;
err.message = err.message || err.toString();
res.end(JSON.stringify({error: {message: err.message, code: err.code}}));
});
};
var app = require('express')();
@ -425,6 +520,12 @@ module.exports.create = function (deps, conf) {
app.get( '/config/:group', config.restful.readConfig);
app.get( '/config/:group/:name(modules|domains)/:id?', config.restful.readConfig);
app.get( '/config/:group/:name(domains)/:id/:name2(modules)/:id2?', config.restful.readConfig);
app.post( '/config', config.restful.saveConfig);
app.post( '/config', config.restful.saveBaseConfig);
app.post( '/config/:group', config.restful.saveBaseConfig);
app.post( '/config/:group/modules', config.restful.createModule);
app.post( '/config/:group/domains', config.restful.createDomain);
app.post( '/config/:group/domains/:id/modules', config.restful.createModule);
return app;
};

View File

@ -3,23 +3,6 @@
var validator = new (require('jsonschema').Validator)();
var recase = require('recase').create({});
function deepCopy(obj) {
if (!obj || typeof obj !== 'object') {
return obj;
}
var result;
if (Array.isArray(obj)) {
result = [];
} else {
result = {};
}
Object.keys(obj).forEach(function (key) {
result[key] = deepCopy(obj[key]);
});
return result;
}
var portSchema = { type: 'number', minimum: 1, maximum: 65535 };
var moduleSchemas = {
@ -67,7 +50,7 @@ var moduleSchemas = {
}
};
// forward is basically the name for the TCP proxy
moduleSchemas.forward = deepCopy(moduleSchemas.proxy);
moduleSchemas.forward = JSON.parse(JSON.stringify(moduleSchemas.proxy));
moduleSchemas.forward.required = [ 'ports' ];
moduleSchemas.forward.properties.ports = { type: 'array', items: portSchema };
@ -180,6 +163,12 @@ var socks5Schema = {
, port: portSchema
}
};
var deviceSchema = {
type: 'object'
, properties: {
hostname: { type: 'string' }
}
};
var mainSchema = {
type: 'object'
@ -192,10 +181,143 @@ var mainSchema = {
, mdns: mdnsSchema
, ddns: ddnsSchema
, socks5: socks5Schema
, device: deviceSchema
}
, additionalProperties: false
};
module.exports.validate = function (config) {
function validate(config) {
return validator.validate(recase.snakeCopy(config), mainSchema).errors;
};
}
module.exports.validate = validate;
class ModuleList extends Array {
constructor(rawList) {
super();
if (Array.isArray(rawList)) {
Object.assign(this, JSON.parse(JSON.stringify(rawList)));
}
}
find(id) {
return Array.prototype.find.call(this, function (mod) {
return mod.id === id;
});
}
add(mod) {
if (!mod.type) {
throw new Error("module must have a 'type' defined");
}
if (!moduleSchemas[mod.type]) {
throw new Error("invalid module type '"+mod.type+"'");
}
mod.id = require('crypto').randomBytes(4).toString('hex');
this.push(mod);
}
}
class DomainList extends Array {
constructor(rawList) {
super();
if (Array.isArray(rawList)) {
Object.assign(this, JSON.parse(JSON.stringify(rawList)));
}
this.forEach(function (dom) {
dom.modules = new ModuleList(dom.modules);
});
}
find(id) {
return Array.prototype.find.call(this, function (dom) {
return dom.id === id;
});
}
add(dom) {
if (!Array.isArray(dom.names) || !dom.names.length) {
throw new Error("domains must have a non-empty array for 'names'");
}
if (dom.names.some(function (name) { return typeof name !== 'string'; })) {
throw new Error("all domain names must be strings");
}
var modList = new ModuleList();
if (Array.isArray(dom.modules)) {
dom.modules.forEach(function (mod) {
modList.add(mod);
});
}
dom.id = require('crypto').randomBytes(4).toString('hex');
dom.modules = modList;
this.push(dom);
}
}
class ConfigChanger {
constructor(start) {
Object.assign(this, JSON.parse(JSON.stringify(start)));
delete this.device;
this.http.modules = new ModuleList(this.http.modules);
this.http.domains = new DomainList(this.http.domains);
this.tls.modules = new ModuleList(this.tls.modules);
this.tls.domains = new DomainList(this.tls.domains);
this.tcp.modules = new ModuleList(this.tcp.modules);
this.dns.modules = new ModuleList(this.dns.modules);
}
update(update) {
var self = this;
if (update.http && update.http.modules) {
update.http.modules.forEach(self.http.modules.add.bind(self.http.modules));
delete update.http.modules;
}
if (update.http && update.http.domains) {
update.http.domains.forEach(self.http.domains.add.bind(self.http.domains));
delete update.http.domains;
}
if (update.tls && update.tls.modules) {
update.tls.modules.forEach(self.tls.modules.add.bind(self.tls.modules));
delete update.tls.modules;
}
if (update.tls && update.tls.domains) {
update.tls.domains.forEach(self.tls.domains.add.bind(self.tls.domains));
delete update.tls.domains;
}
if (update.tcp && update.tcp.modules) {
update.tcp.modules.forEach(self.tcp.modules.add.bind(self.tcp.modules));
delete update.tcp.modules;
}
if (update.dns && update.dns.modules) {
update.dns.modules.forEach(self.dns.modules.add.bind(self.dns.modules));
delete update.dns.modules;
}
function mergeSettings(orig, changes) {
Object.keys(changes).forEach(function (key) {
// TODO: use an API that can properly handle updating arrays.
if (!changes[key] || (typeof changes[key] !== 'object') || Array.isArray(changes[key])) {
orig[key] = changes[key];
}
else if (!orig[key] || typeof orig[key] !== 'object') {
orig[key] = changes[key];
}
else {
mergeSettings(orig[key], changes[key]);
}
});
}
mergeSettings(this, update);
return validate(this);
}
validate() {
return validate(this);
}
}
module.exports.ConfigChanger = ConfigChanger;

View File

@ -65,14 +65,33 @@ module.exports.create = function (deps, conf) {
}
};
var confCb;
var config = {
save: function (changes) {
deps.messenger.send({
type: 'com.daplie.goldilocks/config'
, changes: changes
});
return new deps.PromiseA(function (resolve, reject) {
var timeoutId = setTimeout(function () {
reject(new Error('Did not receive config update from main process in a reasonable time'));
confCb = null;
}, 15*1000);
confCb = function (config) {
confCb = null;
clearTimeout(timeoutId);
resolve(config);
};
});
}
};
function updateConf(config) {
if (confCb) {
confCb(config);
}
}
var mdnsId = {
_filename: 'mdns-id'
@ -99,6 +118,7 @@ module.exports.create = function (deps, conf) {
return {
owners: owners
, config: config
, updateConf: updateConf
, mdnsId: mdnsId
};
};

View File

@ -1,6 +1,7 @@
'use strict';
var config;
var modules;
// Everything that uses the config should be reading it when relevant rather than
// just at the beginning, so we keep the reference for the main object and just
@ -15,7 +16,13 @@ function update(conf) {
config[key] = conf[key];
}
});
console.log('config', JSON.stringify(config));
console.log('config update', JSON.stringify(config));
Object.values(modules).forEach(function (mod) {
if (typeof mod.updateConf === 'function') {
mod.updateConf(config);
}
});
}
function create(conf) {
@ -38,11 +45,15 @@ function create(conf) {
// HTTP proxying connection creation is not something we currently control.
, net: require('net')
};
deps.storage = require('./storage').create(deps, conf);
deps.proxy = require('./proxy-conn').create(deps, conf);
deps.socks5 = require('./socks5-server').create(deps, conf);
deps.loopback = require('./loopback').create(deps, conf);
deps.ddns = require('./ddns').create(deps, conf);
modules = {
storage: require('./storage').create(deps, conf)
, proxy: require('./proxy-conn').create(deps, conf)
, socks5: require('./socks5-server').create(deps, conf)
, loopback: require('./loopback').create(deps, conf)
, ddns: require('./ddns').create(deps, conf)
};
Object.assign(deps, modules);
require('./goldilocks.js').create(deps, conf);
process.removeListener('message', create);