├── .github
└── workflows
│ └── ci-module.yml
├── .gitignore
├── .npmrc
├── API.md
├── LICENSE.md
├── README.md
├── SPONSORS.md
├── lib
├── auth.js
├── compression.js
├── config.js
├── core.js
├── cors.js
├── ext.js
├── handler.js
├── headers.js
├── index.d.ts
├── index.js
├── methods.js
├── request.js
├── response.js
├── route.js
├── security.js
├── server.js
├── streams.js
├── toolkit.js
├── transmit.js
├── types
│ ├── index.d.ts
│ ├── plugin.d.ts
│ ├── request.d.ts
│ ├── response.d.ts
│ ├── route.d.ts
│ ├── server
│ │ ├── auth.d.ts
│ │ ├── cache.d.ts
│ │ ├── encoders.d.ts
│ │ ├── events.d.ts
│ │ ├── ext.d.ts
│ │ ├── index.d.ts
│ │ ├── info.d.ts
│ │ ├── inject.d.ts
│ │ ├── methods.d.ts
│ │ ├── options.d.ts
│ │ ├── server.d.ts
│ │ └── state.d.ts
│ └── utils.d.ts
└── validation.js
├── package.json
└── test
├── .hidden
├── auth.js
├── common.js
├── core.js
├── cors.js
├── file
├── image.jpg
├── image.png
├── image.png.gz
└── note.txt
├── handler.js
├── headers.js
├── index.js
├── methods.js
├── payload.js
├── request.js
├── response.js
├── route.js
├── security.js
├── server.js
├── state.js
├── templates
├── invalid.html
├── plugin
│ └── test.html
└── test.html
├── toolkit.js
├── transmit.js
├── types
└── index.ts
└── validation.js
/.github/workflows/ci-module.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - v21
7 | - master
8 | pull_request:
9 | workflow_dispatch:
10 |
11 | jobs:
12 | test:
13 | uses: hapijs/.github/.github/workflows/ci-module.yml@master
14 | with:
15 | min-node-version: 14
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/package-lock.json
3 |
4 | coverage.*
5 |
6 | **/.DS_Store
7 | **/._*
8 |
9 | **/*.pem
10 |
11 | **/.vs
12 | **/.vscode
13 | **/.idea
14 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save=false
2 |
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011-2022, Project contributors
2 | Copyright (c) 2011-2020, Sideway Inc
3 | Copyright (c) 2011-2014, Walmart
4 | Copyright (c) 2011, Yahoo Inc.
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
8 |
9 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
10 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
11 | - The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission.
12 |
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS OFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # @hapi/hapi
4 |
5 | #### The Simple, Secure Framework Developers Trust
6 |
7 | Build powerful, scalable applications, with minimal overhead and full out-of-the-box functionality - your code, your way.
8 |
9 | ### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support
10 |
11 | ## Useful resources
12 |
13 | - [Documentation and API](https://hapi.dev/)
14 | - [Version status](https://hapi.dev/resources/status/#hapi) (builds, dependencies, node versions, licenses, eol)
15 | - [Changelog](https://hapi.dev/resources/changelog/)
16 | - [Project policies](https://hapi.dev/policies/)
17 | - [Support](https://hapi.dev/support/)
18 |
19 | ## Technical Steering Committee (TSC) Members
20 |
21 | - Devin Ivy ([@devinivy](https://github.com/devinivy))
22 | - Lloyd Benson ([@lloydbenson](https://github.com/lloydbenson))
23 | - Nathan LaFreniere ([@nlf](https://github.com/nlf))
24 | - Wyatt Lyon Preul ([@geek](https://github.com/geek))
25 | - Nicolas Morel ([@marsup](https://github.com/marsup))
26 | - Jonathan Samines ([@jonathansamines](https://github.com/jonathansamines))
27 |
--------------------------------------------------------------------------------
/SPONSORS.md:
--------------------------------------------------------------------------------
1 | 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!
2 |
3 | > 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).
4 |
5 | # Staff Sponsors
6 |
7 | - [Big Room Studios](https://www.bigroomstudios.com/)
8 | - [Dixeed](https://dixeed.com/)
9 |
10 | # Top Sponsors
11 |
12 | - Fabian Gündel / [DataWrapper.de](https://www.datawrapper.de/)
13 | - Devin Stewart
14 | - [Raider.IO](https://raider.io/)
15 | - [Florence Healthcare](https://florencehc.com/)
16 |
--------------------------------------------------------------------------------
/lib/compression.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Zlib = require('zlib');
4 |
5 | const Accept = require('@hapi/accept');
6 | const Bounce = require('@hapi/bounce');
7 | const Hoek = require('@hapi/hoek');
8 |
9 |
10 | const internals = {
11 | common: ['gzip, deflate', 'deflate, gzip', 'gzip', 'deflate', 'gzip, deflate, br']
12 | };
13 |
14 |
15 | exports = module.exports = internals.Compression = class {
16 |
17 | decoders = {
18 | gzip: (options) => Zlib.createGunzip(options),
19 | deflate: (options) => Zlib.createInflate(options)
20 | };
21 |
22 | encodings = ['identity', 'gzip', 'deflate'];
23 |
24 | encoders = {
25 | identity: null,
26 | gzip: (options) => Zlib.createGzip(options),
27 | deflate: (options) => Zlib.createDeflate(options)
28 | };
29 |
30 | #common = null;
31 |
32 | constructor() {
33 |
34 | this._updateCommons();
35 | }
36 |
37 | _updateCommons() {
38 |
39 | this.#common = new Map();
40 |
41 | for (const header of internals.common) {
42 | this.#common.set(header, Accept.encoding(header, this.encodings));
43 | }
44 | }
45 |
46 | addEncoder(encoding, encoder) {
47 |
48 | Hoek.assert(this.encoders[encoding] === undefined, `Cannot override existing encoder for ${encoding}`);
49 | Hoek.assert(typeof encoder === 'function', `Invalid encoder function for ${encoding}`);
50 | this.encoders[encoding] = encoder;
51 | this.encodings.unshift(encoding);
52 | this._updateCommons();
53 | }
54 |
55 | addDecoder(encoding, decoder) {
56 |
57 | Hoek.assert(this.decoders[encoding] === undefined, `Cannot override existing decoder for ${encoding}`);
58 | Hoek.assert(typeof decoder === 'function', `Invalid decoder function for ${encoding}`);
59 | this.decoders[encoding] = decoder;
60 | }
61 |
62 | accept(request) {
63 |
64 | const header = request.headers['accept-encoding'];
65 | if (!header) {
66 | return 'identity';
67 | }
68 |
69 | const common = this.#common.get(header);
70 | if (common) {
71 | return common;
72 | }
73 |
74 | try {
75 | return Accept.encoding(header, this.encodings);
76 | }
77 | catch (err) {
78 | Bounce.rethrow(err, 'system');
79 | err.header = header;
80 | request._log(['accept-encoding', 'error'], err);
81 | return 'identity';
82 | }
83 | }
84 |
85 | encoding(response, length) {
86 |
87 | if (response.settings.compressed) {
88 | response.headers['content-encoding'] = response.settings.compressed;
89 | return null;
90 | }
91 |
92 | const request = response.request;
93 | if (!request._core.settings.compression ||
94 | length !== null && length < request._core.settings.compression.minBytes) {
95 |
96 | return null;
97 | }
98 |
99 | const mime = request._core.mime.type(response.headers['content-type'] || 'application/octet-stream');
100 | if (!mime.compressible) {
101 | return null;
102 | }
103 |
104 | response.vary('accept-encoding');
105 |
106 | if (response.headers['content-encoding']) {
107 | return null;
108 | }
109 |
110 | return request.info.acceptEncoding === 'identity' ? null : request.info.acceptEncoding;
111 | }
112 |
113 | encoder(request, encoding) {
114 |
115 | const encoder = this.encoders[encoding];
116 | Hoek.assert(encoder !== undefined, `Unknown encoding ${encoding}`);
117 | return encoder(request.route.settings.compression[encoding]);
118 | }
119 | };
120 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Os = require('os');
4 |
5 | const Somever = require('@hapi/somever');
6 | const Validate = require('@hapi/validate');
7 |
8 |
9 | const internals = {};
10 |
11 |
12 | exports.symbol = Symbol('hapi-response');
13 |
14 |
15 | exports.apply = function (type, options, ...message) {
16 |
17 | const result = internals[type].validate(options);
18 |
19 | if (result.error) {
20 | throw new Error(`Invalid ${type} options ${message.length ? '(' + message.join(' ') + ')' : ''} ${result.error.annotate()}`);
21 | }
22 |
23 | return result.value;
24 | };
25 |
26 |
27 | exports.enable = function (options) {
28 |
29 | const settings = options ? Object.assign({}, options) : {}; // Shallow cloned
30 |
31 | if (settings.security === true) {
32 | settings.security = {};
33 | }
34 |
35 | if (settings.cors === true) {
36 | settings.cors = {};
37 | }
38 |
39 | return settings;
40 | };
41 |
42 | exports.versionMatch = (version, range) => Somever.match(version, range, { includePrerelease: true });
43 |
44 | internals.access = Validate.object({
45 | entity: Validate.valid('user', 'app', 'any'),
46 | scope: [false, Validate.array().items(Validate.string()).single().min(1)]
47 | });
48 |
49 |
50 | internals.auth = Validate.alternatives([
51 | Validate.string(),
52 | internals.access.keys({
53 | mode: Validate.valid('required', 'optional', 'try'),
54 | strategy: Validate.string(),
55 | strategies: Validate.array().items(Validate.string()).min(1),
56 | access: Validate.array().items(internals.access.min(1)).single().min(1),
57 | payload: [
58 | Validate.valid('required', 'optional'),
59 | Validate.boolean()
60 | ]
61 | })
62 | .without('strategy', 'strategies')
63 | .without('access', ['scope', 'entity'])
64 | ]);
65 |
66 |
67 | internals.event = Validate.object({
68 | method: Validate.array().items(Validate.function()).single(),
69 | options: Validate.object({
70 | before: Validate.array().items(Validate.string()).single(),
71 | after: Validate.array().items(Validate.string()).single(),
72 | bind: Validate.any(),
73 | sandbox: Validate.valid('server', 'plugin'),
74 | timeout: Validate.number().integer().min(1)
75 | })
76 | .default({})
77 | });
78 |
79 |
80 | internals.exts = Validate.array()
81 | .items(internals.event.keys({ type: Validate.string().required() })).single();
82 |
83 |
84 | internals.failAction = Validate.alternatives([
85 | Validate.valid('error', 'log', 'ignore'),
86 | Validate.function()
87 | ])
88 | .default('error');
89 |
90 |
91 | internals.routeBase = Validate.object({
92 | app: Validate.object().allow(null),
93 | auth: internals.auth.allow(false),
94 | bind: Validate.object().allow(null),
95 | cache: Validate.object({
96 | expiresIn: Validate.number(),
97 | expiresAt: Validate.string(),
98 | privacy: Validate.valid('default', 'public', 'private'),
99 | statuses: Validate.array().items(Validate.number().integer().min(200)).min(1).single().default([200, 204]),
100 | otherwise: Validate.string().default('no-cache')
101 | })
102 | .allow(false)
103 | .default(),
104 | compression: Validate.object()
105 | .pattern(/.+/, Validate.object())
106 | .default(),
107 | cors: Validate.object({
108 | origin: Validate.array().min(1).allow('ignore').default(['*']),
109 | maxAge: Validate.number().default(86400),
110 | headers: Validate.array().items(Validate.string()).default(['Accept', 'Authorization', 'Content-Type', 'If-None-Match']),
111 | additionalHeaders: Validate.array().items(Validate.string()).default([]),
112 | exposedHeaders: Validate.array().items(Validate.string()).default(['WWW-Authenticate', 'Server-Authorization']),
113 | additionalExposedHeaders: Validate.array().items(Validate.string()).default([]),
114 | credentials: Validate.boolean().when('origin', { is: 'ignore', then: false }).default(false),
115 | preflightStatusCode: Validate.valid(200, 204).default(200)
116 | })
117 | .allow(false, true)
118 | .default(false),
119 | ext: Validate.object({
120 | onPreAuth: Validate.array().items(internals.event).single(),
121 | onCredentials: Validate.array().items(internals.event).single(),
122 | onPostAuth: Validate.array().items(internals.event).single(),
123 | onPreHandler: Validate.array().items(internals.event).single(),
124 | onPostHandler: Validate.array().items(internals.event).single(),
125 | onPreResponse: Validate.array().items(internals.event).single(),
126 | onPostResponse: Validate.array().items(internals.event).single()
127 | })
128 | .default({}),
129 | files: Validate.object({
130 | relativeTo: Validate.string().pattern(/^([\/\.])|([A-Za-z]:\\)|(\\\\)/).default('.')
131 | })
132 | .default(),
133 | json: Validate.object({
134 | replacer: Validate.alternatives(Validate.function(), Validate.array()).allow(null).default(null),
135 | space: Validate.number().allow(null).default(null),
136 | suffix: Validate.string().allow(null).default(null),
137 | escape: Validate.boolean().default(false)
138 | })
139 | .default(),
140 | log: Validate.object({
141 | collect: Validate.boolean().default(false)
142 | })
143 | .default(),
144 | payload: Validate.object({
145 | output: Validate.valid('data', 'stream', 'file').default('data'),
146 | parse: Validate.boolean().allow('gunzip').default(true),
147 | multipart: Validate.object({
148 | output: Validate.valid('data', 'stream', 'file', 'annotated').required()
149 | })
150 | .default(false)
151 | .allow(true, false),
152 | allow: Validate.array().items(Validate.string()).single(),
153 | override: Validate.string(),
154 | protoAction: Validate.valid('error', 'remove', 'ignore').default('error'),
155 | maxBytes: Validate.number().integer().positive().default(1024 * 1024),
156 | maxParts: Validate.number().integer().positive().default(1000),
157 | uploads: Validate.string().default(Os.tmpdir()),
158 | failAction: internals.failAction,
159 | timeout: Validate.number().integer().positive().allow(false).default(10 * 1000),
160 | defaultContentType: Validate.string().default('application/json'),
161 | compression: Validate.object()
162 | .pattern(/.+/, Validate.object())
163 | .default()
164 | })
165 | .default(),
166 | plugins: Validate.object(),
167 | response: Validate.object({
168 | disconnectStatusCode: Validate.number().integer().min(400).default(499),
169 | emptyStatusCode: Validate.valid(200, 204).default(204),
170 | failAction: internals.failAction,
171 | modify: Validate.boolean(),
172 | options: Validate.object(),
173 | ranges: Validate.boolean().default(true),
174 | sample: Validate.number().min(0).max(100).when('modify', { then: Validate.forbidden() }),
175 | schema: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false),
176 | status: Validate.object().pattern(/\d\d\d/, Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false))
177 | })
178 | .default(),
179 | security: Validate.object({
180 | hsts: Validate.alternatives([
181 | Validate.object({
182 | maxAge: Validate.number(),
183 | includeSubdomains: Validate.boolean(),
184 | includeSubDomains: Validate.boolean(),
185 | preload: Validate.boolean()
186 | }),
187 | Validate.boolean(),
188 | Validate.number()
189 | ])
190 | .default(15768000),
191 | xframe: Validate.alternatives([
192 | Validate.boolean(),
193 | Validate.valid('sameorigin', 'deny'),
194 | Validate.object({
195 | rule: Validate.valid('sameorigin', 'deny', 'allow-from'),
196 | source: Validate.string()
197 | })
198 | ])
199 | .default('deny'),
200 | xss: Validate.valid('enabled', 'disabled', false).default('disabled'),
201 | noOpen: Validate.boolean().default(true),
202 | noSniff: Validate.boolean().default(true),
203 | referrer: Validate.alternatives([
204 | Validate.boolean().valid(false),
205 | Validate.valid('', 'no-referrer', 'no-referrer-when-downgrade',
206 | 'unsafe-url', 'same-origin', 'origin', 'strict-origin',
207 | 'origin-when-cross-origin', 'strict-origin-when-cross-origin')
208 | ])
209 | .default(false)
210 | })
211 | .allow(null, false, true)
212 | .default(false),
213 | state: Validate.object({
214 | parse: Validate.boolean().default(true),
215 | failAction: internals.failAction
216 | })
217 | .default(),
218 | timeout: Validate.object({
219 | socket: Validate.number().integer().positive().allow(false),
220 | server: Validate.number().integer().positive().allow(false).default(false)
221 | })
222 | .default(),
223 | validate: Validate.object({
224 | headers: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
225 | params: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
226 | query: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
227 | payload: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
228 | state: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
229 | failAction: internals.failAction,
230 | errorFields: Validate.object(),
231 | options: Validate.object().default(),
232 | validator: Validate.object()
233 | })
234 | .default()
235 | });
236 |
237 |
238 | internals.server = Validate.object({
239 | address: Validate.string().hostname(),
240 | app: Validate.object().allow(null),
241 | autoListen: Validate.boolean(),
242 | cache: Validate.allow(null), // Validated elsewhere
243 | compression: Validate.object({
244 | minBytes: Validate.number().min(1).integer().default(1024)
245 | })
246 | .allow(false)
247 | .default(),
248 | debug: Validate.object({
249 | request: Validate.array().items(Validate.string()).single().allow(false).default(['implementation']),
250 | log: Validate.array().items(Validate.string()).single().allow(false)
251 | })
252 | .allow(false)
253 | .default(),
254 | host: Validate.string().hostname().allow(null),
255 | info: Validate.object({
256 | remote: Validate.boolean().default(false)
257 | })
258 | .default({}),
259 | listener: Validate.any(),
260 | load: Validate.object({
261 | sampleInterval: Validate.number().integer().min(0).default(0)
262 | })
263 | .unknown()
264 | .default(),
265 | mime: Validate.object().empty(null).default(),
266 | operations: Validate.object({
267 | cleanStop: Validate.boolean().default(true)
268 | })
269 | .default(),
270 | plugins: Validate.object(),
271 | port: Validate.alternatives([
272 | Validate.number().integer().min(0), // TCP port
273 | Validate.string().pattern(/\//), // Unix domain socket
274 | Validate.string().pattern(/^\\\\\.\\pipe\\/) // Windows named pipe
275 | ])
276 | .allow(null),
277 | query: Validate.object({
278 | parser: Validate.function()
279 | })
280 | .default(),
281 | router: Validate.object({
282 | isCaseSensitive: Validate.boolean().default(true),
283 | stripTrailingSlash: Validate.boolean().default(false)
284 | })
285 | .default(),
286 | routes: internals.routeBase.default(),
287 | state: Validate.object(), // Cookie defaults
288 | tls: Validate.alternatives([
289 | Validate.object().allow(null),
290 | Validate.boolean()
291 | ]),
292 | uri: Validate.string().pattern(/[^/]$/)
293 | });
294 |
295 |
296 | internals.vhost = Validate.alternatives([
297 | Validate.string().hostname(),
298 | Validate.array().items(Validate.string().hostname()).min(1)
299 | ]);
300 |
301 |
302 | internals.handler = Validate.alternatives([
303 | Validate.function(),
304 | Validate.object().length(1)
305 | ]);
306 |
307 |
308 | internals.route = Validate.object({
309 | method: Validate.string().pattern(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/).required(),
310 | path: Validate.string().required(),
311 | rules: Validate.object(),
312 | vhost: internals.vhost,
313 |
314 | // Validated in route construction
315 |
316 | handler: Validate.any(),
317 | options: Validate.any(),
318 | config: Validate.any() // Backwards compatibility
319 | })
320 | .without('config', 'options');
321 |
322 |
323 | internals.pre = [
324 | Validate.function(),
325 | Validate.object({
326 | method: Validate.alternatives(Validate.string(), Validate.function()).required(),
327 | assign: Validate.string(),
328 | mode: Validate.valid('serial', 'parallel'),
329 | failAction: internals.failAction
330 | })
331 | ];
332 |
333 |
334 | internals.routeConfig = internals.routeBase.keys({
335 | description: Validate.string(),
336 | id: Validate.string(),
337 | isInternal: Validate.boolean(),
338 | notes: [
339 | Validate.string(),
340 | Validate.array().items(Validate.string())
341 | ],
342 | pre: Validate.array().items(...internals.pre.concat(Validate.array().items(...internals.pre).min(1))),
343 | tags: [
344 | Validate.string(),
345 | Validate.array().items(Validate.string())
346 | ]
347 | });
348 |
349 |
350 | internals.cacheConfig = Validate.alternatives([
351 | Validate.function(),
352 | Validate.object({
353 | name: Validate.string().invalid('_default'),
354 | shared: Validate.boolean(),
355 | provider: [
356 | Validate.function(),
357 | {
358 | constructor: Validate.function().required(),
359 | options: Validate.object({
360 | partition: Validate.string().default('hapi-cache')
361 | })
362 | .unknown() // Catbox client validates other keys
363 | .default({})
364 | }
365 | ],
366 | engine: Validate.object()
367 | })
368 | .xor('provider', 'engine')
369 | ]);
370 |
371 |
372 | internals.cache = Validate.array().items(internals.cacheConfig).min(1).single();
373 |
374 |
375 | internals.cachePolicy = Validate.object({
376 | cache: Validate.string().allow(null).allow(''),
377 | segment: Validate.string(),
378 | shared: Validate.boolean()
379 | })
380 | .unknown(); // Catbox policy validates other keys
381 |
382 |
383 | internals.method = Validate.object({
384 | bind: Validate.object().allow(null),
385 | generateKey: Validate.function(),
386 | cache: internals.cachePolicy
387 | });
388 |
389 |
390 | internals.methodObject = Validate.object({
391 | name: Validate.string().required(),
392 | method: Validate.function().required(),
393 | options: Validate.object()
394 | });
395 |
396 |
397 | internals.register = Validate.object({
398 | once: true,
399 | routes: Validate.object({
400 | prefix: Validate.string().pattern(/^\/.+/),
401 | vhost: internals.vhost
402 | })
403 | .default({})
404 | });
405 |
406 |
407 | internals.semver = Validate.string();
408 |
409 |
410 | internals.plugin = internals.register.keys({
411 | options: Validate.any(),
412 | plugin: Validate.object({
413 | register: Validate.function().required(),
414 | name: Validate.string().when('pkg.name', { is: Validate.exist(), otherwise: Validate.required() }),
415 | version: Validate.string(),
416 | multiple: Validate.boolean().default(false),
417 | dependencies: [
418 | Validate.array().items(Validate.string()).single(),
419 | Validate.object().pattern(/.+/, internals.semver)
420 | ],
421 | once: true,
422 | requirements: Validate.object({
423 | hapi: Validate.string(),
424 | node: Validate.string()
425 | })
426 | .default(),
427 | pkg: Validate.object({
428 | name: Validate.string(),
429 | version: Validate.string().default('0.0.0')
430 | })
431 | .unknown()
432 | .default({})
433 | })
434 | .unknown()
435 | })
436 | .without('once', 'options')
437 | .unknown();
438 |
439 |
440 | internals.rules = Validate.object({
441 | validate: Validate.object({
442 | schema: Validate.alternatives(Validate.object(), Validate.array()).required(),
443 | options: Validate.object()
444 | .default({ allowUnknown: true })
445 | })
446 | });
447 |
--------------------------------------------------------------------------------
/lib/cors.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Boom = require('@hapi/boom');
4 | const Hoek = require('@hapi/hoek');
5 |
6 | let Route = null; // Delayed load due to circular dependency
7 |
8 |
9 | const internals = {};
10 |
11 |
12 | exports.route = function (options) {
13 |
14 | if (!options) {
15 | return false;
16 | }
17 |
18 | const settings = Hoek.clone(options);
19 | settings._headers = settings.headers.concat(settings.additionalHeaders);
20 | settings._headersString = settings._headers.join(',');
21 | for (let i = 0; i < settings._headers.length; ++i) {
22 | settings._headers[i] = settings._headers[i].toLowerCase();
23 | }
24 |
25 | if (settings._headers.indexOf('origin') === -1) {
26 | settings._headers.push('origin');
27 | }
28 |
29 | settings._exposedHeaders = settings.exposedHeaders.concat(settings.additionalExposedHeaders).join(',');
30 |
31 | if (settings.origin === 'ignore') {
32 | settings._origin = false;
33 | }
34 | else if (settings.origin.indexOf('*') !== -1) {
35 | Hoek.assert(settings.origin.length === 1, 'Cannot specify cors.origin * together with other values');
36 | settings._origin = true;
37 | }
38 | else {
39 | settings._origin = {
40 | qualified: [],
41 | wildcards: []
42 | };
43 |
44 | for (const origin of settings.origin) {
45 | if (origin.indexOf('*') !== -1) {
46 | settings._origin.wildcards.push(new RegExp('^' + Hoek.escapeRegex(origin).replace(/\\\*/g, '.*').replace(/\\\?/g, '.') + '$'));
47 | }
48 | else {
49 | settings._origin.qualified.push(origin);
50 | }
51 | }
52 | }
53 |
54 | return settings;
55 | };
56 |
57 |
58 | exports.options = function (route, server) {
59 |
60 | if (route.method === 'options' ||
61 | !route.settings.cors) {
62 |
63 | return;
64 | }
65 |
66 | exports.handler(server);
67 | };
68 |
69 |
70 | exports.handler = function (server) {
71 |
72 | Route = Route || require('./route');
73 |
74 | if (server._core.router.specials.options) {
75 | return;
76 | }
77 |
78 | const definition = {
79 | method: '_special',
80 | path: '/{p*}',
81 | handler: internals.handler,
82 | options: {
83 | cors: false
84 | }
85 | };
86 |
87 | const route = new Route(definition, server, { special: true });
88 | server._core.router.special('options', route);
89 | };
90 |
91 |
92 | internals.handler = function (request, h) {
93 |
94 | // Validate CORS preflight request
95 |
96 | const method = request.headers['access-control-request-method'];
97 | if (!method) {
98 | throw Boom.notFound('CORS error: Missing Access-Control-Request-Method header');
99 | }
100 |
101 | // Lookup route
102 |
103 | const route = request.server.match(method, request.path, request.info.hostname);
104 | if (!route) {
105 | throw Boom.notFound();
106 | }
107 |
108 | const settings = route.settings.cors;
109 | if (!settings) {
110 | return { message: 'CORS is disabled for this route' };
111 | }
112 |
113 | // Validate Origin header
114 |
115 | const origin = request.headers.origin;
116 |
117 | if (!origin &&
118 | settings._origin !== false) {
119 |
120 | throw Boom.notFound('CORS error: Missing Origin header');
121 | }
122 |
123 | if (!exports.matchOrigin(origin, settings)) {
124 | return { message: 'CORS error: Origin not allowed' };
125 | }
126 |
127 | // Validate allowed headers
128 |
129 | let headers = request.headers['access-control-request-headers'];
130 | if (headers) {
131 | headers = headers.toLowerCase().split(/\s*,\s*/);
132 | if (Hoek.intersect(headers, settings._headers).length !== headers.length) {
133 | return { message: 'CORS error: Some headers are not allowed' };
134 | }
135 | }
136 |
137 | // Reply with the route CORS headers
138 |
139 | const response = h.response();
140 | response.code(settings.preflightStatusCode);
141 | response._header('access-control-allow-origin', settings._origin ? origin : '*');
142 | response._header('access-control-allow-methods', method);
143 | response._header('access-control-allow-headers', settings._headersString);
144 | response._header('access-control-max-age', settings.maxAge);
145 |
146 | if (settings.credentials) {
147 | response._header('access-control-allow-credentials', 'true');
148 | }
149 |
150 | if (settings._exposedHeaders) {
151 | response._header('access-control-expose-headers', settings._exposedHeaders);
152 | }
153 |
154 | return response;
155 | };
156 |
157 |
158 | exports.headers = function (response) {
159 |
160 | const request = response.request;
161 | const settings = request.route.settings.cors;
162 |
163 | if (settings._origin !== false) {
164 | response.vary('origin');
165 | }
166 |
167 | if ((request.info.cors && !request.info.cors.isOriginMatch) || // After route lookup
168 | !exports.matchOrigin(request.headers.origin, request.route.settings.cors)) { // Response from onRequest
169 |
170 | return;
171 | }
172 |
173 | response._header('access-control-allow-origin', settings._origin ? request.headers.origin : '*');
174 |
175 | if (settings.credentials) {
176 | response._header('access-control-allow-credentials', 'true');
177 | }
178 |
179 | if (settings._exposedHeaders) {
180 | response._header('access-control-expose-headers', settings._exposedHeaders, { append: true });
181 | }
182 | };
183 |
184 |
185 | exports.matchOrigin = function (origin, settings) {
186 |
187 | if (settings._origin === true ||
188 | settings._origin === false) {
189 |
190 | return true;
191 | }
192 |
193 | if (!origin) {
194 | return false;
195 | }
196 |
197 | if (settings._origin.qualified.indexOf(origin) !== -1) {
198 | return true;
199 | }
200 |
201 | for (const wildcard of settings._origin.wildcards) {
202 | if (origin.match(wildcard)) {
203 | return true;
204 | }
205 | }
206 |
207 | return false;
208 | };
209 |
--------------------------------------------------------------------------------
/lib/ext.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Hoek = require('@hapi/hoek');
4 | const Topo = require('@hapi/topo');
5 |
6 |
7 | const internals = {};
8 |
9 |
10 | exports = module.exports = internals.Ext = class {
11 |
12 | type = null;
13 | nodes = null;
14 |
15 | #core = null;
16 | #routes = [];
17 | #topo = new Topo.Sorter();
18 |
19 | constructor(type, core) {
20 |
21 | this.#core = core;
22 | this.type = type;
23 | }
24 |
25 | add(event) {
26 |
27 | const methods = [].concat(event.method);
28 | for (const method of methods) {
29 | const settings = {
30 | before: event.options.before,
31 | after: event.options.after,
32 | group: event.realm.plugin,
33 | sort: this.#core.extensionsSeq++
34 | };
35 |
36 | const node = {
37 | func: method, // Request: function (request, h), Server: function (server)
38 | bind: event.options.bind,
39 | server: event.server, // Server event
40 | realm: event.realm,
41 | timeout: event.options.timeout
42 | };
43 |
44 | this.#topo.add(node, settings);
45 | }
46 |
47 | this.nodes = this.#topo.nodes;
48 |
49 | // Notify routes
50 |
51 | for (const route of this.#routes) {
52 | route.rebuild(event);
53 | }
54 | }
55 |
56 | merge(others) {
57 |
58 | const merge = [];
59 | for (const other of others) {
60 | merge.push(other.#topo);
61 | }
62 |
63 | this.#topo.merge(merge);
64 | this.nodes = this.#topo.nodes.length ? this.#topo.nodes : null;
65 | }
66 |
67 | subscribe(route) {
68 |
69 | this.#routes.push(route);
70 | }
71 |
72 | static combine(route, type) {
73 |
74 | const ext = new internals.Ext(type, route._core);
75 |
76 | const events = route.settings.ext[type];
77 | if (events) {
78 | for (let event of events) {
79 | event = Object.assign({}, event); // Shallow cloned
80 | Hoek.assert(!event.options.sandbox, 'Cannot specify sandbox option for route extension');
81 | event.realm = route.realm;
82 | ext.add(event);
83 | }
84 | }
85 |
86 | const server = route._core.extensions.route[type];
87 | const realm = route.realm._extensions[type];
88 |
89 | ext.merge([server, realm]);
90 |
91 | server.subscribe(route);
92 | realm.subscribe(route);
93 |
94 | return ext;
95 | }
96 | };
97 |
--------------------------------------------------------------------------------
/lib/handler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Hoek = require('@hapi/hoek');
4 |
5 |
6 | const internals = {};
7 |
8 |
9 | exports.execute = async function (request) {
10 |
11 | // Prerequisites
12 |
13 | if (request._route._prerequisites) {
14 | for (const set of request._route._prerequisites) { // Serial execution of each set
15 | const pres = [];
16 | for (const item of set) {
17 | pres.push(internals.handler(request, item.method, item));
18 | }
19 |
20 | const responses = await Promise.all(pres); // Parallel execution within sets
21 | for (const response of responses) {
22 | if (response !== undefined) {
23 | return response;
24 | }
25 | }
26 | }
27 | }
28 |
29 | // Handler
30 |
31 | const result = await internals.handler(request, request.route.settings.handler);
32 | if (result._takeover ||
33 | typeof result === 'symbol') {
34 |
35 | return result;
36 | }
37 |
38 | request._setResponse(result);
39 | };
40 |
41 |
42 | internals.handler = async function (request, method, pre) {
43 |
44 | const bind = request.route.settings.bind;
45 | const realm = request.route.realm;
46 | let response = await request._core.toolkit.execute(method, request, { bind, realm, continue: 'null' });
47 |
48 | // Handler
49 |
50 | if (!pre) {
51 | if (response.isBoom) {
52 | request._log(['handler', 'error'], response);
53 | throw response;
54 | }
55 |
56 | return response;
57 | }
58 |
59 | // Pre
60 |
61 | if (response.isBoom) {
62 | response.assign = pre.assign;
63 | response = await request._core.toolkit.failAction(request, pre.failAction, response, { tags: ['pre', 'error'], retain: true });
64 | }
65 |
66 | if (typeof response === 'symbol') {
67 | return response;
68 | }
69 |
70 | if (pre.assign) {
71 | request.pre[pre.assign] = (response.isBoom ? response : response.source);
72 | request.preResponses[pre.assign] = response;
73 | }
74 |
75 | if (response._takeover) {
76 | return response;
77 | }
78 | };
79 |
80 |
81 | exports.defaults = function (method, handler, core) {
82 |
83 | let defaults = null;
84 |
85 | if (typeof handler === 'object') {
86 | const type = Object.keys(handler)[0];
87 | const serverHandler = core.decorations.handler.get(type);
88 |
89 | Hoek.assert(serverHandler, 'Unknown handler:', type);
90 |
91 | if (serverHandler.defaults) {
92 | defaults = (typeof serverHandler.defaults === 'function' ? serverHandler.defaults(method) : serverHandler.defaults);
93 | }
94 | }
95 |
96 | return defaults ?? {};
97 | };
98 |
99 |
100 | exports.configure = function (handler, route) {
101 |
102 | if (typeof handler === 'object') {
103 | const type = Object.keys(handler)[0];
104 | const serverHandler = route._core.decorations.handler.get(type);
105 |
106 | Hoek.assert(serverHandler, 'Unknown handler:', type);
107 |
108 | return serverHandler(route.public, handler[type]);
109 | }
110 |
111 | return handler;
112 | };
113 |
114 |
115 | exports.prerequisitesConfig = function (config) {
116 |
117 | if (!config) {
118 | return null;
119 | }
120 |
121 | /*
122 | [
123 | [
124 | function (request, h) { },
125 | {
126 | method: function (request, h) { }
127 | assign: key1
128 | },
129 | {
130 | method: function (request, h) { },
131 | assign: key2
132 | }
133 | ],
134 | {
135 | method: function (request, h) { },
136 | assign: key3
137 | }
138 | ]
139 | */
140 |
141 | const prerequisites = [];
142 |
143 | for (let pres of config) {
144 | pres = [].concat(pres);
145 |
146 | const set = [];
147 | for (let pre of pres) {
148 | if (typeof pre !== 'object') {
149 | pre = { method: pre };
150 | }
151 |
152 | const item = {
153 | method: pre.method,
154 | assign: pre.assign,
155 | failAction: pre.failAction ?? 'error'
156 | };
157 |
158 | set.push(item);
159 | }
160 |
161 | prerequisites.push(set);
162 | }
163 |
164 | return prerequisites.length ? prerequisites : null;
165 | };
166 |
--------------------------------------------------------------------------------
/lib/headers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | const Stream = require('stream');
5 |
6 | const Boom = require('@hapi/boom');
7 |
8 |
9 | const internals = {};
10 |
11 |
12 | exports.cache = function (response) {
13 |
14 | const request = response.request;
15 | if (response.headers['cache-control']) {
16 | return;
17 | }
18 |
19 | const settings = request.route.settings.cache;
20 | const policy = settings && request._route._cache && (settings._statuses.has(response.statusCode) || (response.statusCode === 304 && settings._statuses.has(200)));
21 |
22 | if (policy ||
23 | response.settings.ttl) {
24 |
25 | const ttl = response.settings.ttl !== null ? response.settings.ttl : request._route._cache.ttl();
26 | const privacy = request.auth.isAuthenticated || response.headers['set-cookie'] ? 'private' : settings.privacy ?? 'default';
27 | response._header('cache-control', 'max-age=' + Math.floor(ttl / 1000) + ', must-revalidate' + (privacy !== 'default' ? ', ' + privacy : ''));
28 | }
29 | else if (settings) {
30 | response._header('cache-control', settings.otherwise);
31 | }
32 | };
33 |
34 |
35 | exports.content = async function (response) {
36 |
37 | const request = response.request;
38 | if (response._isPayloadSupported() ||
39 | request.method === 'head') {
40 |
41 | await response._marshal();
42 |
43 | if (typeof response._payload.size === 'function') {
44 | response._header('content-length', response._payload.size(), { override: false });
45 | }
46 |
47 | if (!response._isPayloadSupported()) {
48 | response._close(); // Close unused file streams
49 | response._payload = new internals.Empty(); // Set empty stream
50 | }
51 |
52 | exports.type(response);
53 | }
54 | else {
55 |
56 | // Set empty stream
57 |
58 | response._close(); // Close unused file streams
59 | response._payload = new internals.Empty();
60 | delete response.headers['content-length'];
61 | }
62 | };
63 |
64 |
65 | exports.state = async function (response) {
66 |
67 | const request = response.request;
68 | const states = [];
69 |
70 | for (const stateName in request._states) {
71 | states.push(request._states[stateName]);
72 | }
73 |
74 | try {
75 | for (const name in request._core.states.cookies) {
76 | const autoValue = request._core.states.cookies[name].autoValue;
77 | if (!autoValue || name in request._states || name in request.state) {
78 | continue;
79 | }
80 |
81 | if (typeof autoValue !== 'function') {
82 | states.push({ name, value: autoValue });
83 | continue;
84 | }
85 |
86 | const value = await autoValue(request);
87 | states.push({ name, value });
88 | }
89 |
90 | if (!states.length) {
91 | return;
92 | }
93 |
94 | let header = await request._core.states.format(states, request);
95 | const existing = response.headers['set-cookie'];
96 | if (existing) {
97 | header = (Array.isArray(existing) ? existing : [existing]).concat(header);
98 | }
99 |
100 | response._header('set-cookie', header);
101 | }
102 | catch (err) {
103 | const error = Boom.boomify(err);
104 | request._log(['state', 'response', 'error'], error);
105 | request._states = {}; // Clear broken state
106 | throw error;
107 | }
108 | };
109 |
110 |
111 | exports.type = function (response) {
112 |
113 | const type = response.contentType;
114 | if (type !== null && type !== response.headers['content-type']) {
115 | response.type(type);
116 | }
117 | };
118 |
119 |
120 | exports.entity = function (response) {
121 |
122 | const request = response.request;
123 |
124 | if (!request._entity) {
125 | return;
126 | }
127 |
128 | if (request._entity.etag &&
129 | !response.headers.etag) {
130 |
131 | response.etag(request._entity.etag, { vary: request._entity.vary });
132 | }
133 |
134 | if (request._entity.modified &&
135 | !response.headers['last-modified']) {
136 |
137 | response.header('last-modified', request._entity.modified);
138 | }
139 | };
140 |
141 |
142 | exports.unmodified = function (response) {
143 |
144 | const request = response.request;
145 | if (response.statusCode === 304) {
146 | return;
147 | }
148 |
149 | const entity = {
150 | etag: response.headers.etag,
151 | vary: response.settings.varyEtag,
152 | modified: response.headers['last-modified']
153 | };
154 |
155 | const etag = request._core.Response.unmodified(request, entity);
156 | if (etag) {
157 | response.code(304);
158 |
159 | if (etag !== true) { // Override etag with incoming weak match
160 | response.headers.etag = etag;
161 | }
162 | }
163 | };
164 |
165 |
166 | internals.Empty = class extends Stream.Readable {
167 |
168 | _read(/* size */) {
169 |
170 | this.push(null);
171 | }
172 |
173 | writeToStream(stream) {
174 |
175 | stream.end();
176 | }
177 | };
178 |
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Server = require('./server');
4 |
5 |
6 | const internals = {};
7 |
8 |
9 | exports.Server = Server;
10 |
11 | exports.server = Server;
12 |
--------------------------------------------------------------------------------
/lib/methods.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Boom = require('@hapi/boom');
4 | const Hoek = require('@hapi/hoek');
5 |
6 | const Config = require('./config');
7 |
8 |
9 | const internals = {
10 | methodNameRx: /^[_$a-zA-Z][$\w]*(?:\.[_$a-zA-Z][$\w]*)*$/
11 | };
12 |
13 |
14 | exports = module.exports = internals.Methods = class {
15 |
16 | methods = {};
17 |
18 | #core = null;
19 |
20 | constructor(core) {
21 |
22 | this.#core = core;
23 | }
24 |
25 | add(name, method, options, realm) {
26 |
27 | if (typeof name !== 'object') {
28 | return this._add(name, method, options, realm);
29 | }
30 |
31 | // {} or [{}, {}]
32 |
33 | const items = [].concat(name);
34 | for (let item of items) {
35 | item = Config.apply('methodObject', item);
36 | this._add(item.name, item.method, item.options ?? {}, realm);
37 | }
38 | }
39 |
40 | _add(name, method, options, realm) {
41 |
42 | Hoek.assert(typeof method === 'function', 'method must be a function');
43 | Hoek.assert(typeof name === 'string', 'name must be a string');
44 | Hoek.assert(name.match(internals.methodNameRx), 'Invalid name:', name);
45 | Hoek.assert(!Hoek.reach(this.methods, name, { functions: false }), 'Server method function name already exists:', name);
46 |
47 | options = Config.apply('method', options, name);
48 |
49 | const settings = Hoek.clone(options, { shallow: ['bind'] });
50 | settings.generateKey = settings.generateKey ?? internals.generateKey;
51 |
52 | const bind = settings.bind ?? realm.settings.bind ?? null;
53 | const bound = !bind ? method : (...args) => method.apply(bind, args);
54 |
55 | // Not cached
56 |
57 | if (!settings.cache) {
58 | return this._assign(name, bound);
59 | }
60 |
61 | // Cached
62 |
63 | Hoek.assert(!settings.cache.generateFunc, 'Cannot set generateFunc with method caching:', name);
64 | Hoek.assert(settings.cache.generateTimeout !== undefined, 'Method caching requires a timeout value in generateTimeout:', name);
65 |
66 | settings.cache.generateFunc = (id, flags) => bound(...id.args, flags);
67 | const cache = this.#core._cachePolicy(settings.cache, '#' + name);
68 |
69 | const func = function (...args) {
70 |
71 | const key = settings.generateKey.apply(bind, args);
72 | if (typeof key !== 'string') {
73 | return Promise.reject(Boom.badImplementation('Invalid method key when invoking: ' + name, { name, args }));
74 | }
75 |
76 | return cache.get({ id: key, args });
77 | };
78 |
79 | func.cache = {
80 | drop: function (...args) {
81 |
82 | const key = settings.generateKey.apply(bind, args);
83 | if (typeof key !== 'string') {
84 | return Promise.reject(Boom.badImplementation('Invalid method key when invoking: ' + name, { name, args }));
85 | }
86 |
87 | return cache.drop(key);
88 | },
89 | stats: cache.stats
90 | };
91 |
92 | this._assign(name, func, func);
93 | }
94 |
95 | _assign(name, method) {
96 |
97 | const path = name.split('.');
98 | let ref = this.methods;
99 | for (let i = 0; i < path.length; ++i) {
100 | if (!ref[path[i]]) {
101 | ref[path[i]] = (i + 1 === path.length ? method : {});
102 | }
103 |
104 | ref = ref[path[i]];
105 | }
106 | }
107 | };
108 |
109 |
110 | internals.supportedArgs = ['string', 'number', 'boolean'];
111 |
112 |
113 | internals.generateKey = function (...args) {
114 |
115 | let key = '';
116 | for (let i = 0; i < args.length; ++i) {
117 | const arg = args[i];
118 | if (!internals.supportedArgs.includes(typeof arg)) {
119 | return null;
120 | }
121 |
122 | key = key + (i ? ':' : '') + encodeURIComponent(arg.toString());
123 | }
124 |
125 | return key;
126 | };
127 |
--------------------------------------------------------------------------------
/lib/route.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Assert = require('assert');
4 |
5 | const Bounce = require('@hapi/bounce');
6 | const Catbox = require('@hapi/catbox');
7 | const Hoek = require('@hapi/hoek');
8 | const Subtext = require('@hapi/subtext');
9 | const Validate = require('@hapi/validate');
10 |
11 | const Auth = require('./auth');
12 | const Config = require('./config');
13 | const Cors = require('./cors');
14 | const Ext = require('./ext');
15 | const Handler = require('./handler');
16 | const Headers = require('./headers');
17 | const Security = require('./security');
18 | const Streams = require('./streams');
19 | const Validation = require('./validation');
20 |
21 |
22 | const internals = {};
23 |
24 |
25 | exports = module.exports = internals.Route = class {
26 |
27 | constructor(route, server, options = {}) {
28 |
29 | const core = server._core;
30 | const realm = server.realm;
31 |
32 | // Routing information
33 |
34 | Config.apply('route', route, route.method, route.path);
35 |
36 | const method = route.method.toLowerCase();
37 | Hoek.assert(method !== 'head', 'Cannot set HEAD route:', route.path);
38 |
39 | const path = realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (route.path !== '/' ? route.path : '') : route.path;
40 | Hoek.assert(path === '/' || path[path.length - 1] !== '/' || !core.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when configured to strip:', route.method, route.path);
41 |
42 | const vhost = realm.modifiers.route.vhost ?? route.vhost;
43 |
44 | // Set identifying members (assert)
45 |
46 | this.method = method;
47 | this.path = path;
48 |
49 | // Prepare configuration
50 |
51 | let config = route.options ?? route.config ?? {};
52 | if (typeof config === 'function') {
53 | config = config.call(realm.settings.bind, server);
54 | }
55 |
56 | config = Config.enable(config); // Shallow clone
57 |
58 | // Verify route level config (as opposed to the merged settings)
59 |
60 | this._assert(method !== 'get' || !config.payload, 'Cannot set payload settings on HEAD or GET request');
61 | this._assert(method !== 'get' || !config.validate?.payload, 'Cannot validate HEAD or GET request payload');
62 |
63 | // Rules
64 |
65 | this._assert(!route.rules || !config.rules, 'Route rules can only appear once'); // XOR
66 | const rules = route.rules ?? config.rules;
67 | const rulesConfig = internals.rules(rules, { method, path, vhost }, server);
68 | delete config.rules;
69 |
70 | // Handler
71 |
72 | this._assert(route.handler || config.handler, 'Missing or undefined handler');
73 | this._assert(!!route.handler ^ !!config.handler, 'Handler must only appear once'); // XOR
74 |
75 | const handler = Config.apply('handler', route.handler ?? config.handler);
76 | delete config.handler;
77 |
78 | const handlerDefaults = Handler.defaults(method, handler, core);
79 |
80 | // Apply settings in order: server <- handler <- realm <- route
81 |
82 | const settings = internals.config([core.settings.routes, handlerDefaults, realm.settings, rulesConfig, config]);
83 | this.settings = Config.apply('routeConfig', settings, method, path);
84 |
85 |
86 | // Route members
87 |
88 | this._core = core;
89 | this.realm = realm;
90 |
91 | this.settings.vhost = vhost;
92 | this.settings.plugins = this.settings.plugins ?? {}; // Route-specific plugins settings, namespaced using plugin name
93 | this.settings.app = this.settings.app ?? {}; // Route-specific application settings
94 |
95 | // Path parsing
96 |
97 | this._special = !!options.special;
98 | this._analysis = this._core.router.analyze(this.path);
99 | this.params = this._analysis.params;
100 | this.fingerprint = this._analysis.fingerprint;
101 |
102 | this.public = {
103 | method: this.method,
104 | path: this.path,
105 | vhost,
106 | realm,
107 | settings: this.settings,
108 | fingerprint: this.fingerprint,
109 | auth: {
110 | access: (request) => Auth.testAccess(request, this.public)
111 | }
112 | };
113 |
114 | // Validation
115 |
116 | this._setupValidation();
117 |
118 | // Payload parsing
119 |
120 | if (this.method === 'get') {
121 | this.settings.payload = null;
122 | }
123 | else {
124 | this.settings.payload.decoders = this._core.compression.decoders; // Reference the shared object to keep up to date
125 | }
126 |
127 | this._assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled');
128 | this._assert(!this.settings.validate.state || this.settings.state.parse, 'Route state must be set to \'parse\' when state validation enabled');
129 |
130 | // Authentication configuration
131 |
132 | this.settings.auth = this._special ? false : this._core.auth._setupRoute(this.settings.auth, path);
133 |
134 | // Cache
135 |
136 | if (this.method === 'get' &&
137 | typeof this.settings.cache === 'object' &&
138 | (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) {
139 |
140 | this.settings.cache._statuses = new Set(this.settings.cache.statuses);
141 | this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt });
142 | }
143 |
144 | // CORS
145 |
146 | this.settings.cors = Cors.route(this.settings.cors);
147 |
148 | // Security
149 |
150 | this.settings.security = Security.route(this.settings.security);
151 |
152 | // Handler
153 |
154 | this.settings.handler = Handler.configure(handler, this);
155 | this._prerequisites = Handler.prerequisitesConfig(this.settings.pre);
156 |
157 | // Route lifecycle
158 |
159 | this._extensions = {
160 | onPreResponse: Ext.combine(this, 'onPreResponse'),
161 | onPostResponse: Ext.combine(this, 'onPostResponse')
162 | };
163 |
164 | if (this._special) {
165 | this._cycle = [internals.drain, Handler.execute];
166 | this.rebuild();
167 | return;
168 | }
169 |
170 | this._extensions.onPreAuth = Ext.combine(this, 'onPreAuth');
171 | this._extensions.onCredentials = Ext.combine(this, 'onCredentials');
172 | this._extensions.onPostAuth = Ext.combine(this, 'onPostAuth');
173 | this._extensions.onPreHandler = Ext.combine(this, 'onPreHandler');
174 | this._extensions.onPostHandler = Ext.combine(this, 'onPostHandler');
175 |
176 | this.rebuild();
177 | }
178 |
179 | _setupValidation() {
180 |
181 | const validation = this.settings.validate;
182 | if (this.method === 'get') {
183 | validation.payload = null;
184 | }
185 |
186 | this._assert(!validation.params || this.params.length, 'Cannot set path parameters validations without path parameters');
187 |
188 | for (const type of ['headers', 'params', 'query', 'payload', 'state']) {
189 | validation[type] = Validation.compile(validation[type], this.settings.validate.validator, this.realm, this._core);
190 | }
191 |
192 | if (this.settings.response.schema !== undefined ||
193 | this.settings.response.status) {
194 |
195 | this.settings.response._validate = true;
196 |
197 | const rule = this.settings.response.schema;
198 | this.settings.response.status = this.settings.response.status ?? {};
199 | const statuses = Object.keys(this.settings.response.status);
200 |
201 | if (rule === true &&
202 | !statuses.length) {
203 |
204 | this.settings.response._validate = false;
205 | }
206 | else {
207 | this.settings.response.schema = Validation.compile(rule, this.settings.validate.validator, this.realm, this._core);
208 | for (const code of statuses) {
209 | this.settings.response.status[code] = Validation.compile(this.settings.response.status[code], this.settings.validate.validator, this.realm, this._core);
210 | }
211 | }
212 | }
213 | }
214 |
215 | rebuild(event) {
216 |
217 | if (event) {
218 | this._extensions[event.type].add(event);
219 | }
220 |
221 | if (this._special) {
222 | this._postCycle = this._extensions.onPreResponse.nodes ? [this._extensions.onPreResponse] : [];
223 | this._buildMarshalCycle();
224 | return;
225 | }
226 |
227 | // Build lifecycle array
228 |
229 | this._cycle = [];
230 |
231 | // 'onRequest'
232 |
233 | if (this.settings.state.parse) {
234 | this._cycle.push(internals.state);
235 | }
236 |
237 | if (this._extensions.onPreAuth.nodes) {
238 | this._cycle.push(this._extensions.onPreAuth);
239 | }
240 |
241 | if (this._core.auth._enabled(this, 'authenticate')) {
242 | this._cycle.push(Auth.authenticate);
243 | }
244 |
245 | if (this.method !== 'get') {
246 | this._cycle.push(internals.payload);
247 |
248 | if (this._core.auth._enabled(this, 'payload')) {
249 | this._cycle.push(Auth.payload);
250 | }
251 | }
252 |
253 | if (this._core.auth._enabled(this, 'authenticate') &&
254 | this._extensions.onCredentials.nodes) {
255 |
256 | this._cycle.push(this._extensions.onCredentials);
257 | }
258 |
259 | if (this._core.auth._enabled(this, 'access')) {
260 | this._cycle.push(Auth.access);
261 | }
262 |
263 | if (this._extensions.onPostAuth.nodes) {
264 | this._cycle.push(this._extensions.onPostAuth);
265 | }
266 |
267 | if (this.settings.validate.headers) {
268 | this._cycle.push(Validation.headers);
269 | }
270 |
271 | if (this.settings.validate.params) {
272 | this._cycle.push(Validation.params);
273 | }
274 |
275 | if (this.settings.validate.query) {
276 | this._cycle.push(Validation.query);
277 | }
278 |
279 | if (this.settings.validate.payload) {
280 | this._cycle.push(Validation.payload);
281 | }
282 |
283 | if (this.settings.validate.state) {
284 | this._cycle.push(Validation.state);
285 | }
286 |
287 | if (this._extensions.onPreHandler.nodes) {
288 | this._cycle.push(this._extensions.onPreHandler);
289 | }
290 |
291 | this._cycle.push(Handler.execute);
292 |
293 | if (this._extensions.onPostHandler.nodes) {
294 | this._cycle.push(this._extensions.onPostHandler);
295 | }
296 |
297 | this._postCycle = [];
298 |
299 | if (this.settings.response._validate &&
300 | this.settings.response.sample !== 0) {
301 |
302 | this._postCycle.push(Validation.response);
303 | }
304 |
305 | if (this._extensions.onPreResponse.nodes) {
306 | this._postCycle.push(this._extensions.onPreResponse);
307 | }
308 |
309 | this._buildMarshalCycle();
310 |
311 | // onPostResponse
312 | }
313 |
314 | _buildMarshalCycle() {
315 |
316 | this._marshalCycle = [Headers.type];
317 |
318 | if (this.settings.cors) {
319 | this._marshalCycle.push(Cors.headers);
320 | }
321 |
322 | if (this.settings.security) {
323 | this._marshalCycle.push(Security.headers);
324 | }
325 |
326 | this._marshalCycle.push(Headers.entity);
327 |
328 | if (this.method === 'get' ||
329 | this.method === '*') {
330 |
331 | this._marshalCycle.push(Headers.unmodified);
332 | }
333 |
334 | this._marshalCycle.push(Headers.cache);
335 | this._marshalCycle.push(Headers.state);
336 | this._marshalCycle.push(Headers.content);
337 |
338 | if (this._core.auth._enabled(this, 'response')) {
339 | this._marshalCycle.push(Auth.response); // Must be last in case requires access to headers
340 | }
341 | }
342 |
343 | _assert(condition, message) {
344 |
345 | if (condition) {
346 | return;
347 | }
348 |
349 | if (this.method[0] !== '_') {
350 | message = `${message}: ${this.method.toUpperCase()} ${this.path}`;
351 | }
352 |
353 | throw new Assert.AssertionError({
354 | message,
355 | actual: false,
356 | expected: true,
357 | operator: '==',
358 | stackStartFunction: this._assert
359 | });
360 | }
361 | };
362 |
363 |
364 | internals.state = async function (request) {
365 |
366 | request.state = {};
367 |
368 | const req = request.raw.req;
369 | const cookies = req.headers.cookie;
370 | if (!cookies) {
371 | return;
372 | }
373 |
374 | try {
375 | var result = await request._core.states.parse(cookies);
376 | }
377 | catch (err) {
378 | Bounce.rethrow(err, 'system');
379 | var parseError = err;
380 | }
381 |
382 | const { states, failed = [] } = result ?? parseError;
383 | request.state = states ?? {};
384 |
385 | // Clear cookies
386 |
387 | for (const item of failed) {
388 | if (item.settings.clearInvalid) {
389 | request._clearState(item.name);
390 | }
391 | }
392 |
393 | if (!parseError) {
394 | return;
395 | }
396 |
397 | parseError.header = cookies;
398 |
399 | return request._core.toolkit.failAction(request, request.route.settings.state.failAction, parseError, { tags: ['state', 'error'] });
400 | };
401 |
402 |
403 | internals.payload = async function (request) {
404 |
405 | if (request.method === 'get' ||
406 | request.method === 'head') { // When route.method is '*'
407 |
408 | return;
409 | }
410 |
411 | if (request.payload !== undefined) {
412 | return internals.drain(request);
413 | }
414 |
415 | if (request._expectContinue) {
416 | request._expectContinue = false;
417 | request.raw.res.writeContinue();
418 | }
419 |
420 | try {
421 | const { payload, mime } = await Subtext.parse(request.raw.req, request._tap(), request.route.settings.payload);
422 |
423 | request._isPayloadPending = !!payload?._readableState;
424 | request.mime = mime;
425 | request.payload = payload;
426 | }
427 | catch (err) {
428 | Bounce.rethrow(err, 'system');
429 |
430 | await internals.drain(request);
431 |
432 | request.mime = err.mime;
433 | request.payload = null;
434 |
435 | return request._core.toolkit.failAction(request, request.route.settings.payload.failAction, err, { tags: ['payload', 'error'] });
436 | }
437 | };
438 |
439 |
440 | internals.drain = async function (request) {
441 |
442 | // Flush out any pending request payload not consumed due to errors
443 |
444 | if (request._expectContinue) {
445 | request._isPayloadPending = false; // If we don't continue, client should not send a payload
446 | request._expectContinue = false;
447 | }
448 |
449 | if (request._isPayloadPending) {
450 | await Streams.drain(request.raw.req);
451 | request._isPayloadPending = false;
452 | }
453 | };
454 |
455 |
456 | internals.config = function (chain) {
457 |
458 | if (!chain.length) {
459 | return {};
460 | }
461 |
462 | let config = chain[0];
463 | for (const item of chain) {
464 | config = Hoek.applyToDefaults(config, item, { shallow: ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query', 'validate.state'] });
465 | }
466 |
467 | return config;
468 | };
469 |
470 |
471 | internals.rules = function (rules, info, server) {
472 |
473 | const configs = [];
474 |
475 | let realm = server.realm;
476 | while (realm) {
477 | if (realm._rules) {
478 | const source = !realm._rules.settings.validate ? rules : Validate.attempt(rules, realm._rules.settings.validate.schema, realm._rules.settings.validate.options);
479 | const config = realm._rules.processor(source, info);
480 | if (config) {
481 | configs.unshift(config);
482 | }
483 | }
484 |
485 | realm = realm.parent;
486 | }
487 |
488 | return internals.config(configs);
489 | };
490 |
--------------------------------------------------------------------------------
/lib/security.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const internals = {};
4 |
5 |
6 | exports.route = function (settings) {
7 |
8 | if (!settings) {
9 | return null;
10 | }
11 |
12 | const security = settings;
13 | if (security.hsts) {
14 | if (security.hsts === true) {
15 | security._hsts = 'max-age=15768000';
16 | }
17 | else if (typeof security.hsts === 'number') {
18 | security._hsts = 'max-age=' + security.hsts;
19 | }
20 | else {
21 | security._hsts = 'max-age=' + (security.hsts.maxAge ?? 15768000);
22 | if (security.hsts.includeSubdomains || security.hsts.includeSubDomains) {
23 | security._hsts = security._hsts + '; includeSubDomains';
24 | }
25 |
26 | if (security.hsts.preload) {
27 | security._hsts = security._hsts + '; preload';
28 | }
29 | }
30 | }
31 |
32 | if (security.xframe) {
33 | if (security.xframe === true) {
34 | security._xframe = 'DENY';
35 | }
36 | else if (typeof security.xframe === 'string') {
37 | security._xframe = security.xframe.toUpperCase();
38 | }
39 | else if (security.xframe.rule === 'allow-from') {
40 | if (!security.xframe.source) {
41 | security._xframe = 'SAMEORIGIN';
42 | }
43 | else {
44 | security._xframe = 'ALLOW-FROM ' + security.xframe.source;
45 | }
46 | }
47 | else {
48 | security._xframe = security.xframe.rule.toUpperCase();
49 | }
50 | }
51 |
52 | return security;
53 | };
54 |
55 |
56 | exports.headers = function (response) {
57 |
58 | const security = response.request.route.settings.security;
59 |
60 | if (security._hsts) {
61 | response._header('strict-transport-security', security._hsts, { override: false });
62 | }
63 |
64 | if (security._xframe) {
65 | response._header('x-frame-options', security._xframe, { override: false });
66 | }
67 |
68 | if (security.xss === 'enabled') {
69 | response._header('x-xss-protection', '1; mode=block', { override: false });
70 | }
71 | else if (security.xss === 'disabled') {
72 | response._header('x-xss-protection', '0', { override: false });
73 | }
74 |
75 | if (security.noOpen) {
76 | response._header('x-download-options', 'noopen', { override: false });
77 | }
78 |
79 | if (security.noSniff) {
80 | response._header('x-content-type-options', 'nosniff', { override: false });
81 | }
82 |
83 | if (security.referrer !== false) {
84 | response._header('referrer-policy', security.referrer, { override: false });
85 | }
86 | };
87 |
--------------------------------------------------------------------------------
/lib/streams.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Stream = require('stream');
4 |
5 | const Boom = require('@hapi/boom');
6 | const Teamwork = require('@hapi/teamwork');
7 |
8 | const internals = {
9 | team: Symbol('team')
10 | };
11 |
12 |
13 | exports.isStream = function (stream) {
14 |
15 | const isReadableStream = stream instanceof Stream.Readable;
16 |
17 | if (!isReadableStream &&
18 | typeof stream?.pipe === 'function') {
19 | throw Boom.badImplementation('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
20 | }
21 |
22 | if (!isReadableStream) {
23 | return false;
24 | }
25 |
26 | if (stream.readableObjectMode) {
27 | throw Boom.badImplementation('Cannot reply with stream in object mode');
28 | }
29 |
30 | return true;
31 | };
32 |
33 |
34 | exports.drain = function (stream) {
35 |
36 | const team = new Teamwork.Team();
37 | stream[internals.team] = team;
38 |
39 | stream.on('readable', internals.read);
40 | stream.on('error', internals.end);
41 | stream.on('end', internals.end);
42 | stream.on('close', internals.end);
43 |
44 | return team.work;
45 | };
46 |
47 |
48 | internals.read = function () {
49 |
50 | while (this.read()) { }
51 | };
52 |
53 |
54 | internals.end = function () {
55 |
56 | this.removeListener('readable', internals.read);
57 | this.removeListener('error', internals.end);
58 | this.removeListener('end', internals.end);
59 | this.removeListener('close', internals.end);
60 |
61 | this[internals.team].attend();
62 | };
63 |
--------------------------------------------------------------------------------
/lib/toolkit.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Boom = require('@hapi/boom');
4 | const Bounce = require('@hapi/bounce');
5 | const Hoek = require('@hapi/hoek');
6 |
7 |
8 | const internals = {};
9 |
10 |
11 | exports.reserved = [
12 | 'abandon',
13 | 'authenticated',
14 | 'close',
15 | 'context',
16 | 'continue',
17 | 'entity',
18 | 'redirect',
19 | 'realm',
20 | 'request',
21 | 'response',
22 | 'state',
23 | 'unauthenticated',
24 | 'unstate'
25 | ];
26 |
27 |
28 | exports.symbols = {
29 | abandon: Symbol('abandon'),
30 | close: Symbol('close'),
31 | continue: Symbol('continue')
32 | };
33 |
34 |
35 | exports.Manager = class {
36 |
37 | constructor() {
38 |
39 | this._toolkit = internals.toolkit();
40 | }
41 |
42 | async execute(method, request, options) {
43 |
44 | const h = new this._toolkit(request, options);
45 | const bind = options.bind ?? null;
46 |
47 | try {
48 | let operation;
49 |
50 | if (bind) {
51 | operation = method.call(bind, request, h);
52 | }
53 | else if (options.args) {
54 | operation = method(request, h, ...options.args);
55 | }
56 | else {
57 | operation = method(request, h);
58 | }
59 |
60 | var response = await exports.timed(operation, options);
61 | }
62 | catch (err) {
63 | if (Bounce.isSystem(err)) {
64 | response = Boom.badImplementation(err);
65 | }
66 | else if (!Bounce.isError(err)) {
67 | response = Boom.badImplementation('Cannot throw non-error object', err);
68 | }
69 | else {
70 | response = Boom.boomify(err);
71 | }
72 | }
73 |
74 | // Process response
75 |
76 | if (options.ignoreResponse) {
77 | return response;
78 | }
79 |
80 | if (response === undefined) {
81 | response = Boom.badImplementation(`${method.name} method did not return a value, a promise, or throw an error`);
82 | }
83 |
84 | if (options.continue &&
85 | response === exports.symbols.continue) {
86 |
87 | if (options.continue === 'undefined') {
88 | return;
89 | }
90 |
91 | // 'null'
92 |
93 | response = null;
94 | }
95 |
96 | if (options.auth &&
97 | response instanceof internals.Auth) {
98 |
99 | return response;
100 | }
101 |
102 | if (typeof response !== 'symbol') {
103 | response = request._core.Response.wrap(response, request);
104 | if (!response.isBoom && response._state === 'init') {
105 | await response._prepare();
106 | }
107 | }
108 |
109 | return response;
110 | }
111 |
112 | decorate(name, method) {
113 |
114 | this._toolkit.prototype[name] = method;
115 | }
116 |
117 | async failAction(request, failAction, err, options) {
118 |
119 | const retain = options.retain ? err : undefined;
120 | if (failAction === 'ignore') {
121 | return retain;
122 | }
123 |
124 | if (failAction === 'log') {
125 | request._log(options.tags, err);
126 | return retain;
127 | }
128 |
129 | if (failAction === 'error') {
130 | throw err;
131 | }
132 |
133 | return await this.execute(failAction, request, { realm: request.route.realm, args: [options.details ?? err] });
134 | }
135 | };
136 |
137 |
138 | exports.timed = async function (method, options) {
139 |
140 | if (!options.timeout) {
141 | return method;
142 | }
143 |
144 | const timer = new Promise((resolve, reject) => {
145 |
146 | const handler = () => {
147 |
148 | reject(Boom.internal(`${options.name} timed out`));
149 | };
150 |
151 | setTimeout(handler, options.timeout);
152 | });
153 |
154 | return await Promise.race([timer, method]);
155 | };
156 |
157 |
158 | /*
159 | const handler = function (request, h) {
160 |
161 | result / h.response(result) -> result // Not allowed before handler
162 | h.response(result).takeover() -> result (respond)
163 | h.continue -> null // Defaults to null only in handler and pre, not allowed in auth
164 |
165 | throw error / h.response(error) -> error (respond) // failAction override in pre
166 | -> badImplementation (respond)
167 |
168 | // Auth only (scheme.payload and scheme.response use the same interface as pre-handler extension methods)
169 |
170 | h.unauthenticated(error, data) -> error (respond) + data
171 | h.authenticated(data ) -> (continue) + data
172 | };
173 | */
174 |
175 | internals.toolkit = function () {
176 |
177 | const Toolkit = class {
178 |
179 | constructor(request, options) {
180 |
181 | this.context = options.bind;
182 | this.realm = options.realm;
183 | this.request = request;
184 |
185 | this._auth = options.auth;
186 | }
187 |
188 | response(result) {
189 |
190 | Hoek.assert(!result || typeof result !== 'object' || typeof result.then !== 'function', 'Cannot wrap a promise');
191 | Hoek.assert(result instanceof Error === false, 'Cannot wrap an error');
192 | Hoek.assert(typeof result !== 'symbol', 'Cannot wrap a symbol');
193 |
194 | return this.request._core.Response.wrap(result, this.request);
195 | }
196 |
197 | redirect(location) {
198 |
199 | return this.response('').redirect(location);
200 | }
201 |
202 | entity(options) {
203 |
204 | Hoek.assert(options, 'Entity method missing required options');
205 | Hoek.assert(options.etag || options.modified, 'Entity methods missing required options key');
206 |
207 | this.request._entity = options;
208 |
209 | const entity = this.request._core.Response.entity(options.etag, options);
210 | if (this.request._core.Response.unmodified(this.request, entity)) {
211 | return this.response().code(304).takeover();
212 | }
213 | }
214 |
215 | state(name, value, options) {
216 |
217 | this.request._setState(name, value, options);
218 | }
219 |
220 | unstate(name, options) {
221 |
222 | this.request._clearState(name, options);
223 | }
224 |
225 | authenticated(data) {
226 |
227 | Hoek.assert(this._auth, 'Method not supported outside of authentication');
228 | Hoek.assert(data?.credentials, 'Authentication data missing credentials information');
229 |
230 | return new internals.Auth(null, data);
231 | }
232 |
233 | unauthenticated(error, data) {
234 |
235 | Hoek.assert(this._auth, 'Method not supported outside of authentication');
236 | Hoek.assert(!data || data.credentials, 'Authentication data missing credentials information');
237 |
238 | return new internals.Auth(error, data);
239 | }
240 | };
241 |
242 | Toolkit.prototype.abandon = exports.symbols.abandon;
243 | Toolkit.prototype.close = exports.symbols.close;
244 | Toolkit.prototype.continue = exports.symbols.continue;
245 |
246 | return Toolkit;
247 | };
248 |
249 |
250 | internals.Auth = class {
251 |
252 | constructor(error, data) {
253 |
254 | this.isAuth = true;
255 | this.error = error;
256 | this.data = data;
257 | }
258 | };
259 |
--------------------------------------------------------------------------------
/lib/transmit.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Http = require('http');
4 |
5 | const Ammo = require('@hapi/ammo');
6 | const Boom = require('@hapi/boom');
7 | const Bounce = require('@hapi/bounce');
8 | const Hoek = require('@hapi/hoek');
9 | const Teamwork = require('@hapi/teamwork');
10 |
11 | const Config = require('./config');
12 |
13 |
14 | const internals = {};
15 |
16 |
17 | exports.send = async function (request) {
18 |
19 | const response = request.response;
20 |
21 | try {
22 | if (response.isBoom) {
23 | await internals.fail(request, response);
24 | return;
25 | }
26 |
27 | await internals.marshal(response);
28 | await internals.transmit(response);
29 | }
30 | catch (err) {
31 | Bounce.rethrow(err, 'system');
32 | request._setResponse(err);
33 | return internals.fail(request, err);
34 | }
35 | };
36 |
37 |
38 | internals.marshal = async function (response) {
39 |
40 | for (const func of response.request._route._marshalCycle) {
41 | await func(response);
42 | }
43 | };
44 |
45 |
46 | internals.fail = async function (request, boom) {
47 |
48 | const response = internals.error(request, boom);
49 | request.response = response; // Not using request._setResponse() to avoid double log
50 |
51 | try {
52 | await internals.marshal(response);
53 | }
54 | catch (err) {
55 | Bounce.rethrow(err, 'system');
56 |
57 | // Failed to marshal an error - replace with minimal representation of original error
58 |
59 | const minimal = {
60 | statusCode: response.statusCode,
61 | error: Http.STATUS_CODES[response.statusCode],
62 | message: boom.message
63 | };
64 |
65 | response._payload = new request._core.Response.Payload(JSON.stringify(minimal), {});
66 | }
67 |
68 | return internals.transmit(response);
69 | };
70 |
71 |
72 | internals.error = function (request, boom) {
73 |
74 | const error = boom.output;
75 | const response = new request._core.Response(error.payload, request, { error: boom });
76 | response.code(error.statusCode);
77 | response.headers = Hoek.clone(error.headers); // Prevent source from being modified
78 | return response;
79 | };
80 |
81 |
82 | internals.transmit = function (response) {
83 |
84 | const request = response.request;
85 | const length = internals.length(response);
86 |
87 | // Pipes
88 |
89 | const encoding = request._core.compression.encoding(response, length);
90 | const ranger = encoding ? null : internals.range(response, length);
91 | const compressor = internals.encoding(response, encoding);
92 |
93 | // Connection: close
94 |
95 | const isInjection = request.isInjected;
96 | if (!(isInjection || request._core.started) ||
97 | request._isPayloadPending && !request.raw.req._readableState.ended) {
98 |
99 | response._header('connection', 'close');
100 | }
101 |
102 | // Write headers
103 |
104 | internals.writeHead(response);
105 |
106 | // Injection
107 |
108 | if (isInjection) {
109 | request.raw.res[Config.symbol] = { request };
110 |
111 | if (response.variety === 'plain') {
112 | request.raw.res[Config.symbol].result = response._isPayloadSupported() ? response.source : null;
113 | }
114 | }
115 |
116 | // Finalize response stream
117 |
118 | const stream = internals.chain([response._payload, response._tap(), compressor, ranger]);
119 | return internals.pipe(request, stream);
120 | };
121 |
122 |
123 | internals.length = function (response) {
124 |
125 | const request = response.request;
126 |
127 | const header = response.headers['content-length'];
128 | if (header === undefined) {
129 | return null;
130 | }
131 |
132 | let length = header;
133 | if (typeof length === 'string') {
134 | length = parseInt(header, 10);
135 | if (!isFinite(length)) {
136 | delete response.headers['content-length'];
137 | return null;
138 | }
139 | }
140 |
141 | // Empty response
142 |
143 | if (length === 0 &&
144 | !response._statusCode &&
145 | response.statusCode === 200 &&
146 | request.route.settings.response.emptyStatusCode !== 200) {
147 |
148 | response.code(204);
149 | delete response.headers['content-length'];
150 | }
151 |
152 | return length;
153 | };
154 |
155 |
156 | internals.range = function (response, length) {
157 |
158 | const request = response.request;
159 |
160 | if (!length ||
161 | !request.route.settings.response.ranges ||
162 | request.method !== 'get' ||
163 | response.statusCode !== 200) {
164 |
165 | return null;
166 | }
167 |
168 | response._header('accept-ranges', 'bytes');
169 |
170 | if (!request.headers.range) {
171 | return null;
172 | }
173 |
174 | // Check If-Range
175 |
176 | if (request.headers['if-range'] &&
177 | request.headers['if-range'] !== response.headers.etag) { // Ignoring last-modified date (weak)
178 |
179 | return null;
180 | }
181 |
182 | // Parse header
183 |
184 | const ranges = Ammo.header(request.headers.range, length);
185 | if (!ranges) {
186 | const error = Boom.rangeNotSatisfiable();
187 | error.output.headers['content-range'] = 'bytes */' + length;
188 | throw error;
189 | }
190 |
191 | // Prepare transform
192 |
193 | if (ranges.length !== 1) { // Ignore requests for multiple ranges
194 | return null;
195 | }
196 |
197 | const range = ranges[0];
198 | response.code(206);
199 | response.bytes(range.to - range.from + 1);
200 | response._header('content-range', 'bytes ' + range.from + '-' + range.to + '/' + length);
201 |
202 | return new Ammo.Clip(range);
203 | };
204 |
205 |
206 | internals.encoding = function (response, encoding) {
207 |
208 | const request = response.request;
209 |
210 | const header = response.headers['content-encoding'] || encoding;
211 | if (header &&
212 | response.headers.etag &&
213 | response.settings.varyEtag) {
214 |
215 | response.headers.etag = response.headers.etag.slice(0, -1) + '-' + header + '"';
216 | }
217 |
218 | if (!encoding ||
219 | response.statusCode === 206 ||
220 | !response._isPayloadSupported()) {
221 |
222 | return null;
223 | }
224 |
225 | delete response.headers['content-length'];
226 | response._header('content-encoding', encoding);
227 | const compressor = request._core.compression.encoder(request, encoding);
228 | if (response.variety === 'stream' &&
229 | typeof response._payload.setCompressor === 'function') {
230 |
231 | response._payload.setCompressor(compressor);
232 | }
233 |
234 | return compressor;
235 | };
236 |
237 |
238 | internals.pipe = function (request, stream) {
239 |
240 | const team = new Teamwork.Team();
241 |
242 | // Write payload
243 |
244 | const env = { stream, request, team };
245 |
246 | if (request._closed) {
247 |
248 | // The request has already been aborted - no need to wait or attempt to write.
249 |
250 | internals.end(env, 'aborted');
251 | return team.work;
252 | }
253 |
254 | const aborted = internals.end.bind(null, env, 'aborted');
255 | const close = internals.end.bind(null, env, 'close');
256 | const end = internals.end.bind(null, env, null);
257 |
258 | request.raw.req.on('aborted', aborted);
259 |
260 | request.raw.res.on('close', close);
261 | request.raw.res.on('error', end);
262 | request.raw.res.on('finish', end);
263 |
264 | if (stream.writeToStream) {
265 | stream.writeToStream(request.raw.res);
266 | }
267 | else {
268 | stream.on('error', end);
269 | stream.pipe(request.raw.res);
270 | }
271 |
272 | return team.work;
273 | };
274 |
275 |
276 | internals.end = function (env, event, err) {
277 |
278 | const { request, stream, team } = env;
279 |
280 | if (!team) { // Used instead of cleaning up emitter listeners
281 | return;
282 | }
283 |
284 | env.team = null;
285 |
286 | if (request.raw.res.writableEnded) {
287 | request.info.responded = Date.now();
288 |
289 | team.attend();
290 | return;
291 | }
292 |
293 | if (err) {
294 | request.raw.res.destroy();
295 | request._core.Response.drain(stream);
296 | }
297 |
298 | // Update reported response to reflect the error condition
299 |
300 | const origResponse = request.response;
301 | const error = err ? Boom.boomify(err) :
302 | new Boom.Boom(`Request ${event}`, { statusCode: request.route.settings.response.disconnectStatusCode, data: origResponse });
303 |
304 | request._setResponse(error);
305 |
306 | // Make inject throw a disconnect error
307 |
308 | if (request.raw.res[Config.symbol]) {
309 | request.raw.res[Config.symbol].error = event ? error :
310 | new Boom.Boom(`Response error`, { statusCode: request.route.settings.response.disconnectStatusCode, data: origResponse });
311 | }
312 |
313 | if (event) {
314 | request._log(['response', 'error', event]);
315 | }
316 | else {
317 | request._log(['response', 'error'], err);
318 | }
319 |
320 | request.raw.res.end(); // Triggers injection promise resolve
321 | team.attend();
322 | };
323 |
324 |
325 | internals.writeHead = function (response) {
326 |
327 | const res = response.request.raw.res;
328 | const headers = Object.keys(response.headers);
329 | let i = 0;
330 |
331 | try {
332 | for (; i < headers.length; ++i) {
333 | const header = headers[i];
334 | const value = response.headers[header];
335 | if (value !== undefined) {
336 | res.setHeader(header, value);
337 | }
338 | }
339 | }
340 | catch (err) {
341 | for (--i; i >= 0; --i) {
342 | res.removeHeader(headers[i]); // Undo headers
343 | }
344 |
345 | throw Boom.boomify(err);
346 | }
347 |
348 | if (response.settings.message) {
349 | res.statusMessage = response.settings.message;
350 | }
351 |
352 | try {
353 | res.writeHead(response.statusCode);
354 | }
355 | catch (err) {
356 | throw Boom.boomify(err);
357 | }
358 | };
359 |
360 |
361 | internals.chain = function (sources) {
362 |
363 | let from = sources[0];
364 | for (let i = 1; i < sources.length; ++i) {
365 | const to = sources[i];
366 | if (to) {
367 | from.on('error', internals.errorPipe.bind(from, to));
368 | from = from.pipe(to);
369 | }
370 | }
371 |
372 | return from;
373 | };
374 |
375 |
376 | internals.errorPipe = function (to, err) {
377 |
378 | to.emit('error', err);
379 | };
380 |
--------------------------------------------------------------------------------
/lib/types/index.d.ts:
--------------------------------------------------------------------------------
1 | // Definitions adapted from DefinitelyTyped, originally created by:
2 | // Rafael Souza Fijalkowski
3 | // Justin Simms
4 | // Simon Schick
5 | // Rodrigo Saboya
6 | // Silas Rech
7 |
8 | export * from './plugin';
9 | export * from './response';
10 | export * from './request';
11 | export * from './route';
12 | export * from './server';
13 | export * from './utils';
14 |
15 | // Kept for backwards compatibility only (remove in next major)
16 |
17 | export namespace Utils {
18 | interface Dictionary {
19 | [key: string]: T;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lib/types/plugin.d.ts:
--------------------------------------------------------------------------------
1 | import { RequestRoute } from './request';
2 | import { RouteOptions } from './route';
3 | import { Server } from './server';
4 | import { Lifecycle } from './utils';
5 |
6 | /**
7 | * one of
8 | * a single plugin name string.
9 | * an array of plugin name strings.
10 | * an object where each key is a plugin name and each matching value is a
11 | * {@link https://www.npmjs.com/package/semver version range string} which must match the registered
12 | * plugin version.
13 | */
14 | export type Dependencies = string | string[] | Record;
15 |
16 | /**
17 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverregistrations)
18 | */
19 | export interface PluginsListRegistered {
20 | }
21 |
22 | /**
23 | * An object of the currently registered plugins where each key is a registered plugin name and the value is an
24 | * object containing:
25 | * * version - the plugin version.
26 | * * name - the plugin name.
27 | * * options - (optional) options passed to the plugin during registration.
28 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverregistrations)
29 | */
30 | export interface PluginRegistered {
31 | /**
32 | * the plugin version.
33 | */
34 | version: string;
35 |
36 | /**
37 | * the plugin name.
38 | */
39 | name: string;
40 |
41 | /**
42 | * options used to register the plugin.
43 | */
44 | options: object;
45 | }
46 |
47 | export interface PluginsStates {
48 | }
49 |
50 | export interface PluginSpecificConfiguration {
51 | }
52 |
53 | export interface PluginNameVersion {
54 | /**
55 | * (required) the plugin name string. The name is used as a unique key. Published plugins (e.g. published in the npm
56 | * registry) should use the same name as the name field in their 'package.json' file. Names must be
57 | * unique within each application.
58 | */
59 | name: string;
60 |
61 | /**
62 | * optional plugin version. The version is only used informatively to enable other plugins to find out the versions loaded. The version should be the same as the one specified in the plugin's
63 | * 'package.json' file.
64 | */
65 | version?: string | undefined;
66 | }
67 |
68 | export interface PluginPackage {
69 | /**
70 | * Alternatively, the name and version can be included via the pkg property containing the 'package.json' file for the module which already has the name and version included
71 | */
72 | pkg: PluginNameVersion;
73 | }
74 |
75 | export interface PluginBase {
76 | /**
77 | * (required) the registration function with the signature async function(server, options) where:
78 | * * server - the server object with a plugin-specific server.realm.
79 | * * options - any options passed to the plugin during registration via server.register().
80 | */
81 | register: (server: Server, options: T) => void | Promise;
82 |
83 | /** (optional) if true, allows the plugin to be registered multiple times with the same server. Defaults to false. */
84 | multiple?: boolean | undefined;
85 |
86 | /** (optional) a string or an array of strings indicating a plugin dependency. Same as setting dependencies via server.dependency(). */
87 | dependencies?: Dependencies | undefined;
88 |
89 | /**
90 | * Allows defining semver requirements for node and hapi.
91 | * @default Allows all.
92 | */
93 | requirements?: {
94 | node?: string | undefined;
95 | hapi?: string | undefined;
96 | } | undefined;
97 |
98 | /** once - (optional) if true, will only register the plugin once per server. If set, overrides the once option passed to server.register(). Defaults to no override. */
99 | once?: boolean | undefined;
100 |
101 | /**
102 | * We need to use D within the PluginBase type to be able to infer it later on,
103 | * but this property has no concrete existence in the code.
104 | *
105 | * See https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-type-inference-work-on-this-interface-interface-foot-- for details.
106 | */
107 | ___$type_of_plugin_decorations$___?: D;
108 | }
109 |
110 | /**
111 | * A plugin that is registered by name and version.
112 | */
113 | export interface NamedPlugin extends PluginBase, PluginNameVersion {}
114 |
115 | /**
116 | * A plugin that is registered by its package.json file.
117 | */
118 | export interface PackagedPlugin extends PluginBase, PluginPackage {}
119 |
120 |
121 | /**
122 | * Plugins provide a way to organize application code by splitting the server logic into smaller components. Each
123 | * plugin can manipulate the server through the standard server interface, but with the added ability to sandbox
124 | * certain properties. For example, setting a file path in one plugin doesn't affect the file path set
125 | * in another plugin.
126 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#plugins)
127 | *
128 | * The type T is the type of the plugin options.
129 | */
130 | export type Plugin = NamedPlugin | PackagedPlugin;
131 |
132 | /**
133 | * The realm object contains sandboxed server settings specific to each plugin or authentication strategy. When registering a plugin or an authentication scheme, a server object reference is provided
134 | * with a new server.realm container specific to that registration. It allows each plugin to maintain its own settings without leaking and affecting other plugins. For example, a plugin can set a
135 | * default file path for local resources without breaking other plugins' configured paths. When calling server.bind(), the active realm's settings.bind property is set which is then used by routes
136 | * and extensions added at the same level (server root or plugin).
137 | *
138 | * https://github.com/hapijs/hapi/blob/master/API.md#server.realm
139 | */
140 | export interface ServerRealm {
141 | /** when the server object is provided as an argument to the plugin register() method, modifiers provides the registration preferences passed the server.register() method and includes: */
142 | modifiers: {
143 | /** routes preferences: */
144 | route: {
145 | /**
146 | * the route path prefix used by any calls to server.route() from the server. Note that if a prefix is used and the route path is set to '/', the resulting path will not include
147 | * the trailing slash.
148 | */
149 | prefix: string;
150 | /** the route virtual host settings used by any calls to server.route() from the server. */
151 | vhost: string;
152 | }
153 | };
154 | /** the realm of the parent server object, or null for the root server. */
155 | parent: ServerRealm | null;
156 | /** the active plugin name (empty string if at the server root). */
157 | plugin: string;
158 | /** the plugin options object passed at registration. */
159 | pluginOptions: object;
160 | /** plugin-specific state to be shared only among activities sharing the same active state. plugins is an object where each key is a plugin name and the value is the plugin state. */
161 | plugins: PluginsStates;
162 | /** settings overrides */
163 | settings: {
164 | files: {
165 | relativeTo: string;
166 | };
167 | bind: object;
168 | };
169 | }
170 |
171 | /**
172 | * Registration options (different from the options passed to the registration function):
173 | * * once - if true, subsequent registrations of the same plugin are skipped without error. Cannot be used with plugin options. Defaults to false. If not set to true, an error will be thrown the
174 | * second time a plugin is registered on the server.
175 | * * routes - modifiers applied to each route added by the plugin:
176 | * * * prefix - string added as prefix to any route path (must begin with '/'). If a plugin registers a child plugin the prefix is passed on to the child or is added in front of the child-specific
177 | * prefix.
178 | * * * vhost - virtual host string (or array of strings) applied to every route. The outer-most vhost overrides the any nested configuration.
179 | * For reference [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-await-serverregisterplugins-options)
180 | */
181 | export interface ServerRegisterOptions {
182 | /**
183 | * if true, subsequent registrations of the same plugin are skipped without error. Cannot be used with plugin options. Defaults to false. If not set to true, an error will be thrown the second
184 | * time a plugin is registered on the server.
185 | */
186 | once?: boolean | undefined;
187 | /**
188 | * modifiers applied to each route added by the plugin:
189 | */
190 | routes?: {
191 | /**
192 | * string added as prefix to any route path (must begin with '/'). If a plugin registers a child plugin the prefix is passed on to the child or is added in front of the child-specific prefix.
193 | */
194 | prefix: string;
195 | /**
196 | * virtual host string (or array of strings) applied to every route. The outer-most vhost overrides the any nested configuration.
197 | */
198 | vhost?: string | string[] | undefined;
199 | } | undefined;
200 | }
201 |
202 | export interface ServerRegisterPluginObjectDirect extends ServerRegisterOptions {
203 | /**
204 | * a plugin object.
205 | */
206 | plugin: Plugin;
207 | /**
208 | * options passed to the plugin during registration.
209 | */
210 | options?: T | undefined;
211 | }
212 |
213 | export interface ServerRegisterPluginObjectWrapped extends ServerRegisterOptions {
214 | /**
215 | * a plugin object.
216 | */
217 | plugin: { plugin: Plugin };
218 | /**
219 | * options passed to the plugin during registration.
220 | */
221 | options?: T | undefined;
222 | }
223 |
224 | /**
225 | * An object with the following:
226 | * * plugin - a plugin object or a wrapped plugin loaded module.
227 | * * options - (optional) options passed to the plugin during registration.
228 | * * once - if true, subsequent registrations of the same plugin are skipped without error. Cannot be used with plugin options. Defaults to false. If not set to true, an error will be thrown the
229 | * second time a plugin is registered on the server.
230 | * * routes - modifiers applied to each route added by the plugin:
231 | * * * prefix - string added as prefix to any route path (must begin with '/'). If a plugin registers a child plugin the prefix is passed on to the child or is added in front of the child-specific
232 | * prefix.
233 | * * * vhost - virtual host string (or array of strings) applied to every route. The outer-most vhost overrides the any nested configuration.
234 | * For reference [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-await-serverregisterplugins-options)
235 | *
236 | * The type parameter T is the type of the plugin configuration options.
237 | */
238 | export type ServerRegisterPluginObject =
239 | ServerRegisterPluginObjectDirect |
240 | ServerRegisterPluginObjectWrapped;
241 |
242 | export type ServerRegisterPluginObjectArray = (
243 | ServerRegisterPluginObject |
244 | ServerRegisterPluginObject |
245 | ServerRegisterPluginObject |
246 | ServerRegisterPluginObject |
247 | ServerRegisterPluginObject |
248 | ServerRegisterPluginObject |
249 | ServerRegisterPluginObject
250 | )[];
251 |
252 | /**
253 | * The method function can have a defaults object or function property. If the property is set to an object, that object is used as the default route config for routes using this handler.
254 | * If the property is set to a function, the function uses the signature function(method) and returns the route default configuration.
255 | */
256 | export interface HandlerDecorationMethod {
257 | (route: RequestRoute, options: any): Lifecycle.Method;
258 | defaults?: RouteOptions | ((method: any) => RouteOptions) | undefined;
259 | }
260 |
261 | /**
262 | * An empty interface to allow typings of custom plugin properties.
263 | */
264 |
265 | export interface PluginProperties {
266 | }
267 |
--------------------------------------------------------------------------------
/lib/types/server/auth.d.ts:
--------------------------------------------------------------------------------
1 | import { Server } from './server';
2 | import {
3 | MergeType,
4 | ReqRef,
5 | ReqRefDefaults,
6 | MergeRefs,
7 | Request,
8 | RequestAuth} from '../request';
9 | import { ResponseToolkit, AuthenticationData } from '../response';
10 | import { RouteOptionsAccess, InternalRouteOptionType, RouteOptionTypes} from '../route';
11 | import { Lifecycle } from '../utils';
12 |
13 | /**
14 | * The scheme options argument passed to server.auth.strategy() when instantiation a strategy.
15 | * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverauthschemename-scheme)
16 | */
17 | export type ServerAuthSchemeOptions = object;
18 |
19 | /**
20 | * the method implementing the scheme with signature function(server, options) where:
21 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverauthschemename-scheme)
22 | * @param server - a reference to the server object the scheme is added to.
23 | * @param options - (optional) the scheme options argument passed to server.auth.strategy() when instantiation a strategy.
24 | */
25 | export type ServerAuthScheme<
26 | // tslint:disable-next-line no-unnecessary-generics
27 | Options extends ServerAuthSchemeOptions = ServerAuthSchemeOptions,
28 | // tslint:disable-next-line no-unnecessary-generics
29 | Refs extends ReqRef = ReqRefDefaults
30 | > = (server: Server, options?: Options) => ServerAuthSchemeObject;
31 |
32 | export interface ServerAuthSchemeObjectApi {}
33 |
34 | /**
35 | * The scheme method must return an object with the following
36 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#authentication-scheme)
37 | */
38 |
39 | export interface ServerAuthSchemeObject {
40 | /**
41 | * optional object which is exposed via the [server.auth.api](https://github.com/hapijs/hapi/blob/master/API.md#server.auth.api) object.
42 | */
43 | api?: MergeRefs['AuthApi'] | undefined;
44 |
45 | /**
46 | * A lifecycle method function called for each incoming request configured with the authentication scheme. The
47 | * method is provided with two special toolkit methods for returning an authenticated or an unauthenticated result:
48 | * * h.authenticated() - indicate request authenticated successfully.
49 | * * h.unauthenticated() - indicate request failed to authenticate.
50 | * @param request the request object.
51 | * @param h the ResponseToolkit
52 | * @return the Lifecycle.ReturnValue
53 | */
54 | authenticate(request: Request, h: ResponseToolkit): Lifecycle.ReturnValue;
55 |
56 | /**
57 | * A lifecycle method to authenticate the request payload.
58 | * When the scheme payload() method returns an error with a message, it means payload validation failed due to bad
59 | * payload. If the error has no message but includes a scheme name (e.g. Boom.unauthorized(null, 'Custom')),
60 | * authentication may still be successful if the route auth.payload configuration is set to 'optional'.
61 | * @param request the request object.
62 | * @param h the ResponseToolkit
63 | * @return the Lifecycle.ReturnValue
64 | */
65 | payload?(request: Request, h: ResponseToolkit): Lifecycle.ReturnValue;
66 |
67 | /**
68 | * A lifecycle method to decorate the response with authentication headers before the response headers or payload is written.
69 | * @param request the request object.
70 | * @param h the ResponseToolkit
71 | * @return the Lifecycle.ReturnValue
72 | */
73 | response?(request: Request, h: ResponseToolkit): Lifecycle.ReturnValue;
74 |
75 | /**
76 | * a method used to verify the authentication credentials provided
77 | * are still valid (e.g. not expired or revoked after the initial authentication).
78 | * the method throws an `Error` when the credentials passed are no longer valid (e.g. expired or
79 | * revoked). Note that the method does not have access to the original request, only to the
80 | * credentials and artifacts produced by the `authenticate()` method.
81 | */
82 | verify?(
83 | auth: RequestAuth<
84 | MergeRefs['AuthUser'],
85 | MergeRefs['AuthApp'],
86 | MergeRefs['AuthCredentialsExtra'],
87 | MergeRefs['AuthArtifactsExtra']
88 | >
89 | ): Promise;
90 |
91 | /**
92 | * An object with the following keys:
93 | * * payload
94 | */
95 | options?: {
96 | /**
97 | * if true, requires payload validation as part of the scheme and forbids routes from disabling payload auth validation. Defaults to false.
98 | */
99 | payload?: boolean | undefined;
100 | } | undefined;
101 | }
102 |
103 | /**
104 | * An authentication configuration object using the same format as the route auth handler options.
105 | * For reference [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions)
106 | */
107 |
108 | export interface ServerAuthConfig extends RouteOptionsAccess {
109 | }
110 |
111 | export interface ServerAuth {
112 | /**
113 | * An object where each key is an authentication strategy name and the value is the exposed strategy API.
114 | * Available only when the authentication scheme exposes an API by returning an api key in the object
115 | * returned from its implementation function.
116 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverauthapi)
117 | */
118 | api: Record;
119 |
120 | /**
121 | * Contains the default authentication configuration is a default strategy was set via
122 | * [server.auth.default()](https://github.com/hapijs/hapi/blob/master/API.md#server.auth.default()).
123 | */
124 | readonly settings: {
125 | default: ServerAuthConfig;
126 | };
127 |
128 | /**
129 | * Sets a default strategy which is applied to every route where:
130 | * @param options - one of:
131 | * * a string with the default strategy name
132 | * * an authentication configuration object using the same format as the route auth handler options.
133 | * @return void.
134 | * The default does not apply when a route config specifies auth as false, or has an authentication strategy
135 | * configured (contains the strategy or strategies authentication settings). Otherwise, the route authentication
136 | * config is applied to the defaults.
137 | * Note that if the route has authentication configured, the default only applies at the time of adding the route,
138 | * not at runtime. This means that calling server.auth.default() after adding a route with some authentication
139 | * config will have no impact on the routes added prior. However, the default will apply to routes added
140 | * before server.auth.default() is called if those routes lack any authentication config.
141 | * The default auth strategy configuration can be accessed via server.auth.settings.default. To obtain the active
142 | * authentication configuration of a route, use server.auth.lookup(request.route).
143 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions)
144 | */
145 | default(options: string | ServerAuthConfig): void;
146 |
147 | /**
148 | * Registers an authentication scheme where:
149 | * @param name the scheme name.
150 | * @param scheme - the method implementing the scheme with signature function(server, options) where:
151 | * * server - a reference to the server object the scheme is added to.
152 | * * options - (optional) the scheme options argument passed to server.auth.strategy() when instantiation a strategy.
153 | * @return void.
154 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverauthschemename-scheme)
155 | */
156 |
157 | scheme <
158 | Refs extends ReqRef = ReqRefDefaults,
159 | Options extends object = {}
160 | // tslint:disable-next-line no-unnecessary-generics
161 | >(name: string, scheme: ServerAuthScheme): void;
162 |
163 | /**
164 | * Registers an authentication strategy where:
165 | * @param name - the strategy name.
166 | * @param scheme - the scheme name (must be previously registered using server.auth.scheme()).
167 | * @param options - scheme options based on the scheme requirements.
168 | * @return Return value: none.
169 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverauthstrategyname-scheme-options)
170 | */
171 | strategy(
172 | name: MergeType['Strategy'],
173 | scheme: string,
174 | options?: object
175 | ): void;
176 |
177 | /**
178 | * Tests a request against an authentication strategy where:
179 | * @param strategy - the strategy name registered with server.auth.strategy().
180 | * @param request - the request object.
181 | * @return an object containing the authentication credentials and artifacts if authentication was successful, otherwise throws an error.
182 | * Note that the test() method does not take into account the route authentication configuration. It also does not
183 | * perform payload authentication. It is limited to the basic strategy authentication execution. It does not
184 | * include verifying scope, entity, or other route properties.
185 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-await-serverauthteststrategy-request)
186 | */
187 | test(strategy: string, request: Request): Promise;
188 |
189 | /**
190 | * Verify a request's authentication credentials against an authentication strategy.
191 | * Returns nothing if verification was successful, otherwise throws an error.
192 | *
193 | * Note that the `verify()` method does not take into account the route authentication configuration
194 | * or any other information from the request other than the `request.auth` object. It also does not
195 | * perform payload authentication. It is limited to verifying that the previously valid credentials
196 | * are still valid (e.g. have not been revoked or expired). It does not include verifying scope,
197 | * entity, or other route properties.
198 | */
199 | // tslint:disable-next-line no-unnecessary-generics
200 | verify (request: Request): Promise;
201 | }
202 |
--------------------------------------------------------------------------------
/lib/types/server/cache.d.ts:
--------------------------------------------------------------------------------
1 | import { PolicyOptionVariants, Policy, ClientApi, ClientOptions, EnginePrototype, PolicyOptions } from '@hapi/catbox';
2 |
3 | export type CachePolicyOptions = PolicyOptionVariants & {
4 | /**
5 | * @default '_default'
6 | */
7 | cache?: string | undefined;
8 | segment?: string | undefined;
9 | };
10 |
11 | /**
12 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servercacheoptions)
13 | */
14 | export interface ServerCache {
15 | /**
16 | * Provisions a cache segment within the server cache facility where:
17 | * @param options - [catbox policy](https://github.com/hapijs/catbox#policy) configuration where:
18 | * * expiresIn - relative expiration expressed in the number of milliseconds since the item was saved in the cache. Cannot be used together with expiresAt.
19 | * * expiresAt - time of day expressed in 24h notation using the 'HH:MM' format, at which point all cache records expire. Uses local time. Cannot be used together with expiresIn.
20 | * * generateFunc - a function used to generate a new cache item if one is not found in the cache when calling get(). The method's signature is async function(id, flags) where:
21 | * - `id` - the `id` string or object provided to the `get()` method.
22 | * - `flags` - an object used to pass back additional flags to the cache where:
23 | * - `ttl` - the cache ttl value in milliseconds. Set to `0` to skip storing in the cache. Defaults to the cache global policy.
24 | * * staleIn - number of milliseconds to mark an item stored in cache as stale and attempt to regenerate it when generateFunc is provided. Must be less than expiresIn.
25 | * * staleTimeout - number of milliseconds to wait before checking if an item is stale.
26 | * * generateTimeout - number of milliseconds to wait before returning a timeout error when the generateFunc function takes too long to return a value. When the value is eventually returned, it
27 | * is stored in the cache for future requests. Required if generateFunc is present. Set to false to disable timeouts which may cause all get() requests to get stuck forever.
28 | * * generateOnReadError - if false, an upstream cache read error will stop the cache.get() method from calling the generate function and will instead pass back the cache error. Defaults to true.
29 | * * generateIgnoreWriteError - if false, an upstream cache write error when calling cache.get() will be passed back with the generated value when calling. Defaults to true.
30 | * * dropOnError - if true, an error or timeout in the generateFunc causes the stale value to be evicted from the cache. Defaults to true.
31 | * * pendingGenerateTimeout - number of milliseconds while generateFunc call is in progress for a given id, before a subsequent generateFunc call is allowed. Defaults to 0 (no blocking of
32 | * concurrent generateFunc calls beyond staleTimeout).
33 | * * cache - the cache name configured in server.cache. Defaults to the default cache.
34 | * * segment - string segment name, used to isolate cached items within the cache partition. When called within a plugin, defaults to '!name' where 'name' is the plugin name. When called within a
35 | * server method, defaults to '#name' where 'name' is the server method name. Required when called outside of a plugin.
36 | * * shared - if true, allows multiple cache provisions to share the same segment. Default to false.
37 | * @return Catbox Policy.
38 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servercacheoptions)
39 | */
40 | = CachePolicyOptions>(options: O): Policy;
41 |
42 | /**
43 | * Provisions a server cache as described in server.cache where:
44 | * @param options - same as the server cache configuration options.
45 | * @return Return value: none.
46 | * Note that if the server has been initialized or started, the cache will be automatically started to match the state of any other provisioned server cache.
47 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-await-servercacheprovisionoptions)
48 | */
49 | provision(options: ServerOptionsCache): Promise;
50 | }
51 |
52 | export type CacheProvider = EnginePrototype | {
53 | constructor: EnginePrototype;
54 | options?: T | undefined;
55 | };
56 |
57 | /**
58 | * hapi uses catbox for its cache implementation which includes support for common storage solutions (e.g. Redis,
59 | * MongoDB, Memcached, Riak, among others). Caching is only utilized if methods and plugins explicitly store their state in the cache.
60 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-cache)
61 | */
62 | export interface ServerOptionsCache extends PolicyOptions {
63 | /** catbox engine object. */
64 | engine?: ClientApi | undefined;
65 |
66 | /**
67 | * a class or a prototype function
68 | */
69 | provider?: CacheProvider | undefined;
70 |
71 | /**
72 | * an identifier used later when provisioning or configuring caching for server methods or plugins. Each cache name must be unique. A single item may omit the name option which defines
73 | * the default cache. If every cache includes a name, a default memory cache is provisioned as well.
74 | */
75 | name?: string | undefined;
76 |
77 | /** if true, allows multiple cache users to share the same segment (e.g. multiple methods using the same cache storage container). Default to false. */
78 | shared?: boolean | undefined;
79 |
80 | /** (optional) string used to isolate cached data. Defaults to 'hapi-cache'. */
81 | partition?: string | undefined;
82 |
83 | /** other options passed to the catbox strategy used. Other options are only passed to catbox when engine above is a class or function and ignored if engine is a catbox engine object). */
84 | [s: string]: any;
85 | }
86 |
--------------------------------------------------------------------------------
/lib/types/server/encoders.d.ts:
--------------------------------------------------------------------------------
1 | import { createDeflate, createGunzip, createGzip, createInflate } from 'zlib';
2 |
3 | /**
4 | * Available [content encoders](https://github.com/hapijs/hapi/blob/master/API.md#-serverencoderencoding-encoder).
5 | */
6 | export interface ContentEncoders {
7 |
8 | deflate: typeof createDeflate;
9 | gzip: typeof createGzip;
10 | }
11 |
12 | /**
13 | * Available [content decoders](https://github.com/hapijs/hapi/blob/master/API.md#-serverdecoderencoding-decoder).
14 | */
15 | export interface ContentDecoders {
16 |
17 | deflate: typeof createInflate;
18 | gzip: typeof createGunzip;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/types/server/events.d.ts:
--------------------------------------------------------------------------------
1 | import { Podium } from '@hapi/podium';
2 |
3 | import { Request, RequestRoute } from '../request';
4 |
5 | /**
6 | * an event name string.
7 | * an event options object.
8 | * a podium emitter object.
9 | * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servereventevents)
10 | */
11 | export type ServerEventsApplication = string | ServerEventsApplicationObject | Podium;
12 |
13 | /**
14 | * Object that it will be used in Event
15 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servereventevents)
16 | */
17 | export interface ServerEventsApplicationObject {
18 | /** the event name string (required). */
19 | name: string;
20 | /** a string or array of strings specifying the event channels available. Defaults to no channel restrictions (event updates can specify a channel or not). */
21 | channels?: string | string[] | undefined;
22 | /**
23 | * if true, the data object passed to server.events.emit() is cloned before it is passed to the listeners (unless an override specified by each listener). Defaults to false (data is passed as-is).
24 | */
25 | clone?: boolean | undefined;
26 | /**
27 | * if true, the data object passed to server.event.emit() must be an array and the listener method is called with each array element passed as a separate argument (unless an override specified
28 | * by each listener). This should only be used when the emitted data structure is known and predictable. Defaults to false (data is emitted as a single argument regardless of its type).
29 | */
30 | spread?: boolean | undefined;
31 | /**
32 | * if true and the criteria object passed to server.event.emit() includes tags, the tags are mapped to an object (where each tag string is the key and the value is true) which is appended to
33 | * the arguments list at the end. A configuration override can be set by each listener. Defaults to false.
34 | */
35 | tags?: boolean | undefined;
36 | /**
37 | * if true, the same event name can be registered multiple times where the second registration is ignored. Note that if the registration config is changed between registrations, only the first
38 | * configuration is used. Defaults to false (a duplicate registration will throw an error).
39 | */
40 | shared?: boolean | undefined;
41 | }
42 |
43 | /**
44 | * A criteria object with the following optional keys (unless noted otherwise):
45 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servereventsoncriteria-listener)
46 | *
47 | * The type parameter T is the type of the name of the event.
48 | */
49 | export interface ServerEventCriteria {
50 | /** (required) the event name string. */
51 | name: T;
52 | /**
53 | * a string or array of strings specifying the event channels to subscribe to. If the event registration specified a list of allowed channels, the channels array must match the allowed
54 | * channels. If channels are specified, event updates without any channel designation will not be included in the subscription. Defaults to no channels filter.
55 | */
56 | channels?: string | string[] | undefined;
57 | /** if true, the data object passed to server.event.emit() is cloned before it is passed to the listener method. Defaults to the event registration option (which defaults to false). */
58 | clone?: boolean | undefined;
59 | /**
60 | * a positive integer indicating the number of times the listener can be called after which the subscription is automatically removed. A count of 1 is the same as calling server.events.once().
61 | * Defaults to no limit.
62 | */
63 | count?: number | undefined;
64 | /**
65 | * filter - the event tags (if present) to subscribe to which can be one of:
66 | * * a tag string.
67 | * * an array of tag strings.
68 | * * an object with the following:
69 | * * * tags - a tag string or array of tag strings.
70 | * * * all - if true, all tags must be present for the event update to match the subscription. Defaults to false (at least one matching tag).
71 | */
72 | filter?: string | string[] | { tags: string | string[] | undefined, all?: boolean | undefined } | undefined;
73 | /**
74 | * if true, and the data object passed to server.event.emit() is an array, the listener method is called with each array element passed as a separate argument. This should only be used
75 | * when the emitted data structure is known and predictable. Defaults to the event registration option (which defaults to false).
76 | */
77 | spread?: boolean | undefined;
78 | /**
79 | * if true and the criteria object passed to server.event.emit() includes tags, the tags are mapped to an object (where each tag string is the key and the value is true) which is appended
80 | * to the arguments list at the end. Defaults to the event registration option (which defaults to false).
81 | */
82 | tags?: boolean | undefined;
83 | }
84 |
85 | export interface LogEvent {
86 | /** the event timestamp. */
87 | timestamp: string;
88 | /** an array of tags identifying the event (e.g. ['error', 'http']) */
89 | tags: string[];
90 | /** set to 'internal' for internally generated events, otherwise 'app' for events generated by server.log() */
91 | channel: 'internal' | 'app';
92 | /** the request identifier. */
93 | request: string;
94 | /** event-specific information. Available when event data was provided and is not an error. Errors are passed via error. */
95 | data: T;
96 | /** the error object related to the event if applicable. Cannot appear together with data */
97 | error: object;
98 | }
99 |
100 | export interface RequestEvent {
101 | /** the event timestamp. */
102 | timestamp: string;
103 | /** an array of tags identifying the event (e.g. ['error', 'http']) */
104 | tags: string[];
105 | /** set to 'internal' for internally generated events, otherwise 'app' for events generated by server.log() */
106 | channel: 'internal' | 'app' | 'error';
107 | /** event-specific information. Available when event data was provided and is not an error. Errors are passed via error. */
108 | data: object | string;
109 | /** the error object related to the event if applicable. Cannot appear together with data */
110 | error: object;
111 | }
112 |
113 | export type LogEventHandler = (event: LogEvent, tags: { [key: string]: true }) => void;
114 | export type RequestEventHandler = (request: Request, event: RequestEvent, tags: { [key: string]: true }) => void;
115 | export type ResponseEventHandler = (request: Request) => void;
116 | export type RouteEventHandler = (route: RequestRoute) => void;
117 | export type StartEventHandler = () => void;
118 | export type StopEventHandler = () => void;
119 |
120 | export interface PodiumEvent {
121 | emit(criteria: K, listener: (value: T) => void): void;
122 |
123 | on(criteria: K, listener: (value: T) => void): void;
124 |
125 | once(criteria: K, listener: (value: T) => void): void;
126 |
127 | once(criteria: K): Promise;
128 |
129 | removeListener(criteria: K, listener: Podium.Listener): this;
130 |
131 | removeAllListeners(criteria: K): this;
132 |
133 | hasListeners(criteria: K): this;
134 | }
135 |
136 | /**
137 | * Access: podium public interface.
138 | * The server events emitter. Utilizes the podium with support for event criteria validation, channels, and filters.
139 | * Use the following methods to interact with server.events:
140 | * [server.event(events)](https://github.com/hapijs/hapi/blob/master/API.md#server.event()) - register application events.
141 | * [server.events.emit(criteria, data)](https://github.com/hapijs/hapi/blob/master/API.md#server.events.emit()) - emit server events.
142 | * [server.events.on(criteria, listener)](https://github.com/hapijs/hapi/blob/master/API.md#server.events.on()) - subscribe to all events.
143 | * [server.events.once(criteria, listener)](https://github.com/hapijs/hapi/blob/master/API.md#server.events.once()) - subscribe to
144 | * Other methods include: server.events.removeListener(name, listener), server.events.removeAllListeners(name), and server.events.hasListeners(name).
145 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverevents)
146 | */
147 | export interface ServerEvents extends Podium {
148 | /**
149 | * Subscribe to an event where:
150 | * @param criteria - the subscription criteria which must be one of:
151 | * * event name string which can be any of the built-in server events
152 | * * a custom application event registered with server.event().
153 | * * a criteria object
154 | * @param listener - the handler method set to receive event updates. The function signature depends on the event argument, and the spread and tags options.
155 | * @return Return value: none.
156 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servereventsoncriteria-listener)
157 | * See ['log' event](https://github.com/hapijs/hapi/blob/master/API.md#-log-event)
158 | * See ['request' event](https://github.com/hapijs/hapi/blob/master/API.md#-request-event)
159 | * See ['response' event](https://github.com/hapijs/hapi/blob/master/API.md#-response-event)
160 | * See ['route' event](https://github.com/hapijs/hapi/blob/master/API.md#-route-event)
161 | * See ['start' event](https://github.com/hapijs/hapi/blob/master/API.md#-start-event)
162 | * See ['stop' event](https://github.com/hapijs/hapi/blob/master/API.md#-stop-event)
163 | */
164 | on(criteria: 'log' | ServerEventCriteria<'log'>, listener: LogEventHandler): this;
165 | on(criteria: 'request' | ServerEventCriteria<'request'>, listener: RequestEventHandler): this;
166 | on(criteria: 'response' | ServerEventCriteria<'response'>, listener: ResponseEventHandler): this;
167 | on(criteria: 'route' | ServerEventCriteria<'route'>, listener: RouteEventHandler): this;
168 | on(criteria: 'start' | ServerEventCriteria<'start'>, listener: StartEventHandler): this;
169 | on(criteria: 'stop' | ServerEventCriteria<'stop'>, listener: StopEventHandler): this;
170 | on(criteria: string | ServerEventCriteria, listener: (value: any) => void): this;
171 |
172 | /**
173 | * Same as calling [server.events.on()](https://github.com/hapijs/hapi/blob/master/API.md#server.events.on()) with the count option set to 1.
174 | * @param criteria - the subscription criteria which must be one of:
175 | * * event name string which can be any of the built-in server events
176 | * * a custom application event registered with server.event().
177 | * * a criteria object
178 | * @param listener - the handler method set to receive event updates. The function signature depends on the event argument, and the spread and tags options.
179 | * @return Return value: none.
180 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servereventsoncecriteria-listener)
181 | */
182 | once(criteria: 'log' | ServerEventCriteria<'log'>, listener: LogEventHandler): this;
183 | once(criteria: 'request' | ServerEventCriteria<'request'>, listener: RequestEventHandler): this;
184 | once(criteria: 'response' | ServerEventCriteria<'response'>, listener: ResponseEventHandler): this;
185 | once(criteria: 'route' | ServerEventCriteria<'route'>, listener: RouteEventHandler): this;
186 | once(criteria: 'start' | ServerEventCriteria<'start'>, listener: StartEventHandler): this;
187 | once(criteria: 'stop' | ServerEventCriteria<'stop'>, listener: StopEventHandler): this;
188 |
189 | /**
190 | * Same as calling server.events.on() with the count option set to 1.
191 | * @param criteria - the subscription criteria which must be one of:
192 | * * event name string which can be any of the built-in server events
193 | * * a custom application event registered with server.event().
194 | * * a criteria object
195 | * @return Return value: a promise that resolves when the event is emitted.
196 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-await-servereventsoncecriteria)
197 | */
198 | once(criteria: string | ServerEventCriteria): Promise;
199 |
200 | /**
201 | * The follow method is only mentioned in Hapi API. The doc about that method can be found [here](https://github.com/hapijs/podium/blob/master/API.md#podiumremovelistenername-listener)
202 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverevents)
203 | */
204 | removeListener(name: string, listener: Podium.Listener): this;
205 |
206 | /**
207 | * The follow method is only mentioned in Hapi API. The doc about that method can be found [here](https://github.com/hapijs/podium/blob/master/API.md#podiumremovealllistenersname)
208 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverevents)
209 | */
210 | removeAllListeners(name: string): this;
211 |
212 | /**
213 | * The follow method is only mentioned in Hapi API. The doc about that method can be found [here](https://github.com/hapijs/podium/blob/master/API.md#podiumhaslistenersname)
214 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverevents)
215 | */
216 | hasListeners(name: string): boolean;
217 | }
218 |
--------------------------------------------------------------------------------
/lib/types/server/ext.d.ts:
--------------------------------------------------------------------------------
1 | import { Server, ServerApplicationState } from './server';
2 | import { ReqRef, ReqRefDefaults } from '../request';
3 | import { Lifecycle } from '../utils';
4 |
5 | /**
6 | * The extension point event name. The available extension points include the request extension points as well as the following server extension points:
7 | * 'onPreStart' - called before the connection listeners are started.
8 | * 'onPostStart' - called after the connection listeners are started.
9 | * 'onPreStop' - called before the connection listeners are stopped.
10 | * 'onPostStop' - called after the connection listeners are stopped.
11 | * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverextevents)
12 | * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#request-lifecycle)
13 | */
14 | export type ServerExtType = 'onPreStart' | 'onPostStart' | 'onPreStop' | 'onPostStop';
15 | export type RouteRequestExtType = 'onPreAuth'
16 | | 'onCredentials'
17 | | 'onPostAuth'
18 | | 'onPreHandler'
19 | | 'onPostHandler'
20 | | 'onPreResponse'
21 | | 'onPostResponse';
22 |
23 | export type ServerRequestExtType =
24 | RouteRequestExtType
25 | | 'onRequest';
26 |
27 | /**
28 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverextevents)
29 | * Registers an extension function in one of the request lifecycle extension points where:
30 | * @param events - an object or array of objects with the following:
31 | * * type - (required) the extension point event name. The available extension points include the request extension points as well as the following server extension points:
32 | * * * 'onPreStart' - called before the connection listeners are started.
33 | * * * 'onPostStart' - called after the connection listeners are started.
34 | * * * 'onPreStop' - called before the connection listeners are stopped.
35 | * * * 'onPostStop' - called after the connection listeners are stopped.
36 | * * method - (required) a function or an array of functions to be executed at a specified point during request processing. The required extension function signature is:
37 | * * * server extension points: async function(server) where:
38 | * * * * server - the server object.
39 | * * * * this - the object provided via options.bind or the current active context set with server.bind().
40 | * * * request extension points: a lifecycle method.
41 | * * options - (optional) an object with the following:
42 | * * * before - a string or array of strings of plugin names this method must execute before (on the same event). Otherwise, extension methods are executed in the order added.
43 | * * * after - a string or array of strings of plugin names this method must execute after (on the same event). Otherwise, extension methods are executed in the order added.
44 | * * * bind - a context object passed back to the provided method (via this) when called. Ignored if the method is an arrow function.
45 | * * * sandbox - if set to 'plugin' when adding a request extension points the extension is only added to routes defined by the current plugin. Not allowed when configuring route-level extensions, or
46 | * when adding server extensions. Defaults to 'server' which applies to any route added to the server the extension is added to.
47 | * @return void
48 | */
49 | export interface ServerExtEventsObject {
50 | /**
51 | * (required) the extension point event name. The available extension points include the request extension points as well as the following server extension points:
52 | * * 'onPreStart' - called before the connection listeners are started.
53 | * * 'onPostStart' - called after the connection listeners are started.
54 | * * 'onPreStop' - called before the connection listeners are stopped.
55 | */
56 | type: ServerExtType;
57 | /**
58 | * (required) a function or an array of functions to be executed at a specified point during request processing. The required extension function signature is:
59 | * * server extension points: async function(server) where:
60 | * * * server - the server object.
61 | * * * this - the object provided via options.bind or the current active context set with server.bind().
62 | * * request extension points: a lifecycle method.
63 | */
64 | method: ServerExtPointFunction | ServerExtPointFunction[];
65 | options?: ServerExtOptions | undefined;
66 | }
67 |
68 | export interface RouteExtObject {
69 | method: Lifecycle.Method;
70 | options?: ServerExtOptions | undefined;
71 | }
72 |
73 | /**
74 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverextevents)
75 | * Registers an extension function in one of the request lifecycle extension points where:
76 | * @param events - an object or array of objects with the following:
77 | * * type - (required) the extension point event name. The available extension points include the request extension points as well as the following server extension points:
78 | * * * 'onPreStart' - called before the connection listeners are started.
79 | * * * 'onPostStart' - called after the connection listeners are started.
80 | * * * 'onPreStop' - called before the connection listeners are stopped.
81 | * * * 'onPostStop' - called after the connection listeners are stopped.
82 | * * method - (required) a function or an array of functions to be executed at a specified point during request processing. The required extension function signature is:
83 | * * * server extension points: async function(server) where:
84 | * * * * server - the server object.
85 | * * * * this - the object provided via options.bind or the current active context set with server.bind().
86 | * * * request extension points: a lifecycle method.
87 | * * options - (optional) an object with the following:
88 | * * * before - a string or array of strings of plugin names this method must execute before (on the same event). Otherwise, extension methods are executed in the order added.
89 | * * * after - a string or array of strings of plugin names this method must execute after (on the same event). Otherwise, extension methods are executed in the order added.
90 | * * * bind - a context object passed back to the provided method (via this) when called. Ignored if the method is an arrow function.
91 | * * * sandbox - if set to 'plugin' when adding a request extension points the extension is only added to routes defined by the current plugin. Not allowed when configuring route-level extensions, or
92 | * when adding server extensions. Defaults to 'server' which applies to any route added to the server the extension is added to.
93 | * @return void
94 | */
95 | export interface ServerExtEventsRequestObject {
96 | /**
97 | * (required) the extension point event name. The available extension points include the request extension points as well as the following server extension points:
98 | * * 'onPreStart' - called before the connection listeners are started.
99 | * * 'onPostStart' - called after the connection listeners are started.
100 | * * 'onPreStop' - called before the connection listeners are stopped.
101 | * * 'onPostStop' - called after the connection listeners are stopped.
102 | */
103 | type: ServerRequestExtType;
104 | /**
105 | * (required) a function or an array of functions to be executed at a specified point during request processing. The required extension function signature is:
106 | * * server extension points: async function(server) where:
107 | * * * server - the server object.
108 | * * * this - the object provided via options.bind or the current active context set with server.bind().
109 | * * request extension points: a lifecycle method.
110 | */
111 | method: Lifecycle.Method | Lifecycle.Method[];
112 | /**
113 | * (optional) an object with the following:
114 | * * before - a string or array of strings of plugin names this method must execute before (on the same event). Otherwise, extension methods are executed in the order added.
115 | * * after - a string or array of strings of plugin names this method must execute after (on the same event). Otherwise, extension methods are executed in the order added.
116 | * * bind - a context object passed back to the provided method (via this) when called. Ignored if the method is an arrow function.
117 | * * sandbox - if set to 'plugin' when adding a request extension points the extension is only added to routes defined by the current plugin. Not allowed when configuring route-level extensions,
118 | * or when adding server extensions. Defaults to 'server' which applies to any route added to the server the extension is added to.
119 | */
120 | options?: ServerExtOptions | undefined;
121 | }
122 |
123 | export type ServerExtPointFunction = (server: Server) => void;
124 |
125 | /**
126 | * An object with the following:
127 | * * before - a string or array of strings of plugin names this method must execute before (on the same event). Otherwise, extension methods are executed in the order added.
128 | * * after - a string or array of strings of plugin names this method must execute after (on the same event). Otherwise, extension methods are executed in the order added.
129 | * * bind - a context object passed back to the provided method (via this) when called. Ignored if the method is an arrow function.
130 | * * sandbox - if set to 'plugin' when adding a request extension points the extension is only added to routes defined by the current plugin. Not allowed when configuring route-level extensions, or
131 | * when adding server extensions. Defaults to 'server' which applies to any route added to the server the extension is added to. For context [See
132 | * docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverextevents)
133 | */
134 | export interface ServerExtOptions {
135 | /**
136 | * a string or array of strings of plugin names this method must execute before (on the same event). Otherwise, extension methods are executed in the order added.
137 | */
138 | before?: string | string[] | undefined;
139 | /**
140 | * a string or array of strings of plugin names this method must execute after (on the same event). Otherwise, extension methods are executed in the order added.
141 | */
142 | after?: string | string[] | undefined;
143 | /**
144 | * a context object passed back to the provided method (via this) when called. Ignored if the method is an arrow function.
145 | */
146 | bind?: object | undefined;
147 | /**
148 | * if set to 'plugin' when adding a request extension points the extension is only added to routes defined by the current plugin. Not allowed when configuring route-level extensions, or when
149 | * adding server extensions. Defaults to 'server' which applies to any route added to the server the extension is added to.
150 | */
151 | sandbox?: 'server' | 'plugin' | undefined;
152 | }
153 |
--------------------------------------------------------------------------------
/lib/types/server/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './auth';
2 | export * from './cache';
3 | export * from './encoders';
4 | export * from './events';
5 | export * from './ext';
6 | export * from './info';
7 | export * from './inject';
8 | export * from './methods';
9 | export * from './options';
10 | export * from './server';
11 | export * from './state';
12 |
--------------------------------------------------------------------------------
/lib/types/server/info.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverinfo)
3 | * An object containing information about the server where:
4 | */
5 | export interface ServerInfo {
6 | /**
7 | * a unique server identifier (using the format '{hostname}:{pid}:{now base36}').
8 | */
9 | id: string;
10 |
11 | /**
12 | * server creation timestamp.
13 | */
14 | created: number;
15 |
16 | /**
17 | * server start timestamp (0 when stopped).
18 | */
19 | started: number;
20 |
21 | /**
22 | * the connection [port](https://github.com/hapijs/hapi/blob/master/API.md#server.options.port) based on the following rules:
23 | * * before the server has been started: the configured port value.
24 | * * after the server has been started: the actual port assigned when no port is configured or was set to 0.
25 | */
26 | port: number | string;
27 |
28 | /**
29 | * The [host](https://github.com/hapijs/hapi/blob/master/API.md#server.options.host) configuration value.
30 | */
31 | host: string;
32 |
33 | /**
34 | * the active IP address the connection was bound to after starting. Set to undefined until the server has been
35 | * started or when using a non TCP port (e.g. UNIX domain socket).
36 | */
37 | address: undefined | string;
38 |
39 | /**
40 | * the protocol used:
41 | * * 'http' - HTTP.
42 | * * 'https' - HTTPS.
43 | * * 'socket' - UNIX domain socket or Windows named pipe.
44 | */
45 | protocol: 'http' | 'https' | 'socket';
46 |
47 | /**
48 | * a string representing the connection (e.g. 'http://example.com:8080' or 'socket:/unix/domain/socket/path'). Contains
49 | * the uri value if set, otherwise constructed from the available settings. If no port is configured or is set
50 | * to 0, the uri will not include a port component until the server is started.
51 | */
52 | uri: string;
53 | }
54 |
--------------------------------------------------------------------------------
/lib/types/server/inject.d.ts:
--------------------------------------------------------------------------------
1 | import { RequestOptions as ShotRequestOptions, ResponseObject as ShotResponseObject } from '@hapi/shot';
2 | import { PluginsStates } from '../plugin';
3 | import { AuthArtifacts, AuthCredentials, Request, RequestApplicationState } from '../request';
4 |
5 | /**
6 | * An object with:
7 | * * method - (optional) the request HTTP method (e.g. 'POST'). Defaults to 'GET'.
8 | * * url - (required) the request URL. If the URI includes an authority (e.g. 'example.com:8080'), it is used to automatically set an HTTP 'Host' header, unless one was specified in headers.
9 | * * headers - (optional) an object with optional request headers where each key is the header name and the value is the header content. Defaults to no additions to the default shot headers.
10 | * * payload - (optional) an string, buffer or object containing the request payload. In case of an object it will be converted to a string for you. Defaults to no payload. Note that payload
11 | * processing defaults to 'application/json' if no 'Content-Type' header provided.
12 | * * credentials - (optional) an credentials object containing authentication information. The credentials are used to bypass the default authentication strategies, and are validated directly as if
13 | * they were received via an authentication scheme. Defaults to no credentials.
14 | * * artifacts - (optional) an artifacts object containing authentication artifact information. The artifacts are used to bypass the default authentication strategies, and are validated directly as
15 | * if they were received via an authentication scheme. Ignored if set without credentials. Defaults to no artifacts.
16 | * * app - (optional) sets the initial value of request.app, defaults to {}.
17 | * * plugins - (optional) sets the initial value of request.plugins, defaults to {}.
18 | * * allowInternals - (optional) allows access to routes with config.isInternal set to true. Defaults to false.
19 | * * remoteAddress - (optional) sets the remote address for the incoming connection.
20 | * * simulate - (optional) an object with options used to simulate client request stream conditions for testing:
21 | * * error - if true, emits an 'error' event after payload transmission (if any). Defaults to false.
22 | * * close - if true, emits a 'close' event after payload transmission (if any). Defaults to false.
23 | * * end - if false, does not end the stream. Defaults to true.
24 | * * split - indicates whether the request payload will be split into chunks. Defaults to undefined, meaning payload will not be chunked.
25 | * * validate - (optional) if false, the options inputs are not validated. This is recommended for run-time usage of inject() to make it perform faster where input validation can be tested
26 | * separately.
27 | * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-await-serverinjectoptions)
28 | * For context [Shot module](https://github.com/hapijs/shot)
29 | */
30 | export interface ServerInjectOptions extends ShotRequestOptions {
31 | /**
32 | * Authentication bypass options.
33 | */
34 | auth?: {
35 | /**
36 | * The authentication strategy name matching the provided credentials.
37 | */
38 | strategy: string;
39 | /**
40 | * The credentials are used to bypass the default authentication strategies,
41 | * and are validated directly as if they were received via an authentication scheme.
42 | */
43 | credentials: AuthCredentials;
44 | /**
45 | * The artifacts are used to bypass the default authentication strategies,
46 | * and are validated directly as if they were received via an authentication scheme. Defaults to no artifacts.
47 | */
48 | artifacts?: AuthArtifacts | undefined;
49 | } | undefined;
50 | /**
51 | * sets the initial value of request.app, defaults to {}.
52 | */
53 | app?: RequestApplicationState | undefined;
54 | /**
55 | * sets the initial value of request.plugins, defaults to {}.
56 | */
57 | plugins?: PluginsStates | undefined;
58 | /**
59 | * allows access to routes with config.isInternal set to true. Defaults to false.
60 | */
61 | allowInternals?: boolean | undefined;
62 | }
63 |
64 | /**
65 | * A response object with the following properties:
66 | * * statusCode - the HTTP status code.
67 | * * headers - an object containing the headers set.
68 | * * payload - the response payload string.
69 | * * rawPayload - the raw response payload buffer.
70 | * * raw - an object with the injection request and response objects:
71 | * * req - the simulated node request object.
72 | * * res - the simulated node response object.
73 | * * result - the raw handler response (e.g. when not a stream or a view) before it is serialized for transmission. If not available, the value is set to payload. Useful for inspection and reuse of
74 | * the internal objects returned (instead of parsing the response string).
75 | * * request - the request object.
76 | * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-await-serverinjectoptions)
77 | * For context [Shot module](https://github.com/hapijs/shot)
78 | */
79 | export interface ServerInjectResponse extends ShotResponseObject {
80 | /**
81 | * the raw handler response (e.g. when not a stream or a view) before it is serialized for transmission. If not available, the value is set to payload. Useful for inspection and reuse of the
82 | * internal objects returned (instead of parsing the response string).
83 | */
84 | result: Result | undefined;
85 | /**
86 | * the request object.
87 | */
88 | request: Request;
89 | }
90 |
--------------------------------------------------------------------------------
/lib/types/server/methods.d.ts:
--------------------------------------------------------------------------------
1 | import { CacheStatisticsObject, PolicyOptions } from "@hapi/catbox";
2 |
3 | type AnyMethod = (...args: any[]) => any;
4 |
5 | export type CachedServerMethod = T & {
6 | cache?: {
7 | drop(...args: Parameters): Promise;
8 | stats: CacheStatisticsObject
9 | }
10 | };
11 |
12 | /**
13 | * The method function with a signature async function(...args, [flags]) where:
14 | * * ...args - the method function arguments (can be any number of arguments or none).
15 | * * flags - when caching is enabled, an object used to set optional method result flags:
16 | * * * ttl - 0 if result is valid but cannot be cached. Defaults to cache policy.
17 | * For reference [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servermethodname-method-options)
18 | */
19 | export type ServerMethod = AnyMethod;
20 |
21 | /**
22 | * The same cache configuration used in server.cache().
23 | * The generateTimeout option is required.
24 | * For reference [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servermethodname-method-options)
25 | * For reference [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servercacheoptions)
26 | */
27 | export interface ServerMethodCache extends PolicyOptions {
28 | generateTimeout: number | false;
29 | cache?: string;
30 | segment?: string;
31 | }
32 |
33 | /**
34 | * Configuration object:
35 | * * bind - a context object passed back to the method function (via this) when called. Defaults to active context (set via server.bind() when the method is registered. Ignored if the method is an
36 | * arrow function.
37 | * * cache - the same cache configuration used in server.cache(). The generateTimeout option is required.
38 | * * generateKey - a function used to generate a unique key (for caching) from the arguments passed to the method function (the flags argument is not passed as input). The server will automatically
39 | * generate a unique key if the function's arguments are all of types 'string', 'number', or 'boolean'. However if the method uses other types of arguments, a key generation function must be provided
40 | * which takes the same arguments as the function and returns a unique string (or null if no key can be generated). For reference [See
41 | * docs](https://github.com/hapijs/hapi/blob/master/API.md#-servermethodname-method-options)
42 | */
43 | export interface ServerMethodOptions {
44 | /**
45 | * a context object passed back to the method function (via this) when called. Defaults to active context (set via server.bind() when the method is registered. Ignored if the method is an arrow
46 | * function.
47 | */
48 | bind?: object | undefined;
49 | /**
50 | * the same cache configuration used in server.cache(). The generateTimeout option is required.
51 | */
52 | cache?: ServerMethodCache | undefined;
53 | /**
54 | * a function used to generate a unique key (for caching) from the arguments passed to the method function (the flags argument is not passed as input). The server will automatically generate a
55 | * unique key if the function's arguments are all of types 'string', 'number', or 'boolean'. However if the method uses other types of arguments, a key generation function must be provided which
56 | * takes the same arguments as the function and returns a unique string (or null if no key can be generated).
57 | */
58 | generateKey?(...args: any[]): string | null;
59 | }
60 |
61 | /**
62 | * An object or an array of objects where each one contains:
63 | * * name - the method name.
64 | * * method - the method function.
65 | * * options - (optional) settings.
66 | * For reference [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-servermethodmethods)
67 | */
68 | export interface ServerMethodConfigurationObject {
69 | /**
70 | * the method name.
71 | */
72 | name: string;
73 | /**
74 | * the method function.
75 | */
76 | method: ServerMethod;
77 | /**
78 | * (optional) settings.
79 | */
80 | options?: ServerMethodOptions | undefined;
81 | }
82 |
83 | interface BaseServerMethods {
84 | [name: string]: (
85 | ServerMethod |
86 | CachedServerMethod |
87 | BaseServerMethods
88 | );
89 | }
90 |
91 | /**
92 | * An empty interface to allow typings of custom server.methods.
93 | */
94 | export interface ServerMethods extends BaseServerMethods {
95 | }
96 |
--------------------------------------------------------------------------------
/lib/types/server/options.d.ts:
--------------------------------------------------------------------------------
1 | import * as http from 'http';
2 | import * as https from 'https';
3 |
4 | import { MimosOptions } from '@hapi/mimos';
5 |
6 | import { PluginSpecificConfiguration } from '../plugin';
7 | import { RouteOptions } from '../route';
8 | import { CacheProvider, ServerOptionsCache } from './cache';
9 | import { SameSitePolicy } from './state';
10 |
11 | export interface ServerOptionsCompression {
12 | minBytes: number;
13 | }
14 |
15 | /**
16 | * Empty interface to allow for custom augmentation.
17 | */
18 |
19 | export interface ServerOptionsApp {
20 | }
21 |
22 | /**
23 | * The server options control the behavior of the server object. Note that the options object is deeply cloned
24 | * (with the exception of listener which is shallowly copied) and should not contain any values that are unsafe to perform deep copy on.
25 | * All options are optionals.
26 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-server-options)
27 | */
28 | export interface ServerOptions {
29 | /**
30 | * @default '0.0.0.0' (all available network interfaces).
31 | * Sets the hostname or IP address the server will listen on. If not configured, defaults to host if present, otherwise to all available network interfaces. Set to '127.0.0.1' or 'localhost' to
32 | * restrict the server to only those coming from the same host.
33 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serveroptionsaddress)
34 | */
35 | address?: string | undefined;
36 |
37 | /**
38 | * @default {}.
39 | * Provides application-specific configuration which can later be accessed via server.settings.app. The framework does not interact with this object. It is simply a reference made available
40 | * anywhere a server reference is provided. Note the difference between server.settings.app which is used to store static configuration values and server.app which is meant for storing run-time
41 | * state.
42 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serveroptionsapp)
43 | */
44 | app?: ServerOptionsApp | undefined;
45 |
46 | /**
47 | * @default true.
48 | * Used to disable the automatic initialization of the listener. When false, indicates that the listener will be started manually outside the framework.
49 | * Cannot be set to true along with a port value.
50 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serveroptionsautolisten)
51 | */
52 | autoListen?: boolean | undefined;
53 |
54 | /**
55 | * @default { engine: require('@hapi/catbox-memory' }.
56 | * Sets up server-side caching providers. Every server includes a default cache for storing application state. By default, a simple memory-based cache is created which has limited capacity and
57 | * capabilities. hapi uses catbox for its cache implementation which includes support for common storage solutions (e.g. Redis, MongoDB, Memcached, Riak, among others). Caching is only utilized
58 | * if methods and plugins explicitly store their state in the cache. The server cache configuration only defines the storage container itself. The configuration can be assigned one or more
59 | * (array):
60 | * * a class or prototype function (usually obtained by calling require() on a catbox strategy such as require('@hapi/catbox-redis')). A new catbox client will be created internally using this
61 | * function.
62 | * * a configuration object with the following:
63 | * * * engine - a class, a prototype function, or a catbox engine object.
64 | * * * name - an identifier used later when provisioning or configuring caching for server methods or plugins. Each cache name must be unique. A single item may omit the name option which defines
65 | * the default cache. If every cache includes a name, a default memory cache is provisioned as well.
66 | * * * shared - if true, allows multiple cache users to share the same segment (e.g. multiple methods using the same cache storage container). Default to false.
67 | * * * partition - (optional) string used to isolate cached data. Defaults to 'hapi-cache'.
68 | * * * other options passed to the catbox strategy used. Other options are only passed to catbox when engine above is a class or function and ignored if engine is a catbox engine object).
69 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serveroptionscache)
70 | */
71 | cache?: CacheProvider | ServerOptionsCache | ServerOptionsCache[] | undefined;
72 |
73 | /**
74 | * @default { minBytes: 1024 }.
75 | * Defines server handling of content encoding requests. If false, response content encoding is disabled and no compression is performed by the server.
76 | */
77 | compression?: boolean | ServerOptionsCompression | undefined;
78 |
79 | /**
80 | * @default { request: ['implementation'] }.
81 | * Determines which logged events are sent to the console. This should only be used for development and does not affect which events are actually logged internally and recorded. Set to false to
82 | * disable all console logging, or to an object with:
83 | * * log - a string array of server log tags to be displayed via console.error() when the events are logged via server.log() as well as internally generated server logs. Defaults to no output.
84 | * * request - a string array of request log tags to be displayed via console.error() when the events are logged via request.log() as well as internally generated request logs. For example, to
85 | * display all errors, set the option to ['error']. To turn off all console debug messages set it to false. To display all request logs, set it to '*'. Defaults to uncaught errors thrown in
86 | * external code (these errors are handled automatically and result in an Internal Server Error response) or runtime errors due to developer error. For example, to display all errors, set the log
87 | * or request to ['error']. To turn off all output set the log or request to false. To display all server logs, set the log or request to '*'. To disable all debug information, set debug to
88 | * false.
89 | */
90 | debug?: false | {
91 | log?: string | string[] | false | undefined;
92 | request?: string | string[] | false | undefined;
93 | } | undefined;
94 |
95 | /**
96 | * @default the operating system hostname and if not available, to 'localhost'.
97 | * The public hostname or IP address. Used to set server.info.host and server.info.uri and as address is none provided.
98 | */
99 | host?: string | undefined;
100 |
101 | info?: {
102 | /**
103 | * @default false.
104 | * If true, the request.info.remoteAddress and request.info.remotePort are populated when the request is received which can consume more resource (but is ok if the information is needed,
105 | * especially for aborted requests). When false, the fields are only populated upon demand (but will be undefined if accessed after the request is aborted).
106 | */
107 | remote?: boolean | undefined;
108 | } | undefined;
109 |
110 | /**
111 | * @default none.
112 | * An optional node HTTP (or HTTPS) http.Server object (or an object with a compatible interface).
113 | * If the listener needs to be manually started, set autoListen to false.
114 | * If the listener uses TLS, set tls to true.
115 | */
116 | listener?: http.Server | undefined;
117 |
118 | /**
119 | * @default { sampleInterval: 0 }.
120 | * Server excessive load handling limits where:
121 | * * sampleInterval - the frequency of sampling in milliseconds. When set to 0, the other load options are ignored. Defaults to 0 (no sampling).
122 | * * maxHeapUsedBytes - maximum V8 heap size over which incoming requests are rejected with an HTTP Server Timeout (503) response. Defaults to 0 (no limit).
123 | * * maxRssBytes - maximum process RSS size over which incoming requests are rejected with an HTTP Server Timeout (503) response. Defaults to 0 (no limit).
124 | * * maxEventLoopDelay - maximum event loop delay duration in milliseconds over which incoming requests are rejected with an HTTP Server Timeout (503) response. Defaults to 0 (no limit).
125 | */
126 | load?: {
127 | /** the frequency of sampling in milliseconds. When set to 0, the other load options are ignored. Defaults to 0 (no sampling). */
128 | sampleInterval?: number | undefined;
129 |
130 | /** maximum V8 heap size over which incoming requests are rejected with an HTTP Server Timeout (503) response. Defaults to 0 (no limit). */
131 | maxHeapUsedBytes?: number | undefined;
132 | /**
133 | * maximum process RSS size over which incoming requests are rejected with an HTTP Server Timeout (503) response. Defaults to 0 (no limit).
134 | */
135 | maxRssBytes?: number | undefined;
136 | /**
137 | * maximum event loop delay duration in milliseconds over which incoming requests are rejected with an HTTP Server Timeout (503) response.
138 | * Defaults to 0 (no limit).
139 | */
140 | maxEventLoopDelay?: number | undefined;
141 | } | undefined;
142 |
143 | /**
144 | * @default none.
145 | * Options passed to the mimos module when generating the mime database used by the server (and accessed via server.mime):
146 | * * override - an object hash that is merged into the built in mime information specified here. Each key value pair represents a single mime object. Each override value must contain:
147 | * * key - the lower-cased mime-type string (e.g. 'application/javascript').
148 | * * value - an object following the specifications outlined here. Additional values include:
149 | * * * type - specify the type value of result objects, defaults to key.
150 | * * * predicate - method with signature function(mime) when this mime type is found in the database, this function will execute to allows customizations.
151 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serveroptionsmime)
152 | */
153 | mime?: MimosOptions | undefined;
154 |
155 | /**
156 | * @default { cleanStop: true }
157 | * Defines server handling of server operations.
158 | */
159 | operations?: {
160 | /**
161 | * @default true
162 | * If true, the server keeps track of open connections and properly closes them when the server is stopped.
163 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serveroptionsoperations)
164 | */
165 | cleanStop?: boolean;
166 | }
167 |
168 | /**
169 | * @default {}.
170 | * Plugin-specific configuration which can later be accessed via server.settings.plugins. plugins is an object where each key is a plugin name and the value is the configuration. Note the
171 | * difference between server.settings.plugins which is used to store static configuration values and server.plugins which is meant for storing run-time state.
172 | */
173 | plugins?: PluginSpecificConfiguration | undefined;
174 |
175 | /**
176 | * @default 0 (an ephemeral port).
177 | * The TCP port the server will listen to. Defaults the next available port when the server is started (and assigned to server.info.port).
178 | * If port is a string containing a '/' character, it is used as a UNIX domain socket path. If it starts with '\.\pipe', it is used as a Windows named pipe.
179 | */
180 | port?: number | string | undefined;
181 |
182 | /**
183 | * @default { isCaseSensitive: true, stripTrailingSlash: false }.
184 | * Controls how incoming request URIs are matched against the routing table:
185 | * * isCaseSensitive - determines whether the paths '/example' and '/EXAMPLE' are considered different resources. Defaults to true.
186 | * * stripTrailingSlash - removes trailing slashes on incoming paths. Defaults to false.
187 | */
188 | router?: {
189 | isCaseSensitive?: boolean | undefined;
190 | stripTrailingSlash?: boolean | undefined;
191 | } | undefined;
192 |
193 | /**
194 | * @default none.
195 | * A route options object used as the default configuration for every route.
196 | */
197 | routes?: RouteOptions | undefined;
198 |
199 | /**
200 | * Default value:
201 | * {
202 | * strictHeader: true,
203 | * ignoreErrors: false,
204 | * isSecure: true,
205 | * isHttpOnly: true,
206 | * isSameSite: 'Strict',
207 | * encoding: 'none'
208 | * }
209 | * Sets the default configuration for every state (cookie) set explicitly via server.state() or implicitly (without definition) using the state configuration object.
210 | */
211 | // TODO I am not sure if I need to use all the server.state() definition (like the default value) OR only the options below. The v16 use "any" here.
212 | // state?: ServerStateCookieOptions;
213 | state?: {
214 | strictHeader?: boolean | undefined;
215 | ignoreErrors?: boolean | undefined;
216 | isSecure?: boolean | undefined;
217 | isHttpOnly?: boolean | undefined;
218 | isSameSite?: SameSitePolicy | undefined;
219 | encoding?: 'none' | 'base64' | 'base64json' | 'form' | 'iron' | undefined;
220 | } | undefined;
221 |
222 | /**
223 | * @default none.
224 | * Used to create an HTTPS connection. The tls object is passed unchanged to the node HTTPS server as described in the node HTTPS documentation.
225 | */
226 | tls?: boolean | https.ServerOptions | undefined;
227 |
228 | /**
229 | * @default constructed from runtime server information.
230 | * The full public URI without the path (e.g. 'http://example.com:8080'). If present, used as the server server.info.uri, otherwise constructed from the server settings.
231 | */
232 | uri?: string | undefined;
233 |
234 | /**
235 | * Query parameter configuration.
236 | */
237 | query?: {
238 | /**
239 | * the method must return an object where each key is a parameter and matching value is the parameter value.
240 | * If the method throws, the error is used as the response or returned when `request.setUrl` is called.
241 | */
242 | parser(raw: Record): Record;
243 | } | undefined;
244 | }
245 |
--------------------------------------------------------------------------------
/lib/types/server/state.d.ts:
--------------------------------------------------------------------------------
1 | import { StateOptions, SameSitePolicy } from '@hapi/statehood';
2 |
3 | import { Request } from '../request';
4 |
5 | export { SameSitePolicy };
6 |
7 | /**
8 | * Optional cookie settings
9 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverstatename-options)
10 | */
11 | export interface ServerStateCookieOptions extends StateOptions {}
12 |
13 | /**
14 | * A single object or an array of object where each contains:
15 | * * name - the cookie name.
16 | * * value - the cookie value.
17 | * * options - cookie configuration to override the server settings.
18 | */
19 | export interface ServerStateFormat {
20 | name: string;
21 | value: string;
22 | options: ServerStateCookieOptions;
23 | }
24 |
25 | /**
26 | * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverstatename-options)
27 | * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serveroptionsstate)
28 | */
29 | export interface ServerState {
30 | /**
31 | * The server cookies manager.
32 | * Access: read only and statehood public interface.
33 | */
34 | readonly states: object;
35 |
36 | /**
37 | * The server cookies manager settings. The settings are based on the values configured in [server.options.state](https://github.com/hapijs/hapi/blob/master/API.md#server.options.state).
38 | */
39 | readonly settings: ServerStateCookieOptions;
40 |
41 | /**
42 | * An object containing the configuration of each cookie added via [server.state()](https://github.com/hapijs/hapi/blob/master/API.md#server.state()) where each key is the
43 | * cookie name and value is the configuration object.
44 | */
45 | readonly cookies: {
46 | [key: string]: ServerStateCookieOptions;
47 | };
48 |
49 | /**
50 | * An array containing the names of all configured cookies.
51 | */
52 | readonly names: string[];
53 |
54 | /**
55 | * Same as calling [server.state()](https://github.com/hapijs/hapi/blob/master/API.md#server.state()).
56 | */
57 | add(name: string, options?: ServerStateCookieOptions | undefined): void;
58 |
59 | /**
60 | * Formats an HTTP 'Set-Cookie' header based on the server.options.state where:
61 | * @param cookies - a single object or an array of object where each contains:
62 | * * name - the cookie name.
63 | * * value - the cookie value.
64 | * * options - cookie configuration to override the server settings.
65 | * @return Return value: a header string.
66 | * Note that this utility uses the server configuration but does not change the server state. It is provided for manual cookie formatting (e.g. when headers are set manually).
67 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-async-serverstatesformatcookies)
68 | */
69 | format(cookies: ServerStateFormat | ServerStateFormat[]): Promise;
70 |
71 | /**
72 | * Parses an HTTP 'Cookies' header based on the server.options.state where:
73 | * @param header - the HTTP header.
74 | * @return Return value: an object where each key is a cookie name and value is the parsed cookie.
75 | * Note that this utility uses the server configuration but does not change the server state. It is provided for manual cookie parsing (e.g. when server parsing is disabled).
76 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-async-serverstatesparseheader)
77 | */
78 | parse(header: string): Promise>;
79 | }
80 |
--------------------------------------------------------------------------------
/lib/types/utils.d.ts:
--------------------------------------------------------------------------------
1 | import * as https from 'https';
2 | import * as stream from 'stream';
3 |
4 | import { Boom } from '@hapi/boom';
5 | import { ResponseObject as ShotResponseObject } from '@hapi/shot';
6 |
7 | import {
8 | ReqRef,
9 | ReqRefDefaults,
10 | MergeRefs,
11 | Request} from './request';
12 | import { ResponseToolkit, Auth } from './response';
13 |
14 | /**
15 | * All http parser [supported HTTP methods](https://nodejs.org/api/http.html#httpmethods).
16 | */
17 | export type HTTP_METHODS = 'ACL' | 'BIND' | 'CHECKOUT' | 'CONNECT' | 'COPY' | 'DELETE' | 'GET' | 'HEAD' | 'LINK' | 'LOCK' |
18 | 'M-SEARCH' | 'MERGE' | 'MKACTIVITY' | 'MKCALENDAR' | 'MKCOL' | 'MOVE' | 'NOTIFY' | 'OPTIONS' | 'PATCH' | 'POST' |
19 | 'PROPFIND' | 'PROPPATCH' | 'PURGE' | 'PUT' | 'REBIND' | 'REPORT' | 'SEARCH' | 'SOURCE' | 'SUBSCRIBE' | 'TRACE' |
20 | 'UNBIND' | 'UNLINK' | 'UNLOCK' | 'UNSUBSCRIBE';
21 |
22 | export type PeekListener = (chunk: string, encoding: string) => void;
23 |
24 | export namespace Json {
25 | /**
26 | * @see {@link https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter}
27 | */
28 | type StringifyReplacer = ((key: string, value: any) => any) | (string | number)[] | undefined;
29 |
30 | /**
31 | * Any value greater than 10 is truncated.
32 | */
33 | type StringifySpace = number | string;
34 |
35 | /**
36 | * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsjson)
37 | */
38 | interface StringifyArguments {
39 | /** the replacer function or array. Defaults to no action. */
40 | replacer?: StringifyReplacer | undefined;
41 | /** number of spaces to indent nested object keys. Defaults to no indentation. */
42 | space?: StringifySpace | undefined;
43 | /* string suffix added after conversion to JSON string. Defaults to no suffix. */
44 | suffix?: string | undefined;
45 | /* calls Hoek.jsonEscape() after conversion to JSON string. Defaults to false. */
46 | escape?: boolean | undefined;
47 | }
48 | }
49 |
50 | export namespace Lifecycle {
51 | /**
52 | * Lifecycle methods are the interface between the framework and the application. Many of the request lifecycle steps:
53 | * extensions, authentication, handlers, pre-handler methods, and failAction function values are lifecycle methods
54 | * provided by the developer and executed by the framework.
55 | * Each lifecycle method is a function with the signature await function(request, h, [err]) where:
56 | * * request - the request object.
57 | * * h - the response toolkit the handler must call to set a response and return control back to the framework.
58 | * * err - an error object available only when the method is used as a failAction value.
59 | */
60 | type Method<
61 | Refs extends ReqRef = ReqRefDefaults,
62 | R extends ReturnValue = ReturnValue
63 | > = (
64 | this: MergeRefs['Bind'],
65 | request: Request,
66 | h: ResponseToolkit,
67 | err?: Error | undefined
68 | ) => R;
69 |
70 | /**
71 | * Each lifecycle method must return a value or a promise that resolves into a value. If a lifecycle method returns
72 | * without a value or resolves to an undefined value, an Internal Server Error (500) error response is sent.
73 | * The return value must be one of:
74 | * - Plain value: null, string, number, boolean
75 | * - Buffer object
76 | * - Error object: plain Error OR a Boom object.
77 | * - Stream object
78 | * - any object or array
79 | * - a toolkit signal:
80 | * - a toolkit method response:
81 | * - a promise object that resolve to any of the above values
82 | * For more info please [See docs](https://github.com/hapijs/hapi/blob/master/API.md#lifecycle-methods)
83 | */
84 | type ReturnValue = ReturnValueTypes | (Promise>);
85 | type ReturnValueTypes =
86 | (null | string | number | boolean) |
87 | (Buffer) |
88 | (Error | Boom) |
89 | (stream.Stream) |
90 | (object | object[]) |
91 | symbol |
92 | Auth<
93 | MergeRefs['AuthUser'],
94 | MergeRefs['AuthApp'],
95 | MergeRefs['AuthCredentialsExtra'],
96 | MergeRefs['AuthArtifactsExtra']
97 | > |
98 | ShotResponseObject;
99 |
100 | /**
101 | * Various configuration options allows defining how errors are handled. For example, when invalid payload is received or malformed cookie, instead of returning an error, the framework can be
102 | * configured to perform another action. When supported the failAction option supports the following values:
103 | * * 'error' - return the error object as the response.
104 | * * 'log' - report the error but continue processing the request.
105 | * * 'ignore' - take no action and continue processing the request.
106 | * * a lifecycle method with the signature async function(request, h, err) where:
107 | * * * request - the request object.
108 | * * * h - the response toolkit.
109 | * * * err - the error object.
110 | * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-failaction-configuration)
111 | */
112 | type FailAction = 'error' | 'log' | 'ignore' | Method;
113 | }
114 |
--------------------------------------------------------------------------------
/lib/validation.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Boom = require('@hapi/boom');
4 | const Hoek = require('@hapi/hoek');
5 | const Validate = require('@hapi/validate');
6 |
7 |
8 | const internals = {};
9 |
10 |
11 | exports.validator = function (validator) {
12 |
13 | Hoek.assert(validator, 'Missing validator');
14 | Hoek.assert(typeof validator.compile === 'function', 'Invalid validator compile method');
15 |
16 | return validator;
17 | };
18 |
19 |
20 | exports.compile = function (rule, validator, realm, core) {
21 |
22 | validator = validator ?? internals.validator(realm, core);
23 |
24 | // false - nothing allowed
25 |
26 | if (rule === false) {
27 | return Validate.object({}).allow(null);
28 | }
29 |
30 | // Custom function
31 |
32 | if (typeof rule === 'function') {
33 | return rule;
34 | }
35 |
36 | // null, undefined, true - anything allowed
37 |
38 | if (!rule || // false tested above
39 | rule === true) {
40 |
41 | return null;
42 | }
43 |
44 | // {...} - ... allowed
45 |
46 | if (typeof rule.validate === 'function') {
47 | return rule;
48 | }
49 |
50 | Hoek.assert(validator, 'Cannot set uncompiled validation rules without configuring a validator');
51 | return validator.compile(rule);
52 | };
53 |
54 |
55 | internals.validator = function (realm, core) {
56 |
57 | while (realm) {
58 | if (realm.validator) {
59 | return realm.validator;
60 | }
61 |
62 | realm = realm.parent;
63 | }
64 |
65 | return core.validator;
66 | };
67 |
68 |
69 | exports.headers = function (request) {
70 |
71 | return internals.input('headers', request);
72 | };
73 |
74 |
75 | exports.params = function (request) {
76 |
77 | return internals.input('params', request);
78 | };
79 |
80 |
81 | exports.payload = function (request) {
82 |
83 | if (request.method === 'get' ||
84 | request.method === 'head') { // When route.method is '*'
85 |
86 | return;
87 | }
88 |
89 | return internals.input('payload', request);
90 | };
91 |
92 |
93 | exports.query = function (request) {
94 |
95 | return internals.input('query', request);
96 | };
97 |
98 |
99 | exports.state = function (request) {
100 |
101 | return internals.input('state', request);
102 | };
103 |
104 |
105 | internals.input = async function (source, request) {
106 |
107 | const localOptions = {
108 | context: {
109 | headers: request.headers,
110 | params: request.params,
111 | query: request.query,
112 | payload: request.payload,
113 | state: request.state,
114 | auth: request.auth,
115 | app: {
116 | route: request.route.settings.app,
117 | request: request.app
118 | }
119 | }
120 | };
121 |
122 | delete localOptions.context[source];
123 | Hoek.merge(localOptions, request.route.settings.validate.options);
124 |
125 | try {
126 | const schema = request.route.settings.validate[source];
127 | const bind = request.route.settings.bind;
128 |
129 | var value = await (typeof schema !== 'function' ? internals.validate(request[source], schema, localOptions) : schema.call(bind, request[source], localOptions));
130 | return;
131 | }
132 | catch (err) {
133 | var validationError = err;
134 | }
135 | finally {
136 | request.orig[source] = request[source];
137 | if (value !== undefined) {
138 | request[source] = value;
139 | }
140 | }
141 |
142 | if (request.route.settings.validate.failAction === 'ignore') {
143 | return;
144 | }
145 |
146 | // Prepare error
147 |
148 | const defaultError = validationError.isBoom ? validationError : Boom.badRequest(`Invalid request ${source} input`);
149 | const detailedError = Boom.boomify(validationError, { statusCode: 400, override: false, data: { defaultError } });
150 | detailedError.output.payload.validation = { source, keys: [] };
151 | if (validationError.details) {
152 | for (const details of validationError.details) {
153 | const path = details.path;
154 | detailedError.output.payload.validation.keys.push(Hoek.escapeHtml(path.join('.')));
155 | }
156 | }
157 |
158 | if (request.route.settings.validate.errorFields) {
159 | for (const field in request.route.settings.validate.errorFields) {
160 | detailedError.output.payload[field] = request.route.settings.validate.errorFields[field];
161 | }
162 | }
163 |
164 | return request._core.toolkit.failAction(request, request.route.settings.validate.failAction, defaultError, { details: detailedError, tags: ['validation', 'error', source] });
165 | };
166 |
167 |
168 | exports.response = async function (request) {
169 |
170 | if (request.route.settings.response.sample) {
171 | const currentSample = Math.ceil(Math.random() * 100);
172 | if (currentSample > request.route.settings.response.sample) {
173 | return;
174 | }
175 | }
176 |
177 | const response = request.response;
178 | const statusCode = response.isBoom ? response.output.statusCode : response.statusCode;
179 |
180 | const statusSchema = request.route.settings.response.status[statusCode];
181 | if (statusCode >= 400 &&
182 | !statusSchema) {
183 |
184 | return; // Do not validate errors by default
185 | }
186 |
187 | const schema = statusSchema !== undefined ? statusSchema : request.route.settings.response.schema;
188 | if (schema === null) {
189 | return; // No rules
190 | }
191 |
192 | if (!response.isBoom &&
193 | request.response.variety !== 'plain') {
194 |
195 | throw Boom.badImplementation('Cannot validate non-object response');
196 | }
197 |
198 | const localOptions = {
199 | context: {
200 | headers: request.headers,
201 | params: request.params,
202 | query: request.query,
203 | payload: request.payload,
204 | state: request.state,
205 | auth: request.auth,
206 | app: {
207 | route: request.route.settings.app,
208 | request: request.app
209 | }
210 | }
211 | };
212 |
213 | const source = response.isBoom ? response.output.payload : response.source;
214 | Hoek.merge(localOptions, request.route.settings.response.options);
215 |
216 | try {
217 | let value;
218 |
219 | if (typeof schema !== 'function') {
220 | value = await internals.validate(source, schema, localOptions);
221 | }
222 | else {
223 | value = await schema(source, localOptions);
224 | }
225 |
226 | if (value !== undefined &&
227 | request.route.settings.response.modify) {
228 |
229 | if (response.isBoom) {
230 | response.output.payload = value;
231 | }
232 | else {
233 | response.source = value;
234 | }
235 | }
236 | }
237 | catch (err) {
238 | return request._core.toolkit.failAction(request, request.route.settings.response.failAction, err, { tags: ['validation', 'response', 'error'] });
239 | }
240 | };
241 |
242 |
243 | internals.validate = function (value, schema, options) {
244 |
245 | if (typeof schema.validateAsync === 'function') {
246 | return schema.validateAsync(value, options);
247 | }
248 |
249 | return schema.validate(value, options);
250 | };
251 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hapi/hapi",
3 | "description": "HTTP Server framework",
4 | "homepage": "https://hapi.dev",
5 | "version": "21.4.0",
6 | "repository": "git://github.com/hapijs/hapi",
7 | "main": "lib/index.js",
8 | "types": "lib/index.d.ts",
9 | "engines": {
10 | "node": ">=14.15.0"
11 | },
12 | "files": [
13 | "lib"
14 | ],
15 | "keywords": [
16 | "framework",
17 | "http",
18 | "api",
19 | "web"
20 | ],
21 | "eslintConfig": {
22 | "extends": [
23 | "plugin:@hapi/module"
24 | ]
25 | },
26 | "dependencies": {
27 | "@hapi/accept": "^6.0.3",
28 | "@hapi/ammo": "^6.0.1",
29 | "@hapi/boom": "^10.0.1",
30 | "@hapi/bounce": "^3.0.2",
31 | "@hapi/call": "^9.0.1",
32 | "@hapi/catbox": "^12.1.1",
33 | "@hapi/catbox-memory": "^6.0.2",
34 | "@hapi/heavy": "^8.0.1",
35 | "@hapi/hoek": "^11.0.6",
36 | "@hapi/mimos": "^7.0.1",
37 | "@hapi/podium": "^5.0.1",
38 | "@hapi/shot": "^6.0.1",
39 | "@hapi/somever": "^4.1.1",
40 | "@hapi/statehood": "^8.2.0",
41 | "@hapi/subtext": "^8.1.0",
42 | "@hapi/teamwork": "^6.0.0",
43 | "@hapi/topo": "^6.0.2",
44 | "@hapi/validate": "^2.0.1"
45 | },
46 | "devDependencies": {
47 | "@hapi/code": "^9.0.3",
48 | "@hapi/eslint-plugin": "^6.0.0",
49 | "@hapi/inert": "^7.1.0",
50 | "@hapi/joi-legacy-test": "npm:@hapi/joi@^15.0.0",
51 | "@hapi/lab": "^25.3.2",
52 | "@hapi/vision": "^7.0.3",
53 | "@hapi/wreck": "^18.1.0",
54 | "@types/node": "^18.19.59",
55 | "handlebars": "^4.7.8",
56 | "joi": "^17.13.3",
57 | "legacy-readable-stream": "npm:readable-stream@^1.0.34",
58 | "typescript": "^4.9.4"
59 | },
60 | "scripts": {
61 | "test": "lab -a @hapi/code -t 100 -L -m 5000 -Y",
62 | "test-tap": "lab -a @hapi/code -r tap -o tests.tap -m 5000",
63 | "test-cov-html": "lab -a @hapi/code -r html -o coverage.html -m 5000"
64 | },
65 | "license": "BSD-3-Clause"
66 | }
67 |
--------------------------------------------------------------------------------
/test/.hidden:
--------------------------------------------------------------------------------
1 | Ssssh!
2 |
--------------------------------------------------------------------------------
/test/common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ChildProcess = require('child_process');
4 | const Http = require('http');
5 | const Net = require('net');
6 |
7 | const internals = {};
8 |
9 | internals.hasLsof = () => {
10 |
11 | try {
12 | ChildProcess.execSync(`lsof -p ${process.pid}`, { stdio: 'ignore' });
13 | }
14 | catch (err) {
15 | return false;
16 | }
17 |
18 | return true;
19 | };
20 |
21 | internals.hasIPv6 = () => {
22 |
23 | const server = Http.createServer().listen();
24 | const { address } = server.address();
25 | server.close();
26 |
27 | return Net.isIPv6(address);
28 | };
29 |
30 | exports.hasLsof = internals.hasLsof();
31 |
32 | exports.hasIPv6 = internals.hasIPv6();
33 |
--------------------------------------------------------------------------------
/test/file/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hapijs/hapi/76e6cfeb4d6a4f7d5b945f9c50ac8e12b84e32a5/test/file/image.jpg
--------------------------------------------------------------------------------
/test/file/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hapijs/hapi/76e6cfeb4d6a4f7d5b945f9c50ac8e12b84e32a5/test/file/image.png
--------------------------------------------------------------------------------
/test/file/image.png.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hapijs/hapi/76e6cfeb4d6a4f7d5b945f9c50ac8e12b84e32a5/test/file/image.png.gz
--------------------------------------------------------------------------------
/test/file/note.txt:
--------------------------------------------------------------------------------
1 | Test
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Hapi = require('..');
5 | const Lab = require('@hapi/lab');
6 |
7 |
8 | const internals = {};
9 |
10 |
11 | const { describe, it } = exports.lab = Lab.script();
12 | const expect = Code.expect;
13 |
14 |
15 | describe('Server', () => {
16 |
17 | it('supports new Server()', async () => {
18 |
19 | const server = new Hapi.Server();
20 | server.route({ method: 'GET', path: '/', handler: () => 'old school' });
21 |
22 | const res = await server.inject('/');
23 | expect(res.result).to.equal('old school');
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/security.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Hapi = require('..');
5 | const Lab = require('@hapi/lab');
6 |
7 |
8 | const internals = {};
9 |
10 |
11 | const { describe, it } = exports.lab = Lab.script();
12 | const expect = Code.expect;
13 |
14 |
15 | describe('security', () => {
16 |
17 | it('handles missing routes', async () => {
18 |
19 | const server = Hapi.server({ port: 8080, routes: { security: { xframe: true } } });
20 |
21 | const res = await server.inject('/');
22 | expect(res.statusCode).to.equal(404);
23 | expect(res.headers['x-frame-options']).to.exist();
24 | });
25 |
26 | it('blocks response splitting through the request.create method', async () => {
27 |
28 | const server = Hapi.server();
29 | const handler = (request, h) => h.response('Moved').created('/item/' + request.payload.name);
30 | server.route({ method: 'POST', path: '/item', handler });
31 |
32 | const res = await server.inject({
33 | method: 'POST', url: '/item',
34 | payload: '{"name": "foobar\r\nContent-Length: \r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 19\r\n\r\nShazam"}',
35 | headers: { 'Content-Type': 'application/json' }
36 | });
37 |
38 | expect(res.statusCode).to.equal(400);
39 | });
40 |
41 | it('prevents xss with invalid content types', async () => {
42 |
43 | const server = Hapi.server();
44 | server.state('encoded', { encoding: 'iron' });
45 | server.route({
46 | method: 'POST', path: '/',
47 | handler: () => 'Success'
48 | });
49 |
50 | const res = await server.inject({
51 | method: 'POST',
52 | url: '/',
53 | payload: '{"something":"something"}',
54 | headers: { 'content-type': ';' }
55 | });
56 |
57 | expect(res.result.message).to.not.contain('script');
58 | });
59 |
60 | it('prevents xss with invalid cookie values in the request', async () => {
61 |
62 | const server = Hapi.server();
63 | server.state('encoded', { encoding: 'iron' });
64 | server.route({
65 | method: 'POST', path: '/',
66 | handler: () => 'Success'
67 | });
68 |
69 | const res = await server.inject({
70 | method: 'POST',
71 | url: '/',
72 | payload: '{"something":"something"}',
73 | headers: { cookie: 'encoded="";' }
74 | });
75 |
76 | expect(res.result.message).to.not.contain('=value;' }
93 | });
94 |
95 | expect(res.result.message).to.not.contain('