@mbtest/mountebank 2.9.2-beta.9050

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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/bin/mb +136 -0
  4. package/package.json +71 -0
  5. package/releases.json +52 -0
  6. package/src/cli/api.js +112 -0
  7. package/src/cli/cli.js +420 -0
  8. package/src/controllers/configController.js +64 -0
  9. package/src/controllers/feedController.js +115 -0
  10. package/src/controllers/homeController.js +58 -0
  11. package/src/controllers/imposterController.js +328 -0
  12. package/src/controllers/impostersController.js +215 -0
  13. package/src/controllers/logsController.js +52 -0
  14. package/src/models/behaviors.js +553 -0
  15. package/src/models/behaviorsValidator.js +186 -0
  16. package/src/models/compatibility.js +133 -0
  17. package/src/models/dryRunValidator.js +261 -0
  18. package/src/models/filesystemBackedImpostersRepository.js +908 -0
  19. package/src/models/http/baseHttpServer.js +207 -0
  20. package/src/models/http/headersMap.js +87 -0
  21. package/src/models/http/httpProxy.js +230 -0
  22. package/src/models/http/httpRequest.js +82 -0
  23. package/src/models/http/httpServer.js +18 -0
  24. package/src/models/http/index.js +18 -0
  25. package/src/models/https/cert/mb-cert.pem +20 -0
  26. package/src/models/https/cert/mb-csr.pem +16 -0
  27. package/src/models/https/cert/mb-key.pem +27 -0
  28. package/src/models/https/httpsServer.js +42 -0
  29. package/src/models/https/index.js +18 -0
  30. package/src/models/imposter.js +243 -0
  31. package/src/models/imposterPrinter.js +120 -0
  32. package/src/models/impostersRepository.js +49 -0
  33. package/src/models/inMemoryImpostersRepository.js +418 -0
  34. package/src/models/jsonpath.js +44 -0
  35. package/src/models/mbConnection.js +107 -0
  36. package/src/models/predicates.js +438 -0
  37. package/src/models/protocols.js +242 -0
  38. package/src/models/responseResolver.js +398 -0
  39. package/src/models/smtp/index.js +16 -0
  40. package/src/models/smtp/smtpRequest.js +60 -0
  41. package/src/models/smtp/smtpServer.js +109 -0
  42. package/src/models/tcp/index.js +18 -0
  43. package/src/models/tcp/tcpProxy.js +110 -0
  44. package/src/models/tcp/tcpRequest.js +23 -0
  45. package/src/models/tcp/tcpServer.js +156 -0
  46. package/src/models/tcp/tcpValidator.js +19 -0
  47. package/src/models/xpath.js +95 -0
  48. package/src/mountebank.js +245 -0
  49. package/src/public/images/arrow_down.png +0 -0
  50. package/src/public/images/arrow_up.png +0 -0
  51. package/src/public/images/book.jpg +0 -0
  52. package/src/public/images/dataflow.png +0 -0
  53. package/src/public/images/favicon.ico +0 -0
  54. package/src/public/images/forkme_right_orange_ff7600.png +0 -0
  55. package/src/public/images/mountebank.png +0 -0
  56. package/src/public/images/overview.gif +0 -0
  57. package/src/public/images/quote.png +0 -0
  58. package/src/public/images/tw-logo.png +0 -0
  59. package/src/public/scripts/jquery/jquery-3.6.1.min.js +2 -0
  60. package/src/public/scripts/urlHashHandler.js +31 -0
  61. package/src/public/stylesheets/application.css +424 -0
  62. package/src/public/stylesheets/ie.css +14 -0
  63. package/src/public/stylesheets/imposters.css +121 -0
  64. package/src/public/stylesheets/jqueryui/1.10.4/themes/smoothness/jquery-ui.css +1178 -0
  65. package/src/util/combinators.js +68 -0
  66. package/src/util/date.js +51 -0
  67. package/src/util/errors.js +55 -0
  68. package/src/util/helpers.js +131 -0
  69. package/src/util/inherit.js +28 -0
  70. package/src/util/ip.js +54 -0
  71. package/src/util/logger.js +83 -0
  72. package/src/util/middleware.js +256 -0
  73. package/src/util/scopedLogger.js +47 -0
  74. package/src/views/_footer.ejs +20 -0
  75. package/src/views/_header.ejs +113 -0
  76. package/src/views/_imposter.ejs +8 -0
  77. package/src/views/config.ejs +71 -0
  78. package/src/views/docs/api/behaviors/copy.ejs +427 -0
  79. package/src/views/docs/api/behaviors/decorate.ejs +182 -0
  80. package/src/views/docs/api/behaviors/lookup.ejs +220 -0
  81. package/src/views/docs/api/behaviors/shellTransform.ejs +153 -0
  82. package/src/views/docs/api/behaviors/wait.ejs +121 -0
  83. package/src/views/docs/api/behaviors.ejs +141 -0
  84. package/src/views/docs/api/contracts/addStub-description.ejs +10 -0
  85. package/src/views/docs/api/contracts/addStub.ejs +10 -0
  86. package/src/views/docs/api/contracts/config-description.ejs +32 -0
  87. package/src/views/docs/api/contracts/config.ejs +23 -0
  88. package/src/views/docs/api/contracts/home-description.ejs +18 -0
  89. package/src/views/docs/api/contracts/home.ejs +13 -0
  90. package/src/views/docs/api/contracts/imposter-description.ejs +439 -0
  91. package/src/views/docs/api/contracts/imposter.ejs +182 -0
  92. package/src/views/docs/api/contracts/imposters-description.ejs +13 -0
  93. package/src/views/docs/api/contracts/imposters.ejs +13 -0
  94. package/src/views/docs/api/contracts/logs-description.ejs +3 -0
  95. package/src/views/docs/api/contracts/logs.ejs +14 -0
  96. package/src/views/docs/api/contracts/stub-description.ejs +4 -0
  97. package/src/views/docs/api/contracts/stub.ejs +7 -0
  98. package/src/views/docs/api/contracts/stubs-description.ejs +4 -0
  99. package/src/views/docs/api/contracts/stubs.ejs +11 -0
  100. package/src/views/docs/api/contracts.ejs +133 -0
  101. package/src/views/docs/api/errors.ejs +64 -0
  102. package/src/views/docs/api/fault/connectionReset.ejs +31 -0
  103. package/src/views/docs/api/fault/randomDataThenClose.ejs +31 -0
  104. package/src/views/docs/api/faults.ejs +57 -0
  105. package/src/views/docs/api/injection.ejs +426 -0
  106. package/src/views/docs/api/json.ejs +205 -0
  107. package/src/views/docs/api/jsonpath.ejs +210 -0
  108. package/src/views/docs/api/mocks.ejs +130 -0
  109. package/src/views/docs/api/overview.ejs +968 -0
  110. package/src/views/docs/api/predicates/and.ejs +62 -0
  111. package/src/views/docs/api/predicates/contains.ejs +64 -0
  112. package/src/views/docs/api/predicates/deepEquals.ejs +114 -0
  113. package/src/views/docs/api/predicates/endsWith.ejs +66 -0
  114. package/src/views/docs/api/predicates/equals.ejs +125 -0
  115. package/src/views/docs/api/predicates/exists.ejs +118 -0
  116. package/src/views/docs/api/predicates/inject.ejs +67 -0
  117. package/src/views/docs/api/predicates/matches.ejs +66 -0
  118. package/src/views/docs/api/predicates/not.ejs +52 -0
  119. package/src/views/docs/api/predicates/or.ejs +79 -0
  120. package/src/views/docs/api/predicates/startsWith.ejs +62 -0
  121. package/src/views/docs/api/predicates.ejs +382 -0
  122. package/src/views/docs/api/proxies.ejs +191 -0
  123. package/src/views/docs/api/proxy/addDecorateBehavior.ejs +115 -0
  124. package/src/views/docs/api/proxy/addWaitBehavior.ejs +96 -0
  125. package/src/views/docs/api/proxy/injectHeaders.ejs +91 -0
  126. package/src/views/docs/api/proxy/predicateGenerators.ejs +600 -0
  127. package/src/views/docs/api/proxy/proxyModes.ejs +495 -0
  128. package/src/views/docs/api/stubs.ejs +391 -0
  129. package/src/views/docs/api/xpath.ejs +281 -0
  130. package/src/views/docs/cli/configFiles.ejs +133 -0
  131. package/src/views/docs/cli/customFormatters.ejs +53 -0
  132. package/src/views/docs/cli/help.ejs +6 -0
  133. package/src/views/docs/cli/replay.ejs +42 -0
  134. package/src/views/docs/cli/restart.ejs +10 -0
  135. package/src/views/docs/cli/save.ejs +68 -0
  136. package/src/views/docs/cli/start.ejs +234 -0
  137. package/src/views/docs/cli/stop.ejs +32 -0
  138. package/src/views/docs/commandLine.ejs +93 -0
  139. package/src/views/docs/communityExtensions.ejs +233 -0
  140. package/src/views/docs/gettingStarted.ejs +146 -0
  141. package/src/views/docs/mentalModel.ejs +51 -0
  142. package/src/views/docs/protocols/custom.ejs +231 -0
  143. package/src/views/docs/protocols/http.ejs +238 -0
  144. package/src/views/docs/protocols/https.ejs +246 -0
  145. package/src/views/docs/protocols/smtp.ejs +142 -0
  146. package/src/views/docs/protocols/tcp.ejs +431 -0
  147. package/src/views/docs/security.ejs +38 -0
  148. package/src/views/faqs.ejs +65 -0
  149. package/src/views/feed.ejs +33 -0
  150. package/src/views/imposter.ejs +22 -0
  151. package/src/views/imposters.ejs +33 -0
  152. package/src/views/index.ejs +89 -0
  153. package/src/views/license.ejs +30 -0
  154. package/src/views/logs.ejs +77 -0
  155. package/src/views/releases/v1.1.0.ejs +55 -0
  156. package/src/views/releases/v1.1.36.ejs +84 -0
  157. package/src/views/releases/v1.1.72.ejs +92 -0
  158. package/src/views/releases/v1.10.0.ejs +108 -0
  159. package/src/views/releases/v1.11.0.ejs +109 -0
  160. package/src/views/releases/v1.12.0.ejs +96 -0
  161. package/src/views/releases/v1.13.0.ejs +118 -0
  162. package/src/views/releases/v1.14.0.ejs +107 -0
  163. package/src/views/releases/v1.14.1.ejs +94 -0
  164. package/src/views/releases/v1.15.0.ejs +113 -0
  165. package/src/views/releases/v1.16.0.ejs +104 -0
  166. package/src/views/releases/v1.2.0.ejs +78 -0
  167. package/src/views/releases/v1.2.103.ejs +86 -0
  168. package/src/views/releases/v1.2.122.ejs +86 -0
  169. package/src/views/releases/v1.2.30.ejs +84 -0
  170. package/src/views/releases/v1.2.45.ejs +84 -0
  171. package/src/views/releases/v1.2.56.ejs +79 -0
  172. package/src/views/releases/v1.3.0.ejs +86 -0
  173. package/src/views/releases/v1.3.1.ejs +100 -0
  174. package/src/views/releases/v1.4.0.ejs +96 -0
  175. package/src/views/releases/v1.4.1.ejs +103 -0
  176. package/src/views/releases/v1.4.2.ejs +100 -0
  177. package/src/views/releases/v1.4.3.ejs +113 -0
  178. package/src/views/releases/v1.5.0.ejs +104 -0
  179. package/src/views/releases/v1.5.1.ejs +91 -0
  180. package/src/views/releases/v1.6.0.ejs +109 -0
  181. package/src/views/releases/v1.7.0.ejs +113 -0
  182. package/src/views/releases/v1.7.1.ejs +90 -0
  183. package/src/views/releases/v1.7.2.ejs +96 -0
  184. package/src/views/releases/v1.8.0.ejs +121 -0
  185. package/src/views/releases/v1.9.0.ejs +111 -0
  186. package/src/views/releases/v2.0.0.ejs +159 -0
  187. package/src/views/releases/v2.1.0.ejs +121 -0
  188. package/src/views/releases/v2.1.1.ejs +106 -0
  189. package/src/views/releases/v2.1.2.ejs +84 -0
  190. package/src/views/releases/v2.2.0.ejs +115 -0
  191. package/src/views/releases/v2.2.1.ejs +102 -0
  192. package/src/views/releases/v2.3.0.ejs +121 -0
  193. package/src/views/releases/v2.3.1.ejs +100 -0
  194. package/src/views/releases/v2.3.2.ejs +102 -0
  195. package/src/views/releases/v2.3.3.ejs +97 -0
  196. package/src/views/releases/v2.4.0.ejs +114 -0
  197. package/src/views/releases/v2.5.0.ejs +51 -0
  198. package/src/views/releases/v2.6.0.ejs +35 -0
  199. package/src/views/releases/v2.7.0.ejs +32 -0
  200. package/src/views/releases/v2.8.0.ejs +36 -0
  201. package/src/views/releases/v2.8.1.ejs +7 -0
  202. package/src/views/releases/v2.8.2.ejs +26 -0
  203. package/src/views/releases/v2.9.0.ejs +32 -0
  204. package/src/views/releases/v2.9.1.ejs +10 -0
  205. package/src/views/releases.ejs +26 -0
  206. package/src/views/sitemap.ejs +36 -0
  207. package/src/views/support.ejs +14 -0
