@stuntman/server 0.1.5 → 0.1.7
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 +3 -3
- package/package.json +8 -6
- package/src/api/api.ts +69 -48
- package/src/api/utils.ts +26 -26
- package/src/api/validators.ts +44 -35
- package/src/api/webgui/rules.pug +4 -2
- package/src/api/webgui/style.css +3 -3
- package/src/api/webgui/traffic.pug +1 -0
- package/src/bin/stuntman.ts +2 -2
- package/src/ipUtils.ts +1 -1
- package/src/mock.ts +137 -153
- package/src/ruleExecutor.ts +2 -1
- package/src/rules/index.ts +5 -5
- package/src/storage.ts +2 -2
- package/dist/api/api.d.ts +0 -22
- package/dist/api/api.js +0 -188
- package/dist/api/utils.d.ts +0 -4
- package/dist/api/utils.js +0 -69
- package/dist/api/validators.d.ts +0 -3
- package/dist/api/validators.js +0 -118
- package/dist/api/webgui/rules.pug +0 -145
- package/dist/api/webgui/style.css +0 -28
- package/dist/api/webgui/traffic.pug +0 -37
- package/dist/bin/stuntman.d.ts +0 -2
- package/dist/bin/stuntman.js +0 -7
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -5
- package/dist/ipUtils.d.ts +0 -17
- package/dist/ipUtils.js +0 -101
- package/dist/mock.d.ts +0 -30
- package/dist/mock.js +0 -321
- package/dist/requestContext.d.ts +0 -9
- package/dist/requestContext.js +0 -18
- package/dist/ruleExecutor.d.ts +0 -22
- package/dist/ruleExecutor.js +0 -187
- package/dist/rules/catchAll.d.ts +0 -2
- package/dist/rules/catchAll.js +0 -15
- package/dist/rules/echo.d.ts +0 -2
- package/dist/rules/echo.js +0 -15
- package/dist/rules/index.d.ts +0 -3
- package/dist/rules/index.js +0 -70
- package/dist/storage.d.ts +0 -4
- package/dist/storage.js +0 -42
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ pnpm stuntman
|
|
|
31
31
|
|
|
32
32
|
Stuntman uses [config](https://github.com/node-config/node-config)
|
|
33
33
|
|
|
34
|
-
You can create `config/default.json` with settings of your liking matching `
|
|
34
|
+
You can create `config/default.json` with settings of your liking matching `Stuntman.Config` type
|
|
35
35
|
|
|
36
36
|
## Running as a package
|
|
37
37
|
|
|
@@ -55,9 +55,9 @@ node ./node_modules/.bin/stuntman
|
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
57
|
import { Mock } from '../mock';
|
|
58
|
-
import {
|
|
58
|
+
import { stuntmanConfig } from '@stuntman/shared';
|
|
59
59
|
|
|
60
|
-
const mock = new Mock(
|
|
60
|
+
const mock = new Mock(stuntmanConfig);
|
|
61
61
|
|
|
62
62
|
mock.start();
|
|
63
63
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stuntman/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Stuntman - HTTP proxy / mock server with API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -41,17 +41,19 @@
|
|
|
41
41
|
"pug": "3.0.2",
|
|
42
42
|
"serialize-javascript": "6.0.1",
|
|
43
43
|
"ts-import": "4.0.0-beta.10",
|
|
44
|
-
"typescript": "4.9.5",
|
|
45
44
|
"undici": "5.20.0",
|
|
46
45
|
"uuid": "9.0.0"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
48
|
+
"@jest/globals": "29.4.3",
|
|
49
49
|
"@prettier/plugin-pug": "2.4.1",
|
|
50
50
|
"@types/express": "4.17.17",
|
|
51
51
|
"@types/glob": "8.1.0",
|
|
52
52
|
"@types/serialize-javascript": "5.0.2",
|
|
53
53
|
"@types/uuid": "9.0.0",
|
|
54
|
-
"
|
|
54
|
+
"jest": "29.4.3",
|
|
55
|
+
"prettier": "2.8.4",
|
|
56
|
+
"typescript": "4.9.5"
|
|
55
57
|
},
|
|
56
58
|
"bin": {
|
|
57
59
|
"stuntman": "./dist/bin/stuntman.js"
|
|
@@ -64,11 +66,11 @@
|
|
|
64
66
|
"CHANGELOG.md"
|
|
65
67
|
],
|
|
66
68
|
"scripts": {
|
|
67
|
-
"test": "
|
|
69
|
+
"test": "SUPPRESS_NO_CONFIG_WARNING=1 jest",
|
|
68
70
|
"clean": "rm -fr dist",
|
|
69
71
|
"build": "tsc && cp -rv src/api/webgui dist/api",
|
|
70
|
-
"lint": "prettier --check
|
|
71
|
-
"lint:fix": "prettier --write ./{src,test} && eslint ./{src,test} --
|
|
72
|
+
"lint": "prettier --check \"./{src,test}/**/*\" && eslint \"./{src,test}/**/*\"",
|
|
73
|
+
"lint:fix": "prettier --write \"./{src,test}/**/*\" && eslint \"./{src,test}/**/*\" --fix",
|
|
72
74
|
"start": "node ./dist/bin/stuntman.js",
|
|
73
75
|
"start:dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' ./src/bin/stuntman.ts",
|
|
74
76
|
"start:debug": "node --inspect-brk=0.0.0.0 ./node_modules/.bin/ts-node --transpile-only ./src/bin/stuntman.ts"
|
package/src/api/api.ts
CHANGED
|
@@ -19,67 +19,81 @@ const API_KEY_HEADER = 'x-api-key';
|
|
|
19
19
|
|
|
20
20
|
export class API {
|
|
21
21
|
protected options: Required<ApiOptions>;
|
|
22
|
-
protected
|
|
22
|
+
protected webGuiOptions: Stuntman.WebGuiConfig;
|
|
23
|
+
protected apiApp: ExpressServer | null = null;
|
|
23
24
|
trafficStore: LRUCache<string, Stuntman.LogEntry>;
|
|
24
25
|
server: http.Server | null = null;
|
|
25
|
-
auth: (req: Request, type: 'read' | 'write') => void;
|
|
26
|
-
authReadOnly: (req: Request, res: Response, next: NextFunction) => void;
|
|
27
|
-
authReadWrite: (req: Request, res: Response, next: NextFunction) => void;
|
|
28
26
|
|
|
29
|
-
constructor(options: ApiOptions, webGuiOptions
|
|
27
|
+
constructor(options: ApiOptions, webGuiOptions: Stuntman.WebGuiConfig = { disabled: false }) {
|
|
30
28
|
if (!options.apiKeyReadOnly !== !options.apiKeyReadWrite) {
|
|
31
29
|
throw new Error('apiKeyReadOnly and apiKeyReadWrite options need to be set either both or none');
|
|
32
30
|
}
|
|
33
31
|
this.options = options;
|
|
32
|
+
this.webGuiOptions = webGuiOptions;
|
|
34
33
|
|
|
35
34
|
this.trafficStore = getTrafficStore(this.options.mockUuid);
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
|
|
35
|
+
this.auth = this.auth.bind(this);
|
|
36
|
+
this.authReadOnly = this.authReadOnly.bind(this);
|
|
37
|
+
this.authReadWrite = this.authReadWrite.bind(this);
|
|
38
|
+
}
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const hasValidReadKey = req.header(API_KEY_HEADER) === this.options.apiKeyReadOnly;
|
|
46
|
-
const hasValidWriteKey = req.header(API_KEY_HEADER) === this.options.apiKeyReadWrite;
|
|
47
|
-
const hasValidKey = type === 'read' ? hasValidReadKey || hasValidWriteKey : hasValidWriteKey;
|
|
48
|
-
if (!hasValidKey) {
|
|
49
|
-
throw new AppError({ httpCode: HttpCode.UNAUTHORIZED, message: 'unauthorized' });
|
|
50
|
-
}
|
|
40
|
+
private auth(req: Request, type: 'read' | 'write'): void {
|
|
41
|
+
if (!this.options.apiKeyReadOnly && !this.options.apiKeyReadWrite) {
|
|
51
42
|
return;
|
|
52
|
-
}
|
|
43
|
+
}
|
|
44
|
+
const hasValidReadKey = req.header(API_KEY_HEADER) === this.options.apiKeyReadOnly;
|
|
45
|
+
const hasValidWriteKey = req.header(API_KEY_HEADER) === this.options.apiKeyReadWrite;
|
|
46
|
+
const hasValidKey = type === 'read' ? hasValidReadKey || hasValidWriteKey : hasValidWriteKey;
|
|
47
|
+
if (!hasValidKey) {
|
|
48
|
+
throw new AppError({ httpCode: HttpCode.UNAUTHORIZED, message: 'unauthorized' });
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
protected authReadOnly(req: Request, _res: Response, next: NextFunction): void {
|
|
54
|
+
this.auth(req, 'read');
|
|
55
|
+
next();
|
|
56
|
+
}
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
protected authReadWrite(req: Request, _res: Response, next: NextFunction): void {
|
|
59
|
+
this.auth(req, 'write');
|
|
60
|
+
next();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private initApi() {
|
|
64
|
+
this.apiApp = express();
|
|
65
|
+
|
|
66
|
+
this.apiApp.use(express.json());
|
|
67
|
+
this.apiApp.use(express.text());
|
|
63
68
|
|
|
64
|
-
this.apiApp.use((req: Request,
|
|
69
|
+
this.apiApp.use((req: Request, _res: Response, next: NextFunction) => {
|
|
65
70
|
RequestContext.bind(req, this.options.mockUuid);
|
|
66
71
|
next();
|
|
67
72
|
});
|
|
68
73
|
|
|
69
|
-
this.apiApp.get('/rule', this.authReadOnly, async (
|
|
74
|
+
this.apiApp.get('/rule', this.authReadOnly.bind, async (_req, res) => {
|
|
70
75
|
res.send(stringify(await getRuleExecutor(this.options.mockUuid).getRules()));
|
|
71
76
|
});
|
|
72
77
|
|
|
73
78
|
this.apiApp.get('/rule/:ruleId', this.authReadOnly, async (req, res) => {
|
|
79
|
+
if (!req.params.ruleId) {
|
|
80
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'missing ruleId' });
|
|
81
|
+
}
|
|
74
82
|
res.send(stringify(await getRuleExecutor(this.options.mockUuid).getRule(req.params.ruleId)));
|
|
75
83
|
});
|
|
76
84
|
|
|
77
85
|
this.apiApp.get('/rule/:ruleId/disable', this.authReadWrite, (req, res) => {
|
|
86
|
+
if (!req.params.ruleId) {
|
|
87
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'missing ruleId' });
|
|
88
|
+
}
|
|
78
89
|
getRuleExecutor(this.options.mockUuid).disableRule(req.params.ruleId);
|
|
79
90
|
res.send();
|
|
80
91
|
});
|
|
81
92
|
|
|
82
93
|
this.apiApp.get('/rule/:ruleId/enable', this.authReadWrite, (req, res) => {
|
|
94
|
+
if (!req.params.ruleId) {
|
|
95
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'missing ruleId' });
|
|
96
|
+
}
|
|
83
97
|
getRuleExecutor(this.options.mockUuid).enableRule(req.params.ruleId);
|
|
84
98
|
res.send();
|
|
85
99
|
});
|
|
@@ -98,35 +112,41 @@ export class API {
|
|
|
98
112
|
);
|
|
99
113
|
|
|
100
114
|
this.apiApp.get('/rule/:ruleId/remove', this.authReadWrite, async (req, res) => {
|
|
115
|
+
if (!req.params.ruleId) {
|
|
116
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'missing ruleId' });
|
|
117
|
+
}
|
|
101
118
|
await getRuleExecutor(this.options.mockUuid).removeRule(req.params.ruleId);
|
|
102
119
|
res.send();
|
|
103
120
|
});
|
|
104
121
|
|
|
105
|
-
this.apiApp.get('/traffic', this.authReadOnly, (
|
|
106
|
-
const serializedTraffic:
|
|
107
|
-
for (const
|
|
108
|
-
serializedTraffic
|
|
122
|
+
this.apiApp.get('/traffic', this.authReadOnly, (_req, res) => {
|
|
123
|
+
const serializedTraffic: Stuntman.LogEntry[] = [];
|
|
124
|
+
for (const value of this.trafficStore.values()) {
|
|
125
|
+
serializedTraffic.push(value);
|
|
109
126
|
}
|
|
110
127
|
res.json(serializedTraffic);
|
|
111
128
|
});
|
|
112
129
|
|
|
113
130
|
this.apiApp.get('/traffic/:ruleIdOrLabel', this.authReadOnly, (req, res) => {
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
if (!req.params.ruleIdOrLabel) {
|
|
132
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'missing ruleIdOrLabel' });
|
|
133
|
+
}
|
|
134
|
+
const serializedTraffic: Stuntman.LogEntry[] = [];
|
|
135
|
+
for (const value of this.trafficStore.values()) {
|
|
116
136
|
if (value.mockRuleId === req.params.ruleIdOrLabel || (value.labels || []).includes(req.params.ruleIdOrLabel)) {
|
|
117
|
-
serializedTraffic
|
|
137
|
+
serializedTraffic.push(value);
|
|
118
138
|
}
|
|
119
139
|
}
|
|
120
140
|
res.json(serializedTraffic);
|
|
121
141
|
});
|
|
122
142
|
|
|
123
|
-
if (!webGuiOptions
|
|
143
|
+
if (!this.webGuiOptions.disabled) {
|
|
124
144
|
this.apiApp.set('views', __dirname + '/webgui');
|
|
125
145
|
this.apiApp.set('view engine', 'pug');
|
|
126
146
|
this.initWebGui();
|
|
127
147
|
}
|
|
128
148
|
|
|
129
|
-
this.apiApp.all(/.*/, (
|
|
149
|
+
this.apiApp.all(/.*/, (_req: Request, res: Response) => res.status(404).send());
|
|
130
150
|
|
|
131
151
|
this.apiApp.use((error: Error | AppError, req: Request, res: Response) => {
|
|
132
152
|
const ctx: RequestContext | null = RequestContext.get(req);
|
|
@@ -152,7 +172,10 @@ export class API {
|
|
|
152
172
|
}
|
|
153
173
|
|
|
154
174
|
private initWebGui() {
|
|
155
|
-
this.apiApp
|
|
175
|
+
if (!this.apiApp) {
|
|
176
|
+
throw new Error('initialization error');
|
|
177
|
+
}
|
|
178
|
+
this.apiApp.get('/webgui/rules', this.authReadOnly, async (_req, res) => {
|
|
156
179
|
const rules: Record<string, string> = {};
|
|
157
180
|
for (const rule of await getRuleExecutor(this.options.mockUuid).getRules()) {
|
|
158
181
|
rules[rule.id] = serializeJavascript(liveRuleToRule(rule), { unsafe: true });
|
|
@@ -160,7 +183,7 @@ export class API {
|
|
|
160
183
|
res.render('rules', { rules: escapedSerialize(rules), INDEX_DTS, ruleKeys: Object.keys(rules) });
|
|
161
184
|
});
|
|
162
185
|
|
|
163
|
-
this.apiApp.get('/webgui/traffic', this.authReadOnly, async (
|
|
186
|
+
this.apiApp.get('/webgui/traffic', this.authReadOnly, async (_req, res) => {
|
|
164
187
|
const serializedTraffic: Stuntman.LogEntry[] = [];
|
|
165
188
|
for (const value of this.trafficStore.values()) {
|
|
166
189
|
serializedTraffic.push(value);
|
|
@@ -190,13 +213,7 @@ export class API {
|
|
|
190
213
|
id: rule.id,
|
|
191
214
|
matches: rule.matches,
|
|
192
215
|
ttlSeconds: rule.ttlSeconds,
|
|
193
|
-
|
|
194
|
-
actions: {
|
|
195
|
-
...(rule.actions.mockResponse
|
|
196
|
-
? { mockResponse: rule.actions.mockResponse }
|
|
197
|
-
: { modifyRequest: rule.actions.modifyRequest, modifyResponse: rule.actions.modifyResponse }),
|
|
198
|
-
},
|
|
199
|
-
}),
|
|
216
|
+
actions: rule.actions,
|
|
200
217
|
...(rule.disableAfterUse !== undefined && { disableAfterUse: rule.disableAfterUse }),
|
|
201
218
|
...(rule.isEnabled !== undefined && { isEnabled: rule.isEnabled }),
|
|
202
219
|
...(rule.labels !== undefined && { labels: rule.labels }),
|
|
@@ -214,6 +231,10 @@ export class API {
|
|
|
214
231
|
if (this.server) {
|
|
215
232
|
throw new Error('mock server already started');
|
|
216
233
|
}
|
|
234
|
+
this.initApi();
|
|
235
|
+
if (!this.apiApp) {
|
|
236
|
+
throw new Error('initialization error');
|
|
237
|
+
}
|
|
217
238
|
this.server = this.apiApp.listen(this.options.port, () => {
|
|
218
239
|
logger.info(`API listening on ${this.options.port}`);
|
|
219
240
|
});
|
package/src/api/utils.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const deserializeRule = (serializedRule: Stuntman.SerializedRule): Stuntm
|
|
|
11
11
|
id: serializedRule.id,
|
|
12
12
|
matches: (req: Stuntman.Request) => new Function('____arg0', serializedRule.matches.remoteFn)(req),
|
|
13
13
|
ttlSeconds: serializedRule.ttlSeconds,
|
|
14
|
+
actions: { mockResponse: { status: 200 } },
|
|
14
15
|
...(serializedRule.disableAfterUse !== undefined && { disableAfterUse: serializedRule.disableAfterUse }),
|
|
15
16
|
...(serializedRule.removeAfterUse !== undefined && { removeAfterUse: serializedRule.removeAfterUse }),
|
|
16
17
|
...(serializedRule.labels !== undefined && { labels: serializedRule.labels }),
|
|
@@ -18,37 +19,36 @@ export const deserializeRule = (serializedRule: Stuntman.SerializedRule): Stuntm
|
|
|
18
19
|
...(serializedRule.isEnabled !== undefined && { isEnabled: serializedRule.isEnabled }),
|
|
19
20
|
...(serializedRule.storeTraffic !== undefined && { storeTraffic: serializedRule.storeTraffic }),
|
|
20
21
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
mockResponse
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
rule.actions.modifyRequest = (req: Stuntman.Request) =>
|
|
22
|
+
// TODO store the original localFn and variables sent from client for web UI editing maybe
|
|
23
|
+
if (serializedRule.actions.mockResponse) {
|
|
24
|
+
rule.actions = {
|
|
25
|
+
mockResponse:
|
|
26
|
+
'remoteFn' in serializedRule.actions.mockResponse
|
|
27
|
+
? (req: Stuntman.Request) =>
|
|
28
|
+
new Function(
|
|
29
|
+
'____arg0',
|
|
30
|
+
(serializedRule.actions.mockResponse as Stuntman.SerializedRemotableFunction).remoteFn
|
|
31
|
+
)(req)
|
|
32
|
+
: serializedRule.actions.mockResponse,
|
|
33
|
+
};
|
|
34
|
+
} else {
|
|
35
|
+
rule.actions = {
|
|
36
|
+
...(serializedRule.actions.modifyRequest && {
|
|
37
|
+
modifyRequest: ((req: Stuntman.Request) =>
|
|
38
38
|
new Function(
|
|
39
39
|
'____arg0',
|
|
40
|
-
(serializedRule.actions
|
|
41
|
-
)(req)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
(serializedRule.actions.modifyRequest as Stuntman.SerializedRemotableFunction).remoteFn
|
|
41
|
+
)(req)) as Stuntman.RequestManipulationFn,
|
|
42
|
+
}),
|
|
43
|
+
...(serializedRule.actions.modifyResponse && {
|
|
44
|
+
modifyResponse: ((req: Stuntman.Request, res: Stuntman.Response) =>
|
|
45
45
|
new Function(
|
|
46
46
|
'____arg0',
|
|
47
47
|
'____arg1',
|
|
48
|
-
(serializedRule.actions
|
|
49
|
-
)(req, res)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
48
|
+
(serializedRule.actions.modifyResponse as Stuntman.SerializedRemotableFunction).remoteFn
|
|
49
|
+
)(req, res)) as Stuntman.ResponseManipulationFn,
|
|
50
|
+
}),
|
|
51
|
+
} as Stuntman.Actions;
|
|
52
52
|
}
|
|
53
53
|
logger.debug(rule, 'deserialized rule');
|
|
54
54
|
return rule;
|
package/src/api/validators.ts
CHANGED
|
@@ -14,44 +14,53 @@ export const validateSerializedRuleProperties = (rule: Stuntman.SerializedRule):
|
|
|
14
14
|
if (rule.priority && typeof rule.priority !== 'number') {
|
|
15
15
|
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.priority' });
|
|
16
16
|
}
|
|
17
|
-
if (typeof rule.actions !== '
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
if (typeof rule.actions !== 'object') {
|
|
18
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions - not an object' });
|
|
19
|
+
}
|
|
20
|
+
if (
|
|
21
|
+
rule.actions.proxyPass !== true &&
|
|
22
|
+
typeof rule.actions.mockResponse !== 'object' &&
|
|
23
|
+
typeof rule.actions.modifyRequest !== 'object' &&
|
|
24
|
+
typeof rule.actions.modifyResponse !== 'object'
|
|
25
|
+
) {
|
|
26
|
+
throw new AppError({
|
|
27
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
28
|
+
message: 'invalid rule.actions - missing one of: proxyPass, mockResponse, modifyRequest, modifyResponse',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (typeof rule.actions.mockResponse !== 'undefined') {
|
|
32
|
+
if (typeof rule.actions.mockResponse !== 'object') {
|
|
33
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
20
34
|
}
|
|
21
|
-
if (typeof rule.actions.mockResponse !== '
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
if ('remoteFn' in rule.actions.mockResponse && typeof rule.actions.mockResponse.remoteFn !== 'string') {
|
|
36
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
37
|
+
} else if ('status' in rule.actions.mockResponse) {
|
|
38
|
+
if (typeof rule.actions.mockResponse.status !== 'number') {
|
|
39
|
+
throw new AppError({
|
|
40
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
41
|
+
message: 'invalid rule.actions.mockResponse.status',
|
|
42
|
+
});
|
|
24
43
|
}
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
rule.actions.mockResponse.rawHeaders.some((header) => typeof header !== 'string'))
|
|
38
|
-
) {
|
|
39
|
-
throw new AppError({
|
|
40
|
-
httpCode: HttpCode.BAD_REQUEST,
|
|
41
|
-
message: 'invalid rule.actions.mockResponse.rawHeaders',
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
if (typeof rule.actions.mockResponse.body !== 'undefined' && typeof rule.actions.mockResponse.body !== 'string') {
|
|
45
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse.body' });
|
|
46
|
-
}
|
|
44
|
+
if (
|
|
45
|
+
typeof rule.actions.mockResponse.rawHeaders !== 'undefined' &&
|
|
46
|
+
(!Array.isArray(rule.actions.mockResponse.rawHeaders) ||
|
|
47
|
+
rule.actions.mockResponse.rawHeaders.some((header) => typeof header !== 'string'))
|
|
48
|
+
) {
|
|
49
|
+
throw new AppError({
|
|
50
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
51
|
+
message: 'invalid rule.actions.mockResponse.rawHeaders',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (typeof rule.actions.mockResponse.body !== 'undefined' && typeof rule.actions.mockResponse.body !== 'string') {
|
|
55
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse.body' });
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
58
|
+
}
|
|
59
|
+
if (typeof rule.actions.modifyRequest !== 'undefined' && typeof rule.actions.modifyRequest.remoteFn !== 'string') {
|
|
60
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyRequest' });
|
|
61
|
+
}
|
|
62
|
+
if (typeof rule.actions.modifyResponse !== 'undefined' && typeof rule.actions.modifyResponse.remoteFn !== 'string') {
|
|
63
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyResponse' });
|
|
55
64
|
}
|
|
56
65
|
if (
|
|
57
66
|
typeof rule.disableAfterUse !== 'undefined' &&
|
|
@@ -73,7 +82,7 @@ export const validateSerializedRuleProperties = (rule: Stuntman.SerializedRule):
|
|
|
73
82
|
) {
|
|
74
83
|
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.labels' });
|
|
75
84
|
}
|
|
76
|
-
if (rule.actions
|
|
85
|
+
if (rule.actions.mockResponse && rule.actions.modifyResponse) {
|
|
77
86
|
throw new AppError({
|
|
78
87
|
httpCode: HttpCode.BAD_REQUEST,
|
|
79
88
|
message: 'rule.actions.mockResponse and rule.actions.modifyResponse are mutually exclusive',
|
package/src/api/webgui/rules.pug
CHANGED
|
@@ -22,6 +22,7 @@ html
|
|
|
22
22
|
div(style='margin-left: 240px')
|
|
23
23
|
#container(style='height: 400px')
|
|
24
24
|
script.
|
|
25
|
+
/* eslint no-undef: 0 */
|
|
25
26
|
const uuidv4 = () => {
|
|
26
27
|
function getRandomSymbol(symbol) {
|
|
27
28
|
var array;
|
|
@@ -72,7 +73,6 @@ html
|
|
|
72
73
|
}
|
|
73
74
|
const editor = monaco.editor.create(document.getElementById('container'), {
|
|
74
75
|
theme: 'vs-dark',
|
|
75
|
-
autoIndent: true,
|
|
76
76
|
formatOnPaste: true,
|
|
77
77
|
formatOnType: true,
|
|
78
78
|
automaticLayout: true,
|
|
@@ -120,6 +120,8 @@ html
|
|
|
120
120
|
body: ruleFunctionText + '\n return STUNTMAN_RULE;',
|
|
121
121
|
}).then((response) => {
|
|
122
122
|
if (response.ok) {
|
|
123
|
+
const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?ruleId=${encodeURIComponent(newId)}`;
|
|
124
|
+
window.history.pushState({ path: newUrl }, '', newUrl);
|
|
123
125
|
window.location.reload();
|
|
124
126
|
return;
|
|
125
127
|
}
|
|
@@ -133,7 +135,7 @@ html
|
|
|
133
135
|
|
|
134
136
|
window.newRule = () => {
|
|
135
137
|
const ruleId = uuidv4();
|
|
136
|
-
const emptyRule = `import type * as Stuntman from
|
|
138
|
+
const emptyRule = `import type * as Stuntman from 'stuntman';\n\nvar STUNTMAN_RULE: Stuntman.Rule = { id: '${ruleId}', matches: (req: Stuntman.Request) => true, ttlSeconds: 600, actions: { mockResponse: { status: '200', body: '${ruleId}' }} };`;
|
|
137
139
|
models[ruleId] = monaco.editor.createModel(emptyRule, 'typescript', `file:///${ruleId}.ts`);
|
|
138
140
|
const ruleKeyNode = document.getElementById('ruleKeys').firstChild;
|
|
139
141
|
const ruleKeyNodeClone = ruleKeyNode.cloneNode(true);
|
package/src/api/webgui/style.css
CHANGED
package/src/bin/stuntman.ts
CHANGED
package/src/ipUtils.ts
CHANGED
|
@@ -55,7 +55,7 @@ export class IPUtils {
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
logger.debug({ ip: addresses, hostname }, 'resolved hostname');
|
|
58
|
-
resolve([addresses[0]
|
|
58
|
+
resolve([addresses[0]!, ...addresses.slice(1)]);
|
|
59
59
|
};
|
|
60
60
|
if (options?.useExternalDns) {
|
|
61
61
|
if (!this.externalDns) {
|