@stuntman/server 0.1.0 → 0.1.2
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/README.md +128 -0
- package/dist/api/api.js +12 -11
- package/dist/api/validatiors.js +1 -1
- package/dist/mock.d.ts +2 -1
- package/dist/mock.js +30 -21
- package/dist/ruleExecutor.d.ts +3 -2
- package/dist/ruleExecutor.js +27 -14
- package/package.json +17 -4
- package/config/default.json +0 -23
- package/config/test.json +0 -26
- package/src/api/api.ts +0 -194
- package/src/api/utils.ts +0 -69
- package/src/api/validatiors.ts +0 -123
- package/src/api/webgui/rules.pug +0 -145
- package/src/api/webgui/style.css +0 -28
- package/src/api/webgui/traffic.pug +0 -37
- package/src/bin/stuntman.ts +0 -8
- package/src/index.ts +0 -1
- package/src/ipUtils.ts +0 -83
- package/src/mock.ts +0 -349
- package/src/requestContext.ts +0 -23
- package/src/ruleExecutor.ts +0 -193
- package/src/rules/catchAll.ts +0 -14
- package/src/rules/echo.ts +0 -14
- package/src/rules/index.ts +0 -7
- package/src/storage.ts +0 -39
- package/tsconfig.json +0 -16
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Stuntman
|
|
2
|
+
|
|
3
|
+
Stuntman is a proxy/mock server that can be deployed remotely together with your application under test, working as either pass-through proxy allowing you to inspect traffic or proxy/mock which can intercept requests/responses and modify them or stub with predefined ones.
|
|
4
|
+
|
|
5
|
+
It offers API and client library that can be used for example within E2E functional test scripts to dynamically alter it's behaviour for specific traffic matching set of rules of your definition.
|
|
6
|
+
|
|
7
|
+
In order to get more familiar with the concept and how to use it please refer to [example app](https://github.com/andrzej-woof/stuntman/tree/master/packages/example#readme)
|
|
8
|
+
|
|
9
|
+
> **_NOTE:_** This project is at a very early stage of developement and as such may often contain breaking changes in upcoming releases before reaching stable version 1.0.0
|
|
10
|
+
|
|
11
|
+
## Building from source
|
|
12
|
+
|
|
13
|
+
### Prerequisites
|
|
14
|
+
|
|
15
|
+
* [pnpm](https://github.com/pnpm/pnpm) package manager
|
|
16
|
+
* [nvm](https://github.com/nvm-sh/nvm) node version manager (optional)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
nvm use
|
|
20
|
+
pnpm install --frozen-lockfile
|
|
21
|
+
pnpm build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Start server
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm stuntman
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Stuntman uses [config](https://github.com/node-config/node-config)
|
|
33
|
+
|
|
34
|
+
You can create `config/default.json` with settings of your liking matching `ServerConfig` type
|
|
35
|
+
|
|
36
|
+
## Running as a package
|
|
37
|
+
|
|
38
|
+
### Install with package manager of your choice
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @stuntman/server
|
|
42
|
+
yarn add @stuntman/server
|
|
43
|
+
pnpm add @stuntman/server
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Run from bin
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
stuntman
|
|
50
|
+
yarn stuntman
|
|
51
|
+
node ./node_modules/.bin/stuntman
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Run programatically
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { Mock } from '../mock';
|
|
58
|
+
import { serverConfig } from '@stuntman/shared';
|
|
59
|
+
|
|
60
|
+
const mock = new Mock(serverConfig);
|
|
61
|
+
|
|
62
|
+
mock.start();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Point domain to localhost
|
|
66
|
+
|
|
67
|
+
Add some domains with `.stuntman` suffix (or `.stuntmanhttp` / `.stuntmanhttps` depending where you want to direct the traffic in proxy mode) to your `/etc/hosts` for example
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
127.0.0.1 www.example.com.stuntman
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Try in browser
|
|
74
|
+
|
|
75
|
+
go to your browser and visit `http://www.example.com.stuntman:2015/` to see the proxied page
|
|
76
|
+
for local playground you can also use `http://www.example.com.localhost:2015`
|
|
77
|
+
|
|
78
|
+
### Take a look at client
|
|
79
|
+
|
|
80
|
+
Mind the scope of `Stuntman.RemotableFunction` like `matches`, `modifyRequest`, `modifyResponse`.
|
|
81
|
+
`Stuntman.RemotableFunction.localFn` contains the function, but since it'll be executed on a remote mock server it cannot access any variables outside it's body. In order to pass variable values into the function use `Stuntman.RemotableFunction.variables` for example:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
matches: {
|
|
85
|
+
localFn: (req) => {
|
|
86
|
+
// you might need to ignore typescript errors about undefined variables in this scope
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
return /http:\/\/[^/]+\/somepath$/.test(req.url) && req.url.includes(`?someparam=${myVar}`);
|
|
90
|
+
},
|
|
91
|
+
localVariables: { myVar: 'myValue' },
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
You can build the rules using fluentish `ruleBuilder`
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { Client } from './apiClient';
|
|
99
|
+
import { ruleBuilder } from './ruleBuilder';
|
|
100
|
+
|
|
101
|
+
const client = new Client();
|
|
102
|
+
|
|
103
|
+
const uniqueQaUserEmail = 'unique_qa_email@example.com';
|
|
104
|
+
const rule = ruleBuilder()
|
|
105
|
+
.limitedUse(2)
|
|
106
|
+
.onRequestToHostname('example.com')
|
|
107
|
+
.withSearchParam('user', uniqueQaUserEmail)
|
|
108
|
+
.mockResponse({
|
|
109
|
+
localFn: (req) => {
|
|
110
|
+
if (JSON.parse(req.body).email !== uniqueQaUserEmail) {
|
|
111
|
+
return {
|
|
112
|
+
status: 500,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { status: 201 };
|
|
116
|
+
},
|
|
117
|
+
localVariables: { uniqueQaUserEmail },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
client.addRule(rule).then((x) => console.log(x));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Take a look at PoC of WebGUI
|
|
124
|
+
|
|
125
|
+
....just don't look to closely, it's very much incomplete and hacky
|
|
126
|
+
|
|
127
|
+
* http://stuntman:1985/webgui/rules - rule viewer/editor
|
|
128
|
+
* http://stuntman:1985/webgui/traffic - traffic viewer for the rules that store traffic
|
package/dist/api/api.js
CHANGED
|
@@ -26,17 +26,17 @@ class API {
|
|
|
26
26
|
next();
|
|
27
27
|
});
|
|
28
28
|
this.apiApp.get('/rule', async (req, res) => {
|
|
29
|
-
res.send((0, shared_1.stringify)(await ruleExecutor_1.
|
|
29
|
+
res.send((0, shared_1.stringify)(await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).getRules()));
|
|
30
30
|
});
|
|
31
31
|
this.apiApp.get('/rule/:ruleId', async (req, res) => {
|
|
32
|
-
res.send((0, shared_1.stringify)(await ruleExecutor_1.
|
|
32
|
+
res.send((0, shared_1.stringify)(await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).getRule(req.params.ruleId)));
|
|
33
33
|
});
|
|
34
34
|
this.apiApp.get('/rule/:ruleId/disable', (req, res) => {
|
|
35
|
-
ruleExecutor_1.
|
|
35
|
+
(0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).disableRule(req.params.ruleId);
|
|
36
36
|
res.send();
|
|
37
37
|
});
|
|
38
38
|
this.apiApp.get('/rule/:ruleId/enable', (req, res) => {
|
|
39
|
-
ruleExecutor_1.
|
|
39
|
+
(0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).enableRule(req.params.ruleId);
|
|
40
40
|
res.send();
|
|
41
41
|
});
|
|
42
42
|
this.apiApp.post('/rule', async (req, res) => {
|
|
@@ -44,11 +44,11 @@ class API {
|
|
|
44
44
|
(0, validatiors_1.validateDeserializedRule)(deserializedRule);
|
|
45
45
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
46
46
|
// @ts-ignore
|
|
47
|
-
const rule = await ruleExecutor_1.
|
|
47
|
+
const rule = await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).addRule(deserializedRule);
|
|
48
48
|
res.send((0, shared_1.stringify)(rule));
|
|
49
49
|
});
|
|
50
50
|
this.apiApp.get('/rule/:ruleId/remove', async (req, res) => {
|
|
51
|
-
await ruleExecutor_1.
|
|
51
|
+
await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).removeRule(req.params.ruleId);
|
|
52
52
|
res.send();
|
|
53
53
|
});
|
|
54
54
|
this.apiApp.get('/traffic', (req, res) => {
|
|
@@ -84,19 +84,20 @@ class API {
|
|
|
84
84
|
});
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.log('API server encountered a critical error. Exiting');
|
|
88
89
|
process.exit(1);
|
|
89
90
|
});
|
|
90
|
-
this.apiApp.set('views', __dirname + '/webgui');
|
|
91
|
-
this.apiApp.set('view engine', 'pug');
|
|
92
91
|
if (!(webGuiOptions === null || webGuiOptions === void 0 ? void 0 : webGuiOptions.disabled)) {
|
|
92
|
+
this.apiApp.set('views', __dirname + '/webgui');
|
|
93
|
+
this.apiApp.set('view engine', 'pug');
|
|
93
94
|
this.initWebGui();
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
initWebGui() {
|
|
97
98
|
this.apiApp.get('/webgui/rules', async (req, res) => {
|
|
98
99
|
const rules = {};
|
|
99
|
-
for (const rule of await ruleExecutor_1.
|
|
100
|
+
for (const rule of await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).getRules()) {
|
|
100
101
|
rules[rule.id] = (0, serialize_javascript_1.default)((0, utils_1.liveRuleToRule)(rule), { unsafe: true });
|
|
101
102
|
}
|
|
102
103
|
res.render('rules', { rules: (0, utils_1.escapedSerialize)(rules), INDEX_DTS: shared_1.INDEX_DTS, ruleKeys: Object.keys(rules) });
|
|
@@ -120,7 +121,7 @@ class API {
|
|
|
120
121
|
rule.ttlSeconds > shared_1.MAX_RULE_TTL_SECONDS) {
|
|
121
122
|
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'Invalid rule' });
|
|
122
123
|
}
|
|
123
|
-
await ruleExecutor_1.
|
|
124
|
+
await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).addRule({
|
|
124
125
|
id: rule.id,
|
|
125
126
|
matches: rule.matches,
|
|
126
127
|
ttlSeconds: rule.ttlSeconds,
|
package/dist/api/validatiors.js
CHANGED
|
@@ -104,7 +104,7 @@ const validateDeserializedRule = (deserializedRule) => {
|
|
|
104
104
|
shared_1.logger.error({ ruleId: deserializedRule.id }, error);
|
|
105
105
|
throw new shared_1.AppError({
|
|
106
106
|
httpCode: shared_1.HttpCode.UNPROCESSABLE_ENTITY,
|
|
107
|
-
message: 'match function
|
|
107
|
+
message: 'match function threw an error',
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
if (matchValidationResult !== true &&
|
package/dist/mock.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { IPUtils } from './ipUtils';
|
|
|
8
8
|
import LRUCache from 'lru-cache';
|
|
9
9
|
import { API } from './api/api';
|
|
10
10
|
export declare class Mock {
|
|
11
|
-
|
|
11
|
+
readonly mockUuid: string;
|
|
12
12
|
protected options: Stuntman.ServerConfig;
|
|
13
13
|
protected mockApp: express.Express;
|
|
14
14
|
protected MOCK_DOMAIN_REGEX: RegExp;
|
|
@@ -19,6 +19,7 @@ export declare class Mock {
|
|
|
19
19
|
protected ipUtils: IPUtils | null;
|
|
20
20
|
private _api;
|
|
21
21
|
get apiServer(): API | null;
|
|
22
|
+
get ruleExecutor(): Stuntman.RuleExecutorInterface;
|
|
22
23
|
constructor(options: Stuntman.ServerConfig);
|
|
23
24
|
start(): void;
|
|
24
25
|
stop(): void;
|
package/dist/mock.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Mock = void 0;
|
|
7
|
+
const undici_1 = require("undici");
|
|
7
8
|
const https_1 = __importDefault(require("https"));
|
|
8
9
|
const express_1 = __importDefault(require("express"));
|
|
9
10
|
const uuid_1 = require("uuid");
|
|
@@ -57,6 +58,9 @@ class Mock {
|
|
|
57
58
|
}
|
|
58
59
|
return this._api;
|
|
59
60
|
}
|
|
61
|
+
get ruleExecutor() {
|
|
62
|
+
return (0, ruleExecutor_1.getRuleExecutor)(this.mockUuid);
|
|
63
|
+
}
|
|
60
64
|
constructor(options) {
|
|
61
65
|
this.server = null;
|
|
62
66
|
this.serverHttps = null;
|
|
@@ -67,7 +71,7 @@ class Mock {
|
|
|
67
71
|
if (this.options.mock.httpsPort && (!this.options.mock.httpsKey || !this.options.mock.httpsCert)) {
|
|
68
72
|
throw new Error('missing https key/cert');
|
|
69
73
|
}
|
|
70
|
-
this.MOCK_DOMAIN_REGEX = new RegExp(`(?:\\.([0-9]+))?\\.(?:(?:${this.options.mock.domain})|(?:localhost))(
|
|
74
|
+
this.MOCK_DOMAIN_REGEX = new RegExp(`(?:\\.([0-9]+))?\\.(?:(?:${this.options.mock.domain}(https?)?)|(?:localhost))(:${this.options.mock.port}${this.options.mock.httpsPort ? `|${this.options.mock.httpsPort}` : ''})?(?:\\b|$)`, 'i');
|
|
71
75
|
this.URL_PORT_REGEX = new RegExp(`^(https?:\\/\\/[^:/]+):(?:${this.options.mock.port}${this.options.mock.httpsPort ? `|${this.options.mock.httpsPort}` : ''})(\\/.*)`, 'i');
|
|
72
76
|
this.trafficStore = (0, storage_1.getTrafficStore)(this.mockUuid, this.options.storage.traffic);
|
|
73
77
|
this.ipUtils =
|
|
@@ -86,7 +90,7 @@ class Mock {
|
|
|
86
90
|
const ctx = requestContext_1.default.get(req);
|
|
87
91
|
const requestUuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)();
|
|
88
92
|
const timestamp = Date.now();
|
|
89
|
-
const originalHostname = req.hostname;
|
|
93
|
+
const originalHostname = req.headers.host || req.hostname;
|
|
90
94
|
const unproxiedHostname = req.hostname.replace(this.MOCK_DOMAIN_REGEX, '');
|
|
91
95
|
const isProxiedHostname = originalHostname !== unproxiedHostname;
|
|
92
96
|
const originalRequest = {
|
|
@@ -95,7 +99,8 @@ class Mock {
|
|
|
95
99
|
url: `${req.protocol}://${req.hostname}${req.originalUrl}`,
|
|
96
100
|
method: req.method,
|
|
97
101
|
rawHeaders: new shared_1.RawHeaders(...req.rawHeaders),
|
|
98
|
-
...(Buffer.isBuffer(req.body) && { body: req.body.toString('utf-8') })
|
|
102
|
+
...((Buffer.isBuffer(req.body) && { body: req.body.toString('utf-8') }) ||
|
|
103
|
+
(typeof req.body === 'string' && { body: req.body })),
|
|
99
104
|
};
|
|
100
105
|
shared_1.logger.debug(originalRequest, 'processing request');
|
|
101
106
|
const logContext = {
|
|
@@ -113,7 +118,7 @@ class Mock {
|
|
|
113
118
|
if (!isProxiedHostname) {
|
|
114
119
|
this.removeProxyPort(mockEntry.modifiedRequest);
|
|
115
120
|
}
|
|
116
|
-
const matchingRule = await ruleExecutor_1.
|
|
121
|
+
const matchingRule = await (0, ruleExecutor_1.getRuleExecutor)(this.mockUuid).findMatchingRule(mockEntry.modifiedRequest);
|
|
117
122
|
if (matchingRule) {
|
|
118
123
|
mockEntry.mockRuleId = matchingRule.id;
|
|
119
124
|
mockEntry.labels = matchingRule.labels;
|
|
@@ -170,36 +175,39 @@ class Mock {
|
|
|
170
175
|
}
|
|
171
176
|
});
|
|
172
177
|
let targetResponse;
|
|
173
|
-
const hasKeepAlive = !!mockEntry.modifiedRequest.rawHeaders
|
|
174
|
-
.toHeaderPairs()
|
|
175
|
-
.find((h) => /^connection$/.test(h[0]) && /^keep-alive$/.test(h[1]));
|
|
176
178
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
headers: mockEntry.modifiedRequest.rawHeaders
|
|
180
|
-
.toHeaderPairs()
|
|
181
|
-
.filter((h) => !/^connection$/.test(h[0]) && !/^keep-alive$/.test(h[1])),
|
|
179
|
+
const requestOptions = {
|
|
180
|
+
headers: mockEntry.modifiedRequest.rawHeaders,
|
|
182
181
|
body: mockEntry.modifiedRequest.body,
|
|
183
|
-
method: mockEntry.modifiedRequest.method,
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
method: mockEntry.modifiedRequest.method.toUpperCase(),
|
|
183
|
+
};
|
|
184
|
+
shared_1.logger.debug({
|
|
185
|
+
...logContext,
|
|
186
|
+
url: mockEntry.modifiedRequest.url,
|
|
187
|
+
...requestOptions,
|
|
188
|
+
}, 'outgoing request attempt');
|
|
189
|
+
targetResponse = await (0, undici_1.request)(mockEntry.modifiedRequest.url, requestOptions);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
shared_1.logger.error({ ...logContext, error, request: mockEntry.modifiedRequest }, 'error fetching');
|
|
193
|
+
throw error;
|
|
186
194
|
}
|
|
187
195
|
finally {
|
|
188
196
|
controller = null;
|
|
189
197
|
clearTimeout(fetchTimeout);
|
|
190
198
|
}
|
|
191
|
-
const targetResponseBuffer = Buffer.from(await targetResponse.arrayBuffer());
|
|
199
|
+
const targetResponseBuffer = Buffer.from(await targetResponse.body.arrayBuffer());
|
|
192
200
|
const originalResponse = {
|
|
193
201
|
timestamp: Date.now(),
|
|
194
202
|
body: targetResponseBuffer.toString('binary'),
|
|
195
|
-
status: targetResponse.
|
|
196
|
-
rawHeaders:
|
|
203
|
+
status: targetResponse.statusCode,
|
|
204
|
+
rawHeaders: shared_1.RawHeaders.fromHeadersRecord(targetResponse.headers),
|
|
197
205
|
};
|
|
198
206
|
shared_1.logger.debug({ ...logContext, originalResponse }, 'received response');
|
|
199
207
|
mockEntry.originalResponse = originalResponse;
|
|
200
208
|
let modifedResponse = {
|
|
201
209
|
...originalResponse,
|
|
202
|
-
rawHeaders: new shared_1.RawHeaders(...Array.from(
|
|
210
|
+
rawHeaders: new shared_1.RawHeaders(...Array.from(originalResponse.rawHeaders.toHeaderPairs()).flatMap(([key, value]) => {
|
|
203
211
|
// TODO this replace may be too aggressive and doesn't handle protocol (won't be necessary with a trusted cert and mock serving http+https)
|
|
204
212
|
return [
|
|
205
213
|
key,
|
|
@@ -227,7 +235,7 @@ class Mock {
|
|
|
227
235
|
if (/^content-(?:length|encoding)$/i.test(header[0])) {
|
|
228
236
|
continue;
|
|
229
237
|
}
|
|
230
|
-
res.setHeader(header[0], header[1]);
|
|
238
|
+
res.setHeader(header[0], isProxiedHostname ? header[1].replace(unproxiedHostname, originalHostname) : header[1]);
|
|
231
239
|
}
|
|
232
240
|
}
|
|
233
241
|
res.end(Buffer.from(modifedResponse.body, 'binary'));
|
|
@@ -242,7 +250,8 @@ class Mock {
|
|
|
242
250
|
});
|
|
243
251
|
return;
|
|
244
252
|
}
|
|
245
|
-
|
|
253
|
+
// eslint-disable-next-line no-console
|
|
254
|
+
console.error('mock server encountered a critical error. exiting');
|
|
246
255
|
process.exit(1);
|
|
247
256
|
});
|
|
248
257
|
}
|
package/dist/ruleExecutor.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type * as Stuntman from '@stuntman/shared';
|
|
2
|
-
declare class RuleExecutor {
|
|
2
|
+
declare class RuleExecutor implements Stuntman.RuleExecutorInterface {
|
|
3
3
|
private _rules;
|
|
4
|
+
private rulesLock;
|
|
4
5
|
private get enabledRules();
|
|
5
6
|
constructor(rules?: Stuntman.Rule[]);
|
|
6
7
|
private hasExpired;
|
|
@@ -17,5 +18,5 @@ declare class RuleExecutor {
|
|
|
17
18
|
getRules(): Promise<readonly Stuntman.LiveRule[]>;
|
|
18
19
|
getRule(id: string): Promise<Stuntman.LiveRule | undefined>;
|
|
19
20
|
}
|
|
20
|
-
export declare const
|
|
21
|
+
export declare const getRuleExecutor: (mockUuid: string) => RuleExecutor;
|
|
21
22
|
export {};
|
package/dist/ruleExecutor.js
CHANGED
|
@@ -3,11 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.getRuleExecutor = void 0;
|
|
7
7
|
const await_lock_1 = __importDefault(require("await-lock"));
|
|
8
8
|
const shared_1 = require("@stuntman/shared");
|
|
9
9
|
const rules_1 = require("./rules");
|
|
10
|
-
const
|
|
10
|
+
const ruleExecutors = {};
|
|
11
11
|
const transformMockRuleToLive = (rule) => {
|
|
12
12
|
var _a;
|
|
13
13
|
return {
|
|
@@ -28,6 +28,7 @@ class RuleExecutor {
|
|
|
28
28
|
.sort((a, b) => { var _a, _b; return ((_a = a.priority) !== null && _a !== void 0 ? _a : shared_1.DEFAULT_RULE_PRIORITY) - ((_b = b.priority) !== null && _b !== void 0 ? _b : shared_1.DEFAULT_RULE_PRIORITY); });
|
|
29
29
|
}
|
|
30
30
|
constructor(rules) {
|
|
31
|
+
this.rulesLock = new await_lock_1.default();
|
|
31
32
|
this._rules = (rules || []).map(transformMockRuleToLive);
|
|
32
33
|
}
|
|
33
34
|
hasExpired() {
|
|
@@ -38,7 +39,7 @@ class RuleExecutor {
|
|
|
38
39
|
if (!this.hasExpired()) {
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
await rulesLock.acquireAsync();
|
|
42
|
+
await this.rulesLock.acquireAsync();
|
|
42
43
|
const now = Date.now();
|
|
43
44
|
try {
|
|
44
45
|
this._rules = this._rules.filter((r) => {
|
|
@@ -50,12 +51,12 @@ class RuleExecutor {
|
|
|
50
51
|
});
|
|
51
52
|
}
|
|
52
53
|
finally {
|
|
53
|
-
await rulesLock.release();
|
|
54
|
+
await this.rulesLock.release();
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
async addRule(rule, overwrite) {
|
|
57
58
|
await this.cleanUpExpired();
|
|
58
|
-
await rulesLock.acquireAsync();
|
|
59
|
+
await this.rulesLock.acquireAsync();
|
|
59
60
|
try {
|
|
60
61
|
if (this._rules.some((r) => r.id === rule.id)) {
|
|
61
62
|
if (!overwrite) {
|
|
@@ -69,7 +70,7 @@ class RuleExecutor {
|
|
|
69
70
|
return liveRule;
|
|
70
71
|
}
|
|
71
72
|
finally {
|
|
72
|
-
await rulesLock.release();
|
|
73
|
+
await this.rulesLock.release();
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
_removeRule(ruleOrId) {
|
|
@@ -83,12 +84,12 @@ class RuleExecutor {
|
|
|
83
84
|
}
|
|
84
85
|
async removeRule(ruleOrId) {
|
|
85
86
|
await this.cleanUpExpired();
|
|
86
|
-
await rulesLock.acquireAsync();
|
|
87
|
+
await this.rulesLock.acquireAsync();
|
|
87
88
|
try {
|
|
88
89
|
this._removeRule(ruleOrId);
|
|
89
90
|
}
|
|
90
91
|
finally {
|
|
91
|
-
await rulesLock.release();
|
|
92
|
+
await this.rulesLock.release();
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
enableRule(ruleOrId) {
|
|
@@ -112,11 +113,17 @@ class RuleExecutor {
|
|
|
112
113
|
requestId: request.id,
|
|
113
114
|
};
|
|
114
115
|
const matchingRule = this.enabledRules.find((rule) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
try {
|
|
117
|
+
const matchResult = rule.matches(request);
|
|
118
|
+
shared_1.logger.trace({ ...logContext, matchResult }, `rule match attempt for ${rule.id}`);
|
|
119
|
+
if (typeof matchResult === 'boolean') {
|
|
120
|
+
return matchResult;
|
|
121
|
+
}
|
|
122
|
+
return matchResult.result;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
shared_1.logger.error({ ...logContext, ruleId: rule === null || rule === void 0 ? void 0 : rule.id, error }, 'error in rule match function');
|
|
118
126
|
}
|
|
119
|
-
return matchResult.result;
|
|
120
127
|
});
|
|
121
128
|
if (!matchingRule) {
|
|
122
129
|
shared_1.logger.debug(logContext, 'no matching rule found');
|
|
@@ -124,7 +131,7 @@ class RuleExecutor {
|
|
|
124
131
|
}
|
|
125
132
|
const matchResult = matchingRule.matches(request);
|
|
126
133
|
logContext.ruleId = matchingRule.id;
|
|
127
|
-
shared_1.logger.debug(logContext, 'matching rule
|
|
134
|
+
shared_1.logger.debug({ ...logContext, matchResultMessage: typeof matchResult !== 'boolean' ? matchResult.description : null }, 'found matching rule');
|
|
128
135
|
const matchingRuleClone = Object.freeze(Object.assign({}, matchingRule));
|
|
129
136
|
++matchingRule.counter;
|
|
130
137
|
logContext.ruleCounter = matchingRule.counter;
|
|
@@ -169,4 +176,10 @@ class RuleExecutor {
|
|
|
169
176
|
return this._rules.find((r) => r.id === id);
|
|
170
177
|
}
|
|
171
178
|
}
|
|
172
|
-
|
|
179
|
+
const getRuleExecutor = (mockUuid) => {
|
|
180
|
+
if (!ruleExecutors[mockUuid]) {
|
|
181
|
+
ruleExecutors[mockUuid] = new RuleExecutor(rules_1.DEFAULT_RULES.map((r) => ({ ...r, ttlSeconds: Infinity })));
|
|
182
|
+
}
|
|
183
|
+
return ruleExecutors[mockUuid];
|
|
184
|
+
};
|
|
185
|
+
exports.getRuleExecutor = getRuleExecutor;
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stuntman/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Stuntman - HTTP proxy / mock server with API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/andrzej-woof/stuntman.git"
|
|
9
9
|
},
|
|
10
|
+
"homepage": "https://github.com/andrzej-woof/stuntman#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/andrzej-woof/stuntman/issues"
|
|
13
|
+
},
|
|
10
14
|
"keywords": [
|
|
11
15
|
"proxy",
|
|
12
16
|
"mock",
|
|
@@ -28,23 +32,32 @@
|
|
|
28
32
|
"author": "Andrzej Pasterczyk",
|
|
29
33
|
"license": "MIT",
|
|
30
34
|
"dependencies": {
|
|
31
|
-
"@stuntman/shared": "^0.1.
|
|
35
|
+
"@stuntman/shared": "^0.1.2",
|
|
32
36
|
"await-lock": "2.2.2",
|
|
33
37
|
"express": "5.0.0-beta.1",
|
|
34
38
|
"lru-cache": "7.16.0",
|
|
35
39
|
"object-sizeof": "2.6.1",
|
|
36
|
-
"pug": "
|
|
40
|
+
"pug": "3.0.2",
|
|
37
41
|
"serialize-javascript": "6.0.1",
|
|
42
|
+
"undici": "5.20.0",
|
|
38
43
|
"uuid": "9.0.0"
|
|
39
44
|
},
|
|
40
45
|
"devDependencies": {
|
|
46
|
+
"@prettier/plugin-pug": "2.4.1",
|
|
41
47
|
"@types/express": "4.17.17",
|
|
42
48
|
"@types/serialize-javascript": "5.0.2",
|
|
43
|
-
"@types/uuid": "9.0.0"
|
|
49
|
+
"@types/uuid": "9.0.0",
|
|
50
|
+
"prettier": "2.8.4"
|
|
44
51
|
},
|
|
45
52
|
"bin": {
|
|
46
53
|
"stuntman": "./dist/bin/stuntman.js"
|
|
47
54
|
},
|
|
55
|
+
"files": [
|
|
56
|
+
"dist/",
|
|
57
|
+
"README.md",
|
|
58
|
+
"LICENSE",
|
|
59
|
+
"CHANGELOG.md"
|
|
60
|
+
],
|
|
48
61
|
"scripts": {
|
|
49
62
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
50
63
|
"clean": "rm -fr dist",
|
package/config/default.json
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"stuntman": {
|
|
3
|
-
"mock": {
|
|
4
|
-
"domain": "stuntman",
|
|
5
|
-
"port": 2015,
|
|
6
|
-
"timeout": 60000,
|
|
7
|
-
"externalDns": ["8.8.8.8", "1.1.1.1"]
|
|
8
|
-
},
|
|
9
|
-
"api": {
|
|
10
|
-
"port": 1985,
|
|
11
|
-
"disabled": false
|
|
12
|
-
},
|
|
13
|
-
"webgui": {
|
|
14
|
-
"disabled": false
|
|
15
|
-
},
|
|
16
|
-
"storage": {
|
|
17
|
-
"traffic": {
|
|
18
|
-
"limitCount": 500,
|
|
19
|
-
"limitSize": 524288000
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
package/config/test.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"stuntman": {
|
|
3
|
-
"mock": {
|
|
4
|
-
"domain": "stuntman",
|
|
5
|
-
"port": 80,
|
|
6
|
-
"httpsPort": 443,
|
|
7
|
-
"httpsCert": "-----BEGIN CERTIFICATE-----\nMIIC7jCCAdYCCQDHj59tQDx5iTANBgkqhkiG9w0BAQsFADA5MREwDwYDVQQKDAhz\ndHVudG1hbjERMA8GA1UECwwIc3R1bnRtYW4xETAPBgNVBAMMCHN0dW50bWFuMB4X\nDTIzMDIxNjE1MzQzNFoXDTI0MDIxNjE1MzQzNFowOTERMA8GA1UECgwIc3R1bnRt\nYW4xETAPBgNVBAsMCHN0dW50bWFuMREwDwYDVQQDDAhzdHVudG1hbjCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK23MCp+grcLiyFzxvSU9iOFReIKZRVN\n0+DTlxl/sN4pvYlt7Hji7n/Yh55m7ACF1j8LRjaU6MOYIGofF4lbgA3nEbZVNJoH\nAtlSjk/JZc4LnDFinAWWxna2FpwrpfEnknIJ2B7fjtk5dM/WzSMn1MdPiC9V9Ee0\nPFe0PlpFl/hQSd4/VXfLxNy3bzW5AXa5CuTVRaEmts21TbL4VYe6KNPMkbTe+NJh\nwBrwVqS5lB3Z5racxOn5Dw5g5NuHgSA6LvUxdKhdkPs7y7e87XADadakibd9u02j\nimJwQih31O4rPJINLDYhVj5muyPGw9lpxEQ7UthxRxuzodm4F+5ZM1ECAwEAATAN\nBgkqhkiG9w0BAQsFAAOCAQEAhqISsPYrM+G37vw8I6YCWNSW0dJrpvfNpiz6oXal\nicIxOJz06qg0HsEXoWhdneo9PSA66KAmdcTplwPJtZ486izwD3F46+TZLkesOuCS\nDW9ihEPY5XPyjZDSz2J4EwBD4pH0AFeXSVFDIyXCSoWypSKjSq5lm7hOQuCOLkkm\ntlsptc4R3MGuvNYKSDBvxCjTy76jlXpMWINdVV18M4bVmRnVj+vYlQbYP5tCYGUm\nnzlFVi0dCLdvS2LGiKhARLQILP9YzC86a9UDPyWs703Zvqm5cnknCLEpjaR8dhd8\njAcDPHUe1RkR8wGrGwkkrIQfe8r8ovEylJgLT8HtNLqEXg==\n-----END CERTIFICATE-----\n",
|
|
8
|
-
"httpsKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArbcwKn6CtwuLIXPG9JT2I4VF4gplFU3T4NOXGX+w3im9iW3s\neOLuf9iHnmbsAIXWPwtGNpTow5ggah8XiVuADecRtlU0mgcC2VKOT8llzgucMWKc\nBZbGdrYWnCul8SeScgnYHt+O2Tl0z9bNIyfUx0+IL1X0R7Q8V7Q+WkWX+FBJ3j9V\nd8vE3LdvNbkBdrkK5NVFoSa2zbVNsvhVh7oo08yRtN740mHAGvBWpLmUHdnmtpzE\n6fkPDmDk24eBIDou9TF0qF2Q+zvLt7ztcANp1qSJt327TaOKYnBCKHfU7is8kg0s\nNiFWPma7I8bD2WnERDtS2HFHG7Oh2bgX7lkzUQIDAQABAoIBAH+L7GKXBvTNFfeW\n4XK9WMgV14yzIyr0POhrkxrWxY8pSI/6VNEhlgnqexET8p4jpn4dkg0LYqgSL2Kb\nt5VTyH7stPWSNBAPq8jTM8hjUEtr/N/JzlLQNKH+6jT6W1noOz9d+QAaFvFpnVnp\nFi+E1FcPDyfqTXTEYjXnEo0HYiCf5RAIw64VYRR3OfmCWFHjwz3sDbhvTW1bYfZA\nddwViTIoELfebF3cCLg4zWVkyCeZRpmbRJaeyttrqgOLbjD6tn7SFkZVJ8v4BBoK\nZSdRaFrzPrRxBYcLRbXlIaNp0QeM/NkSBZIg63vhwZydR+Y3wDE4mCzZ8UqiOyLC\nGIdHky0CgYEA1yY1WJ1ubOB67w4dckWMw9SOw00AP/i5o0j4kFz8nkcWhx8W6rJW\nrZrPq6yAZ6ffzR4aIwrq0W22nHB20sOvamO7UH7TzG3BuM787ChMqB0bEy98hKHZ\nHTAOKyqG9A50N+QNicUS9gXDWk67/i3j19bw8rLWMrNsQDM9SZHYvaMCgYEAzrMB\nz+ofYQ04z7gIKmlOK+lG9pT3JyNVdnLnHFhil4Q4AKMDDIsoKkDqjE2jJeTNzE+G\nIGlZyiBK6sArTJdNthrvrJLmvJJfVEGWpSnShNxDf+gzIJeUoA/TCJvUac1CXd8g\nHwnhR3Dp1I3SZwm32Hig/vzxe8Dd+YONPoNm8nsCgYBJ1pcgXodzXmdSe+mnOi9h\nViXY6ShYzCgJ3hVQllksiQE2Rnk6+xG8axEyvfUjnf21C8u0kx6b2ad+cSqWkwo0\n3R2ANsbBtjlyD7fF5N7KI5MTNozpiBJXbhKuxd2jDQLd26q5yaUEQl4VNEhYp682\neFIhOTdCF0njjrJN+XwFOQKBgF6j2aWQBhQS0LtTAPIiSzeR1PscE9notL3KOIVi\n9ql3UYkBGmlI4fgOxxW8ioHUNGJi2v/GHOWOSZ8Yo/qqoFtMFAdJL7qRrnJOoaI3\n9vr8Oy+6aoZ2wQdUl4SujOBwqf1/Jx7vECX8ziOTWA3zhijoepalzA+krD4NfMNt\nuNo3AoGBAISjSwEUrpR3II4uj8UuPZaVFNvACujaJLWnKcGwFvQsn/2GTcfzdzm/\nTxwHwpRZLJhFFpboFGVW5pX7g9leqdZERGlqPpTkCSAiQ0eFRbHsIhX7TS9VyYMz\n7iLq9dccEn5DDnUVXWkZxz2h0yG8/nTlNGli0BL2O+DEWZ9b32xE\n-----END RSA PRIVATE KEY-----\n",
|
|
9
|
-
"timeout": 60000,
|
|
10
|
-
"externalDns": ["8.8.8.8", "1.1.1.1"]
|
|
11
|
-
},
|
|
12
|
-
"api": {
|
|
13
|
-
"port": 1985,
|
|
14
|
-
"disabled": false
|
|
15
|
-
},
|
|
16
|
-
"webgui": {
|
|
17
|
-
"disabled": false
|
|
18
|
-
},
|
|
19
|
-
"storage": {
|
|
20
|
-
"traffic": {
|
|
21
|
-
"limitCount": 500,
|
|
22
|
-
"limitSize": 524288000
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|