@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,207 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The base implementation of http/s servers
5
+ * @module
6
+ */
7
+
8
+ const net = require('net'),
9
+ headersMap = require('./headersMap.js'),
10
+ errors = require('../../util/errors.js'),
11
+ httpProxy = require('./httpProxy.js'),
12
+ httpRequest = require('./httpRequest.js'),
13
+ helpers = require('../../util/helpers.js');
14
+
15
+ module.exports = function (createBaseServer) {
16
+
17
+ function create (options, logger, responseFn) {
18
+ const connections = {},
19
+ defaultResponse = options.defaultResponse || {};
20
+
21
+ function postProcess (stubResponse, request) {
22
+ /* eslint complexity: 0 */
23
+ const defaultHeaders = defaultResponse.headers || {},
24
+ response = {
25
+ statusCode: stubResponse.statusCode || defaultResponse.statusCode || 200,
26
+ headers: stubResponse.headers || defaultHeaders,
27
+ body: stubResponse.body || defaultResponse.body || '',
28
+ _mode: stubResponse._mode || defaultResponse._mode || 'text'
29
+ },
30
+ responseHeaders = headersMap.of(response.headers),
31
+ encoding = response._mode === 'binary' ? 'base64' : 'utf8',
32
+ isObject = helpers.isObject;
33
+
34
+ if (isObject(response.body)) {
35
+ // Support JSON response bodies
36
+ response.body = JSON.stringify(response.body, null, 4);
37
+ }
38
+
39
+ if (options.allowCORS) {
40
+ const requestHeaders = headersMap.of(request.headers),
41
+ isCrossOriginPreflight = request.method === 'OPTIONS' &&
42
+ requestHeaders.get('Access-Control-Request-Headers') &&
43
+ requestHeaders.get('Access-Control-Request-Method') &&
44
+ requestHeaders.get('Origin');
45
+
46
+ if (isCrossOriginPreflight) {
47
+ responseHeaders.set('Access-Control-Allow-Headers', requestHeaders.get('Access-Control-Request-Headers'));
48
+ responseHeaders.set('Access-Control-Allow-Methods', requestHeaders.get('Access-Control-Request-Method'));
49
+ responseHeaders.set('Access-Control-Allow-Origin', requestHeaders.get('Origin'));
50
+ }
51
+ }
52
+
53
+ if (encoding === 'base64') {
54
+ // ensure the base64 has no newlines or other non
55
+ // base64 chars that will cause the body to be garbled.
56
+ response.body = response.body.replace(/[^A-Za-z0-9=+/]+/g, '');
57
+ }
58
+
59
+ if (!responseHeaders.has('Connection')) {
60
+ responseHeaders.set('Connection', 'close');
61
+ }
62
+
63
+ if (responseHeaders.has('Content-Length')) {
64
+ responseHeaders.set('Content-Length', Buffer.byteLength(response.body, encoding));
65
+ }
66
+
67
+ return response;
68
+ }
69
+
70
+ const baseServer = createBaseServer(options),
71
+ server = baseServer.createNodeServer();
72
+
73
+ // Allow long wait behaviors
74
+ server.timeout = 0;
75
+
76
+ server.on('connect', (response, client, head) => {
77
+ const host = response.socket.servername;
78
+ const port = response.socket.localPort;
79
+ // may we prefer this method instead?
80
+ const proxyServer = net.createConnection({ host, port }, () => {
81
+ client.on('error', err => {
82
+ logger.warn('CLIENT TO PROXY ERROR: [%s] %s', err.code, err.message);
83
+ logger.debug('%s', err.stack);
84
+ });
85
+ proxyServer.on('error', err => {
86
+ logger.warn('SERVER PROXY ERROR: [%s] %s', err.code, err.message);
87
+ logger.debug('%s', err.stack);
88
+ });
89
+
90
+ logger.info('PROXY TO SERVER SET UP TO %s ON %s', host, port);
91
+ client.write('HTTP/1.1 200 Connection established\r\n\r\n');
92
+ proxyServer.write(head);
93
+ proxyServer.pipe(client);
94
+ client.pipe(proxyServer);
95
+ });
96
+ });
97
+
98
+ server.on('connection', socket => {
99
+ const name = helpers.socketName(socket);
100
+
101
+ logger.debug('%s ESTABLISHED', name);
102
+
103
+ if (socket.on) {
104
+ connections[name] = socket;
105
+
106
+ socket.on('error', error => {
107
+ logger.error('%s transmission error X=> %s', name, JSON.stringify(error));
108
+ });
109
+
110
+ socket.on('end', () => {
111
+ logger.debug('%s LAST-ACK', name);
112
+ });
113
+
114
+ socket.on('close', () => {
115
+ logger.debug('%s CLOSED', name);
116
+ delete connections[name];
117
+ });
118
+ }
119
+ });
120
+
121
+ server.on('request', async (request, response) => {
122
+ const clientName = helpers.socketName(request.socket);
123
+
124
+ logger.info(`${clientName} => ${request.method} ${request.url}`);
125
+
126
+ try {
127
+ const simplifiedRequest = await httpRequest.createFrom(request);
128
+ logger.debug('%s => %s', clientName, JSON.stringify(simplifiedRequest));
129
+
130
+ const mbResponse = await responseFn(simplifiedRequest, { rawUrl: request.url }),
131
+ stubResponse = postProcess(mbResponse, simplifiedRequest),
132
+ encoding = stubResponse._mode === 'binary' ? 'base64' : 'utf8';
133
+
134
+ if (mbResponse.blocked) {
135
+ request.socket.destroy();
136
+ return;
137
+ }
138
+
139
+ if (helpers.simulateFault(request.socket, mbResponse.fault, logger)) {
140
+ return;
141
+ }
142
+
143
+ response.writeHead(stubResponse.statusCode, stubResponse.headers);
144
+ response.end(stubResponse.body.toString(), encoding);
145
+
146
+ if (stubResponse) {
147
+ logger.debug('%s <= %s', clientName, JSON.stringify(stubResponse));
148
+ }
149
+ }
150
+ catch (error) {
151
+ const exceptions = errors;
152
+ logger.error('%s X=> %s', clientName, JSON.stringify(exceptions.details(error)));
153
+ response.writeHead(500, { 'content-type': 'application/json' });
154
+ response.end(JSON.stringify({ errors: [exceptions.details(error)] }), 'utf8');
155
+ }
156
+ });
157
+
158
+ return new Promise((resolve, reject) => {
159
+ server.on('error', error => {
160
+ if (error.errno === 'EADDRINUSE') {
161
+ reject(errors.ResourceConflictError(`Port ${options.port} is already in use`));
162
+ }
163
+ else if (error.errno === 'EACCES') {
164
+ reject(errors.InsufficientAccessError());
165
+ }
166
+ else {
167
+ reject(error);
168
+ }
169
+ });
170
+
171
+ // Bind the socket to a port (the || 0 bit auto-selects a port if one isn't provided)
172
+ server.listen(options.port || 0, options.host, () => {
173
+ resolve({
174
+ port: server.address().port,
175
+ metadata: baseServer.metadata,
176
+ close: callback => {
177
+ server.close(callback);
178
+ Object.keys(connections).forEach(socket => {
179
+ connections[socket].destroy();
180
+ });
181
+ },
182
+ proxy: httpProxy.create(logger),
183
+ encoding: 'utf8'
184
+ });
185
+ });
186
+ });
187
+ }
188
+
189
+ return {
190
+ testRequest: {
191
+ requestFrom: '',
192
+ method: 'GET',
193
+ path: '/',
194
+ query: {},
195
+ headers: {},
196
+ form: {},
197
+ body: ''
198
+ },
199
+ testProxyResponse: {
200
+ statusCode: 200,
201
+ headers: {},
202
+ body: ''
203
+ },
204
+ create: create,
205
+ validate: undefined
206
+ };
207
+ };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Utility module to to get and set headers in a case-insensitive way while maintaining
5
+ * the original case sent in the request
6
+ * @module
7
+ */
8
+
9
+ /**
10
+ * Creates a map from the given headers
11
+ * @param {Object} headers - the starting headers
12
+ * @returns {Object} - the map
13
+ */
14
+
15
+ const helpers = require('../../util/helpers.js');
16
+
17
+ function of (headers) {
18
+ /**
19
+ * Returns whether the map contains the given headerName, regardless of case
20
+ * @param {String} headerName - the header name
21
+ * @returns {boolean}
22
+ */
23
+ function has (headerName) {
24
+ return Object.keys(headers).some(header => header.toLowerCase() === headerName.toLowerCase());
25
+ }
26
+
27
+ function headerNameFor (headerName) {
28
+ const result = Object.keys(headers).find(header => header.toLowerCase() === headerName.toLowerCase());
29
+
30
+ return helpers.defined(result) ? result : headerName;
31
+ }
32
+
33
+ /**
34
+ * Retrieves the value for the given header in a case-insensitive way
35
+ * @param {String} headerName - the header name
36
+ * @returns {*}
37
+ */
38
+ function get (headerName) {
39
+ return headers[headerNameFor(headerName)];
40
+ }
41
+
42
+ /**
43
+ * Sets the value for the given header in a case-insensitive way.
44
+ * Will add the key if it does not already exist
45
+ * @param {String} headerName - the header name
46
+ * @param {String} value - the value
47
+ */
48
+ function set (headerName, value) {
49
+ headers[headerNameFor(headerName)] = value;
50
+ }
51
+
52
+ /**
53
+ * Retrieves all the headers with the original case for the keys
54
+ * @returns {Object} - the key/value pairs
55
+ */
56
+ function all () {
57
+ return headers;
58
+ }
59
+
60
+ return { get, set, has, all };
61
+ }
62
+
63
+ function add (current, value) {
64
+ return Array.isArray(current) ? current.concat(value) : [current].concat(value);
65
+ }
66
+
67
+ function arrayifyIfExists (current, value) {
68
+ return current ? add(current, value) : value;
69
+ }
70
+
71
+ /**
72
+ * Converts the array of raw headers to a map, maintaining the original case of the keys.
73
+ * This is necessary since node downcases the headers in its request.headers
74
+ * @param {Object} rawHeaders - the array of alternating keys and values
75
+ * @returns {Object} - the map
76
+ */
77
+ function ofRaw (rawHeaders) {
78
+ const result = {};
79
+ for (let i = 0; i < rawHeaders.length; i += 2) {
80
+ const name = rawHeaders[i];
81
+ const value = rawHeaders[i + 1];
82
+ result[name] = arrayifyIfExists(result[name], value);
83
+ }
84
+ return of(result);
85
+ }
86
+
87
+ module.exports = { ofRaw, of };
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The proxy implementation for http/s imposters
5
+ * @module
6
+ */
7
+
8
+ const https = require('https'),
9
+ http = require('http'),
10
+ queryString = require('querystring'),
11
+ HttpProxyAgent = require('http-proxy-agent'),
12
+ HttpsProxyAgent = require('https-proxy-agent'),
13
+ helpers = require('../../util/helpers.js'),
14
+ headersMap = require('./headersMap.js'),
15
+ errors = require('../../util/errors.js');
16
+
17
+
18
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
19
+
20
+ /**
21
+ * Creates the proxy
22
+ * @param {Object} logger - The logger
23
+ * @returns {Object}
24
+ */
25
+ function create (logger) {
26
+ const BINARY_CONTENT_ENCODINGS = [
27
+ 'gzip', 'br', 'compress', 'deflate'
28
+ ];
29
+ const BINARY_MIME_TYPES = [
30
+ 'audio/',
31
+ 'application/epub+zip',
32
+ 'application/gzip',
33
+ 'application/java-archive',
34
+ 'application/msword',
35
+ 'application/octet-stream',
36
+ 'application/pdf',
37
+ 'application/rtf',
38
+ 'application/vnd.ms-excel',
39
+ 'application/vnd.ms-fontobject',
40
+ 'application/vnd.ms-powerpoint',
41
+ 'application/vnd.visio',
42
+ 'application/x-shockwave-flash',
43
+ 'application/x-tar',
44
+ 'application/zip',
45
+ 'font/',
46
+ 'image/',
47
+ 'model/',
48
+ 'video/'
49
+ ];
50
+
51
+ function addInjectedHeadersTo (request, headersToInject) {
52
+ Object.keys(headersToInject || {}).forEach(key => {
53
+ request.headers[key] = headersToInject[key];
54
+ });
55
+ }
56
+
57
+ function toUrl (path, query, requestDetails) {
58
+ if (requestDetails) {
59
+ // Not passed in outOfProcess mode
60
+ return requestDetails.rawUrl;
61
+ }
62
+
63
+ const tail = queryString.stringify(query);
64
+
65
+ if (tail === '') {
66
+ return path;
67
+ }
68
+ return `${path}?${tail}`;
69
+ }
70
+
71
+ function hostnameFor (protocol, host, port) {
72
+ let result = host;
73
+ if ((protocol === 'http:' && port !== 80) || (protocol === 'https:' && port !== 443)) {
74
+ result += `:${port}`;
75
+ }
76
+ return result;
77
+ }
78
+
79
+ function setProxyAgent (parts, options) {
80
+ if (process.env.http_proxy && parts.protocol === 'http:') {
81
+ options.agent = new HttpProxyAgent(process.env.http_proxy);
82
+ }
83
+ else if (process.env.https_proxy && parts.protocol === 'https:') {
84
+ options.agent = new HttpsProxyAgent(process.env.https_proxy);
85
+ }
86
+ }
87
+
88
+ function getProxyRequest (baseUrl, originalRequest, proxyOptions, requestDetails) {
89
+ /* eslint complexity: 0 */
90
+ const parts = new URL(baseUrl),
91
+ protocol = parts.protocol === 'https:' ? https : http,
92
+ defaultPort = parts.protocol === 'https:' ? 443 : 80,
93
+ options = {
94
+ method: originalRequest.method,
95
+ hostname: parts.hostname,
96
+ port: parts.port || defaultPort,
97
+ auth: parts.auth,
98
+ path: toUrl(originalRequest.path, originalRequest.query, requestDetails),
99
+ headers: helpers.clone(originalRequest.headers),
100
+ cert: proxyOptions.cert,
101
+ key: proxyOptions.key,
102
+ ciphers: proxyOptions.ciphers || 'ALL',
103
+ secureProtocol: proxyOptions.secureProtocol,
104
+ passphrase: proxyOptions.passphrase,
105
+ rejectUnauthorized: false
106
+ };
107
+
108
+ // Only set host header if not overridden via injectHeaders (issue #388)
109
+ if (!proxyOptions.injectHeaders || !headersMap.of(proxyOptions.injectHeaders).has('host')) {
110
+ options.headers.host = hostnameFor(parts.protocol, parts.hostname, options.port);
111
+ }
112
+ setProxyAgent(parts, options);
113
+
114
+ // Avoid implicit chunked encoding (issue #132)
115
+ if (originalRequest.body &&
116
+ !headersMap.of(originalRequest.headers).has('Transfer-Encoding') &&
117
+ !headersMap.of(originalRequest.headers).has('Content-Length')) {
118
+ options.headers['Content-Length'] = Buffer.byteLength(originalRequest.body);
119
+ }
120
+
121
+ const proxiedRequest = protocol.request(options);
122
+ if (originalRequest.body) {
123
+ proxiedRequest.write(originalRequest.body);
124
+ }
125
+ return proxiedRequest;
126
+ }
127
+
128
+ function isBinaryResponse (headers) {
129
+ const contentEncoding = headers['content-encoding'] || '',
130
+ contentType = headers['content-type'] || '';
131
+
132
+ if (BINARY_CONTENT_ENCODINGS.some(binEncoding => contentEncoding.indexOf(binEncoding) >= 0)) {
133
+ return true;
134
+ }
135
+
136
+ return BINARY_MIME_TYPES.some(typeName => contentType.indexOf(typeName) >= 0);
137
+ }
138
+
139
+ function maybeJSON (text) {
140
+ try {
141
+ return JSON.parse(text);
142
+ }
143
+ catch {
144
+ return text;
145
+ }
146
+ }
147
+
148
+ function proxy (proxiedRequest) {
149
+ return new Promise(resolve => {
150
+ proxiedRequest.end();
151
+
152
+ proxiedRequest.once('response', response => {
153
+ const packets = [];
154
+
155
+ response.on('data', chunk => {
156
+ packets.push(chunk);
157
+ });
158
+
159
+ response.on('end', () => {
160
+ const body = Buffer.concat(packets),
161
+ mode = isBinaryResponse(response.headers) ? 'binary' : 'text',
162
+ encoding = mode === 'binary' ? 'base64' : 'utf8',
163
+ stubResponse = {
164
+ statusCode: response.statusCode,
165
+ headers: headersMap.ofRaw(response.rawHeaders).all(),
166
+ body: maybeJSON(body.toString(encoding)),
167
+ _mode: mode
168
+ };
169
+ resolve(stubResponse);
170
+ });
171
+ });
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Proxies an http/s request to a destination
177
+ * @memberOf module:models/http/httpProxy#
178
+ * @param {string} proxyDestination - The base URL to proxy to, without a path (e.g. http://www.google.com)
179
+ * @param {Object} originalRequest - The original http/s request to forward on to proxyDestination
180
+ * @param {Object} options - Proxy options
181
+ * @param {string} [options.cert] - The certificate, in case the destination requires mutual authentication
182
+ * @param {string} [options.key] - The private key, in case the destination requires mutual authentication
183
+ * @param {Object} [options.injectHeaders] - The headers to inject in the proxied request
184
+ * @param {Object} [options.passphrase] - The passphrase for the private key
185
+ * @param {Object} requestDetails - Additional details about the request not stored in the simplified JSON
186
+ * @returns {Object} - Promise resolving to the response
187
+ */
188
+ function to (proxyDestination, originalRequest, options, requestDetails) {
189
+
190
+ addInjectedHeadersTo(originalRequest, options.injectHeaders);
191
+
192
+ function log (direction, what) {
193
+ logger.debug('Proxy %s %s %s %s %s',
194
+ originalRequest.requestFrom, direction, JSON.stringify(what), direction, proxyDestination);
195
+ }
196
+
197
+ return new Promise((resolve, reject) => {
198
+ let proxiedRequest;
199
+ try {
200
+ proxiedRequest = getProxyRequest(proxyDestination, originalRequest, options, requestDetails);
201
+ }
202
+ catch (e) {
203
+ reject(errors.InvalidProxyError(`Unable to connect to ${JSON.stringify(proxyDestination)}`));
204
+ }
205
+
206
+ log('=>', originalRequest);
207
+
208
+ proxiedRequest.once('error', error => {
209
+ if (error.code === 'ENOTFOUND' || error.code === 'EAI_AGAIN') {
210
+ reject(errors.InvalidProxyError(`Cannot resolve ${JSON.stringify(proxyDestination)}`));
211
+ }
212
+ else if (error.code === 'ECONNREFUSED' || error.code === 'ECONNRESET') {
213
+ reject(errors.InvalidProxyError(`Unable to connect to ${JSON.stringify(proxyDestination)}`));
214
+ }
215
+ else {
216
+ reject(error);
217
+ }
218
+ });
219
+
220
+ proxy(proxiedRequest).then(response => {
221
+ log('<=', response);
222
+ resolve(response);
223
+ });
224
+ });
225
+ }
226
+
227
+ return { to };
228
+ }
229
+
230
+ module.exports = { create };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const queryString = require('querystring'),
4
+ zlib = require('zlib'),
5
+ helpers = require('../../util/helpers.js'),
6
+ headersMapModule = require('./headersMap.js');
7
+
8
+
9
+ /**
10
+ * Transforms a node http/s request to a simplified mountebank http/s request
11
+ * that will be shown in the API
12
+ * @module
13
+ */
14
+
15
+ function transform (request) {
16
+ const url = new URL(request.url, 'http://localhost'),
17
+ search = url.search === '' ? '' : url.search.substr(1),
18
+ headersMap = headersMapModule.ofRaw(request.rawHeaders),
19
+ transformed = {
20
+ requestFrom: helpers.socketName(request.socket),
21
+ method: request.method,
22
+ path: url.pathname,
23
+ query: queryString.parse(search),
24
+ headers: headersMap.all(),
25
+ body: request.body,
26
+ ip: request.socket.remoteAddress
27
+ },
28
+ contentType = headersMap.get('Content-Type');
29
+
30
+ if (request.body && isUrlEncodedForm(contentType)) {
31
+ transformed.form = queryString.parse(request.body);
32
+ }
33
+
34
+ return transformed;
35
+ }
36
+
37
+ function isUrlEncodedForm (contentType) {
38
+ if (!contentType) {
39
+ return false;
40
+ }
41
+
42
+ const index = contentType.indexOf(';'),
43
+ type = index !== -1 ? contentType.substr(0, index).trim() : contentType.trim();
44
+
45
+ return type === 'application/x-www-form-urlencoded';
46
+ }
47
+
48
+ /**
49
+ * Creates the API-friendly http/s request
50
+ * @param {Object} request - The raw http/s request
51
+ * @returns {Object} - Promise resolving to the simplified request
52
+ */
53
+ function createFrom (request) {
54
+ return new Promise(resolve => {
55
+ const chunks = [];
56
+ request.on('data', chunk => { chunks.push(Buffer.from(chunk)); });
57
+ request.on('end', () => {
58
+ const headersMap = headersMapModule.ofRaw(request.rawHeaders),
59
+ contentEncoding = headersMap.get('Content-Encoding'),
60
+ buffer = Buffer.concat(chunks);
61
+
62
+ if (contentEncoding === 'gzip') {
63
+ try {
64
+ request.body = zlib.gunzipSync(buffer).toString();
65
+ }
66
+ catch (error) { /* do nothing */ }
67
+ }
68
+ else if (contentEncoding === 'br') {
69
+ try {
70
+ request.body = zlib.brotliDecompressSync(buffer).toString();
71
+ }
72
+ catch (error) { /* do nothing */ }
73
+ }
74
+ else {
75
+ request.body = buffer.toString();
76
+ }
77
+ resolve(transform(request));
78
+ });
79
+ });
80
+ }
81
+
82
+ module.exports = { createFrom };
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const http = require('http'),
4
+ baseHttpServer = require('./baseHttpServer.js');
5
+
6
+ /**
7
+ * Represents an http imposter
8
+ * @module
9
+ */
10
+
11
+ function createBaseServer () {
12
+ return {
13
+ metadata: {},
14
+ createNodeServer: http.createServer
15
+ };
16
+ }
17
+
18
+ module.exports = baseHttpServer(createBaseServer);
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const config = JSON.parse(process.argv[2]),
4
+ httpServer = require('./httpServer.js'),
5
+ httpProxy = require('./httpProxy.js'),
6
+ mbConnection = require('../mbConnection.js').create(config);
7
+
8
+ httpServer.create(config, mbConnection.logger(), mbConnection.getResponse).then(server => {
9
+ mbConnection.setPort(server.port);
10
+ mbConnection.setProxy(httpProxy.create(mbConnection.logger()));
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
+ });
@@ -0,0 +1,20 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDMTCCAhkCFC1be7uOugfu2q1NmVapw9+SQ0skMA0GCSqGSIb3DQEBCwUAMFUx
3
+ CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJUWDEPMA0GA1UEBwwGRGFsbGFzMRMwEQYD
4
+ VQQKDAptb3VudGViYW5rMRMwEQYDVQQDDAptb3VudGViYW5rMB4XDTIzMDYwNjAy
5
+ MjE0MVoXDTMzMDYwMzAyMjE0MVowVTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlRY
6
+ MQ8wDQYDVQQHDAZEYWxsYXMxEzARBgNVBAoMCm1vdW50ZWJhbmsxEzARBgNVBAMM
7
+ Cm1vdW50ZWJhbmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM+JWj
8
+ JFwtRE++8pTUs6GyWFLKMGSsZDUyJG0n9QivhJPE3fovecKzBdRZZUrAjHWP1Xxp
9
+ DLsqfy3FKzD9bEI2bIaa6Bv3QCUiSEheXteBlljltd4Rhup9yWnpwkBmDW4FIxV6
10
+ Hm5aOyRx41EGcq4Ir0rwpTMkHRrakCKkyVCuhcPbAeTtJn2jw9QIJB3G2IsWuziU
11
+ Fmg2TO90HPUpeoVVEgoO9B9DuCX3QmFBki3xTmm9HnvZ7C0tRZM6Y2aEWoPlzosE
12
+ 7PcFjDmd9jmSXo3NED9+tDc4Xaoc9iPBpLMCqe6DaZBbqHxSFFS6spBJClqev09g
13
+ 45PC3HqXl/Qxzj7VAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAH2pBSp0wtEmRiuY
14
+ 9jhnB8C7pFDOUSUHIfOXKovH92ErqA0KGFvAhCShV71OW63C+7BfJTkt0Gq0pwfD
15
+ TdDQ1T9YNFU6NbNbMfSG9u0TMwajnawnNdO5Gk6LtXJFseWsn5PblCVrWD/qvLMF
16
+ DcllKYW8a8GBbttngt0bhGvh+DxPesjkHoeIWed6UCo3+Wdypes/vol3GL/e8Re5
17
+ uy3eKSBPR5dx/s6l/ZPDEksYV1ve5jv/lBKmRFKv5bFGiTrd5eUE25P2+n9DNGyY
18
+ yNrcLvc1WY/0sqGFmo2ci4ECZVDwx/GhKhdZw7IY8tfLfsKR7sAI6nDHO/GbPYYd
19
+ piqTpzQ=
20
+ -----END CERTIFICATE-----
@@ -0,0 +1,16 @@
1
+ -----BEGIN CERTIFICATE REQUEST-----
2
+ MIICmjCCAYICAQAwVTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlRYMQ8wDQYDVQQH
3
+ DAZEYWxsYXMxEzARBgNVBAoMCm1vdW50ZWJhbmsxEzARBgNVBAMMCm1vdW50ZWJh
4
+ bmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM+JWjJFwtRE++8pTU
5
+ s6GyWFLKMGSsZDUyJG0n9QivhJPE3fovecKzBdRZZUrAjHWP1XxpDLsqfy3FKzD9
6
+ bEI2bIaa6Bv3QCUiSEheXteBlljltd4Rhup9yWnpwkBmDW4FIxV6Hm5aOyRx41EG
7
+ cq4Ir0rwpTMkHRrakCKkyVCuhcPbAeTtJn2jw9QIJB3G2IsWuziUFmg2TO90HPUp
8
+ eoVVEgoO9B9DuCX3QmFBki3xTmm9HnvZ7C0tRZM6Y2aEWoPlzosE7PcFjDmd9jmS
9
+ Xo3NED9+tDc4Xaoc9iPBpLMCqe6DaZBbqHxSFFS6spBJClqev09g45PC3HqXl/Qx
10
+ zj7VAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAUiKzIKhw1QfOeJ2HGmyC9cyh
11
+ r32YyUWVyKWEIdFedcsZcisGE+95UFeakP18LEnfnz8fMfxJHA/J/2rsLxvwx7UI
12
+ naZ9IW+RjSbpZvA3HKoiDubc29sBYX6QGdjMYQpxLzJhW20VpPx29TTVmR2Sg2rM
13
+ P5zhUtYi8YqE2P8bRGZTMsm585Y36dzUl4u5JKVNWBH6Z7q+339I0i+Y+EfoWDzb
14
+ HXztEH9OcyGbWD2sHJGLaEwYVCm6RmFRtCX9LCBjt2lPfmHwm4L+qSfUPRdsfixh
15
+ clLJfcE5i11Ydacp6Mlff3PXkM96Aw4t6CE9fudu1uaAMVoSq9vpmSp0tnrtXA==
16
+ -----END CERTIFICATE REQUEST-----