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.

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