diff --git a/package.json b/package.json index 04d259c173..7fbc56e0d9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "./src/core/runtime/repo-nodejs.js": "./src/core/runtime/repo-browser.js", "./src/core/runtime/dns-nodejs.js": "./src/core/runtime/dns-browser.js", "./test/utils/create-repo-nodejs.js": "./test/utils/create-repo-browser.js", - "stream": "readable-stream" + "stream": "readable-stream", + "joi": "joi-browser" }, "engines": { "node": ">=6.0.0", @@ -119,6 +120,8 @@ "is-ipfs": "^0.3.2", "is-stream": "^1.1.0", "joi": "^13.1.2", + "joi-browser": "^13.0.1", + "joi-multiaddr": "^1.0.1", "libp2p": "~0.18.0", "libp2p-circuit": "~0.1.4", "libp2p-floodsub": "~0.14.1", diff --git a/src/core/config.js b/src/core/config.js new file mode 100644 index 0000000000..493a30e5eb --- /dev/null +++ b/src/core/config.js @@ -0,0 +1,43 @@ +'use strict' + +const Joi = require('joi').extend(require('joi-multiaddr')) + +const schema = Joi.object().keys({ + repo: Joi.alternatives().try( + Joi.object(), // TODO: schema for IPFS repo + Joi.string() + ).allow(null), + init: Joi.alternatives().try( + Joi.boolean(), + Joi.object().keys({ bits: Joi.number().integer() }) + ).allow(null), + start: Joi.boolean(), + pass: Joi.string().allow(''), + EXPERIMENTAL: Joi.object().keys({ + pubsub: Joi.boolean(), + sharding: Joi.boolean(), + dht: Joi.boolean() + }).allow(null), + config: Joi.object().keys({ + Addresses: Joi.object().keys({ + Swarm: Joi.array().items(Joi.multiaddr().options({ convert: false })), + API: Joi.multiaddr().options({ convert: false }), + Gateway: Joi.multiaddr().options({ convert: false }) + }).allow(null), + Discovery: Joi.object().keys({ + MDNS: Joi.object().keys({ + Enabled: Joi.boolean(), + Interval: Joi.number().integer() + }).allow(null), + webRTCStar: Joi.object().keys({ + Enabled: Joi.boolean() + }).allow(null) + }).allow(null), + Bootstrap: Joi.array().items(Joi.multiaddr().IPFS().options({ convert: false })) + }).allow(null), + libp2p: Joi.object().keys({ + modules: Joi.object().allow(null) // TODO: schemas for libp2p modules? + }).allow(null) +}).options({ allowUnknown: true }) + +module.exports.validate = (config) => Joi.attempt(config, schema) diff --git a/src/core/index.js b/src/core/index.js index f9912d25d4..d27d178f85 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -12,6 +12,7 @@ const debug = require('debug') const extend = require('deep-extend') const EventEmitter = require('events') +const config = require('./config') const boot = require('./boot') const components = require('./components') // replaced by repo-browser when running in the browser @@ -27,7 +28,7 @@ class IPFS extends EventEmitter { EXPERIMENTAL: {} } - options = options || {} + options = config.validate(options || {}) this._libp2pModules = options.libp2p && options.libp2p.modules extend(this._options, options) diff --git a/test/core/config.spec.js b/test/core/config.spec.js new file mode 100644 index 0000000000..c90e9454c6 --- /dev/null +++ b/test/core/config.spec.js @@ -0,0 +1,220 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) + +const config = require('../../src/core/config') + +describe('config', () => { + it('should allow empty config', () => { + const cfg = {} + expect(() => config.validate(cfg)).to.not.throw() + }) + + it('should allow undefined config', () => { + const cfg = undefined + expect(() => config.validate(cfg)).to.not.throw() + }) + + it('should allow unknown key at root', () => { + const cfg = { [`${Date.now()}`]: 'test' } + expect(() => config.validate(cfg)).to.not.throw() + }) + + it('should validate valid repo', () => { + const cfgs = [ + { repo: { unknown: 'value' } }, + { repo: '/path/to-repo' }, + { repo: null }, + { repo: undefined } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw()) + }) + + it('should validate invalid repo', () => { + const cfgs = [ + { repo: 138 } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw()) + }) + + it('should validate valid init', () => { + const cfgs = [ + { init: { bits: 138 } }, + { init: { bits: 138, unknown: 'value' } }, + { init: true }, + { init: false }, + { init: null }, + { init: undefined } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw()) + }) + + it('should validate invalid init', () => { + const cfgs = [ + { init: 138 }, + { init: { bits: 'not an int' } } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw()) + }) + + it('should validate valid start', () => { + const cfgs = [ + { start: true }, + { start: false }, + { start: undefined } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw()) + }) + + it('should validate invalid start', () => { + const cfgs = [ + { start: 138 }, + { start: 'make it so number 1' }, + { start: null } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw()) + }) + + it('should validate valid pass', () => { + const cfgs = [ + { pass: 'correctbatteryhorsestaple' }, + { pass: '' }, + { pass: undefined } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw()) + }) + + it('should validate invalid pass', () => { + const cfgs = [ + { pass: 138 }, + { pass: null } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw()) + }) + + it('should validate valid EXPERIMENTAL', () => { + const cfgs = [ + { EXPERIMENTAL: { pubsub: true, dht: true, sharding: true } }, + { EXPERIMENTAL: { pubsub: false, dht: false, sharding: false } }, + { EXPERIMENTAL: { unknown: 'value' } }, + { EXPERIMENTAL: null }, + { EXPERIMENTAL: undefined } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw()) + }) + + it('should validate invalid EXPERIMENTAL', () => { + const cfgs = [ + { EXPERIMENTAL: { pubsub: 138 } }, + { EXPERIMENTAL: { dht: 138 } }, + { EXPERIMENTAL: { sharding: 138 } } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw()) + }) + + it('should validate valid config', () => { + const cfgs = [ + { config: { Addresses: { Swarm: ['/ip4/0.0.0.0/tcp/4002'] } } }, + { config: { Addresses: { Swarm: [] } } }, + { config: { Addresses: { Swarm: undefined } } }, + + { config: { Addresses: { API: '/ip4/127.0.0.1/tcp/5002' } } }, + { config: { Addresses: { API: undefined } } }, + + { config: { Addresses: { Gateway: '/ip4/127.0.0.1/tcp/9090' } } }, + { config: { Addresses: { Gateway: undefined } } }, + + { config: { Addresses: { unknown: 'value' } } }, + { config: { Addresses: null } }, + { config: { Addresses: undefined } }, + + { config: { Discovery: { MDNS: { Enabled: true } } } }, + { config: { Discovery: { MDNS: { Enabled: false } } } }, + { config: { Discovery: { MDNS: { Interval: 138 } } } }, + { config: { Discovery: { MDNS: { unknown: 'value' } } } }, + { config: { Discovery: { MDNS: null } } }, + { config: { Discovery: { MDNS: undefined } } }, + + { config: { Discovery: { webRTCStar: { Enabled: true } } } }, + { config: { Discovery: { webRTCStar: { Enabled: false } } } }, + { config: { Discovery: { webRTCStar: { unknown: 'value' } } } }, + { config: { Discovery: { webRTCStar: null } } }, + { config: { Discovery: { webRTCStar: undefined } } }, + + { config: { Discovery: { unknown: 'value' } } }, + { config: { Discovery: null } }, + { config: { Discovery: undefined } }, + + { config: { Bootstrap: ['/ip4/104.236.176.52/tcp/4001/ipfs/QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z'] } }, + { config: { Bootstrap: [] } }, + + { config: { unknown: 'value' } }, + { config: null }, + { config: undefined } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw()) + }) + + it('should validate invalid config', () => { + const cfgs = [ + { config: { Addresses: { Swarm: 138 } } }, + { config: { Addresses: { Swarm: null } } }, + + { config: { Addresses: { API: 138 } } }, + { config: { Addresses: { API: null } } }, + + { config: { Addresses: { Gateway: 138 } } }, + { config: { Addresses: { Gateway: null } } }, + + { config: { Discovery: { MDNS: { Enabled: 138 } } } }, + { config: { Discovery: { MDNS: { Interval: true } } } }, + + { config: { Discovery: { webRTCStar: { Enabled: 138 } } } }, + + { config: { Bootstrap: ['/ip4/0.0.0.0/tcp/4002'] } }, + { config: { Bootstrap: 138 } }, + + { config: 138 } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw()) + }) + + it('should validate valid libp2p', () => { + const cfgs = [ + { libp2p: { modules: {} } }, + { libp2p: { modules: { unknown: 'value' } } }, + { libp2p: { modules: null } }, + { libp2p: { modules: undefined } }, + { libp2p: { unknown: 'value' } }, + { libp2p: null }, + { libp2p: undefined } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.not.throw()) + }) + + it('should validate invalid libp2p', () => { + const cfgs = [ + { libp2p: { modules: 138 } }, + { libp2p: 138 } + ] + + cfgs.forEach(cfg => expect(() => config.validate(cfg)).to.throw()) + }) +})