From c44e73e0cb44eda97f9b4f0ec1d02014ad1d9ec6 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Mon, 18 Apr 2022 17:13:25 +0200 Subject: [PATCH 01/13] Remove JSONP support. Closes #4270 (#4271) --- API.md | 26 ---------- lib/config.js | 1 - lib/headers.js | 12 +---- lib/request.js | 3 +- lib/response.js | 39 +------------- lib/route.js | 34 ------------ test/headers.js | 12 ----- test/response.js | 12 ----- test/transmit.js | 132 ----------------------------------------------- 9 files changed, 4 insertions(+), 267 deletions(-) diff --git a/API.md b/API.md index 66279dea1..fc9b3860b 100755 --- a/API.md +++ b/API.md @@ -2973,21 +2973,6 @@ string payload or escaping it after stringification. Supports the following: - `escape` - calls [`Hoek.jsonEscape()`](https://hapi.dev/family/hoek/api/#escapejsonstring) after conversion to JSON string. Defaults to `false`. -### `route.options.jsonp` - -Default value: none. - -Enables JSONP support by setting the value to the query parameter name containing the function name -used to wrap the response payload. - -For example, if the value is `'callback'`, a request comes in with `'callback=me'`, and the JSON -response is `'{ "a":"b" }'`, the payload will be `'me({ "a":"b" });'`. Cannot be used with stream -responses. - -The 'Content-Type' response header is set to `'text/javascript'` and the 'X-Content-Type-Options' -response header is set to `'nosniff'`, and will override those headers even if explicitly set by -[`response.type()`](#response.type()). - ### `route.options.log` Default value: `{ collect: false }`. @@ -3615,19 +3600,12 @@ the same. The following is the complete list of steps a request can go through: - [`request.route`](#request.route) is unassigned. - [`request.url`](#request.url) can be `null` if the incoming request path is invalid. - [`request.path`](#request.path) can be an invalid path. - - JSONP configuration is ignored for any response returned from the extension point since no - route is matched yet and the JSONP configuration is unavailable. - _**Route lookup**_ - lookup based on `request.path` and `request.method`. - skips to _**onPreResponse**_ if no route is found or if the path violates the HTTP specification. -- _**JSONP processing**_ - - based on the route [`jsonp`](#route.options.jsonp) option. - - parses JSONP parameter from [`request.query`](#request.query). - - skips to _**Response validation**_ on error. - - _**Cookies processing**_ - based on the route [`state`](#route.options.state) option. - error handling based on [`failAction`](#route.options.state.failAction). @@ -3662,10 +3640,6 @@ the same. The following is the complete list of steps a request can go through: - based on the route [`validate.params`](#route.options.validate.params) option. - error handling based on [`failAction`](#route.options.validate.failAction). -- _**JSONP cleanup**_ - - based on the route [`jsonp`](#route.options.jsonp) option. - - remove the JSONP parameter from [`request.query`](#request.query). - - _**Query validation**_ - based on the route [`validate.query`](#route.options.validate.query) option. - error handling based on [`failAction`](#route.options.validate.failAction). diff --git a/lib/config.js b/lib/config.js index 5279a187d..94ca0586f 100755 --- a/lib/config.js +++ b/lib/config.js @@ -134,7 +134,6 @@ internals.routeBase = Validate.object({ escape: Validate.boolean().default(false) }) .default(), - jsonp: Validate.string(), log: Validate.object({ collect: Validate.boolean().default(false) }) diff --git a/lib/headers.js b/lib/headers.js index 8e3a86654..bdb2a3833 100755 --- a/lib/headers.js +++ b/lib/headers.js @@ -40,17 +40,7 @@ exports.content = async function (response) { await response._marshal(); - if (request.jsonp && - response._payload.jsonp) { - - response._header('content-type', 'text/javascript' + (response.settings.charset ? '; charset=' + response.settings.charset : '')); - response._header('x-content-type-options', 'nosniff'); - response._payload.jsonp(request.jsonp); - } - - if (response._payload.size && - typeof response._payload.size === 'function') { - + if (typeof response._payload.size === 'function') { response._header('content-length', response._payload.size(), { override: false }); } diff --git a/lib/request.js b/lib/request.js index 7ae525fc5..f0e6f42fe 100755 --- a/lib/request.js +++ b/lib/request.js @@ -15,7 +15,7 @@ const Transmit = require('./transmit'); const internals = { events: Podium.validate(['finish', { name: 'peek', spread: true }, 'disconnect']), - reserved: ['server', 'url', 'query', 'path', 'method', 'mime', 'setUrl', 'setMethod', 'headers', 'id', 'app', 'plugins', 'route', 'auth', 'pre', 'preResponses', 'info', 'isInjected', 'orig', 'params', 'paramsArray', 'payload', 'state', 'jsonp', 'response', 'raw', 'domain', 'log', 'logs', 'generateResponse'] + reserved: ['server', 'url', 'query', 'path', 'method', 'mime', 'setUrl', 'setMethod', 'headers', 'id', 'app', 'plugins', 'route', 'auth', 'pre', 'preResponses', 'info', 'isInjected', 'orig', 'params', 'paramsArray', 'payload', 'state', 'response', 'raw', 'domain', 'log', 'logs', 'generateResponse'] }; @@ -41,7 +41,6 @@ exports = module.exports = internals.Request = class { this.app = options.app ? Object.assign({}, options.app) : {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins (shallow cloned) this.headers = req.headers; - this.jsonp = null; this.logs = []; this.method = req.method.toLowerCase(); this.mime = null; diff --git a/lib/response.js b/lib/response.js index 6bb8044c1..8840c661b 100755 --- a/lib/response.js +++ b/lib/response.js @@ -711,68 +711,33 @@ internals.Response.Payload = class extends Stream.Readable { super(); this._data = payload; - this._prefix = null; - this._suffix = null; - this._sizeOffset = 0; this._encoding = options.encoding; } _read(size) { - if (this._prefix) { - this.push(this._prefix, this._encoding); - } - if (this._data) { this.push(this._data, this._encoding); } - if (this._suffix) { - this.push(this._suffix, this._encoding); - } - this.push(null); } size() { if (!this._data) { - return this._sizeOffset; + return 0; } - return (Buffer.isBuffer(this._data) ? this._data.length : Buffer.byteLength(this._data, this._encoding)) + this._sizeOffset; - } - - jsonp(variable) { - - this._sizeOffset = this._sizeOffset + variable.length + 7; - this._prefix = '/**/' + variable + '('; // '/**/' prefix prevents CVE-2014-4671 security exploit - - if (this._data !== null && - !Buffer.isBuffer(this._data)) { - - this._data = this._data - .replace(/\u2028/g, '\\u2028') - .replace(/\u2029/g, '\\u2029'); - } - - this._suffix = ');'; + return Buffer.isBuffer(this._data) ? this._data.length : Buffer.byteLength(this._data, this._encoding); } writeToStream(stream) { - if (this._prefix) { - stream.write(this._prefix, this._encoding); - } - if (this._data) { stream.write(this._data, this._encoding); } - if (this._suffix) { - stream.write(this._suffix, this._encoding); - } - stream.end(); } }; diff --git a/lib/route.js b/lib/route.js index 0e6a70dcd..6feca6011 100755 --- a/lib/route.js +++ b/lib/route.js @@ -2,7 +2,6 @@ const Assert = require('assert'); -const Boom = require('@hapi/boom'); const Bounce = require('@hapi/bounce'); const Catbox = require('@hapi/catbox'); const Hoek = require('@hapi/hoek'); @@ -127,7 +126,6 @@ exports = module.exports = internals.Route = class { this._assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled'); this._assert(!this.settings.validate.state || this.settings.state.parse, 'Route state must be set to \'parse\' when state validation enabled'); - this._assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name'); // Authentication configuration @@ -232,10 +230,6 @@ exports = module.exports = internals.Route = class { // 'onRequest' - if (this.settings.jsonp) { - this._cycle.push(internals.parseJSONP); - } - if (this.settings.state.parse) { this._cycle.push(internals.state); } @@ -278,10 +272,6 @@ exports = module.exports = internals.Route = class { this._cycle.push(Validation.params); } - if (this.settings.jsonp) { - this._cycle.push(internals.cleanupJSONP); - } - if (this.settings.validate.query) { this._cycle.push(Validation.query); } @@ -457,30 +447,6 @@ internals.drain = async function (request) { }; -internals.jsonpRegex = /^[\w\$\[\]\.]+$/; - - -internals.parseJSONP = function (request) { - - const jsonp = request.query[request.route.settings.jsonp]; - if (jsonp) { - if (internals.jsonpRegex.test(jsonp) === false) { - throw Boom.badRequest('Invalid JSONP parameter value'); - } - - request.jsonp = jsonp; - } -}; - - -internals.cleanupJSONP = function (request) { - - if (request.jsonp) { - delete request.query[request.route.settings.jsonp]; - } -}; - - internals.config = function (chain) { if (!chain.length) { diff --git a/test/headers.js b/test/headers.js index 70edba394..c6a447649 100755 --- a/test/headers.js +++ b/test/headers.js @@ -6,7 +6,6 @@ const Code = require('@hapi/code'); const Hapi = require('..'); const Inert = require('@hapi/inert'); const Lab = require('@hapi/lab'); -const Wreck = require('@hapi/wreck'); const internals = {}; @@ -513,17 +512,6 @@ describe('Headers', () => { expect(res.headers['content-type']).to.equal('text/html'); }); - it('returns a normal response when JSONP requested but stream returned', async () => { - - const server = Hapi.server(); - const stream = Wreck.toReadableStream('test'); - stream.size = 4; // Non function for coverage - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => stream } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('test'); - }); - it('does not set content-type by default on 204 response', async () => { const server = Hapi.server(); diff --git a/test/response.js b/test/response.js index f0757fba6..28a3fb303 100755 --- a/test/response.js +++ b/test/response.js @@ -1553,16 +1553,4 @@ describe('Response', () => { await finish; }); }); - - describe('Payload', () => { - - it('streams empty string', async () => { - - const server = Hapi.server({ compression: { minBytes: 1 } }); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => '' } }); - - const res = await server.inject({ url: '/?callback=me', headers: { 'Accept-Encoding': 'gzip' } }); - expect(res.statusCode).to.equal(200); - }); - }); }); diff --git a/test/transmit.js b/test/transmit.js index 4109cc013..8449bca89 100755 --- a/test/transmit.js +++ b/test/transmit.js @@ -362,138 +362,6 @@ describe('transmission', () => { expect(res.statusCode).to.equal(201); }); - it('returns an JSONP response', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me({"some":"value"});'); - expect(res.headers['content-length']).to.equal(25); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - }); - - it('returns an JSONP response with no payload', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => null } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me();'); - expect(res.headers['content-length']).to.equal(9); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - }); - - it('returns an JSONP response (no charset)', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: (request, h) => h.response({ some: 'value' }).charset('') } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me({"some":"value"});'); - expect(res.headers['content-length']).to.equal(25); - expect(res.headers['content-type']).to.equal('text/javascript'); - }); - - it('returns a X-Content-Type-Options: nosniff header on JSONP responses', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me({"some":"value"});'); - expect(res.headers['x-content-type-options']).to.equal('nosniff'); - }); - - it('returns a normal response when JSONP enabled but not requested', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } }); - - const res = await server.inject('/'); - expect(res.payload).to.equal('{"some":"value"}'); - }); - - it('returns an JSONP response with compression', async () => { - - const server = Hapi.server({ compression: { minBytes: 1 } }); - server.route({ - method: 'GET', - path: '/user/{name*2}', - options: { - handler: (request) => { - - const parts = request.params.name.split('/'); - return { first: parts[0], last: parts[1] }; - }, - jsonp: 'callback' - } - }); - - const res = await server.inject({ url: '/user/1/2?callback=docall', headers: { 'accept-encoding': 'gzip' } }); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - expect(res.headers['content-encoding']).to.equal('gzip'); - expect(res.headers.vary).to.equal('accept-encoding'); - - const uncompressed = await internals.uncompress('unzip', res.rawPayload); - expect(uncompressed.toString()).to.equal('/**/docall({"first":"1","last":"2"});'); - }); - - it('returns an JSONP response when response is a buffer', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => Buffer.from('value') } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me(value);'); - expect(res.headers['content-length']).to.equal(14); - }); - - it('returns response on bad JSONP parameter', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } }); - - const res = await server.inject('/?callback=me*'); - expect(res.result).to.exist(); - expect(res.result.message).to.equal('Invalid JSONP parameter value'); - }); - - it('returns an JSONP handler error', async () => { - - const handler = () => { - - throw Boom.badRequest('wrong'); - }; - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me({"statusCode":400,"error":"Bad Request","message":"wrong"});'); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - }); - - it('returns an JSONP state error', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => 'ok' } }); - - let validState = false; - const preResponse = (request, h) => { - - validState = request.state && typeof request.state === 'object'; - return h.continue; - }; - - server.ext('onPreResponse', preResponse); - - const res = await server.inject({ method: 'GET', url: '/?callback=me', headers: { cookie: '+' } }); - expect(res.payload).to.equal('/**/me({"statusCode":400,"error":"Bad Request","message":"Invalid cookie header"});'); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - expect(validState).to.equal(true); - }); - it('sets specific caching headers', async () => { const server = Hapi.server(); From de4b992e32a17c39edc976df087854385a21bac1 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 25 Apr 2022 20:35:36 -0400 Subject: [PATCH 02/13] Ignore return value of response prepare() (#4348) --- lib/toolkit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/toolkit.js b/lib/toolkit.js index c678aa2d9..9db308675 100755 --- a/lib/toolkit.js +++ b/lib/toolkit.js @@ -102,7 +102,7 @@ exports.Manager = class { if (typeof response !== 'symbol') { response = request._core.Response.wrap(response, request); if (!response.isBoom && response._state === 'init') { - response = await response._prepare(); + await response._prepare(); } } From df42a0c4afcca4ab23dd18f3791957f0051d7a4b Mon Sep 17 00:00:00 2001 From: devin ivy Date: Sun, 1 May 2022 00:58:31 -0400 Subject: [PATCH 03/13] Update XSS security header default to disabled (#4352) --- API.md | 18 +++++++++++------- lib/config.js | 2 +- lib/security.js | 5 ++++- test/headers.js | 36 +++++++++++++++++++++++++++++++++--- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/API.md b/API.md index fc9b3860b..6141acfb8 100755 --- a/API.md +++ b/API.md @@ -3345,13 +3345,17 @@ following options: otherwise this field is ignored. If `rule` is `'allow-from'` but `source` is unset, the rule will be automatically changed to `'sameorigin'`. -- `xss` - boolean that controls the 'X-XSS-PROTECTION' header for Internet Explorer. Defaults to - `true` which sets the header to equal `'1; mode=block'`. - - Note: this setting can create a security vulnerability in versions of Internet Exploere below - 8, as well as unpatched versions of IE8. See [here](https://hackademix.net/2009/11/21/ies-xss-filter-creates-xss-vulnerabilities/) - and [here](https://technet.microsoft.com/library/security/ms10-002) for more information. If - you actively support old versions of IE, it may be wise to explicitly set this flag to - `false`. +- `xss` - controls the 'X-XSS-Protection' header, where: + + - `'disable'` - the header will be set to `'0'`. This is the default value. + - `'enable'` - the header will be set to `'1; mode=block'`. + - `false` - the header will be omitted. + + Note: when enabled, this setting can create a security vulnerabilities in versions of Internet Explorer + below 8, unpatched versions of IE8, and browsers that employ an XSS filter/auditor. See + [here](https://hackademix.net/2009/11/21/ies-xss-filter-creates-xss-vulnerabilities/), + [here](https://technet.microsoft.com/library/security/ms10-002), and + [here](https://blog.innerht.ml/the-misunderstood-x-xss-protection/) for more information. - `noOpen` - boolean controlling the 'X-Download-Options' header for Internet Explorer, preventing downloads from executing in your context. Defaults to `true` setting the header to `'noopen'`. diff --git a/lib/config.js b/lib/config.js index 94ca0586f..b56a5c08f 100755 --- a/lib/config.js +++ b/lib/config.js @@ -193,7 +193,7 @@ internals.routeBase = Validate.object({ }) ]) .default('deny'), - xss: Validate.boolean().default(true), + xss: Validate.valid('enabled', 'disabled', false).default('disabled'), noOpen: Validate.boolean().default(true), noSniff: Validate.boolean().default(true), referrer: Validate.alternatives([ diff --git a/lib/security.js b/lib/security.js index cd0063861..3b243f27e 100755 --- a/lib/security.js +++ b/lib/security.js @@ -65,9 +65,12 @@ exports.headers = function (response) { response._header('x-frame-options', security._xframe, { override: false }); } - if (security.xss) { + if (security.xss === 'enabled') { response._header('x-xss-protection', '1; mode=block', { override: false }); } + else if (security.xss === 'disabled') { + response._header('x-xss-protection', '0', { override: false }); + } if (security.noOpen) { response._header('x-download-options', 'noopen', { override: false }); diff --git a/test/headers.js b/test/headers.js index c6a447649..d04dc9074 100755 --- a/test/headers.js +++ b/test/headers.js @@ -213,7 +213,7 @@ describe('Headers', () => { expect(res.result).to.equal('Test'); expect(res.headers['strict-transport-security']).to.equal('max-age=15768000'); expect(res.headers['x-frame-options']).to.equal('DENY'); - expect(res.headers['x-xss-protection']).to.equal('1; mode=block'); + expect(res.headers['x-xss-protection']).to.equal('0'); expect(res.headers['x-download-options']).to.equal('noopen'); expect(res.headers['x-content-type-options']).to.equal('nosniff'); }); @@ -243,7 +243,7 @@ describe('Headers', () => { expect(res.result).to.equal('Test'); expect(res.headers['strict-transport-security']).to.not.exist(); expect(res.headers['x-frame-options']).to.equal('DENY'); - expect(res.headers['x-xss-protection']).to.equal('1; mode=block'); + expect(res.headers['x-xss-protection']).to.equal('0'); expect(res.headers['x-download-options']).to.equal('noopen'); expect(res.headers['x-content-type-options']).to.equal('nosniff'); }); @@ -335,7 +335,7 @@ describe('Headers', () => { expect(res.result).to.equal('Test'); expect(res.headers['x-frame-options']).to.not.exist(); expect(res.headers['strict-transport-security']).to.equal('max-age=15768000'); - expect(res.headers['x-xss-protection']).to.equal('1; mode=block'); + expect(res.headers['x-xss-protection']).to.equal('0'); expect(res.headers['x-download-options']).to.equal('noopen'); expect(res.headers['x-content-type-options']).to.equal('nosniff'); }); @@ -418,6 +418,36 @@ describe('Headers', () => { expect(res.headers['x-content-type-options']).to.not.exist(); }); + it('sets the x-xss-protection header when security.xss is enabled', async () => { + + const server = Hapi.server({ routes: { security: { xss: 'enabled' } } }); + server.route({ method: 'GET', path: '/', handler: () => 'Test' }); + + const res = await server.inject({ url: '/' }); + expect(res.result).to.exist(); + expect(res.result).to.equal('Test'); + expect(res.headers['x-xss-protection']).to.equal('1; mode=block'); + expect(res.headers['strict-transport-security']).to.equal('max-age=15768000'); + expect(res.headers['x-frame-options']).to.equal('DENY'); + expect(res.headers['x-download-options']).to.equal('noopen'); + expect(res.headers['x-content-type-options']).to.equal('nosniff'); + }); + + it('sets the x-xss-protection header when security.xss is disabled', async () => { + + const server = Hapi.server({ routes: { security: { xss: 'disabled' } } }); + server.route({ method: 'GET', path: '/', handler: () => 'Test' }); + + const res = await server.inject({ url: '/' }); + expect(res.result).to.exist(); + expect(res.result).to.equal('Test'); + expect(res.headers['x-xss-protection']).to.equal('0'); + expect(res.headers['strict-transport-security']).to.equal('max-age=15768000'); + expect(res.headers['x-frame-options']).to.equal('DENY'); + expect(res.headers['x-download-options']).to.equal('noopen'); + expect(res.headers['x-content-type-options']).to.equal('nosniff'); + }); + it('does not set the x-xss-protection header when security.xss is false', async () => { const server = Hapi.server({ routes: { security: { xss: false } } }); From db0cb4506c3bf96467a9fdaba6afcbe688bd17d3 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Sun, 1 May 2022 00:58:45 -0400 Subject: [PATCH 04/13] Change preflight status code default to 200. Make configurable as cors.preflightStatusCode (#4351) --- API.md | 3 +++ lib/config.js | 3 ++- lib/cors.js | 1 + test/cors.js | 65 +++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/API.md b/API.md index 6141acfb8..25765db4b 100755 --- a/API.md +++ b/API.md @@ -2894,6 +2894,9 @@ object with the following options: - `credentials` - if `true`, allows user credentials to be sent ('Access-Control-Allow-Credentials'). Defaults to `false`. + - `preflightStatusCode` - the status code used for CORS preflight responses, either `200` or `204`. + Defaults to `200`. + ### `route.options.description` Default value: none. diff --git a/lib/config.js b/lib/config.js index b56a5c08f..d64f3c834 100755 --- a/lib/config.js +++ b/lib/config.js @@ -109,7 +109,8 @@ internals.routeBase = Validate.object({ additionalHeaders: Validate.array().items(Validate.string()).default([]), exposedHeaders: Validate.array().items(Validate.string()).default(['WWW-Authenticate', 'Server-Authorization']), additionalExposedHeaders: Validate.array().items(Validate.string()).default([]), - credentials: Validate.boolean().when('origin', { is: 'ignore', then: false }).default(false) + credentials: Validate.boolean().when('origin', { is: 'ignore', then: false }).default(false), + preflightStatusCode: Validate.valid(200, 204).default(200) }) .allow(false, true) .default(false), diff --git a/lib/cors.js b/lib/cors.js index 741900c34..6600531f2 100755 --- a/lib/cors.js +++ b/lib/cors.js @@ -137,6 +137,7 @@ internals.handler = function (request, h) { // Reply with the route CORS headers const response = h.response(); + response.code(settings.preflightStatusCode); response._header('access-control-allow-origin', settings._origin ? origin : '*'); response._header('access-control-allow-methods', method); response._header('access-control-allow-headers', settings._headersString); diff --git a/test/cors.js b/test/cors.js index 57c928e05..079562efd 100755 --- a/test/cors.js +++ b/test/cors.js @@ -59,7 +59,7 @@ describe('CORS', () => { server.route({ method: 'GET', path: '/b', handler: () => 'ok' }); const res1 = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } }); - expect(res1.statusCode).to.equal(204); + expect(res1.statusCode).to.equal(200); expect(res1.result).to.be.null(); expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/'); @@ -77,12 +77,12 @@ describe('CORS', () => { server.route({ method: 'GET', path: '/c', handler: () => 'ok' }); const res1 = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } }); - expect(res1.statusCode).to.equal(204); + expect(res1.statusCode).to.equal(200); expect(res1.result).to.be.null(); expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/'); const res2 = await server.inject({ method: 'OPTIONS', url: '/b', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } }); - expect(res2.statusCode).to.equal(204); + expect(res2.statusCode).to.equal(200); expect(res2.result).to.be.null(); expect(res2.headers['access-control-allow-origin']).to.equal('http://example.com/'); @@ -99,7 +99,7 @@ describe('CORS', () => { server.route({ method: 'POST', path: '/a', handler: () => 'ok', options: { cors: true } }); const res = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.result).to.be.null(); expect(res.headers['access-control-allow-origin']).to.equal('http://example.com/'); }); @@ -111,12 +111,12 @@ describe('CORS', () => { server.route({ method: 'GET', path: '/b', handler: () => 'ok' }); const res1 = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'a', 'access-control-request-method': 'GET' } }); - expect(res1.statusCode).to.equal(204); + expect(res1.statusCode).to.equal(200); expect(res1.result).to.be.null(); expect(res1.headers['access-control-allow-origin']).to.equal('a'); const res2 = await server.inject({ method: 'OPTIONS', url: '/b', headers: { origin: 'b', 'access-control-request-method': 'GET' } }); - expect(res2.statusCode).to.equal(204); + expect(res2.statusCode).to.equal(200); expect(res2.result).to.be.null(); expect(res2.headers['access-control-allow-origin']).to.equal('b'); }); @@ -143,7 +143,7 @@ describe('CORS', () => { expect(res1.headers['access-control-allow-credentials']).to.equal('true'); const res2 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } }); - expect(res2.statusCode).to.equal(204); + expect(res2.statusCode).to.equal(200); expect(res2.result).to.equal(null); expect(res2.headers['access-control-allow-credentials']).to.equal('true'); @@ -219,7 +219,7 @@ describe('CORS', () => { expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/'); const res2 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } }); - expect(res2.statusCode).to.equal(204); + expect(res2.statusCode).to.equal(200); expect(res2.result).to.be.null(); expect(res2.headers['access-control-allow-origin']).to.equal('http://example.com/'); }); @@ -242,7 +242,7 @@ describe('CORS', () => { server.route({ method: 'GET', path: '/', handler: () => 'ok' }); const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://test.example.com', 'access-control-request-method': 'GET' } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.payload.length).to.equal(0); expect(res.headers['access-control-allow-origin']).to.equal('http://test.example.com'); }); @@ -377,7 +377,7 @@ describe('CORS', () => { expect(res1.headers['access-control-expose-headers']).to.not.exist(); const res2 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } }); - expect(res2.statusCode).to.equal(204); + expect(res2.statusCode).to.equal(200); expect(res2.headers['access-control-allow-origin']).to.equal('http://example.com/'); expect(res2.headers['access-control-expose-headers']).to.not.exist(); }); @@ -465,7 +465,7 @@ describe('CORS', () => { } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.headers['access-control-allow-origin']).to.equal('*'); }); @@ -484,7 +484,7 @@ describe('CORS', () => { } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.headers['access-control-allow-origin']).to.equal('*'); }); @@ -503,7 +503,7 @@ describe('CORS', () => { } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match'); }); @@ -522,7 +522,7 @@ describe('CORS', () => { } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match'); }); @@ -541,11 +541,40 @@ describe('CORS', () => { } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match,Origin'); expect(res.headers['access-control-expose-headers']).to.equal('WWW-Authenticate,Server-Authorization'); }); + it('responds with configured preflight status code', async () => { + + const server = Hapi.server({ routes: { cors: { preflightStatusCode: 204 } } }); + server.route({ method: 'GET', path: '/204', handler: () => 'ok', options: { cors: true } }); + server.route({ method: 'GET', path: '/200', handler: () => 'ok', options: { cors: { preflightStatusCode: 200 } } }); + + const res1 = await server.inject({ + method: 'OPTIONS', + url: '/204', + headers: { + origin: 'http://test.example.com', + 'access-control-request-method': 'GET' + } + }); + + expect(res1.statusCode).to.equal(204); + + const res2 = await server.inject({ + method: 'OPTIONS', + url: '/200', + headers: { + origin: 'http://test.example.com', + 'access-control-request-method': 'GET' + } + }); + + expect(res2.statusCode).to.equal(200); + }); + it('matches allowed headers (Origin implicit)', async () => { const server = Hapi.server({ routes: { cors: true } }); @@ -561,7 +590,7 @@ describe('CORS', () => { } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match'); }); @@ -594,7 +623,7 @@ describe('CORS', () => { }); const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.headers['access-control-allow-credentials']).to.equal('true'); }); @@ -609,7 +638,7 @@ describe('CORS', () => { }); const res = await server.inject({ method: 'OPTIONS', url: 'http://example.com:4000/', headers: { origin: 'http://localhost', 'access-control-request-method': 'POST' } }); - expect(res.statusCode).to.equal(204); + expect(res.statusCode).to.equal(200); expect(res.headers['access-control-allow-methods']).to.equal('POST'); }); }); From 619380ab23ef0b5cd331a9eaef5a81635dce44f1 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Sun, 1 May 2022 00:59:04 -0400 Subject: [PATCH 05/13] Make default error available in validation failAction (#4350) --- API.md | 3 ++- lib/validation.js | 2 +- test/validation.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/API.md b/API.md index 25765db4b..aeea9b095 100755 --- a/API.md +++ b/API.md @@ -3439,7 +3439,8 @@ Default value: `'error'` (return a Bad Request (400) error response). A [`failAction` value](#lifecycle-failAction) which determines how to handle failed validations. When set to a function, the `err` argument includes the type of validation error under -`err.output.payload.validation.source`. +`err.output.payload.validation.source`. The default error that would otherwise have been logged + or returned can be accessed under `err.data.defaultError`. #### `route.options.validate.headers` diff --git a/lib/validation.js b/lib/validation.js index 2424cb226..3a2255812 100755 --- a/lib/validation.js +++ b/lib/validation.js @@ -146,7 +146,7 @@ internals.input = async function (source, request) { // Prepare error const defaultError = validationError.isBoom ? validationError : Boom.badRequest(`Invalid request ${source} input`); - const detailedError = Boom.boomify(validationError, { statusCode: 400, override: false }); + const detailedError = Boom.boomify(validationError, { statusCode: 400, override: false, data: { defaultError } }); detailedError.output.payload.validation = { source, keys: [] }; if (validationError.details) { for (const details of validationError.details) { diff --git a/test/validation.js b/test/validation.js index 4ac18239d..b4e363ff0 100755 --- a/test/validation.js +++ b/test/validation.js @@ -571,6 +571,36 @@ describe('validation', () => { expect(res.result).to.equal('Got error in query where a is bad'); }); + it('makes default error available in failAction', async () => { + + const server = Hapi.server(); + server.validator(Joi); + server.route({ + method: 'GET', + path: '/', + handler: () => 'ok', + options: { + validate: { + query: { + a: Joi.string().min(2) + }, + failAction: function (request, h, err) { + + err.data.defaultError.output.payload.message += ': ' + err.output.payload.validation.keys.join(', '); + + throw err.data.defaultError; + } + } + } + }); + + const res = await server.inject('/?a=1'); + expect(res.statusCode).to.equal(400); + expect(res.result).to.contain({ + message: 'Invalid request query input: a' + }); + }); + it('catches error thrown in failAction', async () => { const server = Hapi.server({ debug: false }); From 0bd3273dcb935280b2661697c42496baadda266d Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 6 May 2022 21:49:03 -0400 Subject: [PATCH 06/13] Enforce that response streams are always Stream.Readable (#4349) * Enforce that response streams are always Stream.Readable * Remove stray period in error response message --- lib/response.js | 13 +------- lib/streams.js | 17 ++++++----- test/response.js | 38 +++++++++++++++++++++-- test/transmit.js | 79 ------------------------------------------------ 4 files changed, 45 insertions(+), 102 deletions(-) diff --git a/lib/response.js b/lib/response.js index 8840c661b..b333d6506 100755 --- a/lib/response.js +++ b/lib/response.js @@ -676,18 +676,7 @@ exports = module.exports = internals.Response = class { static drain(stream) { - if (stream.destroy) { - stream.destroy(); - return; - } - - // Fallback for old-style streams - - stream.unpipe(); - - if (stream.close) { - stream.close(); - } + stream.destroy(); } }; diff --git a/lib/streams.js b/lib/streams.js index e69270e70..9316b9e97 100755 --- a/lib/streams.js +++ b/lib/streams.js @@ -1,9 +1,10 @@ 'use strict'; +const Stream = require('stream'); + const Boom = require('@hapi/boom'); const Teamwork = require('@hapi/teamwork'); - const internals = { team: Symbol('team') }; @@ -11,18 +12,18 @@ const internals = { exports.isStream = function (stream) { - if (!stream || - typeof stream !== 'object' || - typeof stream.pipe !== 'function') { + const isReadableStream = stream instanceof Stream.Readable; - return false; + if (!isReadableStream && + typeof stream?.pipe === 'function') { + throw Boom.badImplementation('Cannot reply with a stream-like object that is not an instance of Stream.Readable'); } - if (typeof stream._read !== 'function') { - throw Boom.badImplementation('Stream must have a readable interface'); + if (!isReadableStream) { + return false; } - if (stream._readableState.objectMode) { + if (stream.readableObjectMode) { throw Boom.badImplementation('Cannot reply with stream in object mode'); } diff --git a/test/response.js b/test/response.js index 28a3fb303..61056a1ba 100755 --- a/test/response.js +++ b/test/response.js @@ -7,6 +7,7 @@ const Stream = require('stream'); const Code = require('@hapi/code'); const Handlebars = require('handlebars'); +const LegacyReadableStream = require('legacy-readable-stream'); const Hapi = require('..'); const Inert = require('@hapi/inert'); const Lab = require('@hapi/lab'); @@ -1194,14 +1195,14 @@ describe('Response', () => { expect(res1.statusCode).to.equal(500); const [, event1] = await log1; - expect(event1.error).to.be.an.error('Stream must have a readable interface'); + expect(event1.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable'); const log2 = server.events.once({ name: 'request', channels: 'error' }); const res2 = await server.inject('/writable'); expect(res2.statusCode).to.equal(500); const [, event2] = await log2; - expect(event2.error).to.be.an.error('Stream must have a readable interface'); + expect(event2.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable'); }); it('errors on an http client stream response', async () => { @@ -1223,7 +1224,38 @@ describe('Response', () => { expect(res.statusCode).to.equal(500); const [, event] = await log; - expect(event.error).to.be.an.error('Stream must have a readable interface'); + expect(event.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable'); + }); + + it('errors on a legacy readable stream response', async () => { + + const streamHandler = () => { + + const stream = new LegacyReadableStream.Readable(); + stream._read = function (size) { + + const chunk = new Array(size).join('x'); + + setTimeout(() => { + + this.push(chunk); + }, 10); + }; + + return stream; + }; + + const server = Hapi.server({ debug: false }); + server.route({ method: 'GET', path: '/stream', handler: streamHandler }); + + const log = server.events.once({ name: 'request', channels: 'error' }); + + await server.initialize(); + const res = await server.inject('/stream'); + expect(res.statusCode).to.equal(500); + + const [, event] = await log; + expect(event.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable'); }); it('errors on objectMode stream response', async () => { diff --git a/test/transmit.js b/test/transmit.js index 8449bca89..35754d539 100755 --- a/test/transmit.js +++ b/test/transmit.js @@ -16,7 +16,6 @@ const Hoek = require('@hapi/hoek'); const Bounce = require('@hapi/bounce'); const Inert = require('@hapi/inert'); const Lab = require('@hapi/lab'); -const LegacyReadableStream = require('legacy-readable-stream'); const Teamwork = require('@hapi/teamwork'); const Wreck = require('@hapi/wreck'); @@ -1334,84 +1333,6 @@ describe('transmission', () => { await server.stop(); }); - it('close() stream when no destroy() method', async () => { - - const server = Hapi.server(); - - const team = new Teamwork.Team(); - const handler = (request) => { - - const stream = new LegacyReadableStream.Readable(); - stream._read = function (size) { - - const chunk = new Array(size).join('x'); - - setTimeout(() => { - - this.push(chunk); - }, 10); - }; - - stream.close = () => team.attend(); - - return stream; - }; - - server.route({ method: 'GET', path: '/', handler }); - - await server.start(); - - const res = await Wreck.request('GET', 'http://localhost:' + server.info.port); - res.once('data', (chunk) => { - - res.destroy(); - }); - - await team.work; - await server.stop(); - - expect(res.statusCode).to.equal(200); - }); - - it('unpipe() stream when no destroy() or close() method', async () => { - - const server = Hapi.server(); - - const team = new Teamwork.Team(); - const handler = (request) => { - - const stream = new LegacyReadableStream.Readable(); - stream._read = function (size) { - - const chunk = new Array(size).join('x'); - - setTimeout(() => { - - this.push(chunk); - }, 10); - }; - - stream.unpipe = () => team.attend(); - - return stream; - }; - - server.route({ method: 'GET', path: '/', handler }); - - await server.start(); - - const res = await Wreck.request('GET', 'http://localhost:' + server.info.port); - res.once('data', (chunk) => { - - res.destroy(); - }); - - await team.work; - await server.stop(); - - expect(res.statusCode).to.equal(200); - }); - it('changes etag when content-encoding set manually', async () => { const payload = new Array(1000).fill('x').join(); From a0346735d7ee2d120a5fe04e332b647836cafa97 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 16 May 2022 15:43:04 -0400 Subject: [PATCH 07/13] Drop node v12 support (#4346) * Drop node v12 support * Replace deprecated node APIs * Update lab, code, and shot to beta versions --- .github/workflows/ci-module.yml | 3 +++ lib/auth.js | 22 ++++++++++------------ lib/core.js | 28 ++++++++++++++-------------- lib/handler.js | 4 ++-- lib/headers.js | 2 +- lib/methods.js | 6 +++--- lib/request.js | 20 ++++++++++---------- lib/response.js | 28 ++++++++++++++-------------- lib/route.js | 22 +++++++++++----------- lib/security.js | 2 +- lib/server.js | 28 ++++++++++++++-------------- lib/toolkit.js | 6 +++--- lib/transmit.js | 2 +- lib/validation.js | 2 +- package.json | 6 +++--- test/core.js | 31 +++++++++++++++++++++++++++++++ 16 files changed, 122 insertions(+), 90 deletions(-) diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 9dc4315b6..7229300a8 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -3,6 +3,7 @@ name: ci on: push: branches: + - v21 - master pull_request: workflow_dispatch: @@ -10,3 +11,5 @@ on: jobs: test: uses: hapijs/.github/.github/workflows/ci-module.yml@master + with: + min-node-version: 14 diff --git a/lib/auth.js b/lib/auth.js index 8f35c9794..e3de0fc7f 100755 --- a/lib/auth.js +++ b/lib/auth.js @@ -67,7 +67,7 @@ exports = module.exports = internals.Auth = class { Hoek.assert(typeof strategy.authenticate === 'function', 'Invalid scheme:', name, 'invalid authenticate() method'); Hoek.assert(!strategy.payload || typeof strategy.payload === 'function', 'Invalid scheme:', name, 'invalid payload() method'); Hoek.assert(!strategy.response || typeof strategy.response === 'function', 'Invalid scheme:', name, 'invalid response() method'); - strategy.options = strategy.options || {}; + strategy.options = strategy.options ?? {}; Hoek.assert(strategy.payload || !strategy.options.payload, 'Cannot require payload validation without a payload method'); this.#strategies[name] = { @@ -171,10 +171,10 @@ exports = module.exports = internals.Auth = class { options = Hoek.applyToDefaults(this.settings.default, options); } - path = path || 'default strategy'; - Hoek.assert(options.strategies && options.strategies.length, 'Missing authentication strategy:', path); + path = path ?? 'default strategy'; + Hoek.assert(options.strategies?.length, 'Missing authentication strategy:', path); - options.mode = options.mode || 'required'; + options.mode = options.mode ?? 'required'; if (options.entity !== undefined || // Backwards compatibility with <= 11.x.x options.scope !== undefined) { @@ -304,9 +304,7 @@ exports = module.exports = internals.Auth = class { _access(request, route) { const config = this.lookup(route || request.route); - if (!config || - !config.access) { - + if (!config?.access) { return true; } @@ -389,7 +387,7 @@ exports = module.exports = internals.Auth = class { } const config = auth.lookup(request.route); - const setting = config.payload || (strategy.methods.options.payload ? 'required' : false); + const setting = config.payload ?? (strategy.methods.options.payload ? 'required' : false); if (!setting) { return; } @@ -451,13 +449,13 @@ internals.setupScope = function (access) { const prefix = value[0]; const type = prefix === '+' ? 'required' : (prefix === '!' ? 'forbidden' : 'selection'); const clean = type === 'selection' ? value : value.slice(1); - scope[type] = scope[type] || []; + scope[type] = scope[type] ?? []; scope[type].push(clean); - if ((!scope._hasParameters || !scope._hasParameters[type]) && + if ((!scope._hasParameters?.[type]) && /{([^}]+)}/.test(clean)) { - scope._hasParameters = scope._hasParameters || {}; + scope._hasParameters = scope._hasParameters ?? {}; scope._hasParameters[type] = true; } } @@ -468,7 +466,7 @@ internals.setupScope = function (access) { internals.validate = function (err, result, name, config, request, errors) { // err can be Boom, Error, or a valid response object - result = result || {}; + result = result ?? {}; request.auth.isAuthenticated = !err; if (err) { diff --git a/lib/core.js b/lib/core.js index 64faab4d1..b5e96319f 100755 --- a/lib/core.js +++ b/lib/core.js @@ -148,8 +148,8 @@ exports = module.exports = internals.Core = class { const method = (event) => { - const data = event.error || event.data; - console.error('Debug:', event.tags.join(', '), data ? '\n ' + (data.stack || (typeof data === 'object' ? Hoek.stringify(data) : data)) : ''); + const data = event.error ?? event.data; + console.error('Debug:', event.tags.join(', '), data ? '\n ' + (data.stack ?? (typeof data === 'object' ? Hoek.stringify(data) : data)) : ''); }; if (debug.log) { @@ -188,7 +188,7 @@ exports = module.exports = internals.Core = class { port, protocol, id: Os.hostname() + ':' + process.pid + ':' + now.toString(36), - uri: this.settings.uri || (protocol + ':' + (this.type === 'tcp' ? '//' + host + (port ? ':' + port : '') : port)) + uri: this.settings.uri ?? (protocol + ':' + (this.type === 'tcp' ? '//' + host + (port ? ':' + port : '') : port)) }; return info; @@ -223,7 +223,7 @@ exports = module.exports = internals.Core = class { config = { provider: { constructor: config } }; } - const name = config.name || '_default'; + const name = config.name ?? '_default'; Hoek.assert(!this.caches.has(name), 'Cannot configure the same cache more than once: ', name === '_default' ? 'default cache' : name); let client = null; @@ -234,13 +234,13 @@ exports = module.exports = internals.Core = class { provider = { constructor: provider }; } - client = new Catbox.Client(provider.constructor, provider.options || { partition: 'hapi-cache' }); + client = new Catbox.Client(provider.constructor, provider.options ?? { partition: 'hapi-cache' }); } else { client = new Catbox.Client(config.engine); } - this.caches.set(name, { client, segments: {}, shared: config.shared || false }); + this.caches.set(name, { client, segments: {}, shared: config.shared ?? false }); added.push(client); } @@ -392,7 +392,7 @@ exports = module.exports = internals.Core = class { async _stop(options = {}) { - options.timeout = options.timeout || 5000; // Default timeout to 5 seconds + options.timeout = options.timeout ?? 5000; // Default timeout to 5 seconds if (['stopped', 'initialized', 'started', 'invalid'].indexOf(this.phase) === -1) { throw new Error('Cannot stop server while in ' + this.phase + ' phase'); @@ -486,7 +486,7 @@ exports = module.exports = internals.Core = class { // Execute extensions for (const ext of exts.nodes) { - const bind = ext.bind || ext.realm.settings.bind; + const bind = ext.bind ?? ext.realm.settings.bind; const operation = ext.func.call(bind, ext.server, bind); await Toolkit.timed(operation, { timeout: ext.timeout, name: type }); } @@ -540,7 +540,7 @@ exports = module.exports = internals.Core = class { _createListener() { - const listener = this.settings.listener || (this.settings.tls ? Https.createServer(this.settings.tls) : Http.createServer()); + const listener = this.settings.listener ?? (this.settings.tls ? Https.createServer(this.settings.tls) : Http.createServer()); listener.on('request', this._dispatch()); listener.on('checkContinue', this._dispatch({ expectContinue: true })); @@ -577,7 +577,7 @@ exports = module.exports = internals.Core = class { const address = this.listener.address(); this.info.address = address.address; this.info.port = address.port; - this.info.uri = this.settings.uri || this.info.protocol + '://' + this.info.host + ':' + this.info.port; + this.info.uri = this.settings.uri ?? this.info.protocol + '://' + this.info.host + ':' + this.info.port; } if (this.settings.operations.cleanStop) { @@ -604,11 +604,11 @@ exports = module.exports = internals.Core = class { options = Config.apply('cachePolicy', options); - const plugin = realm && realm.plugin; - const segment = options.segment || _segment || (plugin ? `!${plugin}` : ''); + const plugin = realm?.plugin; + const segment = options.segment ?? _segment ?? (plugin ? `!${plugin}` : ''); Hoek.assert(segment, 'Missing cache segment name'); - const cacheName = options.cache || '_default'; + const cacheName = options.cache ?? '_default'; const cache = this.caches.get(cacheName); Hoek.assert(cache, 'Unknown cache', cacheName); Hoek.assert(!cache.segments[segment] || cache.shared || options.shared, 'Cannot provision the same cache segment more than once'); @@ -652,7 +652,7 @@ exports = module.exports = internals.Core = class { internals.setup = function (options = {}) { let settings = Hoek.clone(options, { shallow: ['cache', 'listener', 'routes.bind'] }); - settings.app = settings.app || {}; + settings.app = settings.app ?? {}; settings.routes = Config.enable(settings.routes); settings = Config.apply('server', settings); diff --git a/lib/handler.js b/lib/handler.js index 735d49c74..9ae261022 100755 --- a/lib/handler.js +++ b/lib/handler.js @@ -93,7 +93,7 @@ exports.defaults = function (method, handler, core) { } } - return defaults || {}; + return defaults ?? {}; }; @@ -152,7 +152,7 @@ exports.prerequisitesConfig = function (config) { const item = { method: pre.method, assign: pre.assign, - failAction: pre.failAction || 'error' + failAction: pre.failAction ?? 'error' }; set.push(item); diff --git a/lib/headers.js b/lib/headers.js index bdb2a3833..69a528e18 100755 --- a/lib/headers.js +++ b/lib/headers.js @@ -23,7 +23,7 @@ exports.cache = function (response) { response.settings.ttl) { const ttl = response.settings.ttl !== null ? response.settings.ttl : request._route._cache.ttl(); - const privacy = request.auth.isAuthenticated || response.headers['set-cookie'] ? 'private' : settings.privacy || 'default'; + const privacy = request.auth.isAuthenticated || response.headers['set-cookie'] ? 'private' : settings.privacy ?? 'default'; response._header('cache-control', 'max-age=' + Math.floor(ttl / 1000) + ', must-revalidate' + (privacy !== 'default' ? ', ' + privacy : '')); } else if (settings) { diff --git a/lib/methods.js b/lib/methods.js index 48959ee83..858e93111 100755 --- a/lib/methods.js +++ b/lib/methods.js @@ -33,7 +33,7 @@ exports = module.exports = internals.Methods = class { const items = [].concat(name); for (let item of items) { item = Config.apply('methodObject', item); - this._add(item.name, item.method, item.options || {}, realm); + this._add(item.name, item.method, item.options ?? {}, realm); } } @@ -47,9 +47,9 @@ exports = module.exports = internals.Methods = class { options = Config.apply('method', options, name); const settings = Hoek.clone(options, { shallow: ['bind'] }); - settings.generateKey = settings.generateKey || internals.generateKey; + settings.generateKey = settings.generateKey ?? internals.generateKey; - const bind = settings.bind || realm.settings.bind || null; + const bind = settings.bind ?? realm.settings.bind ?? null; const bound = !bind ? method : (...args) => method.apply(bind, args); // Not cached diff --git a/lib/request.js b/lib/request.js index f0e6f42fe..65b9b1e43 100755 --- a/lib/request.js +++ b/lib/request.js @@ -65,10 +65,10 @@ exports = module.exports = internals.Request = class { isAuthenticated: false, isAuthorized: false, isInjected: options.auth ? true : false, - [internals.Request.symbols.authPayload]: options.auth && options.auth.payload !== undefined ? options.auth.payload : true, - credentials: options.auth ? options.auth.credentials : null, // Special keys: 'app', 'user', 'scope' - artifacts: options.auth && options.auth.artifacts || null, // Scheme-specific artifacts - strategy: options.auth ? options.auth.strategy : null, + [internals.Request.symbols.authPayload]: options.auth?.payload ?? true, + credentials: options.auth?.credentials ?? null, // Special keys: 'app', 'user', 'scope' + artifacts: options.auth?.artifacts ?? null, // Scheme-specific artifacts + strategy: options.auth?.strategy ?? null, mode: null, error: null }; @@ -325,8 +325,8 @@ exports = module.exports = internals.Request = class { this.route = this._route.public; } - this.params = match.params || {}; - this.paramsArray = match.paramsArray || []; + this.params = match.params ?? {}; + this.paramsArray = match.paramsArray ?? []; if (this.route.settings.cors) { this.info.cors = { @@ -393,7 +393,7 @@ exports = module.exports = internals.Request = class { for (const ext of event.nodes) { const realm = ext.realm; - const bind = ext.bind || realm.settings.bind; + const bind = ext.bind ?? realm.settings.bind; const response = await this._core.toolkit.execute(ext.func, this, { bind, realm, timeout: ext.timeout, name: event.type, ignoreResponse: options.ignoreResponse }); if (options.ignoreResponse) { @@ -656,7 +656,7 @@ internals.Info = class { get remoteAddress() { if (!this._remoteAddress) { - this._remoteAddress = this._request.raw.req.connection.remoteAddress; + this._remoteAddress = this._request.raw.req.socket.remoteAddress; } return this._remoteAddress; @@ -665,7 +665,7 @@ internals.Info = class { get remotePort() { if (this._remotePort === null) { - this._remotePort = this._request.raw.req.connection.remotePort || ''; + this._remotePort = this._request.raw.req.socket.remotePort || ''; } return this._remotePort; @@ -704,7 +704,7 @@ internals.event = function ({ request }, event, err) { request._isPayloadPending = false; if (event === 'close' && - request.raw.res.finished) { + request.raw.res.writableEnded) { return; } diff --git a/lib/response.js b/lib/response.js index b333d6506..d7a836fd5 100755 --- a/lib/response.js +++ b/lib/response.js @@ -55,7 +55,7 @@ exports = module.exports = internals.Response = class { this._events = null; this._payload = null; // Readable stream - this._error = options.error || null; // The boom object when created from an error (used for logging) + this._error = options.error ?? null; // The boom object when created from an error (used for logging) this._contentType = null; // Used if no explicit content-type is set and type is known this._takeover = false; this._statusCode = false; // true when code() called @@ -89,7 +89,7 @@ exports = module.exports = internals.Response = class { // Method must not set any headers or other properties as source can change later - this.variety = variety || 'plain'; + this.variety = variety ?? 'plain'; if (source === null || source === undefined) { @@ -151,7 +151,7 @@ exports = module.exports = internals.Response = class { _header(key, value, options = {}) { - const append = options.append || false; + const append = options.append ?? false; const separator = options.separator || ','; const override = options.override !== false; const duplicate = options.duplicate !== false; @@ -345,28 +345,28 @@ exports = module.exports = internals.Response = class { replacer(method) { - this.settings.stringify = this.settings.stringify || {}; + this.settings.stringify = this.settings.stringify ?? {}; this.settings.stringify.replacer = method; return this; } spaces(count) { - this.settings.stringify = this.settings.stringify || {}; + this.settings.stringify = this.settings.stringify ?? {}; this.settings.stringify.space = count; return this; } suffix(suffix) { - this.settings.stringify = this.settings.stringify || {}; + this.settings.stringify = this.settings.stringify ?? {}; this.settings.stringify.suffix = suffix; return this; } escape(escape) { - this.settings.stringify = this.settings.stringify || {}; + this.settings.stringify = this.settings.stringify ?? {}; this.settings.stringify.escape = escape; return this; } @@ -466,7 +466,7 @@ exports = module.exports = internals.Response = class { charset(charset) { - this.settings.charset = charset || null; + this.settings.charset = charset ?? null; return this; } @@ -558,7 +558,7 @@ exports = module.exports = internals.Response = class { } } - this.statusCode = this.statusCode || 200; + this.statusCode = this.statusCode ?? 200; } async _marshal() { @@ -600,11 +600,11 @@ exports = module.exports = internals.Response = class { let payload = source; if (jsonify) { - const options = this.settings.stringify || {}; - const space = options.space || this.request.route.settings.json.space; - const replacer = options.replacer || this.request.route.settings.json.replacer; - const suffix = options.suffix || this.request.route.settings.json.suffix || ''; - const escape = this.request.route.settings.json.escape || false; + const options = this.settings.stringify ?? {}; + const space = options.space ?? this.request.route.settings.json.space; + const replacer = options.replacer ?? this.request.route.settings.json.replacer; + const suffix = options.suffix ?? this.request.route.settings.json.suffix ?? ''; + const escape = this.request.route.settings.json.escape; try { if (replacer || space) { diff --git a/lib/route.js b/lib/route.js index 6feca6011..0fb4e0d49 100755 --- a/lib/route.js +++ b/lib/route.js @@ -39,7 +39,7 @@ exports = module.exports = internals.Route = class { const path = realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (route.path !== '/' ? route.path : '') : route.path; 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); - const vhost = realm.modifiers.route.vhost || route.vhost; + const vhost = realm.modifiers.route.vhost ?? route.vhost; // Set identifying members (assert) @@ -48,7 +48,7 @@ exports = module.exports = internals.Route = class { // Prepare configuration - let config = route.options || route.config || {}; + let config = route.options ?? route.config ?? {}; if (typeof config === 'function') { config = config.call(realm.settings.bind, server); } @@ -58,12 +58,12 @@ exports = module.exports = internals.Route = class { // Verify route level config (as opposed to the merged settings) this._assert(method !== 'get' || !config.payload, 'Cannot set payload settings on HEAD or GET request'); - this._assert(method !== 'get' || !config.validate || !config.validate.payload, 'Cannot validate HEAD or GET request payload'); + this._assert(method !== 'get' || !config.validate?.payload, 'Cannot validate HEAD or GET request payload'); // Rules this._assert(!route.rules || !config.rules, 'Route rules can only appear once'); // XOR - const rules = route.rules || config.rules; + const rules = route.rules ?? config.rules; const rulesConfig = internals.rules(rules, { method, path, vhost }, server); delete config.rules; @@ -72,7 +72,7 @@ exports = module.exports = internals.Route = class { this._assert(route.handler || config.handler, 'Missing or undefined handler'); this._assert(!!route.handler ^ !!config.handler, 'Handler must only appear once'); // XOR - const handler = Config.apply('handler', route.handler || config.handler); + const handler = Config.apply('handler', route.handler ?? config.handler); delete config.handler; const handlerDefaults = Handler.defaults(method, handler, core); @@ -89,8 +89,8 @@ exports = module.exports = internals.Route = class { this.realm = realm; this.settings.vhost = vhost; - this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name - this.settings.app = this.settings.app || {}; // Route-specific application settings + this.settings.plugins = this.settings.plugins ?? {}; // Route-specific plugins settings, namespaced using plugin name + this.settings.app = this.settings.app ?? {}; // Route-specific application settings // Path parsing @@ -195,7 +195,7 @@ exports = module.exports = internals.Route = class { this.settings.response._validate = true; const rule = this.settings.response.schema; - this.settings.response.status = this.settings.response.status || {}; + this.settings.response.status = this.settings.response.status ?? {}; const statuses = Object.keys(this.settings.response.status); if (rule === true && @@ -379,8 +379,8 @@ internals.state = async function (request) { var parseError = err; } - const { states, failed = [] } = result || parseError; - request.state = states || {}; + const { states, failed = [] } = result ?? parseError; + request.state = states ?? {}; // Clear cookies @@ -419,7 +419,7 @@ internals.payload = async function (request) { try { const { payload, mime } = await Subtext.parse(request.raw.req, request._tap(), request.route.settings.payload); - request._isPayloadPending = !!(payload && payload._readableState); + request._isPayloadPending = !!payload?._readableState; request.mime = mime; request.payload = payload; } diff --git a/lib/security.js b/lib/security.js index 3b243f27e..de8776114 100755 --- a/lib/security.js +++ b/lib/security.js @@ -18,7 +18,7 @@ exports.route = function (settings) { security._hsts = 'max-age=' + security.hsts; } else { - security._hsts = 'max-age=' + (security.hsts.maxAge || 15768000); + security._hsts = 'max-age=' + (security.hsts.maxAge ?? 15768000); if (security.hsts.includeSubdomains || security.hsts.includeSubDomains) { security._hsts = security._hsts + '; includeSubDomains'; } diff --git a/lib/server.js b/lib/server.js index c78856dd2..939ebe60d 100755 --- a/lib/server.js +++ b/lib/server.js @@ -101,7 +101,7 @@ internals.Server = class { Hoek.assert(server instanceof internals.Server, 'Can only control Server objects'); - this._core.controlled = this._core.controlled || []; + this._core.controlled = this._core.controlled ?? []; this._core.controlled.push(server); } @@ -146,7 +146,7 @@ internals.Server = class { Hoek.assert(!this._core.Request.reserved.includes(property), 'Cannot override built-in request interface decoration:', propertyName); if (options.apply) { - this._core.decorations.requestApply = this._core.decorations.requestApply || new Map(); + this._core.decorations.requestApply = this._core.decorations.requestApply ?? new Map(); this._core.decorations.requestApply.set(property, method); } else { @@ -238,7 +238,7 @@ internals.Server = class { }); } - this._core.plugins[plugin] = this._core.plugins[plugin] || {}; + this._core.plugins[plugin] = this._core.plugins[plugin] ?? {}; if (typeof key === 'string') { this._core.plugins[plugin][key] = value; @@ -323,7 +323,7 @@ internals.Server = class { delete settings.plugins; delete settings.allowInternals; - settings.authority = settings.authority || this._core.info.host + ':' + this._core.info.port; + settings.authority = settings.authority ?? this._core.info.host + ':' + this._core.info.port; } Hoek.assert(!options.credentials, 'options.credentials no longer supported (use options.auth)'); @@ -414,10 +414,10 @@ internals.Server = class { this.realm.modifiers.route.vhost) { options = Hoek.clone(options); - options.routes = options.routes || {}; + options.routes = options.routes ?? {}; - options.routes.prefix = (this.realm.modifiers.route.prefix || '') + (options.routes.prefix || '') || undefined; - options.routes.vhost = this.realm.modifiers.route.vhost || options.routes.vhost; + options.routes.prefix = (this.realm.modifiers.route.prefix ?? '') + (options.routes.prefix ?? '') || undefined; + options.routes.vhost = this.realm.modifiers.route.vhost ?? options.routes.vhost; } options = Config.apply('register', options); @@ -453,12 +453,12 @@ internals.Server = class { item = Config.apply('plugin', item); - const name = item.plugin.name || item.plugin.pkg.name; + const name = item.plugin.name ?? item.plugin.pkg.name; const clone = this._clone(name); - clone.realm.modifiers.route.prefix = item.routes.prefix || options.routes.prefix; - clone.realm.modifiers.route.vhost = item.routes.vhost || options.routes.vhost; - clone.realm.pluginOptions = item.options || {}; + clone.realm.modifiers.route.prefix = item.routes.prefix ?? options.routes.prefix; + clone.realm.modifiers.route.vhost = item.routes.vhost ?? options.routes.vhost; + clone.realm.pluginOptions = item.options ?? {}; // Validate requirements @@ -480,7 +480,7 @@ internals.Server = class { } else { this._core.registrations[name] = { - version: item.plugin.version || item.plugin.pkg.version, + version: item.plugin.version ?? item.plugin.pkg.version, name, options: item.options }; @@ -492,7 +492,7 @@ internals.Server = class { // Register - await item.plugin.register(clone, item.options || {}); + await item.plugin.register(clone, item.options ?? {}); } } finally { @@ -522,7 +522,7 @@ internals.Server = class { _addRoute(config, server) { const route = new Route(config, server); // Do no use config beyond this point, use route members - const vhosts = [].concat(route.settings.vhost || '*'); + const vhosts = [].concat(route.settings.vhost ?? '*'); for (const vhost of vhosts) { const record = this._core.router.add({ method: route.method, path: route.path, vhost, analysis: route._analysis, id: route.settings.id }, route); diff --git a/lib/toolkit.js b/lib/toolkit.js index 9db308675..ded406f5f 100755 --- a/lib/toolkit.js +++ b/lib/toolkit.js @@ -42,7 +42,7 @@ exports.Manager = class { async execute(method, request, options) { const h = new this._toolkit(request, options); - const bind = options.bind || null; + const bind = options.bind ?? null; try { let operation; @@ -130,7 +130,7 @@ exports.Manager = class { throw err; } - return this.execute(failAction, request, { realm: request.route.realm, args: [options.details || err] }); + return this.execute(failAction, request, { realm: request.route.realm, args: [options.details ?? err] }); } }; @@ -225,7 +225,7 @@ internals.toolkit = function () { authenticated(data) { Hoek.assert(this._auth, 'Method not supported outside of authentication'); - Hoek.assert(data && data.credentials, 'Authentication data missing credentials information'); + Hoek.assert(data?.credentials, 'Authentication data missing credentials information'); return new internals.Auth(null, data); } diff --git a/lib/transmit.js b/lib/transmit.js index 705a29956..7a1424153 100755 --- a/lib/transmit.js +++ b/lib/transmit.js @@ -283,7 +283,7 @@ internals.end = function (env, event, err) { env.team = null; - if (request.raw.res.finished) { + if (request.raw.res.writableEnded) { if (!event) { request.info.responded = Date.now(); } diff --git a/lib/validation.js b/lib/validation.js index 3a2255812..e94bf1349 100755 --- a/lib/validation.js +++ b/lib/validation.js @@ -19,7 +19,7 @@ exports.validator = function (validator) { exports.compile = function (rule, validator, realm, core) { - validator = validator || internals.validator(realm, core); + validator = validator ?? internals.validator(realm, core); // false - nothing allowed diff --git a/package.json b/package.json index 64552b356..3d0911e6d 100755 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@hapi/hoek": "^9.0.4", "@hapi/mimos": "^6.0.0", "@hapi/podium": "^4.1.1", - "@hapi/shot": "^5.0.5", + "@hapi/shot": "6.0.0-beta.1", "@hapi/somever": "^3.0.0", "@hapi/statehood": "^7.0.3", "@hapi/subtext": "^7.0.3", @@ -43,11 +43,11 @@ "@hapi/validate": "^1.1.1" }, "devDependencies": { - "@hapi/code": "^8.0.0", + "@hapi/code": "9.0.0-beta.0", "@hapi/eslint-plugin": "*", "@hapi/inert": "^6.0.2", "@hapi/joi-legacy-test": "npm:@hapi/joi@^15.0.0", - "@hapi/lab": "^24.4.0", + "@hapi/lab": "25.0.0-beta.0", "@hapi/vision": "^6.0.1", "@hapi/wreck": "^17.0.0", "handlebars": "^4.7.4", diff --git a/test/core.js b/test/core.js index 9a4c51892..2e0a1838e 100755 --- a/test/core.js +++ b/test/core.js @@ -1080,6 +1080,37 @@ describe('Core', () => { await server.stop(); }); + + it('doesn\'t setup listeners for cleanStop when socket is missing', async () => { + + const server = Hapi.server(); + + server.route({ + method: 'get', + path: '/', + handler: (request) => request.raw.res.listenerCount('finish') + }); + + const { result: normalFinishCount } = await server.inject('/'); + + const { _dispatch } = server._core; + + server._core._dispatch = (opts) => { + + const fn = _dispatch.call(server._core, opts); + + return (req, res) => { + + req.socket = null; + + fn(req, res); + }; + }; + + const { result: missingSocketFinishCount } = await server.inject('/'); + + expect(missingSocketFinishCount).to.be.lessThan(normalFinishCount); + }); }); describe('inject()', () => { From 554294ee22049160586091706f4880a9cf7250ec Mon Sep 17 00:00:00 2001 From: devin ivy Date: Tue, 14 Jun 2022 08:04:59 -0400 Subject: [PATCH 08/13] Change default host to be IPv6-friendly (#4357) * Change default host from 0.0.0.0 to node's ipv6-friendly default (:: or 0.0.0.0) * Normalize IPv4-mapped IPv6 addresses in request.info.remoteAddress * Document IPv6-friendly default host in API docs --- API.md | 4 +-- lib/core.js | 3 +- lib/request.js | 10 +++++- package.json | 2 +- test/common.js | 19 ++++++---- test/core.js | 49 +++++++++++++++++-------- test/payload.js | 6 +--- test/request.js | 94 ++++++++++++++++++++++++++++++++++++++++-------- test/server.js | 5 +-- test/transmit.js | 4 +-- 10 files changed, 143 insertions(+), 53 deletions(-) diff --git a/API.md b/API.md index aeea9b095..4cbc8da04 100755 --- a/API.md +++ b/API.md @@ -26,11 +26,11 @@ All options are optionals. #### `server.options.address` -Default value: `'0.0.0.0'` (all available network interfaces). +Default value: `'::'` if IPv6 is available, otherwise `'0.0.0.0'` (i.e. all available network interfaces). Sets the hostname or IP address the server will listen on. If not configured, defaults to [`host`](#server.options.host) if present, otherwise to all available network interfaces. Set to -`'127.0.0.1'` or `'localhost'` to restrict the server to only those coming from the same host. +`'127.0.0.1'`, `'::1'`, or `'localhost'` to restrict the server to only those coming from the same host. #### `server.options.app` diff --git a/lib/core.js b/lib/core.js index b5e96319f..0d82d8cb5 100755 --- a/lib/core.js +++ b/lib/core.js @@ -336,7 +336,8 @@ exports = module.exports = internals.Core = class { this.listener.listen(this.settings.port, finalize); } else { - const address = this.settings.address || this.settings.host || '0.0.0.0'; + // Default is the unspecified address, :: if IPv6 is available or otherwise the IPv4 address 0.0.0.0 + const address = this.settings.address || this.settings.host || null; this.listener.listen(this.settings.port, address, finalize); } }); diff --git a/lib/request.js b/lib/request.js index 65b9b1e43..f0f953645 100755 --- a/lib/request.js +++ b/lib/request.js @@ -656,7 +656,15 @@ internals.Info = class { get remoteAddress() { if (!this._remoteAddress) { - this._remoteAddress = this._request.raw.req.socket.remoteAddress; + const ipv6Prefix = '::ffff:'; + const socketAddress = this._request.raw.req.socket.remoteAddress; + if (socketAddress.startsWith(ipv6Prefix) && socketAddress.includes('.', ipv6Prefix.length)) { + // Normalize IPv4-mapped IPv6 address, e.g. ::ffff:127.0.0.1 -> 127.0.0.1 + this._remoteAddress = socketAddress.slice(ipv6Prefix.length); + } + else { + this._remoteAddress = socketAddress; + } } return this._remoteAddress; diff --git a/package.json b/package.json index 3d0911e6d..fb39c75ff 100755 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ }, "devDependencies": { "@hapi/code": "9.0.0-beta.0", - "@hapi/eslint-plugin": "*", + "@hapi/eslint-plugin": "^5.0.0", "@hapi/inert": "^6.0.2", "@hapi/joi-legacy-test": "npm:@hapi/joi@^15.0.0", "@hapi/lab": "25.0.0-beta.0", diff --git a/test/common.js b/test/common.js index 7bdd6f763..eae48a36b 100644 --- a/test/common.js +++ b/test/common.js @@ -1,7 +1,8 @@ 'use strict'; const ChildProcess = require('child_process'); -const Dns = require('dns'); +const Http = require('http'); +const Net = require('net'); const internals = {}; @@ -17,11 +18,15 @@ internals.hasLsof = () => { return true; }; -exports.hasLsof = internals.hasLsof(); +internals.hasIPv6 = () => { -exports.setDefaultDnsOrder = () => { - // Resolve localhost to ipv4 address on node v17 - if (Dns.setDefaultResultOrder) { - Dns.setDefaultResultOrder('ipv4first'); - } + const server = Http.createServer().listen(); + const { address } = server.address(); + server.close(); + + return Net.isIPv6(address); }; + +exports.hasLsof = internals.hasLsof(); + +exports.hasIPv6 = internals.hasIPv6(); diff --git a/test/core.js b/test/core.js index 2e0a1838e..1e1ce0146 100755 --- a/test/core.js +++ b/test/core.js @@ -1,6 +1,7 @@ 'use strict'; const ChildProcess = require('child_process'); +const Events = require('events'); const Fs = require('fs'); const Http = require('http'); const Https = require('https'); @@ -27,14 +28,12 @@ const Common = require('./common'); const internals = {}; -const { describe, it, before } = exports.lab = Lab.script(); +const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('Core', () => { - before(Common.setDefaultDnsOrder); - it('sets app settings defaults', () => { const server = Hapi.server(); @@ -99,38 +98,58 @@ describe('Core', () => { }).to.throw('Cannot specify port when autoListen is false'); }); - it('defaults address to 0.0.0.0 or :: when no host is provided', async () => { + it('defaults address to 0.0.0.0 or :: when no host is provided', async (flags) => { const server = Hapi.server(); await server.start(); + flags.onCleanup = () => server.stop(); - let expectedBoundAddress = '0.0.0.0'; - if (Net.isIPv6(server.listener.address().address)) { - expectedBoundAddress = '::'; - } + const expectedBoundAddress = Common.hasIPv6 ? '::' : '0.0.0.0'; expect(server.info.address).to.equal(expectedBoundAddress); - await server.stop(); }); - it('uses address when present instead of host', async () => { + it('is accessible on localhost when using default host', async (flags) => { + // With hapi v20 this would fail on ipv6 machines on node v18+ due to DNS resolution changes in node (see nodejs/node#40537). + // To address this in hapi v21 we bind to :: if available, otherwise the former default of 0.0.0.0. + + const server = Hapi.server(); + server.route({ method: 'get', path: '/', handler: () => 'ok' }); + + await server.start(); + flags.onCleanup = () => server.stop(); + + const req = Http.get(`http://localhost:${server.info.port}`); + const [res] = await Events.once(req, 'response'); + + let result = ''; + for await (const chunk of res) { + result += chunk.toString(); + } + + expect(result).to.equal('ok'); + }); + + it('uses address when present instead of host', async (flags) => { const server = Hapi.server({ host: 'no.such.domain.hapi', address: 'localhost' }); await server.start(); + flags.onCleanup = () => server.stop(); + expect(server.info.host).to.equal('no.such.domain.hapi'); - expect(server.info.address).to.equal('127.0.0.1'); - await server.stop(); + expect(server.info.address).to.match(/^127\.0\.0\.1|::1$/); // ::1 on node v18 with ipv6 support }); - it('uses uri when present instead of host and port', async () => { + it('uses uri when present instead of host and port', async (flags) => { const server = Hapi.server({ host: 'no.such.domain.hapi', address: 'localhost', uri: 'http://uri.example.com:8080' }); expect(server.info.uri).to.equal('http://uri.example.com:8080'); await server.start(); + flags.onCleanup = () => server.stop(); + expect(server.info.host).to.equal('no.such.domain.hapi'); - expect(server.info.address).to.equal('127.0.0.1'); + expect(server.info.address).to.match(/^127\.0\.0\.1|::1$/); // ::1 on node v18 with ipv6 support expect(server.info.uri).to.equal('http://uri.example.com:8080'); - await server.stop(); }); it('throws on uri ending with /', () => { diff --git a/test/payload.js b/test/payload.js index 8607d3e84..9361b8d54 100755 --- a/test/payload.js +++ b/test/payload.js @@ -11,19 +11,15 @@ const Hoek = require('@hapi/hoek'); const Lab = require('@hapi/lab'); const Wreck = require('@hapi/wreck'); -const Common = require('./common'); - const internals = {}; -const { describe, it, before } = exports.lab = Lab.script(); +const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('Payload', () => { - before(Common.setDefaultDnsOrder); - it('sets payload', async () => { const payload = '{"x":"1","y":"2","z":"3"}'; diff --git a/test/request.js b/test/request.js index e526dcd8b..2669f670a 100755 --- a/test/request.js +++ b/test/request.js @@ -20,14 +20,12 @@ const Common = require('./common'); const internals = {}; -const { describe, it, before } = exports.lab = Lab.script(); +const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('Request.Generator', () => { - before(Common.setDefaultDnsOrder); - it('decorates request multiple times', async () => { const server = Hapi.server(); @@ -125,35 +123,103 @@ describe('Request.Generator', () => { describe('Request', () => { - it('sets client address', async () => { + it('sets client address (default)', async (flags) => { const server = Hapi.server(); const handler = (request) => { - let expectedClientAddress = '127.0.0.1'; - if (Net.isIPv6(server.listener.address().address)) { - expectedClientAddress = '::ffff:127.0.0.1'; + // Call twice to reuse cached values + + if (Common.hasIPv6) { + // 127.0.0.1 on node v14 and v16, ::1 on node v18 since DNS resolved to IPv6. + expect(request.info.remoteAddress).to.match(/^127\.0\.0\.1|::1$/); + expect(request.info.remoteAddress).to.match(/^127\.0\.0\.1|::1$/); + } + else { + expect(request.info.remoteAddress).to.equal('127.0.0.1'); + expect(request.info.remoteAddress).to.equal('127.0.0.1'); } - expect(request.info.remoteAddress).to.equal(expectedClientAddress); expect(request.info.remotePort).to.be.above(0); - - // Call twice to reuse cached values - - expect(request.info.remoteAddress).to.equal(expectedClientAddress); expect(request.info.remotePort).to.be.above(0); return 'ok'; }; - server.route({ method: 'GET', path: '/', handler }); + server.route({ method: 'get', path: '/', handler }); await server.start(); + flags.onCleanup = () => server.stop(); const { payload } = await Wreck.get('http://localhost:' + server.info.port); expect(payload.toString()).to.equal('ok'); - await server.stop(); + }); + + it('sets client address (ipv4)', async (flags) => { + + const server = Hapi.server(); + + const handler = (request) => { + + Object.defineProperty(request.raw.req.socket, 'remoteAddress', { + value: '100.100.100.100' + }); + + return request.info.remoteAddress; + }; + + server.route({ method: 'get', path: '/', handler }); + + await server.start(); + flags.onCleanup = () => server.stop(); + + const { payload } = await Wreck.get('http://localhost:' + server.info.port); + expect(payload.toString()).to.equal('100.100.100.100'); + }); + + it('sets client address (ipv6)', async (flags) => { + + const server = Hapi.server(); + + const handler = (request) => { + + Object.defineProperty(request.raw.req.socket, 'remoteAddress', { + value: '::ffff:0:0:0:0:1' + }); + + return request.info.remoteAddress; + }; + + server.route({ method: 'get', path: '/', handler }); + + await server.start(); + flags.onCleanup = () => server.stop(); + + const { payload } = await Wreck.get('http://localhost:' + server.info.port); + expect(payload.toString()).to.equal('::ffff:0:0:0:0:1'); + }); + + it('sets client address (ipv4-mapped ipv6)', async (flags) => { + + const server = Hapi.server(); + + const handler = (request) => { + + Object.defineProperty(request.raw.req.socket, 'remoteAddress', { + value: '::ffff:100.100.100.100' + }); + + return request.info.remoteAddress; + }; + + server.route({ method: 'get', path: '/', handler }); + + await server.start(); + flags.onCleanup = () => server.stop(); + + const { payload } = await Wreck.get('http://localhost:' + server.info.port); + expect(payload.toString()).to.equal('100.100.100.100'); }); it('sets port to nothing when not available', async () => { diff --git a/test/server.js b/test/server.js index bfc9dae59..c43eb9a3f 100755 --- a/test/server.js +++ b/test/server.js @@ -14,21 +14,18 @@ const Lab = require('@hapi/lab'); const Vision = require('@hapi/vision'); const Wreck = require('@hapi/wreck'); -const Common = require('./common'); const Pkg = require('../package.json'); const internals = {}; -const { describe, it, before } = exports.lab = Lab.script(); +const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('Server', () => { - before(Common.setDefaultDnsOrder); - describe('auth', () => { it('adds auth strategy via plugin', async () => { diff --git a/test/transmit.js b/test/transmit.js index 35754d539..06b954c96 100755 --- a/test/transmit.js +++ b/test/transmit.js @@ -24,14 +24,12 @@ const Common = require('./common'); const internals = {}; -const { describe, it, before } = exports.lab = Lab.script(); +const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('transmission', () => { - before(Common.setDefaultDnsOrder); - describe('send()', () => { it('handlers invalid headers in error', async () => { From 0f8238d18cdcc59e31a752f62f9bd3a449d48b63 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Thu, 23 Jun 2022 16:31:23 -0400 Subject: [PATCH 09/13] Upgrade production deps for hapi v21 and node v18 (#4361) * Upgrade production deps for hapi v21 and node v18 * Account for change to podium emit(), no longer can be awaited. Closes #4184 --- API.md | 20 +++++++++++++------- lib/core.js | 10 +++++----- lib/request.js | 2 +- lib/response.js | 2 +- package.json | 44 ++++++++++++++++++++++---------------------- test/core.js | 2 +- test/headers.js | 2 +- test/methods.js | 2 +- test/server.js | 4 ++-- 9 files changed, 47 insertions(+), 41 deletions(-) diff --git a/API.md b/API.md index 4cbc8da04..2e3c9edce 100755 --- a/API.md +++ b/API.md @@ -1539,11 +1539,11 @@ async function example() { const server = Hapi.server({ port: 80 }); server.event('test'); server.events.on('test', (update) => console.log(update)); - await server.events.emit('test', 'hello'); + await server.events.gauge('test', 'hello'); } ``` -### `await server.events.emit(criteria, data)` +### `server.events.emit(criteria, data)` Emits a custom application event to all the subscribed listeners where: @@ -1569,7 +1569,7 @@ async function example() { const server = Hapi.server({ port: 80 }); server.event('test'); server.events.on('test', (update) => console.log(update)); - await server.events.emit('test', 'hello'); // await is optional + server.events.emit('test', 'hello'); } ``` @@ -1633,7 +1633,7 @@ async function example() { const server = Hapi.server({ port: 80 }); server.event('test'); server.events.on('test', (update) => console.log(update)); - await server.events.emit('test', 'hello'); + server.events.emit('test', 'hello'); } ``` @@ -1651,8 +1651,8 @@ async function example() { const server = Hapi.server({ port: 80 }); server.event('test'); server.events.once('test', (update) => console.log(update)); - await server.events.emit('test', 'hello'); - await server.events.emit('test', 'hello'); // Ignored + server.events.emit('test', 'hello'); + server.events.emit('test', 'hello'); // Ignored } ``` @@ -1670,11 +1670,17 @@ async function example() { const server = Hapi.server({ port: 80 }); server.event('test'); const pending = server.events.once('test'); - await server.events.emit('test', 'hello'); + server.events.emit('test', 'hello'); const update = await pending; } ``` +### `await server.events.gauge(criteria, data)` + +Behaves identically to [`server.events.emit()`](#server.events.emit()), but also returns an array of the results of all the event listeners that run. The return value is that of `Promise.allSettled()`, where each item in the resulting array is `{ status: 'fulfilled', value }` in the case of a successful handler, or `{ status: 'rejected', reason }` in the case of a handler that throws. + +Please note that system errors such as a `TypeError` are not handled specially, and it's recommended to scrutinize any rejections using something like [bounce](https://hapi.dev/module/bounce/). + ### `server.expose(key, value, [options])` Used within a plugin to expose a property via [`server.plugins[name]`](#server.plugins) where: diff --git a/lib/core.js b/lib/core.js index 0d82d8cb5..9691158e7 100755 --- a/lib/core.js +++ b/lib/core.js @@ -9,8 +9,8 @@ const Boom = require('@hapi/boom'); const Bounce = require('@hapi/bounce'); const Call = require('@hapi/call'); const Catbox = require('@hapi/catbox'); -const CatboxMemory = require('@hapi/catbox-memory'); -const Heavy = require('@hapi/heavy'); +const { Engine: CatboxMemory } = require('@hapi/catbox-memory'); +const { Heavy } = require('@hapi/heavy'); const Hoek = require('@hapi/hoek'); const { Mimos } = require('@hapi/mimos'); const Podium = require('@hapi/podium'); @@ -58,7 +58,7 @@ exports = module.exports = internals.Core = class { compression = new Compression(); controlled = null; // Other servers linked to the phases of this server dependencies = []; // Plugin dependencies - events = new Podium(internals.events); + events = new Podium.Podium(internals.events); heavy = null; info = null; instances = new Set(); @@ -293,7 +293,7 @@ exports = module.exports = internals.Core = class { } this.phase = 'started'; - await this.events.emit('start'); + this.events.emit('start'); try { if (this.controlled) { @@ -415,7 +415,7 @@ exports = module.exports = internals.Core = class { this.caches.forEach((cache) => caches.push(cache.client.stop())); await Promise.all(caches); - await this.events.emit('stop'); + this.events.emit('stop'); this.heavy.stop(); if (this.controlled) { diff --git a/lib/request.js b/lib/request.js index f0f953645..4924e22cd 100755 --- a/lib/request.js +++ b/lib/request.js @@ -97,7 +97,7 @@ exports = module.exports = internals.Request = class { get events() { if (!this._events) { - this._events = new Podium(internals.events); + this._events = new Podium.Podium(internals.events); } return this._events; diff --git a/lib/response.js b/lib/response.js index d7a836fd5..2acd8320a 100755 --- a/lib/response.js +++ b/lib/response.js @@ -117,7 +117,7 @@ exports = module.exports = internals.Response = class { get events() { if (!this._events) { - this._events = new Podium(internals.events); + this._events = new Podium.Podium(internals.events); } return this._events; diff --git a/package.json b/package.json index fb39c75ff..e9ec9c26a 100755 --- a/package.json +++ b/package.json @@ -23,33 +23,33 @@ ] }, "dependencies": { - "@hapi/accept": "^5.0.1", - "@hapi/ammo": "^5.0.1", - "@hapi/boom": "^9.1.0", - "@hapi/bounce": "^2.0.0", - "@hapi/call": "^8.0.0", - "@hapi/catbox": "^11.1.1", - "@hapi/catbox-memory": "^5.0.0", - "@hapi/heavy": "^7.0.1", - "@hapi/hoek": "^9.0.4", - "@hapi/mimos": "^6.0.0", - "@hapi/podium": "^4.1.1", - "@hapi/shot": "6.0.0-beta.1", - "@hapi/somever": "^3.0.0", - "@hapi/statehood": "^7.0.3", - "@hapi/subtext": "^7.0.3", - "@hapi/teamwork": "^5.1.0", - "@hapi/topo": "^5.0.0", - "@hapi/validate": "^1.1.1" + "@hapi/accept": "^6.0.0", + "@hapi/ammo": "^6.0.0", + "@hapi/boom": "^10.0.0", + "@hapi/bounce": "^3.0.0", + "@hapi/call": "^9.0.0", + "@hapi/catbox": "^12.0.0", + "@hapi/catbox-memory": "^6.0.0", + "@hapi/heavy": "^8.0.0", + "@hapi/hoek": "^10.0.0", + "@hapi/mimos": "^7.0.0", + "@hapi/podium": "^5.0.0", + "@hapi/shot": "^6.0.0", + "@hapi/somever": "^4.0.0", + "@hapi/statehood": "^8.0.0", + "@hapi/subtext": "^8.0.0", + "@hapi/teamwork": "^6.0.0", + "@hapi/topo": "^6.0.0", + "@hapi/validate": "^2.0.0" }, "devDependencies": { - "@hapi/code": "9.0.0-beta.0", - "@hapi/eslint-plugin": "^5.0.0", + "@hapi/code": "^9.0.0", + "@hapi/eslint-plugin": "^6.0.0", "@hapi/inert": "^6.0.2", "@hapi/joi-legacy-test": "npm:@hapi/joi@^15.0.0", - "@hapi/lab": "25.0.0-beta.0", + "@hapi/lab": "^25.0.1", "@hapi/vision": "^6.0.1", - "@hapi/wreck": "^17.0.0", + "@hapi/wreck": "^18.0.0", "handlebars": "^4.7.4", "joi": "^17.0.0", "legacy-readable-stream": "npm:readable-stream@^1.0.34" diff --git a/test/core.js b/test/core.js index 1e1ce0146..5b484c518 100755 --- a/test/core.js +++ b/test/core.js @@ -12,7 +12,7 @@ const Stream = require('stream'); const TLS = require('tls'); const Boom = require('@hapi/boom'); -const CatboxMemory = require('@hapi/catbox-memory'); +const { Engine: CatboxMemory } = require('@hapi/catbox-memory'); const Code = require('@hapi/code'); const Handlebars = require('handlebars'); const Hapi = require('..'); diff --git a/test/headers.js b/test/headers.js index d04dc9074..b5ed11004 100755 --- a/test/headers.js +++ b/test/headers.js @@ -1,7 +1,7 @@ 'use strict'; const Boom = require('@hapi/boom'); -const CatboxMemory = require('@hapi/catbox-memory'); +const { Engine: CatboxMemory } = require('@hapi/catbox-memory'); const Code = require('@hapi/code'); const Hapi = require('..'); const Inert = require('@hapi/inert'); diff --git a/test/methods.js b/test/methods.js index 36c14de38..70a3dcb38 100755 --- a/test/methods.js +++ b/test/methods.js @@ -1,7 +1,7 @@ 'use strict'; const Catbox = require('@hapi/catbox'); -const CatboxMemory = require('@hapi/catbox-memory'); +const { Engine: CatboxMemory } = require('@hapi/catbox-memory'); const Code = require('@hapi/code'); const Hapi = require('..'); const Hoek = require('@hapi/hoek'); diff --git a/test/server.js b/test/server.js index c43eb9a3f..20ad2df3d 100755 --- a/test/server.js +++ b/test/server.js @@ -4,7 +4,7 @@ const Path = require('path'); const Zlib = require('zlib'); const Boom = require('@hapi/boom'); -const CatboxMemory = require('@hapi/catbox-memory'); +const { Engine: CatboxMemory } = require('@hapi/catbox-memory'); const Code = require('@hapi/code'); const Handlebars = require('handlebars'); const Hapi = require('..'); @@ -858,7 +858,7 @@ describe('Server', () => { server.events.emit('test', 1); server.events.emit({ name: 'test', channel: 'x' }, 2); - await plugin.events.emit({ name: 'test', channel: 'y' }, 3); + plugin.events.emit({ name: 'test', channel: 'y' }, 3); expect(updates).to.equal([ { id: 'server', update: 1 }, From bd2bfded2c132302f1931c45d0deaf54dc2f4a00 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sun, 26 Jun 2022 23:36:24 -0400 Subject: [PATCH 10/13] 21.0.0-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de3dbd60a..3dfac5437 100755 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@hapi/hapi", "description": "HTTP Server framework", "homepage": "https://hapi.dev", - "version": "20.2.2", + "version": "21.0.0-beta.0", "repository": "git://github.com/hapijs/hapi", "main": "lib/index.js", "engines": { From 7e7ea149ad9958be50da7a7cce204242b67bbe54 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Tue, 12 Jul 2022 08:24:10 -0400 Subject: [PATCH 11/13] Allow matching prereleases when validating plugin version requirements (#4366) * Allow matching prereleases when validating plugin version requirements of node, hapi, and other plugins * Update tests to avoid unintentionally triggering error on node v18.5.0+ w/ llhttp upgrade --- lib/config.js | 2 ++ lib/core.js | 3 +-- lib/server.js | 5 ++-- package.json | 2 +- test/core.js | 8 +++---- test/request.js | 4 ++-- test/server.js | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/lib/config.js b/lib/config.js index d64f3c834..e3768aecf 100755 --- a/lib/config.js +++ b/lib/config.js @@ -2,6 +2,7 @@ const Os = require('os'); +const Somever = require('@hapi/somever'); const Validate = require('@hapi/validate'); @@ -38,6 +39,7 @@ exports.enable = function (options) { return settings; }; +exports.versionMatch = (version, range) => Somever.match(version, range, { includePrerelease: true }); internals.access = Validate.object({ entity: Validate.valid('user', 'app', 'any'), diff --git a/lib/core.js b/lib/core.js index 9691158e7..6ab2f9bdd 100755 --- a/lib/core.js +++ b/lib/core.js @@ -14,7 +14,6 @@ const { Heavy } = require('@hapi/heavy'); const Hoek = require('@hapi/hoek'); const { Mimos } = require('@hapi/mimos'); const Podium = require('@hapi/podium'); -const Somever = require('@hapi/somever'); const Statehood = require('@hapi/statehood'); const Auth = require('./auth'); @@ -386,7 +385,7 @@ exports = module.exports = internals.Core = class { for (const dep in deps) { const version = deps[dep]; Hoek.assert(this.registrations[dep], 'Plugin', plugin, 'missing dependency', dep); - Hoek.assert(version === '*' || Somever.match(this.registrations[dep].version, version), 'Plugin', plugin, 'requires', dep, 'version', version, 'but found', this.registrations[dep].version); + Hoek.assert(version === '*' || Config.versionMatch(this.registrations[dep].version, version), 'Plugin', plugin, 'requires', dep, 'version', version, 'but found', this.registrations[dep].version); } } } diff --git a/lib/server.js b/lib/server.js index 939ebe60d..06af15648 100755 --- a/lib/server.js +++ b/lib/server.js @@ -2,7 +2,6 @@ const Hoek = require('@hapi/hoek'); const Shot = require('@hapi/shot'); -const Somever = require('@hapi/somever'); const Teamwork = require('@hapi/teamwork'); const Config = require('./config'); @@ -463,8 +462,8 @@ internals.Server = class { // Validate requirements const requirements = item.plugin.requirements; - Hoek.assert(!requirements.node || Somever.match(process.version, requirements.node), 'Plugin', name, 'requires node version', requirements.node, 'but found', process.version); - Hoek.assert(!requirements.hapi || Somever.match(this.version, requirements.hapi), 'Plugin', name, 'requires hapi version', requirements.hapi, 'but found', this.version); + Hoek.assert(!requirements.node || Config.versionMatch(process.version, requirements.node), 'Plugin', name, 'requires node version', requirements.node, 'but found', process.version); + Hoek.assert(!requirements.hapi || Config.versionMatch(this.version, requirements.hapi), 'Plugin', name, 'requires hapi version', requirements.hapi, 'but found', this.version); // Protect against multiple registrations diff --git a/package.json b/package.json index 3dfac5437..10a1b088e 100755 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@hapi/mimos": "^7.0.0", "@hapi/podium": "^5.0.0", "@hapi/shot": "^6.0.0", - "@hapi/somever": "^4.0.0", + "@hapi/somever": "^4.1.0", "@hapi/statehood": "^8.0.0", "@hapi/subtext": "^8.0.0", "@hapi/teamwork": "^6.0.0", diff --git a/test/core.js b/test/core.js index 5b484c518..d2d85356f 100755 --- a/test/core.js +++ b/test/core.js @@ -835,7 +835,7 @@ describe('Core', () => { await server.start(); const socket = await internals.socket(server); - socket.write('GET / HTTP/1.0\nHost: test\n\n'); + socket.write('GET / HTTP/1.0\r\nHost: test\r\n\r\n'); await Hoek.wait(10); const count1 = await internals.countConnections(server); @@ -853,7 +853,7 @@ describe('Core', () => { await server.start(); const socket = await internals.socket(server); - socket.write('GET / HTTP/1.0\nHost: test\n\n'); + socket.write('GET / HTTP/1.0\r\nHost: test\r\n\r\n'); await Hoek.wait(10); const count1 = await internals.countConnections(server); @@ -874,7 +874,7 @@ describe('Core', () => { await server.start(); const socket = await internals.socket(server); - socket.write('GET / HTTP/1.1\nHost: test\nConnection: Keep-Alive\n\n\n'); + socket.write('GET / HTTP/1.1\r\nHost: test\r\nConnection: Keep-Alive\r\n\r\n\r\n'); await new Promise((resolve) => socket.on('data', resolve)); const count = await internals.countConnections(server); @@ -892,7 +892,7 @@ describe('Core', () => { await server.start(); const socket = await internals.socket(server); - socket.write('GET / HTTP/1.0\nHost: test\n\n'); + socket.write('GET / HTTP/1.0\r\nHost: test\r\n\r\n'); await Hoek.wait(10); const count1 = await internals.countConnections(server); diff --git a/test/request.js b/test/request.js index e17ba915f..4a976628c 100755 --- a/test/request.js +++ b/test/request.js @@ -2260,7 +2260,7 @@ describe('Request', () => { }); await new Promise((resolve) => client.on('connect', resolve)); - client.write('GET / HTTP/1.1\nHost: test\nContent-Length: 0\n\n\ninvalid data'); + client.write('GET / HTTP/1.1\r\nHost: test\r\nContent-Length: 0\r\n\r\n\r\ninvalid data'); const [request] = await log; expect(request.response.statusCode).to.equal(400); @@ -2290,7 +2290,7 @@ describe('Request', () => { }); await new Promise((resolve) => client.on('connect', resolve)); - client.write('GET / HTTP/1.1\nHost: test\nContent-Length: 0\n\n\ninvalid data'); + client.write('GET / HTTP/1.1\r\nHost: test\nContent-Length: 0\r\n\r\n\r\ninvalid data'); const clientResponse = await clientEnded; expect(clientResponse).to.contain('400 Bad Request'); diff --git a/test/server.js b/test/server.js index 20ad2df3d..49d1247df 100755 --- a/test/server.js +++ b/test/server.js @@ -2790,6 +2790,24 @@ describe('Server', () => { await expect(server.register(test)).to.not.reject(); }); + it('validates node version, allowing prereleases', async (flags) => { + + const test = { + name: 'test', + requirements: { + node: '>=8.x.x' + }, + register: function (srv, options) { } + }; + + const origVersion = process.version; + Object.defineProperty(process, 'version', { value: 'v100.0.0-beta' }); + flags.onCleanup = () => Object.defineProperty(process, 'version', { value: origVersion }); + + const server = Hapi.server(); + await expect(server.register(test)).to.not.reject(); + }); + it('errors on invalid node version', async () => { const test = { @@ -2818,6 +2836,21 @@ describe('Server', () => { await expect(server.register(test)).to.not.reject(); }); + it('validates hapi version, allowing prereleases', async () => { + + const test = { + name: 'test', + requirements: { + hapi: '>=17.x.x' + }, + register: function (srv, options) { } + }; + + const server = Hapi.server(); + server.version = '100.0.0-beta'; + await expect(server.register(test)).to.not.reject(); + }); + it('errors on invalid hapi version', async () => { const test = { @@ -2832,6 +2865,37 @@ describe('Server', () => { await expect(server.register(test)).to.reject(`Plugin test requires hapi version 4.x.x but found ${Pkg.version}`); }); + it('validates plugin version, allowing prereleases', async () => { + + const a = { + name: 'a', + version: '0.1.2', + dependencies: { + b: '>=3.x.x', + c: '>=2.x.x' + }, + register: Hoek.ignore + }; + + const b = { + name: 'b', + version: '4.0.0-beta', + register: Hoek.ignore + }; + + const c = { + name: 'c', + version: '2.3.4', + register: Hoek.ignore + }; + + const server = Hapi.server(); + await server.register(b); + await server.register(c); + await server.register(a); + await expect(server.initialize()).to.not.reject(); + }); + it('errors on invalid plugin version', async () => { const a = { From 8346f4068ab8b94851302e4756bcb4cb641188b0 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 12 Jul 2022 21:20:52 -0400 Subject: [PATCH 12/13] 21.0.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 10a1b088e..ac7237770 100755 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@hapi/hapi", "description": "HTTP Server framework", "homepage": "https://hapi.dev", - "version": "21.0.0-beta.0", + "version": "21.0.0-beta.1", "repository": "git://github.com/hapijs/hapi", "main": "lib/index.js", "engines": { From 923491bf7c390a999ad9e1845613a0a242bf5350 Mon Sep 17 00:00:00 2001 From: Jonas Pauthier Date: Wed, 21 Sep 2022 02:37:40 -0400 Subject: [PATCH 13/13] Update license (#4375) --- LICENSE.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 309153fc1..a5899ce28 100755 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,11 +1,13 @@ -Copyright (c) 2011-2020, Sideway Inc, and project contributors -Copyright (c) 2011-2014, Walmart -Copyright (c) 2011, Yahoo Inc. +Copyright (c) 2011-2022, Project contributors +Copyright (c) 2011-2020, Sideway Inc +Copyright (c) 2011-2014, Walmart +Copyright (c) 2011, Yahoo Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -* 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. -* The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission. + +- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +- 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. +- The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission. 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.