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,193 @@
|
|
|
1
|
+
import * as _ from 'lodash';
|
|
2
|
+
import now = require("performance-now");
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as net from 'net';
|
|
5
|
+
import * as tls from 'tls';
|
|
6
|
+
import * as http2 from 'http2';
|
|
7
|
+
|
|
8
|
+
import { isNode } from './util';
|
|
9
|
+
import { OngoingRequest, TlsConnectionEvent } from '../types';
|
|
10
|
+
|
|
11
|
+
// Test if a local port for a given interface (IPv4/6) is currently in use
|
|
12
|
+
export async function isLocalPortActive(interfaceIp: '::1' | '127.0.0.1', port: number) {
|
|
13
|
+
if (interfaceIp === '::1' && !isLocalIPv6Available) return false;
|
|
14
|
+
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const server = net.createServer();
|
|
17
|
+
server.listen({
|
|
18
|
+
host: interfaceIp,
|
|
19
|
+
port,
|
|
20
|
+
ipv6Only: interfaceIp === '::1'
|
|
21
|
+
});
|
|
22
|
+
server.once('listening', () => {
|
|
23
|
+
resolve(false);
|
|
24
|
+
server.close(() => {});
|
|
25
|
+
});
|
|
26
|
+
server.once('error', (e) => {
|
|
27
|
+
resolve(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// This file imported in browsers etc as it's used in handlers, but none of these methods are used
|
|
33
|
+
// directly. It is useful though to guard sections that immediately perform actions:
|
|
34
|
+
export const isLocalIPv6Available = isNode
|
|
35
|
+
? _.some(os.networkInterfaces(),
|
|
36
|
+
(addresses) => _.some(addresses, a => a.address === '::1')
|
|
37
|
+
)
|
|
38
|
+
: true;
|
|
39
|
+
|
|
40
|
+
// We need to normalize ips for comparison, because the same ip may be reported as ::ffff:127.0.0.1
|
|
41
|
+
// and 127.0.0.1 on the two sides of the connection, for the same ip.
|
|
42
|
+
const normalizeIp = (ip: string | null | undefined) =>
|
|
43
|
+
(ip && ip.startsWith('::ffff:'))
|
|
44
|
+
? ip.slice('::ffff:'.length)
|
|
45
|
+
: ip;
|
|
46
|
+
|
|
47
|
+
export const isLocalhostAddress = (host: string | null | undefined) =>
|
|
48
|
+
!!host && ( // Null/undef are something else weird, but not localhost
|
|
49
|
+
host === 'localhost' || // Most common
|
|
50
|
+
host.endsWith('.localhost') ||
|
|
51
|
+
host === '::1' || // IPv6
|
|
52
|
+
normalizeIp(host)!.match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) // 127.0.0.0/8 range
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// Check whether an incoming socket is the other end of one of our outgoing sockets:
|
|
57
|
+
export const isSocketLoop = (outgoingSockets: net.Socket[] | Set<net.Socket>, incomingSocket: net.Socket) =>
|
|
58
|
+
// We effectively just compare the address & port: if they match, we've almost certainly got a loop.
|
|
59
|
+
|
|
60
|
+
// I don't think it's generally possible to see the same ip on different interfaces from one process (you need
|
|
61
|
+
// ip-netns network namespaces), but if it is, then there's a tiny chance of false positives here. If we have ip X,
|
|
62
|
+
// and on another interface somebody else has ip X, and they send a request with the same incoming port as an
|
|
63
|
+
// outgoing request we have on the other interface, we'll assume it's a loop. Extremely unlikely imo.
|
|
64
|
+
|
|
65
|
+
_.some([...outgoingSockets], (outgoingSocket) => {
|
|
66
|
+
if (!outgoingSocket.localAddress || !outgoingSocket.localPort) {
|
|
67
|
+
// It's possible for sockets in outgoingSockets to be closed, in which case these properties
|
|
68
|
+
// will be undefined. If so, we know they're not relevant to loops, so skip entirely.
|
|
69
|
+
return false;
|
|
70
|
+
} else {
|
|
71
|
+
return normalizeIp(outgoingSocket.localAddress) === normalizeIp(incomingSocket.remoteAddress) &&
|
|
72
|
+
outgoingSocket.localPort === incomingSocket.remotePort;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export function getParentSocket(socket: net.Socket) {
|
|
77
|
+
return socket._parent || // TLS wrapper
|
|
78
|
+
socket.stream || // SocketWrapper
|
|
79
|
+
(socket as any)._handle?._parentWrap?.stream; // HTTP/2 CONNECT'd TLS wrapper
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isSocketResetSupported = isNode
|
|
83
|
+
? !!net.Socket.prototype.resetAndDestroy
|
|
84
|
+
: false; // Avoid errors in browsers
|
|
85
|
+
export const requireSocketResetSupport = () => {
|
|
86
|
+
if (!isSocketResetSupported) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
'Connection reset is only supported in Node v16.17+, v18.3.0+, or later'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const isHttp2Stream = (maybeStream: any): maybeStream is http2.Http2ServerRequest =>
|
|
94
|
+
'httpVersion' in maybeStream &&
|
|
95
|
+
maybeStream.httpVersion?.startsWith('2');
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reset the socket where possible, or at least destroy it where that's not possible.
|
|
99
|
+
*
|
|
100
|
+
* This has a few cases for different layers of socket & tunneling, designed to
|
|
101
|
+
* simulate a real connection reset as closely as possible. That means, in general,
|
|
102
|
+
* we unwrap the connection as far as possible whilst still only affecting a single
|
|
103
|
+
* request.
|
|
104
|
+
*
|
|
105
|
+
* In practice, we unwrap HTTP/1 & TLS back as far as we can, until we hit either an
|
|
106
|
+
* HTTP/2 stream or a raw TCP connection. We then either send a RST_FRAME or a TCP RST
|
|
107
|
+
* to kill that connection.
|
|
108
|
+
*/
|
|
109
|
+
export function resetOrDestroy(requestOrSocket:
|
|
110
|
+
| net.Socket
|
|
111
|
+
| OngoingRequest & { socket?: net.Socket }
|
|
112
|
+
| http2.Http2ServerRequest
|
|
113
|
+
) {
|
|
114
|
+
let socket: net.Socket | http2.Http2Stream =
|
|
115
|
+
(isHttp2Stream(requestOrSocket) && requestOrSocket.stream)
|
|
116
|
+
? requestOrSocket.stream
|
|
117
|
+
: ('socket' in requestOrSocket && requestOrSocket.socket)
|
|
118
|
+
? requestOrSocket.socket
|
|
119
|
+
: requestOrSocket as net.Socket;
|
|
120
|
+
|
|
121
|
+
while (socket instanceof tls.TLSSocket) {
|
|
122
|
+
const parent = getParentSocket(socket);
|
|
123
|
+
if (!parent) break; // Not clear why, but it seems in some cases we run out of parents here
|
|
124
|
+
socket = parent;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if ('rstCode' in socket) {
|
|
128
|
+
// It's an HTTP/2 stream instance - let's kill it here.
|
|
129
|
+
|
|
130
|
+
// If it's the innermost stream, i.e. this is the stream of the request we're
|
|
131
|
+
// resetting, then we want to send an internal error. If it's a tunneling
|
|
132
|
+
// stream, then we want to send a CONNECT error:
|
|
133
|
+
const isOuterSocket = socket === (requestOrSocket as any).stream;
|
|
134
|
+
|
|
135
|
+
const errorCode = isOuterSocket
|
|
136
|
+
? http2.constants.NGHTTP2_INTERNAL_ERROR
|
|
137
|
+
: http2.constants.NGHTTP2_CONNECT_ERROR;
|
|
138
|
+
|
|
139
|
+
const h2Stream = socket as http2.ServerHttp2Stream;
|
|
140
|
+
h2Stream.close(errorCode);
|
|
141
|
+
} else {
|
|
142
|
+
// Must be a net.Socket then, so we let's reset it for real:
|
|
143
|
+
if (isSocketResetSupported) {
|
|
144
|
+
try {
|
|
145
|
+
socket.resetAndDestroy!();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// This could fail in funky ways if the socket is not just the right kind
|
|
148
|
+
// of socket. We should still fail in that case, but it's useful to log
|
|
149
|
+
// some extra data first beforehand, so we can fix this if it ever happens:
|
|
150
|
+
console.warn(`Failed to reset on socket of type ${
|
|
151
|
+
socket.constructor.name
|
|
152
|
+
} with parent of type ${getParentSocket(socket as any)?.constructor.name}`);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
socket.destroy();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export function buildSocketEventData(socket: net.Socket & Partial<tls.TLSSocket>): TlsConnectionEvent {
|
|
162
|
+
const timingInfo = socket.__timingInfo ||
|
|
163
|
+
socket._parent?.__timingInfo ||
|
|
164
|
+
buildSocketTimingInfo();
|
|
165
|
+
|
|
166
|
+
// Attached in passThroughMatchingTls TLS sniffing logic in http-combo-server:
|
|
167
|
+
const tlsMetadata = socket.__tlsMetadata ||
|
|
168
|
+
socket._parent?.__tlsMetadata ||
|
|
169
|
+
{};
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
hostname: socket.servername,
|
|
173
|
+
// These only work because of oncertcb monkeypatch in http-combo-server:
|
|
174
|
+
remoteIpAddress: socket.remoteAddress || // Normal case
|
|
175
|
+
socket._parent?.remoteAddress || // Pre-certCB error, e.g. timeout
|
|
176
|
+
socket.initialRemoteAddress!, // Recorded by certCB monkeypatch
|
|
177
|
+
remotePort: socket.remotePort ||
|
|
178
|
+
socket._parent?.remotePort ||
|
|
179
|
+
socket.initialRemotePort!,
|
|
180
|
+
tags: [],
|
|
181
|
+
timingEvents: {
|
|
182
|
+
startTime: timingInfo.initialSocket,
|
|
183
|
+
connectTimestamp: timingInfo.initialSocketTimestamp,
|
|
184
|
+
tunnelTimestamp: timingInfo.tunnelSetupTimestamp,
|
|
185
|
+
handshakeTimestamp: timingInfo.tlsConnectedTimestamp
|
|
186
|
+
},
|
|
187
|
+
tlsMetadata
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function buildSocketTimingInfo(): Required<net.Socket>['__timingInfo'] {
|
|
192
|
+
return { initialSocket: Date.now(), initialSocketTimestamp: now() };
|
|
193
|
+
}
|
package/src/util/tls.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import * as _ from 'lodash';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import { v4 as uuid } from "uuid";
|
|
4
|
+
import * as forge from 'node-forge';
|
|
5
|
+
|
|
6
|
+
const { asn1, pki, md, util } = forge;
|
|
7
|
+
|
|
8
|
+
export type CAOptions = (CertDataOptions | CertPathOptions);
|
|
9
|
+
|
|
10
|
+
export interface CertDataOptions extends BaseCAOptions {
|
|
11
|
+
key: string;
|
|
12
|
+
cert: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export interface CertPathOptions extends BaseCAOptions {
|
|
16
|
+
keyPath: string;
|
|
17
|
+
certPath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BaseCAOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Minimum key length when generating certificates. Defaults to 2048.
|
|
23
|
+
*/
|
|
24
|
+
keyLength?: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The countryName that will be used in the certificate for incoming TLS
|
|
28
|
+
* connections.
|
|
29
|
+
*/
|
|
30
|
+
countryName?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The localityName that will be used in the certificate for incoming TLS
|
|
34
|
+
* connections.
|
|
35
|
+
*/
|
|
36
|
+
localityName?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The organizationName that will be used in the certificate for incoming TLS
|
|
40
|
+
* connections.
|
|
41
|
+
*/
|
|
42
|
+
organizationName?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type PEM = string | string[] | Buffer | Buffer[];
|
|
46
|
+
|
|
47
|
+
export type GeneratedCertificate = {
|
|
48
|
+
key: string,
|
|
49
|
+
cert: string,
|
|
50
|
+
ca: string
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate a CA certificate for mocking HTTPS.
|
|
55
|
+
*
|
|
56
|
+
* Returns a promise, for an object with key and cert properties,
|
|
57
|
+
* containing the generated private key and certificate in PEM format.
|
|
58
|
+
*
|
|
59
|
+
* These can be saved to disk, and their paths passed
|
|
60
|
+
* as HTTPS options to a Mockttp server.
|
|
61
|
+
*/
|
|
62
|
+
export async function generateCACertificate(options: {
|
|
63
|
+
commonName?: string,
|
|
64
|
+
organizationName?: string,
|
|
65
|
+
countryName?: string,
|
|
66
|
+
bits?: number,
|
|
67
|
+
nameConstraints?: {
|
|
68
|
+
permitted?: string[]
|
|
69
|
+
}
|
|
70
|
+
} = {}) {
|
|
71
|
+
options = _.defaults({}, options, {
|
|
72
|
+
commonName: 'Mockttp Testing CA - DO NOT TRUST - TESTING ONLY',
|
|
73
|
+
organizationName: 'Mockttp',
|
|
74
|
+
countryName: 'XX', // ISO-3166-1 alpha-2 'unknown country' code
|
|
75
|
+
bits: 2048,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const keyPair = await new Promise<forge.pki.rsa.KeyPair>((resolve, reject) => {
|
|
79
|
+
pki.rsa.generateKeyPair({ bits: options.bits }, (error, keyPair) => {
|
|
80
|
+
if (error) reject(error);
|
|
81
|
+
else resolve(keyPair);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const cert = pki.createCertificate();
|
|
86
|
+
cert.publicKey = keyPair.publicKey;
|
|
87
|
+
cert.serialNumber = generateSerialNumber();
|
|
88
|
+
|
|
89
|
+
cert.validity.notBefore = new Date();
|
|
90
|
+
// Make it valid for the last 24h - helps in cases where clocks slightly disagree
|
|
91
|
+
cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1);
|
|
92
|
+
|
|
93
|
+
cert.validity.notAfter = new Date();
|
|
94
|
+
// Valid for the next year by default.
|
|
95
|
+
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1);
|
|
96
|
+
|
|
97
|
+
cert.setSubject([
|
|
98
|
+
// All of these are required for a fully valid CA cert that will be accepted when imported anywhere:
|
|
99
|
+
{ name: 'commonName', value: options.commonName },
|
|
100
|
+
{ name: 'countryName', value: options.countryName },
|
|
101
|
+
{ name: 'organizationName', value: options.organizationName }
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const extensions: any[] = [
|
|
105
|
+
{ name: 'basicConstraints', cA: true, critical: true },
|
|
106
|
+
{ name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, cRLSign: true, critical: true },
|
|
107
|
+
{ name: 'subjectKeyIdentifier' },
|
|
108
|
+
];
|
|
109
|
+
const permittedDomains = options.nameConstraints?.permitted || [];
|
|
110
|
+
if(permittedDomains.length > 0) {
|
|
111
|
+
extensions.push({
|
|
112
|
+
critical: true,
|
|
113
|
+
id: '2.5.29.30',
|
|
114
|
+
name: 'nameConstraints',
|
|
115
|
+
value: generateNameConstraints({
|
|
116
|
+
permitted: permittedDomains,
|
|
117
|
+
}),
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
cert.setExtensions(extensions);
|
|
121
|
+
|
|
122
|
+
// Self-issued too
|
|
123
|
+
cert.setIssuer(cert.subject.attributes);
|
|
124
|
+
|
|
125
|
+
// Self-sign the certificate - we're the root
|
|
126
|
+
cert.sign(keyPair.privateKey, md.sha256.create());
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
key: pki.privateKeyToPem(keyPair.privateKey),
|
|
130
|
+
cert: pki.certificateToPem(cert)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
type GenerateNameConstraintsInput = {
|
|
136
|
+
/**
|
|
137
|
+
* Array of permitted domains
|
|
138
|
+
*/
|
|
139
|
+
permitted?: string[];
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate name constraints in conformance with
|
|
144
|
+
* [RFC 5280 § 4.2.1.10](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10)
|
|
145
|
+
*/
|
|
146
|
+
function generateNameConstraints(
|
|
147
|
+
input: GenerateNameConstraintsInput
|
|
148
|
+
): forge.asn1.Asn1 {
|
|
149
|
+
const domainsToSequence = (ips: string[]) =>
|
|
150
|
+
ips.map((domain) => {
|
|
151
|
+
return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
|
|
152
|
+
asn1.create(
|
|
153
|
+
asn1.Class.CONTEXT_SPECIFIC,
|
|
154
|
+
2,
|
|
155
|
+
false,
|
|
156
|
+
util.encodeUtf8(domain)
|
|
157
|
+
),
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const permittedAndExcluded: forge.asn1.Asn1[] = [];
|
|
162
|
+
|
|
163
|
+
if (input.permitted && input.permitted.length > 0) {
|
|
164
|
+
permittedAndExcluded.push(
|
|
165
|
+
asn1.create(
|
|
166
|
+
asn1.Class.CONTEXT_SPECIFIC,
|
|
167
|
+
0,
|
|
168
|
+
true,
|
|
169
|
+
domainsToSequence(input.permitted)
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return asn1.create(
|
|
175
|
+
asn1.Class.UNIVERSAL,
|
|
176
|
+
asn1.Type.SEQUENCE,
|
|
177
|
+
true,
|
|
178
|
+
permittedAndExcluded
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function generateSPKIFingerprint(certPem: PEM) {
|
|
183
|
+
let cert = pki.certificateFromPem(certPem.toString('utf8'));
|
|
184
|
+
return util.encode64(
|
|
185
|
+
pki.getPublicKeyFingerprint(cert.publicKey, {
|
|
186
|
+
type: 'SubjectPublicKeyInfo',
|
|
187
|
+
md: md.sha256.create(),
|
|
188
|
+
encoding: 'binary'
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Generates a unique serial number for a certificate as a hex string:
|
|
194
|
+
function generateSerialNumber() {
|
|
195
|
+
return 'A' + uuid().replace(/-/g, '');
|
|
196
|
+
// We add a leading 'A' to ensure it's always positive (not 'F') and always
|
|
197
|
+
// valid (e.g. leading 000 is bad padding, and would be unparseable).
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function getCA(options: CAOptions): Promise<CA> {
|
|
201
|
+
let certOptions: CertDataOptions;
|
|
202
|
+
if ('key' in options && 'cert' in options) {
|
|
203
|
+
certOptions = options;
|
|
204
|
+
}
|
|
205
|
+
else if ('keyPath' in options && 'certPath' in options) {
|
|
206
|
+
certOptions = await Promise.all([
|
|
207
|
+
fs.readFile(options.keyPath, 'utf8'),
|
|
208
|
+
fs.readFile(options.certPath, 'utf8')
|
|
209
|
+
]).then(([ keyContents, certContents ]) => ({
|
|
210
|
+
..._.omit(options, ['keyPath', 'certPath']),
|
|
211
|
+
key: keyContents,
|
|
212
|
+
cert: certContents
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
throw new Error('Unrecognized https options: you need to provide either a keyPath & certPath, or a key & cert.')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return new CA(certOptions);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// We share a single keypair across all certificates in this process, and
|
|
223
|
+
// instantiate it once when the first CA is created, because it can be
|
|
224
|
+
// expensive (depending on the key length).
|
|
225
|
+
// This would be a terrible idea for a real server, but for a mock server
|
|
226
|
+
// it's ok - if anybody can steal this, they can steal the CA cert anyway.
|
|
227
|
+
let KEY_PAIR: {
|
|
228
|
+
publicKey: forge.pki.rsa.PublicKey,
|
|
229
|
+
privateKey: forge.pki.rsa.PrivateKey,
|
|
230
|
+
length: number
|
|
231
|
+
} | undefined;
|
|
232
|
+
|
|
233
|
+
export class CA {
|
|
234
|
+
private caCert: forge.pki.Certificate;
|
|
235
|
+
private caKey: forge.pki.PrivateKey;
|
|
236
|
+
private options: CertDataOptions;
|
|
237
|
+
|
|
238
|
+
private certCache: { [domain: string]: GeneratedCertificate };
|
|
239
|
+
|
|
240
|
+
constructor(options: CertDataOptions) {
|
|
241
|
+
this.caKey = pki.privateKeyFromPem(options.key.toString());
|
|
242
|
+
this.caCert = pki.certificateFromPem(options.cert.toString());
|
|
243
|
+
this.certCache = {};
|
|
244
|
+
this.options = options ?? {};
|
|
245
|
+
|
|
246
|
+
const keyLength = options.keyLength || 2048;
|
|
247
|
+
|
|
248
|
+
if (!KEY_PAIR || KEY_PAIR.length < keyLength) {
|
|
249
|
+
// If we have no key, or not a long enough one, generate one.
|
|
250
|
+
KEY_PAIR = Object.assign(
|
|
251
|
+
pki.rsa.generateKeyPair(keyLength),
|
|
252
|
+
{ length: keyLength }
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
generateCertificate(domain: string): GeneratedCertificate {
|
|
258
|
+
// TODO: Expire domains from the cache? Based on their actual expiry?
|
|
259
|
+
if (this.certCache[domain]) return this.certCache[domain];
|
|
260
|
+
|
|
261
|
+
if (domain.includes('_')) {
|
|
262
|
+
// TLS certificates cannot cover domains with underscores, bizarrely. More info:
|
|
263
|
+
// https://www.digicert.com/kb/ssl-support/underscores-not-allowed-in-fqdns.htm
|
|
264
|
+
// To fix this, we use wildcards instead. This is only possible for one level of
|
|
265
|
+
// certificate, and only for subdomains, so our options are a little limited, but
|
|
266
|
+
// this should be very rare (because it's not supported elsewhere either).
|
|
267
|
+
const [ , ...otherParts] = domain.split('.');
|
|
268
|
+
if (
|
|
269
|
+
otherParts.length <= 1 || // *.com is never valid
|
|
270
|
+
otherParts.some(p => p.includes('_'))
|
|
271
|
+
) {
|
|
272
|
+
throw new Error(`Cannot generate certificate for domain due to underscores: ${domain}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Replace the first part with a wildcard to solve the problem:
|
|
276
|
+
domain = `*.${otherParts.join('.')}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let cert = pki.createCertificate();
|
|
280
|
+
|
|
281
|
+
cert.publicKey = KEY_PAIR!.publicKey;
|
|
282
|
+
cert.serialNumber = generateSerialNumber();
|
|
283
|
+
|
|
284
|
+
cert.validity.notBefore = new Date();
|
|
285
|
+
// Make it valid for the last 24h - helps in cases where clocks slightly disagree.
|
|
286
|
+
cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1);
|
|
287
|
+
|
|
288
|
+
cert.validity.notAfter = new Date();
|
|
289
|
+
// Valid for the next year by default. TODO: Shorten (and expire the cache) automatically.
|
|
290
|
+
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1);
|
|
291
|
+
|
|
292
|
+
cert.setSubject([
|
|
293
|
+
...(domain[0] === '*'
|
|
294
|
+
? [] // We skip the CN (deprecated, rarely used) for wildcards, since they can't be used here.
|
|
295
|
+
: [{ name: 'commonName', value: domain }]
|
|
296
|
+
),
|
|
297
|
+
{ name: 'countryName', value: this.options?.countryName ?? 'XX' }, // ISO-3166-1 alpha-2 'unknown country' code
|
|
298
|
+
{ name: 'localityName', value: this.options?.localityName ?? 'Unknown' },
|
|
299
|
+
{ name: 'organizationName', value: this.options?.organizationName ?? 'Mockttp Cert - DO NOT TRUST' }
|
|
300
|
+
]);
|
|
301
|
+
cert.setIssuer(this.caCert.subject.attributes);
|
|
302
|
+
|
|
303
|
+
const policyList = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [
|
|
304
|
+
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [
|
|
305
|
+
forge.asn1.create(
|
|
306
|
+
forge.asn1.Class.UNIVERSAL,
|
|
307
|
+
forge.asn1.Type.OID,
|
|
308
|
+
false,
|
|
309
|
+
forge.asn1.oidToDer('2.5.29.32.0').getBytes() // Mark all as Domain Verified
|
|
310
|
+
)
|
|
311
|
+
])
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
cert.setExtensions([
|
|
315
|
+
{ name: 'basicConstraints', cA: false, critical: true },
|
|
316
|
+
{ name: 'keyUsage', digitalSignature: true, keyEncipherment: true, critical: true },
|
|
317
|
+
{ name: 'extKeyUsage', serverAuth: true, clientAuth: true },
|
|
318
|
+
{
|
|
319
|
+
name: 'subjectAltName',
|
|
320
|
+
altNames: [{
|
|
321
|
+
type: 2,
|
|
322
|
+
value: domain
|
|
323
|
+
}]
|
|
324
|
+
},
|
|
325
|
+
{ name: 'certificatePolicies', value: policyList },
|
|
326
|
+
{ name: 'subjectKeyIdentifier' },
|
|
327
|
+
{
|
|
328
|
+
name: 'authorityKeyIdentifier',
|
|
329
|
+
// We have to calculate this ourselves due to
|
|
330
|
+
// https://github.com/digitalbazaar/forge/issues/462
|
|
331
|
+
keyIdentifier: (
|
|
332
|
+
this.caCert as any // generateSubjectKeyIdentifier is missing from node-forge types
|
|
333
|
+
).generateSubjectKeyIdentifier().getBytes()
|
|
334
|
+
}
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
cert.sign(this.caKey, md.sha256.create());
|
|
338
|
+
|
|
339
|
+
const generatedCertificate = {
|
|
340
|
+
key: pki.privateKeyToPem(KEY_PAIR!.privateKey),
|
|
341
|
+
cert: pki.certificateToPem(cert),
|
|
342
|
+
ca: pki.certificateToPem(this.caCert)
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
this.certCache[domain] = generatedCertificate;
|
|
346
|
+
return generatedCertificate;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
|
|
2
|
+
|
|
3
|
+
// Turns T, making all props K required
|
|
4
|
+
export type RequireProps<T, K extends keyof T> =
|
|
5
|
+
Omit<T, K> & Required<Pick<T, K>>;
|
|
6
|
+
|
|
7
|
+
export type MaybePromise<T> = T | Promise<T>;
|
|
8
|
+
|
|
9
|
+
type SubsetKeyOf<T, Ks extends keyof T = keyof T> = Ks;
|
|
10
|
+
export type Replace<T, KV extends { [K in SubsetKeyOf<T, any>]: unknown }> =
|
|
11
|
+
Omit<T, keyof KV> & { [K in keyof KV]: KV[K] };
|
|
12
|
+
|
|
13
|
+
export type Mutable<T> = {
|
|
14
|
+
-readonly [K in keyof T]: T[K]
|
|
15
|
+
}
|
package/src/util/url.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as url from 'url';
|
|
2
|
+
import * as _ from 'lodash';
|
|
3
|
+
|
|
4
|
+
import { nthIndexOf } from './util';
|
|
5
|
+
|
|
6
|
+
// Is this URL fully qualified?
|
|
7
|
+
// Note that this supports only HTTP - no websockets or anything else.
|
|
8
|
+
export const isAbsoluteUrl = (url: string) =>
|
|
9
|
+
url.toLowerCase().startsWith('http://') ||
|
|
10
|
+
url.toLowerCase().startsWith('https://');
|
|
11
|
+
|
|
12
|
+
export const isRelativeUrl = (url: string) =>
|
|
13
|
+
url.startsWith('/');
|
|
14
|
+
|
|
15
|
+
export const isAbsoluteProtocollessUrl = (url: string) =>
|
|
16
|
+
!isAbsoluteUrl(url) && !isRelativeUrl(url);
|
|
17
|
+
|
|
18
|
+
export const getUrlWithoutProtocol = (url: string): string => {
|
|
19
|
+
return url.split('://', 2).slice(-1).join('');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const getPathFromAbsoluteUrl = (url: string) => {
|
|
23
|
+
const pathIndex = nthIndexOf(url, '/', 3);
|
|
24
|
+
if (pathIndex !== -1) {
|
|
25
|
+
return url.slice(pathIndex);
|
|
26
|
+
} else {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const getEffectivePort = (url: { protocol: string | null, port: string | null }) => {
|
|
32
|
+
if (url.port) {
|
|
33
|
+
return parseInt(url.port, 10);
|
|
34
|
+
} else if (url.protocol === 'https:' || url.protocol === 'wss:') {
|
|
35
|
+
return 443;
|
|
36
|
+
} else {
|
|
37
|
+
return 80;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Normalizes URLs to the form used when matching them.
|
|
43
|
+
*
|
|
44
|
+
* This accepts URLs in all three formats: relative, absolute, and protocolless-absolute,
|
|
45
|
+
* and returns them in the same format but normalized.
|
|
46
|
+
*/
|
|
47
|
+
export const normalizeUrl: (url: string) => string =
|
|
48
|
+
_.memoize(
|
|
49
|
+
(urlInput: string): string => {
|
|
50
|
+
let parsedUrl: Partial<url.UrlWithStringQuery> | undefined;
|
|
51
|
+
|
|
52
|
+
let isProtocolless = false;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Strip the query and anything following it
|
|
56
|
+
const queryIndex = urlInput.indexOf('?');
|
|
57
|
+
if (queryIndex !== -1) {
|
|
58
|
+
urlInput = urlInput.slice(0, queryIndex);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isAbsoluteProtocollessUrl(urlInput)) {
|
|
62
|
+
parsedUrl = url.parse('http://' + urlInput);
|
|
63
|
+
isProtocolless = true;
|
|
64
|
+
} else {
|
|
65
|
+
parsedUrl = url.parse(urlInput);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Trim out lots of the bits we don't like:
|
|
69
|
+
delete parsedUrl.host;
|
|
70
|
+
delete parsedUrl.query;
|
|
71
|
+
delete parsedUrl.search;
|
|
72
|
+
delete parsedUrl.hash;
|
|
73
|
+
|
|
74
|
+
if (parsedUrl.pathname) {
|
|
75
|
+
parsedUrl.pathname = parsedUrl.pathname.replace(
|
|
76
|
+
/\%[A-Fa-z0-9]{2}/g,
|
|
77
|
+
(encoded) => encoded.toUpperCase()
|
|
78
|
+
).replace(
|
|
79
|
+
/[^\u0000-\u007F]+/g,
|
|
80
|
+
(unicodeChar) => encodeURIComponent(unicodeChar)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (parsedUrl.hostname && parsedUrl.hostname.endsWith('.')) {
|
|
85
|
+
parsedUrl.hostname = parsedUrl.hostname.slice(0, -1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
(parsedUrl.protocol === 'https:' && parsedUrl.port === '443') ||
|
|
90
|
+
(parsedUrl.protocol === 'http:' && parsedUrl.port === '80')
|
|
91
|
+
) {
|
|
92
|
+
delete parsedUrl.port;
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.log(`Failed to normalize URL ${urlInput}`);
|
|
96
|
+
console.log(e);
|
|
97
|
+
|
|
98
|
+
if (!parsedUrl) return urlInput; // Totally unparseble: use as-is
|
|
99
|
+
// If we've successfully parsed it, we format what we managed
|
|
100
|
+
// and leave it at that:
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let normalizedUrl = url.format(parsedUrl);
|
|
104
|
+
|
|
105
|
+
// If the URL came in with no protocol, it should leave with
|
|
106
|
+
// no protocol (protocol added temporarily above to allow parsing)
|
|
107
|
+
if (isProtocolless) {
|
|
108
|
+
normalizedUrl = normalizedUrl.slice('http://'.length);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return normalizedUrl;
|
|
112
|
+
}
|
|
113
|
+
);
|