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