├── .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 |
--------------------------------------------------------------------------------