forked from coolaj86/goldilocks.js
		
	implemented better management of arrays in the config
This commit is contained in:
		
							parent
							
								
									5761ab9d62
								
							
						
					
					
						commit
						485a223f86
					
				@ -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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user