+
-### Web and services application framework
+# @hapi/hapi
-Lead Maintainer: [Eran Hammer](https://github.com/hueniverse)
+#### The Simple, Secure Framework Developers Trust
-**hapi** is a simple to use configuration-centric framework with built-in support for input validation, caching,
-authentication, and other essential facilities for building web and services applications. **hapi** enables
-developers to focus on writing reusable application logic in a highly modular and prescriptive approach.
+Build powerful, scalable applications, with minimal overhead and full out-of-the-box functionality - your code, your way.
-Version 17.x only supports node v8.9.0 and over. For older version of node please use version 16.x.
+### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support
-Development version: **17.6.x** ([release notes](https://github.com/hapijs/hapi/issues?labels=release+notes&page=1&state=closed))
-[](https://travis-ci.org/hapijs/hapi)
-[](https://ci.appveyor.com/project/hueniverse/hapi)
+## Useful resources
-For the latest updates, [change log](https://hapijs.com/updates), and release information visit [hapijs.com](https://hapijs.com) and follow [@hapijs](https://twitter.com/hapijs) on twitter. If you have questions, please open an issue in the
-[discussion forum](https://github.com/hapijs/discuss).
+- [Documentation and API](https://hapi.dev/)
+- [Version status](https://hapi.dev/resources/status/#hapi) (builds, dependencies, node versions, licenses, eol)
+- [Changelog](https://hapi.dev/resources/changelog/)
+- [Project policies](https://hapi.dev/policies/)
+- [Support](https://hapi.dev/support/)
-# Sponsorship
+## Technical Steering Committee (TSC) Members
-Development of the **hapi** core module generously supported by contributions from [individuals and corporations](https://github.com/hapijs/hapi/blob/master/SPONSORS.md).
-If you are benefiting from **hapi** and would like to help keep the project financially sustainable, please visit
-Eran Hammer's [Patreon page](https://www.patreon.com/eranhammer) or contact him [via email](mailto:eran@hammer.io).
-
-## Featured Sponsors
-
-[](https://bit.ly/auth0h-rn)
-
-## Active Supporters
-
-- **CNN Digital**
-- **[Contentful](https://www.contentful.com/)**
-- The product development team at **Creative Artists Agency**
-- **[First + Third](https://firstandthird.com)**
-
-## Legacy Supporters
-
-Past major financial support for the project was provided by:
-- Benjamin Flesch and [StriveWire](https://strivewire.com/)
-- [Condé Nast Technology](https://technology.condenast.com/)
-- [](https://lob.com/)
+ - Devin Ivy ([@devinivy](https://github.com/devinivy))
+ - Lloyd Benson ([@lloydbenson](https://github.com/lloydbenson))
+ - Nathan LaFreniere ([@nlf](https://github.com/nlf))
+ - Wyatt Lyon Preul ([@geek](https://github.com/geek))
+ - Nicolas Morel ([@marsup](https://github.com/marsup))
+ - Jonathan Samines ([@jonathansamines](https://github.com/jonathansamines))
diff --git a/SPONSORS.md b/SPONSORS.md
index c75767110..3a1841fdf 100755
--- a/SPONSORS.md
+++ b/SPONSORS.md
@@ -1,64 +1,15 @@
-# Leaders
+We'd like to thank our sponsors as well as the legacy sponsors who have supported hapi throughout the years. Thanks so much for your support!
-- Alexander Alimovs
-- Alvin Lumowa
-- Andres Garcia / Florence Healthcare
-- Auth0
-- Benjamin Flesch / StriveWire
-- Biao Xie
-- Bryan Sapot
-- Christian Giacomi
-- CNN Digital
-- CodePilot.ai
-- Condé Nast Technology
-- Contentful
-- Dave Hyndman
-- David Roncancio
-- Dmitry Savinkov
-- Elba Sánchez Márquez
-- Eric Lanehart
-- [gitconnected](https://gitconnected.com)
-- Greg Allen
-- Healthline Media
-- Jeremy Whitbred
-- John Brett
-- Julian Lannigan
-- Lob
-- Louis Beltramo
-- Mahesh Babu R
-- Manny Pamintuan
-- NZZ Storytelling
-- Paulo Vieira
-- Peter Ka
-- Raffi Minassian
-- Raquel Hernandez
-- Saul Maddox
-- Shane Warren
-- Srikanth Minnam
-- Sven Lito
-- Tim Boudreau
-- Umut Şirin
-- Yehor Sergeenko
+> Below are hapi's top recurring sponsors, but there are many more to thank. For the complete list, see [hapi.dev/policies/sponsors](https://hapi.dev/policies/sponsors/) or [hapijs/.github/SPONSORS.md](https://github.com/hapijs/.github/blob/master/SPONSORS.md).
-# Supporters
+# Staff Sponsors
-- Adilson Schmitt Junior
-- Andreas Zeuch
-- Chris St
-- Dave Stevens
-- Essau Ramirez
-- Finnegan
-- Geraint Corneu
-- Luke Bond
-- Marcos Bérgamo
-- Max Fierro
-- Nathan Buchar
-- Nicklas Laine Overgaard
-- Oakley Hall
-- PIer Bover
-- Robin van der Vleuten
-- Sheridan Tighe
-- Troy Whiteley
-- Viswanadha Pratap Kondoju
-- [quickytools](https://www.quickytools.com)
+- [Big Room Studios](https://www.bigroomstudios.com/)
+- [Dixeed](https://dixeed.com/)
+# Top Sponsors
+
+- Fabian Gündel / [DataWrapper.de](https://www.datawrapper.de/)
+- Devin Stewart
+- [Raider.IO](https://raider.io/)
+- [Florence Healthcare](https://florencehc.com/)
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index 26baf4d2a..000000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-environment:
- matrix:
- - nodejs_version: "8"
- - nodejs_version: "10"
-
-install:
- - ps: Install-Product node $env:nodejs_version
- - npm install
-
-test_script:
- - node --version
- - npm --version
- - npm test
-
-build: off
diff --git a/images/17.png b/images/17.png
deleted file mode 100755
index 035f2e561..000000000
Binary files a/images/17.png and /dev/null differ
diff --git a/images/hapi.png b/images/hapi.png
deleted file mode 100755
index 92938ede7..000000000
Binary files a/images/hapi.png and /dev/null differ
diff --git a/lib/auth.js b/lib/auth.js
index 85bc7c732..e3de0fc7f 100755
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -1,16 +1,13 @@
'use strict';
-// Load modules
-
-const Boom = require('boom');
-const Bounce = require('bounce');
-const Hoek = require('hoek');
+const Boom = require('@hapi/boom');
+const Bounce = require('@hapi/bounce');
+const Hoek = require('@hapi/hoek');
const Config = require('./config');
+const Request = require('./request');
-// Declare internals
-
const internals = {
missing: Symbol('missing')
};
@@ -18,47 +15,62 @@ const internals = {
exports = module.exports = internals.Auth = class {
+ #core = null;
+ #schemes = {};
+ #strategies = {};
+
+ api = {}; // Do not reassign api or settings, as they are referenced in public()
+ settings = {
+ default: null // Strategy used as default if route has no auth settings
+ };
+
constructor(core) {
- this._core = core;
- this._schemes = {};
- this._strategies = {};
- this.settings = {
- default: null // Strategy used as default if route has no auth settings
- };
+ this.#core = core;
+ }
- this.api = {};
+ public(server) {
+
+ return {
+ api: this.api,
+ settings: this.settings,
+ scheme: this.scheme.bind(this),
+ strategy: this._strategy.bind(this, server),
+ default: this.default.bind(this),
+ test: this.test.bind(this),
+ verify: this.verify.bind(this),
+ lookup: this.lookup.bind(this)
+ };
}
scheme(name, scheme) {
Hoek.assert(name, 'Authentication scheme must have a name');
- Hoek.assert(!this._schemes[name], 'Authentication scheme name already exists:', name);
+ Hoek.assert(!this.#schemes[name], 'Authentication scheme name already exists:', name);
Hoek.assert(typeof scheme === 'function', 'scheme must be a function:', name);
- this._schemes[name] = scheme;
+ this.#schemes[name] = scheme;
}
_strategy(server, name, scheme, options = {}) {
Hoek.assert(name, 'Authentication strategy must have a name');
Hoek.assert(typeof options === 'object', 'options must be an object');
- Hoek.assert(name !== 'bypass', 'Cannot use reserved strategy name: bypass');
- Hoek.assert(!this._strategies[name], 'Authentication strategy name already exists');
+ Hoek.assert(!this.#strategies[name], 'Authentication strategy name already exists');
Hoek.assert(scheme, 'Authentication strategy', name, 'missing scheme');
- Hoek.assert(this._schemes[scheme], 'Authentication strategy', name, 'uses unknown scheme:', scheme);
+ Hoek.assert(this.#schemes[scheme], 'Authentication strategy', name, 'uses unknown scheme:', scheme);
server = server._clone();
- const strategy = this._schemes[scheme](server, options);
+ const strategy = this.#schemes[scheme](server, options);
Hoek.assert(strategy.authenticate, 'Invalid scheme:', name, 'missing authenticate() method');
Hoek.assert(typeof strategy.authenticate === 'function', 'Invalid scheme:', name, 'invalid authenticate() method');
Hoek.assert(!strategy.payload || typeof strategy.payload === 'function', 'Invalid scheme:', name, 'invalid payload() method');
Hoek.assert(!strategy.response || typeof strategy.response === 'function', 'Invalid scheme:', name, 'invalid response() method');
- strategy.options = strategy.options || {};
+ strategy.options = strategy.options ?? {};
Hoek.assert(strategy.payload || !strategy.options.payload, 'Cannot require payload validation without a payload method');
- this._strategies[name] = {
+ this.#strategies[name] = {
methods: strategy,
realm: server.realm
};
@@ -75,7 +87,7 @@ exports = module.exports = internals.Auth = class {
this.settings.default = this._setupRoute(Hoek.clone(options)); // Prevent changes to options
- const routes = this._core.router.table();
+ const routes = this.#core.router.table();
for (const route of routes) {
route.rebuild();
}
@@ -84,7 +96,7 @@ exports = module.exports = internals.Auth = class {
async test(name, request) {
Hoek.assert(name, 'Missing authentication strategy name');
- const strategy = this._strategies[name];
+ const strategy = this.#strategies[name];
Hoek.assert(strategy, 'Unknown authentication strategy:', name);
const bind = strategy.methods;
@@ -99,7 +111,30 @@ exports = module.exports = internals.Auth = class {
throw response.error;
}
- return response.data.credentials;
+ return response.data;
+ }
+
+ async verify(request) {
+
+ const auth = request.auth;
+
+ if (auth.error) {
+ throw auth.error;
+ }
+
+ if (!auth.isAuthenticated) {
+ return;
+ }
+
+ const strategy = this.#strategies[auth.strategy];
+ Hoek.assert(strategy, 'Unknown authentication strategy:', auth.strategy);
+
+ if (!strategy.methods.verify) {
+ return;
+ }
+
+ const bind = strategy.methods;
+ await strategy.methods.verify.call(bind, auth);
}
static testAccess(request, route) {
@@ -136,10 +171,10 @@ exports = module.exports = internals.Auth = class {
options = Hoek.applyToDefaults(this.settings.default, options);
}
- path = path || 'default strategy';
- Hoek.assert(options.strategies && options.strategies.length, 'Missing authentication strategy:', path);
+ path = path ?? 'default strategy';
+ Hoek.assert(options.strategies?.length, 'Missing authentication strategy:', path);
- options.mode = options.mode || 'required';
+ options.mode = options.mode ?? 'required';
if (options.entity !== undefined || // Backwards compatibility with <= 11.x.x
options.scope !== undefined) {
@@ -161,7 +196,7 @@ exports = module.exports = internals.Auth = class {
let hasAuthenticatePayload = false;
for (const name of options.strategies) {
- const strategy = this._strategies[name];
+ const strategy = this.#strategies[name];
Hoek.assert(strategy, 'Unknown authentication strategy', name, 'in', path);
Hoek.assert(strategy.methods.payload || options.payload !== 'required', 'Payload validation can only be required when all strategies support it in', path);
@@ -199,7 +234,7 @@ exports = module.exports = internals.Auth = class {
}
for (const name of config.strategies) {
- const strategy = this._strategies[name];
+ const strategy = this.#strategies[name];
if (strategy.methods[type]) {
return true;
}
@@ -224,14 +259,14 @@ exports = module.exports = internals.Auth = class {
// Injection bypass
if (request.auth.credentials) {
- internals.validate(null, { credentials: request.auth.credentials, artifacts: request.auth.artifacts }, 'bypass', config, request, errors);
+ internals.validate(null, { credentials: request.auth.credentials, artifacts: request.auth.artifacts }, request.auth.strategy, config, request, errors);
return;
}
// Try each strategy
for (const name of config.strategies) {
- const strategy = this._strategies[name];
+ const strategy = this.#strategies[name];
const bind = strategy.methods;
const realm = strategy.realm;
@@ -269,9 +304,7 @@ exports = module.exports = internals.Auth = class {
_access(request, route) {
const config = this.lookup(route || request.route);
- if (!config ||
- !config.access) {
-
+ if (!config?.access) {
return true;
}
@@ -341,21 +374,20 @@ exports = module.exports = internals.Auth = class {
static async payload(request) {
- if (!request.auth.isAuthenticated ||
- request.auth.strategy === 'bypass') {
-
+ if (!request.auth.isAuthenticated || !request.auth[Request.symbols.authPayload]) {
return;
}
const auth = request._core.auth;
- const strategy = auth._strategies[request.auth.strategy];
+ const strategy = auth.#strategies[request.auth.strategy];
+ Hoek.assert(strategy, 'Unknown authentication strategy:', request.auth.strategy);
if (!strategy.methods.payload) {
return;
}
const config = auth.lookup(request.route);
- const setting = config.payload || (strategy.methods.options.payload ? 'required' : false);
+ const setting = config.payload ?? (strategy.methods.options.payload ? 'required' : false);
if (!setting) {
return;
}
@@ -367,22 +399,23 @@ exports = module.exports = internals.Auth = class {
if (response.isBoom &&
response.isMissing) {
- return (setting === 'optional' ? undefined : Boom.unauthorized('Missing payload authentication'));
+ return setting === 'optional' ? undefined : Boom.unauthorized('Missing payload authentication');
}
return response;
}
- static async response(request) {
+ static async response(response) {
+ const request = response.request;
const auth = request._core.auth;
- if (!request.auth.isAuthenticated ||
- request.auth.strategy === 'bypass') {
-
+ if (!request.auth.isAuthenticated) {
return;
}
- const strategy = auth._strategies[request.auth.strategy];
+ const strategy = auth.#strategies[request.auth.strategy];
+ Hoek.assert(strategy, 'Unknown authentication strategy:', request.auth.strategy);
+
if (!strategy.methods.response) {
return;
}
@@ -399,23 +432,31 @@ exports = module.exports = internals.Auth = class {
internals.setupScope = function (access) {
+ // No scopes
+
if (!access.scope) {
return false;
}
+ // Already setup
+
+ if (!Array.isArray(access.scope)) {
+ return access.scope;
+ }
+
const scope = {};
for (const value of access.scope) {
const prefix = value[0];
- const type = (prefix === '+' ? 'required' : (prefix === '!' ? 'forbidden' : 'selection'));
- const clean = (type === 'selection' ? value : value.slice(1));
- scope[type] = scope[type] || [];
+ const type = prefix === '+' ? 'required' : (prefix === '!' ? 'forbidden' : 'selection');
+ const clean = type === 'selection' ? value : value.slice(1);
+ scope[type] = scope[type] ?? [];
scope[type].push(clean);
- if ((!scope._parameters || !scope._parameters[type]) &&
+ if ((!scope._hasParameters?.[type]) &&
/{([^}]+)}/.test(clean)) {
- scope._parameters = scope._parameters || {};
- scope._parameters[type] = true;
+ scope._hasParameters = scope._hasParameters ?? {};
+ scope._hasParameters[type] = true;
}
}
@@ -425,52 +466,54 @@ internals.setupScope = function (access) {
internals.validate = function (err, result, name, config, request, errors) { // err can be Boom, Error, or a valid response object
- result = result || {};
-
- // Unauthenticated
+ result = result ?? {};
+ request.auth.isAuthenticated = !err;
if (err) {
+
+ // Non-error response
+
if (err instanceof Error === false) {
request._log(['auth', 'unauthenticated', 'response', name], { statusCode: err.statusCode });
- return err; // Non-error response
+ return err;
}
- if (err.isMissing) {
-
- // Try next strategy
+ // Missing authenticated
+ if (err.isMissing) {
request._log(['auth', 'unauthenticated', 'missing', name], err);
errors.push(err.output.headers['WWW-Authenticate']);
return internals.missing;
}
+ }
- if (config.mode === 'try') {
- request.auth.isAuthenticated = false;
- request.auth.strategy = name;
- request.auth.credentials = result.credentials;
- request.auth.artifacts = result.artifacts;
- request.auth.error = err;
- request._log(['auth', 'unauthenticated', 'try', name], err);
- return;
- }
+ request.auth.strategy = name;
+ request.auth.credentials = result.credentials;
+ request.auth.artifacts = result.artifacts;
- request._log(['auth', 'unauthenticated', 'error', name], err);
- throw err;
+ // Authenticated
+
+ if (!err) {
+ return;
}
- // Authenticated
+ // Unauthenticated
- const credentials = result.credentials;
- request.auth.strategy = name;
- request.auth.credentials = credentials;
- request.auth.artifacts = result.artifacts;
- request.auth.isAuthenticated = true;
+ request.auth.error = err;
+
+ if (config.mode === 'try') {
+ request._log(['auth', 'unauthenticated', 'try', name], err);
+ return;
+ }
+
+ request._log(['auth', 'unauthenticated', 'error', name], err);
+ throw err;
};
internals.expandScope = function (request, scope) {
- if (!scope._parameters) {
+ if (!scope._hasParameters) {
return scope;
}
@@ -486,9 +529,7 @@ internals.expandScope = function (request, scope) {
internals.expandScopeType = function (request, scope, type) {
- if (!scope[type] ||
- !scope._parameters[type]) {
-
+ if (!scope._hasParameters[type]) {
return scope[type];
}
@@ -515,7 +556,7 @@ internals.validateScope = function (credentials, scope, type) {
}
const count = typeof credentials.scope === 'string' ?
- (scope[type].indexOf(credentials.scope) !== -1 ? 1 : 0) :
+ scope[type].indexOf(credentials.scope) !== -1 ? 1 : 0 :
Hoek.intersect(scope[type], credentials.scope).length;
if (type === 'forbidden') {
diff --git a/lib/compression.js b/lib/compression.js
index 2afc6cd41..3e4c692e4 100755
--- a/lib/compression.js
+++ b/lib/compression.js
@@ -1,15 +1,11 @@
'use strict';
-// Load modules
-
const Zlib = require('zlib');
-const Accept = require('accept');
-const Bounce = require('bounce');
-const Hoek = require('hoek');
-
+const Accept = require('@hapi/accept');
+const Bounce = require('@hapi/bounce');
+const Hoek = require('@hapi/hoek');
-// Declare internals
const internals = {
common: ['gzip, deflate', 'deflate, gzip', 'gzip', 'deflate', 'gzip, deflate, br']
@@ -18,52 +14,59 @@ const internals = {
exports = module.exports = internals.Compression = class {
- constructor() {
+ decoders = {
+ gzip: (options) => Zlib.createGunzip(options),
+ deflate: (options) => Zlib.createInflate(options)
+ };
+
+ encodings = ['identity', 'gzip', 'deflate'];
- this.encodings = ['identity', 'gzip', 'deflate'];
- this._encoders = {
- identity: null,
- gzip: (options) => Zlib.createGzip(options),
- deflate: (options) => Zlib.createDeflate(options)
- };
+ encoders = {
+ identity: null,
+ gzip: (options) => Zlib.createGzip(options),
+ deflate: (options) => Zlib.createDeflate(options)
+ };
- this._decoders = {
- gzip: (options) => Zlib.createGunzip(options),
- deflate: (options) => Zlib.createInflate(options)
- };
+ #common = null;
+
+ constructor() {
this._updateCommons();
}
_updateCommons() {
- this._common = new Map();
- internals.common.forEach((header) => {
+ this.#common = new Map();
- this._common.set(header, Accept.encoding(header, this.encodings));
- });
+ for (const header of internals.common) {
+ this.#common.set(header, Accept.encoding(header, this.encodings));
+ }
}
addEncoder(encoding, encoder) {
- Hoek.assert(this._encoders[encoding] === undefined, `Cannot override existing encoder for ${encoding}`);
+ Hoek.assert(this.encoders[encoding] === undefined, `Cannot override existing encoder for ${encoding}`);
Hoek.assert(typeof encoder === 'function', `Invalid encoder function for ${encoding}`);
- this._encoders[encoding] = encoder;
+ this.encoders[encoding] = encoder;
this.encodings.unshift(encoding);
this._updateCommons();
}
addDecoder(encoding, decoder) {
- Hoek.assert(this._decoders[encoding] === undefined, `Cannot override existing decoder for ${encoding}`);
+ Hoek.assert(this.decoders[encoding] === undefined, `Cannot override existing decoder for ${encoding}`);
Hoek.assert(typeof decoder === 'function', `Invalid decoder function for ${encoding}`);
- this._decoders[encoding] = decoder;
+ this.decoders[encoding] = decoder;
}
accept(request) {
const header = request.headers['accept-encoding'];
- const common = this._common.get(header);
+ if (!header) {
+ return 'identity';
+ }
+
+ const common = this.#common.get(header);
if (common) {
return common;
}
@@ -81,9 +84,14 @@ exports = module.exports = internals.Compression = class {
encoding(response, length) {
+ if (response.settings.compressed) {
+ response.headers['content-encoding'] = response.settings.compressed;
+ return null;
+ }
+
const request = response.request;
if (!request._core.settings.compression ||
- (length !== null && length < request._core.settings.compression.minBytes)) {
+ length !== null && length < request._core.settings.compression.minBytes) {
return null;
}
@@ -99,12 +107,12 @@ exports = module.exports = internals.Compression = class {
return null;
}
- return (request.info.acceptEncoding === 'identity' ? null : request.info.acceptEncoding);
+ return request.info.acceptEncoding === 'identity' ? null : request.info.acceptEncoding;
}
encoder(request, encoding) {
- const encoder = this._encoders[encoding];
+ const encoder = this.encoders[encoding];
Hoek.assert(encoder !== undefined, `Unknown encoding ${encoding}`);
return encoder(request.route.settings.compression[encoding]);
}
diff --git a/lib/config.js b/lib/config.js
index c24905b17..2b668f97d 100755
--- a/lib/config.js
+++ b/lib/config.js
@@ -1,14 +1,10 @@
'use strict';
-// Load modules
-
const Os = require('os');
-const Joi = require('joi');
-const Hoek = require('hoek');
-
+const Somever = require('@hapi/somever');
+const Validate = require('@hapi/validate');
-// Declare internals
const internals = {};
@@ -16,17 +12,21 @@ const internals = {};
exports.symbol = Symbol('hapi-response');
-exports.apply = function (type, options, message) {
+exports.apply = function (type, options, ...message) {
+
+ const result = internals[type].validate(options);
+
+ if (result.error) {
+ throw new Error(`Invalid ${type} options ${message.length ? '(' + message.join(' ') + ')' : ''} ${result.error.annotate()}`);
+ }
- const result = Joi.validate(options, internals[type]);
- Hoek.assert(!result.error, 'Invalid', type, 'options', message ? '(' + message + ')' : '', result.error && result.error.annotate());
return result.value;
};
exports.enable = function (options) {
- const settings = (options ? Hoek.shallow(options) : {});
+ const settings = options ? Object.assign({}, options) : {}; // Shallow cloned
if (settings.security === true) {
settings.security = {};
@@ -39,23 +39,24 @@ exports.enable = function (options) {
return settings;
};
+exports.versionMatch = (version, range) => Somever.match(version, range, { includePrerelease: true });
-internals.access = Joi.object({
- entity: Joi.valid('user', 'app', 'any'),
- scope: [false, Joi.array().items(Joi.string()).single().min(1)]
+internals.access = Validate.object({
+ entity: Validate.valid('user', 'app', 'any'),
+ scope: [false, Validate.array().items(Validate.string()).single().min(1)]
});
-internals.auth = Joi.alternatives([
- Joi.string(),
+internals.auth = Validate.alternatives([
+ Validate.string(),
internals.access.keys({
- mode: Joi.valid('required', 'optional', 'try'),
- strategy: Joi.string(),
- strategies: Joi.array().items(Joi.string()).min(1),
- access: Joi.array().items(internals.access.min(1)).single().min(1),
+ mode: Validate.valid('required', 'optional', 'try'),
+ strategy: Validate.string(),
+ strategies: Validate.array().items(Validate.string()).min(1),
+ access: Validate.array().items(internals.access.min(1)).single().min(1),
payload: [
- Joi.valid('required', 'optional'),
- Joi.boolean()
+ Validate.valid('required', 'optional'),
+ Validate.boolean()
]
})
.without('strategy', 'strategies')
@@ -63,139 +64,145 @@ internals.auth = Joi.alternatives([
]);
-internals.event = Joi.object({
- method: Joi.array().items(Joi.func()).single(),
- options: Joi.object({
- before: Joi.array().items(Joi.string()).single(),
- after: Joi.array().items(Joi.string()).single(),
- bind: Joi.any(),
- sandbox: Joi.valid('server', 'plugin')
+internals.event = Validate.object({
+ method: Validate.array().items(Validate.function()).single(),
+ options: Validate.object({
+ before: Validate.array().items(Validate.string()).single(),
+ after: Validate.array().items(Validate.string()).single(),
+ bind: Validate.any(),
+ sandbox: Validate.valid('server', 'plugin'),
+ timeout: Validate.number().integer().min(1)
})
.default({})
});
-internals.exts = Joi.array().items(internals.event.keys({ type: Joi.string().required() })).single();
+internals.exts = Validate.array()
+ .items(internals.event.keys({ type: Validate.string().required() })).single();
-internals.failAction = Joi.alternatives([
- Joi.valid('error', 'log', 'ignore'),
- Joi.func()
+internals.failAction = Validate.alternatives([
+ Validate.valid('error', 'log', 'ignore'),
+ Validate.function()
])
.default('error');
-internals.routeBase = Joi.object({
- app: Joi.object().allow(null),
+internals.routeBase = Validate.object({
+ app: Validate.object().allow(null),
auth: internals.auth.allow(false),
- bind: Joi.object().allow(null),
- cache: Joi.object({
- expiresIn: Joi.number(),
- expiresAt: Joi.string(),
- privacy: Joi.valid('default', 'public', 'private'),
- statuses: Joi.array().items(Joi.number().integer().min(200)).min(1).single().default([200, 204]),
- otherwise: Joi.string().default('no-cache')
+ bind: Validate.object().allow(null),
+ cache: Validate.object({
+ expiresIn: Validate.number(),
+ expiresAt: Validate.string(),
+ privacy: Validate.valid('default', 'public', 'private'),
+ statuses: Validate.array().items(Validate.number().integer().min(200)).min(1).single().default([200, 204]),
+ otherwise: Validate.string().default('no-cache')
})
.allow(false)
.default(),
- compression: Joi.object()
- .pattern(/.+/, Joi.object())
+ compression: Validate.object()
+ .pattern(/.+/, Validate.object())
.default(),
- cors: Joi.object({
- origin: Joi.array().min(1).allow('ignore').default(['*']),
- maxAge: Joi.number().default(86400),
- headers: Joi.array().items(Joi.string()).default(['Accept', 'Authorization', 'Content-Type', 'If-None-Match']),
- additionalHeaders: Joi.array().items(Joi.string()).default([]),
- exposedHeaders: Joi.array().items(Joi.string()).default(['WWW-Authenticate', 'Server-Authorization']),
- additionalExposedHeaders: Joi.array().items(Joi.string()).default([]),
- credentials: Joi.boolean().when('origin', { is: 'ignore', then: false }).default(false)
+ cors: Validate.object({
+ origin: Validate.array().min(1).allow('ignore').default(['*']),
+ maxAge: Validate.number().default(86400),
+ headers: Validate.array().items(Validate.string()).default(['Accept', 'Authorization', 'Content-Type', 'If-None-Match']),
+ additionalHeaders: Validate.array().items(Validate.string()).default([]),
+ exposedHeaders: Validate.array().items(Validate.string()).default(['WWW-Authenticate', 'Server-Authorization']),
+ additionalExposedHeaders: Validate.array().items(Validate.string()).default([]),
+ credentials: Validate.boolean().when('origin', { is: 'ignore', then: false }).default(false),
+ preflightStatusCode: Validate.valid(200, 204).default(200)
})
.allow(false, true)
.default(false),
- ext: Joi.object({
- onPreAuth: Joi.array().items(internals.event).single(),
- onCredentials: Joi.array().items(internals.event).single(),
- onPostAuth: Joi.array().items(internals.event).single(),
- onPreHandler: Joi.array().items(internals.event).single(),
- onPostHandler: Joi.array().items(internals.event).single(),
- onPreResponse: Joi.array().items(internals.event).single()
+ ext: Validate.object({
+ onPreAuth: Validate.array().items(internals.event).single(),
+ onCredentials: Validate.array().items(internals.event).single(),
+ onPostAuth: Validate.array().items(internals.event).single(),
+ onPreHandler: Validate.array().items(internals.event).single(),
+ onPostHandler: Validate.array().items(internals.event).single(),
+ onPreResponse: Validate.array().items(internals.event).single(),
+ onPostResponse: Validate.array().items(internals.event).single()
})
.default({}),
- files: Joi.object({
- relativeTo: Joi.string().regex(/^([\/\.])|([A-Za-z]:\\)|(\\\\)/).default('.')
+ files: Validate.object({
+ relativeTo: Validate.string().pattern(/^([\/\.])|([A-Za-z]:\\)|(\\\\)/).default('.')
})
.default(),
- json: Joi.object({
- replacer: Joi.alternatives(Joi.func(), Joi.array()).allow(null).default(null),
- space: Joi.number().allow(null).default(null),
- suffix: Joi.string().allow(null).default(null),
- escape: Joi.boolean().default(false)
+ json: Validate.object({
+ replacer: Validate.alternatives(Validate.function(), Validate.array()).allow(null).default(null),
+ space: Validate.number().allow(null).default(null),
+ suffix: Validate.string().allow(null).default(null),
+ escape: Validate.boolean().default(false)
})
.default(),
- jsonp: Joi.string(),
- log: Joi.object({
- collect: Joi.boolean().default(false)
+ log: Validate.object({
+ collect: Validate.boolean().default(false)
})
.default(),
- payload: Joi.object({
- output: Joi.valid('data', 'stream', 'file').default('data'),
- parse: Joi.boolean().allow('gunzip').default(true),
- multipart: Joi.object({
- output: Joi.valid('data', 'stream', 'file', 'annotated').required()
+ payload: Validate.object({
+ output: Validate.valid('data', 'stream', 'file').default('data'),
+ parse: Validate.boolean().allow('gunzip').default(true),
+ multipart: Validate.object({
+ output: Validate.valid('data', 'stream', 'file', 'annotated').required()
})
- .allow(false),
- allow: Joi.array().items(Joi.string()).single(),
- override: Joi.string(),
- maxBytes: Joi.number().integer().positive().default(1024 * 1024),
- uploads: Joi.string().default(Os.tmpdir()),
+ .default(false)
+ .allow(true, false),
+ allow: Validate.array().items(Validate.string()).single(),
+ override: Validate.string(),
+ protoAction: Validate.valid('error', 'remove', 'ignore').default('error'),
+ maxBytes: Validate.number().integer().positive().default(1024 * 1024),
+ maxParts: Validate.number().integer().positive().default(1000),
+ uploads: Validate.string().default(Os.tmpdir()),
failAction: internals.failAction,
- timeout: Joi.number().integer().positive().allow(false).default(10 * 1000),
- defaultContentType: Joi.string().default('application/json'),
- compression: Joi.object()
- .pattern(/.+/, Joi.object())
+ timeout: Validate.number().integer().positive().allow(false).default(10 * 1000),
+ defaultContentType: Validate.string().default('application/json'),
+ compression: Validate.object()
+ .pattern(/.+/, Validate.object())
.default()
})
.default(),
- plugins: Joi.object(),
- response: Joi.object({
- emptyStatusCode: Joi.valid(200, 204).default(200),
+ plugins: Validate.object(),
+ response: Validate.object({
+ disconnectStatusCode: Validate.number().integer().min(400).default(499),
+ emptyStatusCode: Validate.valid(200, 204).default(204),
failAction: internals.failAction,
- modify: Joi.boolean(),
- options: Joi.object().default(),
- ranges: Joi.boolean().default(true),
- sample: Joi.number().min(0).max(100).when('modify', { is: true, then: Joi.forbidden() }),
- schema: Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(true, false),
- status: Joi.object().pattern(/\d\d\d/, Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(true, false))
+ modify: Validate.boolean(),
+ options: Validate.object(),
+ ranges: Validate.boolean().default(true),
+ sample: Validate.number().min(0).max(100).when('modify', { then: Validate.forbidden() }),
+ schema: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false),
+ status: Validate.object().pattern(/\d\d\d/, Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false))
})
- .default()
- .assert('options.stripUnknown', Joi.when('modify', { is: true, otherwise: false }), 'meet requirement of having peer modify set to true'),
- security: Joi.object({
- hsts: Joi.alternatives([
- Joi.object({
- maxAge: Joi.number(),
- includeSubdomains: Joi.boolean(),
- includeSubDomains: Joi.boolean(),
- preload: Joi.boolean()
+ .default(),
+ security: Validate.object({
+ hsts: Validate.alternatives([
+ Validate.object({
+ maxAge: Validate.number(),
+ includeSubdomains: Validate.boolean(),
+ includeSubDomains: Validate.boolean(),
+ preload: Validate.boolean()
}),
- Joi.boolean(),
- Joi.number()
+ Validate.boolean(),
+ Validate.number()
])
.default(15768000),
- xframe: Joi.alternatives([
- Joi.boolean(),
- Joi.valid('sameorigin', 'deny'),
- Joi.object({
- rule: Joi.valid('sameorigin', 'deny', 'allow-from'),
- source: Joi.string()
+ xframe: Validate.alternatives([
+ Validate.boolean(),
+ Validate.valid('sameorigin', 'deny'),
+ Validate.object({
+ rule: Validate.valid('sameorigin', 'deny', 'allow-from'),
+ source: Validate.string()
})
])
.default('deny'),
- xss: Joi.boolean().default(true),
- noOpen: Joi.boolean().default(true),
- noSniff: Joi.boolean().default(true),
- referrer: Joi.alternatives([
- Joi.boolean().valid(false),
- Joi.valid('', 'no-referrer', 'no-referrer-when-downgrade',
+ xss: Validate.valid('enabled', 'disabled', false).default('disabled'),
+ noOpen: Validate.boolean().default(true),
+ noSniff: Validate.boolean().default(true),
+ referrer: Validate.alternatives([
+ Validate.boolean().valid(false),
+ Validate.valid('', 'no-referrer', 'no-referrer-when-downgrade',
'unsafe-url', 'same-origin', 'origin', 'strict-origin',
'origin-when-cross-origin', 'strict-origin-when-cross-origin')
])
@@ -203,193 +210,223 @@ internals.routeBase = Joi.object({
})
.allow(null, false, true)
.default(false),
- state: Joi.object({
- parse: Joi.boolean().default(true),
+ state: Validate.object({
+ parse: Validate.boolean().default(true),
failAction: internals.failAction
})
.default(),
- timeout: Joi.object({
- socket: Joi.number().integer().positive().allow(false),
- server: Joi.number().integer().positive().allow(false).default(false)
+ timeout: Validate.object({
+ socket: Validate.number().integer().positive().allow(false),
+ server: Validate.number().integer().positive().allow(false).default(false)
})
.default(),
- validate: Joi.object({
- headers: Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(null, true),
- params: Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(null, true),
- query: Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(null, false, true),
- payload: Joi.alternatives(Joi.object(), Joi.array(), Joi.func()).allow(null, false, true),
+ validate: Validate.object({
+ headers: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
+ params: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
+ query: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
+ payload: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
+ state: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
failAction: internals.failAction,
- errorFields: Joi.object(),
- options: Joi.object().default()
+ errorFields: Validate.object(),
+ options: Validate.object().default(),
+ validator: Validate.object()
})
.default()
});
-internals.server = Joi.object({
- address: Joi.string().hostname(),
- app: Joi.object().allow(null),
- autoListen: Joi.boolean(),
- cache: Joi.allow(null), // Validated elsewhere
- compression: Joi.object({
- minBytes: Joi.number().min(1).integer().default(1024)
+internals.server = Validate.object({
+ address: Validate.string().hostname(),
+ app: Validate.object().allow(null),
+ autoListen: Validate.boolean(),
+ cache: Validate.allow(null), // Validated elsewhere
+ compression: Validate.object({
+ minBytes: Validate.number().min(1).integer().default(1024)
})
.allow(false)
.default(),
- debug: Joi.object({
- request: Joi.array().items(Joi.string()).single().allow(false).default(['implementation']),
- log: Joi.array().items(Joi.string()).single().allow(false)
+ debug: Validate.object({
+ request: Validate.array().items(Validate.string()).single().allow(false).default(['implementation']),
+ log: Validate.array().items(Validate.string()).single().allow(false)
})
.allow(false)
.default(),
- host: Joi.string().hostname().allow(null),
- listener: Joi.any(),
- load: Joi.object({
- sampleInterval: Joi.number().integer().min(0).default(0),
- concurrent: Joi.number().integer().min(0).default(0)
+ host: Validate.string().hostname().allow(null),
+ info: Validate.object({
+ remote: Validate.boolean().default(false)
+ })
+ .default({}),
+ listener: Validate.any(),
+ load: Validate.object({
+ sampleInterval: Validate.number().integer().min(0).default(0)
})
.unknown()
.default(),
- mime: Joi.object().allow(null).default(null),
- operations: Joi.object({
- cleanStop: Joi.boolean().default(true)
+ mime: Validate.object().empty(null).default(),
+ operations: Validate.object({
+ cleanStop: Validate.boolean().default(true)
})
.default(),
- plugins: Joi.object(),
- port: Joi.alternatives([
- Joi.number().integer().min(0), // TCP port
- Joi.string().regex(/\//), // Unix domain socket
- Joi.string().regex(/^\\\\\.\\pipe\\/) // Windows named pipe
+ plugins: Validate.object(),
+ port: Validate.alternatives([
+ Validate.number().integer().min(0), // TCP port
+ Validate.string().pattern(/\//), // Unix domain socket
+ Validate.string().pattern(/^\\\\\.\\pipe\\/) // Windows named pipe
])
.allow(null),
- router: Joi.object({
- isCaseSensitive: Joi.boolean().default(true),
- stripTrailingSlash: Joi.boolean().default(false)
+ query: Validate.object({
+ parser: Validate.function()
+ })
+ .default(),
+ router: Validate.object({
+ isCaseSensitive: Validate.boolean().default(true),
+ stripTrailingSlash: Validate.boolean().default(false)
})
.default(),
routes: internals.routeBase.default(),
- state: Joi.object(), // Cookie defaults
- tls: Joi.alternatives([
- Joi.object().allow(null),
- Joi.boolean()
+ state: Validate.object(), // Cookie defaults
+ tls: Validate.alternatives([
+ Validate.object().allow(null),
+ Validate.boolean()
]),
- uri: Joi.string().regex(/[^/]$/)
+ uri: Validate.string().pattern(/[^/]$/)
});
-internals.vhost = Joi.alternatives([
- Joi.string().hostname(),
- Joi.array().items(Joi.string().hostname()).min(1)
+internals.vhost = Validate.alternatives([
+ Validate.string().hostname(),
+ Validate.array().items(Validate.string().hostname()).min(1)
]);
-internals.handler = Joi.alternatives([
- Joi.func(),
- Joi.object().length(1)
+internals.handler = Validate.alternatives([
+ Validate.function(),
+ Validate.object().length(1)
]);
-internals.route = Joi.object({
- method: Joi.string().regex(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/).required(),
- path: Joi.string().required(),
- rules: Joi.object(),
+internals.route = Validate.object({
+ method: Validate.string().pattern(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/).required(),
+ path: Validate.string().required(),
+ rules: Validate.object(),
vhost: internals.vhost,
// Validated in route construction
- handler: Joi.any(),
- options: Joi.any(),
- config: Joi.any() // Backwards compatibility
+ handler: Validate.any(),
+ options: Validate.any(),
+ config: Validate.any() // Backwards compatibility
})
.without('config', 'options');
internals.pre = [
- Joi.func(),
- Joi.object({
- method: Joi.alternatives(Joi.string(), Joi.func()).required(),
- assign: Joi.string(),
- mode: Joi.valid('serial', 'parallel'),
+ Validate.function(),
+ Validate.object({
+ method: Validate.alternatives(Validate.string(), Validate.function()).required(),
+ assign: Validate.string(),
+ mode: Validate.valid('serial', 'parallel'),
failAction: internals.failAction
})
];
internals.routeConfig = internals.routeBase.keys({
- description: Joi.string(),
- id: Joi.string(),
- isInternal: Joi.boolean(),
+ description: Validate.string(),
+ id: Validate.string(),
+ isInternal: Validate.boolean(),
notes: [
- Joi.string(),
- Joi.array().items(Joi.string())
+ Validate.string(),
+ Validate.array().items(Validate.string())
],
- pre: Joi.array().items(internals.pre.concat(Joi.array().items(internals.pre).min(1))),
+ pre: Validate.array().items(...internals.pre.concat(Validate.array().items(...internals.pre).min(1))),
tags: [
- Joi.string(),
- Joi.array().items(Joi.string())
+ Validate.string(),
+ Validate.array().items(Validate.string())
]
});
-internals.cacheConfig = Joi.object({
- name: Joi.string().invalid('_default'),
- partition: Joi.string(),
- shared: Joi.boolean(),
- engine: Joi.alternatives([
- Joi.object(),
- Joi.func()
- ])
- .required()
-}).unknown();
+internals.cacheConfig = Validate.alternatives([
+ Validate.function(),
+ Validate.object({
+ name: Validate.string().invalid('_default'),
+ shared: Validate.boolean(),
+ provider: [
+ Validate.function(),
+ {
+ constructor: Validate.function().required(),
+ options: Validate.object({
+ partition: Validate.string().default('hapi-cache')
+ })
+ .unknown() // Catbox client validates other keys
+ .default({})
+ }
+ ],
+ engine: Validate.object()
+ })
+ .xor('provider', 'engine')
+]);
-internals.cache = Joi.array().items(internals.cacheConfig, Joi.func()).min(1).single();
+internals.cache = Validate.array().items(internals.cacheConfig).min(1).single();
-internals.cachePolicy = Joi.object({
- cache: Joi.string().allow(null).allow(''),
- segment: Joi.string(),
- shared: Joi.boolean()
+internals.cachePolicy = Validate.object({
+ cache: Validate.string().allow(null).allow(''),
+ segment: Validate.string(),
+ shared: Validate.boolean()
})
- .options({ allowUnknown: true }); // Catbox validates other keys
+ .unknown(); // Catbox policy validates other keys
-internals.method = Joi.object({
- bind: Joi.object().allow(null),
- generateKey: Joi.func(),
+internals.method = Validate.object({
+ bind: Validate.object().allow(null),
+ generateKey: Validate.function(),
cache: internals.cachePolicy
});
-internals.methodObject = Joi.object({
- name: Joi.string().required(),
- method: Joi.func().required(),
- options: Joi.object()
+internals.methodObject = Validate.object({
+ name: Validate.string().required(),
+ method: Validate.function().required(),
+ options: Validate.object()
});
-internals.register = Joi.object({
+internals.register = Validate.object({
once: true,
- routes: Joi.object({
- prefix: Joi.string().regex(/^\/.+/),
+ routes: Validate.object({
+ prefix: Validate.string().pattern(/^\/.+/),
vhost: internals.vhost
})
.default({})
});
+internals.semver = Validate.string();
+
+
internals.plugin = internals.register.keys({
- options: Joi.any(),
- plugin: Joi.object({
- register: Joi.func().required(),
- name: Joi.string().when('pkg.name', { is: Joi.exist(), otherwise: Joi.required() }),
- version: Joi.string(),
- multiple: Joi.boolean().default(false),
- dependencies: Joi.array().items(Joi.string()).single(),
+ options: Validate.any(),
+ plugin: Validate.object({
+ register: Validate.function().required(),
+ name: Validate.string().when('pkg.name', { is: Validate.exist(), otherwise: Validate.required() }),
+ version: Validate.string(),
+ multiple: Validate.boolean().default(false),
+ dependencies: [
+ Validate.array().items(Validate.string()).single(),
+ Validate.object().pattern(/.+/, internals.semver)
+ ],
once: true,
- pkg: Joi.object({
- name: Joi.string(),
- version: Joi.string().default('0.0.0')
+ requirements: Validate.object({
+ hapi: Validate.string(),
+ node: Validate.string()
+ })
+ .default(),
+ pkg: Validate.object({
+ name: Validate.string(),
+ version: Validate.string().default('0.0.0')
})
.unknown()
.default({})
@@ -400,10 +437,10 @@ internals.plugin = internals.register.keys({
.unknown();
-internals.rules = Joi.object({
- validate: Joi.object({
- schema: Joi.alternatives(Joi.object(), Joi.array()).required(),
- options: Joi.object()
+internals.rules = Validate.object({
+ validate: Validate.object({
+ schema: Validate.alternatives(Validate.object(), Validate.array()).required(),
+ options: Validate.object()
.default({ allowUnknown: true })
})
});
diff --git a/lib/core.js b/lib/core.js
index ccc99ed1b..202f6dbe0 100755
--- a/lib/core.js
+++ b/lib/core.js
@@ -1,22 +1,20 @@
'use strict';
-// Load modules
-
const Http = require('http');
const Https = require('https');
const Os = require('os');
const Path = require('path');
-const Boom = require('boom');
-const Bounce = require('bounce');
-const Call = require('call');
-const Catbox = require('catbox');
-const CatboxMemory = require('catbox-memory');
-const Heavy = require('heavy');
-const Hoek = require('hoek');
-const Mimos = require('mimos');
-const Podium = require('podium');
-const Statehood = require('statehood');
+const Boom = require('@hapi/boom');
+const Bounce = require('@hapi/bounce');
+const Call = require('@hapi/call');
+const Catbox = require('@hapi/catbox');
+const { Engine: CatboxMemory } = require('@hapi/catbox-memory');
+const { Heavy } = require('@hapi/heavy');
+const Hoek = require('@hapi/hoek');
+const { Mimos } = require('@hapi/mimos');
+const Podium = require('@hapi/podium');
+const Statehood = require('@hapi/statehood');
const Auth = require('./auth');
const Compression = require('./compression');
@@ -25,23 +23,25 @@ const Cors = require('./cors');
const Ext = require('./ext');
const Methods = require('./methods');
const Request = require('./request');
+const Response = require('./response');
const Route = require('./route');
const Toolkit = require('./toolkit');
+const Validation = require('./validation');
-// Declare internals
-
const internals = {
counter: {
min: 10000,
max: 99999
},
events: [
+ { name: 'cachePolicy', spread: true },
{ name: 'log', channels: ['app', 'internal'], tags: true },
{ name: 'request', channels: ['app', 'internal', 'error'], tags: true, spread: true },
'response',
'route',
'start',
+ 'closing',
'stop'
],
badRequestResponse: Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')
@@ -50,65 +50,87 @@ const internals = {
exports = module.exports = internals.Core = class {
- constructor(options) {
+ actives = new WeakMap(); // Active requests being processed
+ app = {};
+ auth = new Auth(this);
+ caches = new Map(); // Cache clients
+ compression = new Compression();
+ controlled = null; // Other servers linked to the phases of this server
+ dependencies = []; // Plugin dependencies
+ events = new Podium.Podium(internals.events);
+ heavy = null;
+ info = null;
+ instances = new Set();
+ listener = null;
+ methods = new Methods(this); // Server methods
+ mime = null;
+ onConnection = null; // Used to remove event listener on stop
+ phase = 'stopped'; // 'stopped', 'initializing', 'initialized', 'starting', 'started', 'stopping', 'invalid'
+ plugins = {}; // Exposed plugin properties by name
+ registrations = {}; // Tracks plugin for dependency validation { name -> { version } }
+ registring = 0; // > 0 while register() is waiting for plugin callbacks
+ Request = class extends Request { };
+ Response = class extends Response { };
+ requestCounter = { value: internals.counter.min, min: internals.counter.min, max: internals.counter.max };
+ root = null;
+ router = null;
+ settings = null;
+ sockets = null; // Track open sockets for graceful shutdown
+ started = false;
+ states = null;
+ toolkit = new Toolkit.Manager();
+ type = null;
+ validator = null;
+
+ extensionsSeq = 0; // Used to keep absolute order of extensions based on the order added across locations
+ extensions = {
+ server: {
+ onPreStart: new Ext('onPreStart', this),
+ onPostStart: new Ext('onPostStart', this),
+ onPreStop: new Ext('onPreStop', this),
+ onPostStop: new Ext('onPostStop', this)
+ },
+ route: {
+ onRequest: new Ext('onRequest', this),
+ onPreAuth: new Ext('onPreAuth', this),
+ onCredentials: new Ext('onCredentials', this),
+ onPostAuth: new Ext('onPostAuth', this),
+ onPreHandler: new Ext('onPreHandler', this),
+ onPostHandler: new Ext('onPostHandler', this),
+ onPreResponse: new Ext('onPreResponse', this),
+ onPostResponse: new Ext('onPostResponse', this)
+ }
+ };
+
+ decorations = {
+ handler: new Map(),
+ request: new Map(),
+ response: new Map(),
+ server: new Map(),
+ toolkit: new Map(),
+ requestApply: null,
+ public: { handler: [], request: [], response: [], server: [], toolkit: [] }
+ };
- this.root = null; // Dispatch reference of the root server
+ constructor(options) {
const { settings, type } = internals.setup(options);
this.settings = settings;
this.type = type;
- this.app = {};
- this.auth = new Auth(this);
- this.caches = new Map(); // Cache clients
- this.compression = new Compression();
- this.controlled = null; // Other servers linked to the phases of this server
- this.decorations = { handler: [], request: [], server: [], toolkit: [] }; // Public decoration names
- this.dependencies = []; // Plugin dependencies
- this.events = new Podium(internals.events);
this.heavy = new Heavy(this.settings.load);
- this.instances = new Set();
- this.methods = new Methods(this); // Server methods
this.mime = new Mimos(this.settings.mime);
- this.onConnection = null; // Used to remove event listener on stop
- this.plugins = {}; // Exposed plugin properties by name
- this.queue = new internals.Queue(this.settings.load);
- this.registrations = {}; // Tracks plugin for dependency validation { name -> { version } }
- this.registring = 0; // > 0 while register() is waiting for plugin callbacks
- this.requestCounter = { value: internals.counter.min, min: internals.counter.min, max: internals.counter.max };
this.router = new Call.Router(this.settings.router);
- this.phase = 'stopped'; // 'stopped', 'initializing', 'initialized', 'starting', 'started', 'stopping', 'invalid'
- this.sockets = null; // Track open sockets for graceful shutdown
- this.started = false;
this.states = new Statehood.Definitions(this.settings.state);
- this.toolkit = new Toolkit();
-
- this.extensionsSeq = 0; // Used to keep absolute order of extensions based on the order added across locations
- this.extensions = {
- server: {
- onPreStart: new Ext('onPreStart', this),
- onPostStart: new Ext('onPostStart', this),
- onPreStop: new Ext('onPreStop', this),
- onPostStop: new Ext('onPostStop', this)
- },
- route: {
- onRequest: new Ext('onRequest', this),
- onPreAuth: new Ext('onPreAuth', this),
- onCredentials: new Ext('onCredentials', this),
- onPostAuth: new Ext('onPostAuth', this),
- onPreHandler: new Ext('onPreHandler', this),
- onPostHandler: new Ext('onPostHandler', this),
- onPreResponse: new Ext('onPreResponse', this)
- }
- };
-
- this.Request = class extends Request {};
this._debug();
- this._decorations = { handler: {}, request: {}, server: {}, toolkit: {}, requestApply: null };
this._initializeCache();
+ if (this.settings.routes.validate.validator) {
+ this.validator = Validation.validator(this.settings.routes.validate.validator);
+ }
+
this.listener = this._createListener();
this._initializeListener();
this.info = this._info();
@@ -116,24 +138,27 @@ exports = module.exports = internals.Core = class {
_debug() {
+ const debug = this.settings.debug;
+ if (!debug) {
+ return;
+ }
+
// Subscribe to server log events
- if (this.settings.debug) {
- const debug = (request, event) => {
+ const method = (event) => {
- const data = event.error || event.data;
- console.error('Debug:', event.tags.join(', '), (data ? '\n ' + (data.stack || (typeof data === 'object' ? Hoek.stringify(data) : data)) : ''));
- };
+ const data = event.error ?? event.data;
+ console.error('Debug:', event.tags.join(', '), data ? '\n ' + (data.stack ?? (typeof data === 'object' ? Hoek.stringify(data) : data)) : '');
+ };
- if (this.settings.debug.log) {
- const filter = this.settings.debug.log.some((tag) => tag === '*') ? undefined : this.settings.debug.log;
- this.events.on({ name: 'log', filter }, (event) => debug(null, event));
- }
+ if (debug.log) {
+ const filter = debug.log.some((tag) => tag === '*') ? undefined : debug.log;
+ this.events.on({ name: 'log', filter }, method);
+ }
- if (this.settings.debug.request) {
- const filter = this.settings.debug.request.some((tag) => tag === '*') ? undefined : this.settings.debug.request;
- this.events.on({ name: 'request', filter }, debug);
- }
+ if (debug.request) {
+ const filter = debug.request.some((tag) => tag === '*') ? undefined : debug.request;
+ this.events.on({ name: 'request', filter }, (request, event) => method(event));
}
}
@@ -144,7 +169,7 @@ exports = module.exports = internals.Core = class {
}
if (!this.caches.has('_default')) {
- this._createCache([{ engine: CatboxMemory }]); // Defaults to memory-based
+ this._createCache([{ provider: CatboxMemory }]); // Defaults to memory-based
}
}
@@ -162,42 +187,59 @@ exports = module.exports = internals.Core = class {
port,
protocol,
id: Os.hostname() + ':' + process.pid + ':' + now.toString(36),
- uri: this.settings.uri || (protocol + ':' + (this.type === 'tcp' ? '//' + host + (port ? ':' + port : '') : port))
+ uri: this.settings.uri ?? (protocol + ':' + (this.type === 'tcp' ? '//' + host + (port ? ':' + port : '') : port))
};
return info;
}
- _createCache(options) {
+ _counter() {
+
+ const next = ++this.requestCounter.value;
+
+ if (this.requestCounter.value > this.requestCounter.max) {
+ this.requestCounter.value = this.requestCounter.min;
+ }
+
+ return next - 1;
+ }
+
+ _createCache(configs) {
Hoek.assert(this.phase !== 'initializing', 'Cannot provision server cache while server is initializing');
- options = Config.apply('cache', options);
+ configs = Config.apply('cache', configs);
const added = [];
- for (let config of options) {
+ for (let config of configs) {
+
+ // (type: 'handler', property: P, method: HandlerDecorationMethod, options?: { apply?: boolean | undefined, extend?: never }): void; + + decorate
(type: 'request', property: ExceptName
, method: (existing: ((...args: any[]) => any)) => (request: Request) => DecorationMethod (type: 'request', property: ExceptName , method: (request: Request) => DecorationMethod (type: 'request', property: ExceptName , method: DecorationMethod (type: 'request', property: ExceptName , value: (existing: ((...args: any[]) => any)) => (request: Request) => any, options: {apply: true, extend: true}): void;
+ decorate (type: 'request', property: ExceptName , value: (request: Request) => any, options: {apply: true, extend?: boolean | undefined}): void;
+ decorate (type: 'request', property: ExceptName , value: DecorationValue, options?: never): void;
+
+ decorate (type: 'toolkit', property: ExceptName , method: (existing: ((...args: any[]) => any)) => DecorationMethod (type: 'toolkit', property: ExceptName , method: DecorationMethod (type: 'toolkit', property: ExceptName , value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
+ decorate (type: 'toolkit', property: ExceptName , value: DecorationValue, options?: never): void;
+
+ decorate (type: 'server', property: ExceptName , method: (existing: ((...args: any[]) => any)) => DecorationMethod (type: 'server', property: ExceptName , method: DecorationMethod (type: 'server', property: ExceptName , value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
+ decorate (type: 'server', property: ExceptName , value: DecorationValue, options?: never): void;
+
+ /**
+ * Used within a plugin to declare a required dependency on other plugins where:
+ * @param dependencies - plugins which must be registered in order for this plugin to operate. Plugins listed must be registered before the server is
+ * initialized or started.
+ * @param after - (optional) a function that is called after all the specified dependencies have been registered and before the server starts. The function is only called if the server is
+ * initialized or started. The function signature is async function(server) where: server - the server the dependency() method was called on.
+ * @return Return value: none.
+ * The after method is identical to setting a server extension point on 'onPreStart'.
+ * If a circular dependency is detected, an exception is thrown (e.g. two plugins each has an after function to be called after the other).
+ * The method does not provide version dependency which should be implemented using npm peer dependencies.
+ * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverdependencydependencies-after)
+ */
+ dependency(dependencies: Dependencies, after?: ((server: Server) => Promise