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,424 @@
|
|
|
1
|
+
import _ = require('lodash');
|
|
2
|
+
import now = require('performance-now');
|
|
3
|
+
import net = require('net');
|
|
4
|
+
import tls = require('tls');
|
|
5
|
+
import http = require('http');
|
|
6
|
+
import http2 = require('http2');
|
|
7
|
+
import * as streams from 'stream';
|
|
8
|
+
|
|
9
|
+
import * as semver from 'semver';
|
|
10
|
+
import { makeDestroyable, DestroyableServer } from 'destroyable-server';
|
|
11
|
+
import httpolyglot = require('@httptoolkit/httpolyglot');
|
|
12
|
+
import {
|
|
13
|
+
calculateJa3FromFingerprintData,
|
|
14
|
+
NonTlsError,
|
|
15
|
+
readTlsClientHello
|
|
16
|
+
} from 'read-tls-client-hello';
|
|
17
|
+
import { URLPattern } from "urlpattern-polyfill";
|
|
18
|
+
|
|
19
|
+
import { TlsHandshakeFailure } from '../types';
|
|
20
|
+
import { getCA } from '../util/tls';
|
|
21
|
+
import { delay } from '../util/util';
|
|
22
|
+
import { shouldPassThrough } from '../util/server-utils';
|
|
23
|
+
import {
|
|
24
|
+
getParentSocket,
|
|
25
|
+
buildSocketTimingInfo,
|
|
26
|
+
buildSocketEventData
|
|
27
|
+
} from '../util/socket-util';
|
|
28
|
+
import { MockttpHttpsOptions } from '../mockttp';
|
|
29
|
+
|
|
30
|
+
// Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to
|
|
31
|
+
// sockets as soon as they're available, without waiting for the handshake to fully
|
|
32
|
+
// complete, so we can easily access them if the handshake fails.
|
|
33
|
+
const originalSocketInit = (<any>tls.TLSSocket.prototype)._init;
|
|
34
|
+
(<any>tls.TLSSocket.prototype)._init = function () {
|
|
35
|
+
originalSocketInit.apply(this, arguments);
|
|
36
|
+
|
|
37
|
+
const tlsSocket: tls.TLSSocket = this;
|
|
38
|
+
const { _handle } = tlsSocket;
|
|
39
|
+
if (!_handle) return;
|
|
40
|
+
|
|
41
|
+
const loadSNI = _handle.oncertcb;
|
|
42
|
+
_handle.oncertcb = function (info: any) {
|
|
43
|
+
tlsSocket.servername = info.servername;
|
|
44
|
+
tlsSocket.initialRemoteAddress = tlsSocket.remoteAddress || // Normal case
|
|
45
|
+
tlsSocket._parent?.remoteAddress || // For early failing sockets
|
|
46
|
+
tlsSocket._handle?._parentWrap?.stream?.remoteAddress; // For HTTP/2 CONNECT
|
|
47
|
+
tlsSocket.initialRemotePort = tlsSocket.remotePort ||
|
|
48
|
+
tlsSocket._parent?.remotePort ||
|
|
49
|
+
tlsSocket._handle?._parentWrap?.stream?.remotePort;
|
|
50
|
+
|
|
51
|
+
return loadSNI?.apply(this, arguments as any);
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ComboServerOptions = {
|
|
56
|
+
debug: boolean,
|
|
57
|
+
https: MockttpHttpsOptions | undefined,
|
|
58
|
+
http2: true | false | 'fallback'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Takes an established TLS socket, calls the error listener if it's silently closed
|
|
62
|
+
function ifTlsDropped(socket: tls.TLSSocket, errorCallback: () => void) {
|
|
63
|
+
new Promise((resolve, reject) => {
|
|
64
|
+
// If you send data, you trust the TLS connection
|
|
65
|
+
socket.once('data', resolve);
|
|
66
|
+
|
|
67
|
+
// If you silently close it very quicky, you probably don't trust us
|
|
68
|
+
socket.once('error', reject);
|
|
69
|
+
socket.once('close', reject);
|
|
70
|
+
socket.once('end', reject);
|
|
71
|
+
|
|
72
|
+
// Some clients open later-unused TLS connections for connection pools, preconnect, etc.
|
|
73
|
+
// Even if these are shut later on, that doesn't mean they're are rejected connections.
|
|
74
|
+
// To differentiate the two cases, we consider connections OK after waiting 10x longer
|
|
75
|
+
// than the initial TLS handshake for an unhappy disconnection.
|
|
76
|
+
const timing = socket.__timingInfo;
|
|
77
|
+
const tlsSetupDuration = timing
|
|
78
|
+
? timing.tlsConnectedTimestamp! - (timing.tunnelSetupTimestamp! || timing.initialSocketTimestamp)
|
|
79
|
+
: 0;
|
|
80
|
+
const maxTlsRejectionTime = !Object.is(tlsSetupDuration, NaN)
|
|
81
|
+
? Math.max(tlsSetupDuration * 10, 100) // Ensure a sensible minimum
|
|
82
|
+
: 2000;
|
|
83
|
+
|
|
84
|
+
delay(maxTlsRejectionTime).then(resolve);
|
|
85
|
+
})
|
|
86
|
+
.then(() => {
|
|
87
|
+
// Mark the socket as having completed TLS setup - this ensures that future
|
|
88
|
+
// errors fire as client errors, not TLS setup errors.
|
|
89
|
+
socket.tlsSetupCompleted = true;
|
|
90
|
+
})
|
|
91
|
+
.catch(() => {
|
|
92
|
+
// If TLS setup was confirmed in any way, we know we don't have a TLS error.
|
|
93
|
+
if (socket.tlsSetupCompleted) return;
|
|
94
|
+
|
|
95
|
+
// To get here, the socket must have connected & done the TLS handshake, but then
|
|
96
|
+
// closed/ended without ever sending any data. We can fairly confidently assume
|
|
97
|
+
// in that case that it's rejected our certificate.
|
|
98
|
+
errorCallback();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getCauseFromError(error: Error & { code?: string }) {
|
|
103
|
+
const cause = (
|
|
104
|
+
/alert certificate/.test(error.message) ||
|
|
105
|
+
/alert bad certificate/.test(error.message) ||
|
|
106
|
+
error.code === 'ERR_SSL_SSLV3_ALERT_BAD_CERTIFICATE' ||
|
|
107
|
+
/alert unknown ca/.test(error.message)
|
|
108
|
+
)
|
|
109
|
+
// The client explicitly told us it doesn't like the certificate
|
|
110
|
+
? 'cert-rejected'
|
|
111
|
+
: /no shared cipher/.test(error.message)
|
|
112
|
+
// The client refused to negotiate a cipher. Probably means it didn't like the
|
|
113
|
+
// cert so refused to continue, but it could genuinely not have a shared cipher.
|
|
114
|
+
? 'no-shared-cipher'
|
|
115
|
+
: (/ECONNRESET/.test(error.message) || error.code === 'ECONNRESET')
|
|
116
|
+
// The client sent no TLS alert, it just hard RST'd the connection
|
|
117
|
+
? 'reset'
|
|
118
|
+
: error.code === 'ERR_TLS_HANDSHAKE_TIMEOUT'
|
|
119
|
+
? 'handshake-timeout'
|
|
120
|
+
: 'unknown'; // Something else.
|
|
121
|
+
|
|
122
|
+
if (cause === 'unknown') console.log('Unknown TLS error:', error);
|
|
123
|
+
|
|
124
|
+
return cause;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildTlsError(
|
|
128
|
+
socket: tls.TLSSocket,
|
|
129
|
+
cause: TlsHandshakeFailure['failureCause']
|
|
130
|
+
): TlsHandshakeFailure {
|
|
131
|
+
const eventData = buildSocketEventData(socket) as TlsHandshakeFailure;
|
|
132
|
+
|
|
133
|
+
eventData.failureCause = cause;
|
|
134
|
+
eventData.timingEvents.failureTimestamp = now();
|
|
135
|
+
|
|
136
|
+
return eventData;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The low-level server that handles all the sockets & TLS. The server will correctly call the
|
|
140
|
+
// given handler for both HTTP & HTTPS direct connections, or connections when used as an
|
|
141
|
+
// either HTTP or HTTPS proxy, all on the same port.
|
|
142
|
+
export async function createComboServer(
|
|
143
|
+
options: ComboServerOptions,
|
|
144
|
+
requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void,
|
|
145
|
+
tlsClientErrorListener: (socket: tls.TLSSocket, req: TlsHandshakeFailure) => void,
|
|
146
|
+
tlsPassthroughListener: (socket: net.Socket, address: string, port?: number) => void
|
|
147
|
+
): Promise<DestroyableServer<net.Server>> {
|
|
148
|
+
let server: net.Server;
|
|
149
|
+
if (!options.https) {
|
|
150
|
+
server = httpolyglot.createServer(requestListener);
|
|
151
|
+
} else {
|
|
152
|
+
const ca = await getCA(options.https);
|
|
153
|
+
const defaultCert = ca.generateCertificate(options.https.defaultDomain ?? 'localhost');
|
|
154
|
+
|
|
155
|
+
const serverProtocolPreferences = options.http2 === true
|
|
156
|
+
? ['h2', 'http/1.1', 'http 1.1'] // 'http 1.1' is non-standard, but used by https-proxy-agent
|
|
157
|
+
: options.http2 === 'fallback'
|
|
158
|
+
? ['http/1.1', 'http 1.1', 'h2']
|
|
159
|
+
// options.http2 === false:
|
|
160
|
+
: ['http/1.1', 'http 1.1'];
|
|
161
|
+
|
|
162
|
+
const ALPNOption: tls.TlsOptions = semver.satisfies(process.version, '>=20.4.0')
|
|
163
|
+
? {
|
|
164
|
+
// In modern Node (20+), ALPNProtocols will reject unknown protocols. To allow those (so we can
|
|
165
|
+
// at least read the request, and hopefully handle HTTP-like cases - not uncommon) we use the new
|
|
166
|
+
// ALPNCallback feature instead, which lets us dynamically accept unrecognized protocols:
|
|
167
|
+
ALPNCallback: ({ protocols: clientProtocols }) => {
|
|
168
|
+
const preferredProtocol = serverProtocolPreferences.find(p => clientProtocols.includes(p));
|
|
169
|
+
|
|
170
|
+
// Wherever possible, we tell the client to use our preferred protocol
|
|
171
|
+
if (preferredProtocol) return preferredProtocol;
|
|
172
|
+
|
|
173
|
+
// If the client only offers protocols that we don't understand, shrug and accept:
|
|
174
|
+
else return clientProtocols[1];
|
|
175
|
+
}
|
|
176
|
+
} : {
|
|
177
|
+
// In Node versions without ALPNCallback, we just set preferences directly:
|
|
178
|
+
ALPNProtocols: serverProtocolPreferences
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const tlsServer = tls.createServer({
|
|
182
|
+
key: defaultCert.key,
|
|
183
|
+
cert: defaultCert.cert,
|
|
184
|
+
ca: [defaultCert.ca],
|
|
185
|
+
...ALPNOption,
|
|
186
|
+
SNICallback: (domain: string, cb: Function) => {
|
|
187
|
+
if (options.debug) console.log(`Generating certificate for ${domain}`);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const generatedCert = ca.generateCertificate(domain);
|
|
191
|
+
cb(null, tls.createSecureContext({
|
|
192
|
+
key: generatedCert.key,
|
|
193
|
+
cert: generatedCert.cert,
|
|
194
|
+
ca: generatedCert.ca
|
|
195
|
+
}));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
console.error('Cert generation error', e);
|
|
198
|
+
cb(e);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
analyzeAndMaybePassThroughTls(
|
|
204
|
+
tlsServer,
|
|
205
|
+
options.https.tlsPassthrough,
|
|
206
|
+
options.https.tlsInterceptOnly,
|
|
207
|
+
tlsPassthroughListener
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
server = httpolyglot.createServer(tlsServer, requestListener);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// In Node v20, this option was added, rejecting all requests with no host header. While that's good, in
|
|
214
|
+
// our case, we want to handle the garbage requests too, so we disable it:
|
|
215
|
+
(server as any)._httpServer.requireHostHeader = false;
|
|
216
|
+
|
|
217
|
+
server.on('connection', (socket: net.Socket | http2.ServerHttp2Stream) => {
|
|
218
|
+
socket.__timingInfo = socket.__timingInfo || buildSocketTimingInfo();
|
|
219
|
+
|
|
220
|
+
// All sockets are initially marked as using unencrypted upstream connections.
|
|
221
|
+
// If TLS is used, this is upgraded to 'true' by secureConnection below.
|
|
222
|
+
socket.__lastHopEncrypted = false;
|
|
223
|
+
|
|
224
|
+
// For actual sockets, set NODELAY to avoid any buffering whilst streaming. This is
|
|
225
|
+
// off by default in Node HTTP, but likely to be enabled soon & is default in curl.
|
|
226
|
+
if ('setNoDelay' in socket) socket.setNoDelay(true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
server.on('secureConnection', (socket: tls.TLSSocket) => {
|
|
230
|
+
const parentSocket = getParentSocket(socket);
|
|
231
|
+
if (parentSocket) {
|
|
232
|
+
// Sometimes wrapper TLS sockets created by the HTTP/2 server don't include the
|
|
233
|
+
// underlying socket details, so it's better to make sure we copy them up.
|
|
234
|
+
copyAddressDetails(parentSocket, socket);
|
|
235
|
+
copyTimingDetails(parentSocket, socket);
|
|
236
|
+
// With TLS metadata, we only propagate directly from parent sockets, not through
|
|
237
|
+
// CONNECT etc - we only want it if the final hop is TLS, previous values don't matter.
|
|
238
|
+
socket.__tlsMetadata ??= parentSocket.__tlsMetadata;
|
|
239
|
+
} else if (!socket.__timingInfo) {
|
|
240
|
+
socket.__timingInfo = buildSocketTimingInfo();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
socket.__timingInfo!.tlsConnectedTimestamp = now();
|
|
244
|
+
|
|
245
|
+
socket.__lastHopEncrypted = true;
|
|
246
|
+
ifTlsDropped(socket, () => {
|
|
247
|
+
tlsClientErrorListener(socket, buildTlsError(socket, 'closed'));
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Mark HTTP/2 sockets as set up once we receive a first settings frame. This always
|
|
252
|
+
// happens immediately after the connection preface, as long as the connection is OK.
|
|
253
|
+
server!.on('session', (session) => {
|
|
254
|
+
session.once('remoteSettings', () => {
|
|
255
|
+
session.socket.tlsSetupCompleted = true;
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
server.on('tlsClientError', (error: Error, socket: tls.TLSSocket) => {
|
|
260
|
+
tlsClientErrorListener(socket, buildTlsError(socket, getCauseFromError(error)));
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// If the server receives a HTTP/HTTPS CONNECT request, Pretend to tunnel, then just re-handle:
|
|
264
|
+
server.addListener('connect', function (
|
|
265
|
+
req: http.IncomingMessage | http2.Http2ServerRequest,
|
|
266
|
+
resOrSocket: net.Socket | http2.Http2ServerResponse
|
|
267
|
+
) {
|
|
268
|
+
if (resOrSocket instanceof net.Socket) {
|
|
269
|
+
handleH1Connect(req as http.IncomingMessage, resOrSocket);
|
|
270
|
+
} else {
|
|
271
|
+
handleH2Connect(req as http2.Http2ServerRequest, resOrSocket);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
function handleH1Connect(req: http.IncomingMessage, socket: net.Socket) {
|
|
276
|
+
// Clients may disconnect at this point (for all sorts of reasons), but here
|
|
277
|
+
// nothing else is listening, so we need to catch errors on the socket:
|
|
278
|
+
socket.once('error', (e) => {
|
|
279
|
+
if (options.debug) {
|
|
280
|
+
console.log('Error on client socket', e);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const connectUrl = req.url || req.headers['host'];
|
|
285
|
+
if (!connectUrl) {
|
|
286
|
+
// If we can't work out where to go, send an error.
|
|
287
|
+
socket.write('HTTP/' + req.httpVersion + ' 400 Bad Request\r\n\r\n', 'utf-8');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (options.debug) console.log(`Proxying HTTP/1 CONNECT to ${connectUrl}`);
|
|
292
|
+
|
|
293
|
+
socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'utf-8', () => {
|
|
294
|
+
socket.__timingInfo!.tunnelSetupTimestamp = now();
|
|
295
|
+
socket.__lastHopConnectAddress = connectUrl;
|
|
296
|
+
server.emit('connection', socket);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function handleH2Connect(req: http2.Http2ServerRequest, res: http2.Http2ServerResponse) {
|
|
301
|
+
const connectUrl = req.headers[':authority'];
|
|
302
|
+
|
|
303
|
+
if (!connectUrl) {
|
|
304
|
+
// If we can't work out where to go, send an error.
|
|
305
|
+
res.writeHead(400, {});
|
|
306
|
+
res.end();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (options.debug) console.log(`Proxying HTTP/2 CONNECT to ${connectUrl}`);
|
|
311
|
+
|
|
312
|
+
// Send a 200 OK response, and start the tunnel:
|
|
313
|
+
res.writeHead(200, {});
|
|
314
|
+
copyAddressDetails(res.socket, res.stream);
|
|
315
|
+
copyTimingDetails(res.socket, res.stream);
|
|
316
|
+
res.stream.__lastHopConnectAddress = connectUrl;
|
|
317
|
+
|
|
318
|
+
// When layering HTTP/2 on JS streams, we have to make sure the JS stream won't autoclose
|
|
319
|
+
// when the other side does, because the upper HTTP/2 layers want to handle shutdown, so
|
|
320
|
+
// they end up trying to write a GOAWAY at the same time as the lower stream shuts down,
|
|
321
|
+
// and we get assertion errors in Node v16.7+.
|
|
322
|
+
if (res.socket.constructor.name.includes('JSStreamSocket')) {
|
|
323
|
+
res.socket.allowHalfOpen = true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
server.emit('connection', res.stream);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return makeDestroyable(server);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
type SocketIsh<MinProps extends keyof net.Socket> =
|
|
333
|
+
streams.Duplex & Partial<Pick<net.Socket, MinProps>>;
|
|
334
|
+
|
|
335
|
+
const SOCKET_ADDRESS_METADATA_FIELDS = [
|
|
336
|
+
'localAddress',
|
|
337
|
+
'localPort',
|
|
338
|
+
'remoteAddress',
|
|
339
|
+
'remotePort',
|
|
340
|
+
'__lastHopConnectAddress'
|
|
341
|
+
] as const;
|
|
342
|
+
|
|
343
|
+
// Update the target socket(-ish) with the address details from the source socket,
|
|
344
|
+
// iff the target has no details of its own.
|
|
345
|
+
function copyAddressDetails(
|
|
346
|
+
source: SocketIsh<typeof SOCKET_ADDRESS_METADATA_FIELDS[number]>,
|
|
347
|
+
target: SocketIsh<typeof SOCKET_ADDRESS_METADATA_FIELDS[number]>
|
|
348
|
+
) {
|
|
349
|
+
Object.defineProperties(target, _.zipObject(
|
|
350
|
+
SOCKET_ADDRESS_METADATA_FIELDS,
|
|
351
|
+
_.range(SOCKET_ADDRESS_METADATA_FIELDS.length).map(() => ({ writable: true }))
|
|
352
|
+
) as PropertyDescriptorMap);
|
|
353
|
+
|
|
354
|
+
SOCKET_ADDRESS_METADATA_FIELDS.forEach((fieldName) => {
|
|
355
|
+
if (target[fieldName] === undefined) {
|
|
356
|
+
(target as any)[fieldName] = source[fieldName];
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function copyTimingDetails<T extends SocketIsh<'__timingInfo'>>(
|
|
362
|
+
source: SocketIsh<'__timingInfo'>,
|
|
363
|
+
target: T
|
|
364
|
+
): asserts target is T & { __timingInfo: Required<net.Socket>['__timingInfo'] } {
|
|
365
|
+
if (!target.__timingInfo) {
|
|
366
|
+
// Clone timing info, don't copy it - child sockets get their own independent timing stats
|
|
367
|
+
target.__timingInfo = Object.assign({}, source.__timingInfo);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Takes tls passthrough configuration (may be empty) and reconfigures a given TLS server so that all
|
|
373
|
+
* client hellos are parsed, matching requests are passed to the given passthrough listener (without
|
|
374
|
+
* continuing setup) and client hello metadata is attached to all sockets.
|
|
375
|
+
*/
|
|
376
|
+
function analyzeAndMaybePassThroughTls(
|
|
377
|
+
server: tls.Server,
|
|
378
|
+
passthroughList: Required<MockttpHttpsOptions>['tlsPassthrough'] | undefined,
|
|
379
|
+
interceptOnlyList: Required<MockttpHttpsOptions>['tlsInterceptOnly'] | undefined,
|
|
380
|
+
passthroughListener: (socket: net.Socket, address: string, port?: number) => void
|
|
381
|
+
) {
|
|
382
|
+
if (passthroughList && interceptOnlyList){
|
|
383
|
+
throw new Error('Cannot use both tlsPassthrough and tlsInterceptOnly options at the same time.');
|
|
384
|
+
}
|
|
385
|
+
const passThroughPatterns = passthroughList?.map(({ hostname }) => new URLPattern(`https://${hostname}`)) ?? [];
|
|
386
|
+
const interceptOnlyPatterns = interceptOnlyList?.map(({ hostname }) => new URLPattern(`https://${hostname}`));
|
|
387
|
+
|
|
388
|
+
const tlsConnectionListener = server.listeners('connection')[0] as (socket: net.Socket) => {};
|
|
389
|
+
server.removeListener('connection', tlsConnectionListener);
|
|
390
|
+
server.on('connection', async (socket: net.Socket) => {
|
|
391
|
+
try {
|
|
392
|
+
const helloData = await readTlsClientHello(socket);
|
|
393
|
+
|
|
394
|
+
const [connectHostname, connectPort] = socket.__lastHopConnectAddress?.split(':') ?? [];
|
|
395
|
+
const sniHostname = helloData.serverName;
|
|
396
|
+
|
|
397
|
+
socket.__tlsMetadata = {
|
|
398
|
+
sniHostname,
|
|
399
|
+
connectHostname,
|
|
400
|
+
connectPort,
|
|
401
|
+
clientAlpn: helloData.alpnProtocols,
|
|
402
|
+
ja3Fingerprint: calculateJa3FromFingerprintData(helloData.fingerprintData)
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
if (shouldPassThrough(connectHostname, passThroughPatterns, interceptOnlyPatterns)) {
|
|
406
|
+
const upstreamPort = connectPort ? parseInt(connectPort, 10) : undefined;
|
|
407
|
+
passthroughListener(socket, connectHostname, upstreamPort);
|
|
408
|
+
return; // Do not continue with TLS
|
|
409
|
+
} else if (shouldPassThrough(sniHostname, passThroughPatterns, interceptOnlyPatterns)) {
|
|
410
|
+
passthroughListener(socket, sniHostname!); // Can't guess the port - not included in SNI
|
|
411
|
+
return; // Do not continue with TLS
|
|
412
|
+
}
|
|
413
|
+
} catch (e) {
|
|
414
|
+
if (!(e instanceof NonTlsError)) { // Don't even warn for non-TLS traffic
|
|
415
|
+
console.warn(`TLS client hello data not available for TLS connection from ${
|
|
416
|
+
socket.remoteAddress ?? 'unknown address'
|
|
417
|
+
}: ${(e as Error).message ?? e}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Didn't match a passthrough hostname - continue with TLS setup
|
|
422
|
+
tlsConnectionListener.call(server, socket);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as util from 'util';
|
|
2
|
+
|
|
3
|
+
import type { CompletedRequest, MockedEndpoint } from '../types';
|
|
4
|
+
import type { RequestRule } from '../rules/requests/request-rule';
|
|
5
|
+
import type { WebSocketRule } from '../rules/websockets/websocket-rule';
|
|
6
|
+
|
|
7
|
+
export class ServerMockedEndpoint implements MockedEndpoint {
|
|
8
|
+
|
|
9
|
+
constructor(private rule: RequestRule | WebSocketRule) {
|
|
10
|
+
this.getSeenRequests.bind(this);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
get id() {
|
|
14
|
+
return this.rule.id;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getSeenRequests(): Promise<CompletedRequest[]> {
|
|
18
|
+
// Wait for all completed running requests to have all their details available
|
|
19
|
+
return Promise.all<CompletedRequest>(this.rule.requests);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async isPending(): Promise<boolean> {
|
|
23
|
+
// We don't actually need to wait for rule.requests to complete, because
|
|
24
|
+
// completion rules right now only check requestCount, and that is always
|
|
25
|
+
// updated synchronously when handling starts.
|
|
26
|
+
|
|
27
|
+
const ruleCompletion = this.rule.isComplete();
|
|
28
|
+
if (ruleCompletion !== null) {
|
|
29
|
+
// If the rule has a specific completion value, use it
|
|
30
|
+
return !ruleCompletion;
|
|
31
|
+
} else {
|
|
32
|
+
// If not, then it's default "at least one" completion:
|
|
33
|
+
return this.rule.requestCount === 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[util.inspect.custom]() {
|
|
38
|
+
return "Mocked endpoint: " + this.toString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
toString(withoutExactCompletion = false) {
|
|
42
|
+
return this.rule.explain(withoutExactCompletion);
|
|
43
|
+
}
|
|
44
|
+
}
|