@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.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/bin/mb +136 -0
- package/package.json +71 -0
- package/releases.json +52 -0
- package/src/cli/api.js +112 -0
- package/src/cli/cli.js +420 -0
- package/src/controllers/configController.js +64 -0
- package/src/controllers/feedController.js +115 -0
- package/src/controllers/homeController.js +58 -0
- package/src/controllers/imposterController.js +328 -0
- package/src/controllers/impostersController.js +215 -0
- package/src/controllers/logsController.js +52 -0
- package/src/models/behaviors.js +553 -0
- package/src/models/behaviorsValidator.js +186 -0
- package/src/models/compatibility.js +133 -0
- package/src/models/dryRunValidator.js +261 -0
- package/src/models/filesystemBackedImpostersRepository.js +908 -0
- package/src/models/http/baseHttpServer.js +207 -0
- package/src/models/http/headersMap.js +87 -0
- package/src/models/http/httpProxy.js +230 -0
- package/src/models/http/httpRequest.js +82 -0
- package/src/models/http/httpServer.js +18 -0
- package/src/models/http/index.js +18 -0
- package/src/models/https/cert/mb-cert.pem +20 -0
- package/src/models/https/cert/mb-csr.pem +16 -0
- package/src/models/https/cert/mb-key.pem +27 -0
- package/src/models/https/httpsServer.js +42 -0
- package/src/models/https/index.js +18 -0
- package/src/models/imposter.js +243 -0
- package/src/models/imposterPrinter.js +120 -0
- package/src/models/impostersRepository.js +49 -0
- package/src/models/inMemoryImpostersRepository.js +418 -0
- package/src/models/jsonpath.js +44 -0
- package/src/models/mbConnection.js +107 -0
- package/src/models/predicates.js +438 -0
- package/src/models/protocols.js +242 -0
- package/src/models/responseResolver.js +398 -0
- package/src/models/smtp/index.js +16 -0
- package/src/models/smtp/smtpRequest.js +60 -0
- package/src/models/smtp/smtpServer.js +109 -0
- package/src/models/tcp/index.js +18 -0
- package/src/models/tcp/tcpProxy.js +110 -0
- package/src/models/tcp/tcpRequest.js +23 -0
- package/src/models/tcp/tcpServer.js +156 -0
- package/src/models/tcp/tcpValidator.js +19 -0
- package/src/models/xpath.js +95 -0
- package/src/mountebank.js +245 -0
- package/src/public/images/arrow_down.png +0 -0
- package/src/public/images/arrow_up.png +0 -0
- package/src/public/images/book.jpg +0 -0
- package/src/public/images/dataflow.png +0 -0
- package/src/public/images/favicon.ico +0 -0
- package/src/public/images/forkme_right_orange_ff7600.png +0 -0
- package/src/public/images/mountebank.png +0 -0
- package/src/public/images/overview.gif +0 -0
- package/src/public/images/quote.png +0 -0
- package/src/public/images/tw-logo.png +0 -0
- package/src/public/scripts/jquery/jquery-3.6.1.min.js +2 -0
- package/src/public/scripts/urlHashHandler.js +31 -0
- package/src/public/stylesheets/application.css +424 -0
- package/src/public/stylesheets/ie.css +14 -0
- package/src/public/stylesheets/imposters.css +121 -0
- package/src/public/stylesheets/jqueryui/1.10.4/themes/smoothness/jquery-ui.css +1178 -0
- package/src/util/combinators.js +68 -0
- package/src/util/date.js +51 -0
- package/src/util/errors.js +55 -0
- package/src/util/helpers.js +131 -0
- package/src/util/inherit.js +28 -0
- package/src/util/ip.js +54 -0
- package/src/util/logger.js +83 -0
- package/src/util/middleware.js +256 -0
- package/src/util/scopedLogger.js +47 -0
- package/src/views/_footer.ejs +20 -0
- package/src/views/_header.ejs +113 -0
- package/src/views/_imposter.ejs +8 -0
- package/src/views/config.ejs +71 -0
- package/src/views/docs/api/behaviors/copy.ejs +427 -0
- package/src/views/docs/api/behaviors/decorate.ejs +182 -0
- package/src/views/docs/api/behaviors/lookup.ejs +220 -0
- package/src/views/docs/api/behaviors/shellTransform.ejs +153 -0
- package/src/views/docs/api/behaviors/wait.ejs +121 -0
- package/src/views/docs/api/behaviors.ejs +141 -0
- package/src/views/docs/api/contracts/addStub-description.ejs +10 -0
- package/src/views/docs/api/contracts/addStub.ejs +10 -0
- package/src/views/docs/api/contracts/config-description.ejs +32 -0
- package/src/views/docs/api/contracts/config.ejs +23 -0
- package/src/views/docs/api/contracts/home-description.ejs +18 -0
- package/src/views/docs/api/contracts/home.ejs +13 -0
- package/src/views/docs/api/contracts/imposter-description.ejs +439 -0
- package/src/views/docs/api/contracts/imposter.ejs +182 -0
- package/src/views/docs/api/contracts/imposters-description.ejs +13 -0
- package/src/views/docs/api/contracts/imposters.ejs +13 -0
- package/src/views/docs/api/contracts/logs-description.ejs +3 -0
- package/src/views/docs/api/contracts/logs.ejs +14 -0
- package/src/views/docs/api/contracts/stub-description.ejs +4 -0
- package/src/views/docs/api/contracts/stub.ejs +7 -0
- package/src/views/docs/api/contracts/stubs-description.ejs +4 -0
- package/src/views/docs/api/contracts/stubs.ejs +11 -0
- package/src/views/docs/api/contracts.ejs +133 -0
- package/src/views/docs/api/errors.ejs +64 -0
- package/src/views/docs/api/fault/connectionReset.ejs +31 -0
- package/src/views/docs/api/fault/randomDataThenClose.ejs +31 -0
- package/src/views/docs/api/faults.ejs +57 -0
- package/src/views/docs/api/injection.ejs +426 -0
- package/src/views/docs/api/json.ejs +205 -0
- package/src/views/docs/api/jsonpath.ejs +210 -0
- package/src/views/docs/api/mocks.ejs +130 -0
- package/src/views/docs/api/overview.ejs +968 -0
- package/src/views/docs/api/predicates/and.ejs +62 -0
- package/src/views/docs/api/predicates/contains.ejs +64 -0
- package/src/views/docs/api/predicates/deepEquals.ejs +114 -0
- package/src/views/docs/api/predicates/endsWith.ejs +66 -0
- package/src/views/docs/api/predicates/equals.ejs +125 -0
- package/src/views/docs/api/predicates/exists.ejs +118 -0
- package/src/views/docs/api/predicates/inject.ejs +67 -0
- package/src/views/docs/api/predicates/matches.ejs +66 -0
- package/src/views/docs/api/predicates/not.ejs +52 -0
- package/src/views/docs/api/predicates/or.ejs +79 -0
- package/src/views/docs/api/predicates/startsWith.ejs +62 -0
- package/src/views/docs/api/predicates.ejs +382 -0
- package/src/views/docs/api/proxies.ejs +191 -0
- package/src/views/docs/api/proxy/addDecorateBehavior.ejs +115 -0
- package/src/views/docs/api/proxy/addWaitBehavior.ejs +96 -0
- package/src/views/docs/api/proxy/injectHeaders.ejs +91 -0
- package/src/views/docs/api/proxy/predicateGenerators.ejs +600 -0
- package/src/views/docs/api/proxy/proxyModes.ejs +495 -0
- package/src/views/docs/api/stubs.ejs +391 -0
- package/src/views/docs/api/xpath.ejs +281 -0
- package/src/views/docs/cli/configFiles.ejs +133 -0
- package/src/views/docs/cli/customFormatters.ejs +53 -0
- package/src/views/docs/cli/help.ejs +6 -0
- package/src/views/docs/cli/replay.ejs +42 -0
- package/src/views/docs/cli/restart.ejs +10 -0
- package/src/views/docs/cli/save.ejs +68 -0
- package/src/views/docs/cli/start.ejs +234 -0
- package/src/views/docs/cli/stop.ejs +32 -0
- package/src/views/docs/commandLine.ejs +93 -0
- package/src/views/docs/communityExtensions.ejs +233 -0
- package/src/views/docs/gettingStarted.ejs +146 -0
- package/src/views/docs/mentalModel.ejs +51 -0
- package/src/views/docs/protocols/custom.ejs +231 -0
- package/src/views/docs/protocols/http.ejs +238 -0
- package/src/views/docs/protocols/https.ejs +246 -0
- package/src/views/docs/protocols/smtp.ejs +142 -0
- package/src/views/docs/protocols/tcp.ejs +431 -0
- package/src/views/docs/security.ejs +38 -0
- package/src/views/faqs.ejs +65 -0
- package/src/views/feed.ejs +33 -0
- package/src/views/imposter.ejs +22 -0
- package/src/views/imposters.ejs +33 -0
- package/src/views/index.ejs +89 -0
- package/src/views/license.ejs +30 -0
- package/src/views/logs.ejs +77 -0
- package/src/views/releases/v1.1.0.ejs +55 -0
- package/src/views/releases/v1.1.36.ejs +84 -0
- package/src/views/releases/v1.1.72.ejs +92 -0
- package/src/views/releases/v1.10.0.ejs +108 -0
- package/src/views/releases/v1.11.0.ejs +109 -0
- package/src/views/releases/v1.12.0.ejs +96 -0
- package/src/views/releases/v1.13.0.ejs +118 -0
- package/src/views/releases/v1.14.0.ejs +107 -0
- package/src/views/releases/v1.14.1.ejs +94 -0
- package/src/views/releases/v1.15.0.ejs +113 -0
- package/src/views/releases/v1.16.0.ejs +104 -0
- package/src/views/releases/v1.2.0.ejs +78 -0
- package/src/views/releases/v1.2.103.ejs +86 -0
- package/src/views/releases/v1.2.122.ejs +86 -0
- package/src/views/releases/v1.2.30.ejs +84 -0
- package/src/views/releases/v1.2.45.ejs +84 -0
- package/src/views/releases/v1.2.56.ejs +79 -0
- package/src/views/releases/v1.3.0.ejs +86 -0
- package/src/views/releases/v1.3.1.ejs +100 -0
- package/src/views/releases/v1.4.0.ejs +96 -0
- package/src/views/releases/v1.4.1.ejs +103 -0
- package/src/views/releases/v1.4.2.ejs +100 -0
- package/src/views/releases/v1.4.3.ejs +113 -0
- package/src/views/releases/v1.5.0.ejs +104 -0
- package/src/views/releases/v1.5.1.ejs +91 -0
- package/src/views/releases/v1.6.0.ejs +109 -0
- package/src/views/releases/v1.7.0.ejs +113 -0
- package/src/views/releases/v1.7.1.ejs +90 -0
- package/src/views/releases/v1.7.2.ejs +96 -0
- package/src/views/releases/v1.8.0.ejs +121 -0
- package/src/views/releases/v1.9.0.ejs +111 -0
- package/src/views/releases/v2.0.0.ejs +159 -0
- package/src/views/releases/v2.1.0.ejs +121 -0
- package/src/views/releases/v2.1.1.ejs +106 -0
- package/src/views/releases/v2.1.2.ejs +84 -0
- package/src/views/releases/v2.2.0.ejs +115 -0
- package/src/views/releases/v2.2.1.ejs +102 -0
- package/src/views/releases/v2.3.0.ejs +121 -0
- package/src/views/releases/v2.3.1.ejs +100 -0
- package/src/views/releases/v2.3.2.ejs +102 -0
- package/src/views/releases/v2.3.3.ejs +97 -0
- package/src/views/releases/v2.4.0.ejs +114 -0
- package/src/views/releases/v2.5.0.ejs +51 -0
- package/src/views/releases/v2.6.0.ejs +35 -0
- package/src/views/releases/v2.7.0.ejs +32 -0
- package/src/views/releases/v2.8.0.ejs +36 -0
- package/src/views/releases/v2.8.1.ejs +7 -0
- package/src/views/releases/v2.8.2.ejs +26 -0
- package/src/views/releases/v2.9.0.ejs +32 -0
- package/src/views/releases/v2.9.1.ejs +10 -0
- package/src/views/releases.ejs +26 -0
- package/src/views/sitemap.ejs +36 -0
- package/src/views/support.ejs +14 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net'),
|
|
4
|
+
errors = require('../../util/errors.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents the tcp proxy implementation
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates the proxy
|
|
13
|
+
* @param {Object} logger - The logger
|
|
14
|
+
* @param {string} encoding - utf8 or base64, depending on if the destination expects text or binary
|
|
15
|
+
* @param {Function} isEndOfRequest - the function defining a logical request
|
|
16
|
+
* @returns {Object}
|
|
17
|
+
*/
|
|
18
|
+
function create (logger, encoding, isEndOfRequest) {
|
|
19
|
+
|
|
20
|
+
if (typeof isEndOfRequest === 'undefined') {
|
|
21
|
+
isEndOfRequest = () => true; // defaults to a packet boundary
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function socketName (socket) {
|
|
25
|
+
return `${socket.host}:${socket.port}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function format (request) {
|
|
29
|
+
return request.data.toString(encoding);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function connectionInfoFor (proxyDestination) {
|
|
33
|
+
const parts = new URL(proxyDestination);
|
|
34
|
+
|
|
35
|
+
if (parts.protocol !== 'tcp:') {
|
|
36
|
+
throw errors.InvalidProxyError('Unable to proxy to any protocol other than tcp',
|
|
37
|
+
{ source: proxyDestination });
|
|
38
|
+
}
|
|
39
|
+
return { host: parts.hostname, port: parts.port };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Proxies a tcp request to the destination
|
|
44
|
+
* @param {string} proxyDestination - The URL to proxy to (e.g. tcp://127.0.0.1:3535)
|
|
45
|
+
* @param {Object} originalRequest - The tcp request to forward
|
|
46
|
+
* @param {Object} options - Proxy options
|
|
47
|
+
* @param {Boolean} options.keepalive - Whether to keep the connection alive or not
|
|
48
|
+
* @returns {Object} - A promise resolving to the response
|
|
49
|
+
*/
|
|
50
|
+
function to (proxyDestination, originalRequest, options = {}) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
try {
|
|
53
|
+
const proxyName = socketName(connectionInfoFor(proxyDestination)),
|
|
54
|
+
log = (direction, what) => {
|
|
55
|
+
logger.debug('Proxy %s %s %s %s %s',
|
|
56
|
+
originalRequest.requestFrom, direction, JSON.stringify(format(what)), direction, proxyName);
|
|
57
|
+
},
|
|
58
|
+
buffer = Buffer.from(originalRequest.data, encoding),
|
|
59
|
+
socket = net.connect(connectionInfoFor(proxyDestination), () => {
|
|
60
|
+
socket.write(buffer);
|
|
61
|
+
}),
|
|
62
|
+
packets = [];
|
|
63
|
+
|
|
64
|
+
log('=>', originalRequest);
|
|
65
|
+
|
|
66
|
+
socket.on('end', () => {
|
|
67
|
+
logger.debug('%s LAST-ACK', proxyName);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
socket.on('close', () => {
|
|
71
|
+
logger.debug('%s CLOSED', proxyName);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
socket.on('data', data => {
|
|
75
|
+
packets.push(data);
|
|
76
|
+
const requestBuffer = Buffer.concat(packets);
|
|
77
|
+
if (isEndOfRequest(requestBuffer, logger)) {
|
|
78
|
+
if (!options.keepalive) {
|
|
79
|
+
socket.end();
|
|
80
|
+
}
|
|
81
|
+
const response = { data: requestBuffer.toString(encoding) };
|
|
82
|
+
log('<=', response);
|
|
83
|
+
resolve(response);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
socket.once('error', error => {
|
|
88
|
+
logger.error(`Proxy ${proxyName} transmission error X=> ${JSON.stringify(error)}`);
|
|
89
|
+
|
|
90
|
+
if (error.code === 'ENOTFOUND' || error.code === 'EAI_AGAIN') {
|
|
91
|
+
reject(errors.InvalidProxyError(`Cannot resolve ${JSON.stringify(proxyDestination)}`));
|
|
92
|
+
}
|
|
93
|
+
else if (error.code === 'ECONNREFUSED') {
|
|
94
|
+
reject(errors.InvalidProxyError(`Unable to connect to ${JSON.stringify(proxyDestination)}`));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
reject(error);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
reject(e);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { to };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { create };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const helpers = require('../../util/helpers.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Transforms a raw tcp request into the API-friendly representation of one
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Transforms the raw tcp request into a mountebank tcp request
|
|
12
|
+
* @param {Object} request - The raw tcp request
|
|
13
|
+
* @returns {Object} - A promise resolving to the mountebank tcp request
|
|
14
|
+
*/
|
|
15
|
+
function createFrom (request) {
|
|
16
|
+
return Promise.resolve({
|
|
17
|
+
requestFrom: helpers.socketName(request.socket),
|
|
18
|
+
ip: request.socket.remoteAddress,
|
|
19
|
+
data: request.data
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { createFrom };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const net = require('net'),
|
|
3
|
+
helpers = require('../../util/helpers.js'),
|
|
4
|
+
tcpRequest = require('./tcpRequest.js'),
|
|
5
|
+
tcpProxy = require('./tcpProxy.js'),
|
|
6
|
+
tcpValidator = require('./tcpValidator.js'),
|
|
7
|
+
errors = require('../../util/errors.js');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents a tcp imposter
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function create (options, logger, responseFn) {
|
|
15
|
+
const mode = options.mode ? options.mode : 'text',
|
|
16
|
+
encoding = mode === 'binary' ? 'base64' : 'utf8',
|
|
17
|
+
server = net.createServer(),
|
|
18
|
+
connections = {},
|
|
19
|
+
defaultResponse = options.defaultResponse || { data: '' };
|
|
20
|
+
|
|
21
|
+
// Used to determine logical end of request; defaults to one packet but
|
|
22
|
+
// changeable through injection
|
|
23
|
+
function isEndOfRequest (requestData) {
|
|
24
|
+
if (!options.endOfRequestResolver || !options.endOfRequestResolver.inject) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const injected = `(${options.endOfRequestResolver.inject})(requestData, logger)`;
|
|
29
|
+
|
|
30
|
+
if (mode === 'text') {
|
|
31
|
+
requestData = requestData.toString('utf8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return eval(injected);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logger.error(`injection X=> ${error}`);
|
|
39
|
+
logger.error(` full source: ${JSON.stringify(injected)}`);
|
|
40
|
+
logger.error(` requestData: ${JSON.stringify(requestData)}`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// eslint-disable-next-line complexity
|
|
46
|
+
async function respond (payload, request, clientName, socket) {
|
|
47
|
+
let formattedRequestData = payload.toString(encoding);
|
|
48
|
+
if (formattedRequestData.length > 20) {
|
|
49
|
+
formattedRequestData = formattedRequestData.substring(0, 20) + '...';
|
|
50
|
+
}
|
|
51
|
+
logger.info('%s => %s', clientName, formattedRequestData);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Translate network request to JSON
|
|
55
|
+
const jsonRequest = await tcpRequest.createFrom(request);
|
|
56
|
+
logger.debug('%s => %s', clientName, JSON.stringify(jsonRequest.data.toString(encoding)));
|
|
57
|
+
|
|
58
|
+
// call mountebank with JSON request
|
|
59
|
+
const mbResponse = await responseFn(jsonRequest),
|
|
60
|
+
processedResponse = mbResponse.data || defaultResponse.data,
|
|
61
|
+
buffer = Buffer.isBuffer(processedResponse)
|
|
62
|
+
? processedResponse
|
|
63
|
+
: Buffer.from(processedResponse, encoding);
|
|
64
|
+
|
|
65
|
+
if (mbResponse.blocked) {
|
|
66
|
+
socket.destroy();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (helpers.simulateFault(socket, mbResponse.fault, logger)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (buffer.length > 0) {
|
|
75
|
+
socket.write(buffer);
|
|
76
|
+
logger.debug('%s <= %s', clientName, JSON.stringify(buffer.toString(encoding)));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger.error('%s X=> %s', clientName, JSON.stringify(errors.details(error)));
|
|
81
|
+
socket.write(JSON.stringify({ errors: [error] }), 'utf8');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
server.on('connection', socket => {
|
|
86
|
+
let packets = [];
|
|
87
|
+
const clientName = helpers.socketName(socket);
|
|
88
|
+
|
|
89
|
+
logger.debug('%s ESTABLISHED', clientName);
|
|
90
|
+
|
|
91
|
+
connections[clientName] = socket;
|
|
92
|
+
|
|
93
|
+
socket.on('error', error => {
|
|
94
|
+
logger.error('%s transmission error X=> %s', clientName, JSON.stringify(error));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
socket.on('end', () => {
|
|
98
|
+
logger.debug('%s LAST-ACK', clientName);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
socket.on('close', () => {
|
|
102
|
+
logger.debug('%s CLOSED', clientName);
|
|
103
|
+
delete connections[clientName];
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
socket.on('data', async data => {
|
|
107
|
+
packets.push(data);
|
|
108
|
+
|
|
109
|
+
const payload = Buffer.concat(packets),
|
|
110
|
+
request = { socket: socket, data: payload.toString(encoding) };
|
|
111
|
+
|
|
112
|
+
if (isEndOfRequest(payload)) {
|
|
113
|
+
packets = [];
|
|
114
|
+
await respond(payload, request, clientName, socket);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
server.on('error', error => {
|
|
121
|
+
if (error.errno === 'EADDRINUSE') {
|
|
122
|
+
reject(errors.ResourceConflictError(`Port ${options.port} is already in use`));
|
|
123
|
+
}
|
|
124
|
+
else if (error.errno === 'EACCES') {
|
|
125
|
+
reject(errors.InsufficientAccessError());
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
reject(error);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Bind the socket to a port (the || 0 bit auto-selects a port if one isn't provided)
|
|
133
|
+
server.listen(options.port || 0, options.host, () => {
|
|
134
|
+
resolve({
|
|
135
|
+
port: server.address().port,
|
|
136
|
+
metadata: { mode },
|
|
137
|
+
close: callback => {
|
|
138
|
+
server.close(() => { callback(); });
|
|
139
|
+
Object.keys(connections).forEach(socket => {
|
|
140
|
+
connections[socket].destroy();
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
proxy: tcpProxy.create(logger, encoding, isEndOfRequest),
|
|
144
|
+
encoding: encoding,
|
|
145
|
+
isEndOfRequest: isEndOfRequest
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
testRequest: { data: 'test' },
|
|
153
|
+
testProxyResponse: { data: '' },
|
|
154
|
+
create: create,
|
|
155
|
+
validate: tcpValidator.validate
|
|
156
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const exceptions = require('../../util/errors.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Additional tcp-specific validations
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function validate (request) {
|
|
11
|
+
const errors = [];
|
|
12
|
+
|
|
13
|
+
if (request.mode && ['text', 'binary'].indexOf(request.mode) < 0) {
|
|
14
|
+
errors.push(exceptions.ValidationError("'mode' must be one of ['text', 'binary']"));
|
|
15
|
+
}
|
|
16
|
+
return errors;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { validate };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const xpath = require('xpath'),
|
|
4
|
+
xmlDom = require('@xmldom/xmldom'),
|
|
5
|
+
errors = require('../util/errors.js'),
|
|
6
|
+
helpers = require('../util/helpers.js');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared logic for xpath selector
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function xpathSelect (selectFn, selector, doc) {
|
|
14
|
+
if (!helpers.defined(doc)) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (doc && doc.implementation) {
|
|
19
|
+
// Monkey patch due to some legacy case sensitivity check in xpath
|
|
20
|
+
// Couldn't find a way to patch xpath due to closures so patched the dom instead
|
|
21
|
+
const originalHasFeature = doc.implementation.hasFeature;
|
|
22
|
+
doc.implementation.hasFeature = (feature, version) => {
|
|
23
|
+
if (feature === 'HTML' && version === '2.0') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
return originalHasFeature.apply(doc.implementation, [feature, version]);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
return selectFn(selector, doc);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
throw errors.ValidationError('malformed xpath predicate selector', {
|
|
37
|
+
source: selector,
|
|
38
|
+
inner: e
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nodeValue (node) {
|
|
44
|
+
if (node.nodeType === node.TEXT_NODE) {
|
|
45
|
+
return node.nodeValue;
|
|
46
|
+
}
|
|
47
|
+
else if (node.nodeType === node.ATTRIBUTE_NODE) {
|
|
48
|
+
return node.value;
|
|
49
|
+
}
|
|
50
|
+
else if (node.firstChild) {
|
|
51
|
+
// Converting to a string allows exists to return true if the node exists,
|
|
52
|
+
// even if there's no data
|
|
53
|
+
return String(node.firstChild.data);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
return String(node.data);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns xpath value(s) from given xml
|
|
62
|
+
* @param {String} selector - The xpath selector
|
|
63
|
+
* @param {Object} ns - The namespace map
|
|
64
|
+
* @param {String} possibleXML - the xml
|
|
65
|
+
* @param {Object} logger - Optional, used to log XML parsing errors
|
|
66
|
+
* @returns {Object}
|
|
67
|
+
*/
|
|
68
|
+
function select (selector, ns, possibleXML, logger) {
|
|
69
|
+
const DOMParser = xmlDom.DOMParser,
|
|
70
|
+
parser = new DOMParser({
|
|
71
|
+
errorHandler: (level, message) => {
|
|
72
|
+
const warn = (logger || {}).warn || (() => {});
|
|
73
|
+
warn('%s (source: %s)', message, JSON.stringify(possibleXML));
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
doc = parser.parseFromString(possibleXML),
|
|
77
|
+
selectFn = xpath.useNamespaces(ns || {}),
|
|
78
|
+
result = xpathSelect(selectFn, selector, doc);
|
|
79
|
+
let nodeValues;
|
|
80
|
+
|
|
81
|
+
if (['number', 'boolean'].indexOf(typeof result) >= 0) {
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
nodeValues = result.map(nodeValue);
|
|
86
|
+
|
|
87
|
+
if (nodeValues.length === 0) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
return nodeValues;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { select };
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express'),
|
|
4
|
+
cors = require('cors'),
|
|
5
|
+
promClient = require('prom-client'),
|
|
6
|
+
errorHandler = require('errorhandler'),
|
|
7
|
+
path = require('path'),
|
|
8
|
+
middleware = require('./util/middleware'),
|
|
9
|
+
thisPackage = require('../package.json'),
|
|
10
|
+
releases = require('../releases.json'),
|
|
11
|
+
helpers = require('./util/helpers.js'),
|
|
12
|
+
utilLogger = require('./util/logger.js'),
|
|
13
|
+
utilIp = require('./util/ip.js'),
|
|
14
|
+
protocolsModule = require('./models/protocols.js'),
|
|
15
|
+
imposterRepositoryModule = require('./models/impostersRepository.js'),
|
|
16
|
+
homeControllerModule = require('./controllers/homeController.js'),
|
|
17
|
+
impostersControllerModule = require('./controllers/impostersController.js'),
|
|
18
|
+
imposterControllerModule = require('./controllers/imposterController.js'),
|
|
19
|
+
logsControllerModule = require('./controllers/logsController.js'),
|
|
20
|
+
configControllerModule = require('./controllers/configController.js'),
|
|
21
|
+
feedControllerModule = require('./controllers/feedController.js');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The entry point for mountebank. This module creates the mountebank server,
|
|
25
|
+
* configures all middleware and manages all routing
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates the mountebank express application
|
|
31
|
+
* @param {object} options - The command line options
|
|
32
|
+
* @returns {Object} An object with a close method to stop the server
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
function applyDefaults (options) {
|
|
36
|
+
// Minimal defaults to start bypassing the CLI (e.g. embedding in an express app)
|
|
37
|
+
const defaults = {
|
|
38
|
+
port: 2525,
|
|
39
|
+
ipWhitelist: ['*']
|
|
40
|
+
};
|
|
41
|
+
Object.keys(defaults).forEach(key => {
|
|
42
|
+
options[key] = typeof options[key] === 'undefined' ? defaults[key] : options[key];
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function createApp (options) {
|
|
47
|
+
applyDefaults(options);
|
|
48
|
+
|
|
49
|
+
const app = express(),
|
|
50
|
+
hostname = options.host || 'localhost',
|
|
51
|
+
baseURL = `http://${hostname}:${options.port}`,
|
|
52
|
+
logger = utilLogger.createLogger(options),
|
|
53
|
+
isAllowedConnection = utilIp.createIPVerification(options),
|
|
54
|
+
imposters = imposterRepositoryModule.create(options, logger),
|
|
55
|
+
protocols = protocolsModule.loadProtocols(options, baseURL, logger, isAllowedConnection, imposters),
|
|
56
|
+
homeController = homeControllerModule.create(releases),
|
|
57
|
+
impostersController = impostersControllerModule.create(
|
|
58
|
+
protocols, imposters, logger, options.allowInjection),
|
|
59
|
+
imposterController = imposterControllerModule.create(
|
|
60
|
+
protocols, imposters, logger, options.allowInjection),
|
|
61
|
+
logfile = options.log.transports.file ? options.log.transports.file.path : false,
|
|
62
|
+
logsController = logsControllerModule.create(logfile),
|
|
63
|
+
configController = configControllerModule.create(thisPackage.version, options),
|
|
64
|
+
feedController = feedControllerModule.create(releases),
|
|
65
|
+
validateImposterExists = middleware.createImposterValidator(imposters),
|
|
66
|
+
prometheus = promClient;
|
|
67
|
+
|
|
68
|
+
// Clear only matters when bound using directly in-process through JS rather than the CLI
|
|
69
|
+
prometheus.register.clear();
|
|
70
|
+
prometheus.collectDefaultMetrics({ prefix: 'mb_' });
|
|
71
|
+
|
|
72
|
+
app.use(middleware.useAbsoluteUrls(options.port));
|
|
73
|
+
app.use(middleware.validateApiKey(options.apikey, logger));
|
|
74
|
+
app.use(middleware.logger(logger, ':method :url'));
|
|
75
|
+
app.use(middleware.globals({ port: options.port, version: thisPackage.version }));
|
|
76
|
+
app.use(middleware.defaultIEtoHTML);
|
|
77
|
+
app.use(middleware.json(logger));
|
|
78
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
79
|
+
app.use(express.static(path.join(__dirname, '../node_modules')));
|
|
80
|
+
app.use(errorHandler());
|
|
81
|
+
app.use(cors({ origin: options.origin }));
|
|
82
|
+
|
|
83
|
+
app.disable('etag');
|
|
84
|
+
app.disable('x-powered-by');
|
|
85
|
+
app.set('views', path.join(__dirname, 'views'));
|
|
86
|
+
app.set('view engine', 'ejs');
|
|
87
|
+
app.set('json spaces', 2);
|
|
88
|
+
|
|
89
|
+
app.get('/', homeController.get);
|
|
90
|
+
app.get('/imposters', impostersController.get);
|
|
91
|
+
app.post('/imposters', impostersController.post);
|
|
92
|
+
app.delete('/imposters', impostersController.del);
|
|
93
|
+
app.put('/imposters', impostersController.put);
|
|
94
|
+
app.get('/imposters/:id', validateImposterExists, imposterController.get);
|
|
95
|
+
app.delete('/imposters/:id', imposterController.del);
|
|
96
|
+
app.delete('/imposters/:id/savedProxyResponses', validateImposterExists, imposterController.resetProxies);
|
|
97
|
+
app.delete('/imposters/:id/savedRequests', validateImposterExists, imposterController.resetRequests);
|
|
98
|
+
|
|
99
|
+
// deprecated but saved for backwards compatibility
|
|
100
|
+
app.delete('/imposters/:id/requests', validateImposterExists, imposterController.resetProxies);
|
|
101
|
+
|
|
102
|
+
// Changing stubs without restarting imposter
|
|
103
|
+
app.put('/imposters/:id/stubs', validateImposterExists, imposterController.putStubs);
|
|
104
|
+
app.put('/imposters/:id/stubs/:stubIndex', validateImposterExists, imposterController.putStub);
|
|
105
|
+
app.post('/imposters/:id/stubs', validateImposterExists, imposterController.postStub);
|
|
106
|
+
app.delete('/imposters/:id/stubs/:stubIndex', validateImposterExists, imposterController.deleteStub);
|
|
107
|
+
|
|
108
|
+
// Protocol implementation APIs
|
|
109
|
+
app.post('/imposters/:id/_requests', validateImposterExists, imposterController.postRequest);
|
|
110
|
+
app.post('/imposters/:id/_requests/:proxyResolutionKey', validateImposterExists, imposterController.postProxyResponse);
|
|
111
|
+
|
|
112
|
+
app.get('/logs', logsController.get);
|
|
113
|
+
app.get('/config', configController.get);
|
|
114
|
+
app.get('/feed', feedController.getFeed);
|
|
115
|
+
app.get('/releases', feedController.getReleases);
|
|
116
|
+
app.get('/releases/:version', feedController.getRelease);
|
|
117
|
+
|
|
118
|
+
app.get('/metrics', async function (request, response) {
|
|
119
|
+
const register = promClient.register;
|
|
120
|
+
response.set('Content-Type', register.contentType);
|
|
121
|
+
response.end(await register.metrics());
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
app.get('/sitemap', (request, response) => {
|
|
125
|
+
response.type('text/plain');
|
|
126
|
+
response.render('sitemap', { releases: releases });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
[
|
|
130
|
+
'/support',
|
|
131
|
+
'/license',
|
|
132
|
+
'/faqs',
|
|
133
|
+
'/docs/gettingStarted',
|
|
134
|
+
'/docs/mentalModel',
|
|
135
|
+
'/docs/commandLine',
|
|
136
|
+
'/docs/communityExtensions',
|
|
137
|
+
'/docs/security',
|
|
138
|
+
'/docs/api/overview',
|
|
139
|
+
'/docs/api/contracts',
|
|
140
|
+
'/docs/api/mocks',
|
|
141
|
+
'/docs/api/stubs',
|
|
142
|
+
'/docs/api/predicates',
|
|
143
|
+
'/docs/api/xpath',
|
|
144
|
+
'/docs/api/json',
|
|
145
|
+
'/docs/api/jsonpath',
|
|
146
|
+
'/docs/api/proxies',
|
|
147
|
+
'/docs/api/injection',
|
|
148
|
+
'/docs/api/behaviors',
|
|
149
|
+
'/docs/api/errors',
|
|
150
|
+
'/docs/api/faults',
|
|
151
|
+
'/docs/protocols/http',
|
|
152
|
+
'/docs/protocols/https',
|
|
153
|
+
'/docs/protocols/tcp',
|
|
154
|
+
'/docs/protocols/smtp',
|
|
155
|
+
'/docs/protocols/custom'
|
|
156
|
+
].forEach(endpoint => {
|
|
157
|
+
app.get(endpoint, (request, response) => {
|
|
158
|
+
response.render(endpoint.substring(1));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
process.once('exit', () => {
|
|
163
|
+
imposters.stopAllSync();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (options.allowInjection) {
|
|
167
|
+
logger.warn(`Running with --allowInjection set. See ${baseURL}/docs/security for security info`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await imposters.loadAll(protocols);
|
|
171
|
+
|
|
172
|
+
return app;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Start the mountebank server
|
|
177
|
+
* @param {function} app - mountebank express application
|
|
178
|
+
* @param {object} options - The command line options
|
|
179
|
+
* @returns {Object} An object with a close method to stop the server
|
|
180
|
+
*/
|
|
181
|
+
async function listen (app, options) {
|
|
182
|
+
const hostname = options.host || 'localhost',
|
|
183
|
+
baseURL = `http://${hostname}:${options.port}`,
|
|
184
|
+
logger = utilLogger.createLogger(options),
|
|
185
|
+
isAllowedConnection = utilIp.createIPVerification(options);
|
|
186
|
+
|
|
187
|
+
return new Promise(resolve => {
|
|
188
|
+
const connections = {},
|
|
189
|
+
server = app.listen(options.port, options.host, () => {
|
|
190
|
+
logger.info(`mountebank v${thisPackage.version} now taking orders - point your browser to ${baseURL}/ for help`);
|
|
191
|
+
logger.debug(`config: ${JSON.stringify({
|
|
192
|
+
options: options,
|
|
193
|
+
process: {
|
|
194
|
+
nodeVersion: process.version,
|
|
195
|
+
architecture: process.arch,
|
|
196
|
+
platform: process.platform
|
|
197
|
+
}
|
|
198
|
+
})}`);
|
|
199
|
+
|
|
200
|
+
resolve({
|
|
201
|
+
close: callback => {
|
|
202
|
+
server.close(() => {
|
|
203
|
+
logger.info('Adios - see you soon?');
|
|
204
|
+
callback();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Force kill any open connections to prevent process hanging
|
|
208
|
+
Object.keys(connections).forEach(socket => {
|
|
209
|
+
connections[socket].destroy();
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
server.on('connection', socket => {
|
|
216
|
+
const name = helpers.socketName(socket),
|
|
217
|
+
ipAddress = socket.remoteAddress;
|
|
218
|
+
connections[name] = socket;
|
|
219
|
+
|
|
220
|
+
socket.on('close', () => {
|
|
221
|
+
delete connections[name];
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
socket.on('error', error => {
|
|
225
|
+
logger.error(`${name} transmission error X=> ${JSON.stringify(error)}`);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!isAllowedConnection(ipAddress, logger)) {
|
|
229
|
+
socket.destroy();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Creates the mountebank server
|
|
237
|
+
* @param {object} options - The command line options
|
|
238
|
+
* @returns {Object} An object with a close method to stop the server
|
|
239
|
+
*/
|
|
240
|
+
async function create (options) {
|
|
241
|
+
const app = await createApp(options);
|
|
242
|
+
return listen(app, options);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = { create, createApp };
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|