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,619 @@
1
+ import * as _ from 'lodash';
2
+ import * as express from 'express';
3
+ import * as cors from 'cors';
4
+ import corsGate = require('cors-gate');
5
+ import * as http from 'http';
6
+ import * as net from 'net';
7
+ import * as bodyParser from 'body-parser';
8
+ import * as Ws from 'ws';
9
+ import { v4 as uuid } from "uuid";
10
+
11
+ import { createHandler as createGraphQLHandler } from 'graphql-http/lib/use/express';
12
+ import { execute, formatError, GraphQLScalarType, subscribe } from 'graphql';
13
+ import gql from 'graphql-tag';
14
+ import { makeExecutableSchema } from '@graphql-tools/schema';
15
+ import { SubscriptionServer } from '@httptoolkit/subscriptions-transport-ws';
16
+ import { EventEmitter } from 'stream';
17
+ import DuplexPair = require('native-duplexpair');
18
+
19
+ import { makeDestroyable, DestroyableServer } from "destroyable-server";
20
+ import { isErrorLike } from '../util/error';
21
+ import { objectAllPromise } from '../util/promise';
22
+
23
+ import { DEFAULT_ADMIN_SERVER_PORT } from '../types';
24
+
25
+ import { RuleParameters } from '../rules/rule-parameters';
26
+ import { AdminPlugin, PluginConstructorMap, PluginStartParamsMap } from './admin-plugin-types';
27
+ import { parseAnyAst } from './graphql-utils';
28
+ import { MockttpAdminPlugin } from './mockttp-admin-plugin';
29
+
30
+ export interface AdminServerOptions<Plugins extends { [key: string]: AdminPlugin<any, any> }> {
31
+ /**
32
+ * Should the admin server print extra debug information? This enables admin server debugging
33
+ * only - individual mock session debugging must be enabled separately.
34
+ */
35
+ debug?: boolean;
36
+
37
+ /**
38
+ * Set CORS options to limit the sites which can send requests to manage this admin server.
39
+ */
40
+ corsOptions?: cors.CorsOptions & {
41
+ strict?: boolean,
42
+ allowPrivateNetworkAccess?: boolean
43
+ };
44
+
45
+ /**
46
+ * Set a keep alive frequency in milliseconds for the subscription & stream websockets of each
47
+ * session, to ensure they remain connected in long-lived connections, especially in browsers which
48
+ * often close quiet background connections automatically.
49
+ */
50
+ webSocketKeepAlive?: number;
51
+
52
+ /**
53
+ * Override the default parameters for sessions started from this admin server. These values will be
54
+ * used for each setting that is not explicitly specified by the client when creating a mock session.
55
+ */
56
+ pluginDefaults?: Partial<PluginStartParamsMap<Plugins>>;
57
+
58
+ /**
59
+ * Some rule options can't easily be specified in remote clients, since they need to access
60
+ * server-side state or Node APIs directly. To handle this, referenceable parameters can
61
+ * be provided here, and referenced with a `{ [MOCKTTP_PARAM_REF]: <value> }` value in place
62
+ * of the real parameter in the remote client.
63
+ */
64
+ ruleParameters?: {
65
+ [key: string]: any
66
+ }
67
+
68
+ /**
69
+ * @internal
70
+ *
71
+ * This API is not yet stable, and is intended for internal use only. It may change in future
72
+ * in minor versions without warning.
73
+ *
74
+ * This defines admin plugin modules: remote-controlled types of mocks that should be attached to this
75
+ * admin server, to allow configuring other mocking services through the same HTTP infrastructure.
76
+ *
77
+ * This can be useful when mocking non-HTTP protocols like WebRTC.
78
+ */
79
+ adminPlugins?: PluginConstructorMap<Plugins>
80
+ }
81
+
82
+ async function strictOriginMatch(
83
+ origin: string | undefined,
84
+ expectedOrigin: cors.CorsOptions['origin']
85
+ ): Promise<boolean> {
86
+ if (!origin) return false;
87
+
88
+ if (typeof expectedOrigin === 'string') {
89
+ return expectedOrigin === origin;
90
+ }
91
+
92
+ if (_.isRegExp(expectedOrigin)) {
93
+ return !!origin.match(expectedOrigin);
94
+ }
95
+
96
+ if (_.isArray(expectedOrigin)) {
97
+ return _.some(expectedOrigin, (exp) =>
98
+ strictOriginMatch(origin, exp)
99
+ );
100
+ }
101
+
102
+ if (_.isFunction(expectedOrigin)) {
103
+ return new Promise<boolean>((resolve, reject) => {
104
+ expectedOrigin(origin, (error, result) => {
105
+ if (error) reject(error);
106
+ else resolve(strictOriginMatch(origin, result));
107
+ });
108
+ });
109
+ }
110
+
111
+ // We don't allow boolean or undefined matches
112
+ return false;
113
+ }
114
+
115
+ export class AdminServer<Plugins extends { [key: string]: AdminPlugin<any, any> }> {
116
+
117
+ private debug: boolean;
118
+ private requiredOrigin: cors.CorsOptions['origin'] | false;
119
+ private webSocketKeepAlive: number | undefined;
120
+ private ruleParams: RuleParameters;
121
+
122
+ private app = express();
123
+ private server: DestroyableServer<http.Server> | null = null;
124
+ private eventEmitter = new EventEmitter();
125
+
126
+ private adminPlugins: PluginConstructorMap<Plugins>;
127
+
128
+ private sessions: { [id: string]: {
129
+ router: express.Router,
130
+ stop: () => Promise<void>,
131
+
132
+ subscriptionServer: SubscriptionServer,
133
+ streamServer: Ws.Server,
134
+
135
+ sessionPlugins: Plugins
136
+ } } = { };
137
+
138
+ constructor(options: AdminServerOptions<Plugins> = {}) {
139
+ this.debug = options.debug || false;
140
+ if (this.debug) console.log('Admin server started in debug mode');
141
+
142
+ this.webSocketKeepAlive = options.webSocketKeepAlive || undefined;
143
+ this.ruleParams = options.ruleParameters || {};
144
+ this.adminPlugins = options.adminPlugins || {} as PluginConstructorMap<Plugins>;
145
+
146
+ if (options.corsOptions?.allowPrivateNetworkAccess) {
147
+ // Allow web pages on non-local URLs (testsite.example.com, not localhost) to
148
+ // send requests to this admin server too. Without this, those requests will
149
+ // fail after rejected preflights in recent Chrome (from ~v102, ish? Unclear).
150
+ // This is combined with the origin restrictions that may be set, so only
151
+ // accepted origins will be allowed to make these requests.
152
+ this.app.use((req, res, next) => {
153
+ if (req.headers["access-control-request-private-network"]) {
154
+ res.setHeader("access-control-allow-private-network", "true");
155
+ }
156
+ next(null);
157
+ });
158
+ }
159
+
160
+ this.app.use(cors(options.corsOptions));
161
+
162
+ // If you use strict CORS, and set a specific origin, we'll enforce it:
163
+ this.requiredOrigin = !!options.corsOptions &&
164
+ !!options.corsOptions.strict &&
165
+ !!options.corsOptions.origin &&
166
+ typeof options.corsOptions.origin !== 'boolean' &&
167
+ options.corsOptions.origin;
168
+
169
+ if (this.requiredOrigin) {
170
+ this.app.use(corsGate({
171
+ strict: true, // MUST send an allowed origin
172
+ allowSafe: false, // Even for HEAD/GET requests (should be none anyway)
173
+ origin: '' // No base origin - we accept *no* same-origin requests
174
+ }));
175
+ }
176
+
177
+ this.app.use(bodyParser.json({ limit: '50mb' }));
178
+
179
+ const defaultPluginStartParams: Partial<PluginStartParamsMap<Plugins>> = options.pluginDefaults ?? {};
180
+
181
+ this.app.post('/start', async (req, res) => {
182
+ try {
183
+ const rawConfig = req.body;
184
+
185
+ // New clients send: "{ plugins: { http: {...}, webrtc: {...} } }" etc. Old clients just send
186
+ // the HTTP options bare with no wrapper, so we wrap them for backward compat.
187
+ const isPluginAwareClient = ('plugins' in rawConfig);
188
+
189
+ const providedPluginStartParams = (!isPluginAwareClient
190
+ ? { // Backward compat: this means the client is not plugin-aware, and so all options are Mockttp options
191
+ http: {
192
+ options: _.cloneDeep(rawConfig),
193
+ port: (typeof req.query.port === 'string')
194
+ ? JSON.parse(req.query.port)
195
+ : undefined
196
+ }
197
+ }
198
+ : rawConfig.plugins
199
+ ) as PluginStartParamsMap<Plugins>;
200
+
201
+ // For each plugin that was specified, we pull default params into their start params.
202
+ const pluginStartParams = _.mapValues((providedPluginStartParams), (params, pluginId) => {
203
+ return _.merge({}, defaultPluginStartParams[pluginId], params);
204
+ });
205
+
206
+ if (this.debug) console.log('Admin server starting mock session with config', pluginStartParams);
207
+
208
+ // Backward compat: do an explicit check for HTTP port conflicts
209
+ const httpPort = (pluginStartParams as { http?: { port: number } }).http?.port;
210
+ if (_.isNumber(httpPort) && this.sessions[httpPort] != null) {
211
+ res.status(409).json({
212
+ error: `Cannot start: mock server is already running on port ${httpPort}`
213
+ });
214
+ return;
215
+ }
216
+
217
+ const missingPluginId = Object.keys(pluginStartParams).find(pluginId => !(pluginId in this.adminPlugins));
218
+ if (missingPluginId) {
219
+ res.status(400).json({
220
+ error: `Request to mock using unrecognized plugin: ${missingPluginId}`
221
+ });
222
+ return;
223
+ }
224
+
225
+ const sessionPlugins = _.mapValues(pluginStartParams, (__, pluginId: keyof Plugins) => {
226
+ const PluginType = this.adminPlugins[pluginId];
227
+ return new PluginType();
228
+ }) as Plugins;
229
+
230
+ const pluginStartResults = await objectAllPromise(
231
+ _.mapValues(sessionPlugins, (plugin, pluginId: keyof Plugins) =>
232
+ plugin.start(pluginStartParams[pluginId])
233
+ )
234
+ );
235
+
236
+ // More backward compat: old clients assume that the port is also the management id.
237
+ const sessionId = isPluginAwareClient
238
+ ? uuid()
239
+ : (sessionPlugins as any as {
240
+ 'http': MockttpAdminPlugin
241
+ }).http.getMockServer().port.toString();
242
+
243
+ await this.startSessionManagementAPI(sessionId, sessionPlugins);
244
+
245
+ if (isPluginAwareClient) {
246
+ res.json({
247
+ id: sessionId,
248
+ pluginData: _.mapValues(pluginStartResults, (r: unknown) =>
249
+ r ?? {} // Always return _something_, even if the plugin returns null/undefined.
250
+ )
251
+ });
252
+ } else {
253
+ res.json({
254
+ id: sessionId,
255
+ ...(pluginStartResults['http']!)
256
+ });
257
+ }
258
+ } catch (e) {
259
+ res.status(500).json({ error: `Failed to start mock session: ${
260
+ (isErrorLike(e) && e.message) || e
261
+ }` });
262
+ }
263
+ });
264
+
265
+ this.app.post('/reset', async (req, res) => {
266
+ try {
267
+ await this.resetAdminServer();
268
+ res.json({ success: true });
269
+ } catch (e) {
270
+ res.status(500).json({
271
+ error: (isErrorLike(e) && e.message) || 'Unknown error'
272
+ });
273
+ }
274
+ });
275
+
276
+
277
+ // Dynamically route to mock sessions ourselves, so we can easily add/remove
278
+ // sessions as we see fit later on.
279
+ const sessionRequest = (req: express.Request, res: express.Response, next: express.NextFunction) => {
280
+ const sessionId = req.params.id;
281
+ const sessionRouter = this.sessions[sessionId]?.router;
282
+
283
+ if (!sessionRouter) {
284
+ res.status(404).send('Unknown mock session');
285
+ console.error(`Request for unknown mock session with id: ${sessionId}`);
286
+ return;
287
+ }
288
+
289
+ sessionRouter(req, res, next);
290
+ }
291
+
292
+ this.app.use('/session/:id/', sessionRequest);
293
+ this.app.use('/server/:id/', sessionRequest); // Old URL for backward compat
294
+ }
295
+
296
+ async resetAdminServer() {
297
+ if (this.debug) console.log('Resetting admin server');
298
+ await Promise.all(
299
+ Object.values(this.sessions).map(({ stop }) => stop())
300
+ );
301
+ }
302
+
303
+ /**
304
+ * Subscribe to hear when each mock ession is started. The listener is provided the
305
+ * session plugin data, which can be used to log session startup, add side-effects that
306
+ * run elsewhere at startup, or preconfigure every started plugin in addition ways.
307
+ *
308
+ * This is run synchronously when a session is created, after it has fully started
309
+ * but before its been returned to remote clients.
310
+ */
311
+ on(event: 'mock-session-started', listener: (plugins: Plugins, sessionId: string) => void): void;
312
+
313
+ /**
314
+ * Subscribe to hear when a mock session is stopped. The listener is provided with
315
+ * the state of all plugins that are about to be stopped. This can be used to log
316
+ * mock session shutdown, add side-effects that run elsewhere at shutdown, or clean
317
+ * up after sessions in other ways.
318
+ *
319
+ * This is run synchronously immediately before the session is shutdown, whilst all
320
+ * its state is still available, and before remote clients have had any response to
321
+ * their request. This is also run before shutdown when the admin server itself is
322
+ * cleanly shutdown with `adminServer.stop()`.
323
+ */
324
+ on(event: 'mock-session-stopping', listener: (plugins: Plugins, sessionId: string) => void): void;
325
+ on(event: string, listener: (...args: any) => void): void {
326
+ this.eventEmitter.on(event, listener);
327
+ }
328
+
329
+ async start(
330
+ listenOptions: number | {
331
+ port: number,
332
+ host: string
333
+ } = DEFAULT_ADMIN_SERVER_PORT
334
+ ) {
335
+ if (this.server) throw new Error('Admin server already running');
336
+
337
+ await new Promise<void>((resolve, reject) => {
338
+ this.server = makeDestroyable(this.app.listen(listenOptions, resolve));
339
+
340
+ this.server.on('error', reject);
341
+
342
+ this.server.on('upgrade', async (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
343
+ const reqOrigin = req.headers['origin'] as string | undefined;
344
+ if (this.requiredOrigin && !await strictOriginMatch(reqOrigin, this.requiredOrigin)) {
345
+ console.warn(`Websocket request from invalid origin: ${req.headers['origin']}`);
346
+ socket.destroy();
347
+ return;
348
+ }
349
+
350
+ const isSubscriptionRequest = req.url!.match(/^\/(?:server|session)\/([\w\d\-]+)\/subscription$/);
351
+ const isStreamRequest = req.url!.match(/^\/(?:server|session)\/([\w\d\-]+)\/stream$/);
352
+ const isMatch = isSubscriptionRequest || isStreamRequest;
353
+
354
+ if (isMatch) {
355
+ const sessionId = isMatch[1];
356
+
357
+ let wsServer: Ws.Server = isSubscriptionRequest
358
+ ? this.sessions[sessionId]?.subscriptionServer.server
359
+ : this.sessions[sessionId]?.streamServer;
360
+
361
+ if (wsServer) {
362
+ wsServer.handleUpgrade(req, socket, head, (ws) => {
363
+ wsServer.emit('connection', ws, req);
364
+ });
365
+ } else {
366
+ console.warn(`Websocket request for unrecognized mock session: ${sessionId}`);
367
+ socket.destroy();
368
+ }
369
+ } else {
370
+ console.warn(`Unrecognized websocket request for ${req.url}`);
371
+ socket.destroy();
372
+ }
373
+ });
374
+ });
375
+ }
376
+
377
+ private async startSessionManagementAPI(sessionId: string, plugins: Plugins): Promise<void> {
378
+ const mockSessionRouter = express.Router();
379
+
380
+ let running = true;
381
+ const stopSession = async () => {
382
+ if (!running) return;
383
+ running = false;
384
+
385
+ this.eventEmitter.emit('mock-session-stopping', plugins);
386
+
387
+ const session = this.sessions[sessionId];
388
+ delete this.sessions[sessionId];
389
+
390
+ await Promise.all(Object.values(plugins).map(plugin => plugin.stop()));
391
+
392
+ session.subscriptionServer.close();
393
+
394
+ // Close with code 1000 (purpose is complete - no more streaming happening)
395
+ session.streamServer.clients.forEach((client) => {
396
+ client.close(1000);
397
+ });
398
+ session.streamServer.close();
399
+ session.streamServer.emit('close');
400
+ };
401
+
402
+ mockSessionRouter.post('/stop', async (req, res) => {
403
+ await stopSession();
404
+ res.json({ success: true });
405
+ });
406
+
407
+ // A pair of sockets, representing the 2-way connection between the session & WSs.
408
+ // All websocket messages are written to wsSocket, and then read from sessionSocket
409
+ // All session messages are written to sessionSocket, and then read from wsSocket and sent
410
+ const { socket1: wsSocket, socket2: sessionSocket } = new DuplexPair();
411
+
412
+ // This receives a lot of listeners! One channel per matcher, handler & completion checker,
413
+ // and each adds listeners for data/error/finish/etc. That's OK, it's not generally a leak,
414
+ // but maybe 100 would be a bit suspicious (unless you have 30+ active rules).
415
+ sessionSocket.setMaxListeners(100);
416
+
417
+ if (this.debug) {
418
+ sessionSocket.on('data', (d: any) => {
419
+ console.log('Streaming data from WS clients:', d.toString());
420
+ });
421
+ wsSocket.on('data', (d: any) => {
422
+ console.log('Streaming data to WS clients:', d.toString());
423
+ });
424
+ }
425
+
426
+ const streamServer = new Ws.Server({ noServer: true });
427
+ streamServer.on('connection', (ws) => {
428
+ let newClientStream = Ws.createWebSocketStream(ws, {});
429
+ wsSocket.pipe(newClientStream).pipe(wsSocket, { end: false });
430
+
431
+ const unpipe = () => {
432
+ wsSocket.unpipe(newClientStream);
433
+ newClientStream.unpipe(wsSocket);
434
+ };
435
+
436
+ newClientStream.on('error', unpipe);
437
+ wsSocket.on('end', unpipe);
438
+ });
439
+
440
+ streamServer.on('close', () => {
441
+ wsSocket.end();
442
+ sessionSocket.end();
443
+ });
444
+
445
+ // Handle errors by logging & stopping this session
446
+ const onStreamError = (e: Error) => {
447
+ if (!running) return; // We don't care about connection issues during shutdown
448
+ console.error("Error in admin server stream, shutting down mock session");
449
+ console.error(e);
450
+ stopSession();
451
+ };
452
+ wsSocket.on('error', onStreamError);
453
+ sessionSocket.on('error', onStreamError);
454
+
455
+ const schema = makeExecutableSchema({
456
+ typeDefs: [
457
+ AdminServer.baseSchema,
458
+ ...Object.values(plugins).map(plugin => plugin.schema)
459
+ ],
460
+ resolvers: [
461
+ this.buildBaseResolvers(sessionId),
462
+ ...Object.values(plugins).map(plugin =>
463
+ plugin.buildResolvers(sessionSocket, this.ruleParams)
464
+ )
465
+ ]
466
+ });
467
+
468
+ const subscriptionServer = SubscriptionServer.create({
469
+ schema,
470
+ execute,
471
+ subscribe,
472
+ keepAlive: this.webSocketKeepAlive
473
+ }, {
474
+ noServer: true
475
+ });
476
+
477
+ mockSessionRouter.use(
478
+ createGraphQLHandler({
479
+ schema,
480
+
481
+ // Add console logging of all GQL errors:
482
+ formatError: (error) => {
483
+ console.error(error.stack);
484
+ return error;
485
+ }
486
+ }
487
+ ));
488
+
489
+ if (this.webSocketKeepAlive) {
490
+ // If we have a keep-alive set, send the client a ping frame every Xms to
491
+ // try and stop closes (especially by browsers) due to inactivity.
492
+ const webSocketKeepAlive = setInterval(() => {
493
+ [
494
+ ...streamServer.clients,
495
+ ...subscriptionServer.server.clients
496
+ ].forEach((client) => {
497
+ if (client.readyState !== Ws.OPEN) return;
498
+ client.ping();
499
+ });
500
+ }, this.webSocketKeepAlive);
501
+
502
+ // We use the stream server's shutdown as an easy proxy event for full shutdown:
503
+ streamServer.on('close', () => clearInterval(webSocketKeepAlive));
504
+ }
505
+
506
+ this.sessions[sessionId] = {
507
+ sessionPlugins: plugins,
508
+ router: mockSessionRouter,
509
+ streamServer,
510
+ subscriptionServer,
511
+ stop: stopSession
512
+ };
513
+
514
+ this.eventEmitter.emit('mock-session-started', plugins, sessionId);
515
+ }
516
+
517
+ stop(): Promise<void> {
518
+ if (!this.server) return Promise.resolve();
519
+
520
+ return Promise.all([
521
+ this.server.destroy(),
522
+ ].concat(
523
+ Object.values(this.sessions).map((s) => s.stop())
524
+ )).then(() => {
525
+ this.server = null;
526
+ });
527
+ }
528
+
529
+ private static baseSchema = gql`
530
+ type Mutation {
531
+ reset: Void
532
+ enableDebug: Void
533
+ }
534
+
535
+ type Query {
536
+ ruleParameterKeys: [String!]!
537
+ }
538
+
539
+ type Subscription {
540
+ _empty_placeholder_: Void # A placeholder so we can define an empty extendable type
541
+ }
542
+
543
+ scalar Void
544
+ scalar Raw
545
+ scalar Json
546
+ scalar Buffer
547
+ `;
548
+
549
+ private buildBaseResolvers(sessionId: string) {
550
+ return {
551
+ Query: {
552
+ ruleParameterKeys: () => this.ruleParameterKeys
553
+ },
554
+
555
+ Mutation: {
556
+ reset: () => this.resetPluginsForSession(sessionId),
557
+ enableDebug: () => this.enableDebugForSession(sessionId)
558
+ },
559
+
560
+ Raw: new GraphQLScalarType({
561
+ name: 'Raw',
562
+ description: 'A raw entity, serialized directly (must be JSON-compatible)',
563
+ serialize: (value: any) => value,
564
+ parseValue: (input: string): any => input,
565
+ parseLiteral: parseAnyAst
566
+ }),
567
+
568
+ // Json exists just for API backward compatibility - all new data should be Raw.
569
+ // Converting to JSON is pointless, since bodies all contain JSON anyway.
570
+ Json: new GraphQLScalarType({
571
+ name: 'Json',
572
+ description: 'A JSON entity, serialized as a simple JSON string',
573
+ serialize: (value: any) => JSON.stringify(value),
574
+ parseValue: (input: string): any => JSON.parse(input),
575
+ parseLiteral: parseAnyAst
576
+ }),
577
+
578
+ Void: new GraphQLScalarType({
579
+ name: 'Void',
580
+ description: 'Nothing at all',
581
+ serialize: (value: any) => null,
582
+ parseValue: (input: string): any => null,
583
+ parseLiteral: (): any => { throw new Error('Void literals are not supported') }
584
+ }),
585
+
586
+ Buffer: new GraphQLScalarType({
587
+ name: 'Buffer',
588
+ description: 'A buffer',
589
+ serialize: (value: Buffer) => {
590
+ return value.toString('base64');
591
+ },
592
+ parseValue: (input: string) => {
593
+ return Buffer.from(input, 'base64');
594
+ },
595
+ parseLiteral: parseAnyAst
596
+ })
597
+ };
598
+ };
599
+
600
+ private resetPluginsForSession(sessionId: string) {
601
+ return Promise.all(
602
+ Object.values(this.sessions[sessionId].sessionPlugins).map(plugin =>
603
+ plugin.reset?.()
604
+ )
605
+ );
606
+ }
607
+
608
+ private enableDebugForSession(sessionId: string) {
609
+ return Promise.all(
610
+ Object.values(this.sessions[sessionId].sessionPlugins).map(plugin =>
611
+ plugin.enableDebug?.()
612
+ )
613
+ );
614
+ }
615
+
616
+ get ruleParameterKeys() {
617
+ return Object.keys(this.ruleParams);
618
+ }
619
+ }
@@ -0,0 +1,28 @@
1
+ import * as _ from 'lodash';
2
+ import { Kind, ObjectValueNode, ValueNode } from "graphql";
3
+
4
+ export function astToObject<T>(ast: ObjectValueNode): T {
5
+ return <T> _.zipObject(
6
+ ast.fields.map((f) => f.name.value),
7
+ ast.fields.map((f) => parseAnyAst(f.value))
8
+ );
9
+ }
10
+
11
+ export function parseAnyAst(ast: ValueNode): any {
12
+ switch (ast.kind) {
13
+ case Kind.OBJECT:
14
+ return astToObject<any>(ast);
15
+ case Kind.LIST:
16
+ return ast.values.map(parseAnyAst);
17
+ case Kind.BOOLEAN:
18
+ case Kind.ENUM:
19
+ case Kind.FLOAT:
20
+ case Kind.INT:
21
+ case Kind.STRING:
22
+ return ast.value;
23
+ case Kind.NULL:
24
+ return null;
25
+ case Kind.VARIABLE:
26
+ throw new Error("No idea what parsing a 'variable' means");
27
+ }
28
+ }