diff --git a/bin/goldilocks.js b/bin/goldilocks.js index f7b59c3..abca131 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -202,9 +202,9 @@ var tcpProm; function fillConfig(config, args) { config.debug = config.debug || args.debug; - if (!config.ddns) { - config.ddns = { enabled: false }; - } + config.socks5 = config.socks5 || { enabled: false }; + config.ddns = config.ddns || { enabled: false }; + // Use Object.assign to copy any real config values over the default values so we can // easily make sure all the fields we need exist . var mdnsDefaults = { disabled: false, port: 5353, broadcast: '224.0.0.251', ttl: 300 }; diff --git a/lib/admin/config.js b/lib/admin/config.js new file mode 100644 index 0000000..9bcb688 --- /dev/null +++ b/lib/admin/config.js @@ -0,0 +1,201 @@ +'use strict'; + +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 = { + // 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 = deepCopy(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 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 + } +, additionalProperties: false +}; + +module.exports.validate = function (config) { + return validator.validate(recase.snakeCopy(config), mainSchema).errors; +}; diff --git a/package-lock.json b/package-lock.json index 11aca53..a09087b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1029,6 +1029,11 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, + "jsonschema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.0.tgz", + "integrity": "sha512-XDJApzBauMg0TinJNP4iVcJl99PQ4JbWKK7nwzpOIkAOVveDKgh/2xm41T3x7Spu4PWMhnnQpNJmUSIUgl6sKg==" + }, "jsonwebtoken": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz", @@ -1967,14 +1972,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" }, - "stream-pair": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-pair/-/stream-pair-1.0.3.tgz", - "integrity": "sha1-vIdY/jnTgQuva3VMj5BI8PuRNn0=", - "requires": { - "readable-stream": "2.2.11" - } - }, "string_decoder": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.2.tgz", diff --git a/package.json b/package.json index c5e56ee..e764379 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "human-readable-ids": "git+https://git.daplie.com/Daplie/human-readable-ids-js#master", "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", "js-yaml": "^3.8.3", + "jsonschema": "^1.2.0", "jsonwebtoken": "^7.4.0", "le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", "le-challenge-fs": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master",