├── .github └── workflows │ └── ci-plugin.yml ├── .gitignore ├── API.md ├── LICENSE.md ├── README.md ├── lib ├── index.d.ts └── index.js ├── package.json └── test ├── esm.js ├── index.js └── index.ts /.github/workflows/ci-plugin.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | uses: hapijs/.github/.github/workflows/ci-plugin.yml@master 13 | with: 14 | min-node-version: 14 15 | min-hapi-version: 20 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/package-lock.json 3 | 4 | coverage.* 5 | 6 | **/.DS_Store 7 | **/._* 8 | 9 | **/*.pem 10 | 11 | **/.vs 12 | **/.vscode 13 | **/.idea 14 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | 2 | ## Introduction 3 | 4 | **h2o2** adds proxying functionality to a hapi server. 5 | 6 | ## Manual loading 7 | 8 | ```javascript 9 | const Hapi = require('@hapi/hapi'); 10 | const H2o2 = require('@hapi/h2o2'); 11 | 12 | 13 | const start = async function() { 14 | 15 | const server = Hapi.server(); 16 | 17 | await server.register(H2o2); 18 | await server.start(); 19 | 20 | console.log(`Server started at: ${server.info.uri}`); 21 | }; 22 | 23 | start(); 24 | ``` 25 | 26 | ## Options 27 | 28 | The plugin can be registered with an optional object specifying defaults to be applied to the proxy handler object. 29 | 30 | The proxy handler object has the following properties: 31 | 32 | * `host` - upstream service host to proxy requests to. It will have the same path as the client request. 33 | * `port` - upstream service port. 34 | * `protocol` - protocol to use when making the request to the proxied host: 35 | * 'http' 36 | * 'https' 37 | * `uri` - absolute URI used instead of host, port, protocol, path, and query. Cannot be used with `host`, `port`, `protocol`, or `mapUri`. 38 | * `httpClient` - an http client that abides by the Wreck interface. Defaults to [`wreck`](https://github.com/hapijs/wreck). 39 | * `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`. 40 | * `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` 41 | * `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). 42 | * `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`. 43 | * `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`. 44 | * `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`. 45 | * `timeout` - number of milliseconds before aborting the upstream request. Defaults to `180000` (3 minutes). 46 | * `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: 47 | * `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: 48 | * `uri` - the absolute proxy URI. 49 | * `headers` - optional object where each key is an HTTP request header and the value is the header content. 50 | * `onRequest` - a custom function which is passed the upstream request. Function signature is `function (req)` where: 51 | * `req` - the [wreck] (https://github.com/hapijs/wreck) request to the upstream server. 52 | * `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: 53 | * `err` - internal or upstream error returned from attempting to contact the upstream proxy. 54 | * `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. 55 | * `request` - is the incoming [request object](http://hapijs.com/api#request-object). 56 | * `h` - the [response toolkit](https://hapijs.com/api#response-toolkit). 57 | * `settings` - the proxy handler configuration. 58 | * `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'. 59 | * `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). 60 | * `agent` - a node [http(s) agent](http://nodejs.org/api/http.html#http_class_http_agent) to be used for connections to upstream server. 61 | * `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`. 62 | * `secureProtocol` - [TLS](http://nodejs.org/api/tls.html) flag indicating the SSL method to use, e.g. `SSLv3_method` 63 | 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). 64 | * `ciphers` - [TLS](https://nodejs.org/api/tls.html#tls_modifying_the_default_tls_cipher_suite) list of TLS ciphers to override node's default. 65 | 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). 66 | * `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`. 67 | 68 | ## Usage 69 | 70 | As one of the handlers for hapi, it is used through the route configuration object. 71 | 72 | ### `h.proxy(options)` 73 | 74 | Proxies the request to an upstream endpoint where: 75 | - `options` - an object including the same keys and restrictions defined by the 76 | [route `proxy` handler options](#options). 77 | 78 | No return value. 79 | 80 | The [response flow control rules](http://hapijs.com/api#flow-control) **do not** apply. 81 | 82 | ```js 83 | const handler = function (request, h) { 84 | 85 | return h.proxy({ host: 'example.com', port: 80, protocol: 'http' }); 86 | }; 87 | ``` 88 | 89 | ### Using the `host`, `port`, `protocol` options 90 | 91 | 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`. 92 | 93 | ```javascript 94 | server.route({ 95 | method: 'GET', 96 | path: '/', 97 | handler: { 98 | proxy: { 99 | host: '10.33.33.1', 100 | port: '443', 101 | protocol: 'https' 102 | } 103 | } 104 | }); 105 | ``` 106 | 107 | ### Using the `uri` option 108 | 109 | 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`. 110 | 111 | ```javascript 112 | server.route({ 113 | method: 'GET', 114 | path: '/', 115 | handler: { 116 | proxy: { 117 | uri: 'https://some.upstream.service.com/that/has?what=you&want=todo' 118 | } 119 | } 120 | }); 121 | ``` 122 | ### Custom `uri` template values 123 | 124 | When using the `uri` option, there are optional **default** template values that can be injected from the incoming `request`: 125 | 126 | * `{protocol}` 127 | * `{host}` 128 | * `{port}` 129 | * `{path}` 130 | * `{query}` 131 | 132 | ```javascript 133 | server.route({ 134 | method: 'GET', 135 | path: '/foo', 136 | handler: { 137 | proxy: { 138 | uri: '{protocol}://{host}:{port}/go/to/{path}' 139 | } 140 | } 141 | }); 142 | ``` 143 | 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` 144 | 145 | 146 | Additionally, you can capture request.params and query values and inject them into the upstream uri value using a similar replacement strategy: 147 | ```javascript 148 | server.route({ 149 | method: 'GET', 150 | path: '/foo/{bar}', 151 | handler: { 152 | proxy: { 153 | uri: 'https://some.upstream.service.com/some/path/to/{bar}{query}' 154 | } 155 | } 156 | }); 157 | ``` 158 | **Note** The default variables of `{protocol}`, `{host}`, `{port}`, `{path}`, `{query}` take precedence - it's best to treat those as reserved when naming your own `request.params`. 159 | 160 | 161 | ### Using the `mapUri` and `onResponse` options 162 | 163 | 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`. 164 | 165 | ```javascript 166 | server.route({ 167 | method: 'GET', 168 | path: '/', 169 | handler: { 170 | proxy: { 171 | mapUri: function (request) { 172 | 173 | console.log('doing some additional stuff before redirecting'); 174 | return { 175 | uri: 'https://some.upstream.service.com/' 176 | }; 177 | }, 178 | onResponse: async function (err, res, request, h, settings, ttl) { 179 | 180 | console.log('receiving the response from the upstream.'); 181 | const payload = await Wreck.read(res, { json: true }); 182 | 183 | console.log('some payload manipulation if you want to.'); 184 | const response = h.response(payload); 185 | response.headers = res.headers; 186 | return response; 187 | } 188 | } 189 | } 190 | }); 191 | 192 | ``` 193 | 194 | 195 | ### Using a custom http client 196 | 197 | 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()`. 198 | 199 | ```javascript 200 | server.route({ 201 | method: 'GET', 202 | path: '/', 203 | handler: { 204 | proxy: { 205 | httpClient: { 206 | request(method, uri, options) { 207 | return axios({ 208 | method, 209 | url: 'https://some.upstream.service.com/' 210 | }) 211 | } 212 | } 213 | } 214 | } 215 | }); 216 | ``` 217 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2022, Project contributors 2 | Copyright (c) 2012-2020, Sideway Inc 3 | Copyright (c) 2012-2014, Walmart. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 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. 9 | * The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | 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. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # @hapi/h2o2 4 | 5 | #### Proxy handler for hapi.js. 6 | 7 | **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. 8 | 9 | ### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support 10 | 11 | ## Useful resources 12 | 13 | - [Documentation and API](https://hapi.dev/family/h2o2/) 14 | - [Version status](https://hapi.dev/resources/status/#h2o2) (builds, dependencies, node versions, licenses, eol) 15 | - [Changelog](https://hapi.dev/family/h2o2/changelog/) 16 | - [Project policies](https://hapi.dev/policies/) 17 | - [Free and commercial support options](https://hapi.dev/support/) 18 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for @hapi/h2o2 10.0 2 | // Project: https://github.com/hapijs/h2o2 3 | // Definitions by: Jason Swearingen 4 | // AJP 5 | // Garth Kidd 6 | // Silas Rech 7 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 8 | 9 | /// 10 | 11 | import { Agent, IncomingMessage } from 'http'; 12 | import { Boom } from '@hapi/boom'; 13 | import Wreck = require('@hapi/wreck'); 14 | import { Plugin, Request, ResponseToolkit, Lifecycle, RouteOptions, ResponseObject } from '@hapi/hapi'; 15 | 16 | declare namespace h2o2 { 17 | /** 18 | * `mapURI` return value 19 | */ 20 | interface ProxyTarget { 21 | /** 22 | * The URI to request 23 | */ 24 | uri: string; 25 | 26 | /** 27 | * The headers with which to request `uri` 28 | */ 29 | headers?: { [key: string]: string } | undefined; 30 | } 31 | 32 | /** 33 | * Proxy handler options 34 | */ 35 | interface ProxyHandlerOptions { 36 | /** 37 | * upstream service host to proxy requests to. It will have the same 38 | * path as the client request. 39 | */ 40 | host?: string | undefined; 41 | /** 42 | * upstream service port. 43 | */ 44 | port?: number | string | undefined; 45 | /** 46 | * protocol to use when making the request to the proxied host: 47 | */ 48 | protocol?: 'http' | 'https' | undefined; 49 | /** 50 | * absolute URI used instead of host, port, protocol, path, and query. 51 | * Cannot be used with host, port, protocol, or mapUri. 52 | */ 53 | uri?: string | undefined; 54 | /** 55 | * an http client that abides by the Wreck interface. 56 | * 57 | * @default wreck 58 | */ 59 | httpClient?: Partial | undefined; 60 | /** 61 | * if set to true, it forwards the headers from the client to the 62 | * upstream service, headers sent from the upstream service will also be 63 | * forwarded to the client. 64 | * 65 | * @default false 66 | */ 67 | passThrough?: boolean | undefined; 68 | /** 69 | * if set to false, any locally defined state is removed from incoming 70 | * requests before being sent to the upstream service. This value can be 71 | * overridden on a per state basis via the server.state()``passThrough 72 | * option. 73 | * 74 | * @default false 75 | */ 76 | localStatePassThrough?: boolean | undefined; 77 | /** 78 | * if set to false, does not pass-through the 'Accept-Encoding' HTTP 79 | * header which is useful for the onResponse post-processing to avoid 80 | * receiving an encoded response. Can only be used together with 81 | * passThrough. 82 | * 83 | * @default true (passing header). 84 | */ 85 | acceptEncoding?: boolean | undefined; 86 | /** 87 | * sets the rejectUnauthorized property on the https agent making the 88 | * request. This value is only used when the proxied server uses 89 | * TLS/SSL. If set it will override the node.js rejectUnauthorized 90 | * property. If false then ssl errors will be ignored. When true the 91 | * server certificate is verified and an 500 response will be sent when 92 | * verification fails. This shouldn't be used alongside the agent 93 | * setting as the agent will be used instead. 94 | * 95 | * @default true 96 | */ 97 | rejectUnauthorized?: boolean | undefined; 98 | /** 99 | * if set to true, sets the 'X-Forwarded-For', 'X-Forwarded-Port', 100 | * 'X-Forwarded-Proto', 'X-Forwarded-Host' headers when making a request 101 | * to the proxied upstream endpoint. 102 | * 103 | * @default false 104 | */ 105 | xforward?: boolean | undefined; 106 | /** 107 | * the maximum number of HTTP redirections allowed to be followed 108 | * automatically by the handler. Set to false or 0 to disable all 109 | * redirections (the response will contain the redirection received from 110 | * the upstream service). If redirections are enabled, no redirections 111 | * (301, 302, 307, 308) will be passed along to the client, and reaching 112 | * the maximum allowed redirections will return an error response. 113 | * 114 | * @default false 115 | */ 116 | redirects?: number | false | undefined; 117 | /** 118 | * number of milliseconds before aborting the upstream request. Defaults 119 | * to 180000 (3 minutes). 120 | */ 121 | timeout?: number | undefined; 122 | /** 123 | * a function used to map the request URI to the target `uri` and 124 | * optional `headers` with which to make that request. Cannot be used 125 | * together with `host`, `port`, `protocol`, or `uri`. 126 | * @param request - is the incoming request object. 127 | */ 128 | mapUri?: ((this: ProxyHandlerOptions, request: Request) => ProxyTarget | Promise) | undefined; 129 | /** 130 | * a custom function which is passed the upstream request. 131 | * @param req - the [wreck] (https://github.com/hapijs/wreck) request to the upstream server. 132 | */ 133 | onRequest?: ((req: typeof Wreck) => Promise) | undefined; 134 | /** 135 | * a custom function for processing the response from the upstream 136 | * service before sending to the client. Useful for custom error 137 | * handling of responses from the proxied endpoint or other payload 138 | * manipulation. 139 | * @param err - internal or upstream error returned from attempting to 140 | * contact the upstream proxy. TODO: check this is of type BoomError or 141 | * just Error. 142 | * @param res - the node response object received from the upstream 143 | * service. res is a readable stream (use the wreck module read method 144 | * to easily convert it to a Buffer or string). 145 | * @param request - is the incoming request object. 146 | * @param h - Hapi's response toolkit. 147 | * @param settings - the proxy handler configuration. 148 | * @param ttl - the upstream TTL in milliseconds if proxy.ttl it set to 149 | * 'upstream' and the upstream response included a valid 'Cache-Control' 150 | * header with 'max-age'. 151 | */ 152 | onResponse?: 153 | | (( 154 | this: RouteOptions, 155 | err: null | Boom, 156 | res: IncomingMessage, 157 | req: Request, 158 | h: ResponseToolkit, 159 | settings: ProxyHandlerOptions, 160 | ttl: number, 161 | ) => Lifecycle.ReturnValue) 162 | | undefined; 163 | /** 164 | * if set to 'upstream', applies the upstream response caching policy to 165 | * the response using the response.ttl() method (or passed as an 166 | * argument to the onResponse method if provided). 167 | */ 168 | ttl?: 'upstream' | undefined; 169 | /** 170 | * a node http(s) agent to be used for connections to upstream server. 171 | * @see {@link https://nodejs.org/api/http.html#http_class_http_agent} 172 | */ 173 | agent?: Agent | undefined; 174 | /** 175 | * sets the maximum number of sockets available per outgoing proxy host 176 | * connection. false means use the wreck module default value 177 | * (Infinity). Does not affect non-proxy outgoing client connections. 178 | * 179 | * @default Infinity 180 | */ 181 | maxSockets?: false | number | undefined; 182 | /** 183 | * TLS flag indicating the SSL method to use, e.g. SSLv3_method to force 184 | * SSL version 3. The possible values depend on your installation of 185 | * OpenSSL. Read the official OpenSSL docs for possible SSL_METHODS. 186 | */ 187 | secureProtocol?: string | undefined; 188 | /** 189 | * TLS list of TLS ciphers to override node's default. The possible 190 | * values depend on your installation of OpenSSL. Read the official 191 | * OpenSSL docs for possible TLS_CIPHERS. 192 | */ 193 | ciphers?: string | undefined; 194 | /** 195 | * logs the time spent processing the downstream request using 196 | * process.hrtime. 197 | * 198 | * @default false 199 | */ 200 | downstreamResponseTime?: number[] | false | undefined; 201 | } 202 | } 203 | 204 | declare module '@hapi/hapi' { 205 | interface HandlerDecorations { 206 | /** 207 | * Proxies the request to an upstream endpoint. 208 | */ 209 | proxy?: h2o2.ProxyHandlerOptions | undefined; 210 | } 211 | 212 | interface ResponseToolkit { 213 | /** 214 | * Proxies the request to an upstream endpoint. `async`, so you'll need 215 | * to `await` the `ResponseObject` to work on it before returning it. 216 | */ 217 | proxy(options: h2o2.ProxyHandlerOptions): Promise; 218 | } 219 | } 220 | 221 | declare const h2o2: { 222 | plugin: Plugin; 223 | }; 224 | 225 | export = h2o2; 226 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Http = require('http'); 4 | const Https = require('https'); 5 | const Url = require('url'); 6 | 7 | const Hoek = require('@hapi/hoek'); 8 | const Validate = require('@hapi/validate'); 9 | const Wreck = require('@hapi/wreck'); 10 | 11 | 12 | const internals = { 13 | NS_PER_SEC: 1e9, 14 | CHUNKABLE: ['delete'] 15 | }; 16 | 17 | 18 | internals.defaults = { 19 | httpClient: { 20 | request: Wreck.request.bind(Wreck), 21 | parseCacheControl: Wreck.parseCacheControl.bind(Wreck) 22 | }, 23 | xforward: false, 24 | passThrough: false, 25 | redirects: false, 26 | timeout: 1000 * 60 * 3, // Timeout request after 3 minutes 27 | localStatePassThrough: false, // Pass cookies defined by the server upstream 28 | maxSockets: Infinity, 29 | downstreamResponseTime: false 30 | }; 31 | 32 | 33 | internals.schema = Validate.object({ 34 | httpClient: Validate.object({ 35 | request: Validate.func(), 36 | parseCacheControl: Validate.func() 37 | }), 38 | host: Validate.string(), 39 | port: Validate.number().integer(), 40 | protocol: Validate.string().valid('http', 'https', 'http:', 'https:'), 41 | uri: Validate.string(), 42 | passThrough: Validate.boolean(), 43 | localStatePassThrough: Validate.boolean(), 44 | acceptEncoding: Validate.boolean().when('passThrough', { is: true, otherwise: Validate.forbidden() }), 45 | rejectUnauthorized: Validate.boolean(), 46 | xforward: Validate.boolean(), 47 | redirects: Validate.number().min(0).integer().allow(false), 48 | timeout: Validate.number().integer(), 49 | mapUri: Validate.func(), 50 | onResponse: Validate.func(), 51 | onRequest: Validate.func(), 52 | agent: Validate.object(), 53 | ttl: Validate.string().valid('upstream').allow(null), 54 | maxSockets: Validate.number().positive().allow(false), 55 | secureProtocol: Validate.string(), 56 | ciphers: Validate.string(), 57 | downstreamResponseTime: Validate.boolean() 58 | }) 59 | .xor('host', 'mapUri', 'uri') 60 | .without('mapUri', 'port') 61 | .without('mapUri', 'protocol') 62 | .without('uri', 'port') 63 | .without('uri', 'protocol'); 64 | 65 | 66 | exports.plugin = { 67 | pkg: require('../package.json'), 68 | requirements: { 69 | hapi: '>=20.0.0' 70 | }, 71 | register: function (server, options) { 72 | 73 | internals.defaults = Hoek.applyToDefaults(internals.defaults, options); 74 | 75 | server.expose('_agents', new Map()); // server.info.uri -> { http, https, insecure } 76 | server.decorate('handler', 'proxy', internals.handler); 77 | server.decorate('toolkit', 'proxy', internals.toolkit); 78 | } 79 | }; 80 | 81 | 82 | internals.handler = function (route, handlerOptions) { 83 | 84 | const settings = Hoek.applyToDefaults(internals.defaults, handlerOptions, { shallow: ['agent'] }); 85 | Validate.assert(handlerOptions, internals.schema, 'Invalid proxy handler options (' + route.path + ')'); 86 | 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'); 87 | settings.mapUri = handlerOptions.mapUri ?? internals.mapUri(handlerOptions.protocol, handlerOptions.host, handlerOptions.port, handlerOptions.uri); 88 | 89 | if (settings.ttl === 'upstream') { 90 | settings._upstreamTtl = true; 91 | } 92 | 93 | return async function (request, h) { 94 | 95 | const { uri, headers } = await settings.mapUri(request); 96 | 97 | const protocol = uri.split(':', 1)[0]; 98 | 99 | const options = { 100 | headers: {}, 101 | payload: request.payload, 102 | redirects: settings.redirects, 103 | timeout: settings.timeout, 104 | agent: internals.agent(protocol, settings, request) 105 | }; 106 | 107 | const bind = request.route.settings.bind; 108 | 109 | if (settings.passThrough) { 110 | options.headers = Hoek.clone(request.headers); 111 | delete options.headers.host; 112 | delete options.headers['content-length']; 113 | 114 | if (settings.acceptEncoding === false) { // Defaults to true 115 | delete options.headers['accept-encoding']; 116 | } 117 | 118 | if (options.headers.cookie) { 119 | delete options.headers.cookie; 120 | 121 | const cookieHeader = request.server.states.passThrough(request.headers.cookie, settings.localStatePassThrough); 122 | if (cookieHeader) { 123 | if (typeof cookieHeader !== 'string') { 124 | throw cookieHeader; // Error 125 | } 126 | 127 | options.headers.cookie = cookieHeader; 128 | } 129 | } 130 | } 131 | 132 | if (headers) { 133 | Hoek.merge(options.headers, headers); 134 | } 135 | 136 | if (settings.xforward && 137 | request.info.remotePort) { 138 | 139 | options.headers['x-forwarded-for'] = (options.headers['x-forwarded-for'] ? options.headers['x-forwarded-for'] + ',' : '') + request.info.remoteAddress; 140 | options.headers['x-forwarded-port'] = options.headers['x-forwarded-port'] || request.info.remotePort; 141 | options.headers['x-forwarded-proto'] = options.headers['x-forwarded-proto'] || request.server.info.protocol; 142 | options.headers['x-forwarded-host'] = options.headers['x-forwarded-host'] || request.info.host; 143 | } 144 | 145 | if (settings.ciphers) { 146 | options.ciphers = settings.ciphers; 147 | } 148 | 149 | if (settings.secureProtocol) { 150 | options.secureProtocol = settings.secureProtocol; 151 | } 152 | 153 | const contentType = request.headers['content-type']; 154 | if (contentType) { 155 | options.headers['content-type'] = contentType; 156 | } 157 | 158 | 159 | const encoding = options.headers['transfer-encoding']; 160 | if (!encoding && options.payload && internals.CHUNKABLE.includes(request.method)) { 161 | options.headers['transfer-encoding'] = 'chunked'; 162 | } 163 | 164 | let ttl = null; 165 | 166 | let downstreamStartTime; 167 | if (settings.downstreamResponseTime) { 168 | downstreamStartTime = process.hrtime.bigint(); 169 | } 170 | 171 | const promise = settings.httpClient.request(request.method, uri, options); 172 | 173 | request.events.once('disconnect', () => { 174 | 175 | promise.req.destroy(); 176 | }); 177 | 178 | if (settings.onRequest) { 179 | settings.onRequest(promise.req); 180 | } 181 | 182 | try { 183 | var res = await promise; 184 | if (settings.downstreamResponseTime) { 185 | const downstreamResponseTime = Number(process.hrtime.bigint() - downstreamStartTime); 186 | request.log(['h2o2', 'success'], { downstreamResponseTime }); 187 | } 188 | } 189 | catch (err) { 190 | if (settings.downstreamResponseTime) { 191 | const downstreamResponseTime = Number(process.hrtime.bigint() - downstreamStartTime); 192 | request.log(['h2o2', 'error'], { downstreamResponseTime }); 193 | } 194 | 195 | if (settings.onResponse) { 196 | return settings.onResponse.call(bind, err, res, request, h, settings, ttl); 197 | } 198 | 199 | throw err; 200 | } 201 | 202 | if (settings._upstreamTtl) { 203 | const cacheControlHeader = res.headers['cache-control']; 204 | if (cacheControlHeader) { 205 | const cacheControl = settings.httpClient.parseCacheControl(cacheControlHeader); 206 | if (cacheControl) { 207 | ttl = cacheControl['max-age'] * 1000; 208 | } 209 | } 210 | } 211 | 212 | if (settings.onResponse) { 213 | return settings.onResponse.call(bind, null, res, request, h, settings, ttl); 214 | } 215 | 216 | const response = h.response(res) 217 | .ttl(ttl) 218 | .code(res.statusCode) 219 | .passThrough(!!settings.passThrough); 220 | 221 | if (!settings.passThrough && res.statusCode === 405 && 'allow' in res.headers) { 222 | response.header('allow', res.headers.allow); 223 | } 224 | 225 | return response; 226 | 227 | }; 228 | }; 229 | 230 | 231 | internals.handler.defaults = function (method) { 232 | 233 | const payload = method !== 'get' && method !== 'head'; 234 | return payload ? { 235 | payload: { 236 | output: 'stream', 237 | parse: false 238 | } 239 | } : null; 240 | }; 241 | 242 | 243 | internals.toolkit = function (options) { 244 | 245 | return internals.handler(this.request.route, options)(this.request, this); 246 | }; 247 | 248 | 249 | internals.mapUri = function (protocol, host, port, uri) { 250 | 251 | if (uri) { 252 | return function (request) { 253 | 254 | if (uri.indexOf('{') === -1) { 255 | return { uri }; 256 | } 257 | 258 | let address = uri.replace(/{protocol}/g, request.server.info.protocol) 259 | .replace(/{host}/g, request.server.info.host) 260 | .replace(/{port}/g, request.server.info.port) 261 | .replace(/{path}/g, request.path) 262 | .replace(/{query}/g, request.url.search || ''); 263 | 264 | Object.keys(request.params).forEach((key) => { 265 | 266 | const re = new RegExp(`{${key}}`, 'g'); 267 | address = address.replace(re, request.params[key]); 268 | }); 269 | 270 | return { 271 | uri: address 272 | }; 273 | }; 274 | } 275 | 276 | if (protocol && 277 | protocol[protocol.length - 1] !== ':') { 278 | 279 | protocol += ':'; 280 | } 281 | 282 | protocol = protocol ?? 'http:'; 283 | 284 | port = port ?? (protocol === 'http:' ? 80 : 443); 285 | const baseUrl = Url.format({ protocol, hostname: host, port }); 286 | 287 | return function (request) { 288 | 289 | return { 290 | uri: (null, baseUrl + request.path + (request.url.search || '')) 291 | }; 292 | }; 293 | }; 294 | 295 | 296 | internals.agent = function (protocol, settings, request) { 297 | 298 | if (settings.agent) { 299 | return settings.agent; 300 | } 301 | 302 | if (settings.maxSockets === false) { 303 | return undefined; 304 | } 305 | 306 | const store = request.server.plugins.h2o2._agents; 307 | if (!store.has(request.info.uri)) { 308 | store.set(request.info.uri, {}); 309 | } 310 | 311 | const agents = store.get(request.info.uri); 312 | 313 | const type = (protocol === 'http' ? 'http' : (settings.rejectUnauthorized === false ? 'insecure' : 'https')); 314 | if (!agents[type]) { 315 | agents[type] = (type === 'http' ? new Http.Agent() : (type === 'https' ? new Https.Agent() : new Https.Agent({ rejectUnauthorized: false }))); 316 | agents[type].maxSockets = settings.maxSockets; 317 | } 318 | 319 | return agents[type]; 320 | }; 321 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hapi/h2o2", 3 | "description": "Proxy handler plugin for hapi.js", 4 | "version": "10.0.4", 5 | "repository": "git://github.com/hapijs/h2o2", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "engines": { 9 | "node": ">=14.0.0" 10 | }, 11 | "files": [ 12 | "lib" 13 | ], 14 | "keywords": [ 15 | "HTTP", 16 | "proxy", 17 | "handler", 18 | "hapi", 19 | "plugin" 20 | ], 21 | "eslintConfig": { 22 | "extends": [ 23 | "plugin:@hapi/module" 24 | ] 25 | }, 26 | "dependencies": { 27 | "@hapi/boom": "^10.0.1", 28 | "@hapi/hoek": "^11.0.2", 29 | "@hapi/validate": "^2.0.1", 30 | "@hapi/wreck": "^18.0.1" 31 | }, 32 | "devDependencies": { 33 | "@hapi/code": "^9.0.3", 34 | "@hapi/eslint-plugin": "^6.0.0", 35 | "@hapi/hapi": "^21.3.2", 36 | "@hapi/inert": "^7.1.0", 37 | "@hapi/lab": "^25.1.2", 38 | "@hapi/teamwork": "^6.0.0", 39 | "@types/node": "^14.18.48", 40 | "joi": "^17.9.2", 41 | "typescript": "^5.1.3" 42 | }, 43 | "scripts": { 44 | "test": "lab -a @hapi/code -t 100 -L -Y", 45 | "test-cov-html": "lab -a @hapi/code -r html -o coverage.html" 46 | }, 47 | "license": "BSD-3-Clause" 48 | } 49 | -------------------------------------------------------------------------------- /test/esm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Lab = require('@hapi/lab'); 5 | 6 | 7 | const { before, describe, it } = exports.lab = Lab.script(); 8 | const expect = Code.expect; 9 | 10 | 11 | describe('import()', () => { 12 | 13 | let H2o2; 14 | 15 | before(async () => { 16 | 17 | H2o2 = await import('../lib/index.js'); 18 | }); 19 | 20 | it('exposes all methods and classes as named imports', () => { 21 | 22 | expect(Object.keys(H2o2)).to.equal([ 23 | 'default', 24 | 'plugin' 25 | ]); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Fs = require('fs'); 4 | const Http = require('http'); 5 | const Net = require('net'); 6 | const Zlib = require('zlib'); 7 | const Stream = require('stream'); 8 | const Url = require('url'); 9 | 10 | const Boom = require('@hapi/boom'); 11 | const Code = require('@hapi/code'); 12 | const H2o2 = require('..'); 13 | const Hapi = require('@hapi/hapi'); 14 | const Hoek = require('@hapi/hoek'); 15 | const Inert = require('@hapi/inert'); 16 | const Lab = require('@hapi/lab'); 17 | const Wreck = require('@hapi/wreck'); 18 | const { Team } = require('@hapi/teamwork'); 19 | 20 | 21 | const internals = {}; 22 | 23 | 24 | const { it, describe } = exports.lab = Lab.script(); 25 | const expect = Code.expect; 26 | 27 | 28 | describe('h2o2', () => { 29 | 30 | const tlsOptions = { 31 | 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', 32 | 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' 33 | }; 34 | 35 | const getUri = ({ protocol, address, port }) => Url.format({ protocol, hostname: address, port }); 36 | 37 | it('overrides maxSockets', { parallel: false }, async () => { 38 | 39 | let maxSockets; 40 | const httpClient = { 41 | request(method, uri, options, callback) { 42 | 43 | maxSockets = options.agent.maxSockets; 44 | 45 | return { statusCode: 200 }; 46 | } 47 | }; 48 | 49 | const server = Hapi.server(); 50 | await server.register(H2o2); 51 | 52 | server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'anything', httpClient, maxSockets: 213 } } }); 53 | await server.inject('/'); 54 | expect(maxSockets).to.equal(213); 55 | }); 56 | 57 | it('uses node default with maxSockets set to false', { parallel: false }, async () => { 58 | 59 | let agent; 60 | const httpClient = { 61 | request(method, uri, options) { 62 | 63 | agent = options.agent; 64 | 65 | return { statusCode: 200 }; 66 | } 67 | }; 68 | 69 | const server = Hapi.server(); 70 | await server.register(H2o2); 71 | 72 | server.route({ method: 'GET', path: '/', handler: { proxy: { host: 'anything', httpClient, maxSockets: false } } }); 73 | await server.inject('/'); 74 | expect(agent).to.equal(undefined); 75 | }); 76 | 77 | it('forwards on the response when making a GET request', async () => { 78 | 79 | const profileHandler = function (request, h) { 80 | 81 | return h.response({ id: 'fa0dbda9b1b', name: 'John Doe' }).state('test', '123'); 82 | }; 83 | 84 | const upstream = Hapi.server(); 85 | upstream.route({ method: 'GET', path: '/profile', handler: profileHandler, config: { cache: { expiresIn: 2000, privacy: 'private' } } }); 86 | await upstream.start(); 87 | 88 | const server = Hapi.server(); 89 | await server.register(H2o2); 90 | 91 | server.route({ method: 'GET', path: '/profile', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, xforward: true, passThrough: true } } }); 92 | server.state('auto', { autoValue: 'xyz' }); 93 | 94 | const response = await server.inject('/profile'); 95 | expect(response.statusCode).to.equal(200); 96 | expect(response.payload).to.contain('John Doe'); 97 | expect(response.headers['set-cookie'][0]).to.include(['test=123']); 98 | expect(response.headers['set-cookie'][1]).to.include(['auto=xyz']); 99 | expect(response.headers['cache-control']).to.equal('max-age=2, must-revalidate, private'); 100 | 101 | const res = await server.inject('/profile'); 102 | expect(res.statusCode).to.equal(200); 103 | expect(res.payload).to.contain('John Doe'); 104 | 105 | await upstream.stop(); 106 | }); 107 | 108 | it('forwards on the response when making an OPTIONS request', async () => { 109 | 110 | const upstream = Hapi.server(); 111 | upstream.route({ method: 'OPTIONS', path: '/', handler: () => 'test' }); 112 | await upstream.start(); 113 | 114 | const server = Hapi.server(); 115 | await server.register(H2o2); 116 | 117 | server.route({ 118 | method: 'OPTIONS', 119 | path: '/', 120 | options: { 121 | payload: { parse: false }, 122 | handler: (request, h) => h.proxy({ host: upstream.info.address, port: upstream.info.port }) 123 | } 124 | }); 125 | 126 | const res = await server.inject({ method: 'OPTIONS', url: '/' }); 127 | expect(res.statusCode).to.equal(200); 128 | expect(res.result).to.equal('test'); 129 | 130 | await upstream.stop(); 131 | }); 132 | 133 | it('throws when used with explicit route payload config other than data or steam', async () => { 134 | 135 | const server = Hapi.server(); 136 | await server.register(H2o2); 137 | 138 | expect(() => { 139 | 140 | server.route({ 141 | method: 'POST', 142 | path: '/', 143 | config: { 144 | handler: { 145 | proxy: { host: 'example.com' } 146 | }, 147 | payload: { 148 | output: 'file' 149 | } 150 | } 151 | }); 152 | }).to.throw('Cannot proxy if payload is parsed or if output is not stream or data'); 153 | }); 154 | 155 | it('throws when setup with invalid options', async () => { 156 | 157 | const server = Hapi.server(); 158 | await server.register(H2o2); 159 | 160 | expect(() => { 161 | 162 | server.route({ 163 | method: 'POST', 164 | path: '/', 165 | config: { 166 | handler: { 167 | proxy: {} 168 | } 169 | } 170 | }); 171 | }).to.throw(/\"value\" must contain at least one of \[host, mapUri, uri\]/); 172 | }); 173 | 174 | it('throws when used with explicit route payload parse config set to false', async () => { 175 | 176 | const server = Hapi.server(); 177 | await server.register(H2o2); 178 | 179 | expect(() => { 180 | 181 | server.route({ 182 | method: 'POST', 183 | path: '/', 184 | config: { 185 | handler: { 186 | proxy: { host: 'example.com' } 187 | }, 188 | payload: { 189 | parse: true 190 | } 191 | } 192 | }); 193 | }).to.throw('Cannot proxy if payload is parsed or if output is not stream or data'); 194 | }); 195 | 196 | it('allows when used with explicit route payload output data config', async () => { 197 | 198 | const server = Hapi.server(); 199 | await server.register(H2o2); 200 | 201 | expect(() => { 202 | 203 | server.route({ 204 | method: 'POST', 205 | path: '/', 206 | config: { 207 | handler: { 208 | proxy: { host: 'example.com' } 209 | }, 210 | payload: { 211 | output: 'data' 212 | } 213 | } 214 | }); 215 | }).to.not.throw(); 216 | }); 217 | 218 | it('uses protocol without ":"', async () => { 219 | 220 | const upstream = Hapi.server(); 221 | upstream.route({ 222 | method: 'GET', 223 | path: '/', 224 | handler: function (request, h) { 225 | 226 | return 'ok'; 227 | } 228 | }); 229 | 230 | await upstream.start(); 231 | 232 | const server = Hapi.server(); 233 | await server.register(H2o2); 234 | 235 | server.route({ method: 'GET', path: '/', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, protocol: 'http' } } }); 236 | 237 | const res = await server.inject('/'); 238 | expect(res.statusCode).to.equal(200); 239 | expect(res.payload).to.equal('ok'); 240 | 241 | await upstream.stop(); 242 | }); 243 | 244 | it('forwards upstream headers', async () => { 245 | 246 | const headers = function (request, h) { 247 | 248 | return h.response({ status: 'success' }) 249 | .header('Custom1', 'custom header value 1') 250 | .header('X-Custom2', 'custom header value 2') 251 | .header('x-hostFound', request.headers.host) 252 | .header('x-content-length-found', request.headers['content-length']); 253 | }; 254 | 255 | const upstream = Hapi.server(); 256 | upstream.route({ method: 'GET', path: '/headers', handler: headers }); 257 | await upstream.start(); 258 | 259 | const server = Hapi.server({ routes: { cors: true } }); 260 | await server.register(H2o2); 261 | 262 | server.route({ method: 'GET', path: '/headers', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, passThrough: true } } }); 263 | 264 | const res = await server.inject({ 265 | url: '/headers', 266 | headers: { 267 | host: 'www.h2o2.com', 'content-length': 10000 268 | } 269 | }); 270 | expect(res.statusCode).to.equal(200); 271 | expect(res.payload).to.equal('{\"status\":\"success\"}'); 272 | expect(res.headers.custom1).to.equal('custom header value 1'); 273 | expect(res.headers['x-custom2']).to.equal('custom header value 2'); 274 | expect(res.headers['x-hostFound']).to.equal(undefined); 275 | expect(res.headers['x-content-length-found']).to.equal(undefined); 276 | 277 | await upstream.stop(); 278 | }); 279 | 280 | it('merges upstream headers', async () => { 281 | 282 | const handler = function (request, h) { 283 | 284 | return h.response({ status: 'success' }) 285 | .vary('X-Custom3'); 286 | }; 287 | 288 | const onResponse = function (err, res, request, h, settings, ttl) { 289 | 290 | expect(err).to.be.null(); 291 | return h.response(res).vary('Something'); 292 | }; 293 | 294 | const upstream = Hapi.server(); 295 | upstream.route({ method: 'GET', path: '/headers', handler }); 296 | await upstream.start(); 297 | 298 | const server = Hapi.server(); 299 | await server.register(H2o2); 300 | 301 | server.route({ method: 'GET', path: '/headers', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, passThrough: true, onResponse } } }); 302 | 303 | const res = await server.inject({ url: '/headers', headers: { 'accept-encoding': 'gzip' } }); 304 | expect(res.statusCode).to.equal(200); 305 | //expect(res.headers.vary).to.equal('X-Custom3,accept-encoding,Something'); 306 | 307 | await upstream.stop(); 308 | }); 309 | 310 | it('forwards gzipped content', async () => { 311 | 312 | const gzipHandler = function (request, h) { 313 | 314 | return h.response('123456789012345678901234567890123456789012345678901234567890'); 315 | }; 316 | 317 | const upstream = Hapi.server({ compression: { minBytes: 1 } }); // Payloads under 1kb will not be compressed 318 | upstream.route({ method: 'GET', path: '/gzip', handler: gzipHandler }); 319 | await upstream.start(); 320 | 321 | const server = Hapi.server(); 322 | await server.register(H2o2); 323 | 324 | server.route({ method: 'GET', path: '/gzip', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, passThrough: true } } }); 325 | 326 | const zipped = await Zlib.gzipSync(Buffer.from('123456789012345678901234567890123456789012345678901234567890')); 327 | const res = await server.inject({ url: '/gzip', headers: { 'accept-encoding': 'gzip' } }); 328 | 329 | expect(res.statusCode).to.equal(200); 330 | expect(res.rawPayload).to.equal(zipped); 331 | 332 | await upstream.stop(); 333 | }); 334 | 335 | it('forwards gzipped stream', async () => { 336 | 337 | const gzipStreamHandler = function (request, h) { 338 | 339 | return h.file(__dirname + '/../package.json'); 340 | }; 341 | 342 | const upstream = Hapi.server({ compression: { minBytes: 1 } }); 343 | await upstream.register(Inert); 344 | upstream.route({ method: 'GET', path: '/gzipstream', handler: gzipStreamHandler }); 345 | await upstream.start(); 346 | 347 | const server = Hapi.server(); 348 | await server.register(H2o2); 349 | 350 | server.route({ method: 'GET', path: '/gzipstream', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, passThrough: true } } }); 351 | 352 | const res = await server.inject({ url: '/gzipstream', headers: { 'accept-encoding': 'gzip' } }); 353 | const file = Fs.readFileSync(__dirname + '/../package.json', { encoding: 'utf8' }); 354 | const unzipped = Zlib.unzipSync(res.rawPayload); 355 | 356 | expect(unzipped.toString('utf8')).to.equal(file); 357 | expect(res.statusCode).to.equal(200); 358 | 359 | await upstream.stop(); 360 | }); 361 | 362 | it('does not forward upstream headers without passThrough', async () => { 363 | 364 | const headers = function (request, h) { 365 | 366 | return h.response({ status: 'success' }) 367 | .header('Custom1', 'custom header value 1') 368 | .header('X-Custom2', 'custom header value 2') 369 | .header('access-control-allow-headers', 'Invalid, List, Of, Values'); 370 | }; 371 | 372 | const upstream = Hapi.server(); 373 | upstream.route({ method: 'GET', path: '/noHeaders', handler: headers }); 374 | await upstream.start(); 375 | 376 | const server = Hapi.server(); 377 | await server.register(H2o2); 378 | 379 | server.route({ method: 'GET', path: '/noHeaders', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } } }); 380 | 381 | const res = await server.inject('/noHeaders'); 382 | expect(res.statusCode).to.equal(200); 383 | expect(res.payload).to.equal('{\"status\":\"success\"}'); 384 | expect(res.headers.custom1).to.not.exist(); 385 | expect(res.headers['x-custom2']).to.not.exist(); 386 | 387 | await upstream.stop(); 388 | }); 389 | 390 | it('request a cached proxy route', async () => { 391 | 392 | let activeCount = 0; 393 | const handler = function (request, h) { 394 | 395 | return h.response({ 396 | id: '55cf687663', 397 | name: 'Active Items', 398 | count: activeCount++ 399 | }); 400 | }; 401 | 402 | const upstream = Hapi.server(); 403 | upstream.route({ method: 'GET', path: '/item', handler }); 404 | await upstream.start(); 405 | 406 | const server = Hapi.server(); 407 | await server.register(H2o2); 408 | 409 | server.route({ method: 'GET', path: '/item', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, protocol: 'http:' } }, config: { cache: { expiresIn: 500 } } }); 410 | 411 | const response = await server.inject('/item'); 412 | expect(response.statusCode).to.equal(200); 413 | expect(response.payload).to.contain('Active Items'); 414 | const counter = response.result.count; 415 | 416 | const res = await server.inject('/item'); 417 | expect(res.statusCode).to.equal(200); 418 | expect(res.result.count).to.equal(counter); 419 | 420 | await upstream.stop(); 421 | }); 422 | 423 | it('forwards on the status code when making a POST request', async () => { 424 | 425 | const item = function (request, h) { 426 | 427 | return h.response({ id: '55cf687663', name: 'Items' }).created('http://example.com'); 428 | }; 429 | 430 | const upstream = Hapi.server(); 431 | upstream.route({ method: 'POST', path: '/item', handler: item }); 432 | await upstream.start(); 433 | 434 | const server = Hapi.server(); 435 | await server.register(H2o2); 436 | 437 | server.route({ method: 'POST', path: '/item', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } } }); 438 | 439 | const res = await server.inject({ url: '/item', method: 'POST' }); 440 | expect(res.statusCode).to.equal(201); 441 | expect(res.payload).to.contain('Items'); 442 | 443 | await upstream.stop(); 444 | }); 445 | 446 | it('sends the correct status code when a request is unauthorized', async () => { 447 | 448 | const unauthorized = function (request, h) { 449 | 450 | throw Boom.unauthorized('Not authorized'); 451 | }; 452 | 453 | const upstream = Hapi.server(); 454 | upstream.route({ method: 'GET', path: '/unauthorized', handler: unauthorized }); 455 | await upstream.start(); 456 | 457 | const server = Hapi.server(); 458 | await server.register(H2o2); 459 | 460 | server.route({ method: 'GET', path: '/unauthorized', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } }, config: { cache: { expiresIn: 500 } } }); 461 | 462 | const res = await server.inject('/unauthorized'); 463 | expect(res.statusCode).to.equal(401); 464 | 465 | await upstream.stop(); 466 | }); 467 | 468 | it('sends a 404 status code when a proxied route does not exist', async () => { 469 | 470 | const upstream = Hapi.server(); 471 | await upstream.start(); 472 | 473 | const server = Hapi.server(); 474 | await server.register(H2o2); 475 | 476 | server.route({ method: 'POST', path: '/notfound', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } } }); 477 | 478 | const res = await server.inject('/notfound'); 479 | expect(res.statusCode).to.equal(404); 480 | 481 | await upstream.stop(); 482 | }); 483 | 484 | it('overrides status code when a custom onResponse returns an error', async () => { 485 | 486 | const onResponseWithError = function (err, res, request, h, settings, ttl) { 487 | 488 | expect(err).to.be.null(); 489 | throw Boom.forbidden('Forbidden'); 490 | }; 491 | 492 | const upstream = Hapi.server(); 493 | await upstream.start(); 494 | 495 | const server = Hapi.server(); 496 | await server.register(H2o2); 497 | 498 | server.route({ method: 'GET', path: '/onResponseError', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, onResponse: onResponseWithError } } }); 499 | 500 | const res = await server.inject('/onResponseError'); 501 | expect(res.statusCode).to.equal(403); 502 | 503 | await upstream.stop(); 504 | }); 505 | 506 | it('adds cookie to response', async () => { 507 | 508 | const on = function (err, res, request, h, settings, ttl) { 509 | 510 | expect(err).to.be.null(); 511 | return h.response(res).state('a', 'b'); 512 | }; 513 | 514 | const upstream = Hapi.server(); 515 | await upstream.start(); 516 | 517 | const server = Hapi.server(); 518 | await server.register(H2o2); 519 | 520 | server.route({ method: 'GET', path: '/', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, onResponse: on } } }); 521 | 522 | const res = await server.inject('/'); 523 | 524 | expect(res.statusCode).to.equal(404); 525 | expect(res.headers['set-cookie'][0]).to.equal('a=b; Secure; HttpOnly; SameSite=Strict'); 526 | 527 | await upstream.stop(); 528 | }); 529 | 530 | it('calls onRequest when it\'s created', async () => { 531 | 532 | const upstream = Hapi.Server(); 533 | 534 | let upstreamRequested = false; 535 | upstream.events.on('request', () => { 536 | 537 | upstreamRequested = true; 538 | }); 539 | 540 | await upstream.start(); 541 | 542 | let called = false; 543 | const onRequestWithSocket = function (req) { 544 | 545 | called = true; 546 | expect(upstreamRequested).to.be.false(); 547 | expect(req).to.be.an.instanceof(Http.ClientRequest); 548 | }; 549 | 550 | const on = function (err, res, request, h, settings, ttl) { 551 | 552 | expect(err).to.be.null(); 553 | return h.response(h.context.c); 554 | }; 555 | 556 | const handler = { 557 | proxy: { 558 | host: upstream.info.address, 559 | port: upstream.info.port, 560 | onRequest: onRequestWithSocket, 561 | onResponse: on 562 | } 563 | }; 564 | 565 | const server = Hapi.server(); 566 | await server.register(H2o2); 567 | 568 | server.route({ method: 'GET', path: '/onRequestSocket', config: { handler, bind: { c: 6 } } }); 569 | 570 | const res = await server.inject('/onRequestSocket'); 571 | 572 | expect(res.result).to.equal(6); 573 | expect(called).to.equal(true); 574 | await upstream.stop(); 575 | }); 576 | 577 | it('binds onResponse to route bind config', async () => { 578 | 579 | const onResponseWithError = function (err, res, request, h, settings, ttl) { 580 | 581 | expect(err).to.be.null(); 582 | return h.response(h.context.c); 583 | }; 584 | 585 | const upstream = Hapi.server(); 586 | await upstream.start(); 587 | 588 | const handler = { 589 | proxy: { 590 | host: upstream.info.address, 591 | port: upstream.info.port, 592 | onResponse: onResponseWithError 593 | } 594 | }; 595 | 596 | const server = Hapi.server(); 597 | await server.register(H2o2); 598 | 599 | server.route({ method: 'GET', path: '/onResponseError', config: { handler, bind: { c: 6 } } }); 600 | 601 | const res = await server.inject('/onResponseError'); 602 | expect(res.result).to.equal(6); 603 | 604 | await upstream.stop(); 605 | }); 606 | 607 | it('binds onResponse to route bind config in plugin', async () => { 608 | 609 | const upstream = Hapi.server(); 610 | await upstream.start(); 611 | 612 | const plugin = { 613 | register: function (server, optionos) { 614 | 615 | const onResponseWithError = function (err, res, request, h, settings, ttl) { 616 | 617 | expect(err).to.be.null(); 618 | return h.response(h.context.c); 619 | }; 620 | 621 | const handler = { 622 | proxy: { 623 | host: upstream.info.address, 624 | port: upstream.info.port, 625 | onResponse: onResponseWithError 626 | } 627 | }; 628 | 629 | server.route({ method: 'GET', path: '/', config: { handler, bind: { c: 6 } } }); 630 | }, 631 | name: 'test' 632 | }; 633 | 634 | const server = Hapi.server(); 635 | await server.register(H2o2); 636 | 637 | await server.register(plugin); 638 | 639 | const res = await server.inject('/'); 640 | expect(res.result).to.equal(6); 641 | 642 | await upstream.stop(); 643 | }); 644 | 645 | it('binds onResponse to plugin bind', async () => { 646 | 647 | const upstream = Hapi.server(); 648 | await upstream.start(); 649 | 650 | const plugin = { 651 | register: function (server, options) { 652 | 653 | const onResponseWithError = function (err, res, request, h, settings, ttl) { 654 | 655 | expect(err).to.be.null(); 656 | return h.response(h.context.c); 657 | }; 658 | 659 | const handler = { 660 | proxy: { 661 | host: upstream.info.address, 662 | port: upstream.info.port, 663 | onResponse: onResponseWithError 664 | } 665 | }; 666 | 667 | server.bind({ c: 7 }); 668 | server.route({ method: 'GET', path: '/', config: { handler } }); 669 | }, 670 | name: 'test' 671 | }; 672 | 673 | const server = Hapi.server(); 674 | await server.register(H2o2); 675 | 676 | await server.register(plugin); 677 | 678 | const res = await server.inject('/'); 679 | expect(res.result).to.equal(7); 680 | 681 | await upstream.stop(); 682 | }); 683 | 684 | it('binds onResponse to route bind config in plugin when plugin also has bind', async () => { 685 | 686 | const upstream = Hapi.server(); 687 | await upstream.start(); 688 | 689 | const plugin = { 690 | register: function (server, options) { 691 | 692 | const onResponseWithError = function (err, res, request, h, settings, ttl) { 693 | 694 | expect(err).to.be.null(); 695 | return h.response(h.context.c); 696 | }; 697 | 698 | const handler = { 699 | proxy: { 700 | host: upstream.info.address, 701 | port: upstream.info.port, 702 | onResponse: onResponseWithError 703 | } 704 | }; 705 | 706 | server.bind({ c: 7 }); 707 | server.route({ method: 'GET', path: '/', config: { handler, bind: { c: 4 } } }); 708 | }, 709 | name: 'test' 710 | }; 711 | 712 | const server = Hapi.server(); 713 | await server.register(H2o2); 714 | 715 | await server.register(plugin); 716 | 717 | const res = await server.inject('/'); 718 | expect(res.result).to.equal(4); 719 | 720 | await upstream.stop(); 721 | }); 722 | 723 | it('calls the onResponse function if the upstream is unreachable', async () => { 724 | 725 | const failureResponse = function (err, res, request, h, settings, ttl) { 726 | 727 | expect(h.response).to.exist(); 728 | throw err; 729 | }; 730 | 731 | const dummy = Hapi.server(); 732 | await dummy.start(); 733 | const dummyPort = dummy.info.port; 734 | await dummy.stop(Hoek.ignore); 735 | 736 | const server = Hapi.server(); 737 | await server.register(H2o2); 738 | 739 | server.route({ method: 'GET', path: '/failureResponse', handler: { proxy: { host: dummy.info.address, port: dummyPort, onResponse: failureResponse } }, config: { cache: { expiresIn: 500 } } }); 740 | 741 | const res = await server.inject('/failureResponse'); 742 | expect(res.statusCode).to.equal(502); 743 | }); 744 | 745 | it('sets x-forwarded-* headers', async () => { 746 | 747 | const handler = function (request, h) { 748 | 749 | return h.response(request.raw.req.headers); 750 | }; 751 | 752 | const host = '127.0.0.1'; 753 | 754 | const upstream = Hapi.server({ host }); 755 | upstream.route({ method: 'GET', path: '/', handler }); 756 | await upstream.start(); 757 | 758 | const server = Hapi.server({ host, tls: tlsOptions }); 759 | await server.register(H2o2); 760 | 761 | server.route({ 762 | method: 'GET', 763 | path: '/', 764 | handler: { 765 | proxy: { 766 | host: upstream.info.host, 767 | port: upstream.info.port, 768 | protocol: 'http', 769 | xforward: true 770 | } 771 | } 772 | }); 773 | await server.start(); 774 | 775 | const requestProtocol = 'https'; 776 | const response = await Wreck.get(`${requestProtocol}://${server.info.host}:${server.info.port}/`, { 777 | rejectUnauthorized: false 778 | }); 779 | expect(response.res.statusCode).to.equal(200); 780 | 781 | const result = JSON.parse(response.payload); 782 | let expectedClientAddress = '127.0.0.1'; 783 | let expectedClientAddressAndPort = expectedClientAddress + ':' + server.info.port; 784 | 785 | if (Net.isIPv6(server.listener.address().address)) { 786 | expectedClientAddress = '::ffff:127.0.0.1'; 787 | expectedClientAddressAndPort = '[' + expectedClientAddress + ']:' + server.info.port; 788 | } 789 | 790 | expect(result['x-forwarded-for']).to.equal(expectedClientAddress); 791 | expect(result['x-forwarded-port']).to.match(/\d+/); 792 | expect(result['x-forwarded-proto']).to.equal(requestProtocol); 793 | expect(result['x-forwarded-host']).to.equal(expectedClientAddressAndPort); 794 | 795 | await server.stop(); 796 | await upstream.stop(); 797 | }); 798 | 799 | it('adds x-forwarded-for headers to existing and preserves original port, proto and host', async () => { 800 | 801 | const handler = function (request, h) { 802 | 803 | return h.response(request.raw.req.headers); 804 | }; 805 | 806 | const upstream = Hapi.server(); 807 | upstream.route({ method: 'GET', path: '/', handler }); 808 | await upstream.start(); 809 | 810 | const mapUri = function (request) { 811 | 812 | const headers = { 813 | 'x-forwarded-for': 'testhost', 814 | 'x-forwarded-port': 1337, 815 | 'x-forwarded-proto': 'https', 816 | 'x-forwarded-host': 'example.com' 817 | }; 818 | 819 | return { 820 | uri: `http://127.0.0.1:${upstream.info.port}/`, 821 | headers 822 | }; 823 | }; 824 | 825 | const server = Hapi.server({ host: '127.0.0.1' }); 826 | await server.register(H2o2); 827 | 828 | server.route({ method: 'GET', path: '/', handler: { proxy: { mapUri, xforward: true } } }); 829 | await server.start(); 830 | 831 | const response = await Wreck.get('http://127.0.0.1:' + server.info.port + '/'); 832 | expect(response.res.statusCode).to.equal(200); 833 | 834 | const result = JSON.parse(response.payload); 835 | 836 | let expectedClientAddress = '127.0.0.1'; 837 | if (Net.isIPv6(server.listener.address().address)) { 838 | expectedClientAddress = '::ffff:127.0.0.1'; 839 | } 840 | 841 | expect(result['x-forwarded-for']).to.equal('testhost,' + expectedClientAddress); 842 | expect(result['x-forwarded-port']).to.equal('1337'); 843 | expect(result['x-forwarded-proto']).to.equal('https'); 844 | expect(result['x-forwarded-host']).to.equal('example.com'); 845 | 846 | await upstream.stop(); 847 | await server.stop(); 848 | }); 849 | 850 | it('does not clobber existing x-forwarded-* headers', async () => { 851 | 852 | const handler = function (request, h) { 853 | 854 | return h.response(request.raw.req.headers); 855 | }; 856 | 857 | const mapUri = function (request) { 858 | 859 | const headers = { 860 | 'x-forwarded-for': 'testhost', 861 | 'x-forwarded-port': 1337, 862 | 'x-forwarded-proto': 'https', 863 | 'x-forwarded-host': 'example.com' 864 | }; 865 | 866 | return { 867 | uri: `http://127.0.0.1:${upstream.info.port}/`, 868 | headers 869 | }; 870 | }; 871 | 872 | const upstream = Hapi.server(); 873 | upstream.route({ method: 'GET', path: '/', handler }); 874 | await upstream.start(); 875 | 876 | const server = Hapi.server(); 877 | await server.register(H2o2); 878 | 879 | server.route({ method: 'GET', path: '/', handler: { proxy: { mapUri, xforward: true } } }); 880 | 881 | const res = await server.inject('/'); 882 | const result = JSON.parse(res.payload); 883 | expect(res.statusCode).to.equal(200); 884 | expect(result['x-forwarded-for']).to.equal('testhost'); 885 | expect(result['x-forwarded-port']).to.equal('1337'); 886 | expect(result['x-forwarded-proto']).to.equal('https'); 887 | expect(result['x-forwarded-host']).to.equal('example.com'); 888 | 889 | await upstream.stop(); 890 | }); 891 | 892 | it('does not clobber existing transfer-encoding header', async () => { 893 | 894 | const echoDeleteBody = function (request, h) { 895 | 896 | return h.response(request.raw.req.headers['transfer-encoding']); 897 | }; 898 | 899 | const mapUri = function (request) { 900 | 901 | return { 902 | uri: `http://127.0.0.1:${upstream.info.port}${request.path}${(request.url.search || '')}`, 903 | headers: { 'transfer-encoding': 'gzip,chunked' } 904 | }; 905 | }; 906 | 907 | const upstream = Hapi.server(); 908 | upstream.route({ method: 'DELETE', path: '/echo', handler: echoDeleteBody }); 909 | await upstream.start(); 910 | 911 | const server = Hapi.server(); 912 | await server.register(H2o2); 913 | 914 | server.route({ method: 'DELETE', path: '/echo', handler: { proxy: { mapUri } } }); 915 | 916 | const res = await server.inject({ url: '/echo', method: 'DELETE' }); 917 | expect(res.statusCode).to.equal(200); 918 | expect(res.payload).to.equal('gzip,chunked'); 919 | 920 | await upstream.stop(); 921 | }); 922 | 923 | it('forwards on a POST body', async () => { 924 | 925 | const echoPostBody = function (request, h) { 926 | 927 | return h.response(request.payload.echo + request.raw.req.headers['x-super-special']); 928 | }; 929 | 930 | const mapUri = function (request) { 931 | 932 | return { 933 | uri: `http://127.0.0.1:${upstream.info.port}${request.path}${(request.url.search || '')}`, 934 | headers: { 'x-super-special': '@' } 935 | }; 936 | }; 937 | 938 | const upstream = Hapi.server(); 939 | upstream.route({ method: 'POST', path: '/echo', handler: echoPostBody }); 940 | await upstream.start(); 941 | 942 | const server = Hapi.server(); 943 | await server.register(H2o2); 944 | 945 | server.route({ method: 'POST', path: '/echo', handler: { proxy: { mapUri } } }); 946 | 947 | const res = await server.inject({ url: '/echo', method: 'POST', payload: '{"echo":true}' }); 948 | expect(res.statusCode).to.equal(200); 949 | expect(res.payload).to.equal('true@'); 950 | 951 | await upstream.stop(); 952 | }); 953 | 954 | it('forwards on a DELETE body', async () => { 955 | 956 | const echoDeleteBody = function (request, h) { 957 | 958 | return h.response(request.payload.echo + request.raw.req.headers['x-super-special']); 959 | }; 960 | 961 | const mapUri = function (request) { 962 | 963 | return { 964 | uri: `http://127.0.0.1:${upstream.info.port}${request.path}${(request.url.search || '')}`, 965 | headers: { 'x-super-special': '@' } 966 | }; 967 | }; 968 | 969 | const upstream = Hapi.server(); 970 | upstream.route({ method: 'DELETE', path: '/echo', handler: echoDeleteBody }); 971 | await upstream.start(); 972 | 973 | const server = Hapi.server(); 974 | await server.register(H2o2); 975 | 976 | server.route({ method: 'DELETE', path: '/echo', handler: { proxy: { mapUri } } }); 977 | 978 | const res = await server.inject({ url: '/echo', method: 'DELETE', payload: '{"echo":true}' }); 979 | expect(res.statusCode).to.equal(200); 980 | expect(res.payload).to.equal('true@'); 981 | 982 | await upstream.stop(); 983 | }); 984 | 985 | it('forwards on a PUT body', async () => { 986 | 987 | const echoPutBody = function (request, h) { 988 | 989 | return h.response(request.payload.echo + request.raw.req.headers['x-super-special']); 990 | }; 991 | 992 | const mapUri = function (request) { 993 | 994 | return { 995 | uri: `http://127.0.0.1:${upstream.info.port}${request.path}${(request.url.search || '')}`, 996 | headers: { 'x-super-special': '@' } 997 | }; 998 | }; 999 | 1000 | const upstream = Hapi.server(); 1001 | upstream.route({ method: 'PUT', path: '/echo', handler: echoPutBody }); 1002 | await upstream.start(); 1003 | 1004 | const server = Hapi.server(); 1005 | await server.register(H2o2); 1006 | 1007 | server.route({ method: 'PUT', path: '/echo', handler: { proxy: { mapUri } } }); 1008 | 1009 | const res = await server.inject({ url: '/echo', method: 'PUT', payload: '{"echo":true}' }); 1010 | expect(res.statusCode).to.equal(200); 1011 | expect(res.payload).to.equal('true@'); 1012 | 1013 | await upstream.stop(); 1014 | }); 1015 | 1016 | it('forwards on a PATCH body', async () => { 1017 | 1018 | const echoPatchBody = function (request, h) { 1019 | 1020 | return h.response(request.payload.echo + request.raw.req.headers['x-super-special']); 1021 | }; 1022 | 1023 | const mapUri = function (request) { 1024 | 1025 | return { 1026 | uri: `http://127.0.0.1:${upstream.info.port}${request.path}${(request.url.search || '')}`, 1027 | headers: { 'x-super-special': '@' } 1028 | }; 1029 | }; 1030 | 1031 | const upstream = Hapi.server(); 1032 | upstream.route({ method: 'PATCH', path: '/echo', handler: echoPatchBody }); 1033 | await upstream.start(); 1034 | 1035 | const server = Hapi.server(); 1036 | await server.register(H2o2); 1037 | 1038 | server.route({ method: 'PATCH', path: '/echo', handler: { proxy: { mapUri } } }); 1039 | 1040 | const res = await server.inject({ url: '/echo', method: 'PATCH', payload: '{"echo":true}' }); 1041 | expect(res.statusCode).to.equal(200); 1042 | expect(res.payload).to.equal('true@'); 1043 | 1044 | await upstream.stop(); 1045 | }); 1046 | 1047 | it('replies with an error when it occurs in mapUri', async () => { 1048 | 1049 | const mapUriWithError = function (request) { 1050 | 1051 | throw new Error('myerror'); 1052 | }; 1053 | 1054 | const server = Hapi.server(); 1055 | await server.register(H2o2); 1056 | 1057 | server.route({ method: 'GET', path: '/maperror', handler: { proxy: { mapUri: mapUriWithError } } }); 1058 | const res = await server.inject('/maperror'); 1059 | 1060 | expect(res.statusCode).to.equal(500); 1061 | }); 1062 | 1063 | it('maxs out redirects to same endpoint', async () => { 1064 | 1065 | const redirectHandler = function (request, h) { 1066 | 1067 | return h.redirect('/redirect?x=1'); 1068 | }; 1069 | 1070 | const upstream = Hapi.server(); 1071 | upstream.route({ method: 'GET', path: '/redirect', handler: redirectHandler }); 1072 | await upstream.start(); 1073 | 1074 | const server = Hapi.server(); 1075 | await server.register(H2o2); 1076 | 1077 | server.route({ method: 'GET', path: '/redirect', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, passThrough: true, redirects: 2 } } }); 1078 | 1079 | const res = await server.inject('/redirect?x=1'); 1080 | expect(res.statusCode).to.equal(502); 1081 | 1082 | await upstream.stop(); 1083 | }); 1084 | 1085 | it('errors on redirect missing location header', async () => { 1086 | 1087 | const redirectHandler = function (request, h) { 1088 | 1089 | return h.response().code(302); 1090 | }; 1091 | 1092 | const upstream = Hapi.server(); 1093 | upstream.route({ method: 'GET', path: '/redirect', handler: redirectHandler }); 1094 | await upstream.start(); 1095 | 1096 | const server = Hapi.server(); 1097 | await server.register(H2o2); 1098 | 1099 | server.route({ method: 'GET', path: '/redirect', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, passThrough: true, redirects: 2 } } }); 1100 | 1101 | const res = await server.inject('/redirect?x=3'); 1102 | expect(res.statusCode).to.equal(502); 1103 | 1104 | await upstream.stop(); 1105 | }); 1106 | 1107 | it('errors on redirection to bad host', async () => { 1108 | 1109 | const server = Hapi.server(); 1110 | await server.register(H2o2); 1111 | 1112 | server.route({ method: 'GET', path: '/nowhere', handler: { proxy: { host: 'no.such.domain.x8' } } }); 1113 | 1114 | const res = await server.inject('/nowhere'); 1115 | expect(res.statusCode).to.equal(502); 1116 | }); 1117 | 1118 | it('errors on redirection to bad host (https)', async () => { 1119 | 1120 | const server = Hapi.server(); 1121 | await server.register(H2o2); 1122 | 1123 | server.route({ method: 'GET', path: '/nowhere', handler: { proxy: { host: 'no.such.domain.x8', protocol: 'https' } } }); 1124 | 1125 | const res = await server.inject('/nowhere'); 1126 | expect(res.statusCode).to.equal(502); 1127 | }); 1128 | 1129 | it('redirects to another endpoint', async () => { 1130 | 1131 | const redirectHandler = function (request, h) { 1132 | 1133 | return h.redirect('/profile'); 1134 | }; 1135 | 1136 | const profile = function (request, h) { 1137 | 1138 | return h.response({ id: 'fa0dbda9b1b', name: 'John Doe' }).state('test', '123'); 1139 | }; 1140 | 1141 | const upstream = Hapi.server(); 1142 | upstream.route({ method: 'GET', path: '/redirect', handler: redirectHandler }); 1143 | upstream.route({ method: 'GET', path: '/profile', handler: profile, config: { cache: { expiresIn: 2000 } } }); 1144 | await upstream.start(); 1145 | 1146 | const server = Hapi.server(); 1147 | await server.register(H2o2); 1148 | 1149 | server.route({ method: 'GET', path: '/redirect', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, passThrough: true, redirects: 2 } } }); 1150 | server.state('auto', { autoValue: 'xyz' }); 1151 | 1152 | const res = await server.inject('/redirect'); 1153 | expect(res.statusCode).to.equal(200); 1154 | expect(res.payload).to.contain('John Doe'); 1155 | expect(res.headers['set-cookie'][0]).to.include(['test=123']); 1156 | expect(res.headers['set-cookie'][1]).to.include(['auto=xyz']); 1157 | 1158 | await upstream.stop(); 1159 | }); 1160 | 1161 | it('redirects to another endpoint with relative location', async () => { 1162 | 1163 | const redirectHandler = function (request, h) { 1164 | 1165 | return h.response().header('Location', `${getUri(request.server.info)}/profile`).code(302); 1166 | }; 1167 | 1168 | const profile = function (request, h) { 1169 | 1170 | return h.response({ id: 'fa0dbda9b1b', name: 'John Doe' }).state('test', '123'); 1171 | }; 1172 | 1173 | const upstream = Hapi.server(); 1174 | upstream.route({ method: 'GET', path: '/redirect', handler: redirectHandler }); 1175 | upstream.route({ method: 'GET', path: '/profile', handler: profile, config: { cache: { expiresIn: 2000 } } }); 1176 | await upstream.start(); 1177 | 1178 | const server = Hapi.server(); 1179 | await server.register(H2o2); 1180 | 1181 | server.route({ method: 'GET', path: '/redirect', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, passThrough: true, redirects: 2 } } }); 1182 | server.state('auto', { autoValue: 'xyz' }); 1183 | 1184 | const res = await server.inject('/redirect?x=2'); 1185 | expect(res.statusCode).to.equal(200); 1186 | expect(res.payload).to.contain('John Doe'); 1187 | expect(res.headers['set-cookie'][0]).to.include(['test=123']); 1188 | expect(res.headers['set-cookie'][1]).to.include(['auto=xyz']); 1189 | 1190 | await upstream.stop(); 1191 | }); 1192 | 1193 | it('redirects to a post endpoint with stream', async () => { 1194 | 1195 | const upstream = Hapi.server(); 1196 | upstream.route({ 1197 | method: 'POST', 1198 | path: '/post1', 1199 | handler: function (request, h) { 1200 | 1201 | return h.redirect('/post2').rewritable(false); 1202 | } 1203 | }); 1204 | upstream.route({ 1205 | method: 'POST', 1206 | path: '/post2', 1207 | handler: function (request, h) { 1208 | 1209 | return h.response(request.payload); 1210 | } 1211 | }); 1212 | 1213 | await upstream.start(); 1214 | 1215 | const server = Hapi.server(); 1216 | await server.register(H2o2); 1217 | 1218 | server.route({ method: 'POST', path: '/post1', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, redirects: 3 } }, config: { payload: { output: 'stream' } } }); 1219 | 1220 | const res = await server.inject({ method: 'POST', url: '/post1', payload: 'test', headers: { 'content-type': 'text/plain' } }); 1221 | expect(res.statusCode).to.equal(200); 1222 | expect(res.payload).to.equal('test'); 1223 | 1224 | await upstream.stop(); 1225 | }); 1226 | 1227 | it('errors when proxied request times out', async () => { 1228 | 1229 | const upstream = Hapi.server(); 1230 | upstream.route({ 1231 | method: 'GET', 1232 | path: '/timeout1', 1233 | handler: function (request, h) { 1234 | 1235 | return new Promise((resolve, reject) => { 1236 | 1237 | setTimeout(() => { 1238 | 1239 | return resolve(h.response('Ok')); 1240 | }, 10); 1241 | }); 1242 | 1243 | } 1244 | }); 1245 | 1246 | await upstream.start(); 1247 | 1248 | const server = Hapi.server(); 1249 | await server.register(H2o2); 1250 | 1251 | server.route({ method: 'GET', path: '/timeout1', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, timeout: 5 } } }); 1252 | 1253 | const res = await server.inject('/timeout1'); 1254 | expect(res.statusCode).to.equal(504); 1255 | 1256 | await upstream.stop(); 1257 | }); 1258 | 1259 | it('uses default timeout when nothing is set', async () => { 1260 | 1261 | const upstream = Hapi.server(); 1262 | upstream.route({ 1263 | 1264 | method: 'GET', 1265 | path: '/timeout2', 1266 | handler: function (request, h) { 1267 | 1268 | return new Promise((resolve, reject) => { 1269 | 1270 | setTimeout(() => { 1271 | 1272 | return resolve(h.response('Ok')); 1273 | }, 10); 1274 | }); 1275 | } 1276 | }); 1277 | 1278 | await upstream.start(); 1279 | 1280 | const server = Hapi.server(); 1281 | await server.register(H2o2); 1282 | 1283 | server.route({ method: 'GET', path: '/timeout2', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } } }); 1284 | 1285 | const res = await server.inject('/timeout2'); 1286 | expect(res.statusCode).to.equal(200); 1287 | 1288 | await upstream.stop(); 1289 | }); 1290 | 1291 | it('uses rejectUnauthorized to allow proxy to self sign ssl server', async () => { 1292 | 1293 | 1294 | const upstream = Hapi.server({ tls: tlsOptions }); 1295 | upstream.route({ 1296 | method: 'GET', 1297 | path: '/', 1298 | handler: function (request, h) { 1299 | 1300 | return h.response('Ok'); 1301 | } 1302 | }); 1303 | 1304 | await upstream.start(); 1305 | 1306 | const mapSslUri = function (request) { 1307 | 1308 | return { 1309 | uri: `https://127.0.0.1:${upstream.info.port}` 1310 | }; 1311 | }; 1312 | 1313 | const server = Hapi.server(); 1314 | await server.register(H2o2); 1315 | 1316 | server.route({ method: 'GET', path: '/allow', handler: { proxy: { mapUri: mapSslUri, rejectUnauthorized: false } } }); 1317 | await server.start(); 1318 | 1319 | const res = await server.inject('/allow'); 1320 | expect(res.statusCode).to.equal(200); 1321 | expect(res.payload).to.equal('Ok'); 1322 | 1323 | await server.stop(); 1324 | await upstream.stop(); 1325 | }); 1326 | 1327 | it('uses rejectUnauthorized to not allow proxy to self sign ssl server', async () => { 1328 | 1329 | const upstream = Hapi.server({ tls: tlsOptions }); 1330 | upstream.route({ 1331 | method: 'GET', 1332 | path: '/', 1333 | handler: function (request, h) { 1334 | 1335 | return h.response('Ok'); 1336 | } 1337 | }); 1338 | 1339 | await upstream.start(); 1340 | 1341 | const mapSslUri = function (request, h) { 1342 | 1343 | return { 1344 | uri: `https://127.0.0.1:${upstream.info.port}` 1345 | }; 1346 | }; 1347 | 1348 | const server = Hapi.server(); 1349 | await server.register(H2o2); 1350 | 1351 | server.route({ method: 'GET', path: '/reject', handler: { proxy: { mapUri: mapSslUri, rejectUnauthorized: true } } }); 1352 | await server.start(); 1353 | 1354 | const res = await server.inject('/reject'); 1355 | expect(res.statusCode).to.equal(502); 1356 | 1357 | await server.stop(); 1358 | await upstream.stop(); 1359 | }); 1360 | 1361 | it('the default rejectUnauthorized should not allow proxied server cert to be self signed', async () => { 1362 | 1363 | const upstream = Hapi.server({ tls: tlsOptions }); 1364 | upstream.route({ 1365 | method: 'GET', 1366 | path: '/', 1367 | handler: function (request, h) { 1368 | 1369 | return h.response('Ok'); 1370 | } 1371 | }); 1372 | 1373 | await upstream.start(); 1374 | 1375 | const mapSslUri = function (request) { 1376 | 1377 | return { uri: `https://127.0.0.1:${upstream.info.port}` }; 1378 | }; 1379 | 1380 | const server = Hapi.server(); 1381 | await server.register(H2o2); 1382 | 1383 | server.route({ method: 'GET', path: '/sslDefault', handler: { proxy: { mapUri: mapSslUri } } }); 1384 | await server.start(); 1385 | 1386 | const res = await server.inject('/sslDefault'); 1387 | expect(res.statusCode).to.equal(502); 1388 | 1389 | await server.stop(); 1390 | await upstream.stop(); 1391 | }); 1392 | 1393 | it('times out when proxy timeout is less than server', { parallel: false }, async () => { 1394 | 1395 | const upstream = Hapi.server(); 1396 | upstream.route({ 1397 | method: 'GET', 1398 | path: '/timeout2', 1399 | handler: function (request, h) { 1400 | 1401 | return new Promise((resolve, reject) => { 1402 | 1403 | setTimeout(() => { 1404 | 1405 | return resolve(h.response('Ok')); 1406 | }, 10); 1407 | }); 1408 | 1409 | } 1410 | }); 1411 | 1412 | await upstream.start(); 1413 | 1414 | const server = Hapi.server({ routes: { timeout: { server: 8 } } }); 1415 | await server.register(H2o2); 1416 | 1417 | server.route({ method: 'GET', path: '/timeout2', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, timeout: 2 } } }); 1418 | await server.start(); 1419 | 1420 | const res = await server.inject('/timeout2'); 1421 | expect(res.statusCode).to.equal(504); 1422 | 1423 | await server.stop(); 1424 | await upstream.stop(); 1425 | }); 1426 | 1427 | it('times out when server timeout is less than proxy', async () => { 1428 | 1429 | const upstream = Hapi.server(); 1430 | upstream.route({ 1431 | method: 'GET', 1432 | path: '/timeout1', 1433 | handler: function (request, h) { 1434 | 1435 | return new Promise((resolve, reject) => { 1436 | 1437 | setTimeout(() => { 1438 | 1439 | return resolve(h.response('Ok')); 1440 | }, 10); 1441 | }); 1442 | } 1443 | }); 1444 | 1445 | await upstream.start(); 1446 | 1447 | const server = Hapi.server({ routes: { timeout: { server: 5 } } }); 1448 | await server.register(H2o2); 1449 | 1450 | server.route({ method: 'GET', path: '/timeout1', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, timeout: 15 } } }); 1451 | 1452 | const res = await server.inject('/timeout1'); 1453 | expect(res.statusCode).to.equal(503); 1454 | 1455 | await upstream.stop(); 1456 | }); 1457 | 1458 | it('proxies via uri template', async () => { 1459 | 1460 | const upstream = Hapi.server(); 1461 | upstream.route({ 1462 | method: 'GET', 1463 | path: '/item', 1464 | handler: function (request, h) { 1465 | 1466 | return h.response({ a: 1 }); 1467 | } 1468 | }); 1469 | 1470 | await upstream.start(); 1471 | 1472 | const server = Hapi.server(); 1473 | await server.register(H2o2); 1474 | 1475 | server.route({ method: 'GET', path: '/handlerTemplate', handler: { proxy: { uri: `{protocol}://${getUri({ ...upstream.info, protocol: null })}/item` } } }); 1476 | await server.start(); 1477 | 1478 | const res = await server.inject('/handlerTemplate'); 1479 | expect(res.statusCode).to.equal(200); 1480 | expect(res.payload).to.contain('"a":1'); 1481 | 1482 | await server.stop(); 1483 | await upstream.stop(); 1484 | }); 1485 | 1486 | it('proxies via uri template with request.param variables', async () => { 1487 | 1488 | const upstream = Hapi.server(); 1489 | upstream.route({ 1490 | method: 'GET', 1491 | path: '/item/{param_a}/{param_b}', 1492 | handler: function (request, h) { 1493 | 1494 | return h.response({ a: request.params.param_a, b: request.params.param_b }); 1495 | } 1496 | }); 1497 | 1498 | await upstream.start(); 1499 | 1500 | const server = Hapi.server(); 1501 | await server.register(H2o2); 1502 | 1503 | server.route({ method: 'GET', path: '/handlerTemplate/{a}/{b}', handler: { proxy: { uri: `http://${getUri({ ...upstream.info, protocol: null })}/item/{a}/{b}` } } }); 1504 | 1505 | const prma = 'foo'; 1506 | const prmb = 'bar'; 1507 | const res = await server.inject(`/handlerTemplate/${prma}/${prmb}`); 1508 | expect(res.statusCode).to.equal(200); 1509 | expect(res.payload).to.contain(`"a":"${prma}"`); 1510 | expect(res.payload).to.contain(`"b":"${prmb}"`); 1511 | 1512 | await upstream.stop(); 1513 | }); 1514 | 1515 | it('proxies via uri template with query string', async () => { 1516 | 1517 | const upstream = Hapi.server(); 1518 | upstream.route({ 1519 | method: 'GET', 1520 | path: '/item', 1521 | handler: function (request, h) { 1522 | 1523 | return h.response({ qs: request.query }); 1524 | } 1525 | }); 1526 | 1527 | await upstream.start(); 1528 | 1529 | const server = Hapi.server(); 1530 | await server.register(H2o2); 1531 | 1532 | server.route({ method: 'GET', path: '/handlerTemplate', handler: { proxy: { uri: `{protocol}://${getUri({ ...upstream.info, protocol: null })}/item{query}` } } }); 1533 | await server.start(); 1534 | 1535 | const res = await server.inject('/handlerTemplate?foo=bar'); 1536 | expect(res.statusCode).to.equal(200); 1537 | expect(res.payload).to.contain('"qs":{"foo":"bar"}'); 1538 | 1539 | await server.stop(); 1540 | await upstream.stop(); 1541 | }); 1542 | 1543 | it('passes upstream caching headers', async () => { 1544 | 1545 | const upstream = Hapi.server(); 1546 | upstream.route({ 1547 | method: 'GET', 1548 | path: '/cachedItem', 1549 | handler: function (request, h) { 1550 | 1551 | return h.response({ a: 1 }); 1552 | }, 1553 | config: { 1554 | cache: { 1555 | expiresIn: 2000 1556 | } 1557 | } 1558 | }); 1559 | 1560 | await upstream.start(); 1561 | 1562 | const server = Hapi.server(); 1563 | await server.register(H2o2); 1564 | 1565 | server.route({ method: 'GET', path: '/cachedItem', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, ttl: 'upstream' } } }); 1566 | server.state('auto', { autoValue: 'xyz' }); 1567 | await server.start(); 1568 | 1569 | const res = await server.inject('/cachedItem'); 1570 | expect(res.statusCode).to.equal(200); 1571 | expect(res.headers['cache-control']).to.equal('max-age=2, must-revalidate'); 1572 | 1573 | await server.stop(); 1574 | await upstream.stop(); 1575 | }); 1576 | 1577 | it('ignores when no upstream caching headers to pass', async () => { 1578 | 1579 | const upstream = Http.createServer((req, res) => { 1580 | 1581 | res.end('not much'); 1582 | }); 1583 | await upstream.listen(); 1584 | 1585 | const server = Hapi.server(); 1586 | await server.register(H2o2); 1587 | 1588 | server.route({ 1589 | method: 'GET', 1590 | path: '/', 1591 | handler: { proxy: { host: upstream.address().address, port: upstream.address().port, ttl: 'upstream' } } 1592 | }); 1593 | 1594 | const res = await server.inject('/'); 1595 | expect(res.statusCode).to.equal(200); 1596 | expect(res.headers['cache-control']).to.equal('no-cache'); 1597 | 1598 | await upstream.close(); 1599 | }); 1600 | 1601 | it('ignores when upstream caching header is invalid', async () => { 1602 | 1603 | const upstream = Http.createServer((req, res) => { 1604 | 1605 | res.writeHeader(200, { 'cache-control': 'some crap that does not work' }); 1606 | res.end('not much'); 1607 | }); 1608 | 1609 | await upstream.listen(); 1610 | 1611 | const server = Hapi.server(); 1612 | await server.register(H2o2); 1613 | 1614 | server.route({ 1615 | method: 'GET', 1616 | path: '/', 1617 | handler: { proxy: { host: upstream.address().address, port: upstream.address().port, ttl: 'upstream' } } 1618 | }); 1619 | 1620 | const res = await server.inject('/'); 1621 | expect(res.statusCode).to.equal(200); 1622 | expect(res.headers['cache-control']).to.equal('no-cache'); 1623 | 1624 | await upstream.close(); 1625 | }); 1626 | 1627 | it('overrides response code with 304', async () => { 1628 | 1629 | const upstream = Hapi.server(); 1630 | upstream.route({ 1631 | method: 'GET', 1632 | path: '/item', 1633 | handler: function (request, h) { 1634 | 1635 | return h.response({ a: 1 }); 1636 | } 1637 | }); 1638 | 1639 | await upstream.start(); 1640 | 1641 | const onResponse304 = function (err, res, request, h, settings, ttl) { 1642 | 1643 | expect(err).to.be.null(); 1644 | return h.response(res).code(304); 1645 | }; 1646 | 1647 | const server = Hapi.server(); 1648 | await server.register(H2o2); 1649 | 1650 | server.route({ method: 'GET', path: '/304', handler: { proxy: { uri: `${getUri(upstream.info)}/item`, onResponse: onResponse304 } } }); 1651 | 1652 | const res = await server.inject('/304'); 1653 | expect(res.statusCode).to.equal(304); 1654 | expect(res.payload).to.equal(''); 1655 | 1656 | await upstream.stop(); 1657 | }); 1658 | 1659 | it('cleans up when proxy response replaced in onPreResponse', async () => { 1660 | 1661 | const upstream = Hapi.server(); 1662 | upstream.route({ 1663 | method: 'GET', 1664 | path: '/item', 1665 | handler: function (request, h) { 1666 | 1667 | return h.response({ a: 1 }); 1668 | } 1669 | }); 1670 | 1671 | await upstream.start(); 1672 | 1673 | const server = Hapi.server(); 1674 | await server.register(H2o2); 1675 | 1676 | server.ext('onPreResponse', (request, h) => { 1677 | 1678 | return h.response({ something: 'else' }); 1679 | }); 1680 | server.route({ method: 'GET', path: '/item', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } } }); 1681 | 1682 | const res = await server.inject('/item'); 1683 | expect(res.statusCode).to.equal(200); 1684 | expect(res.result.something).to.equal('else'); 1685 | 1686 | await upstream.stop(); 1687 | }); 1688 | 1689 | it('retails accept-encoding header', async () => { 1690 | 1691 | const profile = function (request, h) { 1692 | 1693 | return h.response(request.headers['accept-encoding']); 1694 | }; 1695 | 1696 | const upstream = Hapi.server(); 1697 | upstream.route({ method: 'GET', path: '/', handler: profile, config: { cache: { expiresIn: 2000 } } }); 1698 | await upstream.start(); 1699 | 1700 | const server = Hapi.server(); 1701 | await server.register(H2o2); 1702 | 1703 | server.route({ method: 'GET', path: '/', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, acceptEncoding: true, passThrough: true } } }); 1704 | 1705 | const res = await server.inject({ url: '/', headers: { 'accept-encoding': '*/*' } }); 1706 | expect(res.statusCode).to.equal(200); 1707 | expect(res.payload).to.equal('*/*'); 1708 | 1709 | await upstream.stop(); 1710 | }); 1711 | 1712 | it('removes accept-encoding header', async () => { 1713 | 1714 | const profile = function (request, h) { 1715 | 1716 | return h.response(request.headers['accept-encoding']); 1717 | }; 1718 | 1719 | const upstream = Hapi.server(); 1720 | upstream.route({ method: 'GET', path: '/', handler: profile, config: { cache: { expiresIn: 2000 } } }); 1721 | await upstream.start(); 1722 | 1723 | const server = Hapi.server(); 1724 | await server.register(H2o2); 1725 | 1726 | server.route({ method: 'GET', path: '/', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, acceptEncoding: false, passThrough: true } } }); 1727 | 1728 | const res = await server.inject({ url: '/', headers: { 'accept-encoding': '*/*' } }); 1729 | expect(res.statusCode).to.be.within(200, 204); 1730 | expect(res.payload).to.equal(''); 1731 | 1732 | await upstream.stop(); 1733 | }); 1734 | 1735 | it('does not send multiple Content-Type headers on passthrough', { parallel: false }, async () => { 1736 | 1737 | const server = Hapi.server(); 1738 | await server.register(H2o2); 1739 | 1740 | const httpClient = { 1741 | request(method, uri, options, callback) { 1742 | 1743 | expect(options.headers['content-type']).to.equal('application/json'); 1744 | expect(options.headers['Content-Type']).to.not.exist(); 1745 | throw new Error('placeholder'); 1746 | } 1747 | }; 1748 | server.route({ method: 'GET', path: '/test', handler: { proxy: { uri: 'http://anything', httpClient, passThrough: true } } }); 1749 | await server.inject({ method: 'GET', url: '/test', headers: { 'Content-Type': 'application/json' } }); 1750 | }); 1751 | 1752 | it('allows passing in an agent through to Wreck', { parallel: false }, async () => { 1753 | 1754 | const server = Hapi.server(); 1755 | await server.register(H2o2); 1756 | 1757 | const agent = { name: 'myagent' }; 1758 | 1759 | const httpClient = { 1760 | request(method, uri, options, callback) { 1761 | 1762 | expect(options.agent).to.equal(agent); 1763 | return { statusCode: 200 }; 1764 | } 1765 | }; 1766 | server.route({ method: 'GET', path: '/agenttest', handler: { proxy: { uri: 'http://anything', httpClient, agent } } }); 1767 | await server.inject({ method: 'GET', url: '/agenttest', headers: {} }, (res) => { }); 1768 | }); 1769 | 1770 | it('excludes request cookies defined locally', async () => { 1771 | 1772 | const handler = function (request, h) { 1773 | 1774 | return h.response(request.state); 1775 | }; 1776 | 1777 | const upstream = Hapi.server(); 1778 | upstream.route({ method: 'GET', path: '/', handler }); 1779 | await upstream.start(); 1780 | 1781 | const server = Hapi.server(); 1782 | await server.register(H2o2); 1783 | 1784 | server.state('a'); 1785 | 1786 | server.route({ 1787 | method: 'GET', 1788 | path: '/', 1789 | handler: { 1790 | proxy: { 1791 | host: upstream.info.address, 1792 | port: upstream.info.port, 1793 | passThrough: true 1794 | } 1795 | } 1796 | }); 1797 | 1798 | const res = await server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }); 1799 | expect(res.statusCode).to.equal(200); 1800 | 1801 | const cookies = JSON.parse(res.payload); 1802 | expect(cookies).to.equal({ b: '2' }); 1803 | 1804 | await upstream.stop(); 1805 | }); 1806 | 1807 | it('includes request cookies defined locally (route level)', async () => { 1808 | 1809 | const handler = function (request, h) { 1810 | 1811 | return h.response(request.state); 1812 | }; 1813 | 1814 | const upstream = Hapi.server(); 1815 | upstream.route({ method: 'GET', path: '/', handler }); 1816 | await upstream.start(); 1817 | 1818 | const server = Hapi.server(); 1819 | await server.register(H2o2); 1820 | 1821 | server.state('a', { passThrough: true }); 1822 | server.route({ 1823 | method: 'GET', 1824 | path: '/', 1825 | handler: { 1826 | proxy: { 1827 | host: upstream.info.address, 1828 | port: upstream.info.port, 1829 | passThrough: true, 1830 | localStatePassThrough: true 1831 | } 1832 | } 1833 | }); 1834 | const res = await server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }); 1835 | expect(res.statusCode).to.equal(200); 1836 | 1837 | const cookies = JSON.parse(res.payload); 1838 | expect(cookies).to.equal({ a: '1', b: '2' }); 1839 | 1840 | await upstream.stop(); 1841 | }); 1842 | 1843 | it('includes request cookies defined locally (cookie level)', async () => { 1844 | 1845 | const handler = function (request, h) { 1846 | 1847 | return h.response(request.state); 1848 | }; 1849 | 1850 | const upstream = Hapi.server(); 1851 | upstream.route({ method: 'GET', path: '/', handler }); 1852 | await upstream.start(); 1853 | 1854 | const server = Hapi.server(); 1855 | await server.register(H2o2); 1856 | 1857 | server.state('a', { passThrough: true }); 1858 | server.route({ 1859 | method: 'GET', 1860 | path: '/', 1861 | handler: { 1862 | proxy: { 1863 | host: upstream.info.address, 1864 | port: upstream.info.port, 1865 | passThrough: true 1866 | } 1867 | } 1868 | }); 1869 | 1870 | const res = await server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }); 1871 | expect(res.statusCode).to.equal(200); 1872 | 1873 | const cookies = JSON.parse(res.payload); 1874 | expect(cookies).to.equal({ a: '1', b: '2' }); 1875 | 1876 | await upstream.stop(); 1877 | }); 1878 | 1879 | it('errors on invalid cookie header', async () => { 1880 | 1881 | const server = Hapi.server({ routes: { state: { failAction: 'ignore' } } }); 1882 | await server.register(H2o2); 1883 | 1884 | server.state('a', { passThrough: true }); 1885 | 1886 | server.route({ 1887 | method: 'GET', 1888 | path: '/', 1889 | handler: { 1890 | proxy: { 1891 | host: 'anything', 1892 | port: 8080, 1893 | passThrough: true 1894 | } 1895 | } 1896 | }); 1897 | 1898 | const res = await server.inject({ url: '/', headers: { cookie: 'a' } }); 1899 | expect(res.statusCode).to.equal(400); 1900 | }); 1901 | 1902 | it('drops cookies when all defined locally', async () => { 1903 | 1904 | const handler = function (request, h) { 1905 | 1906 | return h.response(request.state); 1907 | }; 1908 | 1909 | const upstream = Hapi.server(); 1910 | upstream.route({ method: 'GET', path: '/', handler }); 1911 | await upstream.start(); 1912 | 1913 | const server = Hapi.server(); 1914 | await server.register(H2o2); 1915 | 1916 | server.state('a'); 1917 | server.route({ 1918 | method: 'GET', 1919 | path: '/', 1920 | handler: { 1921 | proxy: { 1922 | host: upstream.info.address, 1923 | port: upstream.info.port, 1924 | passThrough: true 1925 | } 1926 | } 1927 | }); 1928 | 1929 | const res = await server.inject({ url: '/', headers: { cookie: 'a=1' } }); 1930 | expect(res.statusCode).to.equal(200); 1931 | 1932 | const cookies = JSON.parse(res.payload); 1933 | expect(cookies).to.equal({}); 1934 | 1935 | await upstream.stop(); 1936 | }); 1937 | 1938 | it('excludes request cookies defined locally (state override)', async () => { 1939 | 1940 | const handler = function (request, h) { 1941 | 1942 | return h.response(request.state); 1943 | }; 1944 | 1945 | const upstream = Hapi.server(); 1946 | upstream.route({ method: 'GET', path: '/', handler }); 1947 | await upstream.start(); 1948 | 1949 | const server = Hapi.server(); 1950 | await server.register(H2o2); 1951 | 1952 | server.state('a', { passThrough: false }); 1953 | server.route({ 1954 | method: 'GET', 1955 | path: '/', 1956 | handler: { 1957 | proxy: { 1958 | host: upstream.info.address, 1959 | port: upstream.info.port, 1960 | passThrough: true 1961 | } 1962 | } 1963 | }); 1964 | 1965 | const res = await server.inject({ url: '/', headers: { cookie: 'a=1;b=2' } }); 1966 | expect(res.statusCode).to.equal(200); 1967 | 1968 | const cookies = JSON.parse(res.payload); 1969 | expect(cookies).to.equal({ b: '2' }); 1970 | 1971 | await upstream.stop(); 1972 | }); 1973 | 1974 | it('uses reply decorator', async () => { 1975 | 1976 | const upstream = Hapi.server(); 1977 | upstream.route({ 1978 | method: 'GET', 1979 | path: '/', 1980 | handler: function (request, h) { 1981 | 1982 | return h.response('ok'); 1983 | } 1984 | }); 1985 | 1986 | await upstream.start(); 1987 | 1988 | const server = Hapi.server(); 1989 | await server.register(H2o2); 1990 | 1991 | server.route({ 1992 | method: 'GET', 1993 | path: '/', 1994 | handler: function (request, h) { 1995 | 1996 | return h.proxy({ host: upstream.info.address, port: upstream.info.port, xforward: true, passThrough: true }); 1997 | } 1998 | }); 1999 | 2000 | const res = await server.inject('/'); 2001 | expect(res.statusCode).to.equal(200); 2002 | expect(res.payload).to.equal('ok'); 2003 | 2004 | await upstream.stop(); 2005 | }); 2006 | 2007 | it('uses custom TLS settings', async () => { 2008 | 2009 | const upstream = Hapi.server({ tls: tlsOptions }); 2010 | upstream.route({ 2011 | method: 'GET', 2012 | path: '/', 2013 | handler: function (request, h) { 2014 | 2015 | return h.response('ok'); 2016 | } 2017 | }); 2018 | 2019 | await upstream.start(); 2020 | 2021 | const server = Hapi.server(); 2022 | await server.register({ plugin: H2o2, options: { secureProtocol: 'TLSv1_2_method', ciphers: 'ECDHE-RSA-AES128-SHA256' } }); 2023 | server.route({ 2024 | method: 'GET', 2025 | path: '/', 2026 | handler: function (request, h) { 2027 | 2028 | return h.proxy({ host: '127.0.0.1', protocol: 'https', port: upstream.info.port, rejectUnauthorized: false }); 2029 | } 2030 | }); 2031 | 2032 | const res = await server.inject('/'); 2033 | expect(res.statusCode).to.equal(200); 2034 | expect(res.payload).to.equal('ok'); 2035 | 2036 | await upstream.stop(); 2037 | }); 2038 | 2039 | it('adds downstreamResponseTime to the response when downstreamResponseTime is set to true on success', async () => { 2040 | 2041 | const upstream = Hapi.server(); 2042 | upstream.route({ 2043 | method: 'GET', 2044 | path: '/', 2045 | handler: function (request, h) { 2046 | 2047 | return h.response('ok'); 2048 | } 2049 | }); 2050 | 2051 | await upstream.start(); 2052 | 2053 | const server = Hapi.server(); 2054 | await server.register({ plugin: H2o2, options: { downstreamResponseTime: true } }); 2055 | server.route({ 2056 | method: 'GET', 2057 | path: '/', 2058 | handler: function (request, h) { 2059 | 2060 | return h.proxy({ host: upstream.info.address, port: upstream.info.port, xforward: true, passThrough: true }); 2061 | } 2062 | }); 2063 | 2064 | server.events.on('request', (request, event, tags) => { 2065 | 2066 | expect(Object.keys(event.data)).to.equal(['downstreamResponseTime']); 2067 | expect(event.data.downstreamResponseTime).to.be.a.number().and.greaterThan(0); 2068 | expect(tags).to.equal({ h2o2: true, success: true }); 2069 | }); 2070 | 2071 | const res = await server.inject('/'); 2072 | expect(res.statusCode).to.equal(200); 2073 | 2074 | await upstream.stop(); 2075 | }); 2076 | 2077 | it('adds downstreamResponseTime to the response when downstreamResponseTime is set to true on error', async () => { 2078 | 2079 | const failureResponse = function (err, res, request, h, settings, ttl) { 2080 | 2081 | expect(h.response).to.exist(); 2082 | throw err; 2083 | }; 2084 | 2085 | const dummy = Hapi.server(); 2086 | await dummy.start(); 2087 | const dummyPort = dummy.info.port; 2088 | const dummyHost = dummy.info.address; 2089 | await dummy.stop(); 2090 | 2091 | const options = { downstreamResponseTime: true }; 2092 | 2093 | const server = Hapi.server(); 2094 | await server.register({ plugin: H2o2, options }); 2095 | server.route({ method: 'GET', path: '/failureResponse', handler: { proxy: { host: dummyHost, port: dummyPort, onResponse: failureResponse } }, config: { cache: { expiresIn: 500 } } }); 2096 | 2097 | let firstEvent = true; 2098 | server.events.on('request', (request, event, tags) => { 2099 | 2100 | if (firstEvent) { 2101 | firstEvent = false; 2102 | expect(Object.keys(event.data)).to.equal(['downstreamResponseTime']); 2103 | expect(event.data.downstreamResponseTime).to.be.a.number().and.greaterThan(0); 2104 | expect(tags).to.equal({ h2o2: true, error: true }); 2105 | } 2106 | }); 2107 | 2108 | const res = await server.inject('/failureResponse'); 2109 | expect(res.statusCode).to.equal(502); 2110 | }); 2111 | 2112 | it('uses a custom http-client', async () => { 2113 | 2114 | const upstream = Hapi.server(); 2115 | upstream.route({ method: 'GET', path: '/', handler: () => 'ok' }); 2116 | await upstream.start(); 2117 | 2118 | const httpClient = { 2119 | request: Wreck.request.bind(Wreck), 2120 | parseCacheControl: Wreck.parseCacheControl.bind(Wreck) 2121 | }; 2122 | 2123 | const server = Hapi.server(); 2124 | await server.register(H2o2); 2125 | 2126 | server.route({ method: 'GET', path: '/', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, httpClient } } }); 2127 | 2128 | const res = await server.inject('/'); 2129 | 2130 | expect(res.payload).to.equal('ok'); 2131 | }); 2132 | 2133 | it('propagates cancellation to upstream during pre-response stage', { parallel: false }, async () => { 2134 | 2135 | const upstreamAborted = new Team({ meetings: 1, strict: true }); 2136 | 2137 | const upstream = Hapi.server(); 2138 | upstream.route({ 2139 | method: 'GET', 2140 | path: '/cancellation', 2141 | handler: function (request, h) { 2142 | 2143 | request.raw.req.once('aborted', () => { 2144 | 2145 | upstreamAborted.attend(true); 2146 | }); 2147 | 2148 | inboundRequest.abort(); 2149 | 2150 | return Hoek.block(); 2151 | } 2152 | }); 2153 | await upstream.start(); 2154 | 2155 | const server = Hapi.server(); 2156 | await server.register(H2o2); 2157 | server.route({ method: 'GET', path: '/cancellation', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } } }); 2158 | await server.start(); 2159 | 2160 | const promise = Wreck.request('GET', `${getUri(server.info)}/cancellation`); 2161 | const inboundRequest = promise.req; 2162 | 2163 | await expect(promise).to.reject(/socket hang up/); 2164 | expect(await upstreamAborted.work).to.equal(true); 2165 | 2166 | await server.stop(); 2167 | await upstream.stop(); 2168 | }); 2169 | 2170 | it('propagates cancellation to upstream during post-response stage', { parallel: false }, async () => { 2171 | 2172 | const upstreamAborted = new Team({ meetings: 1, strict: true }); 2173 | const downstreamReceived = new Team({ meetings: 1, strict: true }); 2174 | 2175 | const upstream = Hapi.server(); 2176 | upstream.route({ 2177 | method: 'GET', 2178 | path: '/cancellation', 2179 | handler: function (request, h) { 2180 | 2181 | request.raw.req.once('aborted', () => { 2182 | 2183 | upstreamAborted.attend(true); 2184 | }); 2185 | 2186 | const stream = new Stream.PassThrough(); 2187 | stream.write('unterminated data'); 2188 | 2189 | setTimeout(async () => { 2190 | 2191 | await downstreamReceived.work; 2192 | inboundRequest.abort(); 2193 | }, 0); 2194 | 2195 | return h.response(stream); 2196 | } 2197 | }); 2198 | await upstream.start(); 2199 | 2200 | const server = Hapi.server(); 2201 | await server.register(H2o2); 2202 | server.route({ method: 'GET', path: '/cancellation', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } } }); 2203 | await server.start(); 2204 | 2205 | const promise = Wreck.request('GET', `${getUri(server.info)}/cancellation`); 2206 | const inboundRequest = promise.req; 2207 | 2208 | const response = await promise; 2209 | expect(response.statusCode).to.equal(200); 2210 | 2211 | downstreamReceived.attend(); 2212 | 2213 | expect(await upstreamAborted.work).to.equal(true); 2214 | 2215 | await server.stop(); 2216 | await upstream.stop(); 2217 | }); 2218 | 2219 | it('propagates the allow header in case of 405 (with passthrough)', async () => { 2220 | 2221 | const upstream = Hapi.server(); 2222 | upstream.route({ 2223 | method: 'POST', 2224 | path: '/item', 2225 | handler(request, h) { 2226 | 2227 | throw Boom.methodNotAllowed('Not allowed', {}, ['GET']); 2228 | } 2229 | }); 2230 | 2231 | await upstream.start(); 2232 | 2233 | const server = Hapi.server(); 2234 | await server.register(H2o2); 2235 | 2236 | server.route({ method: 'POST', path: '/item', handler: { proxy: { host: upstream.info.address, port: upstream.info.port, passThrough: true } } }); 2237 | await server.start(); 2238 | 2239 | const res = await server.inject({ method: 'POST', url: '/item' }); 2240 | expect(res.statusCode).to.equal(405); 2241 | expect(res.headers.allow).to.equal('GET'); 2242 | 2243 | await server.stop(); 2244 | await upstream.stop(); 2245 | }); 2246 | 2247 | it('propagates the allow header in case of 405 (without passthrough)', async () => { 2248 | 2249 | const upstream = Hapi.server(); 2250 | upstream.route({ 2251 | method: 'POST', 2252 | path: '/item', 2253 | handler(request, h) { 2254 | 2255 | throw Boom.methodNotAllowed('Not allowed', {}, ['GET']); 2256 | } 2257 | }); 2258 | 2259 | await upstream.start(); 2260 | 2261 | const server = Hapi.server(); 2262 | await server.register(H2o2); 2263 | 2264 | server.route({ method: 'POST', path: '/item', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } } }); 2265 | await server.start(); 2266 | 2267 | const res = await server.inject({ method: 'POST', url: '/item' }); 2268 | expect(res.statusCode).to.equal(405); 2269 | expect(res.headers.allow).to.equal('GET'); 2270 | 2271 | await server.stop(); 2272 | await upstream.stop(); 2273 | }); 2274 | 2275 | it('does not propagate a missing allow header in case of malformed 405', async () => { 2276 | 2277 | const upstream = Hapi.server(); 2278 | upstream.route({ 2279 | method: 'POST', 2280 | path: '/item', 2281 | handler(request, h) { 2282 | 2283 | throw Boom.methodNotAllowed('Not allowed'); 2284 | } 2285 | }); 2286 | 2287 | await upstream.start(); 2288 | 2289 | const server = Hapi.server(); 2290 | await server.register(H2o2); 2291 | 2292 | server.route({ method: 'POST', path: '/item', handler: { proxy: { host: upstream.info.address, port: upstream.info.port } } }); 2293 | await server.start(); 2294 | 2295 | const res = await server.inject({ method: 'POST', url: '/item' }); 2296 | expect(res.statusCode).to.equal(405); 2297 | expect(res.headers.allow).to.not.exist(); 2298 | 2299 | await server.stop(); 2300 | await upstream.stop(); 2301 | }); 2302 | }); 2303 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { Server} from '@hapi/hapi'; 2 | import * as h2o2 from '..'; 3 | import { types } from '@hapi/lab' 4 | import type { IncomingMessage } from 'http'; 5 | import type { Boom } from '@hapi/boom'; 6 | import type { Plugin, Request, ResponseObject, ResponseToolkit, ServerRoute } from '@hapi/hapi'; 7 | import type * as Wreck from '@hapi/wreck'; 8 | 9 | types.expect.type>(h2o2.plugin); 10 | 11 | async function main() { 12 | const server = new Server({}); 13 | await server.register(h2o2); 14 | 15 | server.route({ 16 | method: 'GET', 17 | path: '/hproxyoptions', 18 | async handler(request, h) { 19 | // ResponseToolkit augmentation 20 | // https://github.com/hapijs/h2o2#hproxyoptions 21 | return h.proxy({ host: 'example.com', port: 80, protocol: 'http' }); 22 | }, 23 | }); 24 | 25 | server.route({ 26 | method: 'GET', 27 | path: '/using-the-host-port-protocol-options', 28 | handler: { 29 | // HandlerDecorations augmentation 30 | // https://github.com/hapijs/h2o2#using-the-host-port-protocol-options 31 | proxy: { 32 | host: '10.33.33.1', 33 | port: '443', 34 | protocol: 'https', 35 | }, 36 | }, 37 | }); 38 | 39 | server.route({ 40 | method: 'GET', 41 | path: '/using-the-uri-option', 42 | handler: { 43 | // HandlerDecorations augmentation 44 | // https://github.com/hapijs/h2o2#using-the-uri-option 45 | proxy: { 46 | uri: 'https://some.upstream.service.com/that/has?what=you&want=todo', 47 | }, 48 | }, 49 | }); 50 | 51 | server.route({ 52 | method: 'GET', 53 | path: '/custom-uri-template-values', 54 | handler: { 55 | // HandlerDecorations augmentation 56 | // https://github.com/hapijs/h2o2#custom-uri-template-values 57 | proxy: { 58 | uri: '{protocol}://{host}:{port}/go/to/{path}', 59 | }, 60 | }, 61 | }); 62 | 63 | server.route({ 64 | method: 'GET', 65 | path: '/custom-uri-template-values/{bar}', 66 | handler: { 67 | // HandlerDecorations augmentation 68 | // https://github.com/hapijs/h2o2#custom-uri-template-values 69 | proxy: { 70 | uri: 'https://some.upstream.service.com/some/path/to/{bar}', 71 | }, 72 | }, 73 | }); 74 | 75 | server.route({ 76 | method: 'GET', 77 | path: '/', 78 | handler: { 79 | // HandlerDecorations augmentation 80 | // https://github.com/hapijs/h2o2#using-the-mapuri-and-onresponse-options 81 | proxy: { 82 | async mapUri(request) { 83 | return { 84 | uri: 'https://some.upstream.service.com/', 85 | }; 86 | }, 87 | 88 | async onRequest(req) { 89 | types.expect.type(req); 90 | return req.request('GET', 'https://some.upstream.service.com/'); 91 | }, 92 | 93 | async onResponse(err, res, request, h, settings, ttl) { 94 | types.expect.type | null>(err); 95 | types.expect.type(res); 96 | types.expect.type(request); 97 | types.expect.type(h); 98 | types.expect.type(settings); 99 | types.expect.type(ttl); 100 | return null; 101 | }, 102 | }, 103 | }, 104 | }); 105 | 106 | server.route({ 107 | method: 'GET', 108 | path: '/', 109 | handler: { 110 | proxy: { 111 | httpClient: { 112 | // request(method, url, options) { 113 | // return axios({method, url }) 114 | // } 115 | }, 116 | }, 117 | }, 118 | }); 119 | 120 | await server.start(); 121 | await server.stop(); 122 | } 123 | 124 | /** 125 | * test code added in additional to code in docs. Demonstrates that for the moment 126 | * you need flat 127 | * objects with typing along the way to benefit from typescript catching 128 | * misspelt, or unsupported keys. 129 | * This is because of an unknown reason. Expecting this to work because: 130 | * "Object literals get special treatment and undergo excess 131 | * property checking when assigning them to other variables, or passing them 132 | * as arguments", see github.com/Microsoft/TypeScript 133 | */ 134 | 135 | const proxyOptions: h2o2.ProxyHandlerOptions = { 136 | host: '10.33.33.1', 137 | port: '443', 138 | protocol: 'https', // errors correctly if misspelt 139 | }; 140 | 141 | const badProtocolDemo: ServerRoute = { 142 | method: 'GET', 143 | path: '/', 144 | handler: { 145 | proxy: { 146 | host: '10.33.33.1', 147 | port: '443', 148 | // port: null // detected as incompatible 149 | }, 150 | }, 151 | }; 152 | 153 | const replyViaToolkit: ServerRoute = { 154 | method: 'GET', 155 | path: '/', 156 | async handler(req, h): Promise { 157 | return h.proxy({ 158 | host: '10.33.33.1', 159 | port: '443', 160 | protocol: 'https', 161 | }); 162 | }, 163 | }; 164 | --------------------------------------------------------------------------------