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,1110 @@
|
|
|
1
|
+
import _ = require("lodash");
|
|
2
|
+
import net = require("net");
|
|
3
|
+
import url = require("url");
|
|
4
|
+
import tls = require("tls");
|
|
5
|
+
import http = require("http");
|
|
6
|
+
import http2 = require("http2");
|
|
7
|
+
import { EventEmitter } from "events";
|
|
8
|
+
import portfinder = require("portfinder");
|
|
9
|
+
import connect = require("connect");
|
|
10
|
+
import { v4 as uuid } from "uuid";
|
|
11
|
+
import cors = require("cors");
|
|
12
|
+
import now = require("performance-now");
|
|
13
|
+
import WebSocket = require("ws");
|
|
14
|
+
import { Mutex } from 'async-mutex';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
InitiatedRequest,
|
|
18
|
+
OngoingRequest,
|
|
19
|
+
CompletedRequest,
|
|
20
|
+
OngoingResponse,
|
|
21
|
+
CompletedResponse,
|
|
22
|
+
TlsHandshakeFailure,
|
|
23
|
+
ClientError,
|
|
24
|
+
TimingEvents,
|
|
25
|
+
OngoingBody,
|
|
26
|
+
WebSocketMessage,
|
|
27
|
+
WebSocketClose,
|
|
28
|
+
TlsPassthroughEvent,
|
|
29
|
+
RuleEvent,
|
|
30
|
+
RawTrailers
|
|
31
|
+
} from "../types";
|
|
32
|
+
import { DestroyableServer } from "destroyable-server";
|
|
33
|
+
import {
|
|
34
|
+
Mockttp,
|
|
35
|
+
AbstractMockttp,
|
|
36
|
+
MockttpOptions,
|
|
37
|
+
MockttpHttpsOptions,
|
|
38
|
+
PortRange
|
|
39
|
+
} from "../mockttp";
|
|
40
|
+
import { RequestRule, RequestRuleData } from "../rules/requests/request-rule";
|
|
41
|
+
import { ServerMockedEndpoint } from "./mocked-endpoint";
|
|
42
|
+
import { createComboServer } from "./http-combo-server";
|
|
43
|
+
import { filter } from "../util/promise";
|
|
44
|
+
import { Mutable } from "../util/type-utils";
|
|
45
|
+
import { ErrorLike, isErrorLike } from "../util/error";
|
|
46
|
+
import { makePropertyWritable } from "../util/util";
|
|
47
|
+
|
|
48
|
+
import { isAbsoluteUrl, getPathFromAbsoluteUrl } from "../util/url";
|
|
49
|
+
import { buildSocketEventData, isSocketLoop, resetOrDestroy } from "../util/socket-util";
|
|
50
|
+
import {
|
|
51
|
+
parseRequestBody,
|
|
52
|
+
waitForCompletedRequest,
|
|
53
|
+
trackResponse,
|
|
54
|
+
waitForCompletedResponse,
|
|
55
|
+
buildInitiatedRequest,
|
|
56
|
+
tryToParseHttpRequest,
|
|
57
|
+
buildBodyReader,
|
|
58
|
+
parseRawHttpResponse
|
|
59
|
+
} from "../util/request-utils";
|
|
60
|
+
import { asBuffer } from "../util/buffer-utils";
|
|
61
|
+
import {
|
|
62
|
+
pairFlatRawHeaders,
|
|
63
|
+
rawHeadersToObject
|
|
64
|
+
} from "../util/header-utils";
|
|
65
|
+
import { AbortError } from "../rules/requests/request-handlers";
|
|
66
|
+
import { WebSocketRuleData, WebSocketRule } from "../rules/websockets/websocket-rule";
|
|
67
|
+
import { RejectWebSocketHandler, WebSocketHandler } from "../rules/websockets/websocket-handlers";
|
|
68
|
+
|
|
69
|
+
type ExtendedRawRequest = (http.IncomingMessage | http2.Http2ServerRequest) & {
|
|
70
|
+
protocol?: string;
|
|
71
|
+
body?: OngoingBody;
|
|
72
|
+
path?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const serverPortCheckMutex = new Mutex();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A in-process Mockttp implementation. This starts servers on the local machine in the
|
|
79
|
+
* current process, and exposes methods to directly manage them.
|
|
80
|
+
*
|
|
81
|
+
* This class does not work in browsers, as it expects to be able to start HTTP servers.
|
|
82
|
+
*/
|
|
83
|
+
export class MockttpServer extends AbstractMockttp implements Mockttp {
|
|
84
|
+
|
|
85
|
+
private requestRuleSets: { [priority: number]: RequestRule[] } = {};
|
|
86
|
+
private webSocketRuleSets: { [priority: number]: WebSocketRule[] } = {};
|
|
87
|
+
|
|
88
|
+
private httpsOptions: MockttpHttpsOptions | undefined;
|
|
89
|
+
private isHttp2Enabled: true | false | 'fallback';
|
|
90
|
+
private maxBodySize: number;
|
|
91
|
+
|
|
92
|
+
private app: connect.Server;
|
|
93
|
+
private server: DestroyableServer<net.Server> | undefined;
|
|
94
|
+
|
|
95
|
+
private eventEmitter: EventEmitter;
|
|
96
|
+
|
|
97
|
+
private readonly initialDebugSetting: boolean;
|
|
98
|
+
|
|
99
|
+
private readonly defaultWsHandler!: WebSocketHandler;
|
|
100
|
+
|
|
101
|
+
constructor(options: MockttpOptions = {}) {
|
|
102
|
+
super(options);
|
|
103
|
+
|
|
104
|
+
this.initialDebugSetting = this.debug;
|
|
105
|
+
|
|
106
|
+
this.httpsOptions = options.https;
|
|
107
|
+
this.isHttp2Enabled = options.http2 ?? 'fallback';
|
|
108
|
+
this.maxBodySize = options.maxBodySize ?? Infinity;
|
|
109
|
+
this.eventEmitter = new EventEmitter();
|
|
110
|
+
|
|
111
|
+
this.defaultWsHandler = new RejectWebSocketHandler(503, "Request for unmocked endpoint");
|
|
112
|
+
|
|
113
|
+
this.app = connect();
|
|
114
|
+
|
|
115
|
+
if (this.corsOptions) {
|
|
116
|
+
if (this.debug) console.log('Enabling CORS');
|
|
117
|
+
|
|
118
|
+
const corsOptions = this.corsOptions === true
|
|
119
|
+
? { methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'] }
|
|
120
|
+
: this.corsOptions;
|
|
121
|
+
|
|
122
|
+
this.app.use(cors(corsOptions) as connect.HandleFunction);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.app.use(this.handleRequest.bind(this));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async start(portParam: number | PortRange = { startPort: 8000, endPort: 65535 }): Promise<void> {
|
|
129
|
+
this.server = await createComboServer({
|
|
130
|
+
debug: this.debug,
|
|
131
|
+
https: this.httpsOptions,
|
|
132
|
+
http2: this.isHttp2Enabled,
|
|
133
|
+
}, this.app, this.announceTlsErrorAsync.bind(this), this.passthroughSocket.bind(this));
|
|
134
|
+
|
|
135
|
+
// We use a mutex here to avoid contention on ports with parallel setup
|
|
136
|
+
await serverPortCheckMutex.runExclusive(async () => {
|
|
137
|
+
const port = _.isNumber(portParam)
|
|
138
|
+
? portParam
|
|
139
|
+
: await portfinder.getPortPromise({
|
|
140
|
+
port: portParam.startPort,
|
|
141
|
+
stopPort: portParam.endPort
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (this.debug) console.log(`Starting mock server on port ${port}`);
|
|
145
|
+
this.server!.listen(port);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Handle & report client request errors
|
|
149
|
+
this.server!.on('clientError', this.handleInvalidHttp1Request.bind(this));
|
|
150
|
+
this.server!.on('sessionError', this.handleInvalidHttp2Request.bind(this));
|
|
151
|
+
|
|
152
|
+
// Track the socket of HTTP/2 sessions, for error reporting later:
|
|
153
|
+
this.server!.on('session', (session) => {
|
|
154
|
+
session.on('connect', (session: http2.Http2Session, socket: net.Socket) => {
|
|
155
|
+
session.initialSocket = socket;
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.server!.on('upgrade', this.handleWebSocket.bind(this));
|
|
160
|
+
|
|
161
|
+
return new Promise<void>((resolve, reject) => {
|
|
162
|
+
this.server!.on('listening', resolve);
|
|
163
|
+
this.server!.on('error', (e: any) => {
|
|
164
|
+
// Although we try to pick a free port, we may have race conditions, if something else
|
|
165
|
+
// takes the same port at the same time. If you haven't explicitly picked a port, and
|
|
166
|
+
// we do have a collision, simply try again.
|
|
167
|
+
if (e.code === 'EADDRINUSE' && !_.isNumber(portParam)) {
|
|
168
|
+
if (this.debug) console.log('Address in use, retrying...');
|
|
169
|
+
|
|
170
|
+
// Destroy just in case there is something that needs cleanup here. Catch because most
|
|
171
|
+
// of the time this will error with 'Server is not running'.
|
|
172
|
+
this.server!.destroy().catch(() => {});
|
|
173
|
+
resolve(this.start());
|
|
174
|
+
} else {
|
|
175
|
+
reject(e);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async stop(): Promise<void> {
|
|
182
|
+
if (this.debug) console.log(`Stopping server at ${this.url}`);
|
|
183
|
+
|
|
184
|
+
if (this.server) await this.server.destroy();
|
|
185
|
+
|
|
186
|
+
this.reset();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
enableDebug() {
|
|
190
|
+
this.debug = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
reset() {
|
|
194
|
+
Object.values(this.requestRuleSets).flat().forEach(r => r.dispose());
|
|
195
|
+
this.requestRuleSets = [];
|
|
196
|
+
|
|
197
|
+
Object.values(this.webSocketRuleSets).flat().forEach(r => r.dispose());
|
|
198
|
+
this.webSocketRuleSets = [];
|
|
199
|
+
|
|
200
|
+
this.debug = this.initialDebugSetting;
|
|
201
|
+
|
|
202
|
+
this.eventEmitter.removeAllListeners();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private get address() {
|
|
206
|
+
if (!this.server) throw new Error('Cannot get address before server is started');
|
|
207
|
+
|
|
208
|
+
return (this.server.address() as net.AddressInfo)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
get url(): string {
|
|
212
|
+
if (!this.server) throw new Error('Cannot get url before server is started');
|
|
213
|
+
|
|
214
|
+
if (this.httpsOptions) {
|
|
215
|
+
return "https://localhost:" + this.port;
|
|
216
|
+
} else {
|
|
217
|
+
return "http://localhost:" + this.port;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
get port(): number {
|
|
222
|
+
if (!this.server) throw new Error('Cannot get port before server is started');
|
|
223
|
+
|
|
224
|
+
return this.address.port;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private addToRuleSets<R extends RequestRule | WebSocketRule>(
|
|
228
|
+
ruleSets: { [priority: number]: R[] },
|
|
229
|
+
rule: R
|
|
230
|
+
) {
|
|
231
|
+
ruleSets[rule.priority] ??= [];
|
|
232
|
+
ruleSets[rule.priority].push(rule);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public setRequestRules = (...ruleData: RequestRuleData[]): Promise<ServerMockedEndpoint[]> => {
|
|
236
|
+
Object.values(this.requestRuleSets).flat().forEach(r => r.dispose());
|
|
237
|
+
|
|
238
|
+
const rules = ruleData.map((ruleDatum) => new RequestRule(ruleDatum));
|
|
239
|
+
this.requestRuleSets = _.groupBy(rules, r => r.priority);
|
|
240
|
+
|
|
241
|
+
return Promise.resolve(rules.map(r => new ServerMockedEndpoint(r)));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public addRequestRules = (...ruleData: RequestRuleData[]): Promise<ServerMockedEndpoint[]> => {
|
|
245
|
+
return Promise.resolve(ruleData.map((ruleDatum) => {
|
|
246
|
+
const rule = new RequestRule(ruleDatum);
|
|
247
|
+
this.addToRuleSets(this.requestRuleSets, rule);
|
|
248
|
+
return new ServerMockedEndpoint(rule);
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public setWebSocketRules = (...ruleData: WebSocketRuleData[]): Promise<ServerMockedEndpoint[]> => {
|
|
253
|
+
Object.values(this.webSocketRuleSets).flat().forEach(r => r.dispose());
|
|
254
|
+
|
|
255
|
+
const rules = ruleData.map((ruleDatum) => new WebSocketRule(ruleDatum));
|
|
256
|
+
this.webSocketRuleSets = _.groupBy(rules, r => r.priority);
|
|
257
|
+
|
|
258
|
+
return Promise.resolve(rules.map(r => new ServerMockedEndpoint(r)));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public addWebSocketRules = (...ruleData: WebSocketRuleData[]): Promise<ServerMockedEndpoint[]> => {
|
|
262
|
+
return Promise.resolve(ruleData.map((ruleDatum) => {
|
|
263
|
+
const rule = new WebSocketRule(ruleDatum);
|
|
264
|
+
(this.webSocketRuleSets[rule.priority] ??= []).push(rule);
|
|
265
|
+
return new ServerMockedEndpoint(rule);
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public async getMockedEndpoints(): Promise<ServerMockedEndpoint[]> {
|
|
270
|
+
return [
|
|
271
|
+
...Object.values(this.requestRuleSets).flatMap(rules => rules.map(r => new ServerMockedEndpoint(r))),
|
|
272
|
+
...Object.values(this.webSocketRuleSets).flatMap(rules => rules.map(r => new ServerMockedEndpoint(r)))
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public async getPendingEndpoints() {
|
|
277
|
+
const withPendingPromises = (await this.getMockedEndpoints())
|
|
278
|
+
.map(async (endpoint) => ({
|
|
279
|
+
endpoint,
|
|
280
|
+
isPending: await endpoint.isPending()
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
const withPending = await Promise.all(withPendingPromises);
|
|
284
|
+
return withPending.filter(wp => wp.isPending).map(wp => wp.endpoint);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public async getRuleParameterKeys() {
|
|
288
|
+
return []; // Local servers never have rule parameters defined
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public on(event: 'request-initiated', callback: (req: InitiatedRequest) => void): Promise<void>;
|
|
292
|
+
public on(event: 'request', callback: (req: CompletedRequest) => void): Promise<void>;
|
|
293
|
+
public on(event: 'response', callback: (req: CompletedResponse) => void): Promise<void>;
|
|
294
|
+
public on(event: 'abort', callback: (req: InitiatedRequest) => void): Promise<void>;
|
|
295
|
+
public on(event: 'websocket-request', callback: (req: CompletedRequest) => void): Promise<void>;
|
|
296
|
+
public on(event: 'websocket-accepted', callback: (req: CompletedResponse) => void): Promise<void>;
|
|
297
|
+
public on(event: 'websocket-message-received', callback: (req: WebSocketMessage) => void): Promise<void>;
|
|
298
|
+
public on(event: 'websocket-message-sent', callback: (req: WebSocketMessage) => void): Promise<void>;
|
|
299
|
+
public on(event: 'websocket-close', callback: (close: WebSocketClose) => void): Promise<void>;
|
|
300
|
+
public on(event: 'tls-passthrough-opened', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
|
|
301
|
+
public on(event: 'tls-passthrough-closed', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
|
|
302
|
+
public on(event: 'tls-client-error', callback: (req: TlsHandshakeFailure) => void): Promise<void>;
|
|
303
|
+
public on(event: 'client-error', callback: (error: ClientError) => void): Promise<void>;
|
|
304
|
+
public on<T = unknown>(event: 'rule-event', callback: (event: RuleEvent<T>) => void): Promise<void>;
|
|
305
|
+
public on(event: string, callback: (...args: any[]) => void): Promise<void> {
|
|
306
|
+
this.eventEmitter.on(event, callback);
|
|
307
|
+
return Promise.resolve();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private announceInitialRequestAsync(request: OngoingRequest) {
|
|
311
|
+
if (this.eventEmitter.listenerCount('request-initiated') === 0) return;
|
|
312
|
+
|
|
313
|
+
setImmediate(() => {
|
|
314
|
+
const initiatedReq = buildInitiatedRequest(request);
|
|
315
|
+
this.eventEmitter.emit('request-initiated', Object.assign(
|
|
316
|
+
initiatedReq,
|
|
317
|
+
{
|
|
318
|
+
timingEvents: _.clone(initiatedReq.timingEvents),
|
|
319
|
+
tags: _.clone(initiatedReq.tags)
|
|
320
|
+
}
|
|
321
|
+
));
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private announceCompletedRequestAsync(request: OngoingRequest) {
|
|
326
|
+
if (this.eventEmitter.listenerCount('request') === 0) return;
|
|
327
|
+
|
|
328
|
+
waitForCompletedRequest(request)
|
|
329
|
+
.then((completedReq: CompletedRequest) => {
|
|
330
|
+
setImmediate(() => {
|
|
331
|
+
this.eventEmitter.emit('request', Object.assign(
|
|
332
|
+
completedReq,
|
|
333
|
+
{
|
|
334
|
+
timingEvents: _.clone(completedReq.timingEvents),
|
|
335
|
+
tags: _.clone(completedReq.tags)
|
|
336
|
+
}
|
|
337
|
+
));
|
|
338
|
+
});
|
|
339
|
+
})
|
|
340
|
+
.catch(console.error);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private announceResponseAsync(response: OngoingResponse | CompletedResponse) {
|
|
344
|
+
if (this.eventEmitter.listenerCount('response') === 0) return;
|
|
345
|
+
|
|
346
|
+
waitForCompletedResponse(response)
|
|
347
|
+
.then((res: CompletedResponse) => {
|
|
348
|
+
setImmediate(() => {
|
|
349
|
+
this.eventEmitter.emit('response', Object.assign(res, {
|
|
350
|
+
timingEvents: _.clone(res.timingEvents),
|
|
351
|
+
tags: _.clone(res.tags)
|
|
352
|
+
}));
|
|
353
|
+
});
|
|
354
|
+
})
|
|
355
|
+
.catch(console.error);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private announceWebSocketRequestAsync(request: OngoingRequest) {
|
|
359
|
+
if (this.eventEmitter.listenerCount('websocket-request') === 0) return;
|
|
360
|
+
|
|
361
|
+
waitForCompletedRequest(request)
|
|
362
|
+
.then((completedReq: CompletedRequest) => {
|
|
363
|
+
setImmediate(() => {
|
|
364
|
+
this.eventEmitter.emit('websocket-request', Object.assign(completedReq, {
|
|
365
|
+
timingEvents: _.clone(completedReq.timingEvents),
|
|
366
|
+
tags: _.clone(completedReq.tags)
|
|
367
|
+
}));
|
|
368
|
+
});
|
|
369
|
+
})
|
|
370
|
+
.catch(console.error);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private announceWebSocketUpgradeAsync(response: CompletedResponse) {
|
|
374
|
+
if (this.eventEmitter.listenerCount('websocket-accepted') === 0) return;
|
|
375
|
+
|
|
376
|
+
setImmediate(() => {
|
|
377
|
+
this.eventEmitter.emit('websocket-accepted', {
|
|
378
|
+
...response,
|
|
379
|
+
timingEvents: _.clone(response.timingEvents),
|
|
380
|
+
tags: _.clone(response.tags)
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private announceWebSocketMessageAsync(
|
|
386
|
+
request: OngoingRequest,
|
|
387
|
+
direction: 'sent' | 'received',
|
|
388
|
+
content: Buffer,
|
|
389
|
+
isBinary: boolean
|
|
390
|
+
) {
|
|
391
|
+
const eventName = `websocket-message-${direction}`;
|
|
392
|
+
if (this.eventEmitter.listenerCount(eventName) === 0) return;
|
|
393
|
+
|
|
394
|
+
setImmediate(() => {
|
|
395
|
+
this.eventEmitter.emit(eventName, {
|
|
396
|
+
streamId: request.id,
|
|
397
|
+
|
|
398
|
+
direction,
|
|
399
|
+
content,
|
|
400
|
+
isBinary,
|
|
401
|
+
|
|
402
|
+
eventTimestamp: now(),
|
|
403
|
+
timingEvents: request.timingEvents,
|
|
404
|
+
tags: request.tags
|
|
405
|
+
} as WebSocketMessage);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private announceWebSocketCloseAsync(
|
|
410
|
+
request: OngoingRequest,
|
|
411
|
+
closeCode: number | undefined,
|
|
412
|
+
closeReason?: string
|
|
413
|
+
) {
|
|
414
|
+
if (this.eventEmitter.listenerCount('websocket-close') === 0) return;
|
|
415
|
+
|
|
416
|
+
setImmediate(() => {
|
|
417
|
+
this.eventEmitter.emit('websocket-close', {
|
|
418
|
+
streamId: request.id,
|
|
419
|
+
|
|
420
|
+
closeCode,
|
|
421
|
+
closeReason,
|
|
422
|
+
|
|
423
|
+
timingEvents: request.timingEvents,
|
|
424
|
+
tags: request.tags
|
|
425
|
+
} as WebSocketClose);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Hook the request and socket to announce all WebSocket events after the initial request:
|
|
430
|
+
private trackWebSocketEvents(request: OngoingRequest, socket: net.Socket) {
|
|
431
|
+
const originalWrite = socket._write;
|
|
432
|
+
const originalWriteV = socket._writev;
|
|
433
|
+
|
|
434
|
+
// Hook the socket to capture our upgrade response:
|
|
435
|
+
let data = Buffer.from([]);
|
|
436
|
+
socket._writev = undefined;
|
|
437
|
+
socket._write = function (): any {
|
|
438
|
+
data = Buffer.concat([data, asBuffer(arguments[0])]);
|
|
439
|
+
return originalWrite.apply(this, arguments as any);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
let upgradeCompleted = false;
|
|
443
|
+
|
|
444
|
+
socket.once('close', () => {
|
|
445
|
+
if (upgradeCompleted) return;
|
|
446
|
+
|
|
447
|
+
if (data.length) {
|
|
448
|
+
request.timingEvents.responseSentTimestamp = now();
|
|
449
|
+
|
|
450
|
+
const httpResponse = parseRawHttpResponse(data, request);
|
|
451
|
+
this.announceResponseAsync(httpResponse);
|
|
452
|
+
} else {
|
|
453
|
+
// Connect closed during upgrade, before we responded:
|
|
454
|
+
request.timingEvents.abortedTimestamp = now();
|
|
455
|
+
this.announceAbortAsync(request);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
socket.once('ws-upgrade', (ws: WebSocket) => {
|
|
460
|
+
upgradeCompleted = true;
|
|
461
|
+
|
|
462
|
+
// Undo our write hook setup:
|
|
463
|
+
socket._write = originalWrite;
|
|
464
|
+
socket._writev = originalWriteV;
|
|
465
|
+
|
|
466
|
+
request.timingEvents.wsAcceptedTimestamp = now();
|
|
467
|
+
|
|
468
|
+
const httpResponse = parseRawHttpResponse(data, request);
|
|
469
|
+
this.announceWebSocketUpgradeAsync(httpResponse);
|
|
470
|
+
|
|
471
|
+
ws.on('message', (data: Buffer, isBinary) => {
|
|
472
|
+
this.announceWebSocketMessageAsync(request, 'received', data, isBinary);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Wrap ws.send() to report all sent data:
|
|
476
|
+
const _send = ws.send;
|
|
477
|
+
const self = this;
|
|
478
|
+
ws.send = function (data: any, options: any): any {
|
|
479
|
+
const isBinary = options.binary
|
|
480
|
+
?? typeof data !== 'string';
|
|
481
|
+
|
|
482
|
+
_send.apply(this, arguments as any);
|
|
483
|
+
self.announceWebSocketMessageAsync(request, 'sent', asBuffer(data), isBinary);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
ws.on('close', (closeCode, closeReason) => {
|
|
487
|
+
if (closeCode === 1006) {
|
|
488
|
+
// Not a clean close!
|
|
489
|
+
request.timingEvents.abortedTimestamp = now();
|
|
490
|
+
this.announceAbortAsync(request);
|
|
491
|
+
} else {
|
|
492
|
+
request.timingEvents.wsClosedTimestamp = now();
|
|
493
|
+
|
|
494
|
+
this.announceWebSocketCloseAsync(
|
|
495
|
+
request,
|
|
496
|
+
closeCode === 1005
|
|
497
|
+
? undefined // Clean close, but with a close frame with no status
|
|
498
|
+
: closeCode,
|
|
499
|
+
closeReason.toString('utf8')
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private async announceAbortAsync(request: OngoingRequest, abortError?: ErrorLike) {
|
|
507
|
+
setImmediate(() => {
|
|
508
|
+
const req = buildInitiatedRequest(request);
|
|
509
|
+
this.eventEmitter.emit('abort', Object.assign(req, {
|
|
510
|
+
timingEvents: _.clone(req.timingEvents),
|
|
511
|
+
tags: _.clone(req.tags),
|
|
512
|
+
error: abortError ? {
|
|
513
|
+
name: abortError.name,
|
|
514
|
+
code: abortError.code,
|
|
515
|
+
message: abortError.message,
|
|
516
|
+
stack: abortError.stack
|
|
517
|
+
} : undefined
|
|
518
|
+
}));
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private async announceTlsErrorAsync(socket: net.Socket, request: TlsHandshakeFailure) {
|
|
523
|
+
// Ignore errors after TLS is setup, those are client errors
|
|
524
|
+
if (socket instanceof tls.TLSSocket && socket.tlsSetupCompleted) return;
|
|
525
|
+
|
|
526
|
+
setImmediate(() => {
|
|
527
|
+
// We can get falsey but set hostname values - drop them
|
|
528
|
+
if (!request.hostname) delete request.hostname;
|
|
529
|
+
if (this.debug) console.warn(`TLS client error: ${JSON.stringify(request)}`);
|
|
530
|
+
this.eventEmitter.emit('tls-client-error', request);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private async announceClientErrorAsync(socket: net.Socket | undefined, error: ClientError) {
|
|
535
|
+
// Ignore errors before TLS is setup, those are TLS errors
|
|
536
|
+
if (
|
|
537
|
+
socket instanceof tls.TLSSocket &&
|
|
538
|
+
!socket.tlsSetupCompleted &&
|
|
539
|
+
error.errorCode !== 'ERR_HTTP2_ERROR' // Initial HTTP/2 errors are considered post-TLS
|
|
540
|
+
) return;
|
|
541
|
+
|
|
542
|
+
setImmediate(() => {
|
|
543
|
+
if (this.debug) console.warn(`Client error: ${JSON.stringify(error)}`);
|
|
544
|
+
this.eventEmitter.emit('client-error', error);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private async announceRuleEventAsync(requestId: string, ruleId: string, eventType: string, eventData: unknown) {
|
|
549
|
+
setImmediate(() => {
|
|
550
|
+
this.eventEmitter.emit('rule-event', {
|
|
551
|
+
requestId,
|
|
552
|
+
ruleId,
|
|
553
|
+
eventType,
|
|
554
|
+
eventData
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private preprocessRequest(req: ExtendedRawRequest, type: 'request' | 'websocket'): OngoingRequest {
|
|
560
|
+
parseRequestBody(req, { maxSize: this.maxBodySize });
|
|
561
|
+
|
|
562
|
+
// Make req.url always absolute, if it isn't already, using the host header.
|
|
563
|
+
// It might not be if this is a direct request, or if it's being transparently proxied.
|
|
564
|
+
if (!isAbsoluteUrl(req.url!)) {
|
|
565
|
+
req.protocol = req.headers[':scheme'] as string ||
|
|
566
|
+
(req.socket.__lastHopEncrypted ? 'https' : 'http');
|
|
567
|
+
req.path = req.url;
|
|
568
|
+
|
|
569
|
+
const host = req.headers[':authority'] || req.headers['host'];
|
|
570
|
+
const absoluteUrl = `${req.protocol}://${host}${req.path}`;
|
|
571
|
+
|
|
572
|
+
if (!req.headers[':path']) {
|
|
573
|
+
(req as Mutable<ExtendedRawRequest>).url = new url.URL(absoluteUrl).toString();
|
|
574
|
+
} else {
|
|
575
|
+
// Node's HTTP/2 compat logic maps .url to headers[':path']. We want them to
|
|
576
|
+
// diverge: .url should always be absolute, while :path may stay relative,
|
|
577
|
+
// so we override the built-in getter & setter:
|
|
578
|
+
Object.defineProperty(req, 'url', {
|
|
579
|
+
value: new url.URL(absoluteUrl).toString()
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
req.protocol = req.url!.split('://', 1)[0];
|
|
584
|
+
req.path = getPathFromAbsoluteUrl(req.url!);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (type === 'websocket') {
|
|
588
|
+
req.protocol = req.protocol === 'https'
|
|
589
|
+
? 'wss'
|
|
590
|
+
: 'ws';
|
|
591
|
+
|
|
592
|
+
// Transform the protocol in req.url too:
|
|
593
|
+
Object.defineProperty(req, 'url', {
|
|
594
|
+
value: req.url!.replace(/^http/, 'ws')
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const id = uuid();
|
|
599
|
+
const tags: string[] = [];
|
|
600
|
+
|
|
601
|
+
const timingEvents: TimingEvents = {
|
|
602
|
+
startTime: Date.now(),
|
|
603
|
+
startTimestamp: now()
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
req.on('end', () => {
|
|
607
|
+
timingEvents.bodyReceivedTimestamp ||= now();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const rawHeaders = pairFlatRawHeaders(req.rawHeaders);
|
|
611
|
+
const headers = rawHeadersToObject(rawHeaders);
|
|
612
|
+
|
|
613
|
+
// Not writable for HTTP/2:
|
|
614
|
+
makePropertyWritable(req, 'headers');
|
|
615
|
+
makePropertyWritable(req, 'rawHeaders');
|
|
616
|
+
|
|
617
|
+
let rawTrailers: RawTrailers | undefined;
|
|
618
|
+
Object.defineProperty(req, 'rawTrailers', {
|
|
619
|
+
get: () => rawTrailers,
|
|
620
|
+
set: (flatRawTrailers) => {
|
|
621
|
+
rawTrailers = flatRawTrailers
|
|
622
|
+
? pairFlatRawHeaders(flatRawTrailers)
|
|
623
|
+
: undefined;
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
return Object.assign(req, {
|
|
628
|
+
id,
|
|
629
|
+
headers,
|
|
630
|
+
rawHeaders,
|
|
631
|
+
rawTrailers, // Just makes the type happy - really managed by property above
|
|
632
|
+
remoteIpAddress: req.socket.remoteAddress,
|
|
633
|
+
remotePort: req.socket.remotePort,
|
|
634
|
+
timingEvents,
|
|
635
|
+
tags
|
|
636
|
+
}) as OngoingRequest;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private async handleRequest(rawRequest: ExtendedRawRequest, rawResponse: http.ServerResponse) {
|
|
640
|
+
const request = this.preprocessRequest(rawRequest, 'request');
|
|
641
|
+
if (this.debug) console.log(`Handling request for ${rawRequest.url}`);
|
|
642
|
+
|
|
643
|
+
let result: 'responded' | 'aborted' | null = null;
|
|
644
|
+
const abort = (error?: Error) => {
|
|
645
|
+
if (result === null) {
|
|
646
|
+
result = 'aborted';
|
|
647
|
+
request.timingEvents.abortedTimestamp = now();
|
|
648
|
+
this.announceAbortAsync(request, error);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
request.once('aborted', abort);
|
|
652
|
+
// In Node 16+ we don't get an abort event in many cases, just closes, but we know
|
|
653
|
+
// it's aborted because the response is closed with no other result being set.
|
|
654
|
+
rawResponse.once('close', () => setImmediate(abort));
|
|
655
|
+
request.once('error', (error) => setImmediate(() => abort(error)));
|
|
656
|
+
|
|
657
|
+
this.announceInitialRequestAsync(request);
|
|
658
|
+
|
|
659
|
+
const response = trackResponse(
|
|
660
|
+
rawResponse,
|
|
661
|
+
request.timingEvents,
|
|
662
|
+
request.tags,
|
|
663
|
+
{ maxSize: this.maxBodySize }
|
|
664
|
+
);
|
|
665
|
+
response.id = request.id;
|
|
666
|
+
response.on('error', (error) => {
|
|
667
|
+
console.log('Response error:', this.debug ? error : error.message);
|
|
668
|
+
abort(error);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
let nextRulePromise = this.findMatchingRule(this.requestRuleSets, request);
|
|
673
|
+
|
|
674
|
+
// Async: once we know what the next rule is, ping a request event
|
|
675
|
+
nextRulePromise
|
|
676
|
+
.then((rule) => rule ? rule.id : undefined)
|
|
677
|
+
.catch(() => undefined)
|
|
678
|
+
.then((ruleId) => {
|
|
679
|
+
request.matchedRuleId = ruleId;
|
|
680
|
+
this.announceCompletedRequestAsync(request);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
let nextRule = await nextRulePromise;
|
|
684
|
+
if (nextRule) {
|
|
685
|
+
if (this.debug) console.log(`Request matched rule: ${nextRule.explain()}`);
|
|
686
|
+
await nextRule.handle(request, response, {
|
|
687
|
+
record: this.recordTraffic,
|
|
688
|
+
emitEventCallback: (this.eventEmitter.listenerCount('rule-event') !== 0)
|
|
689
|
+
? (type, event) => this.announceRuleEventAsync(request.id, nextRule!.id, type, event)
|
|
690
|
+
: undefined
|
|
691
|
+
});
|
|
692
|
+
} else {
|
|
693
|
+
await this.sendUnmatchedRequestError(request, response);
|
|
694
|
+
}
|
|
695
|
+
result = result || 'responded';
|
|
696
|
+
} catch (e) {
|
|
697
|
+
if (e instanceof AbortError) {
|
|
698
|
+
abort(e);
|
|
699
|
+
|
|
700
|
+
if (this.debug) {
|
|
701
|
+
console.error("Failed to handle request due to abort:", e);
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
console.error("Failed to handle request:",
|
|
705
|
+
this.debug
|
|
706
|
+
? e
|
|
707
|
+
: (isErrorLike(e) && e.message) || e
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// Do whatever we can to tell the client we broke
|
|
711
|
+
try {
|
|
712
|
+
response.writeHead(
|
|
713
|
+
(isErrorLike(e) && e.statusCode) || 500,
|
|
714
|
+
(isErrorLike(e) && e.statusMessage) || 'Server error'
|
|
715
|
+
);
|
|
716
|
+
} catch (e) {}
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
response.end((isErrorLike(e) && e.toString()) || e);
|
|
720
|
+
result = result || 'responded';
|
|
721
|
+
} catch (e) {
|
|
722
|
+
abort(e as Error);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (result === 'responded') {
|
|
728
|
+
this.announceResponseAsync(response);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private async handleWebSocket(rawRequest: ExtendedRawRequest, socket: net.Socket, head: Buffer) {
|
|
733
|
+
if (this.debug) console.log(`Handling websocket for ${rawRequest.url}`);
|
|
734
|
+
|
|
735
|
+
const request = this.preprocessRequest(rawRequest, 'websocket');
|
|
736
|
+
|
|
737
|
+
socket.on('error', (error) => {
|
|
738
|
+
console.log('Response error:', this.debug ? error : error.message);
|
|
739
|
+
socket.destroy();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
let nextRulePromise = this.findMatchingRule(this.webSocketRuleSets, request);
|
|
744
|
+
|
|
745
|
+
// Async: once we know what the next rule is, ping a websocket-request event
|
|
746
|
+
nextRulePromise
|
|
747
|
+
.then((rule) => rule ? rule.id : undefined)
|
|
748
|
+
.catch(() => undefined)
|
|
749
|
+
.then((ruleId) => {
|
|
750
|
+
request.matchedRuleId = ruleId;
|
|
751
|
+
this.announceWebSocketRequestAsync(request);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
this.trackWebSocketEvents(request, socket);
|
|
755
|
+
|
|
756
|
+
let nextRule = await nextRulePromise;
|
|
757
|
+
if (nextRule) {
|
|
758
|
+
if (this.debug) console.log(`Websocket matched rule: ${nextRule.explain()}`);
|
|
759
|
+
await nextRule.handle(request, socket, head, this.recordTraffic);
|
|
760
|
+
} else {
|
|
761
|
+
// Unmatched requests get passed through untouched automatically. This exists for
|
|
762
|
+
// historical/backward-compat reasons, to match the initial WS implementation, and
|
|
763
|
+
// will probably be removed to match handleRequest in future.
|
|
764
|
+
await this.defaultWsHandler.handle(
|
|
765
|
+
request as OngoingRequest & http.IncomingMessage,
|
|
766
|
+
socket,
|
|
767
|
+
head
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
} catch (e) {
|
|
771
|
+
if (e instanceof AbortError) {
|
|
772
|
+
if (this.debug) {
|
|
773
|
+
console.error("Failed to handle websocket due to abort:", e);
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
console.error("Failed to handle websocket:",
|
|
777
|
+
this.debug
|
|
778
|
+
? e
|
|
779
|
+
: (isErrorLike(e) && e.message) || e
|
|
780
|
+
);
|
|
781
|
+
this.sendWebSocketErrorResponse(socket, e);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* To match rules, we find the first rule (by priority then by set order) which matches and which is
|
|
788
|
+
* either not complete (has a completion check that's false) or which has no completion check defined
|
|
789
|
+
* and is the last option at that priority (i.e. by the last option at each priority repeats indefinitely.
|
|
790
|
+
*
|
|
791
|
+
* We move down the priority list only when either no rules match at all, or when all matching rules
|
|
792
|
+
* have explicit completion checks defined that are completed.
|
|
793
|
+
*/
|
|
794
|
+
private async findMatchingRule<R extends WebSocketRule | RequestRule>(
|
|
795
|
+
ruleSets: { [priority: number]: Array<R> },
|
|
796
|
+
request: OngoingRequest
|
|
797
|
+
): Promise<R | undefined> {
|
|
798
|
+
for (let ruleSet of Object.values(ruleSets).reverse()) { // Obj.values returns numeric keys in ascending order
|
|
799
|
+
// Start all rules matching immediately
|
|
800
|
+
const rulesMatches = ruleSet
|
|
801
|
+
.filter((r) => r.isComplete() !== true) // Skip all rules that are definitely completed
|
|
802
|
+
.map((r) => ({ rule: r, match: r.matches(request) }));
|
|
803
|
+
|
|
804
|
+
// Evaluate the matches one by one, and immediately use the first
|
|
805
|
+
for (let { rule, match } of rulesMatches) {
|
|
806
|
+
if (await match && rule.isComplete() === false) {
|
|
807
|
+
// The first matching incomplete rule we find is the one we should use
|
|
808
|
+
return rule;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// There are no incomplete & matching rules! One last option: if the last matching rule is
|
|
813
|
+
// maybe-incomplete (i.e. default completion status but has seen >0 requests) then it should
|
|
814
|
+
// match anyway. This allows us to add rules and have the last repeat indefinitely.
|
|
815
|
+
const lastMatchingRule = _.last(await filter(rulesMatches, m => m.match))?.rule;
|
|
816
|
+
if (!lastMatchingRule || lastMatchingRule.isComplete()) continue; // On to lower priority matches
|
|
817
|
+
// Otherwise, must be a rule with isComplete === null, i.e. no specific completion check:
|
|
818
|
+
else return lastMatchingRule;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return undefined; // There are zero valid matching rules at any priority, give up.
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private async getUnmatchedRequestExplanation(request: OngoingRequest) {
|
|
825
|
+
let requestExplanation = await this.explainRequest(request);
|
|
826
|
+
if (this.debug) console.warn(`Unmatched request received: ${requestExplanation}`);
|
|
827
|
+
|
|
828
|
+
const requestRules = Object.values(this.requestRuleSets).flat();
|
|
829
|
+
const webSocketRules = Object.values(this.webSocketRuleSets).flat();
|
|
830
|
+
|
|
831
|
+
return `No rules were found matching this request.
|
|
832
|
+
This request was: ${requestExplanation}
|
|
833
|
+
|
|
834
|
+
${(requestRules.length > 0 || webSocketRules.length > 0)
|
|
835
|
+
? `The configured rules are:
|
|
836
|
+
${requestRules.map((rule) => rule.explain()).join("\n")}
|
|
837
|
+
${webSocketRules.map((rule) => rule.explain()).join("\n")}
|
|
838
|
+
`
|
|
839
|
+
: "There are no rules configured."
|
|
840
|
+
}
|
|
841
|
+
${await this.suggestRule(request)}`
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private async sendUnmatchedRequestError(request: OngoingRequest, response: http.ServerResponse) {
|
|
845
|
+
response.setHeader('Content-Type', 'text/plain');
|
|
846
|
+
response.writeHead(503, "Request for unmocked endpoint");
|
|
847
|
+
response.end(await this.getUnmatchedRequestExplanation(request));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private async sendWebSocketErrorResponse(socket: net.Socket, error: unknown) {
|
|
851
|
+
if (socket.writable) {
|
|
852
|
+
socket.end(
|
|
853
|
+
'HTTP/1.1 500 Internal Server Error\r\n' +
|
|
854
|
+
'\r\n' +
|
|
855
|
+
(isErrorLike(error)
|
|
856
|
+
? error.message ?? error.toString()
|
|
857
|
+
: ''
|
|
858
|
+
)
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
socket.destroy(error as Error);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private async explainRequest(request: OngoingRequest): Promise<string> {
|
|
866
|
+
let msg = `${request.method} request to ${request.url}`;
|
|
867
|
+
|
|
868
|
+
let bodyText = await request.body.asText();
|
|
869
|
+
if (bodyText) msg += ` with body \`${bodyText}\``;
|
|
870
|
+
|
|
871
|
+
if (!_.isEmpty(request.headers)) {
|
|
872
|
+
msg += ` with headers:\n${JSON.stringify(request.headers, null, 2)}`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return msg;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private async suggestRule(request: OngoingRequest): Promise<string> {
|
|
879
|
+
if (!this.suggestChanges) return '';
|
|
880
|
+
|
|
881
|
+
let msg = "You can fix this by adding a rule to match this request, for example:\n"
|
|
882
|
+
|
|
883
|
+
msg += `mockServer.for${_.startCase(request.method.toLowerCase())}("${request.path}")`;
|
|
884
|
+
|
|
885
|
+
const contentType = request.headers['content-type'];
|
|
886
|
+
let isFormRequest = !!contentType && contentType.indexOf("application/x-www-form-urlencoded") > -1;
|
|
887
|
+
let formBody = await request.body.asFormData().catch(() => undefined);
|
|
888
|
+
|
|
889
|
+
if (isFormRequest && !!formBody) {
|
|
890
|
+
msg += `.withForm(${JSON.stringify(formBody)})`;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
msg += '.thenReply(200, "your response");';
|
|
894
|
+
|
|
895
|
+
return msg;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Called on server clientError, e.g. if the client disconnects during initial
|
|
899
|
+
// request data, or sends totally invalid gibberish. Only called for HTTP/1.1 errors.
|
|
900
|
+
private handleInvalidHttp1Request(
|
|
901
|
+
error: Error & { code?: string, rawPacket?: Buffer },
|
|
902
|
+
socket: net.Socket
|
|
903
|
+
) {
|
|
904
|
+
if (socket.clientErrorInProgress) {
|
|
905
|
+
// For subsequent errors on the same socket, accumulate packet data (linked to the socket)
|
|
906
|
+
// so that the error (probably delayed until next tick) has it all to work with
|
|
907
|
+
const previousPacket = socket.clientErrorInProgress.rawPacket;
|
|
908
|
+
const newPacket = error.rawPacket;
|
|
909
|
+
if (!newPacket || newPacket === previousPacket) return;
|
|
910
|
+
|
|
911
|
+
if (previousPacket && previousPacket.length > 0) {
|
|
912
|
+
if (previousPacket.equals(newPacket.slice(0, previousPacket.length))) {
|
|
913
|
+
// This is the same data, but more - update the client error data
|
|
914
|
+
socket.clientErrorInProgress.rawPacket = newPacket;
|
|
915
|
+
} else {
|
|
916
|
+
// This is different data for the same socket, probably an overflow, append it
|
|
917
|
+
socket.clientErrorInProgress.rawPacket = Buffer.concat([
|
|
918
|
+
previousPacket,
|
|
919
|
+
newPacket
|
|
920
|
+
]);
|
|
921
|
+
}
|
|
922
|
+
} else {
|
|
923
|
+
// The first error had no data, we have data - use our data
|
|
924
|
+
socket.clientErrorInProgress!.rawPacket = newPacket;
|
|
925
|
+
}
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// We can get multiple errors for the same socket in rapid succession as the parser works,
|
|
930
|
+
// so we store the initial buffer, wait a tick, and then reply/report the accumulated
|
|
931
|
+
// buffer from all errors together.
|
|
932
|
+
socket.clientErrorInProgress = {
|
|
933
|
+
// We use HTTP peeked data to catch extra data the parser sees due to httpolyglot peeking,
|
|
934
|
+
// but which gets lost from the raw packet. If that data alone causes an error though
|
|
935
|
+
// (e.g. Q as first char) then this packet data does get thrown! Eugh. In that case,
|
|
936
|
+
// we need to avoid using both by accident, so we use just the non-peeked data instead
|
|
937
|
+
// if the initial data is _exactly_ identical.
|
|
938
|
+
rawPacket: error.rawPacket
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
setImmediate(async () => {
|
|
942
|
+
const errorCode = error.code;
|
|
943
|
+
const isHeaderOverflow = errorCode === "HPE_HEADER_OVERFLOW";
|
|
944
|
+
|
|
945
|
+
const commonParams = {
|
|
946
|
+
id: uuid(),
|
|
947
|
+
tags: [`client-error:${error.code || 'UNKNOWN'}`],
|
|
948
|
+
timingEvents: { startTime: Date.now(), startTimestamp: now() } as TimingEvents
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
const rawPacket = socket.clientErrorInProgress?.rawPacket
|
|
952
|
+
?? Buffer.from([]);
|
|
953
|
+
|
|
954
|
+
// For packets where we get more than just httpolyglot-peeked data, guess-parse them:
|
|
955
|
+
const parsedRequest = rawPacket.byteLength > 1
|
|
956
|
+
? tryToParseHttpRequest(rawPacket, socket)
|
|
957
|
+
: {};
|
|
958
|
+
|
|
959
|
+
if (isHeaderOverflow) commonParams.tags.push('header-overflow');
|
|
960
|
+
|
|
961
|
+
const request: ClientError['request'] = {
|
|
962
|
+
...commonParams,
|
|
963
|
+
httpVersion: parsedRequest.httpVersion,
|
|
964
|
+
method: parsedRequest.method,
|
|
965
|
+
protocol: parsedRequest.protocol,
|
|
966
|
+
url: parsedRequest.url,
|
|
967
|
+
path: parsedRequest.path,
|
|
968
|
+
headers: parsedRequest.headers || {},
|
|
969
|
+
rawHeaders: parsedRequest.rawHeaders || [],
|
|
970
|
+
remoteIpAddress: socket.remoteAddress,
|
|
971
|
+
remotePort: socket.remotePort
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
let response: ClientError['response'];
|
|
975
|
+
|
|
976
|
+
if (socket.writable) {
|
|
977
|
+
response = {
|
|
978
|
+
...commonParams,
|
|
979
|
+
headers: { 'connection': 'close' },
|
|
980
|
+
rawHeaders: [['Connection', 'close']],
|
|
981
|
+
trailers: {},
|
|
982
|
+
rawTrailers: [],
|
|
983
|
+
statusCode:
|
|
984
|
+
isHeaderOverflow
|
|
985
|
+
? 431
|
|
986
|
+
: 400,
|
|
987
|
+
statusMessage:
|
|
988
|
+
isHeaderOverflow
|
|
989
|
+
? "Request Header Fields Too Large"
|
|
990
|
+
: "Bad Request",
|
|
991
|
+
body: buildBodyReader(Buffer.from([]), {})
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const responseBuffer = Buffer.from(
|
|
995
|
+
`HTTP/1.1 ${response.statusCode} ${response.statusMessage}\r\n` +
|
|
996
|
+
"Connection: close\r\n\r\n",
|
|
997
|
+
'ascii'
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
// Wait for the write to complete before we destroy() below
|
|
1001
|
+
await new Promise((resolve) => socket.write(responseBuffer, resolve));
|
|
1002
|
+
|
|
1003
|
+
commonParams.timingEvents.headersSentTimestamp = now();
|
|
1004
|
+
commonParams.timingEvents.responseSentTimestamp = now();
|
|
1005
|
+
} else {
|
|
1006
|
+
response = 'aborted';
|
|
1007
|
+
commonParams.timingEvents.abortedTimestamp = now();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
this.announceClientErrorAsync(socket, { errorCode, request, response });
|
|
1011
|
+
|
|
1012
|
+
socket.destroy(error);
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Handle HTTP/2 client errors. This is a work in progress, but usefully reports
|
|
1017
|
+
// some of the most obvious cases.
|
|
1018
|
+
private handleInvalidHttp2Request(
|
|
1019
|
+
error: Error & { code?: string, errno?: number },
|
|
1020
|
+
session: http2.Http2Session
|
|
1021
|
+
) {
|
|
1022
|
+
// Unlike with HTTP/1.1, we have no control of the actual handling of
|
|
1023
|
+
// the error here, so this is just a matter of announcing the error to subscribers.
|
|
1024
|
+
|
|
1025
|
+
const socket = session.initialSocket;
|
|
1026
|
+
const isTLS = socket instanceof tls.TLSSocket;
|
|
1027
|
+
|
|
1028
|
+
const isBadPreface = (error.errno === -903);
|
|
1029
|
+
|
|
1030
|
+
this.announceClientErrorAsync(session.initialSocket, {
|
|
1031
|
+
errorCode: error.code,
|
|
1032
|
+
request: {
|
|
1033
|
+
id: uuid(),
|
|
1034
|
+
tags: [
|
|
1035
|
+
`client-error:${error.code || 'UNKNOWN'}`,
|
|
1036
|
+
...(isBadPreface ? ['client-error:bad-preface'] : [])
|
|
1037
|
+
],
|
|
1038
|
+
httpVersion: '2',
|
|
1039
|
+
|
|
1040
|
+
// Best guesses:
|
|
1041
|
+
timingEvents: { startTime: Date.now(), startTimestamp: now() },
|
|
1042
|
+
protocol: isTLS ? "https" : "http",
|
|
1043
|
+
url: isTLS ? `https://${
|
|
1044
|
+
(socket as tls.TLSSocket).servername // Use the hostname from SNI
|
|
1045
|
+
}/` : undefined,
|
|
1046
|
+
|
|
1047
|
+
// Unknowable:
|
|
1048
|
+
path: undefined,
|
|
1049
|
+
headers: {},
|
|
1050
|
+
rawHeaders: []
|
|
1051
|
+
},
|
|
1052
|
+
response: 'aborted' // These h2 errors get no app-level response, just a shutdown.
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
private outgoingPassthroughSockets: Set<net.Socket> = new Set();
|
|
1057
|
+
|
|
1058
|
+
private passthroughSocket(
|
|
1059
|
+
socket: net.Socket,
|
|
1060
|
+
host: string,
|
|
1061
|
+
port?: number
|
|
1062
|
+
) {
|
|
1063
|
+
const targetPort = port || 443;
|
|
1064
|
+
|
|
1065
|
+
if (isSocketLoop(this.outgoingPassthroughSockets, socket)) {
|
|
1066
|
+
// Hard to reproduce: loops can only happen if a) SNI triggers this (because tunnels
|
|
1067
|
+
// require a repeated client request at each step) and b) the hostname points back to
|
|
1068
|
+
// us, and c) we're running on the default port. Still good to guard against though.
|
|
1069
|
+
console.warn(`Socket bypass loop for ${host}:${targetPort}`);
|
|
1070
|
+
resetOrDestroy(socket);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (socket.closed) return; // Nothing to do
|
|
1075
|
+
|
|
1076
|
+
const eventData = buildSocketEventData(socket as any) as TlsPassthroughEvent;
|
|
1077
|
+
eventData.id = uuid();
|
|
1078
|
+
eventData.hostname = host;
|
|
1079
|
+
eventData.upstreamPort = targetPort;
|
|
1080
|
+
setImmediate(() => this.eventEmitter.emit('tls-passthrough-opened', eventData));
|
|
1081
|
+
|
|
1082
|
+
const upstreamSocket = net.connect({ host, port: targetPort });
|
|
1083
|
+
|
|
1084
|
+
socket.pipe(upstreamSocket);
|
|
1085
|
+
upstreamSocket.pipe(socket);
|
|
1086
|
+
|
|
1087
|
+
socket.on('error', () => upstreamSocket.destroy());
|
|
1088
|
+
upstreamSocket.on('error', () => socket.destroy());
|
|
1089
|
+
upstreamSocket.on('close', () => socket.destroy());
|
|
1090
|
+
socket.on('close', () => {
|
|
1091
|
+
upstreamSocket.destroy();
|
|
1092
|
+
setImmediate(() => {
|
|
1093
|
+
this.eventEmitter.emit('tls-passthrough-closed', {
|
|
1094
|
+
...eventData,
|
|
1095
|
+
timingEvents: {
|
|
1096
|
+
...eventData.timingEvents,
|
|
1097
|
+
disconnectedTimestamp: now()
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
upstreamSocket.once('connect', () => this.outgoingPassthroughSockets.add(upstreamSocket));
|
|
1104
|
+
upstreamSocket.once('close', () => this.outgoingPassthroughSockets.delete(upstreamSocket));
|
|
1105
|
+
|
|
1106
|
+
if (this.debug) console.log(`Passing through raw bypassed connection to ${host}:${targetPort}${
|
|
1107
|
+
!port ? ' (assumed port)' : ''
|
|
1108
|
+
}`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|