UNPKG

@hapi/hapi

Version:

HTTP Server framework

605 lines (440 loc) 19.1 kB
'use strict'; const Hoek = require('@hapi/hoek'); const Shot = require('@hapi/shot'); const Teamwork = require('@hapi/teamwork'); const Config = require('./config'); const Core = require('./core'); const Cors = require('./cors'); const Ext = require('./ext'); const Package = require('../package.json'); const Route = require('./route'); const Toolkit = require('./toolkit'); const Validation = require('./validation'); const internals = {}; exports = module.exports = function (options) { const core = new Core(options); return new internals.Server(core); }; internals.Server = class { constructor(core, name, parent) { this._core = core; // Public interface this.app = core.app; this.auth = core.auth.public(this); this.decorations = core.decorations.public; this.cache = internals.cache(this); this.events = core.events; this.info = core.info; this.listener = core.listener; this.load = core.heavy.load; this.methods = core.methods.methods; this.mime = core.mime; this.plugins = core.plugins; this.registrations = core.registrations; this.settings = core.settings; this.states = core.states; this.type = core.type; this.version = Package.version; this.realm = { _extensions: { onPreAuth: new Ext('onPreAuth', core), onCredentials: new Ext('onCredentials', core), onPostAuth: new Ext('onPostAuth', core), onPreHandler: new Ext('onPreHandler', core), onPostHandler: new Ext('onPostHandler', core), onPreResponse: new Ext('onPreResponse', core), onPostResponse: new Ext('onPostResponse', core) }, modifiers: { route: {} }, parent: parent ? parent.realm : null, plugin: name, pluginOptions: {}, plugins: {}, _rules: null, settings: { bind: undefined, files: { relativeTo: undefined } }, validator: null }; // Decorations for (const [property, method] of core.decorations.server.entries()) { this[property] = method; } core.registerServer(this); } _clone(name) { return new internals.Server(this._core, name, this); } bind(context) { Hoek.assert(typeof context === 'object', 'bind must be an object'); this.realm.settings.bind = context; } control(server) { Hoek.assert(server instanceof internals.Server, 'Can only control Server objects'); this._core.controlled = this._core.controlled ?? []; this._core.controlled.push(server); } decoder(encoding, decoder) { return this._core.compression.addDecoder(encoding, decoder); } decorate(type, property, method, options = {}) { Hoek.assert(this._core.decorations.public[type], 'Unknown decoration type:', type); Hoek.assert(property, 'Missing decoration property name'); Hoek.assert(typeof property === 'string' || typeof property === 'symbol', 'Decoration property must be a string or a symbol'); const propertyName = property.toString(); Hoek.assert(propertyName[0] !== '_', 'Property name cannot begin with an underscore:', propertyName); const existing = this._core.decorations[type].get(property); if (options.extend) { Hoek.assert(type !== 'handler', 'Cannot extend handler decoration:', propertyName); Hoek.assert(existing, `Cannot extend missing ${type} decoration: ${propertyName}`); Hoek.assert(typeof method === 'function', `Extended ${type} decoration method must be a function: ${propertyName}`); method = method(existing); } else { Hoek.assert(existing === undefined, `${type[0].toUpperCase() + type.slice(1)} decoration already defined: ${propertyName}`); } if (type === 'handler') { // Handler Hoek.assert(typeof method === 'function', 'Handler must be a function:', propertyName); Hoek.assert(!method.defaults || typeof method.defaults === 'object' || typeof method.defaults === 'function', 'Handler defaults property must be an object or function'); Hoek.assert(!options.extend, 'Cannot extend handler decoration:', propertyName); } else if (type === 'request') { // Request Hoek.assert(!this._core.Request.reserved.includes(property), 'Cannot override the built-in request interface decoration:', propertyName); if (options.apply) { this._core.decorations.requestApply = this._core.decorations.requestApply ?? new Map(); this._core.decorations.requestApply.set(property, method); } else { this._core.Request.prototype[property] = method; } } else if (type === 'response') { // Response Hoek.assert(!this._core.Response.reserved.includes(property), 'Cannot override the built-in response interface decoration:', propertyName); this._core.Response.prototype[property] = method; } else if (type === 'toolkit') { // Toolkit Hoek.assert(!Toolkit.reserved.includes(property), 'Cannot override the built-in toolkit decoration:', propertyName); this._core.toolkit.decorate(property, method); } else { // Server if (typeof property === 'string') { Hoek.assert(!Object.getOwnPropertyNames(internals.Server.prototype).includes(property), 'Cannot override the built-in server interface method:', propertyName); } else { Hoek.assert(!Object.getOwnPropertySymbols(internals.Server.prototype).includes(property), 'Cannot override the built-in server interface method:', propertyName); } this._core.instances.forEach((server) => { server[property] = method; }); } this._core.decorations[type].set(property, method); this._core.decorations.public[type].push(property); } dependency(dependencies, after) { Hoek.assert(this.realm.plugin, 'Cannot call dependency() outside of a plugin'); Hoek.assert(!after || typeof after === 'function', 'Invalid after method'); // Normalize to { plugin: version } if (typeof dependencies === 'string') { dependencies = { [dependencies]: '*' }; } else if (Array.isArray(dependencies)) { const map = {}; for (const dependency of dependencies) { map[dependency] = '*'; } dependencies = map; } this._core.dependencies.push({ plugin: this.realm.plugin, deps: dependencies }); if (after) { this.ext('onPreStart', after, { after: Object.keys(dependencies) }); } } encoder(encoding, encoder) { return this._core.compression.addEncoder(encoding, encoder); } event(event) { this._core.events.registerEvent(event); } expose(key, value, options = {}) { Hoek.assert(this.realm.plugin, 'Cannot call expose() outside of a plugin'); let plugin = this.realm.plugin; if (plugin[0] === '@' && options.scope !== true) { plugin = plugin.replace(/^@([^/]+)\//, ($0, $1) => { return !options.scope ? '' : `${$1}__`; }); } this._core.plugins[plugin] = this._core.plugins[plugin] ?? {}; if (typeof key === 'string') { this._core.plugins[plugin][key] = value; } else { Hoek.merge(this._core.plugins[plugin], key); } } ext(events, method, options) { // (event, method, options) -OR- (events) let promise; if (typeof events === 'string') { if (!method) { const team = new Teamwork.Team(); method = (request, h) => { team.attend(request); return h.continue; }; promise = team.work; } events = { type: events, method, options }; } events = Config.apply('exts', events); for (const event of events) { this._ext(event); } return promise; } _ext(event) { event = Object.assign({}, event); // Shallow cloned event.realm = this.realm; const type = event.type; if (!this._core.extensions.server[type]) { // Realm route extensions if (event.options.sandbox === 'plugin') { Hoek.assert(this.realm._extensions[type], 'Unknown event type', type); return this.realm._extensions[type].add(event); } // Connection route extensions Hoek.assert(this._core.extensions.route[type], 'Unknown event type', type); return this._core.extensions.route[type].add(event); } // Server extensions Hoek.assert(!event.options.sandbox, 'Cannot specify sandbox option for server extension'); Hoek.assert(type !== 'onPreStart' || this._core.phase === 'stopped', 'Cannot add onPreStart (after) extension after the server was initialized'); event.server = this; this._core.extensions.server[type].add(event); } async inject(options) { let settings = options; if (typeof settings === 'string') { settings = { url: settings }; } if (!settings.authority || settings.auth || settings.app || settings.plugins || settings.allowInternals !== undefined) { // Can be false settings = Object.assign({}, settings); // options can be reused (shallow cloned) delete settings.auth; delete settings.app; delete settings.plugins; delete settings.allowInternals; settings.authority = settings.authority ?? this._core.info.host + ':' + this._core.info.port; } Hoek.assert(!options.credentials, 'options.credentials no longer supported (use options.auth)'); if (options.auth) { Hoek.assert(typeof options.auth === 'object', 'options.auth must be an object'); Hoek.assert(options.auth.credentials, 'options.auth.credentials is missing'); Hoek.assert(options.auth.strategy, 'options.auth.strategy is missing'); } const needle = this._core._dispatch({ auth: options.auth, allowInternals: options.allowInternals, app: options.app, plugins: options.plugins, isInjected: true }); const res = await Shot.inject(needle, settings); const custom = res.raw.res[Config.symbol]; if (custom) { delete res.raw.res[Config.symbol]; res.request = custom.request; if (custom.error) { throw custom.error; } if (custom.result !== undefined) { res.result = custom.result; } } if (res.result === undefined) { res.result = res.payload; } return res; } log(tags, data) { return this._core.log(tags, data); } lookup(id) { Hoek.assert(id && typeof id === 'string', 'Invalid route id:', id); const record = this._core.router.ids.get(id); if (!record) { return null; } return record.route.public; } match(method, path, host) { Hoek.assert(method && typeof method === 'string', 'Invalid method:', method); Hoek.assert(path && typeof path === 'string' && path[0] === '/', 'Invalid path:', path); Hoek.assert(!host || typeof host === 'string', 'Invalid host:', host); const match = this._core.router.route(method.toLowerCase(), path, host); Hoek.assert(match !== this._core.router.specials.badRequest, 'Invalid path:', path); if (match === this._core.router.specials.notFound) { return null; } return match.route.public; } method(name, method, options = {}) { return this._core.methods.add(name, method, options, this.realm); } path(relativeTo) { Hoek.assert(relativeTo && typeof relativeTo === 'string', 'relativeTo must be a non-empty string'); this.realm.settings.files.relativeTo = relativeTo; } async register(plugins, options = {}) { if (this.realm.modifiers.route.prefix || this.realm.modifiers.route.vhost) { options = Hoek.clone(options); options.routes = options.routes ?? {}; options.routes.prefix = (this.realm.modifiers.route.prefix ?? '') + (options.routes.prefix ?? '') || undefined; options.routes.vhost = this.realm.modifiers.route.vhost ?? options.routes.vhost; } options = Config.apply('register', options); ++this._core.registring; try { const items = [].concat(plugins); for (let item of items) { /* { register, ...attributes } { plugin: { register, ...attributes }, options, once, routes } { plugin: { plugin: { register, ...attributes } }, options, once, routes } // Required module */ if (!item.plugin) { item = { plugin: item }; } else if (!item.plugin.register) { item = { options: item.options, once: item.once, routes: item.routes, plugin: item.plugin.plugin }; } else if (typeof item === 'function') { item = Object.assign({}, item); // Shallow cloned } item = Config.apply('plugin', item); const name = item.plugin.name ?? item.plugin.pkg.name; const clone = this._clone(name); clone.realm.modifiers.route.prefix = item.routes.prefix ?? options.routes.prefix; clone.realm.modifiers.route.vhost = item.routes.vhost ?? options.routes.vhost; clone.realm.pluginOptions = item.options ?? {}; // Validate requirements const requirements = item.plugin.requirements; Hoek.assert(!requirements.node || Config.versionMatch(process.version, requirements.node), 'Plugin', name, 'requires node version', requirements.node, 'but found', process.version); Hoek.assert(!requirements.hapi || Config.versionMatch(this.version, requirements.hapi), 'Plugin', name, 'requires hapi version', requirements.hapi, 'but found', this.version); // Protect against multiple registrations if (this._core.registrations[name]) { if (item.plugin.once || item.once || options.once) { continue; } Hoek.assert(item.plugin.multiple, 'Plugin', name, 'already registered'); } else { this._core.registrations[name] = { version: item.plugin.version ?? item.plugin.pkg.version, name, options: item.options }; } if (item.plugin.dependencies) { clone.dependency(item.plugin.dependencies); } // Register await item.plugin.register(clone, item.options ?? {}); } } finally { --this._core.registring; } return this; } route(options) { Hoek.assert(typeof options === 'object', 'Invalid route options'); options = [].concat(options); for (const config of options) { if (Array.isArray(config.method)) { for (const method of config.method) { const settings = Object.assign({}, config); // Shallow cloned settings.method = method; this._addRoute(settings, this); } } else { this._addRoute(config, this); } } } _addRoute(config, server) { const route = new Route(config, server); // Do no use config beyond this point, use route members const vhosts = [].concat(route.settings.vhost ?? '*'); for (const vhost of vhosts) { const record = this._core.router.add({ method: route.method, path: route.path, vhost, analysis: route._analysis, id: route.settings.id }, route); route.fingerprint = record.fingerprint; route.params = record.params; } this.events.emit('route', route.public); Cors.options(route.public, server); } rules(processor, options = {}) { Hoek.assert(!this.realm._rules, 'Server realm rules already defined'); const settings = Config.apply('rules', options); if (settings.validate) { const schema = settings.validate.schema; settings.validate.schema = Validation.compile(schema, null, this.realm, this._core); } this.realm._rules = { processor, settings }; } state(name, options) { this.states.add(name, options); } table(host) { return this._core.router.table(host); } validator(validator) { Hoek.assert(!this.realm.validator, 'Validator already set'); this.realm.validator = Validation.validator(validator); } start() { return this._core._start(); } initialize() { return this._core._initialize(); } stop(options) { return this._core._stop(options); } }; internals.cache = (plugin) => { const policy = function (options, _segment) { return this._core._cachePolicy(options, _segment, plugin.realm); }; policy.provision = async (opts) => { const clients = plugin._core._createCache(opts); // Start cache if (['initialized', 'starting', 'started'].includes(plugin._core.phase)) { await Promise.all(clients.map((client) => client.start())); } }; return policy; };