@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/src/api/api.ts
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
import express, { NextFunction, Request, Response, Express as ExpressServer } from 'express';
|
|
3
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
-
import { getTrafficStore } from '../storage';
|
|
5
|
-
import { ruleExecutor } from '../ruleExecutor';
|
|
6
|
-
import { logger, AppError, HttpCode, MAX_RULE_TTL_SECONDS, stringify, INDEX_DTS } from '@stuntman/shared';
|
|
7
|
-
import type * as Stuntman from '@stuntman/shared';
|
|
8
|
-
import RequestContext from '../requestContext';
|
|
9
|
-
import serializeJavascript from 'serialize-javascript';
|
|
10
|
-
import LRUCache from 'lru-cache';
|
|
11
|
-
import { validateDeserializedRule } from './validatiors';
|
|
12
|
-
import { deserializeRule, escapedSerialize, liveRuleToRule } from './utils';
|
|
13
|
-
|
|
14
|
-
type ApiOptions = Stuntman.ApiConfig & {
|
|
15
|
-
mockUuid: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export class API {
|
|
19
|
-
protected options: Required<ApiOptions>;
|
|
20
|
-
protected apiApp: ExpressServer;
|
|
21
|
-
trafficStore: LRUCache<string, Stuntman.LogEntry>;
|
|
22
|
-
server: http.Server | null = null;
|
|
23
|
-
|
|
24
|
-
constructor(options: ApiOptions, webGuiOptions?: Stuntman.WebGuiConfig) {
|
|
25
|
-
this.options = options;
|
|
26
|
-
|
|
27
|
-
this.trafficStore = getTrafficStore(this.options.mockUuid);
|
|
28
|
-
this.apiApp = express();
|
|
29
|
-
|
|
30
|
-
this.apiApp.use(express.json());
|
|
31
|
-
this.apiApp.use(express.text());
|
|
32
|
-
|
|
33
|
-
this.apiApp.use((req: Request, res: Response, next: NextFunction) => {
|
|
34
|
-
RequestContext.bind(req, this.options.mockUuid);
|
|
35
|
-
next();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
this.apiApp.get('/rule', async (req, res) => {
|
|
39
|
-
res.send(stringify(await ruleExecutor.getRules()));
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
this.apiApp.get('/rule/:ruleId', async (req, res) => {
|
|
43
|
-
res.send(stringify(await ruleExecutor.getRule(req.params.ruleId)));
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
this.apiApp.get('/rule/:ruleId/disable', (req, res) => {
|
|
47
|
-
ruleExecutor.disableRule(req.params.ruleId);
|
|
48
|
-
res.send();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
this.apiApp.get('/rule/:ruleId/enable', (req, res) => {
|
|
52
|
-
ruleExecutor.enableRule(req.params.ruleId);
|
|
53
|
-
res.send();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
this.apiApp.post('/rule', async (req: Request<object, string, Stuntman.SerializedRule>, res) => {
|
|
57
|
-
const deserializedRule = deserializeRule(req.body);
|
|
58
|
-
validateDeserializedRule(deserializedRule);
|
|
59
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
60
|
-
// @ts-ignore
|
|
61
|
-
const rule = await ruleExecutor.addRule(deserializedRule);
|
|
62
|
-
res.send(stringify(rule));
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
this.apiApp.get('/rule/:ruleId/remove', async (req, res) => {
|
|
66
|
-
await ruleExecutor.removeRule(req.params.ruleId);
|
|
67
|
-
res.send();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
this.apiApp.get('/traffic', (req, res) => {
|
|
71
|
-
const serializedTraffic: Record<string, Stuntman.LogEntry> = {};
|
|
72
|
-
for (const [key, value] of this.trafficStore.entries()) {
|
|
73
|
-
serializedTraffic[key] = value;
|
|
74
|
-
}
|
|
75
|
-
res.json(serializedTraffic);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
this.apiApp.get('/traffic/:ruleIdOrLabel', (req, res) => {
|
|
79
|
-
const serializedTraffic: Record<string, Stuntman.LogEntry> = {};
|
|
80
|
-
for (const [key, value] of this.trafficStore.entries()) {
|
|
81
|
-
if (value.mockRuleId === req.params.ruleIdOrLabel || (value.labels || []).includes(req.params.ruleIdOrLabel)) {
|
|
82
|
-
serializedTraffic[key] = value;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
res.json(serializedTraffic);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
this.apiApp.use((error: Error | AppError, req: Request, res: Response, _next: NextFunction) => {
|
|
89
|
-
const ctx: RequestContext | null = RequestContext.get(req);
|
|
90
|
-
const uuid = ctx?.uuid || uuidv4();
|
|
91
|
-
if (error instanceof AppError && error.isOperational && res) {
|
|
92
|
-
logger.error(error);
|
|
93
|
-
res.status(error.httpCode).json({
|
|
94
|
-
error: { message: error.message, httpCode: error.httpCode, stack: error.stack },
|
|
95
|
-
});
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
logger.error({ ...error, uuid }, 'Unexpected error');
|
|
99
|
-
if (res) {
|
|
100
|
-
res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
|
|
101
|
-
error: { message: error.message, httpCode: HttpCode.INTERNAL_SERVER_ERROR, uuid },
|
|
102
|
-
});
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
console.log('Application encountered a critical error. Exiting');
|
|
106
|
-
process.exit(1);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
this.apiApp.set('views', __dirname + '/webgui');
|
|
110
|
-
this.apiApp.set('view engine', 'pug');
|
|
111
|
-
|
|
112
|
-
if (!webGuiOptions?.disabled) {
|
|
113
|
-
this.initWebGui();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private initWebGui() {
|
|
118
|
-
this.apiApp.get('/webgui/rules', async (req, res) => {
|
|
119
|
-
const rules: Record<string, string> = {};
|
|
120
|
-
for (const rule of await ruleExecutor.getRules()) {
|
|
121
|
-
rules[rule.id] = serializeJavascript(liveRuleToRule(rule), { unsafe: true });
|
|
122
|
-
}
|
|
123
|
-
res.render('rules', { rules: escapedSerialize(rules), INDEX_DTS, ruleKeys: Object.keys(rules) });
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
this.apiApp.get('/webgui/traffic', async (req, res) => {
|
|
127
|
-
const serializedTraffic: Stuntman.LogEntry[] = [];
|
|
128
|
-
for (const value of this.trafficStore.values()) {
|
|
129
|
-
serializedTraffic.push(value);
|
|
130
|
-
}
|
|
131
|
-
res.render('traffic', {
|
|
132
|
-
traffic: JSON.stringify(
|
|
133
|
-
serializedTraffic.sort((a, b) => b.originalRequest.timestamp - a.originalRequest.timestamp)
|
|
134
|
-
),
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// TODO make webui way better and safer, nicer formatting, eslint/prettier, blackjack and hookers
|
|
139
|
-
|
|
140
|
-
this.apiApp.post('/webgui/rules/unsafesave', async (req, res) => {
|
|
141
|
-
const rule: Stuntman.Rule = new Function(req.body)();
|
|
142
|
-
if (
|
|
143
|
-
!rule ||
|
|
144
|
-
!rule.id ||
|
|
145
|
-
typeof rule.matches !== 'function' ||
|
|
146
|
-
typeof rule.ttlSeconds !== 'number' ||
|
|
147
|
-
rule.ttlSeconds > MAX_RULE_TTL_SECONDS
|
|
148
|
-
) {
|
|
149
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'Invalid rule' });
|
|
150
|
-
}
|
|
151
|
-
await ruleExecutor.addRule(
|
|
152
|
-
{
|
|
153
|
-
id: rule.id,
|
|
154
|
-
matches: rule.matches,
|
|
155
|
-
ttlSeconds: rule.ttlSeconds,
|
|
156
|
-
...(rule.actions && {
|
|
157
|
-
actions: {
|
|
158
|
-
...(rule.actions.mockResponse
|
|
159
|
-
? { mockResponse: rule.actions.mockResponse }
|
|
160
|
-
: { modifyRequest: rule.actions.modifyRequest, modifyResponse: rule.actions.modifyResponse }),
|
|
161
|
-
},
|
|
162
|
-
}),
|
|
163
|
-
...(rule.disableAfterUse !== undefined && { disableAfterUse: rule.disableAfterUse }),
|
|
164
|
-
...(rule.isEnabled !== undefined && { isEnabled: rule.isEnabled }),
|
|
165
|
-
...(rule.labels !== undefined && { labels: rule.labels }),
|
|
166
|
-
...(rule.priority !== undefined && { priority: rule.priority }),
|
|
167
|
-
...(rule.removeAfterUse !== undefined && { removeAfterUse: rule.removeAfterUse }),
|
|
168
|
-
...(rule.storeTraffic !== undefined && { storeTraffic: rule.storeTraffic }),
|
|
169
|
-
},
|
|
170
|
-
true
|
|
171
|
-
);
|
|
172
|
-
res.send();
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
public start() {
|
|
177
|
-
if (this.server) {
|
|
178
|
-
throw new Error('mock server already started');
|
|
179
|
-
}
|
|
180
|
-
this.server = this.apiApp.listen(this.options.port, () => {
|
|
181
|
-
logger.info(`API listening on ${this.options.port}`);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
public stop() {
|
|
186
|
-
if (!this.server) {
|
|
187
|
-
throw new Error('mock server not started');
|
|
188
|
-
}
|
|
189
|
-
this.server.close((error) => {
|
|
190
|
-
logger.warn(error, 'problem closing server');
|
|
191
|
-
this.server = null;
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
package/src/api/utils.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import serializeJavascript from 'serialize-javascript';
|
|
2
|
-
import type * as Stuntman from '@stuntman/shared';
|
|
3
|
-
import { logger } from '@stuntman/shared';
|
|
4
|
-
import { validateSerializedRuleProperties } from './validatiors';
|
|
5
|
-
|
|
6
|
-
// TODO
|
|
7
|
-
export const deserializeRule = (serializedRule: Stuntman.SerializedRule): Stuntman.Rule => {
|
|
8
|
-
logger.debug(serializedRule, 'attempt to deserialize rule');
|
|
9
|
-
validateSerializedRuleProperties(serializedRule);
|
|
10
|
-
const rule: Stuntman.Rule = {
|
|
11
|
-
id: serializedRule.id,
|
|
12
|
-
matches: (req: Stuntman.Request) => new Function('____arg0', serializedRule.matches.remoteFn)(req),
|
|
13
|
-
ttlSeconds: serializedRule.ttlSeconds,
|
|
14
|
-
...(serializedRule.disableAfterUse !== undefined && { disableAfterUse: serializedRule.disableAfterUse }),
|
|
15
|
-
...(serializedRule.removeAfterUse !== undefined && { removeAfterUse: serializedRule.removeAfterUse }),
|
|
16
|
-
...(serializedRule.labels !== undefined && { labels: serializedRule.labels }),
|
|
17
|
-
...(serializedRule.priority !== undefined && { priority: serializedRule.priority }),
|
|
18
|
-
...(serializedRule.isEnabled !== undefined && { isEnabled: serializedRule.isEnabled }),
|
|
19
|
-
...(serializedRule.storeTraffic !== undefined && { storeTraffic: serializedRule.storeTraffic }),
|
|
20
|
-
};
|
|
21
|
-
if (serializedRule.actions) {
|
|
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
|
-
if (serializedRule.actions.modifyRequest) {
|
|
37
|
-
rule.actions.modifyRequest = (req: Stuntman.Request) =>
|
|
38
|
-
new Function(
|
|
39
|
-
'____arg0',
|
|
40
|
-
(serializedRule.actions?.modifyRequest as Stuntman.SerializedRemotableFunction).remoteFn
|
|
41
|
-
)(req);
|
|
42
|
-
}
|
|
43
|
-
if (serializedRule.actions.modifyResponse) {
|
|
44
|
-
rule.actions.modifyResponse = (req: Stuntman.Request, res: Stuntman.Response) =>
|
|
45
|
-
new Function(
|
|
46
|
-
'____arg0',
|
|
47
|
-
'____arg1',
|
|
48
|
-
(serializedRule.actions?.modifyResponse as Stuntman.SerializedRemotableFunction).remoteFn
|
|
49
|
-
)(req, res);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
logger.debug(rule, 'deserialized rule');
|
|
54
|
-
return rule;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const escapedSerialize = (obj: any) =>
|
|
58
|
-
serializeJavascript(obj).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, "\\n'\n+ '");
|
|
59
|
-
|
|
60
|
-
export const liveRuleToRule = (liveRule: Stuntman.LiveRule) => {
|
|
61
|
-
const ruleClone: Stuntman.Rule = { ...liveRule };
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
63
|
-
// @ts-ignore
|
|
64
|
-
delete ruleClone.counter;
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
66
|
-
// @ts-ignore
|
|
67
|
-
delete ruleClone.createdTimestamp;
|
|
68
|
-
return ruleClone;
|
|
69
|
-
};
|
package/src/api/validatiors.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { AppError, HttpCode, MAX_RULE_TTL_SECONDS, MIN_RULE_TTL_SECONDS, logger, RawHeaders } from '@stuntman/shared';
|
|
2
|
-
import type * as Stuntman from '@stuntman/shared';
|
|
3
|
-
|
|
4
|
-
export const validateSerializedRuleProperties = (rule: Stuntman.SerializedRule): void => {
|
|
5
|
-
if (!rule) {
|
|
6
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid serialized rule' });
|
|
7
|
-
}
|
|
8
|
-
if (typeof rule.id !== 'string') {
|
|
9
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.id' });
|
|
10
|
-
}
|
|
11
|
-
if (typeof rule.matches !== 'object' || !('remoteFn' in rule.matches) || typeof rule.matches.remoteFn !== 'string') {
|
|
12
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.matches' });
|
|
13
|
-
}
|
|
14
|
-
if (rule.priority && typeof rule.priority !== 'number') {
|
|
15
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.priority' });
|
|
16
|
-
}
|
|
17
|
-
if (typeof rule.actions !== 'undefined') {
|
|
18
|
-
if (typeof rule.actions !== 'object') {
|
|
19
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions' });
|
|
20
|
-
}
|
|
21
|
-
if (typeof rule.actions.mockResponse !== 'undefined') {
|
|
22
|
-
if (typeof rule.actions.mockResponse !== 'object') {
|
|
23
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
24
|
-
}
|
|
25
|
-
if ('remoteFn' in rule.actions.mockResponse && typeof rule.actions.mockResponse.remoteFn !== 'string') {
|
|
26
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
27
|
-
} else if ('status' in rule.actions.mockResponse) {
|
|
28
|
-
if (typeof rule.actions.mockResponse.status !== 'number') {
|
|
29
|
-
throw new AppError({
|
|
30
|
-
httpCode: HttpCode.BAD_REQUEST,
|
|
31
|
-
message: 'invalid rule.actions.mockResponse.status',
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
if (
|
|
35
|
-
typeof rule.actions.mockResponse.rawHeaders !== 'undefined' &&
|
|
36
|
-
(!Array.isArray(rule.actions.mockResponse.rawHeaders) ||
|
|
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
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (typeof rule.actions.modifyRequest !== 'undefined' && typeof rule.actions.modifyRequest.remoteFn !== 'string') {
|
|
50
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyRequest' });
|
|
51
|
-
}
|
|
52
|
-
if (typeof rule.actions.modifyResponse !== 'undefined' && typeof rule.actions.modifyResponse.remoteFn !== 'string') {
|
|
53
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyResponse' });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (
|
|
57
|
-
typeof rule.disableAfterUse !== 'undefined' &&
|
|
58
|
-
typeof rule.disableAfterUse !== 'boolean' &&
|
|
59
|
-
(typeof rule.disableAfterUse !== 'number' || rule.disableAfterUse < 0)
|
|
60
|
-
) {
|
|
61
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.disableAfterUse' });
|
|
62
|
-
}
|
|
63
|
-
if (
|
|
64
|
-
typeof rule.removeAfterUse !== 'undefined' &&
|
|
65
|
-
typeof rule.removeAfterUse !== 'boolean' &&
|
|
66
|
-
(typeof rule.removeAfterUse !== 'number' || rule.removeAfterUse < 0)
|
|
67
|
-
) {
|
|
68
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.removeAfterUse' });
|
|
69
|
-
}
|
|
70
|
-
if (
|
|
71
|
-
typeof rule.labels !== 'undefined' &&
|
|
72
|
-
(!Array.isArray(rule.labels) || rule.labels.some((label) => typeof label !== 'string'))
|
|
73
|
-
) {
|
|
74
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.labels' });
|
|
75
|
-
}
|
|
76
|
-
if (rule.actions?.mockResponse && rule.actions.modifyResponse) {
|
|
77
|
-
throw new AppError({
|
|
78
|
-
httpCode: HttpCode.BAD_REQUEST,
|
|
79
|
-
message: 'rule.actions.mockResponse and rule.actions.modifyResponse are mutually exclusive',
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
if (!rule.ttlSeconds || rule.ttlSeconds < MIN_RULE_TTL_SECONDS || rule.ttlSeconds > MAX_RULE_TTL_SECONDS) {
|
|
83
|
-
throw new AppError({
|
|
84
|
-
httpCode: HttpCode.BAD_REQUEST,
|
|
85
|
-
message: `rule.ttlSeconds should be within ${MIN_RULE_TTL_SECONDS} and ${MAX_RULE_TTL_SECONDS}`,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
if (typeof rule.isEnabled !== 'undefined' && typeof rule.isEnabled !== 'boolean') {
|
|
89
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.isEnabled' });
|
|
90
|
-
}
|
|
91
|
-
if (typeof rule.storeTraffic !== 'undefined' && typeof rule.storeTraffic !== 'boolean') {
|
|
92
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.storeTraffic' });
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export const validateDeserializedRule = (deserializedRule: Stuntman.Rule) => {
|
|
97
|
-
// TODO validate other functions ?
|
|
98
|
-
let matchValidationResult: Stuntman.RuleMatchResult;
|
|
99
|
-
try {
|
|
100
|
-
matchValidationResult = deserializedRule.matches({
|
|
101
|
-
id: 'validation',
|
|
102
|
-
method: 'GET',
|
|
103
|
-
rawHeaders: new RawHeaders(),
|
|
104
|
-
timestamp: Date.now(),
|
|
105
|
-
url: 'http://dummy.invalid/',
|
|
106
|
-
});
|
|
107
|
-
} catch (error: any) {
|
|
108
|
-
logger.error({ ruleId: deserializedRule.id }, error);
|
|
109
|
-
throw new AppError({
|
|
110
|
-
httpCode: HttpCode.UNPROCESSABLE_ENTITY,
|
|
111
|
-
message: 'match function returned invalid value',
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
if (
|
|
115
|
-
matchValidationResult !== true &&
|
|
116
|
-
matchValidationResult !== false &&
|
|
117
|
-
matchValidationResult.result !== true &&
|
|
118
|
-
matchValidationResult.result !== false
|
|
119
|
-
) {
|
|
120
|
-
logger.error({ ruleId: deserializedRule.id, matchValidationResult }, 'match function retruned invalid value');
|
|
121
|
-
throw new AppError({ httpCode: HttpCode.UNPROCESSABLE_ENTITY, message: 'match function returned invalid value' });
|
|
122
|
-
}
|
|
123
|
-
};
|
package/src/api/webgui/rules.pug
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
doctype html
|
|
2
|
-
html
|
|
3
|
-
head
|
|
4
|
-
title Stuntman - rule editor
|
|
5
|
-
script(src='https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.35.0/min/vs/loader.min.js')
|
|
6
|
-
style
|
|
7
|
-
include style.css
|
|
8
|
-
body(style='color: rgb(204, 204, 204); background-color: rgb(50, 50, 50)')
|
|
9
|
-
button#newRule(type='button', onclick='window.newRule()') New rule
|
|
10
|
-
button#saveRule(type='button', onclick='window.saveRule()', disabled) Save rule
|
|
11
|
-
div(style='width: 100%; overflow: hidden')
|
|
12
|
-
div(style='width: 230px; float: left')
|
|
13
|
-
h3 Rules
|
|
14
|
-
ul#ruleKeys.no-bullets
|
|
15
|
-
each ruleId in ruleKeys
|
|
16
|
-
li
|
|
17
|
-
button.rule(
|
|
18
|
-
type='button',
|
|
19
|
-
onclick='window.setRuleModel(this.getAttribute("data-rule-id"))',
|
|
20
|
-
data-rule-id=ruleId
|
|
21
|
-
)= ruleId
|
|
22
|
-
div(style='margin-left: 240px')
|
|
23
|
-
#container(style='height: 400px')
|
|
24
|
-
script.
|
|
25
|
-
const uuidv4 = () => {
|
|
26
|
-
function getRandomSymbol(symbol) {
|
|
27
|
-
var array;
|
|
28
|
-
|
|
29
|
-
if (symbol === 'y') {
|
|
30
|
-
array = ['8', '9', 'a', 'b'];
|
|
31
|
-
return array[Math.floor(Math.random() * array.length)];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
array = new Uint8Array(1);
|
|
35
|
-
window.crypto.getRandomValues(array);
|
|
36
|
-
return (array[0] % 16).toString(16);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, getRandomSymbol);
|
|
40
|
-
};
|
|
41
|
-
require.config({
|
|
42
|
-
paths: {
|
|
43
|
-
vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.35.0/min/vs',
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
require(['vs/editor/editor.main'], function () {
|
|
47
|
-
monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true);
|
|
48
|
-
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
|
49
|
-
noSemanticValidation: false,
|
|
50
|
-
noSyntaxValidation: false,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// compiler options
|
|
54
|
-
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
|
55
|
-
target: monaco.languages.typescript.ScriptTarget.ES6,
|
|
56
|
-
allowNonTsExtensions: true,
|
|
57
|
-
noLib: true,
|
|
58
|
-
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
|
59
|
-
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
|
60
|
-
noEmit: true,
|
|
61
|
-
checkJs: true,
|
|
62
|
-
allowJs: true,
|
|
63
|
-
isolatedModules: true,
|
|
64
|
-
typeRoots: ['node_modules/@types'],
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
monaco.languages.typescript.typescriptDefaults.addExtraLib(`!{INDEX_DTS}`, 'file:///node_modules/@types/stuntman/index.d.ts');
|
|
68
|
-
const models = {};
|
|
69
|
-
const rules = eval('(!{rules})');
|
|
70
|
-
for (const ruleId of Object.keys(rules)) {
|
|
71
|
-
models[ruleId] = monaco.editor.createModel("import type * as Stuntman from 'stuntman';\n\nvar STUNTMAN_RULE: Stuntman.Rule = " + rules[ruleId] + ';', 'typescript', `file:///${ruleId}.ts`);
|
|
72
|
-
}
|
|
73
|
-
const editor = monaco.editor.create(document.getElementById('container'), {
|
|
74
|
-
theme: 'vs-dark',
|
|
75
|
-
autoIndent: true,
|
|
76
|
-
formatOnPaste: true,
|
|
77
|
-
formatOnType: true,
|
|
78
|
-
automaticLayout: true,
|
|
79
|
-
autoIndent: true,
|
|
80
|
-
tabSize: 2,
|
|
81
|
-
});
|
|
82
|
-
editor.onDidChangeModel((event) => {
|
|
83
|
-
const isInternal = /^file:\/\/\/internal\/.+/.test(event.newModelUrl);
|
|
84
|
-
if (isInternal) {
|
|
85
|
-
document.getElementById('saveRule').setAttribute('disabled', 'true');
|
|
86
|
-
} else {
|
|
87
|
-
document.getElementById('saveRule').removeAttribute('disabled');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
setTimeout(() => {
|
|
91
|
-
editor.getAction('editor.action.formatDocument').run();
|
|
92
|
-
}, 100);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
window.setRuleModel = (ruleId) => {
|
|
96
|
-
if (history.pushState) {
|
|
97
|
-
const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?ruleId=${encodeURIComponent(ruleId)}`;
|
|
98
|
-
window.history.pushState({ path: newUrl }, '', newUrl);
|
|
99
|
-
}
|
|
100
|
-
editor.setModel(models[ruleId]);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
104
|
-
if (urlSearchParams.has('ruleId') && urlSearchParams.get('ruleId') in models) {
|
|
105
|
-
editor.setModel(models[urlSearchParams.get('ruleId')]);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
window.saveRule = () => {
|
|
109
|
-
document.getElementById('saveRule').setAttribute('disabled', 'true');
|
|
110
|
-
const modelUri = editor.getModel().uri;
|
|
111
|
-
const result = monaco.languages.typescript.getTypeScriptWorker();
|
|
112
|
-
result.then((worker) => {
|
|
113
|
-
worker(modelUri).then(function (client) {
|
|
114
|
-
client.getEmitOutput(modelUri.toString()).then((output) => {
|
|
115
|
-
const ruleFunctionText = output.outputFiles[0].text.replace(/^export .+$/im, '');
|
|
116
|
-
const newId = new Function(ruleFunctionText + '\n return STUNTMAN_RULE;')().id;
|
|
117
|
-
fetch('/webgui/rules/unsafesave', {
|
|
118
|
-
method: 'POST',
|
|
119
|
-
headers: { 'content-type': 'text/plain' },
|
|
120
|
-
body: ruleFunctionText + '\n return STUNTMAN_RULE;',
|
|
121
|
-
}).then((response) => {
|
|
122
|
-
if (response.ok) {
|
|
123
|
-
window.location.reload();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
alert('Error when saving rule');
|
|
127
|
-
document.getElementById('saveRule').removeAttribute('disabled');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
window.newRule = () => {
|
|
135
|
-
const ruleId = uuidv4();
|
|
136
|
-
const emptyRule = `import type * as Stuntman from \'stuntman\';\n\nvar STUNTMAN_RULE: Stuntman.Rule = { id: '${ruleId}', matches: (req: Stuntman.Request) => true, ttlSeconds: 600 };`;
|
|
137
|
-
models[ruleId] = monaco.editor.createModel(emptyRule, 'typescript', `file:///${ruleId}.ts`);
|
|
138
|
-
const ruleKeyNode = document.getElementById('ruleKeys').firstChild;
|
|
139
|
-
const ruleKeyNodeClone = ruleKeyNode.cloneNode(true);
|
|
140
|
-
ruleKeyNodeClone.getElementsByTagName('button')[0].setAttribute('data-rule-id', ruleId);
|
|
141
|
-
ruleKeyNodeClone.getElementsByTagName('button')[0].innerText = ruleId;
|
|
142
|
-
document.getElementById('ruleKeys').appendChild(ruleKeyNodeClone);
|
|
143
|
-
window.setRuleModel(ruleId);
|
|
144
|
-
};
|
|
145
|
-
});
|
package/src/api/webgui/style.css
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
div#container {
|
|
2
|
-
resize: vertical;
|
|
3
|
-
overflow: auto;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
body {
|
|
7
|
-
font-family: Menlo, Monaco, 'Courier New', monospace;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
button.rule {
|
|
11
|
-
font-family: Menlo, Monaco, 'Courier New', monospace;
|
|
12
|
-
background: none !important;
|
|
13
|
-
border: none;
|
|
14
|
-
padding: 0 !important;
|
|
15
|
-
color: #aaa;
|
|
16
|
-
text-decoration: underline;
|
|
17
|
-
cursor: pointer;
|
|
18
|
-
margin-top: 10px;
|
|
19
|
-
font-size: x-small;
|
|
20
|
-
text-align: left;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
ul.no-bullets {
|
|
24
|
-
list-style-type: none; /* Remove bullets */
|
|
25
|
-
padding: 0; /* Remove padding */
|
|
26
|
-
margin: 0; /* Remove margins */
|
|
27
|
-
text-align: left;
|
|
28
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
doctype html
|
|
2
|
-
html
|
|
3
|
-
head
|
|
4
|
-
title Stuntman - rule editor
|
|
5
|
-
script(src='https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.35.0/min/vs/loader.min.js')
|
|
6
|
-
style
|
|
7
|
-
include style.css
|
|
8
|
-
body(style='color: rgb(204, 204, 204); background-color: rgb(50, 50, 50)')
|
|
9
|
-
div(style='width: 100%; overflow: hidden')
|
|
10
|
-
div(style='width: 200px; float: left')
|
|
11
|
-
h3 Traffic log
|
|
12
|
-
div(style='margin-left: 220px')
|
|
13
|
-
#container(style='height: 800px')
|
|
14
|
-
script.
|
|
15
|
-
require.config({
|
|
16
|
-
paths: {
|
|
17
|
-
vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.35.0/min/vs',
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
require(['vs/editor/editor.main'], function () {
|
|
21
|
-
const traffic = !{ traffic };
|
|
22
|
-
const model = monaco.editor.createModel(JSON.stringify(traffic, null, 2), 'json');
|
|
23
|
-
const editor = monaco.editor.create(document.getElementById('container'), {
|
|
24
|
-
theme: 'vs-dark',
|
|
25
|
-
autoIndent: true,
|
|
26
|
-
formatOnPaste: true,
|
|
27
|
-
formatOnType: true,
|
|
28
|
-
automaticLayout: true,
|
|
29
|
-
readOnly: true,
|
|
30
|
-
});
|
|
31
|
-
editor.onDidChangeModel((event) => {
|
|
32
|
-
setTimeout(() => {
|
|
33
|
-
editor.getAction('editor.action.formatDocument').run();
|
|
34
|
-
}, 100);
|
|
35
|
-
});
|
|
36
|
-
editor.setModel(model);
|
|
37
|
-
});
|
package/src/bin/stuntman.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { Mock as StuntmanMock } from './mock';
|