UNPKG

@beaker/homebase

Version:

An easy-to-administer hosting server for Hyperdrive.

296 lines (241 loc) 9.56 kB
/** Homebase config The main source of truth for homebase's config is the yaml file. The user can change that yaml during operation and homebase will reload it and run the changes. The user can also modify the active config using web apis. In that case, we need to serialize the change back into the yaml file. So that we change as little as possible, we maintain the values as given by the user ('canonical'). That way, when we write back to the yaml file, we don't write back a bunch of defaults. The structure of this is the HomebaseConfig classes which wrap canonical. They use getters to provide computed info and defaults. NOTE: we should *not* modify config to avoid writing those edits back to the yaml! */ const os = require('os') const path = require('path') const fs = require('fs') const EventEmitter = require('events') const yaml = require('js-yaml') const untildify = require('untildify') const isDomain = require('is-domain-name') const isOrigin = require('is-http-url') const _flatten = require('lodash.flatten') const {ConfigError} = require('./errors') const DEFAULT_CONFIG_DIRECTORY = path.join(os.homedir(), '.homebase') const IS_DEBUG = (['debug', 'staging', 'test'].indexOf(process.env.NODE_ENV) !== -1) // exported api // = class HomebaseConfig { constructor (configPath = false) { this.events = new EventEmitter() // where the config is loaded from this.configPath = null // `canonical` the canonical config // - reflects *only* the values that users set // - first read from the yaml file // - can then be updated by APIs // - *may* be slightly massaged so long as it won't annoy users this.canonical = {} if (configPath) { this.readFromFile(configPath) } } readFromFile (configPath = false) { configPath = configPath || this.configPath this.configPath = configPath var configContents // read file try { configContents = fs.readFileSync(configPath, 'utf8') } catch (e) { // throw if other than a not-found configContents = '' if (e.code !== 'ENOENT') { console.error('Failed to load config file at', configPath) throw e } } // parse try { this.canonical = yaml.safeLoad(configContents) } catch (e) { console.error('Failed to parse config file at', configPath) throw e } this.canonical = this.canonical || {} // validate validate(this.canonical) this.events.emit('read-config') } get directory () { return untildify(this.canonical.directory || DEFAULT_CONFIG_DIRECTORY) } get hyperspaceDirectory () { return path.join(this.directory, 'hyperspace') } get httpMirror () { return this.canonical.httpMirror || false } get ports () { var ports = this.canonical.ports || {} ports.http = ports.http || 80 return ports } get dashboard () { return this.canonical.dashboard || false } get hyperdrives () { return this.canonical.hyperdrives ? this.canonical.hyperdrives.map(v => new HomebaseHyperdriveConfig(v, this)) : [] } get proxies () { return this.canonical.proxies ? this.canonical.proxies.map(v => new HomebaseProxyConfig(v, this)) : [] } get redirects () { return this.canonical.redirects ? this.canonical.redirects.map(v => new HomebaseRedirectConfig(v, this)) : [] } get allVhosts () { return this.hyperdrives.concat(this.proxies).concat(this.redirects) } get hostnames () { return _flatten(this.allVhosts.map(vhostCfg => vhostCfg.hostnames)).filter(Boolean) } } class HomebaseHyperdriveConfig { constructor (canonical, config) { for (var k in canonical) { this[k] = canonical[k] } this.config = config } get id () { return 'hyperdrive-' + this.hyperdriveKey } get vhostType () { return 'hyperdrive' } get hyperdriveKey () { return getHyperdriveKey(this.url) } get hostnames () { return this.domains || [] } get additionalUrls () { var urls = [] this.hostnames.forEach(hostname => { if (this.config.httpMirror) { urls.push('http://' + hostname) } }) return urls } get storageDirectory () { return path.join(this.config.directory, this.hyperdriveKey) } } class HomebaseProxyConfig { constructor (canonical, config) { for (var k in canonical) { this[k] = canonical[k] } } get id () { return 'proxy-' + this.from } get vhostType () { return 'proxy' } get hostnames () { return [this.from] } } class HomebaseRedirectConfig { constructor (canonical, config) { for (var k in canonical) { this[k] = canonical[k] } } get id () { return 'redirect-' + this.from } get vhostType () { return 'redirect' } get hostnames () { return [this.from] } } function getHyperdriveKey (url) { return /^(hyper:\/\/)?([0-9a-f]{64})\/?$/i.exec(url)[2] } function validateHyperdriveCfg (hyperdrive, config) { // regular attributes check(hyperdrive && typeof hyperdrive === 'object', 'hyperdrives.* must be an object, see https://github.com/beakerbrowser/homebase/tree/master#hyperdrives', hyperdrive) hyperdrive.domains = (!hyperdrive.domains || Array.isArray(hyperdrive.domains)) ? hyperdrive.domains : [hyperdrive.domains] check(isHyperdriveUrl(hyperdrive.url), 'hyperdrives.*.url must be a valid hyperdrive url, see https://github.com/beakerbrowser/homebase/tree/master#hyperdrivesurl', hyperdrive.url, 'invalidUrl') // aliases if (hyperdrive.domain && !hyperdrive.domains) { hyperdrive.domains = hyperdrive.domain delete hyperdrive.domain hyperdrive.domains = Array.isArray(hyperdrive.domains) ? hyperdrive.domains : [hyperdrive.domains] } // regular attributes if (hyperdrive.domains) { hyperdrive.domains.forEach(domain => { check(isDomain(domain), 'hyperdrives.*.domains.* must be domain names, see https://github.com/beakerbrowser/homebase/tree/master#hyperdrivesdomains', domain, 'invalidDomain') }) } } module.exports = { HomebaseConfig, HomebaseProxyConfig, HomebaseRedirectConfig } // internal methods // = function validate (config) { // deprecated attributes if ('domain' in config) { console.log('FYI, the domain attribute in your homebase.yml was deprecated in v2.0.0. See https://github.com/beakerbrowser/homebase/tree/master#v200') check(typeof config.domain === 'string', 'domain must be a string, see https://github.com/beakerbrowser/homebase/tree/master#domain') } if ('directory' in config) check(typeof config.directory === 'string', 'directory must be a string, see https://github.com/beakerbrowser/homebase/tree/master#directory') if ('httpMirror' in config) check(typeof config.httpMirror === 'boolean', 'httpMirror must be true or false, see https://github.com/beakerbrowser/homebase/tree/master#httpmirror') if ('ports' in config) check(config.ports && typeof config.ports === 'object', 'ports must be an object containing .http and/or .https, see https://github.com/beakerbrowser/homebase/tree/master#ports') if ('ports' in config && 'http' in config.ports) check(typeof config.ports.http === 'number', 'ports.http must be a number, see https://github.com/beakerbrowser/homebase/tree/master#portshttp') if ('ports' in config && 'https' in config.ports) check(typeof config.ports.https === 'number', 'ports.https must be a number, see https://github.com/beakerbrowser/homebase/tree/master#portshttp') if ('dashboard' in config) check(typeof config.dashboard === 'object' || config.dashboard === false, 'dashboard must be an object or false, see https://github.com/beakerbrowser/homebase/tree/master#dashboard') if (config.dashboard && 'port' in config.dashboard) check(typeof config.dashboard.port === 'number', 'dashboard.port must be a number, see https://github.com/beakerbrowser/homebase/tree/master#dashboardport') if (config.hyperdrives) { config.hyperdrives = Array.isArray(config.hyperdrives) ? config.hyperdrives : [config.hyperdrives] config.hyperdrives.forEach(hyperdriveCfg => validateHyperdriveCfg(hyperdriveCfg, config)) } if (config.proxies) { config.proxies = Array.isArray(config.proxies) ? config.proxies : [config.proxies] config.proxies.forEach(proxy => { check(isDomain(proxy.from), 'proxies.*.from must be a domain name, see https://github.com/beakerbrowser/homebase/tree/master#proxiesfrom', proxy.from) check(isOrigin(proxy.to), 'proxies.*.to must be a target origin, see https://github.com/beakerbrowser/homebase/tree/master#proxiesto', proxy.to) }) } if (config.redirects) { config.redirects = Array.isArray(config.redirects) ? config.redirects : [config.redirects] config.redirects.forEach(redirect => { check(isDomain(redirect.from), 'redirects.*.from must be a domain name, see https://github.com/beakerbrowser/homebase/tree/master#redirectsfrom', redirect.from) check(isOrigin(redirect.to), 'redirects.*.to must be a target origin, see https://github.com/beakerbrowser/homebase/tree/master#redirectsto', redirect.to) // remove trailing slash redirect.to = redirect.to.replace(/\/$/, '') }) } } function check (assertion, error, value, errorKey) { if (!assertion) { var err = new ConfigError(error) err.value = value if (errorKey) { err[errorKey] = true } throw err } } function isHyperdriveUrl (str) { if (typeof str !== 'string') return false return /^(hyper:\/\/)?([0-9a-f]{64})\/?$/i.test(str) }