forked from coolaj86/goldilocks.js
		
	
		
			
				
	
	
		
			390 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			390 lines
		
	
	
		
			10 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' }
 | |
|     , challenge_type: { type: 'string' }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // the dns control modules for DDNS
 | |
| , 'dns@oauth3.org': {
 | |
|     type: 'object'
 | |
|   , required: [ 'token_id' ]
 | |
|   , properties: {
 | |
|       token_id: { type: 'string' }
 | |
|     }
 | |
|   }
 | |
| };
 | |
| // forward is basically the same as proxy, but specifies the relevant incoming port(s).
 | |
| // only allows for the raw transport layers (TCP/UDP)
 | |
| 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 addDomainRequirement(itemSchema) {
 | |
|   var result = Object.assign({}, itemSchema);
 | |
|   result.required = (result.required || []).concat('domains');
 | |
|   result.properties = Object.assign({}, result.properties);
 | |
|   result.properties.domains = { type: 'array', items: { type: 'string' }, minLength: 1};
 | |
|   return result;
 | |
| }
 | |
| 
 | |
| function toSchemaRef(name) {
 | |
|   return { '$ref': '/modules/'+name };
 | |
| }
 | |
| var moduleRefs = {
 | |
|   http: [ 'proxy', 'static', 'redirect' ].map(toSchemaRef)
 | |
| , tls:  [ 'proxy', 'acme' ].map(toSchemaRef)
 | |
| , tcp:  [ 'forward' ].map(toSchemaRef)
 | |
| , udp:  [ 'forward' ].map(toSchemaRef)
 | |
| , ddns: [ 'dns@oauth3.org' ].map(toSchemaRef)
 | |
| };
 | |
| 
 | |
| // TCP is a bit special in that it has a module that doesn't operate based on domain name
 | |
| // (ie forward), and a modules that does (ie proxy). It therefore has different module
 | |
| // when part of the `domains` config, and when not part of the `domains` config the proxy
 | |
| // modules must have the `domains` property while forward should not have it.
 | |
| moduleRefs.tcp.push(addDomainRequirement(toSchemaRef('proxy')));
 | |
| 
 | |
| var domainSchema = {
 | |
|   type: 'array'
 | |
| , items: {
 | |
|     type: 'object'
 | |
|   , properties: {
 | |
|       id:      { type: 'string' }
 | |
|     , names:   { type: 'array', items: { type: 'string' }, minLength: 1}
 | |
|     , modules: {
 | |
|         type: 'object'
 | |
|       , properties: {
 | |
|           tls:  { type: 'array', items: { oneOf: moduleRefs.tls }}
 | |
|         , http: { type: 'array', items: { oneOf: moduleRefs.http }}
 | |
|         , ddns: { type: 'array', items: { oneOf: moduleRefs.ddns }}
 | |
|         , tcp:  { type: 'array', items: { oneOf: ['proxy'].map(toSchemaRef)}}
 | |
|         }
 | |
|       , additionalProperties: false
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| };
 | |
| 
 | |
| var httpSchema = {
 | |
|   type: 'object'
 | |
| , properties: {
 | |
|     modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.http }) }
 | |
| 
 | |
|     // 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' }
 | |
| 
 | |
|     // these are forbidden deprecated settings.
 | |
|   , bind:    { not: {} }
 | |
|   , domains: { not: {} }
 | |
|   }
 | |
| };
 | |
| 
 | |
| var tlsSchema = {
 | |
|   type: 'object'
 | |
| , properties: {
 | |
|     modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.tls }) }
 | |
| 
 | |
|     // these are forbidden deprecated settings.
 | |
|   , acme:    { not: {} }
 | |
|   , bind:    { not: {} }
 | |
|   , domains: { not: {} }
 | |
|   }
 | |
| };
 | |
| 
 | |
| var tcpSchema = {
 | |
|   type: 'object'
 | |
| , required: [ 'bind' ]
 | |
| , properties: {
 | |
|     bind:    { type: 'array', items: portSchema, minLength: 1 }
 | |
|   , modules: { type: 'array', items: { oneOf: moduleRefs.tcp }}
 | |
|   }
 | |
| };
 | |
| 
 | |
| var udpSchema = {
 | |
|   type: 'object'
 | |
| , properties: {
 | |
|     bind:    { type: 'array', items: portSchema }
 | |
|   , modules: { type: 'array', items: { oneOf: moduleRefs.udp }}
 | |
|   }
 | |
| };
 | |
| 
 | |
| 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: {
 | |
|     loopback: {
 | |
|       type: 'object'
 | |
|     , required: [ 'type', 'domain' ]
 | |
|     , properties: {
 | |
|         type:   { type: 'string', const: 'tunnel@oauth3.org' }
 | |
|       , domain: { type: 'string'}
 | |
|       }
 | |
|     }
 | |
|   , tunnel: {
 | |
|       type: 'object'
 | |
|     , required: [ 'type', 'token_id' ]
 | |
|     , properties: {
 | |
|         type:  { type: 'string', const: 'tunnel@oauth3.org' }
 | |
|       , token_id: { type: 'string'}
 | |
|       }
 | |
|     }
 | |
|   , modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.ddns })}
 | |
