diff --git a/.github/workflows/ci-plugin.yml b/.github/workflows/ci-plugin.yml new file mode 100644 index 0000000..70c7fa1 --- /dev/null +++ b/.github/workflows/ci-plugin.yml @@ -0,0 +1,32 @@ +name: ci + +on: + push: + branches: + - master + pull_request: + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu, windows, macos] + node: ['*', '14', '12'] + hapi: ['20', '19'] + + runs-on: ${{ matrix.os }}-latest + name: ${{ matrix.os }} node@${{ matrix.node }} hapi@${{ matrix.hapi }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - name: install + run: npm install + - name: install hapi + run: npm install @hapi/hapi@${{ matrix.hapi }} + - name: install eslint + run: npm install --no-save eslint + - name: test + run: npm test diff --git a/.gitignore b/.gitignore index 7e1574d..8f679c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,13 @@ -.idea -*.iml -npm-debug.log -dump.rdb -node_modules -results.tap -results.xml -npm-shrinkwrap.json -config.json -.DS_Store -*/.DS_Store -*/*/.DS_Store -._* -*/._* -*/*/._* +**/node_modules +**/package-lock.json + coverage.* -lib-cov -complexity.md + +**/.DS_Store +**/._* + +**/*.pem + +**/.vs +**/.vscode +**/.idea diff --git a/.travis.yml b/.travis.yml deleted file mode 100755 index fee5429..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: node_js - -node_js: - - "4" - - "node" - -sudo: false diff --git a/API.md b/API.md new file mode 100755 index 0000000..a22e30d --- /dev/null +++ b/API.md @@ -0,0 +1,219 @@ + +## Introduction + +**h2o2** adds proxying functionality to a hapi server. + +## Manual loading + +```javascript +const Hapi = require('@hapi/hapi'); +const H2o2 = require('@hapi/h2o2'); + + +const start = async function() { + + const server = Hapi.server(); + try { + await server.register(H2o2); + await server.start(); + + console.log(`Server started at: ${server.info.uri}`); + } + catch(e) { + console.log('Failed to load h2o2'); + } +} + +start(); +``` + +## Options + +The plugin can be registered with an optional object specifying defaults to be applied to the proxy handler object. + +The proxy handler object has the following properties: + +* `host` - upstream service host to proxy requests to. It will have the same path as the client request. +* `port` - upstream service port. +* `protocol` - protocol to use when making the request to the proxied host: + * 'http' + * 'https' +* `uri` - absolute URI used instead of host, port, protocol, path, and query. Cannot be used with `host`, `port`, `protocol`, or `mapUri`. +* `httpClient` - an http client that abides by the Wreck interface. Defaults to [`wreck`](https://github.com/hapijs/wreck). +* `passThrough` - if set to `true`, it forwards the headers from the client to the upstream service, headers sent from the upstream service will also be forwarded to the client. Defaults to `false`. +* `localStatePassThrough` - if set to`false`, any locally defined state is removed from incoming requests before being sent to the upstream service. This value can be overridden on a per state basis via the `server.state()` `passThrough` option. Defaults to `false` +* `acceptEncoding` - if set to `false`, does not pass-through the 'Accept-Encoding' HTTP header which is useful for the `onResponse` post-processing to avoid receiving an encoded response. Can only be used together with `passThrough`. Defaults to `true` (passing header). +* `rejectUnauthorized` - sets the `rejectUnauthorized` property on the https [agent](http://nodejs.org/api/https.html#https_https_request_options_callback) making the request. This value is only used when the proxied server uses TLS/SSL. If set it will override the node.js `rejectUnauthorized` property. If `false` then ssl errors will be ignored. When `true` the server certificate is verified and an 500 response will be sent when verification fails. This shouldn't be used alongside the `agent` setting as the `agent` will be used instead. Defaults to the https agent default value of `true`. +* `xforward` - if set to `true`, sets the 'X-Forwarded-For', 'X-Forwarded-Port', 'X-Forwarded-Proto', 'X-Forwarded-Host' headers when making a request to the proxied upstream endpoint. Defaults to `false`. +* `redirects` - the maximum number of HTTP redirections allowed to be followed automatically by the handler. Set to `false` or `0` to disable all redirections (the response will contain the redirection received from the upstream service). If redirections are enabled, no redirections (301, 302, 307, 308) will be passed along to the client, and reaching the maximum allowed redirections will return an error response. Defaults to `false`. +* `timeout` - number of milliseconds before aborting the upstream request. Defaults to `180000` (3 minutes). +* `mapUri` - a function used to map the request URI to the proxied URI. Cannot be used together with `host`, `port`, `protocol`, or `uri`. The function signature is `function (request)` where: + * `request` - is the incoming [request object](http://hapijs.com/api#request-object). The response from this function should be an object with the following properties: + * `uri` - the absolute proxy URI. + * `headers` - optional object where each key is an HTTP request header and the value is the header content. +* `onRequest` - a custom function which is passed the upstream request. Function signature is `function (req)` where: + * `req` - the [wreck] (https://github.com/hapijs/wreck) request to the upstream server. +* `onResponse` - a custom function for processing the response from the upstream service before sending to the client. Useful for custom error handling of responses from the proxied endpoint or other payload manipulation. Function signature is `function (err, res, request, h, settings, ttl)` where: + * `err` - internal or upstream error returned from attempting to contact the upstream proxy. + * `res` - the node response object received from the upstream service. `res` is a readable stream (use the [wreck](https://github.com/hapijs/wreck) module `read` method to easily convert it to a Buffer or string). Note that it is your responsibility to close the `res` stream. + * `request` - is the incoming [request object](http://hapijs.com/api#request-object). + * `h` - the [response toolkit](https://hapijs.com/api#response-toolkit). + * `settings` - the proxy handler configuration. + * `ttl` - the upstream TTL in milliseconds if `proxy.ttl` it set to `'upstream'` and the upstream response included a valid 'Cache-Control' header with 'max-age'. +* `ttl` - if set to `'upstream'`, applies the upstream response caching policy to the response using the `response.ttl()` method (or passed as an argument to the `onResponse` method if provided). +* `agent` - a node [http(s) agent](http://nodejs.org/api/http.html#http_class_http_agent) to be used for connections to upstream server. +* `maxSockets` - sets the maximum number of sockets available per outgoing proxy host connection. `false` means use the **wreck** module default value (`Infinity`). Does not affect non-proxy outgoing client connections. Defaults to `Infinity`. +* `secureProtocol` - [TLS](http://nodejs.org/api/tls.html) flag indicating the SSL method to use, e.g. `SSLv3_method` +to force SSL version 3. The possible values depend on your installation of OpenSSL. Read the official OpenSSL docs for possible [SSL_METHODS](https://www.openssl.org/docs/man1.0.2/ssl/ssl.html). +* `ciphers` - [TLS](https://nodejs.org/api/tls.html#tls_modifying_the_default_tls_cipher_suite) list of TLS ciphers to override node's default. +The possible values depend on your installation of OpenSSL. Read the official OpenSSL docs for possible [TLS_CIPHERS](https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT). +* `downstreamResponseTime` - logs the time spent processing the downstream request using [process.hrtime](https://nodejs.org/api/process.html#process_process_hrtime_time). Defaults to `false`. + +## Usage + +As one of the handlers for hapi, it is used through the route configuration object. + +### `h.proxy(options)` + +Proxies the request to an upstream endpoint where: +- `options` - an object including the same keys and restrictions defined by the + [route `proxy` handler options](#options). + +No return value. + +The [response flow control rules](http://hapijs.com/api#flow-control) **do not** apply. + +```js +const handler = function (request, h) { + + return h.proxy({ host: 'example.com', port: 80, protocol: 'http' }); +}; +``` + +### Using the `host`, `port`, `protocol` options + +Setting these options will send the request to certain route to a specific upstream service with the same path as the original request. Cannot be used with `uri`, `mapUri`. + +```javascript +server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + host: '10.33.33.1', + port: '443', + protocol: 'https' + } + } +}); +``` + +### Using the `uri` option + +Setting this option will send the request to an absolute URI instead of the incoming host, port, protocol, path and query. Cannot be used with `host`, `port`, `protocol`, `mapUri`. + +```javascript +server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + uri: 'https://some.upstream.service.com/that/has?what=you&want=todo' + } + } +}); +``` +### Custom `uri` template values + +When using the `uri` option, there are optional **default** template values that can be injected from the incoming `request`: + +* `{protocol}` +* `{host}` +* `{port}` +* `{path}` + +```javascript +server.route({ + method: 'GET', + path: '/foo', + handler: { + proxy: { + uri: '{protocol}://{host}:{port}/go/to/{path}' + } + } +}); +``` +Requests to `http://127.0.0.1:8080/foo/` would be proxied to an upstream destination of `http://127.0.0.1:8080/go/to/foo` + + +Additionally, you can capture request.params values and inject them into the upstream uri value using a similar replacment strategy: +```javascript +server.route({ + method: 'GET', + path: '/foo/{bar}', + handler: { + proxy: { + uri: 'https://some.upstream.service.com/some/path/to/{bar}' + } + } +}); +``` +**Note** The default variables of `{protocol}`, `{host}`, `{port}`, `{path}` take precedence - it's best to treat those as reserved when naming your own `request.params`. + + +### Using the `mapUri` and `onResponse` options + +Setting both options with custom functions will allow you to map the original request to an upstream service and to processing the response from the upstream service, before sending it to the client. Cannot be used together with `host`, `port`, `protocol`, or `uri`. + +```javascript +server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + mapUri: function (request) { + + console.log('doing some additional stuff before redirecting'); + return { + uri: 'https://some.upstream.service.com/' + }; + }, + onResponse: async function (err, res, request, h, settings, ttl) { + + console.log('receiving the response from the upstream.'); + const payload = await Wreck.read(res, { json: true }); + + console.log('some payload manipulation if you want to.'); + const response = h.response(payload); + response.headers = res.headers; + return response; + } + } + } +}); + +``` + + +### Using a custom http client + +By default, `h2o2` uses Wreck to perform requests. A custom http client can be provided by passing a client to `httpClient`, as long as it abides by the [`wreck`](https://github.com/hapijs/wreck) interface. The two functions that `h2o2` utilizes are `request()` and `parseCacheControl()`. + +```javascript +server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + httpClient: { + request(method, uri, options) { + return axios({ + method, + url: 'https://some.upstream.service.com/' + }) + } + } + } + } +}); +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100755 index 6518aa6..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,16 +0,0 @@ -# How to contribute -We welcome contributions from the community and are pleased to have them. Please follow this guide when logging issues or making code changes. - -## Logging Issues -All issues should be created using the [new issue form](https://github.com/hapijs/h2o2/issues/new). Clearly describe the issue including steps -to reproduce if there are any. Also, make sure to indicate the earliest version that has the issue being reported. - -## Patching Code - -Code changes are welcome and should follow the guidelines below. - -* Fork the repository on GitHub. -* Fix the issue ensuring that your code follows the [style guide](https://github.com/hapijs/contrib/blob/master/Style.md). -* Add tests for your new code ensuring that you have 100% code coverage (we can help you reach 100% but will not merge without it). - * Run `npm test` to generate a report of test coverage -* [Pull requests](http://help.github.com/send-pull-requests/) should be made to the [master branch](https://github.com/hapijs/h2o2/tree/master). diff --git a/LICENSE b/LICENSE deleted file mode 100755 index d5f6de6..0000000 --- a/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -Copyright (c) 2012-2014, Walmart and other contributors. -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. - -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 -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - * * * - -The complete list of contributors can be found at: https://github.com/hapijs/h2o2/graphs/contributors diff --git a/LICENSE.md b/LICENSE.md new file mode 100755 index 0000000..0d96bf8 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +Copyright (c) 2012-2020, Sideway Inc, and project contributors +Copyright (c) 2012-2014, Walmart. +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. + +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. diff --git a/README.md b/README.md index ba2af3d..1b8346b 100644 --- a/README.md +++ b/README.md @@ -1,201 +1,25 @@ -#h2o2 + -Proxy handler plugin for hapi.js. +# @hapi/h2o2 -[![NPM](https://nodei.co/npm/h2o2.png?downloads=true&stars=true)](https://nodei.co/npm/h2o2/) +#### Proxy handler for hapi.js. -[![Build Status](https://secure.travis-ci.org/hapijs/h2o2.png)](http://travis-ci.org/hapijs/h2o2) +**h2o2** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together. -Lead Maintainer - [Oscar A. Funes Martinez](https://github.com/osukaa) +### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support -## Introduction +## Useful resources -**h2o2** is a hapi plugin that adds proxying functionality. +- [Documentation and API](https://hapi.dev/family/h2o2/) +- [Version status](https://hapi.dev/resources/status/#h2o2) (builds, dependencies, node versions, licenses, eol) +- [Changelog](https://hapi.dev/family/h2o2/changelog/) +- [Project policies](https://hapi.dev/policies/) +- [Free and commercial support options](https://hapi.dev/support/) -## Manual loading -Starting on version 9, `hapi` does not load the `h2o2` automatically. To add `h2o2` to your server, you should register it normally. +## Tagging new release -```javascript -const Hapi = require('hapi'); -const server = new Hapi.Server(); - -server.register({ - register: require('h2o2') -}, function (err) { - - if (err) { - console.log('Failed to load h2o2'); - } - - server.start(function (err) { - - console.log('Server started at: ' + server.info.uri); - }); -}); -``` -_**NOTE**: h2o2 is included with and loaded by default in Hapi < 9.0._ - -## Options - -The proxy handler object has the following properties: - -* `host` - upstream service host to proxy requests to. It will have the same path as the client request. -* `port` - upstream service port. -* `protocol` - protocol to use when making the request to the proxied host: - * 'http' - * 'https' -* `uri` - absolute URI used instead of host, port, protocol, path, and query. Cannot be used with `host`, `port`, `protocol`, or `mapUri`. -* `passThrough` - if set to `true`, it forwards the headers from the client to the upstream service, headers sent from the upstream service will also be forwarded to the client. Defaults to `false`. -* `localStatePassThrough` - if set to`false`, any locally defined state is removed from incoming requests before being sent to the upstream service. This value can be overridden on a per state basis via the `server.state()``passThrough` option. Defaults to `false` -* `acceptEncoding` - if set to `false`, does not pass-through the 'Accept-Encoding' HTTP header which is useful for the `onResponse` post-processing to avoid receiving an encoded response. Can only be used together with `passThrough`. Defaults to `true` (passing header). -* `rejectUnauthorized` - sets the `rejectUnauthorized` property on the https [agent](http://nodejs.org/api/https.html#https_https_request_options_callback) making the request. This value is only used when the proxied server uses TLS/SSL. If set it will override the node.js `rejectUnauthorized` property. If `false` then ssl errors will be ignored. When `true` the server certificate is verified and an 500 response will be sent when verification fails. This shouldn't be used alongside the `agent` setting as the `agent` will be used instead. Defaults to the https agent default value of `true`. -* `xforward` - if set to `true`, sets the 'X-Forwarded-For', 'X-Forwarded-Port', 'X-Forwarded-Proto', 'X-Forwarded-Host' headers when making a request to the proxied upstream endpoint. Defaults to `false`. -* `redirects` - the maximum number of HTTP redirections allowed to be followed automatically by the handler. Set to `false` or `0` to disable all redirections (the response will contain the redirection received from the upstream service). If redirections are enabled, no redirections (301, 302, 307, 308) will be passed along to the client, and reaching the maximum allowed redirections will return an error response. Defaults to `false`. -* `timeout` - number of milliseconds before aborting the upstream request. Defaults to `180000` (3 minutes). -* `mapUri` - a function used to map the request URI to the proxied URI. Cannot be used together with `host`, `port`, `protocol`, or `uri`. The function signature is `function (request, callback)` where: - * `request` - is the incoming [request object](http://hapijs.com/api#request-object). - * `callback` - is `function (err, uri, headers)` where: - * `err` - internal error condition. - * `uri` - the absolute proxy URI. - * `headers` - optional object where each key is an HTTP request header and the value is the header content. -* `onResponse` - a custom function for processing the response from the upstream service before sending to the client. Useful for custom error handling of responses from the proxied endpoint or other payload manipulation. Function signature is `function (err, res, request, reply, settings, ttl)` where: - * `err` - internal or upstream error returned from attempting to contact the upstream proxy. - * `res` - the node response object received from the upstream service. `res` is a readable stream (use the [wreck](https://github.com/hapijs/wreck) module `read` method to easily convert it to a Buffer or string). - * `request` - is the incoming [request object](http://hapijs.com/api#request-object). - * `reply` - the [reply interface](http://hapijs.com/api#reply-interface) function. - * `settings` - the proxy handler configuration. - * `ttl` - the upstream TTL in milliseconds if `proxy.ttl` it set to `'upstream'` and the upstream response included a valid 'Cache-Control' header with 'max-age'. -* `ttl` - if set to `'upstream'`, applies the upstream response caching policy to the response using the `response.ttl()` method (or passed as an argument to the `onResponse` method if provided). -* `agent` - a node [http(s) agent](http://nodejs.org/api/http.html#http_class_http_agent) to be used for connections to upstream server. -* `maxSockets` - sets the maximum number of sockets available per outgoing proxy host connection. `false` means use the **wreck** module default value (`Infinity`). Does not affect non-proxy outgoing client connections. Defaults to `Infinity`. - -## Usage - -As one of the handlers for hapi, it is used through the route configuration object. - -### `reply.proxy(options)` - -Proxies the request to an upstream endpoint where: -- `options` - an object including the same keys and restrictions defined by the - [route `proxy` handler options](#options). - -No return value. - -The [response flow control rules](http://hapijs.com/api#flow-control) **do not** apply. - -```js -const handler = function (request, reply) { - - return reply.proxy({ host: 'example.com', port: 80, protocol: 'http' }); -}; -``` - -### Using the `host`, `port`, `protocol` options - -Setting these options will send the request to certain route to a specific upstream service with the same path as the original request. Cannot be used with `uri`, `mapUri`. - -```javascript -server.route({ - method: 'GET', - path: '/', - handler: { - proxy: { - host: '10.33.33.1', - port: '443', - protocol: 'https' - } - } -}); -``` - -### Using the `uri` option - -Setting this option will send the request to an absolute URI instead of the incoming host, port, protocol, path and query. Cannot be used with `host`, `port`, `protocol`, `mapUri`. - -```javascript -server.route({ - method: 'GET', - path: '/', - handler: { - proxy: { - uri: 'https://some.upstream.service.com/that/has?what=you&want=todo' - } - } -}); -``` -### Custom `uri` template values - -When using the `uri` option, there are optional **default** template values that can be injected from the incoming `request`: - -* `{protocol}` -* `{host}` -* `{port}` -* `{path}` - -```javascript -server.route({ - method: 'GET', - path: '/foo', - handler: { - proxy: { - uri: '{protocol}://{host}:{port}/go/to/{path}' - } - } -}); -``` -Requests to `http://127.0.0.1:8080/foo/` would be proxied to an upstream destination of `http://127.0.0.1:8080/go/to/foo` - - -Additionally, you can capture request.params values and inject them into the upstream uri value using a similar replacment strategy: -```javascript -server.route({ - method: 'GET', - path: '/foo/{bar}', - handler: { - proxy: { - uri: 'https://some.upstream.service.com/some/path/to/{bar}' - } - } -}); -``` -**Note** The default variables of `{protocol}`, `{host}`, `{port}`, `{path}` take precedence - it's best to treat those as reserved when naming your own `request.params`. - - -### Using the `mapUri` and `onResponse` options - -Setting both options with custom functions will allow you to map the original request to an upstream service and to processing the response from the upstream service, before sending it to the client. Cannot be used together with `host`, `port`, `protocol`, or `uri`. - -```javascript -server.route({ - method: 'GET', - path: '/', - handler: { - proxy: { - mapUri: function (request, callback) { - - console.log('doing some aditional stuff before redirecting'); - callback(null, 'https://some.upstream.service.com/'); - }, - onResponse: function (err, res, request, reply, settings, ttl) { - - console.log('receiving the response from the upstream.'); - Wreck.read(res, { json: true }, function (err, payload) { - - console.log('some payload manipulation if you want to.') - reply(payload).headers = res.headers; - }); - } - } - } -}); - -``` - -### Tagging new release - -Before tagging make sure the version in package.json was bumped and commited +Before tagging make sure the version in package.json was bumped and committed ``` git tag -a 5.4.0-kibi-5 -m 'version 5.4.0-kibi-5' && git push origin 5.4.0-kibi-5 -``` \ No newline at end of file diff --git a/lib/index.js b/lib/index.js old mode 100644 new mode 100755 index d903a58..bc1ec15 --- a/lib/index.js +++ b/lib/index.js @@ -1,301 +1,217 @@ 'use strict'; -/*eslint brace-style: [2, "1tbs"]*/ - -// Load modules const Http = require('http'); const Https = require('https'); -const Hoek = require('hoek'); -const Joi = require('joi'); -const Wreck = require('wreck'); -const Boom = require('boom'); -const URL = require('url'); +const Hoek = require('@hapi/hoek'); +const Validate = require('@hapi/validate'); +const Wreck = require('@hapi/wreck'); +const URL = require('url'); -// Declare internals const internals = { - agents: {} // server.info.uri -> { http, https, insecure } + NS_PER_SEC: 1e9 }; internals.defaults = { + httpClient: { + request: Wreck.request.bind(Wreck), + parseCacheControl: Wreck.parseCacheControl.bind(Wreck) + }, xforward: false, passThrough: false, redirects: false, - timeout: 1000 * 60 * 3, // Timeout request after 3 minutes - localStatePassThrough: false, // Pass cookies defined by the server upstream - maxSockets: Infinity + timeout: 1000 * 60 * 3, // Timeout request after 3 minutes + localStatePassThrough: false, // Pass cookies defined by the server upstream + maxSockets: Infinity, + downstreamResponseTime: false }; -internals.schema = Joi.object({ - host: Joi.string(), - port: Joi.number().integer(), - protocol: Joi.string().valid('http', 'https', 'http:', 'https:'), - uri: Joi.string(), - passThrough: Joi.boolean(), - localStatePassThrough: Joi.boolean(), - acceptEncoding: Joi.boolean().when('passThrough', { is: true, otherwise: Joi.forbidden() }), - rejectUnauthorized: Joi.boolean(), - xforward: Joi.boolean(), - redirects: Joi.number().min(0).integer().allow(false), - timeout: Joi.number().integer(), - mapUri: Joi.func(), - onResponse: Joi.func(), - agent: Joi.object(), - ttl: Joi.string().valid('upstream').allow(null), - maxSockets: Joi.number().positive().allow(false), - - // added for kibi - onBeforeSendRequest: Joi.any() +internals.schema = Validate.object({ + httpClient: Validate.object({ + request: Validate.func(), + parseCacheControl: Validate.func() + }), + host: Validate.string(), + port: Validate.number().integer(), + protocol: Validate.string().valid('http', 'https', 'http:', 'https:'), + uri: Validate.string(), + passThrough: Validate.boolean(), + localStatePassThrough: Validate.boolean(), + acceptEncoding: Validate.boolean().when('passThrough', { is: true, otherwise: Validate.forbidden() }), + rejectUnauthorized: Validate.boolean(), + xforward: Validate.boolean(), + redirects: Validate.number().min(0).integer().allow(false), + timeout: Validate.number().integer(), + mapUri: Validate.func(), + onResponse: Validate.func(), + onRequest: Validate.func(), + agent: Validate.object(), + ttl: Validate.string().valid('upstream').allow(null), + maxSockets: Validate.number().positive().allow(false), + secureProtocol: Validate.string(), + ciphers: Validate.string(), + downstreamResponseTime: Validate.boolean(), + mapHttpClientOptions: Validate.func() }) -.xor('host', 'mapUri', 'uri') -.without('mapUri', 'port', 'protocol') -.without('uri', 'port', 'protocol'); - + .xor('host', 'mapUri', 'uri') + .without('mapUri', 'port') + .without('mapUri', 'protocol') + .without('uri', 'port') + .without('uri', 'protocol'); -exports.register = function (server, pluginOptions, next) { - server.handler('kibi_proxy', internals.handler); +exports.plugin = { + pkg: require('../package.json'), + requirements: { + hapi: '>=19.0.0' + }, - server.decorate('reply', 'kibi_proxy', function (options) { + register: function (server, options) { - internals.handler(this.request.route, options)(this.request, this); - }); + internals.defaults = Hoek.applyToDefaults(internals.defaults, options); - return next(); -}; - -exports.register.attributes = { - pkg: require('../package.json') + server.expose('_agents', new Map()); // server.info.uri -> { http, https, insecure } + server.decorate('handler', 'proxy', internals.handler); + server.decorate('toolkit', 'proxy', internals.toolkit); + } }; internals.handler = function (route, handlerOptions) { - Joi.assert(handlerOptions, internals.schema, 'Invalid proxy handler options (' + route.path + ')'); + const settings = Hoek.applyToDefaults(internals.defaults, handlerOptions, { shallow: ['agent'] }); + Validate.assert(handlerOptions, internals.schema, 'Invalid proxy handler options (' + route.path + ')'); Hoek.assert(!route.settings.payload || ((route.settings.payload.output === 'data' || route.settings.payload.output === 'stream') && !route.settings.payload.parse), 'Cannot proxy if payload is parsed or if output is not stream or data'); - const settings = Hoek.applyToDefaultsWithShallow(internals.defaults, handlerOptions, ['agent']); settings.mapUri = handlerOptions.mapUri || internals.mapUri(handlerOptions.protocol, handlerOptions.host, handlerOptions.port, handlerOptions.uri); - // kibi: added - const onBeforeSendRequest = handlerOptions.onBeforeSendRequest || false; - if (settings.ttl === 'upstream') { settings._upstreamTtl = true; } - return function (request, reply) { + return async function (request, h) { - settings.mapUri(request, (err, uri, headers) => { + const { uri, headers } = await settings.mapUri(request); - if (err) { - return reply(err); - } - - const protocol = uri.split(':', 1)[0]; - - const options = { - headers: {}, - payload: request.payload, - redirects: settings.redirects, - timeout: settings.timeout, - agent: internals.agent(protocol, settings, request.connection) - }; + const protocol = uri.split(':', 1)[0]; - const bind = request.route.settings.bind; + const options = { + headers: {}, + payload: request.payload, + redirects: settings.redirects, + timeout: settings.timeout, + agent: internals.agent(protocol, settings, request) + }; - if (settings.passThrough) { - options.headers = Hoek.clone(request.headers); - delete options.headers.host; + const bind = request.route.settings.bind; - if (settings.acceptEncoding === false) { // Defaults to true - delete options.headers['accept-encoding']; - } + if (settings.passThrough) { + options.headers = Hoek.clone(request.headers); + delete options.headers.host; + delete options.headers['content-length']; - if (options.headers.cookie) { - delete options.headers.cookie; + if (settings.acceptEncoding === false) { // Defaults to true + delete options.headers['accept-encoding']; + } - const cookieHeader = request.connection.states.passThrough(request.headers.cookie, settings.localStatePassThrough); - if (cookieHeader) { - if (typeof cookieHeader !== 'string') { - return reply(cookieHeader); // Error - } + if (options.headers.cookie) { + delete options.headers.cookie; - options.headers.cookie = cookieHeader; + const cookieHeader = request.server.states.passThrough(request.headers.cookie, settings.localStatePassThrough); + if (cookieHeader) { + if (typeof cookieHeader !== 'string') { + throw cookieHeader; // Error } + + options.headers.cookie = cookieHeader; } } + } - if (headers) { - Hoek.merge(options.headers, headers); - } + if (headers) { + Hoek.merge(options.headers, headers); + } - if (settings.xforward && - request.info.remotePort && - request.info.remoteAddress) { - options.headers['x-forwarded-for'] = (options.headers['x-forwarded-for'] ? options.headers['x-forwarded-for'] + ',' : '') + request.info.remoteAddress; - options.headers['x-forwarded-port'] = (options.headers['x-forwarded-port'] ? options.headers['x-forwarded-port'] + ',' : '') + request.info.remotePort; - options.headers['x-forwarded-proto'] = (options.headers['x-forwarded-proto'] ? options.headers['x-forwarded-proto'] + ',' : '') + request.connection.info.protocol; - options.headers['x-forwarded-host'] = (options.headers['x-forwarded-host'] ? options.headers['x-forwarded-host'] + ',' : '') + request.info.host; - } + if (settings.xforward && + request.info.remotePort) { - const contentType = request.headers['content-type']; - if (contentType) { - options.headers['content-type'] = contentType; - } + options.headers['x-forwarded-for'] = (options.headers['x-forwarded-for'] ? options.headers['x-forwarded-for'] + ',' : '') + request.info.remoteAddress; + options.headers['x-forwarded-port'] = options.headers['x-forwarded-port'] || request.info.remotePort; + options.headers['x-forwarded-proto'] = options.headers['x-forwarded-proto'] || request.server.info.protocol; + options.headers['x-forwarded-host'] = options.headers['x-forwarded-host'] || request.info.host; + } - const _onError = function (err, res, ttl) { + if (settings.ciphers) { + options.ciphers = settings.ciphers; + } - if (settings.onResponse) { - return settings.onResponse.call(bind, err, res, request, reply, settings, ttl); - } + if (settings.secureProtocol) { + options.secureProtocol = settings.secureProtocol; + } - return reply(err); - }; + const contentType = request.headers['content-type']; + if (contentType) { + options.headers['content-type'] = contentType; + } - // Encoding request path - const _encodeURL = (url) => { + let ttl = null; - const tempUri = URL.parse(url); - if (tempUri.pathname && decodeURI(tempUri.pathname) === tempUri.pathname) { - tempUri.pathname = encodeURI(tempUri.pathname); - return URL.format(tempUri); - } - return url; - }; + let downstreamStartTime; + if (settings.downstreamResponseTime) { + downstreamStartTime = process.hrtime(); + } + + if (settings.mapHttpClientOptions) { + Hoek.merge(options, await settings.mapHttpClientOptions(request)); + } - uri = _encodeURL(uri); + const promise = settings.httpClient.request(request.method, internals.encodeUri(uri), options); - const _sendRequest = function () { + if (settings.onRequest) { + settings.onRequest(promise.req); + } - Wreck.request(request.method, uri, options, (err, res) => { + try { + var res = await promise; + if (settings.downstreamResponseTime) { + const downstreamResponseTime = process.hrtime(downstreamStartTime); + request.log(['h2o2', 'success'], { downstreamResponseTime: downstreamResponseTime[0] * internals.NS_PER_SEC + downstreamResponseTime[1] }); + } + } + catch (err) { + if (settings.downstreamResponseTime) { + const downstreamResponseTime = process.hrtime(downstreamStartTime); + request.log(['h2o2', 'error'], { downstreamResponseTime: downstreamResponseTime[0] * internals.NS_PER_SEC + downstreamResponseTime[1] }); + } - let ttl = null; + if (settings.onResponse) { + return settings.onResponse.call(bind, err, res, request, h, settings, ttl); + } - if (err) { - if (settings.onResponse) { - return settings.onResponse.call(bind, err, res, request, reply, settings, ttl); - } - return reply(err); - } + throw err; + } - if (settings._upstreamTtl) { - const cacheControlHeader = res.headers['cache-control']; - if (cacheControlHeader) { - const cacheControl = Wreck.parseCacheControl(cacheControlHeader); - if (cacheControl) { - ttl = cacheControl['max-age'] * 1000; - } - } - } + if (settings._upstreamTtl) { + const cacheControlHeader = res.headers['cache-control']; + if (cacheControlHeader) { + const cacheControl = settings.httpClient.parseCacheControl(cacheControlHeader); + if (cacheControl) { + ttl = cacheControl['max-age'] * 1000; + } + } + } - if (settings.onResponse) { - return settings.onResponse.call(bind, null, res, request, reply, settings, ttl); - } + if (settings.onResponse) { + return settings.onResponse.call(bind, null, res, request, h, settings, ttl); + } - return reply(res) - .ttl(ttl) - .code(res.statusCode) - .passThrough(!!settings.passThrough); // Default to false - }); - }; + return h.response(res) + .ttl(ttl) + .code(res.statusCode) + .passThrough(!!settings.passThrough); - // Send request - if (onBeforeSendRequest) { - onBeforeSendRequest(request) - .then((requestUpdates) => { - - Hoek.merge(options, requestUpdates); - - // Note: - // Below change is to mitigate federate bug - unnecessary overhead in schema computation phase - // on indices with many shards - // - // We do NOT send requests without joins to /siren endpoint - if (requestUpdates.payload) { - const payload = requestUpdates.payload.toString(); - const myUri = URL.parse(uri); - let containJoin = false; - let isSearchOrMsearch = false; - - const reviver = function (key, value) { - - if (key === 'join' && value.on !== undefined) { - containJoin = true; - } - return value; - }; - - if (myUri.pathname && myUri.pathname.indexOf('/_msearch') !== -1) { - isSearchOrMsearch = true; - const lines = payload.split('\n'); - for (let i = 1; i < lines.length - 1; i = i + 2 ) { - if (containJoin) { - continue; - } - if (lines[i] !== '') { - try { - JSON.parse(lines[i], reviver); - } catch (e) { - console.log('Error parsing _msearch request payload [' + lines[i] + ']', e); - } - } - } - } else if (myUri.pathname && myUri.pathname.indexOf('/_search') !== -1){ - isSearchOrMsearch = true; - if (payload !== '') { - try { - JSON.parse(payload, reviver); - } catch (e) { - console.log('Error parsing _search request payload [' + payload + ']', e); - } - } - } - - if (isSearchOrMsearch && !containJoin) { - const pathWithNoSiren = myUri.pathname.replace(/^\/siren\//, '/'); - myUri.pathname = pathWithNoSiren; - - let search = myUri.search; - if (search && search.indexOf('preference')) { - if (search.startsWith('?')) { - search = search.substring(1); - } - const pairs = search.split('&'); - for (let i = pairs.length - 1; i >= 0; i = i - 1) { - if (pairs[i].startsWith('preference=')) { - pairs.splice(i, 1); - } - } - search = pairs.join('&'); - myUri.search = search; - } - - try { - uri = URL.format(myUri); - } catch (e) { - console.log('Error when formatting the URL', e); - } - } - } - // end of mitigation code - - _sendRequest(); - }) - .catch((err) => { - - let msg = 'Failed request'; - if (err.message) { - msg = err.message; - } - return _onError(Boom.badRequest(msg, err)); - }); - } else { - _sendRequest(); - } - }); }; }; @@ -312,47 +228,59 @@ internals.handler.defaults = function (method) { }; +internals.toolkit = function (options) { + + return internals.handler(this.request.route, options)(this.request, this); +}; + + internals.mapUri = function (protocol, host, port, uri) { if (uri) { - return function (request, next) { + return function (request) { if (uri.indexOf('{') === -1) { - return next(null, uri); + return { uri }; } - let address = uri.replace(/{protocol}/g, request.connection.info.protocol) - .replace(/{host}/g, request.connection.info.host) - .replace(/{port}/g, request.connection.info.port) - .replace(/{path}/g, request.url.path); + let address = uri.replace(/{protocol}/g, request.server.info.protocol) + .replace(/{host}/g, request.server.info.host) + .replace(/{port}/g, request.server.info.port) + .replace(/{path}/g, request.path); Object.keys(request.params).forEach((key) => { - const re = new RegExp(`{${key}}`,'g'); - address = address.replace(re,request.params[key]); + const re = new RegExp(`{${key}}`, 'g'); + address = address.replace(re, request.params[key]); }); - return next(null, address); + return { + uri: address + }; }; } - if (protocol && protocol[protocol.length - 1] !== ':') { + if (protocol && + protocol[protocol.length - 1] !== ':') { protocol += ':'; } protocol = protocol || 'http:'; + port = port || (protocol === 'http:' ? 80 : 443); const baseUrl = protocol + '//' + host + ':' + port; - return function (request, next) { + return function (request) { - return next(null, baseUrl + request.path + (request.url.search || '')); + return { + uri: (null, baseUrl + request.path + (request.url.search || '')) + }; }; }; -internals.agent = function (protocol, settings, connection) { +internals.agent = function (protocol, settings, request) { if (settings.agent) { return settings.agent; @@ -362,8 +290,12 @@ internals.agent = function (protocol, settings, connection) { return undefined; } - internals.agents[connection.info.uri] = internals.agents[connection.info.uri] || {}; - const agents = internals.agents[connection.info.uri]; + const store = request.server.plugins.h2o2._agents; + if (!store.has(request.info.uri)) { + store.set(request.info.uri, {}); + } + + const agents = store.get(request.info.uri); const type = (protocol === 'http' ? 'http' : (settings.rejectUnauthorized === false ? 'insecure' : 'https')); if (!agents[type]) { @@ -373,3 +305,15 @@ internals.agent = function (protocol, settings, connection) { return agents[type]; }; + + +internals.encodeUri = function (uri) { + + const tempUri = URL.parse(uri); + if (decodeURI(tempUri.pathname) === tempUri.pathname) { + tempUri.pathname = encodeURI(tempUri.pathname); + return URL.format(tempUri); + } + + return uri; +}; diff --git a/package.json b/package.json index 4c62ec6..6a88fac 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,15 @@ { - "name": "kibi-h2o2", - "description": "Kibi fork of proxy handler plugin for hapi.js", - "version": "5.4.0-kibi-5", + "name": "@sirensolutions/h2o2", + "description": "Fork of proxy handler plugin for hapi.js that adds ability to modify request", + "version": "9.0.2-kibi-1", "repository": "git://github.com/sirensolutions/h2o2", "main": "lib/index.js", + "engines": { + "node": ">=12.0.0" + }, + "files": [ + "lib" + ], "keywords": [ "HTTP", "proxy", @@ -11,25 +17,21 @@ "hapi", "plugin" ], - "engines": { - "node": ">=4.0.0" - }, "dependencies": { - "boom": "3.x.x", - "hoek": "4.x.x", - "joi": "9.x.x", - "wreck": "9.x.x" + "@hapi/boom": "9.x.x", + "@hapi/hoek": "9.x.x", + "@hapi/validate": "1.x.x", + "@hapi/wreck": "17.x.x" }, "devDependencies": { - "code": "3.x.x", - "hapi": "14.x.x", - "inert": "4.x.x", - "lab": "11.x.x", - "bluebird": "3.1.1" + "@hapi/code": "8.x.x", + "@hapi/hapi": "20.x.x", + "@hapi/inert": "6.x.x", + "@hapi/lab": "24.x.x" }, "scripts": { - "test": "lab -a code -t 100 -L", - "test-cov-html": "lab -a code -r html -o coverage.html" + "test": "lab -a @hapi/code -t 100 -L", + "test-cov-html": "lab -a @hapi/code -r html -o coverage.html" }, "license": "BSD-3-Clause" } diff --git a/test/index.js b/test/index.js old mode 100644 new mode 100755 index 2553adf..6d0e914 --- a/test/index.js +++ b/test/index.js @@ -1,314 +1,135 @@ 'use strict'; -// Load modules const Fs = require('fs'); const Http = require('http'); const Net = require('net'); const Zlib = require('zlib'); -const Boom = require('boom'); -const Code = require('code'); -const H2o2 = require('..'); -const Hapi = require('hapi'); -const Hoek = require('hoek'); -const Lab = require('lab'); -const Wreck = require('wreck'); -const Promise = require('bluebird'); +const Boom = require('@hapi/boom'); +const Code = require('@hapi/code'); +const H2o2 = require('..'); +const Hapi = require('@hapi/hapi'); +const Hoek = require('@hapi/hoek'); +const Inert = require('@hapi/inert'); +const Lab = require('@hapi/lab'); +const Wreck = require('@hapi/wreck'); -// Declare internals const internals = {}; -// Test shortcuts - -const lab = exports.lab = Lab.script(); -const describe = lab.describe; -const it = lab.it; +const { it, describe } = exports.lab = Lab.script(); const expect = Code.expect; -describe('H2o2', () => { +describe('h2o2', () => { const tlsOptions = { - key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n', - cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n' - }; - - const provisionServer = function (options) { - - const server = new Hapi.Server(); - server.connection(options); - server.register(H2o2, Hoek.ignore); - return server; + key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA3IDFzxorKO8xWeCOosuK1pCPoTUMlhOkis4pWO9CLCv0o0Q7\nyUCZlHzPYWM49+QmWe5u3Xbl1rhkFsoeYowH1bts5r6HY8xYHexvU+6zEyxOU4Q7\nP7EXkFfW5h7WsO6uaEyEBVdniTIjK4c8hzjy7h6hNIvM+kEAAy1UFatMKmOwsp4Z\ns4+oCmS4ZPlItAMbRv/4a5DCopluOS7WN8UwwJ6zRrY8ZVFnkKPThflnwiaIy2Qh\nGgTwLANIUlWPQMh+LLHnV56NOlj1VUO03G+pKxTJ6ZkfYefaD41Ez4iPc7nyg4iD\njqnqFX+jYOLRoCktztYd9T43Sgb2sfgrlY0ENwIDAQABAoIBAQCoznyg/CumfteN\nMvh/cMutT6Zlh7NHAWqqSQImb6R9JHl4tDgA7k+k+ZfZuphWTnd9yadeLDPwmeEm\nAT4Zu5IT8hSA4cPMhxe+cM8ZtlepifW8wjKJpA2iF10RdvJtKYyjlFBNtogw5A1A\nuZuA+fwgh5pqG8ykmTZlOEJzBFye5Z7xKc/gwy9BGv3RLNVf+yaJCqPKLltkAxtu\nFmrBLuIZMoOJvT+btgVxHb/nRVzURKv5iKMY6t3JM84OSxNn0/tHpX2xTcqsVre+\nsdSokKGYoyzk/9miDYhoSVOrM3bU5/ygBDt1Pmf/iyK/MDO2P9tX9cEp/+enJc7a\nLg5O/XCBAoGBAPNwayF6DLu0PKErsdCG5dwGrxhC69+NBEJkVDMPMjSHXAQWneuy\n70H+t2QHxpDbi5wMze0ZClMlgs1wItm4/6iuvOn9HJczwiIG5yM9ZJo+OFIqlBq3\n1vQG+oEXe5VpTfpyQihxqTSiMuCXkTYtNjneHseXWAjFuUQe9AOxxzNRAoGBAOfh\nZEEDY7I1Ppuz7bG1D6lmzYOTZZFfMCVGGTrYmam02+rS8NC+MT0wRFCblQ0E7SzM\nr9Bv2vbjrLY5fCe/yscF+/u/UHJu1dR7j62htdYeSi7XbQiSwyUm1QkMXjKDQPUw\njwR3WO8ZHQf2tywE+7iRs/bJ++Oolaw03HoIp40HAoGBAJJwGpGduJElH5+YCDO3\nIghUIPnIL9lfG6PQdHHufzXoAusWq9J/5brePXU31DOJTZcGgM1SVcqkcuWfwecU\niP3wdwWOU6eE5A/R9TJWmPDL4tdSc5sK4YwTspb7CEVdfiHcn31yueVGeLJvmlNr\nqQXwXrWTjcphHkwjDog2ZeyxAoGBAJ5Yyq+i8uf1eEW3v3AFZyaVr25Ur51wVV5+\n2ifXVkgP28YmOpEx8EoKtfwd4tE7NgPL25wJZowGuiDObLxwOrdinMszwGoEyj0K\nC/nUXmpT0PDf5/Nc1ap/NCezrHfuLePCP0gbgD329l5D2p5S4NsPlMfI8xxqOZuZ\nlZ44XsLtAoGADiM3cnCZ6x6/e5UQGfXa6xN7KoAkjjyO+0gu2AF0U0jDFemu1BNQ\nCRpe9zVX9AJ9XEefNUGfOI4bhRR60RTJ0lB5Aeu1xAT/OId0VTu1wRrbcnwMHGOo\nf7Kk1Vk5+1T7f1QbTu/q4ddp22PEt2oGJ7widRTZrr/gtH2wYUEjMVQ=\n-----END RSA PRIVATE KEY-----\n', + cert: '-----BEGIN CERTIFICATE-----\nMIIC+zCCAeOgAwIBAgIJANnDRcmEqJssMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV\nBAMMCWxvY2FsaG9zdDAeFw0xNzA5MTIyMjMxMDRaFw0yNzA5MTAyMjMxMDRaMBQx\nEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBANyAxc8aKyjvMVngjqLLitaQj6E1DJYTpIrOKVjvQiwr9KNEO8lAmZR8z2Fj\nOPfkJlnubt125da4ZBbKHmKMB9W7bOa+h2PMWB3sb1PusxMsTlOEOz+xF5BX1uYe\n1rDurmhMhAVXZ4kyIyuHPIc48u4eoTSLzPpBAAMtVBWrTCpjsLKeGbOPqApkuGT5\nSLQDG0b/+GuQwqKZbjku1jfFMMCes0a2PGVRZ5Cj04X5Z8ImiMtkIRoE8CwDSFJV\nj0DIfiyx51eejTpY9VVDtNxvqSsUyemZH2Hn2g+NRM+Ij3O58oOIg46p6hV/o2Di\n0aApLc7WHfU+N0oG9rH4K5WNBDcCAwEAAaNQME4wHQYDVR0OBBYEFJBSho+nF530\nsxpoBxYqD/ynn/t0MB8GA1UdIwQYMBaAFJBSho+nF530sxpoBxYqD/ynn/t0MAwG\nA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJFAh3X5CYFAl0cI6Q7Vcp4H\nO0S8s/C4FHNIsyUu54NcRH3taUwn3Fshn5LiwaEdFmouALbxMaejvEVw7hVBtY9X\nOjqt0mZ6+X6GOFhoUvlaG1c7YLOk5x51TXchg8YD2wxNXS0rOrAdZaScOsy8Q62S\nHehBJMN19JK8TiR3XXzxKVNcFcg0wyQvCGgjrHReaUF8WePfWHtZDdP01kBmMEIo\n6wY7E3jFqvDUs33vTOB5kmWixIoJKmkgOVmbgchmu7z27n3J+fawNr2r4IwjdUpK\nc1KvFYBXLiT+2UVkOJbBZ3C8mKfhXKHs2CrI3cSa4+E0sxTy4joG/yzlRs5l954=\n-----END CERTIFICATE-----\n' }; - it('overrides maxSockets', { parallel: false }, (done) => { - - const orig = Wreck.request; - Wreck.request = function (method, uri, options, callback) { - - Wreck.request = orig; - expect(options.agent.maxSockets).to.equal(213); - done(); - }; - - const server = provisionServer(); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { host: 'localhost', maxSockets: 213 } } }); - server.inject('/', (res) => { }); - }); + it('overrides maxSockets', { parallel: false }, async () => { - it('pre/post-process the request', { parallel: false }, (done) => { + let maxSockets; + const httpClient = { + request(method, uri, options, callback) { - const dataHandler = function (request, reply) { + maxSockets = options.agent.maxSockets; - return reply(request.payload); + return { statusCode: 200 }; + } }; - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.route({ method: 'POST', path: '/data', handler: dataHandler }); - upstream.start(() => { - - const server = provisionServer(); - server.route({ - method: 'POST', - path: '/data', - handler: { - kibi_proxy: { - host: 'localhost', - port: upstream.info.port, - onBeforeSendRequest: (request) => { - - const req = request.raw.req; - return new Promise((fulfill, reject) => { - - Wreck.read(req, null, (error, payload) => { - - if (error) { - reject(error); - } + const server = Hapi.server(); + await server.register(H2o2); - const body = JSON.parse(payload.toString()); - body.copy = body.msg; - fulfill({ data: 'connor', payload: Buffer.from(JSON.stringify(body)) }); - }); - }); - }, - onResponse: (error, response, request, reply, settings, ttl) => { + server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'localhost', httpClient, maxSockets: 213 } } }); + await server.inject('/'); + expect(maxSockets).to.equal(213); + }); - if (error) { - reply(error); - } + it('uses node default with maxSockets set to false', { parallel: false }, async () => { - Wreck.read(response, null, (err, payload) => { + let agent; + const httpClient = { + request(method, uri, options) { - if (err) { - reply(err); - } + agent = options.agent; - const body = JSON.parse(payload.toString()); - body.copy = body.copy.toUpperCase(); - reply(Buffer.from(JSON.stringify(body))); - }); - } - } - } - }); + return { statusCode: 200 }; + } + }; - server.inject({ - method: 'POST', - url: '/data', - payload: JSON.stringify({ msg: 'hello' }) - }, - (res) => { + const server = Hapi.server(); + await server.register(H2o2); - expect(res.payload).to.equal(JSON.stringify({ msg: 'hello', copy: 'HELLO' })); - done(); - }); - }); + server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'localhost', httpClient, maxSockets: false } } }); + await server.inject('/'); + expect(agent).to.equal(undefined); }); - it('onBeforeSendRequest the request with error', { parallel: false }, (done) => { + it('forwards on the response when making a GET request', async () => { - const dataHandler = function (request, reply) { + const profileHandler = function (request, h) { - return reply(request.payload); + return h.response({ id: 'fa0dbda9b1b', name: 'John Doe' }).state('test', '123'); }; - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.route({ method: 'POST', path: '/data', handler: dataHandler }); - upstream.start(() => { - - const server = provisionServer(); - server.route({ - method: 'POST', - path: '/data', - handler: { - kibi_proxy: { - host: 'localhost', - port: upstream.info.port, - onBeforeSendRequest: (request) => { - - const req = request.raw.req; - return new Promise((fulfill, reject) => { + const upstream = Hapi.server(); + upstream.route({ method: 'GET', path: '/profile', handler: profileHandler, config: { cache: { expiresIn: 2000, privacy: 'private' } } }); + await upstream.start(); - Wreck.read(req, null, (error, payload) => { + const server = Hapi.server(); + await server.register(H2o2); - if (error) { - reject(error); - } + server.route({ method: 'GET', path: '/profile', handler: { proxy: { host: 'localhost', port: upstream.info.port, xforward: true, passThrough: true } } }); + server.state('auto', { autoValue: 'xyz' }); - reject(new Error('some error')); - }); - }); - } - } - } - }); + const response = await server.inject('/profile'); + expect(response.statusCode).to.equal(200); + expect(response.payload).to.contain('John Doe'); + expect(response.headers['set-cookie'][0]).to.include(['test=123']); + expect(response.headers['set-cookie'][1]).to.include(['auto=xyz']); + expect(response.headers['cache-control']).to.equal('max-age=2, must-revalidate, private'); - server.inject({ - method: 'POST', - url: '/data', - payload: JSON.stringify({ msg: 'hello' }) - }, - (res) => { + const res = await server.inject('/profile'); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.contain('John Doe'); - const payload = JSON.parse(res.payload); - expect(payload.statusCode).to.equal(400); - expect(payload.message).to.equal('some error'); - done(); - }); - }); + await upstream.stop(); }); - it('onResponse the request with error', { parallel: false }, (done) => { - - const dataHandler = function (request, reply) { + it('forwards on the response when making an OPTIONS request', async () => { - return reply(request.payload); - }; - - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.route({ method: 'POST', path: '/data', handler: dataHandler }); - upstream.start(() => { - - const server = provisionServer(); - server.route({ - method: 'POST', - path: '/data', - handler: { - kibi_proxy: { - host: 'localhost', - port: upstream.info.port, - onBeforeSendRequest: (request) => { - - const req = request.raw.req; - return new Promise((fulfill, reject) => { - - const chunks = []; - req.on('error', reject); - req.on('data', (chunk) => { - - chunks.push(chunk); - }); - req.on('end', () => { - - const body = JSON.parse(Buffer.concat(chunks)); - body.copy = body.msg; - const buffer = Buffer.from(JSON.stringify(body)); - fulfill({ data: 'connor', payload: buffer }); - }); - }); - }, - onResponse: (error, response, request, reply, settings, ttl, data) => { - - if (error) { - return reply(error); - } - - response.on('error', (err) => { - - return reply(err); - }); - response.on('data', (chunk) => {}); - response.on('end', () => { - - return reply(Boom.badRequest('some error', new Error())); - }); - } - } - } - }); + const upstream = Hapi.server(); + upstream.route({ method: 'OPTIONS', path: '/', handler: () => 'test' }); + await upstream.start(); - server.inject({ - method: 'POST', - url: '/data', - payload: JSON.stringify({ msg: 'hello' }) - }, - (res) => { + const server = Hapi.server(); + await server.register(H2o2); - const payload = JSON.parse(res.payload); - expect(payload.statusCode).to.equal(400); - expect(payload.message).to.equal('some error'); - done(); - }); + server.route({ + method: 'OPTIONS', + path: '/', + options: { + payload: { parse: false }, + handler: (request, h) => h.proxy({ host: 'localhost', port: upstream.info.port }) + } }); - }); - - it('uses node default with maxSockets set to false', { parallel: false }, (done) => { - const orig = Wreck.request; - Wreck.request = function (method, uri, options, callback) { - - Wreck.request = orig; - expect(options.agent).to.equal(undefined); - done(); - }; + const res = await server.inject({ method: 'OPTIONS', url: '/' }); + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('test'); - const server = provisionServer(); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { host: 'localhost', maxSockets: false } } }); - server.inject('/', (res) => { }); + await upstream.stop(); }); - it('forwards on the response when making a GET request', (done) => { + it('throws when used with explicit route payload config other than data or steam', async () => { - const profile = function (request, reply) { + const server = Hapi.server(); + await server.register(H2o2); - reply({ id: 'fa0dbda9b1b', name: 'John Doe' }).state('test', '123'); - }; - - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.route({ method: 'GET', path: '/profile', handler: profile, config: { cache: { expiresIn: 2000 } } }); - upstream.start(() => { - - const server = provisionServer(); - server.route({ method: 'GET', path: '/profile', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, xforward: true, passThrough: true } } }); - server.state('auto', { autoValue: 'xyz' }); - - server.inject('/profile', (response) => { - - expect(response.statusCode).to.equal(200); - expect(response.payload).to.contain('John Doe'); - expect(response.headers['set-cookie']).to.equal(['test=123', 'auto=xyz']); - expect(response.headers['cache-control']).to.equal('max-age=2, must-revalidate, private'); - - server.inject('/profile', (res) => { - - expect(res.statusCode).to.equal(200); - expect(res.payload).to.contain('John Doe'); - done(); - }); - }); - }); - }); - - it('throws when used with explicit route payload config other than data or steam', (done) => { - - const server = provisionServer(); expect(() => { server.route({ @@ -316,7 +137,7 @@ describe('H2o2', () => { path: '/', config: { handler: { - kibi_proxy: { host: 'example.com' } + proxy: { host: 'example.com' } }, payload: { output: 'file' @@ -324,12 +145,13 @@ describe('H2o2', () => { } }); }).to.throw('Cannot proxy if payload is parsed or if output is not stream or data'); - done(); }); - it('throws when setup with invalid options', (done) => { + it('throws when setup with invalid options', async () => { + + const server = Hapi.server(); + await server.register(H2o2); - const server = provisionServer(); expect(() => { server.route({ @@ -337,17 +159,18 @@ describe('H2o2', () => { path: '/', config: { handler: { - kibi_proxy: { some: 'key' } + proxy: {} } } }); }).to.throw(/\"value\" must contain at least one of \[host, mapUri, uri\]/); - done(); }); - it('throws when used with explicit route payload parse config set to false', (done) => { + it('throws when used with explicit route payload parse config set to false', async () => { + + const server = Hapi.server(); + await server.register(H2o2); - const server = provisionServer(); expect(() => { server.route({ @@ -355,7 +178,7 @@ describe('H2o2', () => { path: '/', config: { handler: { - kibi_proxy: { host: 'example.com' } + proxy: { host: 'example.com' } }, payload: { parse: true @@ -363,12 +186,13 @@ describe('H2o2', () => { } }); }).to.throw('Cannot proxy if payload is parsed or if output is not stream or data'); - done(); }); - it('allows when used with explicit route payload output data config', (done) => { + it('allows when used with explicit route payload output data config', async () => { + + const server = Hapi.server(); + await server.register(H2o2); - const server = provisionServer(); expect(() => { server.route({ @@ -376,7 +200,7 @@ describe('H2o2', () => { path: '/', config: { handler: { - kibi_proxy: { host: 'example.com' } + proxy: { host: 'example.com' } }, payload: { output: 'data' @@ -384,403 +208,413 @@ describe('H2o2', () => { } }); }).to.not.throw(); - done(); }); - it('uses protocol without ":"', (done) => { + it('uses protocol without ":"', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', - handler: function (request, reply) { + handler: function (request, h) { - return reply('ok'); + return 'ok'; } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, protocol: 'http' } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/', (res) => { + server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'localhost', port: upstream.info.port, protocol: 'http' } } }); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.equal('ok'); - done(); - }); - }); + const res = await server.inject('/'); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('ok'); + + await upstream.stop(); }); - it('forwards upstream headers', (done) => { + it('forwards upstream headers', async () => { - const headers = function (request, reply) { + const headers = function (request, h) { - reply({ status: 'success' }) + return h.response({ status: 'success' }) .header('Custom1', 'custom header value 1') - .header('X-Custom2', 'custom header value 2'); + .header('X-Custom2', 'custom header value 2') + .header('x-hostFound', request.headers.host) + .header('x-content-length-found', request.headers['content-length']); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/headers', handler: headers }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer({ routes: { cors: true } }); - server.route({ method: 'GET', path: '/headers', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, passThrough: true } } }); + const server = Hapi.server({ routes: { cors: true } }); + await server.register(H2o2); - server.inject('/headers', (res) => { + server.route({ method: 'GET', path: '/headers', handler: { proxy: { host: 'localhost', port: upstream.info.port, passThrough: true } } }); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.equal('{\"status\":\"success\"}'); - expect(res.headers.custom1).to.equal('custom header value 1'); - expect(res.headers['x-custom2']).to.equal('custom header value 2'); - done(); - }); + const res = await server.inject({ + url: '/headers', + headers: { + host: 'www.h2o2.com', 'content-length': 10000 + } }); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('{\"status\":\"success\"}'); + expect(res.headers.custom1).to.equal('custom header value 1'); + expect(res.headers['x-custom2']).to.equal('custom header value 2'); + expect(res.headers['x-hostFound']).to.equal(undefined); + expect(res.headers['x-content-length-found']).to.equal(undefined); + + await upstream.stop(); }); - // it('overrides upstream cors headers', (done) => { - // - // const headers = function (request, reply) { - // - // reply().header('access-control-allow-headers', 'Invalid, List, Of, Values'); - // }; - // - // const upstream = new Hapi.Server(); - // upstream.connection(); - // upstream.route({ method: 'GET', path: '/', handler: headers }); - // upstream.start(function () { - // - // const server = provisionServer({ routes: { cors: { credentials: true } } }); - // server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, passThrough: true } } }); - // - // server.inject('/', (res) => { - // - // expect(res.headers['access-control-allow-headers']).to.equal('Invalid, List, Of, Values'); - // done(); - // }); - // }); - // }); - - it('merges upstream headers', (done) => { - - const headers = function (request, reply) { - - reply({ status: 'success' }) + it('merges upstream headers', async () => { + + const handler = function (request, h) { + + return h.response({ status: 'success' }) .vary('X-Custom3'); }; - const onResponse = function (err, res, request, reply, settings, ttl) { + const onResponse = function (err, res, request, h, settings, ttl) { expect(err).to.be.null(); - reply(res).vary('Something'); + return h.response(res).vary('Something'); }; - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.route({ method: 'GET', path: '/headers', handler: headers }); - upstream.start(() => { + const upstream = Hapi.server(); + upstream.route({ method: 'GET', path: '/headers', handler }); + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/headers', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, onResponse } } }); - server.inject({ url: '/headers', headers: { 'accept-encoding': 'gzip' } }, (res) => { + const server = Hapi.server(); + await server.register(H2o2); - expect(res.statusCode).to.equal(200); - expect(res.headers.vary).to.equal('X-Custom3,accept-encoding,Something'); - done(); - }); - }); + server.route({ method: 'GET', path: '/headers', handler: { proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, onResponse } } }); + + const res = await server.inject({ url: '/headers', headers: { 'accept-encoding': 'gzip' } }); + expect(res.statusCode).to.equal(200); + //expect(res.headers.vary).to.equal('X-Custom3,accept-encoding,Something'); + + await upstream.stop(); }); - it('forwards gzipped content', (done) => { + it('forwards gzipped content', async () => { - const gzipHandler = function (request, reply) { + const gzipHandler = function (request, h) { - reply('123456789012345678901234567890123456789012345678901234567890'); + return h.response('123456789012345678901234567890123456789012345678901234567890'); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server({ compression: { minBytes: 1 } }); // Payloads under 1kb will not be compressed upstream.route({ method: 'GET', path: '/gzip', handler: gzipHandler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/gzip', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, passThrough: true } } }); + const server = Hapi.server(); + await server.register(H2o2); - Zlib.gzip(Buffer.from('123456789012345678901234567890123456789012345678901234567890'), (err, zipped) => { + server.route({ method: 'GET', path: '/gzip', handler: { proxy: { host: 'localhost', port: upstream.info.port, passThrough: true } } }); - expect(err).to.not.exist(); + const zipped = await Zlib.gzipSync(Buffer.from('123456789012345678901234567890123456789012345678901234567890')); + const res = await server.inject({ url: '/gzip', headers: { 'accept-encoding': 'gzip' } }); - server.inject({ url: '/gzip', headers: { 'accept-encoding': 'gzip' } }, (res) => { + expect(res.statusCode).to.equal(200); + expect(res.rawPayload).to.equal(zipped); - expect(res.statusCode).to.equal(200); - expect(res.rawPayload).to.equal(zipped); - done(); - }); - }); - }); + await upstream.stop(); }); - it('forwards gzipped stream', (done) => { + it('forwards gzipped stream', async () => { - const gzipStreamHandler = function (request, reply) { + const gzipStreamHandler = function (request, h) { - reply.file(__dirname + '/../package.json'); + return h.file(__dirname + '/../package.json'); }; - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.register(require('inert'), Hoek.ignore); + const upstream = Hapi.server({ compression: { minBytes: 1 } }); + await upstream.register(Inert); upstream.route({ method: 'GET', path: '/gzipstream', handler: gzipStreamHandler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/gzipstream', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, passThrough: true } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject({ url: '/gzipstream', headers: { 'accept-encoding': 'gzip' } }, (res) => { + server.route({ method: 'GET', path: '/gzipstream', handler: { proxy: { host: 'localhost', port: upstream.info.port, passThrough: true } } }); - expect(res.statusCode).to.equal(200); + const res = await server.inject({ url: '/gzipstream', headers: { 'accept-encoding': 'gzip' } }); + const file = Fs.readFileSync(__dirname + '/../package.json', { encoding: 'utf8' }); + const unzipped = Zlib.unzipSync(res.rawPayload); - Fs.readFile(__dirname + '/../package.json', { encoding: 'utf8' }, (err, file) => { - - expect(err).to.be.null(); - Zlib.unzip(res.rawPayload, (err, unzipped) => { + expect(unzipped.toString('utf8')).to.equal(file); + expect(res.statusCode).to.equal(200); - expect(err).to.not.exist(); - expect(unzipped.toString('utf8')).to.equal(file); - done(); - }); - }); - }); - }); + await upstream.stop(); }); - it('does not forward upstream headers without passThrough', (done) => { + it('does not forward upstream headers without passThrough', async () => { - const headers = function (request, reply) { + const headers = function (request, h) { - reply({ status: 'success' }) + return h.response({ status: 'success' }) .header('Custom1', 'custom header value 1') .header('X-Custom2', 'custom header value 2') .header('access-control-allow-headers', 'Invalid, List, Of, Values'); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/noHeaders', handler: headers }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/noHeaders', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/noHeaders', (res) => { + server.route({ method: 'GET', path: '/noHeaders', handler: { proxy: { host: 'localhost', port: upstream.info.port } } }); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.equal('{\"status\":\"success\"}'); - expect(res.headers.custom1).to.not.exist(); - expect(res.headers['x-custom2']).to.not.exist(); - done(); - }); - }); + const res = await server.inject('/noHeaders'); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('{\"status\":\"success\"}'); + expect(res.headers.custom1).to.not.exist(); + expect(res.headers['x-custom2']).to.not.exist(); + + await upstream.stop(); }); - it('request a cached proxy route', (done) => { + it('request a cached proxy route', async () => { let activeCount = 0; - const activeItem = function (request, reply) { + const handler = function (request, h) { - reply({ + return h.response({ id: '55cf687663', name: 'Active Items', count: activeCount++ }); }; - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.route({ method: 'GET', path: '/item', handler: activeItem }); - upstream.start(() => { + const upstream = Hapi.server(); + upstream.route({ method: 'GET', path: '/item', handler }); + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/item', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, protocol: 'http:' } }, config: { cache: { expiresIn: 500 } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/item', (response) => { + server.route({ method: 'GET', path: '/item', handler: { proxy: { host: 'localhost', port: upstream.info.port, protocol: 'http:' } }, config: { cache: { expiresIn: 500 } } }); - expect(response.statusCode).to.equal(200); - expect(response.payload).to.contain('Active Items'); - const counter = response.result.count; + const response = await server.inject('/item'); + expect(response.statusCode).to.equal(200); + expect(response.payload).to.contain('Active Items'); + const counter = response.result.count; - server.inject('/item', (res) => { + const res = await server.inject('/item'); + expect(res.statusCode).to.equal(200); + expect(res.result.count).to.equal(counter); - expect(res.statusCode).to.equal(200); - expect(res.result.count).to.equal(counter); - done(); - }); - }); - }); + await upstream.stop(); }); - it('forwards on the status code when making a POST request', (done) => { + it('forwards on the status code when making a POST request', async () => { - const item = function (request, reply) { + const item = function (request, h) { - reply({ id: '55cf687663', name: 'Items' }).created('http://example.com'); + return h.response({ id: '55cf687663', name: 'Items' }).created('http://example.com'); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'POST', path: '/item', handler: item }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'POST', path: '/item', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject({ url: '/item', method: 'POST' }, (res) => { + server.route({ method: 'POST', path: '/item', handler: { proxy: { host: 'localhost', port: upstream.info.port } } }); - expect(res.statusCode).to.equal(201); - expect(res.payload).to.contain('Items'); - done(); - }); - }); + const res = await server.inject({ url: '/item', method: 'POST' }); + expect(res.statusCode).to.equal(201); + expect(res.payload).to.contain('Items'); + + await upstream.stop(); }); - it('sends the correct status code when a request is unauthorized', (done) => { + it('sends the correct status code when a request is unauthorized', async () => { - const unauthorized = function (request, reply) { + const unauthorized = function (request, h) { - reply(Boom.unauthorized('Not authorized')); + throw Boom.unauthorized('Not authorized'); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/unauthorized', handler: unauthorized }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/unauthorized', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port } }, config: { cache: { expiresIn: 500 } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/unauthorized', (res) => { + server.route({ method: 'GET', path: '/unauthorized', handler: { proxy: { host: 'localhost', port: upstream.info.port } }, config: { cache: { expiresIn: 500 } } }); - expect(res.statusCode).to.equal(401); - done(); - }); - }); + const res = await server.inject('/unauthorized'); + expect(res.statusCode).to.equal(401); + + await upstream.stop(); }); - it('sends a 404 status code when a proxied route does not exist', (done) => { + it('sends a 404 status code when a proxied route does not exist', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.start(() => { + const upstream = Hapi.server(); + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'POST', path: '/notfound', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/notfound', (res) => { + server.route({ method: 'POST', path: '/notfound', handler: { proxy: { host: 'localhost', port: upstream.info.port } } }); - expect(res.statusCode).to.equal(404); - done(); - }); - }); + const res = await server.inject('/notfound'); + expect(res.statusCode).to.equal(404); + + await upstream.stop(); }); - it('overrides status code when a custom onResponse returns an error', (done) => { + it('overrides status code when a custom onResponse returns an error', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.start(() => { + const onResponseWithError = function (err, res, request, h, settings, ttl) { + + expect(err).to.be.null(); + throw Boom.forbidden('Forbidden'); + }; - const onResponseWithError = function (err, res, request, reply, settings, ttl) { + const upstream = Hapi.server(); + await upstream.start(); - expect(err).to.be.null(); - reply(Boom.forbidden('Forbidden')); - }; + const server = Hapi.server(); + await server.register(H2o2); - const server = provisionServer(); - server.route({ method: 'GET', path: '/onResponseError', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, onResponse: onResponseWithError } } }); + server.route({ method: 'GET', path: '/onResponseError', handler: { proxy: { host: 'localhost', port: upstream.info.port, onResponse: onResponseWithError } } }); - server.inject('/onResponseError', (res) => { + const res = await server.inject('/onResponseError'); + expect(res.statusCode).to.equal(403); - expect(res.statusCode).to.equal(403); - done(); - }); - }); + await upstream.stop(); }); - it('adds cookie to response', (done) => { + it('adds cookie to response', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.start(() => { + const on = function (err, res, request, h, settings, ttl) { - const on = function (err, res, request, reply, settings, ttl) { + expect(err).to.be.null(); + return h.response(res).state('a', 'b'); + }; - expect(err).to.be.null(); - reply(res).state('a', 'b'); - }; + const upstream = Hapi.server(); + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, onResponse: on } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/', (res) => { + server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'localhost', port: upstream.info.port, onResponse: on } } }); - expect(res.statusCode).to.equal(404); - expect(res.headers['set-cookie'][0]).to.equal('a=b'); - done(); - }); + const res = await server.inject('/'); + + expect(res.statusCode).to.equal(404); + expect(res.headers['set-cookie'][0]).to.equal('a=b; Secure; HttpOnly; SameSite=Strict'); + + await upstream.stop(); + }); + + it('calls onRequest when it\'s created', async () => { + + const upstream = Hapi.Server(); + + let upstreamRequested = false; + upstream.events.on('request', () => { + + upstreamRequested = true; }); + + await upstream.start(); + + let called = false; + const onRequestWithSocket = function (req) { + + called = true; + expect(upstreamRequested).to.be.false(); + expect(req).to.be.an.instanceof(Http.ClientRequest); + }; + + const on = function (err, res, request, h, settings, ttl) { + + expect(err).to.be.null(); + return h.response(h.context.c); + }; + + const handler = { + proxy: { + host: 'localhost', + port: upstream.info.port, + onRequest: onRequestWithSocket, + onResponse: on + } + }; + + const server = Hapi.server(); + await server.register(H2o2); + + server.route({ method: 'GET', path: '/onRequestSocket', config: { handler, bind: { c: 6 } } }); + + const res = await server.inject('/onRequestSocket'); + + expect(res.result).to.equal(6); + expect(called).to.equal(true); + await upstream.stop(); }); - it('binds onResponse to route bind config', (done) => { + it('binds onResponse to route bind config', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.start(() => { + const onResponseWithError = function (err, res, request, h, settings, ttl) { - const onResponseWithError = function (err, res, request, reply, settings, ttl) { + expect(err).to.be.null(); + return h.response(h.context.c); + }; - expect(err).to.be.null(); - reply(this.c); - }; + const upstream = Hapi.server(); + await upstream.start(); - const handler = { - kibi_proxy: { - host: 'localhost', - port: upstream.info.port, - onResponse: onResponseWithError - } - }; + const handler = { + proxy: { + host: 'localhost', + port: upstream.info.port, + onResponse: onResponseWithError + } + }; - const server = provisionServer(); - server.route({ method: 'GET', path: '/onResponseError', config: { handler, bind: { c: 6 } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/onResponseError', (res) => { + server.route({ method: 'GET', path: '/onResponseError', config: { handler, bind: { c: 6 } } }); - expect(res.result).to.equal(6); - done(); - }); - }); + const res = await server.inject('/onResponseError'); + expect(res.result).to.equal(6); + + await upstream.stop(); }); - it('binds onResponse to route bind config in plugin', (done) => { + it('binds onResponse to route bind config in plugin', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.start(() => { + const upstream = Hapi.server(); + await upstream.start(); - const plugin = function (server, options, next) { + const plugin = { + register: function (server, optionos) { - const onResponseWithError = function (err, res, request, reply, settings, ttl) { + const onResponseWithError = function (err, res, request, h, settings, ttl) { expect(err).to.be.null(); - reply(this.c); + return h.response(h.context.c); }; const handler = { - kibi_proxy: { + proxy: { host: 'localhost', port: upstream.info.port, onResponse: onResponseWithError @@ -788,44 +622,37 @@ describe('H2o2', () => { }; server.route({ method: 'GET', path: '/', config: { handler, bind: { c: 6 } } }); - return next(); - }; - - plugin.attributes = { - name: 'test' - }; - - const server = provisionServer(); + }, + name: 'test' + }; - server.register(plugin, (err) => { + const server = Hapi.server(); + await server.register(H2o2); - expect(err).to.not.exist(); + await server.register(plugin); - server.inject('/', (res) => { + const res = await server.inject('/'); + expect(res.result).to.equal(6); - expect(res.result).to.equal(6); - done(); - }); - }); - }); + await upstream.stop(); }); - it('binds onResponse to plugin bind', (done) => { + it('binds onResponse to plugin bind', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.start(() => { + const upstream = Hapi.server(); + await upstream.start(); - const plugin = function (server, options, next) { + const plugin = { + register: function (server, options) { - const onResponseWithError = function (err, res, request, reply, settings, ttl) { + const onResponseWithError = function (err, res, request, h, settings, ttl) { expect(err).to.be.null(); - reply(this.c); + return h.response(h.context.c); }; const handler = { - kibi_proxy: { + proxy: { host: 'localhost', port: upstream.info.port, onResponse: onResponseWithError @@ -834,44 +661,37 @@ describe('H2o2', () => { server.bind({ c: 7 }); server.route({ method: 'GET', path: '/', config: { handler } }); - return next(); - }; - - plugin.attributes = { - name: 'test' - }; - - const server = provisionServer(); + }, + name: 'test' + }; - server.register(plugin, (err) => { + const server = Hapi.server(); + await server.register(H2o2); - expect(err).to.not.exist(); + await server.register(plugin); - server.inject('/', (res) => { + const res = await server.inject('/'); + expect(res.result).to.equal(7); - expect(res.result).to.equal(7); - done(); - }); - }); - }); + await upstream.stop(); }); - it('binds onResponse to route bind config in plugin when plugin also has bind', (done) => { + it('binds onResponse to route bind config in plugin when plugin also has bind', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); - upstream.start(() => { + const upstream = Hapi.server(); + await upstream.start(); - const plugin = function (server, options, next) { + const plugin = { + register: function (server, options) { - const onResponseWithError = function (err, res, request, reply, settings, ttl) { + const onResponseWithError = function (err, res, request, h, settings, ttl) { expect(err).to.be.null(); - reply(this.c); + return h.response(h.context.c); }; const handler = { - kibi_proxy: { + proxy: { host: 'localhost', port: upstream.info.port, onResponse: onResponseWithError @@ -880,700 +700,698 @@ describe('H2o2', () => { server.bind({ c: 7 }); server.route({ method: 'GET', path: '/', config: { handler, bind: { c: 4 } } }); - return next(); - }; - - plugin.attributes = { - name: 'test' - }; - - const server = provisionServer(); + }, + name: 'test' + }; - server.register(plugin, (err) => { + const server = Hapi.server(); + await server.register(H2o2); - expect(err).to.not.exist(); + await server.register(plugin); - server.inject('/', (res) => { + const res = await server.inject('/'); + expect(res.result).to.equal(4); - expect(res.result).to.equal(4); - done(); - }); - }); - }); + await upstream.stop(); }); - it('calls the onResponse function if the upstream is unreachable', (done) => { - - const dummy = new Hapi.Server(); - dummy.connection(); - dummy.start(() => { + it('calls the onResponse function if the upstream is unreachable', async () => { - const dummyPort = dummy.info.port; - dummy.stop(Hoek.ignore); + const failureResponse = function (err, res, request, h, settings, ttl) { - const failureResponse = function (err, res, request, reply, settings, ttl) { + expect(h.response).to.exist(); + throw err; + }; - reply(err); - }; + const dummy = Hapi.server(); + await dummy.start(); + const dummyPort = dummy.info.port; + await dummy.stop(Hoek.ignore); - const server = provisionServer(); - server.route({ method: 'GET', path: '/failureResponse', handler: { kibi_proxy: { host: 'localhost', port: dummyPort, onResponse: failureResponse } }, config: { cache: { expiresIn: 500 } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/failureResponse', (res) => { + server.route({ method: 'GET', path: '/failureResponse', handler: { proxy: { host: 'localhost', port: dummyPort, onResponse: failureResponse } }, config: { cache: { expiresIn: 500 } } }); - expect(res.statusCode).to.equal(502); - done(); - }); - }); + const res = await server.inject('/failureResponse'); + expect(res.statusCode).to.equal(502); }); - it('sets x-forwarded-* headers', (done) => { + it('sets x-forwarded-* headers', async () => { - const handler = function (request, reply) { + const handler = function (request, h) { - reply(request.raw.req.headers); + return h.response(request.raw.req.headers); }; const host = '127.0.0.1'; - const upstream = new Hapi.Server(); - upstream.connection({ - host - }); + const upstream = Hapi.server({ host }); upstream.route({ method: 'GET', path: '/', handler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer({ - host, - tls: tlsOptions - }); + const server = Hapi.server({ host, tls: tlsOptions }); + await server.register(H2o2); - server.route({ - method: 'GET', - path: '/', - handler: { - kibi_proxy: { - host: upstream.info.host, - port: upstream.info.port, - protocol: 'http', - xforward: true - } + server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + host: upstream.info.host, + port: upstream.info.port, + protocol: 'http', + xforward: true } - }); - - server.start(() => { + } + }); + await server.start(); - const requestProtocol = 'https'; + const requestProtocol = 'https'; + const response = await Wreck.get(`${requestProtocol}://${server.info.host}:${server.info.port}/`, { + rejectUnauthorized: false + }); + expect(response.res.statusCode).to.equal(200); - Wreck.get(`${requestProtocol}://${server.info.host}:${server.info.port}/`, { - rejectUnauthorized: false - }, (err, res, body) => { + const result = JSON.parse(response.payload); + let expectedClientAddress = '127.0.0.1'; + let expectedClientAddressAndPort = expectedClientAddress + ':' + server.info.port; - expect(err).to.be.null(); - expect(res.statusCode).to.equal(200); - const result = JSON.parse(body); - - const expectedClientAddress = '127.0.0.1'; - const expectedClientAddressAndPort = expectedClientAddress + ':' + server.info.port; - if (Net.isIPv6(server.listener.address().address)) { - expectedClientAddress = '::ffff:127.0.0.1'; - expectedClientAddressAndPort = '[' + expectedClientAddress + ']:' + server.info.port; - } + if (Net.isIPv6(server.listener.address().address)) { + expectedClientAddress = '::ffff:127.0.0.1'; + expectedClientAddressAndPort = '[' + expectedClientAddress + ']:' + server.info.port; + } - expect(result['x-forwarded-for']).to.equal(expectedClientAddress); - expect(result['x-forwarded-port']).to.match(/\d+/); - expect(result['x-forwarded-proto']).to.equal(requestProtocol); - expect(result['x-forwarded-host']).to.equal(expectedClientAddressAndPort); + expect(result['x-forwarded-for']).to.equal(expectedClientAddress); + expect(result['x-forwarded-port']).to.match(/\d+/); + expect(result['x-forwarded-proto']).to.equal(requestProtocol); + expect(result['x-forwarded-host']).to.equal(expectedClientAddressAndPort); - server.stop(Hoek.ignore); - upstream.stop(Hoek.ignore); - done(); - }); - }); - }); + await server.stop(); + await upstream.stop(); }); - it('adds x-forwarded-* headers to existing', (done) => { + it('adds x-forwarded-for headers to existing and preserves original port, proto and host', async () => { - const handler = function (request, reply) { + const handler = function (request, h) { - reply(request.raw.req.headers); + return h.response(request.raw.req.headers); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', handler }); - upstream.start(() => { + await upstream.start(); - const mapUri = function (request, callback) { + const mapUri = function (request) { - const headers = { - 'x-forwarded-for': 'testhost', - 'x-forwarded-port': 1337, - 'x-forwarded-proto': 'https', - 'x-forwarded-host': 'example.com' - }; + const headers = { + 'x-forwarded-for': 'testhost', + 'x-forwarded-port': 1337, + 'x-forwarded-proto': 'https', + 'x-forwarded-host': 'example.com' + }; - return callback(null, 'http://127.0.0.1:' + upstream.info.port + '/', headers); + return { + uri: `http://127.0.0.1:${upstream.info.port}/`, + headers }; + }; - const server = provisionServer({ host: '127.0.0.1' }); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { mapUri, xforward: true } } }); + const server = Hapi.server({ host: '127.0.0.1' }); + await server.register(H2o2); - server.start(() => { + server.route({ method: 'GET', path: '/', handler: { proxy: { mapUri, xforward: true } } }); + await server.start(); - Wreck.get('http://127.0.0.1:' + server.info.port + '/', (err, res, body) => { + const response = await Wreck.get('http://127.0.0.1:' + server.info.port + '/'); + expect(response.res.statusCode).to.equal(200); - expect(err).to.be.null(); - expect(res.statusCode).to.equal(200); - const result = JSON.parse(body); - - const expectedClientAddress = '127.0.0.1'; - const expectedClientAddressAndPort = expectedClientAddress + ':' + server.info.port; - if (Net.isIPv6(server.listener.address().address)) { - expectedClientAddress = '::ffff:127.0.0.1'; - expectedClientAddressAndPort = '[' + expectedClientAddress + ']:' + server.info.port; - } + const result = JSON.parse(response.payload); - expect(result['x-forwarded-for']).to.equal('testhost,' + expectedClientAddress); - expect(result['x-forwarded-port']).to.match(/1337\,\d+/); - expect(result['x-forwarded-proto']).to.equal('https,http'); - expect(result['x-forwarded-host']).to.equal('example.com,' + expectedClientAddressAndPort); - server.stop(Hoek.ignore); - upstream.stop(Hoek.ignore); - done(); - }); - }); - }); + let expectedClientAddress = '127.0.0.1'; + if (Net.isIPv6(server.listener.address().address)) { + expectedClientAddress = '::ffff:127.0.0.1'; + } + + expect(result['x-forwarded-for']).to.equal('testhost,' + expectedClientAddress); + expect(result['x-forwarded-port']).to.equal('1337'); + expect(result['x-forwarded-proto']).to.equal('https'); + expect(result['x-forwarded-host']).to.equal('example.com'); + + await upstream.stop(); + await server.stop(); }); - it('does not clobber existing x-forwarded-* headers', (done) => { + it('does not clobber existing x-forwarded-* headers', async () => { + + const handler = function (request, h) { + + return h.response(request.raw.req.headers); + }; + + const mapUri = function (request) { - const handler = function (request, reply) { + const headers = { + 'x-forwarded-for': 'testhost', + 'x-forwarded-port': 1337, + 'x-forwarded-proto': 'https', + 'x-forwarded-host': 'example.com' + }; - reply(request.raw.req.headers); + return { + uri: `http://127.0.0.1:${upstream.info.port}/`, + headers + }; }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', handler }); - upstream.start(() => { + await upstream.start(); - const mapUri = function (request, callback) { + const server = Hapi.server(); + await server.register(H2o2); - const headers = { - 'x-forwarded-for': 'testhost', - 'x-forwarded-port': 1337, - 'x-forwarded-proto': 'https', - 'x-forwarded-host': 'example.com' - }; + server.route({ method: 'GET', path: '/', handler: { proxy: { mapUri, xforward: true } } }); - return callback(null, 'http://127.0.0.1:' + upstream.info.port + '/', headers); - }; + const res = await server.inject('/'); + const result = JSON.parse(res.payload); + expect(res.statusCode).to.equal(200); + expect(result['x-forwarded-for']).to.equal('testhost'); + expect(result['x-forwarded-port']).to.equal('1337'); + expect(result['x-forwarded-proto']).to.equal('https'); + expect(result['x-forwarded-host']).to.equal('example.com'); - const server = provisionServer(); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { mapUri, xforward: true } } }); - server.inject('/', (res) => { - - expect(res.statusCode).to.equal(200); - const result = JSON.parse(res.payload); - expect(result['x-forwarded-for']).to.equal('testhost'); - expect(result['x-forwarded-port']).to.equal('1337'); - expect(result['x-forwarded-proto']).to.equal('https'); - expect(result['x-forwarded-host']).to.equal('example.com'); - done(); - }); - }); + await upstream.stop(); }); - it('forwards on a POST body', (done) => { + it('forwards on a POST body', async () => { + + const echoPostBody = function (request, h) { - const echoPostBody = function (request, reply) { + return h.response(request.payload.echo + request.raw.req.headers['x-super-special']); + }; + + const mapUri = function (request) { - reply(request.payload.echo + request.raw.req.headers['x-super-special']); + return { + uri: `http://127.0.0.1:${upstream.info.port}${request.path}${(request.url.search || '')}`, + headers: { 'x-super-special': '@' } + }; }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'POST', path: '/echo', handler: echoPostBody }); - upstream.start(() => { + await upstream.start(); - const mapUri = function (request, callback) { + const server = Hapi.server(); + await server.register(H2o2); - return callback(null, 'http://127.0.0.1:' + upstream.info.port + request.path + (request.url.search || ''), { 'x-super-special': '@' }); - }; + server.route({ method: 'POST', path: '/echo', handler: { proxy: { mapUri } } }); - const server = provisionServer(); - server.route({ method: 'POST', path: '/echo', handler: { kibi_proxy: { mapUri } } }); - server.inject({ url: '/echo', method: 'POST', payload: '{"echo":true}' }, (res) => { + const res = await server.inject({ url: '/echo', method: 'POST', payload: '{"echo":true}' }); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('true@'); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.equal('true@'); - done(); - }); - }); + await upstream.stop(); }); - it('replies with an error when it occurs in mapUri', (done) => { + it('replies with an error when it occurs in mapUri', async () => { - const mapUriWithError = function (request, callback) { + const mapUriWithError = function (request) { - return callback(new Error('myerror')); + throw new Error('myerror'); }; - const server = provisionServer(); - server.route({ method: 'GET', path: '/maperror', handler: { kibi_proxy: { mapUri: mapUriWithError } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/maperror', (res) => { + server.route({ method: 'GET', path: '/maperror', handler: { proxy: { mapUri: mapUriWithError } } }); + const res = await server.inject('/maperror'); - expect(res.statusCode).to.equal(500); - done(); - }); + expect(res.statusCode).to.equal(500); }); - it('maxs out redirects to same endpoint', (done) => { + it('maxs out redirects to same endpoint', async () => { - const redirectHandler = function (request, reply) { + const redirectHandler = function (request, h) { - reply.redirect('/redirect?x=1'); + return h.redirect('/redirect?x=1'); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/redirect', handler: redirectHandler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/redirect', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, redirects: 2 } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/redirect?x=1', (res) => { + server.route({ method: 'GET', path: '/redirect', handler: { proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, redirects: 2 } } }); - expect(res.statusCode).to.equal(502); - done(); - }); - }); + const res = await server.inject('/redirect?x=1'); + expect(res.statusCode).to.equal(502); + + await upstream.stop(); }); - it('errors on redirect missing location header', (done) => { + it('errors on redirect missing location header', async () => { - const redirectHandler = function (request, reply) { + const redirectHandler = function (request, h) { - reply().code(302); + return h.response().code(302); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/redirect', handler: redirectHandler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/redirect', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, redirects: 2 } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/redirect?x=3', (res) => { + server.route({ method: 'GET', path: '/redirect', handler: { proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, redirects: 2 } } }); - expect(res.statusCode).to.equal(502); - done(); - }); - }); + const res = await server.inject('/redirect?x=3'); + expect(res.statusCode).to.equal(502); + + await upstream.stop(); }); - it('errors on redirection to bad host', (done) => { + it('errors on redirection to bad host', async () => { - const server = provisionServer(); - server.route({ method: 'GET', path: '/nowhere', handler: { kibi_proxy: { host: 'no.such.domain.x8' } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/nowhere', (res) => { + server.route({ method: 'GET', path: '/nowhere', handler: { proxy: { host: 'no.such.domain.x8' } } }); - expect(res.statusCode).to.equal(502); - done(); - }); + const res = await server.inject('/nowhere'); + expect(res.statusCode).to.equal(502); }); - it('errors on redirection to bad host (https)', (done) => { + it('errors on redirection to bad host (https)', async () => { - const server = provisionServer(); - server.route({ method: 'GET', path: '/nowhere', handler: { kibi_proxy: { host: 'no.such.domain.x8', protocol: 'https' } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/nowhere', (res) => { + server.route({ method: 'GET', path: '/nowhere', handler: { proxy: { host: 'no.such.domain.x8', protocol: 'https' } } }); - expect(res.statusCode).to.equal(502); - done(); - }); + const res = await server.inject('/nowhere'); + expect(res.statusCode).to.equal(502); }); - it('redirects to another endpoint', (done) => { + it('redirects to another endpoint', async () => { - const redirectHandler = function (request, reply) { + const redirectHandler = function (request, h) { - reply.redirect('/profile'); + return h.redirect('/profile'); }; - const profile = function (request, reply) { + const profile = function (request, h) { - reply({ id: 'fa0dbda9b1b', name: 'John Doe' }).state('test', '123'); + return h.response({ id: 'fa0dbda9b1b', name: 'John Doe' }).state('test', '123'); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/redirect', handler: redirectHandler }); upstream.route({ method: 'GET', path: '/profile', handler: profile, config: { cache: { expiresIn: 2000 } } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/redirect', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, redirects: 2 } } }); - server.state('auto', { autoValue: 'xyz' }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/redirect', (res) => { + server.route({ method: 'GET', path: '/redirect', handler: { proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, redirects: 2 } } }); + server.state('auto', { autoValue: 'xyz' }); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.contain('John Doe'); - expect(res.headers['set-cookie']).to.equal(['test=123', 'auto=xyz']); - done(); - }); - }); + const res = await server.inject('/redirect'); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.contain('John Doe'); + expect(res.headers['set-cookie'][0]).to.include(['test=123']); + expect(res.headers['set-cookie'][1]).to.include(['auto=xyz']); + + await upstream.stop(); }); - it('redirects to another endpoint with relative location', (done) => { + it('redirects to another endpoint with relative location', async () => { - const redirectHandler = function (request, reply) { + const redirectHandler = function (request, h) { - reply().header('Location', '//localhost:' + request.server.info.port + '/profile').code(302); + return h.response().header('Location', '//localhost:' + request.server.info.port + '/profile').code(302); }; - const profile = function (request, reply) { + const profile = function (request, h) { - reply({ id: 'fa0dbda9b1b', name: 'John Doe' }).state('test', '123'); + return h.response({ id: 'fa0dbda9b1b', name: 'John Doe' }).state('test', '123'); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/redirect', handler: redirectHandler }); upstream.route({ method: 'GET', path: '/profile', handler: profile, config: { cache: { expiresIn: 2000 } } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/redirect', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, redirects: 2 } } }); - server.state('auto', { autoValue: 'xyz' }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/redirect?x=2', (res) => { + server.route({ method: 'GET', path: '/redirect', handler: { proxy: { host: 'localhost', port: upstream.info.port, passThrough: true, redirects: 2 } } }); + server.state('auto', { autoValue: 'xyz' }); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.contain('John Doe'); - expect(res.headers['set-cookie']).to.equal(['test=123', 'auto=xyz']); - done(); - }); - }); + const res = await server.inject('/redirect?x=2'); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.contain('John Doe'); + expect(res.headers['set-cookie'][0]).to.include(['test=123']); + expect(res.headers['set-cookie'][1]).to.include(['auto=xyz']); + + await upstream.stop(); }); - it('redirects to a post endpoint with stream', (done) => { + it('redirects to a post endpoint with stream', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'POST', path: '/post1', - handler: function (request, reply) { + handler: function (request, h) { - return reply.redirect('/post2').rewritable(false); + return h.redirect('/post2').rewritable(false); } }); - upstream.route({ method: 'POST', path: '/post2', - handler: function (request, reply) { + handler: function (request, h) { - return reply(request.payload); + return h.response(request.payload); } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'POST', path: '/post1', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, redirects: 3 } }, config: { payload: { output: 'stream' } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject({ method: 'POST', url: '/post1', payload: 'test', headers: { 'content-type': 'text/plain' } }, (res) => { + server.route({ method: 'POST', path: '/post1', handler: { proxy: { host: 'localhost', port: upstream.info.port, redirects: 3 } }, config: { payload: { output: 'stream' } } }); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.equal('test'); - done(); - }); - }); + const res = await server.inject({ method: 'POST', url: '/post1', payload: 'test', headers: { 'content-type': 'text/plain' } }); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('test'); + + await upstream.stop(); }); - it('errors when proxied request times out', (done) => { + it('errors when proxied request times out', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/timeout1', - handler: function (request, reply) { + handler: function (request, h) { - setTimeout(() => { + return new Promise((resolve, reject) => { + + setTimeout(() => { + + return resolve(h.response('Ok')); + }, 10); + }); - return reply('Ok'); - }, 10); } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/timeout1', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, timeout: 5 } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/timeout1', (res) => { + server.route({ method: 'GET', path: '/timeout1', handler: { proxy: { host: 'localhost', port: upstream.info.port, timeout: 5 } } }); - expect(res.statusCode).to.equal(504); - done(); - }); - }); + const res = await server.inject('/timeout1'); + expect(res.statusCode).to.equal(504); + + await upstream.stop(); }); - it('uses default timeout when nothing is set', (done) => { + it('uses default timeout when nothing is set', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/timeout2', - handler: function (request, reply) { + handler: function (request, h) { + + return new Promise((resolve, reject) => { - setTimeout(() => { + setTimeout(() => { - return reply('Ok'); - }, 10); + return resolve(h.response('Ok')); + }, 10); + }); } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/timeout2', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/timeout2', (res) => { + server.route({ method: 'GET', path: '/timeout2', handler: { proxy: { host: 'localhost', port: upstream.info.port } } }); - expect(res.statusCode).to.equal(200); - done(); - }); - }); + const res = await server.inject('/timeout2'); + expect(res.statusCode).to.equal(200); + + await upstream.stop(); }); - it('uses rejectUnauthorized to allow proxy to self signed ssl server', (done) => { + it('uses rejectUnauthorized to allow proxy to self sign ssl server', async () => { - const upstream = new Hapi.Server(); - upstream.connection({ tls: tlsOptions }); + + const upstream = Hapi.server({ tls: tlsOptions }); upstream.route({ method: 'GET', path: '/', - handler: function (request, reply) { + handler: function (request, h) { - return reply('Ok'); + return h.response('Ok'); } }); - upstream.start(() => { + await upstream.start(); - const mapSslUri = function (request, callback) { + const mapSslUri = function (request) { - return callback(null, 'https://127.0.0.1:' + upstream.info.port); + return { + uri: `https://127.0.0.1:${upstream.info.port}` }; + }; - const server = provisionServer(); - server.route({ method: 'GET', path: '/allow', handler: { kibi_proxy: { mapUri: mapSslUri, rejectUnauthorized: false } } }); - server.inject('/allow', (res) => { + const server = Hapi.server(); + await server.register(H2o2); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.equal('Ok'); - done(); - }); - }); + server.route({ method: 'GET', path: '/allow', handler: { proxy: { mapUri: mapSslUri, rejectUnauthorized: false } } }); + await server.start(); + + const res = await server.inject('/allow'); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('Ok'); + + await server.stop(); + await upstream.stop(); }); - it('uses rejectUnauthorized to not allow proxy to self signed ssl server', (done) => { + it('uses rejectUnauthorized to not allow proxy to self sign ssl server', async () => { - const upstream = new Hapi.Server(); - upstream.connection({ tls: tlsOptions }); + const upstream = Hapi.server({ tls: tlsOptions }); upstream.route({ method: 'GET', path: '/', - handler: function (request, reply) { + handler: function (request, h) { - return reply('Ok'); + return h.response('Ok'); } }); - upstream.start(() => { + await upstream.start(); - const mapSslUri = function (request, callback) { + const mapSslUri = function (request, h) { - return callback(null, 'https://127.0.0.1:' + upstream.info.port); + return { + uri: `https://127.0.0.1:${upstream.info.port}` }; + }; - const server = provisionServer(); - server.route({ method: 'GET', path: '/reject', handler: { kibi_proxy: { mapUri: mapSslUri, rejectUnauthorized: true } } }); - server.inject('/reject', (res) => { + const server = Hapi.server(); + await server.register(H2o2); - expect(res.statusCode).to.equal(502); - done(); - }); - }); + server.route({ method: 'GET', path: '/reject', handler: { proxy: { mapUri: mapSslUri, rejectUnauthorized: true } } }); + await server.start(); + + const res = await server.inject('/reject'); + expect(res.statusCode).to.equal(502); + + await server.stop(); + await upstream.stop(); }); - it('the default rejectUnauthorized should not allow proxied server cert to be self signed', (done) => { + it('the default rejectUnauthorized should not allow proxied server cert to be self signed', async () => { - const upstream = new Hapi.Server(); - upstream.connection({ tls: tlsOptions }); + const upstream = Hapi.server({ tls: tlsOptions }); upstream.route({ method: 'GET', path: '/', - handler: function (request, reply) { + handler: function (request, h) { - return reply('Ok'); + return h.response('Ok'); } }); - upstream.start(() => { + await upstream.start(); - const mapSslUri = function (request, callback) { + const mapSslUri = function (request) { - return callback(null, 'https://127.0.0.1:' + upstream.info.port); - }; + return { uri: `https://127.0.0.1:${upstream.info.port}` }; + }; - const server = provisionServer(); - server.route({ method: 'GET', path: '/sslDefault', handler: { kibi_proxy: { mapUri: mapSslUri } } }); - server.inject('/sslDefault', (res) => { + const server = Hapi.server(); + await server.register(H2o2); - expect(res.statusCode).to.equal(502); - done(); - }); - }); + server.route({ method: 'GET', path: '/sslDefault', handler: { proxy: { mapUri: mapSslUri } } }); + await server.start(); + + const res = await server.inject('/sslDefault'); + expect(res.statusCode).to.equal(502); + + await server.stop(); + await upstream.stop(); }); - it('times out when proxy timeout is less than server', { parallel: false }, (done) => { + it('times out when proxy timeout is less than server', { parallel: false }, async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/timeout2', - handler: function (request, reply) { + handler: function (request, h) { + + return new Promise((resolve, reject) => { + + setTimeout(() => { - setTimeout(() => { + return resolve(h.response('Ok')); + }, 10); + }); - return reply('Ok'); - }, 10); } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer({ routes: { timeout: { server: 8 } } }); - server.route({ method: 'GET', path: '/timeout2', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, timeout: 2 } } }); - server.inject('/timeout2', (res) => { + const server = Hapi.server({ routes: { timeout: { server: 8 } } }); + await server.register(H2o2); - expect(res.statusCode).to.equal(504); - done(); - }); - }); + server.route({ method: 'GET', path: '/timeout2', handler: { proxy: { host: 'localhost', port: upstream.info.port, timeout: 2 } } }); + await server.start(); + + const res = await server.inject('/timeout2'); + expect(res.statusCode).to.equal(504); + + await server.stop(); + await upstream.stop(); }); - it('times out when server timeout is less than proxy', (done) => { + it('times out when server timeout is less than proxy', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/timeout1', - handler: function (request, reply) { + handler: function (request, h) { - setTimeout(() => { + return new Promise((resolve, reject) => { - return reply('Ok'); - }, 10); + setTimeout(() => { + + return resolve(h.response('Ok')); + }, 10); + }); } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer({ routes: { timeout: { server: 5 } } }); - server.route({ method: 'GET', path: '/timeout1', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, timeout: 15 } } }); - server.inject('/timeout1', (res) => { + const server = Hapi.server({ routes: { timeout: { server: 5 } } }); + await server.register(H2o2); - expect(res.statusCode).to.equal(503); - done(); - }); - }); + server.route({ method: 'GET', path: '/timeout1', handler: { proxy: { host: 'localhost', port: upstream.info.port, timeout: 15 } } }); + + const res = await server.inject('/timeout1'); + expect(res.statusCode).to.equal(503); + + await upstream.stop(); }); - it('proxies via uri template', (done) => { + it('proxies via uri template', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/item', - handler: function (request, reply) { + handler: function (request, h) { - return reply({ a: 1 }); + return h.response({ a: 1 }); } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/handlerTemplate', handler: { kibi_proxy: { uri: '{protocol}://localhost:' + upstream.info.port + '/item' } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/handlerTemplate', (res) => { + server.route({ method: 'GET', path: '/handlerTemplate', handler: { proxy: { uri: '{protocol}://localhost:' + upstream.info.port + '/item' } } }); + await server.start(); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.contain('"a":1'); - done(); - }); - }); + const res = await server.inject('/handlerTemplate'); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.contain('"a":1'); + + await server.stop(); + await upstream.stop(); }); - it('proxies via uri template with request.param variables', (done) => { + it('proxies via uri template with request.param variables', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/item/{param_a}/{param_b}', - handler: function (request, reply) { + handler: function (request, h) { - return reply({ a: request.params.param_a, b:request.params.param_b }); + return h.response({ a: request.params.param_a, b: request.params.param_b }); } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/handlerTemplate/{a}/{b}', handler: { kibi_proxy: { uri: 'http://localhost:' + upstream.info.port + '/item/{a}/{b}' } } }); + const server = Hapi.server(); + await server.register(H2o2); - const prma = 'foo'; - const prmb = 'bar'; - server.inject(`/handlerTemplate/${prma}/${prmb}`, (res) => { + server.route({ method: 'GET', path: '/handlerTemplate/{a}/{b}', handler: { proxy: { uri: 'http://localhost:' + upstream.info.port + '/item/{a}/{b}' } } }); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.contain(`"a":"${prma}"`); - expect(res.payload).to.contain(`"b":"${prmb}"`); - done(); - }); - }); + const prma = 'foo'; + const prmb = 'bar'; + const res = await server.inject(`/handlerTemplate/${prma}/${prmb}`); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.contain(`"a":"${prma}"`); + expect(res.payload).to.contain(`"b":"${prmb}"`); + + await upstream.stop(); }); - it('passes upstream caching headers', (done) => { + it('passes upstream caching headers', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/cachedItem', - handler: function (request, reply) { + handler: function (request, h) { - return reply({ a: 1 }); + return h.response({ a: 1 }); }, config: { cache: { @@ -1582,43 +1400,44 @@ describe('H2o2', () => { } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/cachedItem', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, ttl: 'upstream' } } }); - server.state('auto', { autoValue: 'xyz' }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/cachedItem', (res) => { + server.route({ method: 'GET', path: '/cachedItem', handler: { proxy: { host: 'localhost', port: upstream.info.port, ttl: 'upstream' } } }); + server.state('auto', { autoValue: 'xyz' }); + await server.start(); - expect(res.statusCode).to.equal(200); - expect(res.headers['cache-control']).to.equal('max-age=2, must-revalidate, private'); - done(); - }); - }); + const res = await server.inject('/cachedItem'); + expect(res.statusCode).to.equal(200); + expect(res.headers['cache-control']).to.equal('max-age=2, must-revalidate'); + + await server.stop(); + await upstream.stop(); }); - it('ignores when no upstream caching headers to pass', (done) => { + it('ignores when no upstream caching headers to pass', async () => { const upstream = Http.createServer((req, res) => { res.end('not much'); }); + await upstream.listen(); - upstream.listen(0, () => { + const server = Hapi.server(); + await server.register(H2o2); - const server = provisionServer(); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { host: 'localhost', port: upstream.address().port, ttl: 'upstream' } } }); + server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'localhost', port: upstream.address().port, ttl: 'upstream' } } }); - server.inject('/', (res) => { + const res = await server.inject('/'); + expect(res.statusCode).to.equal(200); + expect(res.headers['cache-control']).to.equal('no-cache'); - expect(res.statusCode).to.equal(200); - expect(res.headers['cache-control']).to.equal('no-cache'); - done(); - }); - }); + await upstream.close(); }); - it('ignores when upstream caching header is invalid', (done) => { + it('ignores when upstream caching header is invalid', async () => { const upstream = Http.createServer((req, res) => { @@ -1626,291 +1445,284 @@ describe('H2o2', () => { res.end('not much'); }); - upstream.listen(0, () => { + await upstream.listen(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { host: 'localhost', port: upstream.address().port, ttl: 'upstream' } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/', (res) => { + server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'localhost', port: upstream.address().port, ttl: 'upstream' } } }); - expect(res.statusCode).to.equal(200); - expect(res.headers['cache-control']).to.equal('no-cache'); - done(); - }); - }); + const res = await server.inject('/'); + expect(res.statusCode).to.equal(200); + expect(res.headers['cache-control']).to.equal('no-cache'); + + await upstream.close(); }); - it('overrides response code with 304', (done) => { + it('overrides response code with 304', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/item', - handler: function (request, reply) { + handler: function (request, h) { - return reply({ a: 1 }); + return h.response({ a: 1 }); } }); - upstream.start(() => { + await upstream.start(); - const onResponse304 = function (err, res, request, reply, settings, ttl) { + const onResponse304 = function (err, res, request, h, settings, ttl) { - expect(err).to.be.null(); - return reply(res).code(304); - }; + expect(err).to.be.null(); + return h.response(res).code(304); + }; - const server = provisionServer(); - server.route({ method: 'GET', path: '/304', handler: { kibi_proxy: { uri: 'http://localhost:' + upstream.info.port + '/item', onResponse: onResponse304 } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject('/304', (res) => { + server.route({ method: 'GET', path: '/304', handler: { proxy: { uri: 'http://localhost:' + upstream.info.port + '/item', onResponse: onResponse304 } } }); - expect(res.statusCode).to.equal(304); - expect(res.payload).to.equal(''); - done(); - }); - }); + const res = await server.inject('/304'); + expect(res.statusCode).to.equal(304); + expect(res.payload).to.equal(''); + + await upstream.stop(); }); - it('cleans up when proxy response replaced in onPreResponse', (done) => { + it('cleans up when proxy response replaced in onPreResponse', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/item', - handler: function (request, reply) { + handler: function (request, h) { - return reply({ a: 1 }); + return h.response({ a: 1 }); } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.ext('onPreResponse', (request, reply) => { + const server = Hapi.server(); + await server.register(H2o2); - return reply({ something: 'else' }); - }); + server.ext('onPreResponse', (request, h) => { - server.route({ method: 'GET', path: '/item', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port } } }); + return h.response({ something: 'else' }); + }); + server.route({ method: 'GET', path: '/item', handler: { proxy: { host: 'localhost', port: upstream.info.port } } }); - server.inject('/item', (res) => { + const res = await server.inject('/item'); + expect(res.statusCode).to.equal(200); + expect(res.result.something).to.equal('else'); - expect(res.statusCode).to.equal(200); - expect(res.result.something).to.equal('else'); - done(); - }); - }); + await upstream.stop(); }); - it('retails accept-encoding header', (done) => { + it('retails accept-encoding header', async () => { - const profile = function (request, reply) { + const profile = function (request, h) { - reply(request.headers['accept-encoding']); + return h.response(request.headers['accept-encoding']); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', handler: profile, config: { cache: { expiresIn: 2000 } } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, acceptEncoding: true, passThrough: true } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject({ url: '/', headers: { 'accept-encoding': '*/*' } }, (res) => { + server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'localhost', port: upstream.info.port, acceptEncoding: true, passThrough: true } } }); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.equal('*/*'); - done(); - }); - }); + const res = await server.inject({ url: '/', headers: { 'accept-encoding': '*/*' } }); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('*/*'); + + await upstream.stop(); }); - it('removes accept-encoding header', (done) => { + it('removes accept-encoding header', async () => { - const profile = function (request, reply) { + const profile = function (request, h) { - reply(request.headers['accept-encoding']); + return h.response(request.headers['accept-encoding']); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', handler: profile, config: { cache: { expiresIn: 2000 } } }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.route({ method: 'GET', path: '/', handler: { kibi_proxy: { host: 'localhost', port: upstream.info.port, acceptEncoding: false, passThrough: true } } }); + const server = Hapi.server(); + await server.register(H2o2); - server.inject({ url: '/', headers: { 'accept-encoding': '*/*' } }, (res) => { + server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'localhost', port: upstream.info.port, acceptEncoding: false, passThrough: true } } }); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.equal(''); - done(); - }); - }); + const res = await server.inject({ url: '/', headers: { 'accept-encoding': '*/*' } }); + expect(res.statusCode).to.be.within(200, 204); + expect(res.payload).to.equal(''); + + await upstream.stop(); }); - it('does not send multiple Content-Type headers on passthrough', { parallel: false }, (done) => { + it('does not send multiple Content-Type headers on passthrough', { parallel: false }, async () => { - const server = provisionServer(); + const server = Hapi.server(); + await server.register(H2o2); - const requestFn = Wreck.request; - Wreck.request = function (method, url, options, cb) { + const httpClient = { + request(method, uri, options, callback) { - Wreck.request = requestFn; - expect(options.headers['content-type']).to.equal('application/json'); - expect(options.headers['Content-Type']).to.not.exist(); - cb(new Error('placeholder')); + expect(options.headers['content-type']).to.equal('application/json'); + expect(options.headers['Content-Type']).to.not.exist(); + throw new Error('placeholder'); + } }; - server.route({ method: 'GET', path: '/test', handler: { kibi_proxy: { uri: 'http://localhost', passThrough: true } } }); - server.inject({ method: 'GET', url: '/test', headers: { 'Content-Type': 'application/json' } }, (res) => { - - done(); - }); + server.route({ method: 'GET', path: '/test', handler: { proxy: { uri: 'http://localhost', httpClient, passThrough: true } } }); + await server.inject({ method: 'GET', url: '/test', headers: { 'Content-Type': 'application/json' } }); }); - it('allows passing in an agent through to Wreck', { parallel: false }, (done) => { + it('allows passing in an agent through to Wreck', { parallel: false }, async () => { - const server = provisionServer(); - const agent = { name: 'myagent' }; + const server = Hapi.server(); + await server.register(H2o2); - const requestFn = Wreck.request; - Wreck.request = function (method, url, options, cb) { + const agent = { name: 'myagent' }; - Wreck.request = requestFn; - expect(options.agent).to.equal(agent); - done(); + const httpClient = { + request(method, uri, options, callback) { + expect(options.agent).to.equal(agent); + return { statusCode: 200 }; + } }; - server.route({ method: 'GET', path: '/agenttest', handler: { kibi_proxy: { uri: 'http://localhost', agent } } }); - server.inject({ method: 'GET', url: '/agenttest', headers: {} }, (res) => { }); + server.route({ method: 'GET', path: '/agenttest', handler: { proxy: { uri: 'http://localhost', httpClient, agent } } }); + await server.inject({ method: 'GET', url: '/agenttest', headers: {} }, (res) => { }); }); - it('excludes request cookies defined locally', (done) => { + it('excludes request cookies defined locally', async () => { - const handler = function (request, reply) { + const handler = function (request, h) { - reply(request.state); + return h.response(request.state); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', handler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.state('a'); + const server = Hapi.server(); + await server.register(H2o2); - server.route({ - method: 'GET', - path: '/', - handler: { - kibi_proxy: { - host: 'localhost', - port: upstream.info.port, - passThrough: true - } + server.state('a'); + + server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + host: 'localhost', + port: upstream.info.port, + passThrough: true } - }); + } + }); - server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }, (res) => { + const res = await server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }); + expect(res.statusCode).to.equal(200); - expect(res.statusCode).to.equal(200); - const cookies = JSON.parse(res.payload); - expect(cookies).to.equal({ b: '2' }); - done(); - }); - }); + const cookies = JSON.parse(res.payload); + expect(cookies).to.equal({ b: '2' }); + + await upstream.stop(); }); - it('includes request cookies defined locally (route level)', (done) => { + it('includes request cookies defined locally (route level)', async () => { - const handler = function (request, reply) { + const handler = function (request, h) { - return reply(request.state); + return h.response(request.state); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', handler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.state('a', { passThrough: true }); + const server = Hapi.server(); + await server.register(H2o2); - server.route({ - method: 'GET', - path: '/', - handler: { - kibi_proxy: { - host: 'localhost', - port: upstream.info.port, - passThrough: true, - localStatePassThrough: true - } + server.state('a', { passThrough: true }); + server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + host: 'localhost', + port: upstream.info.port, + passThrough: true, + localStatePassThrough: true } - }); + } + }); + const res = await server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }); + expect(res.statusCode).to.equal(200); - server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }, (res) => { + const cookies = JSON.parse(res.payload); + expect(cookies).to.equal({ a: '1', b: '2' }); - expect(res.statusCode).to.equal(200); - const cookies = JSON.parse(res.payload); - expect(cookies).to.equal({ a: '1', b: '2' }); - done(); - }); - }); + await upstream.stop(); }); - it('includes request cookies defined locally (cookie level)', (done) => { + it('includes request cookies defined locally (cookie level)', async () => { - const handler = function (request, reply) { + const handler = function (request, h) { - reply(request.state); + return h.response(request.state); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', handler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.state('a', { passThrough: true }); + const server = Hapi.server(); + await server.register(H2o2); - server.route({ - method: 'GET', - path: '/', - handler: { - kibi_proxy: { - host: 'localhost', - port: upstream.info.port, - passThrough: true - } + server.state('a', { passThrough: true }); + server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + host: 'localhost', + port: upstream.info.port, + passThrough: true } - }); + } + }); - server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }, (res) => { + const res = await server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }); + expect(res.statusCode).to.equal(200); - expect(res.statusCode).to.equal(200); - const cookies = JSON.parse(res.payload); - expect(cookies).to.equal({ a: '1', b: '2' }); - done(); - }); - }); + const cookies = JSON.parse(res.payload); + expect(cookies).to.equal({ a: '1', b: '2' }); + + await upstream.stop(); }); - it('errors on invalid cookie header', (done) => { + it('errors on invalid cookie header', async () => { + + const server = Hapi.server({ routes: { state: { failAction: 'ignore' } } }); + await server.register(H2o2); - const server = provisionServer({ routes: { state: { failAction: 'ignore' } } }); server.state('a', { passThrough: true }); server.route({ method: 'GET', path: '/', handler: { - kibi_proxy: { + proxy: { host: 'localhost', port: 8080, passThrough: true @@ -1918,117 +1730,303 @@ describe('H2o2', () => { } }); - server.inject({ url: '/', headers: { cookie: 'a' } }, (res) => { - - expect(res.statusCode).to.equal(400); - done(); - }); + const res = await server.inject({ url: '/', headers: { cookie: 'a' } }); + expect(res.statusCode).to.equal(400); }); - it('drops cookies when all defined locally', (done) => { + it('drops cookies when all defined locally', async () => { - const handler = function (request, reply) { + const handler = function (request, h) { - reply(request.state); + return h.response(request.state); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', handler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.state('a'); + const server = Hapi.server(); + await server.register(H2o2); - server.route({ - method: 'GET', - path: '/', - handler: { - kibi_proxy: { - host: 'localhost', - port: upstream.info.port, - passThrough: true - } + server.state('a'); + server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + host: 'localhost', + port: upstream.info.port, + passThrough: true } - }); + } + }); - server.inject({ url: '/', headers: { cookie: 'a=1' } }, (res) => { + const res = await server.inject({ url: '/', headers: { cookie: 'a=1' } }); + expect(res.statusCode).to.equal(200); - expect(res.statusCode).to.equal(200); - const cookies = JSON.parse(res.payload); - expect(cookies).to.equal({}); - done(); - }); - }); + const cookies = JSON.parse(res.payload); + expect(cookies).to.equal({}); + + await upstream.stop(); }); - it('excludes request cookies defined locally (state override)', (done) => { + it('excludes request cookies defined locally (state override)', async () => { - const handler = function (request, reply) { + const handler = function (request, h) { - return reply(request.state); + return h.response(request.state); }; - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server(); upstream.route({ method: 'GET', path: '/', handler }); - upstream.start(() => { + await upstream.start(); - const server = provisionServer(); - server.state('a', { passThrough: false }); + const server = Hapi.server(); + await server.register(H2o2); - server.route({ - method: 'GET', - path: '/', - handler: { - kibi_proxy: { - host: 'localhost', - port: upstream.info.port, - passThrough: true - } + server.state('a', { passThrough: false }); + server.route({ + method: 'GET', + path: '/', + handler: { + proxy: { + host: 'localhost', + port: upstream.info.port, + passThrough: true } - }); + } + }); - server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }, (res) => { + const res = await server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }); + expect(res.statusCode).to.equal(200); - expect(res.statusCode).to.equal(200); - const cookies = JSON.parse(res.payload); - expect(cookies).to.equal({ b: '2' }); - done(); - }); + const cookies = JSON.parse(res.payload); + expect(cookies).to.equal({ b: '2' }); + + await upstream.stop(); + }); + + it('uses reply decorator', async () => { + + const upstream = Hapi.server(); + upstream.route({ + method: 'GET', + path: '/', + handler: function (request, h) { + + return h.response('ok'); + } + }); + + await upstream.start(); + + const server = Hapi.server(); + await server.register(H2o2); + + server.route({ + method: 'GET', + path: '/', + handler: function (request, h) { + + return h.proxy({ host: 'localhost', port: upstream.info.port, xforward: true, passThrough: true }); + } }); + + const res = await server.inject('/'); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('ok'); + + await upstream.stop(); }); - it('uses reply decorator', (done) => { + it('uses custom TLS settings', async () => { - const upstream = new Hapi.Server(); - upstream.connection(); + const upstream = Hapi.server({ tls: tlsOptions }); upstream.route({ method: 'GET', path: '/', - handler: function (request, reply) { + handler: function (request, h) { - return reply('ok'); + return h.response('ok'); } }); - upstream.start(() => { - const server = provisionServer(); - server.route({ - method: 'GET', - path: '/', - handler: function (request, reply) { + await upstream.start(); - return reply.kibi_proxy({ host: 'localhost', port: upstream.info.port, xforward: true, passThrough: true }); + const server = Hapi.server(); + await server.register({ plugin: H2o2, options: { secureProtocol: 'TLSv1_2_method', ciphers: 'ECDHE-RSA-AES128-SHA256' } }); + server.route({ + method: 'GET', + path: '/', + handler: function (request, h) { + + return h.proxy({ host: '127.0.0.1', protocol: 'https', port: upstream.info.port, rejectUnauthorized: false }); + } + }); + + const res = await server.inject('/'); + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('ok'); + + await upstream.stop(); + }); + + it('adds downstreamResponseTime to the response when downstreamResponseTime is set to true on success', async () => { + + const upstream = Hapi.server(); + upstream.route({ + method: 'GET', + path: '/', + handler: function (request, h) { + + return h.response('ok'); + } + }); + + await upstream.start(); + + const server = Hapi.server(); + await server.register({ plugin: H2o2, options: { downstreamResponseTime: true } }); + server.route({ + method: 'GET', + path: '/', + handler: function (request, h) { + + return h.proxy({ host: 'localhost', port: upstream.info.port, xforward: true, passThrough: true }); + } + }); + + server.events.on('request', (request, event, tags) => { + + expect(Object.keys(event.data)).to.equal(['downstreamResponseTime']); + expect(tags).to.equal({ h2o2: true, success: true }); + }); + + const res = await server.inject('/'); + expect(res.statusCode).to.equal(200); + + await upstream.stop(); + }); + + it('adds downstreamResponseTime to the response when downstreamResponseTime is set to true on error', async () => { + + const failureResponse = function (err, res, request, h, settings, ttl) { + + expect(h.response).to.exist(); + throw err; + }; + + const dummy = Hapi.server(); + await dummy.start(); + const dummyPort = dummy.info.port; + await dummy.stop(Hoek.ignore); + + const options = { downstreamResponseTime: true }; + + const server = Hapi.server(); + await server.register({ plugin: H2o2, options }); + server.route({ method: 'GET', path: '/failureResponse', handler: { proxy: { host: 'localhost', port: dummyPort, onResponse: failureResponse } }, config: { cache: { expiresIn: 500 } } }); + + let firstEvent = true; + server.events.on('request', (request, event, tags) => { + + if (firstEvent) { + firstEvent = false; + expect(Object.keys(event.data)).to.equal(['downstreamResponseTime']); + expect(tags).to.equal({ h2o2: true, error: true }); + } + }); + + const res = await server.inject('/failureResponse'); + expect(res.statusCode).to.equal(502); + }); + + it('uses a custom http-client', async () => { + + const upstream = Hapi.server(); + upstream.route({ method: 'GET', path: '/', handler: () => 'ok' }); + await upstream.start(); + + const httpClient = { + request: Wreck.request.bind(Wreck), + parseCacheControl: Wreck.parseCacheControl.bind(Wreck) + }; + + const server = Hapi.server(); + await server.register(H2o2); + + server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'localhost', port: upstream.info.port, httpClient } } }); + + const res = await server.inject('/'); + + expect(res.payload).to.equal('ok'); + }); + + it('updates payload based on mapHttpClientOptions', async () => { + + const upstream = Hapi.server(); + upstream.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + await upstream.start(); + + const server = Hapi.server(); + await server.register(H2o2); + + server.route({ + method: 'POST', + path: '/', + handler: { + proxy: { + mapHttpClientOptions: async (request) => ({ payload: await internals.parseReadStream(request.payload) + 'bar' }), + host: 'localhost', + port: upstream.info.port } - }); + } + }); - server.inject('/', (res) => { + const response = await server.inject({ method: 'POST', url: '/', payload: 'foo', headers: { 'Content-Type': 'text/plain' } }); + expect(response.payload).to.equal('foobar'); + expect(response.statusCode).to.equal(200); - expect(res.statusCode).to.equal(200); - expect(res.payload).to.equal('ok'); - done(); - }); + await upstream.stop(); + }); + + it('does not encode URI if already encoded', async () => { + + const upstream = Hapi.server(); + upstream.route({ method: 'GET', path: '/{param}', handler: (request) => ({ path: request.path, param: request.params.param }) }); + await upstream.start(); + + const server = Hapi.server(); + await server.register(H2o2); + + server.route({ + method: 'GET', + path: '/{param}', + handler: { + proxy: { + host: 'localhost', + port: upstream.info.port + } + } }); + + const response = await server.inject(encodeURI('/フーバー')); + const { path, param } = JSON.parse(response.payload); + expect(path).to.equal(encodeURI('/フーバー')); + expect(param).to.equal('フーバー'); + expect(response.statusCode).to.equal(200); + + await upstream.stop(); }); }); + +internals.parseReadStream = function (stream) { + + return new Promise((resolve, reject) => { + + const chunks = []; + stream.on('error', reject); + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('end', () => resolve(chunks.join().toString())); + }); +};