goldilocks.js/lib/admin/config.js

324 lines
8.3 KiB
JavaScript

'use strict';
var validator = new (require('jsonschema').Validator)();
var recase = require('recase').create({});
var portSchema = { type: 'number', minimum: 1, maximum: 65535 };
var moduleSchemas = {
// the proxy module is common to basically all categories.
proxy: {
type: 'object'
, oneOf: [
{ required: [ 'address' ] }
, { required: [ 'port' ] }
]
, properties: {
address: { type: 'string' }
, host: { type: 'string' }
, port: portSchema
}
}
// redirect and static modules are for HTTP
, redirect: {
type: 'object'
, required: [ 'to', 'from' ]
, properties: {
to: { type: 'string'}
, from: { type: 'string'}
, status: { type: 'integer', minimum: 1, maximum: 999 }
, }
}
, static: {
type: 'object'
, required: [ 'root' ]
, properties: {
root: { type: 'string' }
}
}
// the acme module is for TLS
, acme: {
type: 'object'
, required: [ 'email' ]
, properties: {
email: { type: 'string' }
, server: { type: 'string' }
, challengeType: { type: 'string' }
}
}
};
// forward is basically the name for the TCP proxy
moduleSchemas.forward = JSON.parse(JSON.stringify(moduleSchemas.proxy));
moduleSchemas.forward.required = [ 'ports' ];
moduleSchemas.forward.properties.ports = { type: 'array', items: portSchema };
Object.keys(moduleSchemas).forEach(function (name) {
var schema = moduleSchemas[name];
schema.id = '/modules/'+name;
schema.required = ['id', 'type'].concat(schema.required || []);
schema.properties.id = { type: 'string' };
schema.properties.type = { type: 'string', const: name };
validator.addSchema(schema, schema.id);
});
function addDomainsSchema(base, modList) {
var modSchemas = modList.map(function (name) {
return { '$ref': '/modules/'+name };
});
base.required = [ 'modules', 'domains' ].concat(base.required || []);
base.properties.modules = {
type: 'array'
, items: {
type: 'object'
, required: [ 'domains' ]
, properties: {
domains: { type: 'array', items: { type: 'string' }, minLength: 1}
}
, oneOf: modSchemas
}
};
base.properties.domains = {
type: 'array'
, items: {
type: 'object'
, required: [ 'id', 'names', ]
, properties: {
id: { type: 'string' }
, names: { type: 'array', items: { type: 'string' }, minLength: 1}
, modules: { type: 'array', items: { oneOf: modSchemas }}
}
}
};
}
var httpSchema = {
type: 'object'
, properties: {
// These properties should be snake_case to match the API and config format
primary_domain: { type: 'string' }
, allow_insecure: { type: 'boolean' }
, trust_proxy: { type: 'boolean' }
, }
};
addDomainsSchema(httpSchema, ['proxy', 'static', 'redirect']);
var tlsSchema = {
type: 'object'
, properties: {
acme: {
type: 'object'
// These properties should be snake_case to match the API and config format
, required: [ 'email', 'approved_domains' ]
, properties: {
email: { type: 'string' }
, server: { type: 'string' }
, challenge_type: { type: 'string' }
, approved_domains: { type: 'array', items: { type: 'string' }, minLength: 1}
}
}
}
};
addDomainsSchema(tlsSchema, ['proxy', 'acme']);
var tcpSchema = {
type: 'object'
, required: [ 'bind' ]
, properties: {
bind: { type: 'array', items: portSchema, minLength: 1 }
, modules: { type: 'array', items: { '$ref': '/modules/forward' }}
}
};
var dnsSchema = {
type: 'object'
, properties: {
bind: { type: 'array', items: portSchema }
, modules: { type: 'array', items: { '$ref': '/modules/proxy' }}
}
};
var mdnsSchema = {
type: 'object'
, required: [ 'port', 'broadcast', 'ttl' ]
, properties: {
port: portSchema
, broadcast: { type: 'string' }
, ttl: { type: 'integer', minimum: 0, maximum: 2147483647 }
}
};
var ddnsSchema = {
type: 'object'
, properties: {
enabled: { type: 'boolean' }
}
};
var socks5Schema = {
type: 'object'
, properties: {
enabled: { type: 'boolean' }
, port: portSchema
}
};
var deviceSchema = {
type: 'object'
, properties: {
hostname: { type: 'string' }
}
};
var mainSchema = {
type: 'object'
, required: [ 'http', 'tls', 'tcp', 'dns', 'mdns', 'ddns' ]
, properties: {
http: httpSchema
, tls: tlsSchema
, tcp: tcpSchema
, dns: dnsSchema
, mdns: mdnsSchema
, ddns: ddnsSchema
, socks5: socks5Schema
, device: deviceSchema
}
, additionalProperties: false
};
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;