|   }
 | |
| };
 | |
| var socks5Schema = {
 | |
|   type: 'object'
 | |
| , properties: {
 | |
|     enabled: { type: 'boolean' }
 | |
|   , port:    portSchema
 | |
|   }
 | |
| };
 | |
| var deviceSchema = {
 | |
|   type: 'object'
 | |
| , properties: {
 | |
|     hostname: { type: 'string' }
 | |
|   }
 | |
| };
 | |
| 
 | |
| var mainSchema = {
 | |
|   type: 'object'
 | |
| , required: [ 'domains', 'http', 'tls', 'tcp', 'udp', 'mdns', 'ddns' ]
 | |
| , properties: {
 | |
|     domains:domainSchema
 | |
|   , http:   httpSchema
 | |
|   , tls:    tlsSchema
 | |
|   , tcp:    tcpSchema
 | |
|   , udp:    udpSchema
 | |
|   , 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 IdList extends Array {
 | |
|   constructor(rawList) {
 | |
|     super();
 | |
|     if (Array.isArray(rawList)) {
 | |
|       Object.assign(this, JSON.parse(JSON.stringify(rawList)));
 | |
|     }
 | |
|     this._itemName = 'item';
 | |
|   }
 | |
| 
 | |
|   findId(id) {
 | |
|     return Array.prototype.find.call(this, function (dom) {
 | |
|       return dom.id === id;
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   add(item) {
 | |
|     item.id = require('crypto').randomBytes(4).toString('hex');
 | |
|     this.push(item);
 | |
|   }
 | |
| 
 | |
|   update(id, update) {
 | |
|     var item = this.findId(id);
 | |
|     if (!item) {
 | |
|       var error = new Error("no "+this._itemName+" with ID '"+id+"'");
 | |
|       error.statusCode = 404;
 | |
|       throw error;
 | |
|     }
 | |
|     Object.assign(this.findId(id), update);
 | |
|   }
 | |
| 
 | |
|   remove(id) {
 | |
|     var index = this.findIndex(function (dom) {
 | |
|       return dom.id === id;
 | |
|     });
 | |
|     if (index < 0) {
 | |
|       var error = new Error("no "+this._itemName+" with ID '"+id+"'");
 | |
|       error.statusCode = 404;
 | |
|       throw error;
 | |
|     }
 | |
|     this.splice(index, 1);
 | |
|   }
 | |
| }
 | |
| class ModuleList extends IdList {
 | |
|   constructor(rawList) {
 | |
|     super(rawList);
 | |
|     this._itemName = 'module';
 | |
|   }
 | |
| 
 | |
|   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 IdList {
 | |
|   constructor(rawList) {
 | |
|     super(rawList);
 | |
|     this._itemName = 'domain';
 | |
|     this.forEach(function (dom) {
 | |
|       dom.modules = {
 | |
|         http: new ModuleList((dom.modules || {}).http)
 | |
|       , tls:  new ModuleList((dom.modules || {}).tls)
 | |
|       , ddns: new ModuleList((dom.modules || {}).ddns)
 | |
|       , tcp:  new ModuleList((dom.modules || {}).tcp)
 | |
|       };
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   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 modLists = {
 | |
|       http: new ModuleList()
 | |
|     , tls:  new ModuleList()
 | |
|     , ddns: new ModuleList()
 | |
|     , tcp:  new ModuleList()
 | |
|     };
 | |
|     // We add these after instead of in the constructor to run the validation and manipulation
 | |
|     // in the ModList add function since these are all new modules.
 | |
|     if (dom.modules) {
 | |
|       Object.keys(modLists).forEach(function (key) {
 | |
|         if (Array.isArray(dom.modules[key])) {
 | |
|           dom.modules[key].forEach(modLists[key].add, modLists[key]);
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     dom.id = require('crypto').randomBytes(4).toString('hex');
 | |
|     dom.modules = modLists;
 | |
|     this.push(dom);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ConfigChanger {
 | |
|   constructor(start) {
 | |
|     Object.assign(this, JSON.parse(JSON.stringify(start)));
 | |
|     delete this.device;
 | |
|     delete this.debug;
 | |
| 
 | |
|     this.domains = new DomainList(this.domains);
 | |
|     this.http.modules = new ModuleList(this.http.modules);
 | |
|     this.tls.modules  = new ModuleList(this.tls.modules);
 | |
|     this.tcp.modules  = new ModuleList(this.tcp.modules);
 | |
|     this.udp.modules  = new ModuleList(this.udp.modules);
 | |
|     this.ddns.modules = new ModuleList(this.ddns.modules);
 | |
|   }
 | |
| 
 | |
|   update(update) {
 | |
|     var self = this;
 | |
| 
 | |
|     if (update.domains) {
 | |
|       update.domains.forEach(self.domains.add, self.domains);
 | |
|     }
 | |
|     [ 'http', 'tls', 'tcp', 'udp', 'ddns' ].forEach(function (name) {
 | |
|       if (update[name] && update[name].modules) {
 | |
|         update[name].modules.forEach(self[name].modules.add, self[name].modules);
 | |
|         delete update[name].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;
 |