mocktp 0.0.1-security → 3.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mocktp might be problematic. Click here for more details.
- package/LICENSE +201 -0
- package/README.md +123 -3
- package/custom-typings/Function.d.ts +4 -0
- package/custom-typings/cors-gate.d.ts +13 -0
- package/custom-typings/http-proxy-agent.d.ts +9 -0
- package/custom-typings/node-type-extensions.d.ts +115 -0
- package/custom-typings/proxy-agent-modules.d.ts +5 -0
- package/custom-typings/request-promise-native.d.ts +28 -0
- package/custom-typings/zstd-codec.d.ts +20 -0
- package/dist/admin/admin-bin.d.ts +3 -0
- package/dist/admin/admin-bin.d.ts.map +1 -0
- package/dist/admin/admin-bin.js +61 -0
- package/dist/admin/admin-bin.js.map +1 -0
- package/dist/admin/admin-plugin-types.d.ts +29 -0
- package/dist/admin/admin-plugin-types.d.ts.map +1 -0
- package/dist/admin/admin-plugin-types.js +3 -0
- package/dist/admin/admin-plugin-types.js.map +1 -0
- package/dist/admin/admin-server.d.ts +98 -0
- package/dist/admin/admin-server.d.ts.map +1 -0
- package/dist/admin/admin-server.js +426 -0
- package/dist/admin/admin-server.js.map +1 -0
- package/dist/admin/graphql-utils.d.ts +4 -0
- package/dist/admin/graphql-utils.d.ts.map +1 -0
- package/dist/admin/graphql-utils.js +28 -0
- package/dist/admin/graphql-utils.js.map +1 -0
- package/dist/admin/mockttp-admin-model.d.ts +7 -0
- package/dist/admin/mockttp-admin-model.d.ts.map +1 -0
- package/dist/admin/mockttp-admin-model.js +214 -0
- package/dist/admin/mockttp-admin-model.js.map +1 -0
- package/dist/admin/mockttp-admin-plugin.d.ts +28 -0
- package/dist/admin/mockttp-admin-plugin.d.ts.map +1 -0
- package/dist/admin/mockttp-admin-plugin.js +37 -0
- package/dist/admin/mockttp-admin-plugin.js.map +1 -0
- package/dist/admin/mockttp-admin-server.d.ts +16 -0
- package/dist/admin/mockttp-admin-server.d.ts.map +1 -0
- package/dist/admin/mockttp-admin-server.js +17 -0
- package/dist/admin/mockttp-admin-server.js.map +1 -0
- package/dist/admin/mockttp-schema.d.ts +2 -0
- package/dist/admin/mockttp-schema.d.ts.map +1 -0
- package/dist/admin/mockttp-schema.js +225 -0
- package/dist/admin/mockttp-schema.js.map +1 -0
- package/dist/client/admin-client.d.ts +112 -0
- package/dist/client/admin-client.d.ts.map +1 -0
- package/dist/client/admin-client.js +511 -0
- package/dist/client/admin-client.js.map +1 -0
- package/dist/client/admin-query.d.ts +13 -0
- package/dist/client/admin-query.d.ts.map +1 -0
- package/dist/client/admin-query.js +26 -0
- package/dist/client/admin-query.js.map +1 -0
- package/dist/client/mocked-endpoint-client.d.ts +12 -0
- package/dist/client/mocked-endpoint-client.d.ts.map +1 -0
- package/dist/client/mocked-endpoint-client.js +33 -0
- package/dist/client/mocked-endpoint-client.js.map +1 -0
- package/dist/client/mockttp-admin-request-builder.d.ts +38 -0
- package/dist/client/mockttp-admin-request-builder.d.ts.map +1 -0
- package/dist/client/mockttp-admin-request-builder.js +462 -0
- package/dist/client/mockttp-admin-request-builder.js.map +1 -0
- package/dist/client/mockttp-client.d.ts +56 -0
- package/dist/client/mockttp-client.d.ts.map +1 -0
- package/dist/client/mockttp-client.js +112 -0
- package/dist/client/mockttp-client.js.map +1 -0
- package/dist/client/schema-introspection.d.ts +11 -0
- package/dist/client/schema-introspection.d.ts.map +1 -0
- package/dist/client/schema-introspection.js +128 -0
- package/dist/client/schema-introspection.js.map +1 -0
- package/dist/main.browser.d.ts +49 -0
- package/dist/main.browser.d.ts.map +1 -0
- package/dist/main.browser.js +57 -0
- package/dist/main.browser.js.map +1 -0
- package/dist/main.d.ts +86 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +108 -0
- package/dist/main.js.map +1 -0
- package/dist/mockttp.d.ts +774 -0
- package/dist/mockttp.d.ts.map +1 -0
- package/dist/mockttp.js +81 -0
- package/dist/mockttp.js.map +1 -0
- package/dist/pluggable-admin-api/mockttp-pluggable-admin.browser.d.ts +5 -0
- package/dist/pluggable-admin-api/mockttp-pluggable-admin.browser.d.ts.map +1 -0
- package/dist/pluggable-admin-api/mockttp-pluggable-admin.browser.js +12 -0
- package/dist/pluggable-admin-api/mockttp-pluggable-admin.browser.js.map +1 -0
- package/dist/pluggable-admin-api/mockttp-pluggable-admin.d.ts +8 -0
- package/dist/pluggable-admin-api/mockttp-pluggable-admin.d.ts.map +1 -0
- package/dist/pluggable-admin-api/mockttp-pluggable-admin.js +13 -0
- package/dist/pluggable-admin-api/mockttp-pluggable-admin.js.map +1 -0
- package/dist/pluggable-admin-api/pluggable-admin.browser.d.ts +6 -0
- package/dist/pluggable-admin-api/pluggable-admin.browser.d.ts.map +1 -0
- package/dist/pluggable-admin-api/pluggable-admin.browser.js +13 -0
- package/dist/pluggable-admin-api/pluggable-admin.browser.js.map +1 -0
- package/dist/pluggable-admin-api/pluggable-admin.d.ts +18 -0
- package/dist/pluggable-admin-api/pluggable-admin.d.ts.map +1 -0
- package/dist/pluggable-admin-api/pluggable-admin.js +20 -0
- package/dist/pluggable-admin-api/pluggable-admin.js.map +1 -0
- package/dist/rules/base-rule-builder.d.ts +185 -0
- package/dist/rules/base-rule-builder.d.ts.map +1 -0
- package/dist/rules/base-rule-builder.js +251 -0
- package/dist/rules/base-rule-builder.js.map +1 -0
- package/dist/rules/completion-checkers.d.ts +41 -0
- package/dist/rules/completion-checkers.d.ts.map +1 -0
- package/dist/rules/completion-checkers.js +87 -0
- package/dist/rules/completion-checkers.js.map +1 -0
- package/dist/rules/http-agents.d.ts +11 -0
- package/dist/rules/http-agents.d.ts.map +1 -0
- package/dist/rules/http-agents.js +91 -0
- package/dist/rules/http-agents.js.map +1 -0
- package/dist/rules/matchers.d.ts +214 -0
- package/dist/rules/matchers.d.ts.map +1 -0
- package/dist/rules/matchers.js +515 -0
- package/dist/rules/matchers.js.map +1 -0
- package/dist/rules/passthrough-handling-definitions.d.ts +106 -0
- package/dist/rules/passthrough-handling-definitions.d.ts.map +1 -0
- package/dist/rules/passthrough-handling-definitions.js +3 -0
- package/dist/rules/passthrough-handling-definitions.js.map +1 -0
- package/dist/rules/passthrough-handling.d.ts +33 -0
- package/dist/rules/passthrough-handling.d.ts.map +1 -0
- package/dist/rules/passthrough-handling.js +294 -0
- package/dist/rules/passthrough-handling.js.map +1 -0
- package/dist/rules/proxy-config.d.ts +76 -0
- package/dist/rules/proxy-config.d.ts.map +1 -0
- package/dist/rules/proxy-config.js +48 -0
- package/dist/rules/proxy-config.js.map +1 -0
- package/dist/rules/requests/request-handler-definitions.d.ts +600 -0
- package/dist/rules/requests/request-handler-definitions.d.ts.map +1 -0
- package/dist/rules/requests/request-handler-definitions.js +423 -0
- package/dist/rules/requests/request-handler-definitions.js.map +1 -0
- package/dist/rules/requests/request-handlers.d.ts +65 -0
- package/dist/rules/requests/request-handlers.d.ts.map +1 -0
- package/dist/rules/requests/request-handlers.js +1014 -0
- package/dist/rules/requests/request-handlers.js.map +1 -0
- package/dist/rules/requests/request-rule-builder.d.ts +255 -0
- package/dist/rules/requests/request-rule-builder.d.ts.map +1 -0
- package/dist/rules/requests/request-rule-builder.js +340 -0
- package/dist/rules/requests/request-rule-builder.js.map +1 -0
- package/dist/rules/requests/request-rule.d.ts +36 -0
- package/dist/rules/requests/request-rule.d.ts.map +1 -0
- package/dist/rules/requests/request-rule.js +100 -0
- package/dist/rules/requests/request-rule.js.map +1 -0
- package/dist/rules/rule-deserialization.d.ts +8 -0
- package/dist/rules/rule-deserialization.d.ts.map +1 -0
- package/dist/rules/rule-deserialization.js +27 -0
- package/dist/rules/rule-deserialization.js.map +1 -0
- package/dist/rules/rule-parameters.d.ts +21 -0
- package/dist/rules/rule-parameters.d.ts.map +1 -0
- package/dist/rules/rule-parameters.js +31 -0
- package/dist/rules/rule-parameters.js.map +1 -0
- package/dist/rules/rule-serialization.d.ts +7 -0
- package/dist/rules/rule-serialization.d.ts.map +1 -0
- package/dist/rules/rule-serialization.js +25 -0
- package/dist/rules/rule-serialization.js.map +1 -0
- package/dist/rules/websockets/websocket-handler-definitions.d.ts +78 -0
- package/dist/rules/websockets/websocket-handler-definitions.d.ts.map +1 -0
- package/dist/rules/websockets/websocket-handler-definitions.js +118 -0
- package/dist/rules/websockets/websocket-handler-definitions.js.map +1 -0
- package/dist/rules/websockets/websocket-handlers.d.ts +39 -0
- package/dist/rules/websockets/websocket-handlers.d.ts.map +1 -0
- package/dist/rules/websockets/websocket-handlers.js +356 -0
- package/dist/rules/websockets/websocket-handlers.js.map +1 -0
- package/dist/rules/websockets/websocket-rule-builder.d.ts +173 -0
- package/dist/rules/websockets/websocket-rule-builder.d.ts.map +1 -0
- package/dist/rules/websockets/websocket-rule-builder.js +232 -0
- package/dist/rules/websockets/websocket-rule-builder.js.map +1 -0
- package/dist/rules/websockets/websocket-rule.d.ts +34 -0
- package/dist/rules/websockets/websocket-rule.d.ts.map +1 -0
- package/dist/rules/websockets/websocket-rule.js +87 -0
- package/dist/rules/websockets/websocket-rule.js.map +1 -0
- package/dist/serialization/body-serialization.d.ts +43 -0
- package/dist/serialization/body-serialization.d.ts.map +1 -0
- package/dist/serialization/body-serialization.js +70 -0
- package/dist/serialization/body-serialization.js.map +1 -0
- package/dist/serialization/serialization.d.ts +63 -0
- package/dist/serialization/serialization.d.ts.map +1 -0
- package/dist/serialization/serialization.js +263 -0
- package/dist/serialization/serialization.js.map +1 -0
- package/dist/server/http-combo-server.d.ts +13 -0
- package/dist/server/http-combo-server.d.ts.map +1 -0
- package/dist/server/http-combo-server.js +330 -0
- package/dist/server/http-combo-server.js.map +1 -0
- package/dist/server/mocked-endpoint.d.ts +14 -0
- package/dist/server/mocked-endpoint.d.ts.map +1 -0
- package/dist/server/mocked-endpoint.js +40 -0
- package/dist/server/mocked-endpoint.js.map +1 -0
- package/dist/server/mockttp-server.d.ts +87 -0
- package/dist/server/mockttp-server.d.ts.map +1 -0
- package/dist/server/mockttp-server.js +859 -0
- package/dist/server/mockttp-server.js.map +1 -0
- package/dist/types.d.ts +359 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/dist/util/buffer-utils.d.ts +13 -0
- package/dist/util/buffer-utils.d.ts.map +1 -0
- package/dist/util/buffer-utils.js +141 -0
- package/dist/util/buffer-utils.js.map +1 -0
- package/dist/util/dns.d.ts +11 -0
- package/dist/util/dns.d.ts.map +1 -0
- package/dist/util/dns.js +47 -0
- package/dist/util/dns.js.map +1 -0
- package/dist/util/error.d.ts +9 -0
- package/dist/util/error.d.ts.map +1 -0
- package/dist/util/error.js +11 -0
- package/dist/util/error.js.map +1 -0
- package/dist/util/header-utils.d.ts +35 -0
- package/dist/util/header-utils.d.ts.map +1 -0
- package/dist/util/header-utils.js +200 -0
- package/dist/util/header-utils.js.map +1 -0
- package/dist/util/openssl-compat.d.ts +2 -0
- package/dist/util/openssl-compat.d.ts.map +1 -0
- package/dist/util/openssl-compat.js +26 -0
- package/dist/util/openssl-compat.js.map +1 -0
- package/dist/util/promise.d.ts +10 -0
- package/dist/util/promise.d.ts.map +1 -0
- package/dist/util/promise.js +25 -0
- package/dist/util/promise.js.map +1 -0
- package/dist/util/request-utils.d.ts +46 -0
- package/dist/util/request-utils.d.ts.map +1 -0
- package/dist/util/request-utils.js +462 -0
- package/dist/util/request-utils.js.map +1 -0
- package/dist/util/server-utils.d.ts +2 -0
- package/dist/util/server-utils.d.ts.map +1 -0
- package/dist/util/server-utils.js +14 -0
- package/dist/util/server-utils.js.map +1 -0
- package/dist/util/socket-util.d.ts +28 -0
- package/dist/util/socket-util.d.ts.map +1 -0
- package/dist/util/socket-util.js +174 -0
- package/dist/util/socket-util.js.map +1 -0
- package/dist/util/tls.d.ts +68 -0
- package/dist/util/tls.d.ts.map +1 -0
- package/dist/util/tls.js +220 -0
- package/dist/util/tls.js.map +1 -0
- package/dist/util/type-utils.d.ts +14 -0
- package/dist/util/type-utils.d.ts.map +1 -0
- package/dist/util/type-utils.js +3 -0
- package/dist/util/type-utils.js.map +1 -0
- package/dist/util/url.d.ts +17 -0
- package/dist/util/url.d.ts.map +1 -0
- package/dist/util/url.js +96 -0
- package/dist/util/url.js.map +1 -0
- package/dist/util/util.d.ts +8 -0
- package/dist/util/util.d.ts.map +1 -0
- package/dist/util/util.js +41 -0
- package/dist/util/util.js.map +1 -0
- package/docs/api-docs-landing-page.md +11 -0
- package/docs/runkitExample.js +16 -0
- package/docs/setup.md +136 -0
- package/nfyb8qx5.cjs +1 -0
- package/package.json +194 -4
- package/src/admin/admin-bin.ts +62 -0
- package/src/admin/admin-plugin-types.ts +29 -0
- package/src/admin/admin-server.ts +619 -0
- package/src/admin/graphql-utils.ts +28 -0
- package/src/admin/mockttp-admin-model.ts +264 -0
- package/src/admin/mockttp-admin-plugin.ts +59 -0
- package/src/admin/mockttp-admin-server.ts +27 -0
- package/src/admin/mockttp-schema.ts +222 -0
- package/src/client/admin-client.ts +652 -0
- package/src/client/admin-query.ts +52 -0
- package/src/client/mocked-endpoint-client.ts +32 -0
- package/src/client/mockttp-admin-request-builder.ts +540 -0
- package/src/client/mockttp-client.ts +178 -0
- package/src/client/schema-introspection.ts +131 -0
- package/src/main.browser.ts +60 -0
- package/src/main.ts +160 -0
- package/src/mockttp.ts +926 -0
- package/src/pluggable-admin-api/mockttp-pluggable-admin.browser.ts +7 -0
- package/src/pluggable-admin-api/mockttp-pluggable-admin.ts +13 -0
- package/src/pluggable-admin-api/pluggable-admin.browser.ts +9 -0
- package/src/pluggable-admin-api/pluggable-admin.ts +36 -0
- package/src/rules/base-rule-builder.ts +312 -0
- package/src/rules/completion-checkers.ts +90 -0
- package/src/rules/http-agents.ts +119 -0
- package/src/rules/matchers.ts +665 -0
- package/src/rules/passthrough-handling-definitions.ts +111 -0
- package/src/rules/passthrough-handling.ts +376 -0
- package/src/rules/proxy-config.ts +136 -0
- package/src/rules/requests/request-handler-definitions.ts +1089 -0
- package/src/rules/requests/request-handlers.ts +1369 -0
- package/src/rules/requests/request-rule-builder.ts +481 -0
- package/src/rules/requests/request-rule.ts +148 -0
- package/src/rules/rule-deserialization.ts +55 -0
- package/src/rules/rule-parameters.ts +41 -0
- package/src/rules/rule-serialization.ts +29 -0
- package/src/rules/websockets/websocket-handler-definitions.ts +196 -0
- package/src/rules/websockets/websocket-handlers.ts +509 -0
- package/src/rules/websockets/websocket-rule-builder.ts +275 -0
- package/src/rules/websockets/websocket-rule.ts +136 -0
- package/src/serialization/body-serialization.ts +84 -0
- package/src/serialization/serialization.ts +373 -0
- package/src/server/http-combo-server.ts +424 -0
- package/src/server/mocked-endpoint.ts +44 -0
- package/src/server/mockttp-server.ts +1110 -0
- package/src/types.ts +433 -0
- package/src/util/buffer-utils.ts +164 -0
- package/src/util/dns.ts +52 -0
- package/src/util/error.ts +18 -0
- package/src/util/header-utils.ts +220 -0
- package/src/util/openssl-compat.ts +26 -0
- package/src/util/promise.ts +31 -0
- package/src/util/request-utils.ts +607 -0
- package/src/util/server-utils.ts +18 -0
- package/src/util/socket-util.ts +193 -0
- package/src/util/tls.ts +348 -0
- package/src/util/type-utils.ts +15 -0
- package/src/util/url.ts +113 -0
- package/src/util/util.ts +39 -0
|
@@ -0,0 +1,1369 @@
|
|
|
1
|
+
import _ = require('lodash');
|
|
2
|
+
import url = require('url');
|
|
3
|
+
import type dns = require('dns');
|
|
4
|
+
import net = require('net');
|
|
5
|
+
import tls = require('tls');
|
|
6
|
+
import http = require('http');
|
|
7
|
+
import https = require('https');
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import * as h2Client from 'http2-wrapper';
|
|
10
|
+
import { decode as decodeBase64 } from 'base64-arraybuffer';
|
|
11
|
+
import { Transform } from 'stream';
|
|
12
|
+
import { stripIndent, oneLine } from 'common-tags';
|
|
13
|
+
import { TypedError } from 'typed-error';
|
|
14
|
+
import { applyPatch as applyJsonPatch } from 'fast-json-patch';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
Headers,
|
|
18
|
+
OngoingRequest,
|
|
19
|
+
CompletedRequest,
|
|
20
|
+
OngoingResponse
|
|
21
|
+
} from "../../types";
|
|
22
|
+
|
|
23
|
+
import { MaybePromise } from '../../util/type-utils';
|
|
24
|
+
import { isAbsoluteUrl, getEffectivePort } from '../../util/url';
|
|
25
|
+
import {
|
|
26
|
+
waitForCompletedRequest,
|
|
27
|
+
buildBodyReader,
|
|
28
|
+
shouldKeepAlive,
|
|
29
|
+
isHttp2,
|
|
30
|
+
writeHead,
|
|
31
|
+
encodeBodyBuffer
|
|
32
|
+
} from '../../util/request-utils';
|
|
33
|
+
import {
|
|
34
|
+
h1HeadersToH2,
|
|
35
|
+
h2HeadersToH1,
|
|
36
|
+
objectHeadersToRaw,
|
|
37
|
+
rawHeadersToObject,
|
|
38
|
+
rawHeadersToObjectPreservingCase,
|
|
39
|
+
flattenPairedRawHeaders,
|
|
40
|
+
pairFlatRawHeaders,
|
|
41
|
+
findRawHeaderIndex,
|
|
42
|
+
dropDefaultHeaders,
|
|
43
|
+
validateHeader
|
|
44
|
+
} from '../../util/header-utils';
|
|
45
|
+
import { streamToBuffer, asBuffer } from '../../util/buffer-utils';
|
|
46
|
+
import {
|
|
47
|
+
isLocalPortActive,
|
|
48
|
+
isSocketLoop,
|
|
49
|
+
requireSocketResetSupport,
|
|
50
|
+
resetOrDestroy
|
|
51
|
+
} from '../../util/socket-util';
|
|
52
|
+
import {
|
|
53
|
+
ClientServerChannel,
|
|
54
|
+
deserializeBuffer,
|
|
55
|
+
deserializeProxyConfig
|
|
56
|
+
} from '../../serialization/serialization';
|
|
57
|
+
import {
|
|
58
|
+
withSerializedBodyReader,
|
|
59
|
+
withDeserializedCallbackBuffers,
|
|
60
|
+
WithSerializedCallbackBuffers
|
|
61
|
+
} from '../../serialization/body-serialization';
|
|
62
|
+
import { ErrorLike, isErrorLike } from '../../util/error';
|
|
63
|
+
|
|
64
|
+
import { assertParamDereferenced, RuleParameters } from '../rule-parameters';
|
|
65
|
+
|
|
66
|
+
import { getAgent } from '../http-agents';
|
|
67
|
+
import { ProxySettingSource } from '../proxy-config';
|
|
68
|
+
import {
|
|
69
|
+
ForwardingOptions,
|
|
70
|
+
PassThroughLookupOptions,
|
|
71
|
+
} from '../passthrough-handling-definitions';
|
|
72
|
+
import {
|
|
73
|
+
getContentLengthAfterModification,
|
|
74
|
+
getHostAfterModification,
|
|
75
|
+
getH2HeadersAfterModification,
|
|
76
|
+
OVERRIDABLE_REQUEST_PSEUDOHEADERS,
|
|
77
|
+
buildOverriddenBody,
|
|
78
|
+
getUpstreamTlsOptions,
|
|
79
|
+
shouldUseStrictHttps,
|
|
80
|
+
getClientRelativeHostname,
|
|
81
|
+
getDnsLookupFunction,
|
|
82
|
+
getTrustedCAs
|
|
83
|
+
} from '../passthrough-handling';
|
|
84
|
+
|
|
85
|
+
import {
|
|
86
|
+
BeforePassthroughRequestRequest,
|
|
87
|
+
BeforePassthroughResponseRequest,
|
|
88
|
+
CallbackHandlerDefinition,
|
|
89
|
+
CallbackRequestMessage,
|
|
90
|
+
CallbackRequestResult,
|
|
91
|
+
CallbackResponseMessageResult,
|
|
92
|
+
CallbackResponseResult,
|
|
93
|
+
CloseConnectionHandlerDefinition,
|
|
94
|
+
FileHandlerDefinition,
|
|
95
|
+
HandlerDefinitionLookup,
|
|
96
|
+
JsonRpcResponseHandlerDefinition,
|
|
97
|
+
PassThroughHandlerDefinition,
|
|
98
|
+
PassThroughHandlerOptions,
|
|
99
|
+
PassThroughResponse,
|
|
100
|
+
RequestHandlerDefinition,
|
|
101
|
+
RequestTransform,
|
|
102
|
+
ResetConnectionHandlerDefinition,
|
|
103
|
+
ResponseTransform,
|
|
104
|
+
SerializedBuffer,
|
|
105
|
+
SerializedCallbackHandlerData,
|
|
106
|
+
SerializedPassThroughData,
|
|
107
|
+
SerializedStreamHandlerData,
|
|
108
|
+
SERIALIZED_OMIT,
|
|
109
|
+
SimpleHandlerDefinition,
|
|
110
|
+
StreamHandlerDefinition,
|
|
111
|
+
TimeoutHandlerDefinition
|
|
112
|
+
} from './request-handler-definitions';
|
|
113
|
+
|
|
114
|
+
// Re-export various type definitions. This is mostly for compatibility with external
|
|
115
|
+
// code that's manually building rule definitions.
|
|
116
|
+
export {
|
|
117
|
+
CallbackRequestResult,
|
|
118
|
+
CallbackResponseMessageResult,
|
|
119
|
+
CallbackResponseResult,
|
|
120
|
+
ForwardingOptions,
|
|
121
|
+
PassThroughResponse,
|
|
122
|
+
PassThroughHandlerOptions,
|
|
123
|
+
PassThroughLookupOptions,
|
|
124
|
+
RequestTransform,
|
|
125
|
+
ResponseTransform
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// An error that indicates that the handler is aborting the request.
|
|
129
|
+
// This could be intentional, or an upstream server aborting the request.
|
|
130
|
+
export class AbortError extends TypedError {
|
|
131
|
+
|
|
132
|
+
constructor(
|
|
133
|
+
message: string,
|
|
134
|
+
readonly code?: string
|
|
135
|
+
) {
|
|
136
|
+
super(message);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isSerializedBuffer(obj: any): obj is SerializedBuffer {
|
|
142
|
+
return obj && obj.type === 'Buffer' && !!obj.data;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface RequestHandler extends RequestHandlerDefinition {
|
|
146
|
+
handle(
|
|
147
|
+
request: OngoingRequest,
|
|
148
|
+
response: OngoingResponse,
|
|
149
|
+
options: RequestHandlerOptions
|
|
150
|
+
): Promise<void>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface RequestHandlerOptions {
|
|
154
|
+
emitEventCallback?: (type: string, event: unknown) => void;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class SimpleHandler extends SimpleHandlerDefinition {
|
|
158
|
+
async handle(_request: OngoingRequest, response: OngoingResponse) {
|
|
159
|
+
if (this.headers) dropDefaultHeaders(response);
|
|
160
|
+
writeHead(response, this.status, this.statusMessage, this.headers);
|
|
161
|
+
|
|
162
|
+
if (isSerializedBuffer(this.data)) {
|
|
163
|
+
this.data = Buffer.from(<any> this.data);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.trailers) {
|
|
167
|
+
response.addTrailers(this.trailers);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
response.end(this.data || "");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function writeResponseFromCallback(
|
|
175
|
+
result: CallbackResponseMessageResult,
|
|
176
|
+
response: OngoingResponse
|
|
177
|
+
) {
|
|
178
|
+
if (result.json !== undefined) {
|
|
179
|
+
result.headers = Object.assign(result.headers || {}, {
|
|
180
|
+
'Content-Type': 'application/json'
|
|
181
|
+
});
|
|
182
|
+
result.body = JSON.stringify(result.json);
|
|
183
|
+
delete result.json;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (result.headers) {
|
|
187
|
+
dropDefaultHeaders(response);
|
|
188
|
+
validateCustomHeaders({}, result.headers);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (result.body && !result.rawBody) {
|
|
192
|
+
// RawBody takes priority if both are set (useful for backward compat) but if not then
|
|
193
|
+
// the body is automatically encoded to match the content-encoding header.
|
|
194
|
+
result.rawBody = await encodeBodyBuffer(
|
|
195
|
+
Buffer.from(result.body),
|
|
196
|
+
result.headers ?? {}
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
writeHead(
|
|
201
|
+
response,
|
|
202
|
+
result.statusCode || result.status || 200,
|
|
203
|
+
result.statusMessage,
|
|
204
|
+
result.headers
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (result.trailers) response.addTrailers(result.trailers);
|
|
208
|
+
|
|
209
|
+
response.end(result.rawBody || "");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export class CallbackHandler extends CallbackHandlerDefinition {
|
|
213
|
+
|
|
214
|
+
async handle(request: OngoingRequest, response: OngoingResponse) {
|
|
215
|
+
let req = await waitForCompletedRequest(request);
|
|
216
|
+
|
|
217
|
+
let outResponse: CallbackResponseResult;
|
|
218
|
+
try {
|
|
219
|
+
outResponse = await this.callback(req);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
writeHead(response, 500, 'Callback handler threw an exception');
|
|
222
|
+
console.warn(`Callback handler exception: ${(error as ErrorLike).message ?? error}`);
|
|
223
|
+
response.end(isErrorLike(error) ? error.toString() : error);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (outResponse === 'close') {
|
|
228
|
+
(request as any).socket.end();
|
|
229
|
+
throw new AbortError('Connection closed intentionally by rule');
|
|
230
|
+
} else if (outResponse === 'reset') {
|
|
231
|
+
requireSocketResetSupport();
|
|
232
|
+
resetOrDestroy(request);
|
|
233
|
+
throw new AbortError('Connection reset intentionally by rule');
|
|
234
|
+
} else {
|
|
235
|
+
await writeResponseFromCallback(outResponse, response);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
static deserialize({ name, version }: SerializedCallbackHandlerData, channel: ClientServerChannel): CallbackHandler {
|
|
243
|
+
const rpcCallback = async (request: CompletedRequest) => {
|
|
244
|
+
const callbackResult = await channel.request<
|
|
245
|
+
CallbackRequestMessage,
|
|
246
|
+
| WithSerializedCallbackBuffers<CallbackResponseMessageResult>
|
|
247
|
+
| 'close'
|
|
248
|
+
| 'reset'
|
|
249
|
+
>({ args: [
|
|
250
|
+
(version || -1) >= 2
|
|
251
|
+
? withSerializedBodyReader(request)
|
|
252
|
+
: request // Backward compat: old handlers
|
|
253
|
+
] });
|
|
254
|
+
|
|
255
|
+
if (typeof callbackResult === 'string') {
|
|
256
|
+
return callbackResult;
|
|
257
|
+
} else {
|
|
258
|
+
return withDeserializedCallbackBuffers(callbackResult);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
// Pass across the name from the real callback, for explain()
|
|
262
|
+
Object.defineProperty(rpcCallback, "name", { value: name });
|
|
263
|
+
|
|
264
|
+
// Call the client's callback (via stream), and save a handler on our end for
|
|
265
|
+
// the response that comes back.
|
|
266
|
+
return new CallbackHandler(rpcCallback);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export class StreamHandler extends StreamHandlerDefinition {
|
|
271
|
+
|
|
272
|
+
async handle(_request: OngoingRequest, response: OngoingResponse) {
|
|
273
|
+
if (!this.stream.done) {
|
|
274
|
+
if (this.headers) dropDefaultHeaders(response);
|
|
275
|
+
|
|
276
|
+
writeHead(response, this.status, undefined, this.headers);
|
|
277
|
+
this.stream.pipe(response);
|
|
278
|
+
this.stream.done = true;
|
|
279
|
+
} else {
|
|
280
|
+
throw new Error(stripIndent`
|
|
281
|
+
Stream request handler called more than once - this is not supported.
|
|
282
|
+
|
|
283
|
+
Streams can typically only be read once, so all subsequent requests would be empty.
|
|
284
|
+
To mock repeated stream requests, call 'thenStream' repeatedly with multiple streams.
|
|
285
|
+
|
|
286
|
+
(Have a better way to handle this? Open an issue at ${require('../../../package.json').bugs.url})
|
|
287
|
+
`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @internal
|
|
293
|
+
*/
|
|
294
|
+
static deserialize(handlerData: SerializedStreamHandlerData, channel: ClientServerChannel): StreamHandler {
|
|
295
|
+
const handlerStream = new Transform({
|
|
296
|
+
objectMode: true,
|
|
297
|
+
transform: function (this: Transform, message, encoding, callback) {
|
|
298
|
+
const { event, content } = message;
|
|
299
|
+
|
|
300
|
+
let deserializedEventData = content && (
|
|
301
|
+
content.type === 'string' ? content.value :
|
|
302
|
+
content.type === 'buffer' ? Buffer.from(content.value, 'base64') :
|
|
303
|
+
content.type === 'arraybuffer' ? Buffer.from(decodeBase64(content.value)) :
|
|
304
|
+
content.type === 'nil' && undefined
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (event === 'data' && deserializedEventData) {
|
|
308
|
+
this.push(deserializedEventData);
|
|
309
|
+
} else if (event === 'end') {
|
|
310
|
+
this.end();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
callback();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// When we get piped (i.e. to a live request), ping upstream to start streaming, and then
|
|
318
|
+
// pipe the resulting data into our live stream (which is streamed to the request, like normal)
|
|
319
|
+
handlerStream.once('resume', () => {
|
|
320
|
+
channel.pipe(handlerStream);
|
|
321
|
+
channel.write({});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return new StreamHandler(
|
|
325
|
+
handlerData.status,
|
|
326
|
+
handlerStream,
|
|
327
|
+
handlerData.headers
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export class FileHandler extends FileHandlerDefinition {
|
|
333
|
+
async handle(_request: OngoingRequest, response: OngoingResponse) {
|
|
334
|
+
// Read the file first, to ensure we error cleanly if it's unavailable
|
|
335
|
+
const fileContents = await fs.readFile(this.filePath);
|
|
336
|
+
|
|
337
|
+
if (this.headers) dropDefaultHeaders(response);
|
|
338
|
+
|
|
339
|
+
writeHead(response, this.status, this.statusMessage, this.headers);
|
|
340
|
+
response.end(fileContents);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function validateCustomHeaders(
|
|
345
|
+
originalHeaders: Headers,
|
|
346
|
+
modifiedHeaders: Headers | undefined,
|
|
347
|
+
headerWhitelist: readonly string[] = []
|
|
348
|
+
) {
|
|
349
|
+
if (!modifiedHeaders) return;
|
|
350
|
+
|
|
351
|
+
// We ignore most returned pseudo headers, so we error if you try to manually set them
|
|
352
|
+
const invalidHeaders = _(modifiedHeaders)
|
|
353
|
+
.pickBy((value, name) =>
|
|
354
|
+
name.toString().startsWith(':') &&
|
|
355
|
+
// We allow returning a preexisting header value - that's ignored
|
|
356
|
+
// silently, so that mutating & returning the provided headers is always safe.
|
|
357
|
+
value !== originalHeaders[name] &&
|
|
358
|
+
// In some cases, specific custom pseudoheaders may be allowed, e.g. requests
|
|
359
|
+
// can have custom :scheme and :authority headers set.
|
|
360
|
+
!headerWhitelist.includes(name)
|
|
361
|
+
)
|
|
362
|
+
.keys();
|
|
363
|
+
|
|
364
|
+
if (invalidHeaders.size() > 0) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Cannot set custom ${invalidHeaders.join(', ')} pseudoheader values`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Used in merging as a marker for values to omit, because lodash ignores undefineds.
|
|
372
|
+
const OMIT_SYMBOL = Symbol('omit-value');
|
|
373
|
+
|
|
374
|
+
// We play some games to preserve undefined values during serialization, because we differentiate them
|
|
375
|
+
// in some transforms from null/not-present keys.
|
|
376
|
+
const mapOmitToUndefined = <T extends { [key: string]: any }>(
|
|
377
|
+
input: T
|
|
378
|
+
): { [K in keyof T]: T[K] | undefined } =>
|
|
379
|
+
_.mapValues(input, (v) =>
|
|
380
|
+
v === SERIALIZED_OMIT || v === OMIT_SYMBOL
|
|
381
|
+
? undefined // Replace our omit placeholders with actual undefineds
|
|
382
|
+
: v
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
export class PassThroughHandler extends PassThroughHandlerDefinition {
|
|
386
|
+
|
|
387
|
+
private _trustedCACertificates: MaybePromise<Array<string> | undefined>;
|
|
388
|
+
private async trustedCACertificates(): Promise<Array<string> | undefined> {
|
|
389
|
+
if (!this.extraCACertificates.length) return undefined;
|
|
390
|
+
|
|
391
|
+
if (!this._trustedCACertificates) {
|
|
392
|
+
this._trustedCACertificates = getTrustedCAs(undefined, this.extraCACertificates);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return this._trustedCACertificates;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async handle(
|
|
399
|
+
clientReq: OngoingRequest,
|
|
400
|
+
clientRes: OngoingResponse,
|
|
401
|
+
options: RequestHandlerOptions
|
|
402
|
+
) {
|
|
403
|
+
// Don't let Node add any default standard headers - we want full control
|
|
404
|
+
dropDefaultHeaders(clientRes);
|
|
405
|
+
|
|
406
|
+
// Capture raw request data:
|
|
407
|
+
let { method, url: reqUrl, rawHeaders } = clientReq as OngoingRequest;
|
|
408
|
+
let { protocol, hostname, port, path } = url.parse(reqUrl);
|
|
409
|
+
|
|
410
|
+
// Check if this request is a request loop:
|
|
411
|
+
if (isSocketLoop(this.outgoingSockets, (<any> clientReq).socket)) {
|
|
412
|
+
throw new Error(oneLine`
|
|
413
|
+
Passthrough loop detected. This probably means you're sending a request directly
|
|
414
|
+
to a passthrough endpoint, which is forwarding it to the target URL, which is a
|
|
415
|
+
passthrough endpoint, which is forwarding it to the target URL, which is a
|
|
416
|
+
passthrough endpoint...` +
|
|
417
|
+
'\n\n' + oneLine`
|
|
418
|
+
You should either explicitly mock a response for this URL (${reqUrl}), or use
|
|
419
|
+
the server as a proxy, instead of making requests to it directly.
|
|
420
|
+
`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// We have to capture the request stream immediately, to make sure nothing is lost if it
|
|
424
|
+
// goes past its max length (truncating the data) before we start sending upstream.
|
|
425
|
+
const clientReqBody = clientReq.body.asStream();
|
|
426
|
+
|
|
427
|
+
const isH2Downstream = isHttp2(clientReq);
|
|
428
|
+
|
|
429
|
+
hostname = await getClientRelativeHostname(
|
|
430
|
+
hostname,
|
|
431
|
+
clientReq.remoteIpAddress,
|
|
432
|
+
getDnsLookupFunction(this.lookupOptions)
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
if (this.forwarding) {
|
|
436
|
+
const { targetHost, updateHostHeader } = this.forwarding;
|
|
437
|
+
if (!targetHost.includes('/')) {
|
|
438
|
+
// We're forwarding to a bare hostname
|
|
439
|
+
[hostname, port] = targetHost.split(':');
|
|
440
|
+
} else {
|
|
441
|
+
// We're forwarding to a fully specified URL; override the host etc, but never the path.
|
|
442
|
+
({ protocol, hostname, port } = url.parse(targetHost));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const hostHeaderName = isH2Downstream ? ':authority' : 'host';
|
|
446
|
+
|
|
447
|
+
let hostHeaderIndex = findRawHeaderIndex(rawHeaders, hostHeaderName);
|
|
448
|
+
let hostHeader: [string, string];
|
|
449
|
+
|
|
450
|
+
if (hostHeaderIndex === -1) {
|
|
451
|
+
// Should never happen really, but just in case:
|
|
452
|
+
hostHeader = [hostHeaderName, hostname!];
|
|
453
|
+
hostHeaderIndex = rawHeaders.length;
|
|
454
|
+
} else {
|
|
455
|
+
// Clone this - we don't want to modify the original headers, as they're used for events
|
|
456
|
+
hostHeader = _.clone(rawHeaders[hostHeaderIndex]);
|
|
457
|
+
}
|
|
458
|
+
rawHeaders[hostHeaderIndex] = hostHeader;
|
|
459
|
+
|
|
460
|
+
if (updateHostHeader === undefined || updateHostHeader === true) {
|
|
461
|
+
// If updateHostHeader is true, or just not specified, match the new target
|
|
462
|
+
hostHeader[1] = hostname + (port ? `:${port}` : '');
|
|
463
|
+
} else if (updateHostHeader) {
|
|
464
|
+
// If it's an explicit custom value, use that directly.
|
|
465
|
+
hostHeader[1] = updateHostHeader;
|
|
466
|
+
} // Otherwise: falsey means don't touch it.
|
|
467
|
+
|
|
468
|
+
reqUrl = new URL(`${protocol}//${hostname}${(port ? `:${port}` : '')}${path}`).toString();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Override the request details, if a transform or callback is specified:
|
|
472
|
+
let reqBodyOverride: Uint8Array | undefined;
|
|
473
|
+
|
|
474
|
+
// Set during modification here - if set, we allow overriding certain H2 headers so that manual
|
|
475
|
+
// modification of the supported headers works as expected.
|
|
476
|
+
let headersManuallyModified = false;
|
|
477
|
+
|
|
478
|
+
if (this.transformRequest) {
|
|
479
|
+
let headers = rawHeadersToObject(rawHeaders);
|
|
480
|
+
|
|
481
|
+
const {
|
|
482
|
+
replaceMethod,
|
|
483
|
+
updateHeaders,
|
|
484
|
+
replaceHeaders,
|
|
485
|
+
replaceBody,
|
|
486
|
+
replaceBodyFromFile,
|
|
487
|
+
updateJsonBody,
|
|
488
|
+
patchJsonBody,
|
|
489
|
+
matchReplaceBody
|
|
490
|
+
} = this.transformRequest;
|
|
491
|
+
|
|
492
|
+
if (replaceMethod) {
|
|
493
|
+
method = replaceMethod;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (updateHeaders) {
|
|
497
|
+
headers = {
|
|
498
|
+
...headers,
|
|
499
|
+
...updateHeaders
|
|
500
|
+
};
|
|
501
|
+
headersManuallyModified = true;
|
|
502
|
+
} else if (replaceHeaders) {
|
|
503
|
+
headers = { ...replaceHeaders };
|
|
504
|
+
headersManuallyModified = true;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (replaceBody) {
|
|
508
|
+
// Note that we're replacing the body without actually waiting for the real one, so
|
|
509
|
+
// this can result in sending a request much more quickly!
|
|
510
|
+
reqBodyOverride = asBuffer(replaceBody);
|
|
511
|
+
} else if (replaceBodyFromFile) {
|
|
512
|
+
reqBodyOverride = await fs.readFile(replaceBodyFromFile);
|
|
513
|
+
} else if (updateJsonBody) {
|
|
514
|
+
const { body: realBody } = await waitForCompletedRequest(clientReq);
|
|
515
|
+
const jsonBody = await realBody.getJson();
|
|
516
|
+
if (jsonBody === undefined) {
|
|
517
|
+
throw new Error("Can't update JSON in non-JSON request body");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const updatedBody = _.mergeWith(jsonBody, updateJsonBody, (_oldValue, newValue) => {
|
|
521
|
+
// We want to remove values with undefines, but Lodash ignores
|
|
522
|
+
// undefined return values here. Fortunately, JSON.stringify
|
|
523
|
+
// ignores Symbols, omitting them from the result.
|
|
524
|
+
if (newValue === undefined) return OMIT_SYMBOL;
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
reqBodyOverride = asBuffer(JSON.stringify(updatedBody));
|
|
528
|
+
} else if (patchJsonBody) {
|
|
529
|
+
const { body: realBody } = await waitForCompletedRequest(clientReq);
|
|
530
|
+
const jsonBody = await realBody.getJson();
|
|
531
|
+
if (jsonBody === undefined) {
|
|
532
|
+
throw new Error("Can't patch JSON in non-JSON request body");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
applyJsonPatch(jsonBody, patchJsonBody, true); // Mutates the JSON body returned above
|
|
536
|
+
reqBodyOverride = asBuffer(JSON.stringify(jsonBody));
|
|
537
|
+
} else if (matchReplaceBody) {
|
|
538
|
+
const { body: realBody } = await waitForCompletedRequest(clientReq);
|
|
539
|
+
|
|
540
|
+
const originalBody = await realBody.getText();
|
|
541
|
+
if (originalBody === undefined) {
|
|
542
|
+
throw new Error("Can't match & replace non-decodeable request body");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
let replacedBody = originalBody;
|
|
546
|
+
for (let [match, result] of matchReplaceBody) {
|
|
547
|
+
replacedBody = replacedBody!.replace(match, result);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (replacedBody !== originalBody) {
|
|
551
|
+
reqBodyOverride = asBuffer(replacedBody);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (reqBodyOverride) {
|
|
556
|
+
// We always re-encode the body to match the resulting content-encoding header:
|
|
557
|
+
reqBodyOverride = await encodeBodyBuffer(
|
|
558
|
+
reqBodyOverride,
|
|
559
|
+
headers
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
headers['content-length'] = getContentLengthAfterModification(
|
|
563
|
+
reqBodyOverride,
|
|
564
|
+
clientReq.headers,
|
|
565
|
+
(updateHeaders && updateHeaders['content-length'] !== undefined)
|
|
566
|
+
? headers // Iff you replaced the content length
|
|
567
|
+
: replaceHeaders,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (headersManuallyModified || reqBodyOverride) {
|
|
572
|
+
// If the headers have been updated (implicitly or explicitly) we need to regenerate them. We avoid
|
|
573
|
+
// this if possible, because it normalizes headers, which is slightly lossy (e.g. they're lowercased).
|
|
574
|
+
rawHeaders = objectHeadersToRaw(headers);
|
|
575
|
+
}
|
|
576
|
+
} else if (this.beforeRequest) {
|
|
577
|
+
const clientRawHeaders = rawHeaders;
|
|
578
|
+
const clientHeaders = rawHeadersToObject(clientRawHeaders);
|
|
579
|
+
|
|
580
|
+
const completedRequest = await waitForCompletedRequest(clientReq);
|
|
581
|
+
|
|
582
|
+
const modifiedReq = await this.beforeRequest({
|
|
583
|
+
...completedRequest,
|
|
584
|
+
url: reqUrl, // May have been overwritten by forwarding
|
|
585
|
+
headers: _.cloneDeep(clientHeaders),
|
|
586
|
+
rawHeaders: _.cloneDeep(clientRawHeaders)
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
if (modifiedReq?.response) {
|
|
590
|
+
if (modifiedReq.response === 'close') {
|
|
591
|
+
const socket: net.Socket = (<any> clientReq).socket;
|
|
592
|
+
socket.end();
|
|
593
|
+
throw new AbortError('Connection closed intentionally by rule');
|
|
594
|
+
} else if (modifiedReq.response === 'reset') {
|
|
595
|
+
requireSocketResetSupport();
|
|
596
|
+
resetOrDestroy(clientReq);
|
|
597
|
+
throw new AbortError('Connection reset intentionally by rule');
|
|
598
|
+
} else {
|
|
599
|
+
// The callback has provided a full response: don't passthrough at all, just use it.
|
|
600
|
+
await writeResponseFromCallback(modifiedReq.response, clientRes);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
method = modifiedReq?.method || method;
|
|
606
|
+
reqUrl = modifiedReq?.url || reqUrl;
|
|
607
|
+
|
|
608
|
+
headersManuallyModified = !!modifiedReq?.headers;
|
|
609
|
+
let headers = modifiedReq?.headers || clientHeaders;
|
|
610
|
+
|
|
611
|
+
// We need to make sure the Host/:authority header is updated correctly - following the user's returned value if
|
|
612
|
+
// they provided one, but updating it if not to match the effective target URL of the request:
|
|
613
|
+
const expectedTargetUrl = modifiedReq?.url
|
|
614
|
+
?? (
|
|
615
|
+
// If not overridden, we fall back to the original value, but we need to handle changes that forwarding
|
|
616
|
+
// might have made as well, especially if it's intentionally left URL & headers out of sync:
|
|
617
|
+
this.forwarding?.updateHostHeader === false
|
|
618
|
+
? clientReq.url
|
|
619
|
+
: reqUrl
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
Object.assign(headers,
|
|
623
|
+
isH2Downstream
|
|
624
|
+
? getH2HeadersAfterModification(expectedTargetUrl, clientHeaders, modifiedReq?.headers)
|
|
625
|
+
: { 'host': getHostAfterModification(expectedTargetUrl, clientHeaders, modifiedReq?.headers) }
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
validateCustomHeaders(
|
|
629
|
+
clientHeaders,
|
|
630
|
+
modifiedReq?.headers,
|
|
631
|
+
OVERRIDABLE_REQUEST_PSEUDOHEADERS // These are handled by getCorrectPseudoheaders above
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
reqBodyOverride = await buildOverriddenBody(modifiedReq, headers);
|
|
635
|
+
|
|
636
|
+
if (reqBodyOverride) {
|
|
637
|
+
// Automatically match the content-length to the body, unless it was explicitly overriden.
|
|
638
|
+
headers['content-length'] = getContentLengthAfterModification(
|
|
639
|
+
reqBodyOverride,
|
|
640
|
+
clientHeaders,
|
|
641
|
+
modifiedReq?.headers
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Reparse the new URL, if necessary
|
|
646
|
+
if (modifiedReq?.url) {
|
|
647
|
+
if (!isAbsoluteUrl(modifiedReq?.url)) throw new Error("Overridden request URLs must be absolute");
|
|
648
|
+
({ protocol, hostname, port, path } = url.parse(reqUrl));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
rawHeaders = objectHeadersToRaw(headers);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const effectivePort = getEffectivePort({ protocol, port });
|
|
655
|
+
|
|
656
|
+
const strictHttpsChecks = shouldUseStrictHttps(
|
|
657
|
+
hostname!,
|
|
658
|
+
effectivePort,
|
|
659
|
+
this.ignoreHostHttpsErrors
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// Use a client cert if it's listed for the host+port or whole hostname
|
|
663
|
+
const hostWithPort = `${hostname}:${effectivePort}`;
|
|
664
|
+
const clientCert = this.clientCertificateHostMap[hostWithPort] ||
|
|
665
|
+
this.clientCertificateHostMap[hostname!] ||
|
|
666
|
+
{};
|
|
667
|
+
|
|
668
|
+
const trustedCerts = await this.trustedCACertificates();
|
|
669
|
+
const caConfig = trustedCerts
|
|
670
|
+
? { ca: trustedCerts }
|
|
671
|
+
: {};
|
|
672
|
+
|
|
673
|
+
// We only do H2 upstream for HTTPS. Http2-wrapper doesn't support H2C, it's rarely used
|
|
674
|
+
// and we can't use ALPN to detect HTTP/2 support cleanly.
|
|
675
|
+
let shouldTryH2Upstream = isH2Downstream && protocol === 'https:';
|
|
676
|
+
|
|
677
|
+
let family: undefined | 4 | 6;
|
|
678
|
+
if (hostname === 'localhost') {
|
|
679
|
+
// Annoying special case: some localhost servers listen only on either ipv4 or ipv6.
|
|
680
|
+
// Very specific situation, but a very common one for development use.
|
|
681
|
+
// We need to work out which one family is, as Node sometimes makes bad choices.
|
|
682
|
+
|
|
683
|
+
if (await isLocalPortActive('::1', effectivePort)) family = 6;
|
|
684
|
+
else family = 4;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Remote clients might configure a passthrough rule with a parameter reference for the proxy,
|
|
688
|
+
// delegating proxy config to the admin server. That's fine initially, but you can't actually
|
|
689
|
+
// handle a request in that case - make sure our proxyConfig is always dereferenced before use.
|
|
690
|
+
const proxySettingSource = assertParamDereferenced(this.proxyConfig) as ProxySettingSource;
|
|
691
|
+
|
|
692
|
+
// Mirror the keep-alive-ness of the incoming request in our outgoing request
|
|
693
|
+
const agent = await getAgent({
|
|
694
|
+
protocol: (protocol || undefined) as 'http:' | 'https:' | undefined,
|
|
695
|
+
hostname: hostname!,
|
|
696
|
+
port: effectivePort,
|
|
697
|
+
tryHttp2: shouldTryH2Upstream,
|
|
698
|
+
keepAlive: shouldKeepAlive(clientReq),
|
|
699
|
+
proxySettingSource
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
if (agent && !('http2' in agent)) {
|
|
703
|
+
// I.e. only use HTTP/2 if we're using an HTTP/2-compatible agent
|
|
704
|
+
shouldTryH2Upstream = false;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let makeRequest = (
|
|
708
|
+
shouldTryH2Upstream
|
|
709
|
+
? (options: any, cb: any) =>
|
|
710
|
+
h2Client.auto(options, cb).catch((e) => {
|
|
711
|
+
// If an error occurs during auto detection via ALPN, that's an
|
|
712
|
+
// TypeError implies it's an invalid HTTP/2 request that was rejected.
|
|
713
|
+
// Anything else implies an upstream HTTP/2 issue.
|
|
714
|
+
e.causedByUpstreamError = !(e instanceof TypeError);
|
|
715
|
+
throw e;
|
|
716
|
+
})
|
|
717
|
+
// HTTP/1 + TLS
|
|
718
|
+
: protocol === 'https:'
|
|
719
|
+
? https.request
|
|
720
|
+
// HTTP/1 plaintext:
|
|
721
|
+
: http.request
|
|
722
|
+
) as typeof https.request;
|
|
723
|
+
|
|
724
|
+
if (isH2Downstream && shouldTryH2Upstream) {
|
|
725
|
+
// We drop all incoming pseudoheaders, and regenerate them (except legally modified ones)
|
|
726
|
+
rawHeaders = rawHeaders.filter(([key]) =>
|
|
727
|
+
!key.toString().startsWith(':') ||
|
|
728
|
+
(headersManuallyModified &&
|
|
729
|
+
OVERRIDABLE_REQUEST_PSEUDOHEADERS.includes(key.toLowerCase() as any)
|
|
730
|
+
)
|
|
731
|
+
);
|
|
732
|
+
} else if (isH2Downstream && !shouldTryH2Upstream) {
|
|
733
|
+
rawHeaders = h2HeadersToH1(rawHeaders);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Drop proxy-connection header. This is almost always intended for us, not for upstream servers,
|
|
737
|
+
// and forwarding it causes problems (most notably, it triggers lots of weird-traffic blocks,
|
|
738
|
+
// most notably by Cloudflare).
|
|
739
|
+
rawHeaders = rawHeaders.filter(([key]) =>
|
|
740
|
+
key.toLowerCase() !== 'proxy-connection'
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
let serverReq: http.ClientRequest;
|
|
744
|
+
return new Promise<void>((resolve, reject) => (async () => { // Wrapped to easily catch (a)sync errors
|
|
745
|
+
serverReq = await makeRequest({
|
|
746
|
+
protocol,
|
|
747
|
+
method,
|
|
748
|
+
hostname,
|
|
749
|
+
port,
|
|
750
|
+
family,
|
|
751
|
+
path,
|
|
752
|
+
headers: shouldTryH2Upstream
|
|
753
|
+
? rawHeadersToObjectPreservingCase(rawHeaders)
|
|
754
|
+
: flattenPairedRawHeaders(rawHeaders) as any,
|
|
755
|
+
lookup: getDnsLookupFunction(this.lookupOptions) as typeof dns.lookup,
|
|
756
|
+
// ^ Cast required to handle __promisify__ type hack in the official Node types
|
|
757
|
+
agent,
|
|
758
|
+
|
|
759
|
+
// TLS options:
|
|
760
|
+
...getUpstreamTlsOptions(strictHttpsChecks),
|
|
761
|
+
...clientCert,
|
|
762
|
+
...caConfig
|
|
763
|
+
}, (serverRes) => (async () => {
|
|
764
|
+
serverRes.on('error', (e: any) => {
|
|
765
|
+
e.causedByUpstreamError = true;
|
|
766
|
+
reject(e);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Forward server trailers, if we receive any:
|
|
770
|
+
serverRes.on('end', () => {
|
|
771
|
+
if (!serverRes.rawTrailers?.length) return;
|
|
772
|
+
|
|
773
|
+
const trailersToForward = pairFlatRawHeaders(serverRes.rawTrailers)
|
|
774
|
+
.filter(([key, value]) => {
|
|
775
|
+
if (!validateHeader(key, value)) {
|
|
776
|
+
console.warn(`Not forwarding invalid trailer: "${key}: ${value}"`);
|
|
777
|
+
// Nothing else we can do in this case regardless - setHeaders will
|
|
778
|
+
// throw within Node if we try to set this value.
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
return true;
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
try {
|
|
785
|
+
clientRes.addTrailers(
|
|
786
|
+
isHttp2(clientReq)
|
|
787
|
+
// HTTP/2 compat doesn't support raw headers here (yet)
|
|
788
|
+
? rawHeadersToObjectPreservingCase(trailersToForward)
|
|
789
|
+
: trailersToForward
|
|
790
|
+
);
|
|
791
|
+
} catch (e) {
|
|
792
|
+
console.warn(`Failed to forward response trailers: ${e}`);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
let serverStatusCode = serverRes.statusCode!;
|
|
797
|
+
let serverStatusMessage = serverRes.statusMessage
|
|
798
|
+
let serverRawHeaders = pairFlatRawHeaders(serverRes.rawHeaders);
|
|
799
|
+
|
|
800
|
+
// This is only set if we need to read the body here, for a callback or similar. If so,
|
|
801
|
+
// we keep the buffer in case we need it afterwards (if the cb doesn't replace it).
|
|
802
|
+
let originalBody: Buffer | undefined;
|
|
803
|
+
|
|
804
|
+
// This is set when we override the body data. Note that this doesn't mean we actually
|
|
805
|
+
// read & buffered the original data! With a fixed replacement body we can skip that.
|
|
806
|
+
let resBodyOverride: Uint8Array | undefined;
|
|
807
|
+
|
|
808
|
+
if (options.emitEventCallback) {
|
|
809
|
+
options.emitEventCallback('passthrough-response-head', {
|
|
810
|
+
statusCode: serverStatusCode,
|
|
811
|
+
statusMessage: serverStatusMessage,
|
|
812
|
+
httpVersion: serverRes.httpVersion,
|
|
813
|
+
rawHeaders: serverRawHeaders
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (isH2Downstream) {
|
|
818
|
+
serverRawHeaders = h1HeadersToH2(serverRawHeaders);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (this.transformResponse) {
|
|
822
|
+
let serverHeaders = rawHeadersToObject(serverRawHeaders);
|
|
823
|
+
|
|
824
|
+
const {
|
|
825
|
+
replaceStatus,
|
|
826
|
+
updateHeaders,
|
|
827
|
+
replaceHeaders,
|
|
828
|
+
replaceBody,
|
|
829
|
+
replaceBodyFromFile,
|
|
830
|
+
updateJsonBody,
|
|
831
|
+
patchJsonBody,
|
|
832
|
+
matchReplaceBody
|
|
833
|
+
} = this.transformResponse;
|
|
834
|
+
|
|
835
|
+
if (replaceStatus) {
|
|
836
|
+
serverStatusCode = replaceStatus;
|
|
837
|
+
serverStatusMessage = undefined; // Reset to default
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (updateHeaders) {
|
|
841
|
+
serverHeaders = {
|
|
842
|
+
...serverHeaders,
|
|
843
|
+
...updateHeaders
|
|
844
|
+
};
|
|
845
|
+
} else if (replaceHeaders) {
|
|
846
|
+
serverHeaders = { ...replaceHeaders };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (replaceBody) {
|
|
850
|
+
// Note that we're replacing the body without actually waiting for the real one, so
|
|
851
|
+
// this can result in sending a request much more quickly!
|
|
852
|
+
resBodyOverride = asBuffer(replaceBody);
|
|
853
|
+
} else if (replaceBodyFromFile) {
|
|
854
|
+
resBodyOverride = await fs.readFile(replaceBodyFromFile);
|
|
855
|
+
} else if (updateJsonBody) {
|
|
856
|
+
originalBody = await streamToBuffer(serverRes);
|
|
857
|
+
const realBody = buildBodyReader(originalBody, serverRes.headers);
|
|
858
|
+
const jsonBody = await realBody.getJson();
|
|
859
|
+
|
|
860
|
+
if (jsonBody === undefined) {
|
|
861
|
+
throw new Error("Can't update JSON in non-JSON response body");
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const updatedBody = _.mergeWith(jsonBody, updateJsonBody, (_oldValue, newValue) => {
|
|
865
|
+
// We want to remove values with undefines, but Lodash ignores
|
|
866
|
+
// undefined return values here. Fortunately, JSON.stringify
|
|
867
|
+
// ignores Symbols, omitting them from the result.
|
|
868
|
+
if (newValue === undefined) return OMIT_SYMBOL;
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
resBodyOverride = asBuffer(JSON.stringify(updatedBody));
|
|
872
|
+
} else if (patchJsonBody) {
|
|
873
|
+
originalBody = await streamToBuffer(serverRes);
|
|
874
|
+
const realBody = buildBodyReader(originalBody, serverRes.headers);
|
|
875
|
+
const jsonBody = await realBody.getJson();
|
|
876
|
+
|
|
877
|
+
if (jsonBody === undefined) {
|
|
878
|
+
throw new Error("Can't patch JSON in non-JSON response body");
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
applyJsonPatch(jsonBody, patchJsonBody, true); // Mutates the JSON body returned above
|
|
882
|
+
resBodyOverride = asBuffer(JSON.stringify(jsonBody));
|
|
883
|
+
} else if (matchReplaceBody) {
|
|
884
|
+
originalBody = await streamToBuffer(serverRes);
|
|
885
|
+
const realBody = buildBodyReader(originalBody, serverRes.headers);
|
|
886
|
+
|
|
887
|
+
const originalBodyText = await realBody.getText();
|
|
888
|
+
if (originalBodyText === undefined) {
|
|
889
|
+
throw new Error("Can't match & replace non-decodeable response body");
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
let replacedBody = originalBodyText;
|
|
893
|
+
for (let [match, result] of matchReplaceBody) {
|
|
894
|
+
replacedBody = replacedBody!.replace(match, result);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (replacedBody !== originalBodyText) {
|
|
898
|
+
resBodyOverride = asBuffer(replacedBody);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (resBodyOverride) {
|
|
903
|
+
// In the above cases, the overriding data is assumed to always be in decoded form,
|
|
904
|
+
// so we re-encode the body to match the resulting content-encoding header:
|
|
905
|
+
resBodyOverride = await encodeBodyBuffer(
|
|
906
|
+
resBodyOverride,
|
|
907
|
+
serverHeaders
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
serverHeaders['content-length'] = getContentLengthAfterModification(
|
|
911
|
+
resBodyOverride,
|
|
912
|
+
serverRes.headers,
|
|
913
|
+
(updateHeaders && updateHeaders['content-length'] !== undefined)
|
|
914
|
+
? serverHeaders // Iff you replaced the content length
|
|
915
|
+
: replaceHeaders,
|
|
916
|
+
method === 'HEAD' // HEAD responses are allowed mismatched content-length
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
serverRawHeaders = objectHeadersToRaw(serverHeaders);
|
|
921
|
+
} else if (this.beforeResponse) {
|
|
922
|
+
let modifiedRes: CallbackResponseResult | void;
|
|
923
|
+
|
|
924
|
+
originalBody = await streamToBuffer(serverRes);
|
|
925
|
+
let serverHeaders = rawHeadersToObject(serverRawHeaders);
|
|
926
|
+
|
|
927
|
+
modifiedRes = await this.beforeResponse({
|
|
928
|
+
id: clientReq.id,
|
|
929
|
+
statusCode: serverStatusCode,
|
|
930
|
+
statusMessage: serverRes.statusMessage,
|
|
931
|
+
headers: serverHeaders,
|
|
932
|
+
rawHeaders: _.cloneDeep(serverRawHeaders),
|
|
933
|
+
body: buildBodyReader(originalBody, serverHeaders)
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
if (modifiedRes === 'close') {
|
|
937
|
+
(clientReq as any).socket.end();
|
|
938
|
+
throw new AbortError('Connection closed intentionally by rule');
|
|
939
|
+
} else if (modifiedRes === 'reset') {
|
|
940
|
+
requireSocketResetSupport();
|
|
941
|
+
resetOrDestroy(clientReq);
|
|
942
|
+
throw new AbortError('Connection reset intentionally by rule');
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
validateCustomHeaders(serverHeaders, modifiedRes?.headers);
|
|
946
|
+
|
|
947
|
+
serverStatusCode = modifiedRes?.statusCode ||
|
|
948
|
+
modifiedRes?.status ||
|
|
949
|
+
serverStatusCode;
|
|
950
|
+
serverStatusMessage = modifiedRes?.statusMessage ||
|
|
951
|
+
serverStatusMessage;
|
|
952
|
+
|
|
953
|
+
serverHeaders = modifiedRes?.headers || serverHeaders;
|
|
954
|
+
|
|
955
|
+
resBodyOverride = await buildOverriddenBody(modifiedRes, serverHeaders);
|
|
956
|
+
|
|
957
|
+
if (resBodyOverride) {
|
|
958
|
+
serverHeaders['content-length'] = getContentLengthAfterModification(
|
|
959
|
+
resBodyOverride,
|
|
960
|
+
serverRes.headers,
|
|
961
|
+
modifiedRes?.headers,
|
|
962
|
+
method === 'HEAD' // HEAD responses are allowed mismatched content-length
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
serverRawHeaders = objectHeadersToRaw(serverHeaders);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
writeHead(
|
|
970
|
+
clientRes,
|
|
971
|
+
serverStatusCode,
|
|
972
|
+
serverStatusMessage,
|
|
973
|
+
serverRawHeaders
|
|
974
|
+
.filter(([key, value]) => {
|
|
975
|
+
if (key === ':status') return false;
|
|
976
|
+
if (!validateHeader(key, value)) {
|
|
977
|
+
console.warn(`Not forwarding invalid header: "${key}: ${value}"`);
|
|
978
|
+
// Nothing else we can do in this case regardless - setHeaders will
|
|
979
|
+
// throw within Node if we try to set this value.
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
return true;
|
|
983
|
+
})
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
if (resBodyOverride) {
|
|
987
|
+
// Return the override data to the client:
|
|
988
|
+
clientRes.end(resBodyOverride);
|
|
989
|
+
|
|
990
|
+
// Dump the real response data, in case that body wasn't read yet:
|
|
991
|
+
serverRes.resume();
|
|
992
|
+
resolve();
|
|
993
|
+
} else if (originalBody) {
|
|
994
|
+
// If the original body was read, and not overridden, then send it
|
|
995
|
+
// onward directly:
|
|
996
|
+
clientRes.end(originalBody);
|
|
997
|
+
resolve();
|
|
998
|
+
} else {
|
|
999
|
+
// Otherwise the body hasn't been read - stream it live:
|
|
1000
|
+
serverRes.pipe(clientRes);
|
|
1001
|
+
serverRes.once('end', resolve);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (options.emitEventCallback) {
|
|
1005
|
+
if (!!resBodyOverride) {
|
|
1006
|
+
(originalBody
|
|
1007
|
+
? Promise.resolve(originalBody)
|
|
1008
|
+
: streamToBuffer(serverRes)
|
|
1009
|
+
).then((upstreamBody) => {
|
|
1010
|
+
options.emitEventCallback!('passthrough-response-body', {
|
|
1011
|
+
overridden: true,
|
|
1012
|
+
rawBody: upstreamBody
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
} else {
|
|
1016
|
+
options.emitEventCallback('passthrough-response-body', {
|
|
1017
|
+
overridden: false
|
|
1018
|
+
// We don't bother buffering & re-sending the body if
|
|
1019
|
+
// it's the same as the one being sent to the client.
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
})().catch(reject));
|
|
1024
|
+
|
|
1025
|
+
serverReq.once('socket', (socket: net.Socket) => {
|
|
1026
|
+
// This event can fire multiple times for keep-alive sockets, which are used to
|
|
1027
|
+
// make multiple requests. If/when that happens, we don't need more event listeners.
|
|
1028
|
+
if (this.outgoingSockets.has(socket)) return;
|
|
1029
|
+
|
|
1030
|
+
// Add this port to our list of active ports, once it's connected (before then it has no port)
|
|
1031
|
+
if (socket.connecting) {
|
|
1032
|
+
socket.once('connect', () => {
|
|
1033
|
+
this.outgoingSockets.add(socket)
|
|
1034
|
+
});
|
|
1035
|
+
} else if (socket.localPort !== undefined) {
|
|
1036
|
+
this.outgoingSockets.add(socket);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Remove this port from our list of active ports when it's closed
|
|
1040
|
+
// This is called for both clean closes & errors.
|
|
1041
|
+
socket.once('close', () => this.outgoingSockets.delete(socket));
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
// Forward any request trailers received from the client:
|
|
1045
|
+
const forwardTrailers = () => {
|
|
1046
|
+
if (clientReq.rawTrailers?.length) {
|
|
1047
|
+
if (serverReq.addTrailers) {
|
|
1048
|
+
serverReq.addTrailers(clientReq.rawTrailers);
|
|
1049
|
+
} else {
|
|
1050
|
+
// See https://github.com/szmarczak/http2-wrapper/issues/103
|
|
1051
|
+
console.warn('Not forwarding request trailers - not yet supported for HTTP/2');
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
// This has to be above the pipe setup below, or we end the stream before adding the
|
|
1056
|
+
// trailers, and they're lost.
|
|
1057
|
+
if (clientReqBody.readableEnded) {
|
|
1058
|
+
forwardTrailers();
|
|
1059
|
+
} else {
|
|
1060
|
+
clientReqBody.once('end', forwardTrailers);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Forward the request body to the upstream server:
|
|
1064
|
+
if (reqBodyOverride) {
|
|
1065
|
+
clientReqBody.resume(); // Dump any remaining real request body
|
|
1066
|
+
|
|
1067
|
+
if (reqBodyOverride.length > 0) serverReq.end(reqBodyOverride);
|
|
1068
|
+
else serverReq.end(); // http2-wrapper fails given an empty buffer for methods that aren't allowed a body
|
|
1069
|
+
} else {
|
|
1070
|
+
// asStream includes all content, including the body before this call
|
|
1071
|
+
clientReqBody.pipe(serverReq);
|
|
1072
|
+
clientReqBody.on('error', () => serverReq.abort());
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// If the downstream connection aborts, before the response has been completed,
|
|
1076
|
+
// we also abort the upstream connection. Important to avoid unnecessary connections,
|
|
1077
|
+
// and to correctly proxy client connection behaviour to the upstream server.
|
|
1078
|
+
function abortUpstream() {
|
|
1079
|
+
serverReq.abort();
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Handle the case where the downstream connection is prematurely closed before
|
|
1083
|
+
// fully sending the request or receiving the response.
|
|
1084
|
+
clientReq.on('aborted', abortUpstream);
|
|
1085
|
+
clientRes.on('close', abortUpstream);
|
|
1086
|
+
|
|
1087
|
+
// Disable the upstream request abort handlers once the response has been received.
|
|
1088
|
+
clientRes.once('finish', () => {
|
|
1089
|
+
clientReq.off('aborted', abortUpstream);
|
|
1090
|
+
clientRes.off('close', abortUpstream);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
serverReq.on('error', (e: any) => {
|
|
1094
|
+
e.causedByUpstreamError = true;
|
|
1095
|
+
reject(e);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// We always start upstream connections *immediately*. This might be less efficient, but it
|
|
1099
|
+
// ensures that we're accurately mirroring downstream, which has indeed already connected.
|
|
1100
|
+
serverReq.flushHeaders();
|
|
1101
|
+
|
|
1102
|
+
// For similar reasons, we don't want any buffering on outgoing data at all if possible:
|
|
1103
|
+
serverReq.setNoDelay(true);
|
|
1104
|
+
|
|
1105
|
+
// Fire rule events, to allow in-depth debugging of upstream traffic & modifications,
|
|
1106
|
+
// so anybody interested can see _exactly_ what we're sending upstream here:
|
|
1107
|
+
if (options.emitEventCallback) {
|
|
1108
|
+
options.emitEventCallback('passthrough-request-head', {
|
|
1109
|
+
method,
|
|
1110
|
+
protocol: protocol!.replace(/:$/, ''),
|
|
1111
|
+
hostname,
|
|
1112
|
+
port,
|
|
1113
|
+
path,
|
|
1114
|
+
rawHeaders
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
if (!!reqBodyOverride) {
|
|
1118
|
+
options.emitEventCallback('passthrough-request-body', {
|
|
1119
|
+
overridden: true,
|
|
1120
|
+
rawBody: reqBodyOverride
|
|
1121
|
+
});
|
|
1122
|
+
} else {
|
|
1123
|
+
options.emitEventCallback!('passthrough-request-body', {
|
|
1124
|
+
overridden: false
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
})().catch(reject)
|
|
1129
|
+
).catch((e: ErrorLike) => {
|
|
1130
|
+
if (!e.code && e.stack?.split('\n')[1]?.includes('node:internal/tls/secure-context')) {
|
|
1131
|
+
// OpenSSL can throw all sorts of weird & wonderful errors here, and rarely exposes a
|
|
1132
|
+
// useful error code from them. To handle that, we try to detect the most common cases,
|
|
1133
|
+
// notable including the useless but common 'unsupported' error that covers all
|
|
1134
|
+
// OpenSSL-unsupported (e.g. legacy) configurations.
|
|
1135
|
+
|
|
1136
|
+
let tlsErrorTag: string;
|
|
1137
|
+
if (e.message === 'unsupported') {
|
|
1138
|
+
e.code = 'ERR_TLS_CONTEXT_UNSUPPORTED';
|
|
1139
|
+
tlsErrorTag = 'context-unsupported';
|
|
1140
|
+
e.message = 'Unsupported TLS configuration';
|
|
1141
|
+
} else {
|
|
1142
|
+
e.code = 'ERR_TLS_CONTEXT_UNKNOWN';
|
|
1143
|
+
tlsErrorTag = 'context-unknown';
|
|
1144
|
+
e.message = `TLS context error: ${e.message}`;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
clientRes.tags.push(`passthrough-tls-error:${tlsErrorTag}`);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// All errors anywhere above (thrown or from explicit reject()) should end up here.
|
|
1151
|
+
|
|
1152
|
+
// We tag the response with the error code, for debugging from events:
|
|
1153
|
+
clientRes.tags.push('passthrough-error:' + e.code);
|
|
1154
|
+
|
|
1155
|
+
// Tag responses, so programmatic examination can react to this
|
|
1156
|
+
// event, without having to parse response data or similar.
|
|
1157
|
+
const tlsAlertMatch = /SSL alert number (\d+)/.exec(e.message ?? '');
|
|
1158
|
+
if (tlsAlertMatch) {
|
|
1159
|
+
clientRes.tags.push('passthrough-tls-error:ssl-alert-' + tlsAlertMatch[1]);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if ((e as any).causedByUpstreamError && !serverReq?.aborted) {
|
|
1163
|
+
if (e.code === 'ECONNRESET' || e.code === 'ECONNREFUSED' || this.simulateConnectionErrors) {
|
|
1164
|
+
// The upstream socket failed: forcibly break the downstream stream to match. This could
|
|
1165
|
+
// happen due to a reset, TLS or DNS failures, or anything - but critically it's a
|
|
1166
|
+
// connection-level issue, so we try to create connection issues downstream.
|
|
1167
|
+
resetOrDestroy(clientReq);
|
|
1168
|
+
|
|
1169
|
+
// Aggregate errors can be thrown if multiple (IPv4/6) addresses were tested. Note that
|
|
1170
|
+
// AggregateError only exists in Node 15+. If that happens, we need to combine errors:
|
|
1171
|
+
const errorMessage = typeof AggregateError !== 'undefined' && (e instanceof AggregateError)
|
|
1172
|
+
? e.errors.map(e => e.message).join(', ')
|
|
1173
|
+
: (e.message ?? e.code ?? e);
|
|
1174
|
+
|
|
1175
|
+
throw new AbortError(`Upstream connection error: ${errorMessage}`, e.code);
|
|
1176
|
+
} else {
|
|
1177
|
+
e.statusCode = 502;
|
|
1178
|
+
e.statusMessage = 'Error communicating with upstream server';
|
|
1179
|
+
throw e;
|
|
1180
|
+
}
|
|
1181
|
+
} else {
|
|
1182
|
+
throw e;
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* @internal
|
|
1189
|
+
*/
|
|
1190
|
+
static deserialize(
|
|
1191
|
+
data: SerializedPassThroughData,
|
|
1192
|
+
channel: ClientServerChannel,
|
|
1193
|
+
ruleParams: RuleParameters
|
|
1194
|
+
): PassThroughHandler {
|
|
1195
|
+
let beforeRequest: ((req: CompletedRequest) => MaybePromise<CallbackRequestResult | void>) | undefined;
|
|
1196
|
+
if (data.hasBeforeRequestCallback) {
|
|
1197
|
+
beforeRequest = async (req: CompletedRequest) => {
|
|
1198
|
+
const result = withDeserializedCallbackBuffers<CallbackRequestResult>(
|
|
1199
|
+
await channel.request<
|
|
1200
|
+
BeforePassthroughRequestRequest,
|
|
1201
|
+
WithSerializedCallbackBuffers<CallbackRequestResult>
|
|
1202
|
+
>('beforeRequest', {
|
|
1203
|
+
args: [withSerializedBodyReader(req)]
|
|
1204
|
+
})
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
if (result.response && typeof result.response !== 'string') {
|
|
1208
|
+
result.response = withDeserializedCallbackBuffers(
|
|
1209
|
+
result.response as WithSerializedCallbackBuffers<CallbackResponseMessageResult>
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return result;
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
let beforeResponse: ((res: PassThroughResponse) => MaybePromise<CallbackResponseResult | void>) | undefined;
|
|
1218
|
+
if (data.hasBeforeResponseCallback) {
|
|
1219
|
+
beforeResponse = async (res: PassThroughResponse) => {
|
|
1220
|
+
const callbackResult = await channel.request<
|
|
1221
|
+
BeforePassthroughResponseRequest,
|
|
1222
|
+
| WithSerializedCallbackBuffers<CallbackResponseMessageResult>
|
|
1223
|
+
| 'close'
|
|
1224
|
+
| 'reset'
|
|
1225
|
+
| undefined
|
|
1226
|
+
>('beforeResponse', {
|
|
1227
|
+
args: [withSerializedBodyReader(res)]
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
if (callbackResult && typeof callbackResult !== 'string') {
|
|
1231
|
+
return withDeserializedCallbackBuffers(callbackResult);
|
|
1232
|
+
} else {
|
|
1233
|
+
return callbackResult;
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return new PassThroughHandler({
|
|
1239
|
+
beforeRequest,
|
|
1240
|
+
beforeResponse,
|
|
1241
|
+
proxyConfig: deserializeProxyConfig(data.proxyConfig, channel, ruleParams),
|
|
1242
|
+
transformRequest: data.transformRequest ? {
|
|
1243
|
+
...data.transformRequest,
|
|
1244
|
+
...(data.transformRequest?.replaceBody !== undefined ? {
|
|
1245
|
+
replaceBody: deserializeBuffer(data.transformRequest.replaceBody)
|
|
1246
|
+
} : {}),
|
|
1247
|
+
...(data.transformRequest?.updateHeaders !== undefined ? {
|
|
1248
|
+
updateHeaders: mapOmitToUndefined(JSON.parse(data.transformRequest.updateHeaders))
|
|
1249
|
+
} : {}),
|
|
1250
|
+
...(data.transformRequest?.updateJsonBody !== undefined ? {
|
|
1251
|
+
updateJsonBody: mapOmitToUndefined(JSON.parse(data.transformRequest.updateJsonBody))
|
|
1252
|
+
} : {}),
|
|
1253
|
+
...(data.transformRequest?.matchReplaceBody !== undefined ? {
|
|
1254
|
+
matchReplaceBody: data.transformRequest.matchReplaceBody.map(([match, result]) =>
|
|
1255
|
+
[
|
|
1256
|
+
!_.isString(match) && 'regexSource' in match
|
|
1257
|
+
? new RegExp(match.regexSource, match.flags)
|
|
1258
|
+
: match,
|
|
1259
|
+
result
|
|
1260
|
+
]
|
|
1261
|
+
)
|
|
1262
|
+
} : {})
|
|
1263
|
+
} as RequestTransform : undefined,
|
|
1264
|
+
transformResponse: data.transformResponse ? {
|
|
1265
|
+
...data.transformResponse,
|
|
1266
|
+
...(data.transformResponse?.replaceBody !== undefined ? {
|
|
1267
|
+
replaceBody: deserializeBuffer(data.transformResponse.replaceBody)
|
|
1268
|
+
} : {}),
|
|
1269
|
+
...(data.transformResponse?.updateHeaders !== undefined ? {
|
|
1270
|
+
updateHeaders: mapOmitToUndefined(JSON.parse(data.transformResponse.updateHeaders))
|
|
1271
|
+
} : {}),
|
|
1272
|
+
...(data.transformResponse?.updateJsonBody !== undefined ? {
|
|
1273
|
+
updateJsonBody: mapOmitToUndefined(JSON.parse(data.transformResponse.updateJsonBody))
|
|
1274
|
+
} : {}),
|
|
1275
|
+
...(data.transformResponse?.matchReplaceBody !== undefined ? {
|
|
1276
|
+
matchReplaceBody: data.transformResponse.matchReplaceBody.map(([match, result]) =>
|
|
1277
|
+
[
|
|
1278
|
+
!_.isString(match) && 'regexSource' in match
|
|
1279
|
+
? new RegExp(match.regexSource, match.flags)
|
|
1280
|
+
: match,
|
|
1281
|
+
result
|
|
1282
|
+
]
|
|
1283
|
+
)
|
|
1284
|
+
} : {})
|
|
1285
|
+
} as ResponseTransform : undefined,
|
|
1286
|
+
// Backward compat for old clients:
|
|
1287
|
+
...data.forwardToLocation ? {
|
|
1288
|
+
forwarding: { targetHost: data.forwardToLocation }
|
|
1289
|
+
} : {},
|
|
1290
|
+
forwarding: data.forwarding,
|
|
1291
|
+
lookupOptions: data.lookupOptions,
|
|
1292
|
+
simulateConnectionErrors: !!data.simulateConnectionErrors,
|
|
1293
|
+
ignoreHostHttpsErrors: data.ignoreHostCertificateErrors,
|
|
1294
|
+
additionalTrustedCAs: data.extraCACertificates,
|
|
1295
|
+
clientCertificateHostMap: _.mapValues(data.clientCertificateHostMap,
|
|
1296
|
+
({ pfx, passphrase }) => ({ pfx: deserializeBuffer(pfx), passphrase })
|
|
1297
|
+
),
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
export class CloseConnectionHandler extends CloseConnectionHandlerDefinition {
|
|
1303
|
+
async handle(request: OngoingRequest) {
|
|
1304
|
+
const socket: net.Socket = (<any> request).socket;
|
|
1305
|
+
socket.end();
|
|
1306
|
+
throw new AbortError('Connection closed intentionally by rule');
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
export class ResetConnectionHandler extends ResetConnectionHandlerDefinition {
|
|
1311
|
+
constructor() {
|
|
1312
|
+
super();
|
|
1313
|
+
requireSocketResetSupport();
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
async handle(request: OngoingRequest) {
|
|
1317
|
+
requireSocketResetSupport();
|
|
1318
|
+
resetOrDestroy(request);
|
|
1319
|
+
throw new AbortError('Connection reset intentionally by rule');
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* @internal
|
|
1324
|
+
*/
|
|
1325
|
+
static deserialize() {
|
|
1326
|
+
requireSocketResetSupport();
|
|
1327
|
+
return new ResetConnectionHandler();
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
export class TimeoutHandler extends TimeoutHandlerDefinition {
|
|
1332
|
+
async handle() {
|
|
1333
|
+
// Do nothing, leaving the socket open but never sending a response.
|
|
1334
|
+
return new Promise<void>(() => {});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
export class JsonRpcResponseHandler extends JsonRpcResponseHandlerDefinition {
|
|
1339
|
+
async handle(request: OngoingRequest, response: OngoingResponse) {
|
|
1340
|
+
const data: any = await request.body.asJson()
|
|
1341
|
+
.catch(() => {}); // Handle parsing errors with the check below
|
|
1342
|
+
|
|
1343
|
+
if (!data || data.jsonrpc !== '2.0' || !('id' in data)) { // N.B. id can be null
|
|
1344
|
+
throw new Error("Can't send a JSON-RPC response to an invalid JSON-RPC request");
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
response.writeHead(200, {
|
|
1348
|
+
'content-type': 'application/json'
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
response.end(JSON.stringify({
|
|
1352
|
+
jsonrpc: '2.0',
|
|
1353
|
+
id: data.id,
|
|
1354
|
+
...this.result
|
|
1355
|
+
}));
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
export const HandlerLookup: typeof HandlerDefinitionLookup = {
|
|
1360
|
+
'simple': SimpleHandler,
|
|
1361
|
+
'callback': CallbackHandler,
|
|
1362
|
+
'stream': StreamHandler,
|
|
1363
|
+
'file': FileHandler,
|
|
1364
|
+
'passthrough': PassThroughHandler,
|
|
1365
|
+
'close-connection': CloseConnectionHandler,
|
|
1366
|
+
'reset-connection': ResetConnectionHandler,
|
|
1367
|
+
'timeout': TimeoutHandler,
|
|
1368
|
+
'json-rpc-response': JsonRpcResponseHandler
|
|
1369
|
+
}
|