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