@@ -0,0 +1,398 @@
1
+ 'use strict';
2
+
3
+ const prometheus = require('prom-client'),
4
+ stringify = require('safe-stable-stringify'),
5
+ helpers = require('../util/helpers.js'),
6
+ compatibility = require('./compatibility.js'),
7
+ exceptions = require('../util/errors.js'),
8
+ xpath = require('./xpath.js'),
9
+ jsonpath = require('./jsonpath.js'),
10
+ behaviors = require('./behaviors.js');
11
+
12
+ /**
13
+ * Determines the response for a stub based on the user-provided response configuration
14
+ * @module
15
+ */
16
+
17
+ const metrics = {
18
+ proxyDuration: new prometheus.Histogram({
19
+ name: 'mb_proxy_duration_seconds',
20
+ help: 'Time it takes to get the response from the downstream service',
21
+ buckets: [0.1, 0.2, 0.5, 1, 3, 5, 10, 30],
22
+ labelNames: ['imposter']
23
+ }),
24
+ proxyCount: new prometheus.Counter({
25
+ name: 'mb_proxy_total',
26
+ help: 'Number of times a request was proxied to a downstream service',
27
+ labelNames: ['imposter']
28
+ })
29
+ };
30
+
31
+ /**
32
+ * Creates the resolver
33
+ * @param {Object} stubs - The stubs repository
34
+ * @param {Object} proxy - The protocol-specific proxy implementation
35
+ * @param {String} callbackURL - The protocol callback URL for response resolution
36
+ * @returns {Object}
37
+ */
38
+ function create (stubs, proxy, callbackURL) {
39
+ // injectState is deprecated in favor of imposterState, but kept for backwards compatibility
40
+ const injectState = {}, // eslint-disable-line no-unused-vars
41
+ pendingProxyResolutions = {},
42
+ inProcessProxy = Boolean(proxy);
43
+ let nextProxyResolutionKey = 0;
44
+
45
+ function inject (request, fn, logger, imposterState) {
46
+ if (request.isDryRun) {
47
+ return Promise.resolve({});
48
+ }
49
+
50
+ return new Promise((done, reject) => {
51
+ // Leave parameters for older interface
52
+ const injected = `(${fn})(config, injectState, logger, done, imposterState);`,
53
+ config = {
54
+ request: helpers.clone(request),
55
+ state: imposterState,
56
+ logger: logger,
57
+ callback: done
58
+ };
59
+
60
+ compatibility.downcastInjectionConfig(config);
61
+
62
+ try {
63
+ const response = eval(injected);
64
+ if (helpers.defined(response)) {
65
+ done(response);
66
+ }
67
+ }
68
+ catch (error) {
69
+ logger.error(`injection X=> ${error}`);
70
+ logger.error(` full source: ${JSON.stringify(injected)}`);
71
+ logger.error(` config.request: ${JSON.stringify(config.request)}`);
72
+ logger.error(` config.state: ${JSON.stringify(config.state)}`);
73
+ reject(exceptions.InjectionError('invalid response injection', {
74
+ source: injected,
75
+ data: error.message
76
+ }));
77
+ }
78
+ });
79
+ }
80
+
81
+ function selectionValue (nodes) {
82
+ if (!helpers.defined(nodes)) {
83
+ return '';
84
+ }
85
+ else if (!Array.isArray(nodes)) {
86
+ return nodes; // booleans and counts
87
+ }
88
+ else {
89
+ return (nodes.length === 1) ? nodes[0] : nodes;
90
+ }
91
+ }
92
+
93
+ function xpathValue (xpathConfig, possibleXML, logger) {
94
+ const nodes = xpath.select(xpathConfig.selector, xpathConfig.ns, possibleXML, logger);
95
+
96
+ return selectionValue(nodes);
97
+ }
98
+
99
+ function jsonpathValue (jsonpathConfig, possibleJSON, logger) {
100
+ const nodes = jsonpath.select(jsonpathConfig.selector, possibleJSON, logger);
101
+
102
+ return selectionValue(nodes);
103
+ }
104
+
105
+ function buildDeepEqual (request, fieldName, predicateGenerators, valueOf) {
106
+ if (!predicateGenerators.ignore) {
107
+ return valueOf(request[fieldName]);
108
+ }
109
+ const objFilter = helpers.objFilter;
110
+ return valueOf(objFilter(request[fieldName], predicateGenerators.ignore[fieldName]));
111
+ }
112
+
113
+ function buildEquals (request, matchers, valueOf) {
114
+ const result = {},
115
+ isObject = helpers.isObject;
116
+
117
+ Object.keys(matchers).forEach(key => {
118
+ if (isObject(request[key])) {
119
+ result[key] = buildEquals(request[key], matchers[key], valueOf);
120
+ }
121
+ else {
122
+ result[key] = valueOf(request[key]);
123
+ }
124
+ });
125
+ return result;
126
+ }
127
+
128
+ const path = [];
129
+
130
+ function buildExists (request, fieldName, matchers, initialRequest) {
131
+ const isObject = helpers.isObject,
132
+ setDeep = helpers.setDeep;
133
+ Object.keys(request).forEach(key => {
134
+ path.push(key);
135
+ if (isObject(request[key])) {
136
+ buildExists(request[key], fieldName, matchers[key], initialRequest);
137
+ }
138
+ else {
139
+ const booleanValue = (typeof fieldName !== 'undefined' && fieldName !== null && fieldName !== '');
140
+ setDeep(initialRequest, path, booleanValue);
141
+ }
142
+ });
143
+ return initialRequest;
144
+ }
145
+
146
+ function predicatesFor (request, matchers, logger) {
147
+ const predicates = [];
148
+
149
+ matchers.forEach(matcher => {
150
+ if (matcher.inject) {
151
+ // eslint-disable-next-line no-unused-vars
152
+ const config = { request, logger },
153
+ injected = `(${matcher.inject})(config);`;
154
+
155
+ try {
156
+ predicates.push(...eval(injected));
157
+ }
158
+ catch (error) {
159
+ logger.error(`injection X=> ${error}`);
160
+ logger.error(` source: ${JSON.stringify(injected)}`);
161
+ logger.error(` request: ${JSON.stringify(request)}`);
162
+ throw exceptions.InjectionError('invalid predicateGenerator injection', { source: injected, data: error.message });
163
+ }
164
+ return;
165
+ }
166
+
167
+ const basePredicate = {};
168
+ let hasPredicateOperator = false;
169
+ let predicateOperator; // eslint-disable-line no-unused-vars
170
+ let valueOf = field => field;
171
+
172
+ // Add parameters
173
+ Object.keys(matcher).forEach(key => {
174
+ if (key !== 'matches' && key !== 'predicateOperator' && key !== 'ignore') {
175
+ basePredicate[key] = matcher[key];
176
+ }
177
+ if (key === 'xpath') {
178
+ valueOf = field => xpathValue(matcher.xpath, field, logger);
179
+ }
180
+ else if (key === 'jsonpath') {
181
+ valueOf = field => jsonpathValue(matcher.jsonpath, field, logger);
182
+ }
183
+ else if (key === 'predicateOperator') {
184
+ hasPredicateOperator = true;
185
+ }
186
+ });
187
+
188
+ Object.keys(matcher.matches).forEach(fieldName => {
189
+ const matcherValue = matcher.matches[fieldName],
190
+ predicate = helpers.clone(basePredicate);
191
+
192
+ if (matcherValue === true && hasPredicateOperator === false) {
193
+ predicate.deepEquals = {};
194
+ predicate.deepEquals[fieldName] = buildDeepEqual(request, fieldName, matcher, valueOf);
195
+ }
196
+ else if (hasPredicateOperator === true && matcher.predicateOperator === 'exists') {
197
+ predicate[matcher.predicateOperator] = buildExists(request, fieldName, matcherValue, request);
198
+ }
199
+ else if (hasPredicateOperator === true && matcher.predicateOperator !== 'exists') {
200
+ predicate[matcher.predicateOperator] = valueOf(request);
201
+ }
202
+ else {
203
+ predicate.equals = {};
204
+ predicate.equals[fieldName] = buildEquals(request[fieldName], matcherValue, valueOf);
205
+ }
206
+
207
+ predicates.push(predicate);
208
+ });
209
+ });
210
+
211
+ return predicates;
212
+ }
213
+
214
+ function deepEqual (obj1, obj2) {
215
+ return stringify(obj1) === stringify(obj2);
216
+ }
217
+
218
+ function newIsResponse (response, proxyConfig) {
219
+ const result = { is: response },
220
+ addBehaviors = [];
221
+
222
+ if (proxyConfig.addWaitBehavior && response._proxyResponseTime) {
223
+ addBehaviors.push({ wait: response._proxyResponseTime });
224
+ }
225
+ if (proxyConfig.addDecorateBehavior) {
226
+ addBehaviors.push({ decorate: proxyConfig.addDecorateBehavior });
227
+ }
228
+
229
+ if (addBehaviors.length > 0) {
230
+ result.behaviors = addBehaviors;
231
+ }
232
+ return result;
233
+ }
234
+
235
+ async function recordProxyAlways (newPredicates, newResponse, responseConfig) {
236
+ const filter = stubPredicates => deepEqual(newPredicates, stubPredicates),
237
+ index = await responseConfig.stubIndex(),
238
+ match = await stubs.first(filter, index + 1);
239
+
240
+ if (match.success) {
241
+ return match.stub.addResponse(newResponse);
242
+ }
243
+ else {
244
+ return stubs.add({ predicates: newPredicates, responses: [newResponse] });
245
+ }
246
+ }
247
+
248
+ async function recordProxyResponse (responseConfig, request, response, logger) {
249
+ const newPredicates = predicatesFor(request, responseConfig.proxy.predicateGenerators || [], logger),
250
+ newResponse = newIsResponse(response, responseConfig.proxy);
251
+
252
+ if (responseConfig.proxy.mode === 'proxyOnce') {
253
+ const index = await responseConfig.stubIndex();
254
+ await stubs.insertAtIndex({ predicates: newPredicates, responses: [newResponse] }, index);
255
+ }
256
+ else if (responseConfig.proxy.mode === 'proxyAlways') {
257
+ await recordProxyAlways(newPredicates, newResponse, responseConfig);
258
+ }
259
+ }
260
+
261
+ async function proxyAndRecord (responseConfig, request, logger, requestDetails, imposterState) {
262
+ const startTime = new Date(),
263
+ observeProxyDuration = metrics.proxyDuration.startTimer();
264
+
265
+ metrics.proxyCount.inc({ imposter: logger.scopePrefix });
266
+
267
+ if (['proxyOnce', 'proxyAlways', 'proxyTransparent'].indexOf(responseConfig.proxy.mode) < 0) {
268
+ responseConfig.proxy.mode = 'proxyOnce';
269
+ }
270
+
271
+ if (inProcessProxy) {
272
+ const response = await proxy.to(responseConfig.proxy.to, request, responseConfig.proxy, requestDetails);
273
+ observeProxyDuration({ imposter: logger.scopePrefix });
274
+ response._proxyResponseTime = new Date() - startTime;
275
+
276
+ // Run behaviors here to persist decorated response
277
+ const transformed = await behaviors.execute(request, response, responseConfig.behaviors, logger, imposterState);
278
+ await recordProxyResponse(responseConfig, request, transformed, logger);
279
+ return transformed;
280
+ }
281
+ else {
282
+ pendingProxyResolutions[nextProxyResolutionKey] = {
283
+ responseConfig: responseConfig,
284
+ request: request,
285
+ requestDetails: requestDetails,
286
+ observeProxyDuration: observeProxyDuration,
287
+ startTime: startTime
288
+ };
289
+ nextProxyResolutionKey += 1;
290
+ return {
291
+ proxy: responseConfig.proxy,
292
+ request: request,
293
+ callbackURL: `${callbackURL}/${nextProxyResolutionKey - 1}`
294
+ };
295
+ }
296
+ }
297
+
298
+ function processResponse (responseConfig, request, logger, imposterState, requestDetails) {
299
+ if (responseConfig.is) {
300
+ // Clone to prevent accidental state changes downstream
301
+ return Promise.resolve(helpers.clone(responseConfig.is));
302
+ }
303
+ else if (responseConfig.proxy) {
304
+ return proxyAndRecord(responseConfig, request, logger, requestDetails, imposterState);
305
+ }
306
+ else if (responseConfig.inject) {
307
+ return inject(request, responseConfig.inject, logger, imposterState);
308
+ }
309
+ else if (responseConfig.fault) {
310
+ // Clone to prevent accidental state changes downstream
311
+ return Promise.resolve(helpers.clone(responseConfig));
312
+ }
313
+ else {
314
+ return Promise.reject(exceptions.ValidationError('unrecognized response type',
315
+ { source: helpers.clone(responseConfig) }));
316
+ }
317
+ }
318
+
319
+ // eslint-disable-next-line complexity
320
+ function hasMultipleTypes (responseConfig) {
321
+ return (responseConfig.is && responseConfig.proxy) ||
322
+ (responseConfig.is && responseConfig.inject) ||
323
+ (responseConfig.proxy && responseConfig.inject) ||
324
+ (responseConfig.fault && responseConfig.proxy) ||
325
+ (responseConfig.fault && responseConfig.is) ||
326
+ (responseConfig.fault && responseConfig.inject);
327
+ }
328
+
329
+ /**
330
+ * Resolves a single response
331
+ * @memberOf module:models/responseResolver#
332
+ * @param {Object} responseConfig - The API-provided response configuration
333
+ * @param {Object} request - The protocol-specific request object
334
+ * @param {Object} logger - The logger
335
+ * @param {Object} imposterState - The current state for the imposter
336
+ * @param {Object} options - Additional options not carried with the request
337
+ * @returns {Object} - Promise resolving to the response
338
+ */
339
+ async function resolve (responseConfig, request, logger, imposterState, options) {
340
+ if (hasMultipleTypes(responseConfig)) {
341
+ return Promise.reject(exceptions.ValidationError('each response object must have only one response type',
342
+ { source: responseConfig }));
343
+ }
344
+
345
+ let response = await processResponse(responseConfig, helpers.clone(request), logger, imposterState, options);
346
+
347
+ // We may have already run the behaviors in the proxy call to persist the decorated response
348
+ // in the new stub. If so, we need to ensure we don't re-run it
349
+ // If we're doing fault simulation there's no need to execute the behaviours
350
+ if (!responseConfig.proxy && !responseConfig.fault) {
351
+ response = await behaviors.execute(request, response, responseConfig.behaviors, logger, imposterState);
352
+ }
353
+
354
+ if (inProcessProxy) {
355
+ return response;
356
+ }
357
+ else {
358
+ return responseConfig.proxy ? response : { response };
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Finishes the protocol implementation dance for proxy. On the first call,
364
+ * mountebank selects a JSON proxy response and sends it to the protocol implementation,
365
+ * saving state indexed by proxyResolutionKey. The protocol implementation sends the proxy
366
+ * to the downstream system and calls mountebank again with the response so mountebank
367
+ * can save it and add behaviors
368
+ * @memberOf module:models/responseResolver#
369
+ * @param {Object} proxyResponse - the proxy response from the protocol implementation
370
+ * @param {Number} proxyResolutionKey - the key into the saved proxy state
371
+ * @param {Object} logger - the logger
372
+ * @param {Object} imposterState - the user controlled state variable
373
+ * @returns {Object} - Promise resolving to the response
374
+ */
375
+ async function resolveProxy (proxyResponse, proxyResolutionKey, logger, imposterState) {
376
+ const pendingProxyConfig = pendingProxyResolutions[proxyResolutionKey];
377
+
378
+ if (pendingProxyConfig) {
379
+ pendingProxyConfig.observeProxyDuration({ imposter: logger.scopePrefix });
380
+ proxyResponse._proxyResponseTime = new Date() - pendingProxyConfig.startTime;
381
+
382
+ const response = await behaviors.execute(pendingProxyConfig.request, proxyResponse,
383
+ pendingProxyConfig.responseConfig.behaviors, logger, imposterState);
384
+ await recordProxyResponse(pendingProxyConfig.responseConfig, pendingProxyConfig.request, response, logger);
385
+ delete pendingProxyResolutions[proxyResolutionKey];
386
+ return response;
387
+ }
388
+ else {
389
+ logger.error('Invalid proxy resolution key: ' + proxyResolutionKey);
390
+ return Promise.reject(exceptions.MissingResourceError('invalid proxy resolution key',
391
+ { source: `${callbackURL}/${proxyResolutionKey}` }));
392
+ }
393
+ }
394
+
395
+ return { resolve, resolveProxy };
396
+ }
397
+
398
+ module.exports = { create };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const config = JSON.parse(process.argv[2]),
4
+ smtpServer = require('./smtpServer'),
5
+ mbConnection = require('../mbConnection').create(config);
6
+
7
+ smtpServer.create(config, mbConnection.logger(), mbConnection.getResponse).then(server => {
8
+ mbConnection.setPort(server.port);
9
+
10
+ const metadata = server.metadata;
11
+ metadata.port = server.port;
12
+ console.log(JSON.stringify(metadata));
13
+ }).catch(error => {
14
+ console.error(JSON.stringify(error));
15
+ process.exit(1); // eslint-disable-line no-process-exit
16
+ });
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Transforms an SMTP request into the simplified API-friendly mountebank request
5
+ * @module
6
+ */
7
+
8
+ const mailParser = require('mailparser');
9
+
10
+ function addressValues (addresses) {
11
+ // mailparser sometimes returns an array, sometimes an object, so we have to normalize
12
+ if (!addresses) {
13
+ addresses = [];
14
+ }
15
+ if (!Array.isArray(addresses)) {
16
+ addresses = [addresses];
17
+ }
18
+ return addresses.map(address => address.value[0]);
19
+ }
20
+
21
+ function transform (session, email) {
22
+ return {
23
+ requestFrom: session.remoteAddress,
24
+ ip: session.remoteAddress,
25
+ envelopeFrom: session.envelope.mailFrom.address,
26
+ envelopeTo: session.envelope.rcptTo.map(value => value.address),
27
+ from: email.from.value[0],
28
+ to: addressValues(email.to),
29
+ cc: addressValues(email.cc),
30
+ bcc: addressValues(email.bcc),
31
+ subject: email.subject,
32
+ priority: email.priority || 'normal',
33
+ references: email.references || [],
34
+ inReplyTo: email.inReplyTo || [],
35
+ text: (email.text || '').trim(),
36
+ html: (email.html || '').trim(),
37
+ attachments: email.attachments || []
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Transforms the raw SMTP request into the mountebank request
43
+ * @param {Object} request - The raw SMTP request
44
+ * @returns {Object}
45
+ */
46
+ function createFrom (request) {
47
+ return new Promise((resolve, reject) => {
48
+ const simpleParser = mailParser.simpleParser;
49
+ simpleParser(request.source, (err, mail) => {
50
+ if (err) {
51
+ reject(err);
52
+ }
53
+ else {
54
+ resolve(transform(request.session, mail));
55
+ }
56
+ });
57
+ });
58
+ }
59
+
60
+ module.exports = { createFrom };
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ const smtpServer = require('smtp-server'),
4
+ helpers = require('../../util/helpers.js'),
5
+ errors = require('../../util/errors.js'),
6
+ smtpRequest = require('./smtpRequest.js');
7
+
8
+ /**
9
+ * Represents an smtp imposter
10
+ * @module
11
+ */
12
+
13
+ function create (options, logger, responseFn) {
14
+ const connections = {},
15
+ SMTPServer = smtpServer.SMTPServer,
16
+ server = new SMTPServer({
17
+ maxAllowedUnauthenticatedCommands: 1000,
18
+ disableReverseLookup: true,
19
+ authOptional: true,
20
+ onConnect (socket, callback) {
21
+ const name = helpers.socketName(socket);
22
+
23
+ logger.debug('%s ESTABLISHED', name);
24
+
25
+ if (socket.on) {
26
+ connections[name] = socket;
27
+
28
+ socket.on('error', error => {
29
+ logger.error('%s transmission error X=> %s', name, JSON.stringify(error));
30
+ });
31
+
32
+ socket.on('end', () => {
33
+ logger.debug('%s LAST-ACK', name);
34
+ });
35
+
36
+ socket.on('close', () => {
37
+ logger.debug('%s CLOSED', name);
38
+ delete connections[name];
39
+ });
40
+ }
41
+ return callback();
42
+ },
43
+ onData (stream, socket, callback) {
44
+ const request = { session: socket, source: stream, callback: callback },
45
+ clientName = helpers.socketName(socket);
46
+
47
+ try {
48
+ smtpRequest.createFrom(request).then(simpleRequest => {
49
+ logger.info(`${clientName} => Envelope from: ${JSON.stringify(simpleRequest.from)} to: ${JSON.stringify(simpleRequest.to)}`);
50
+ logger.debug('%s => %s', clientName, JSON.stringify(simpleRequest));
51
+ return responseFn(simpleRequest);
52
+ }).then(response => {
53
+ if (response) {
54
+ logger.debug('%s <= %s', clientName, JSON.stringify(response));
55
+ }
56
+ return Promise.resolve(true);
57
+ }).then(() => Promise.resolve(request.callback()))
58
+ .catch(error => {
59
+ logger.error('%s X=> %s', clientName, JSON.stringify(errors.details(error)));
60
+ });
61
+ }
62
+ catch (error) {
63
+ logger.error('%s X=> %s', clientName, JSON.stringify(errors.details(error)));
64
+ }
65
+ }
66
+ });
67
+
68
+ return new Promise((resolve, reject) => {
69
+ server.on('error', error => {
70
+ if (error.errno === 'EADDRINUSE') {
71
+ reject(errors.ResourceConflictError(`Port ${options.port} is already in use`));
72
+ }
73
+ else if (error.errno === 'EACCES') {
74
+ reject(errors.InsufficientAccessError());
75
+ }
76
+ else {
77
+ reject(error);
78
+ }
79
+ });
80
+
81
+ server.listen(options.port || 0, options.host, () => {
82
+ resolve({
83
+ port: server.server.address().port,
84
+ metadata: {},
85
+ close: callback => {
86
+ server.close(callback);
87
+ Object.keys(connections).forEach(socket => {
88
+ connections[socket].destroy();
89
+ });
90
+ },
91
+ proxy: {},
92
+ encoding: 'utf8'
93
+ });
94
+ });
95
+ });
96
+ }
97
+
98
+ module.exports = {
99
+ testRequest: {
100
+ from: 'test@test.com',
101
+ to: ['test@test.com'],
102
+ subject: 'Test',
103
+ text: 'Test'
104
+ },
105
+ testProxyResponse: {},
106
+ create: create,
107
+ validate: undefined
108
+ };
109
+
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const config = JSON.parse(process.argv[2]),
4
+ tcpServer = require('./tcpServer.js'),
5
+ tcpProxy = require('./tcpProxy.js'),
6
+ mbConnection = require('../mbConnection.js').create(config);
7
+
8
+ tcpServer.create(config, mbConnection.logger(), mbConnection.getResponse).then(server => {
9
+ mbConnection.setPort(server.port);
10
+ mbConnection.setProxy(tcpProxy.create(mbConnection.logger(), server.encoding, server.isEndOfRequest));
11
+
12
+ const metadata = server.metadata;
13
+ metadata.port = server.port;
14
+ console.log(JSON.stringify(metadata));
15
+ }).catch(error => {
16
+ console.error(JSON.stringify(error));
17
+ process.exit(1); // eslint-disable-line no-process-exit
18
+ });