@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/src/mock.ts
CHANGED
|
@@ -6,7 +6,7 @@ import express from 'express';
|
|
|
6
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
7
|
import { getRuleExecutor } from './ruleExecutor';
|
|
8
8
|
import { getTrafficStore } from './storage';
|
|
9
|
-
import { RawHeaders, logger, HttpCode } from '@stuntman/shared';
|
|
9
|
+
import { RawHeaders, logger, HttpCode, naiveGQLParser, escapeStringRegexp } from '@stuntman/shared';
|
|
10
10
|
import RequestContext from './requestContext';
|
|
11
11
|
import type * as Stuntman from '@stuntman/shared';
|
|
12
12
|
import { IPUtils } from './ipUtils';
|
|
@@ -17,42 +17,12 @@ type WithRequiredProperty<Type, Key extends keyof Type> = Type & {
|
|
|
17
17
|
[Property in Key]-?: Type[Property];
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
const naiveGQLParser = (body: Buffer | string): Stuntman.GQLRequestBody | undefined => {
|
|
21
|
-
try {
|
|
22
|
-
let json: Stuntman.GQLRequestBody | undefined = undefined;
|
|
23
|
-
try {
|
|
24
|
-
json = JSON.parse(Buffer.isBuffer(body) ? body.toString('utf-8') : body);
|
|
25
|
-
} catch (kiss) {
|
|
26
|
-
// and swallow
|
|
27
|
-
}
|
|
28
|
-
if (!json?.query && !json?.operationName) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const lines = json.query
|
|
32
|
-
.split('\n')
|
|
33
|
-
.map((l) => l.replace(/^\s+/g, '').trim())
|
|
34
|
-
.filter((l) => !!l);
|
|
35
|
-
if (/^query /.test(lines[0])) {
|
|
36
|
-
json.type = 'query';
|
|
37
|
-
} else if (/^mutation /.test(lines[0])) {
|
|
38
|
-
json.type = 'mutation';
|
|
39
|
-
} else {
|
|
40
|
-
throw new Error(`Unable to resolve query type of ${lines[0]}`);
|
|
41
|
-
}
|
|
42
|
-
json.methodName = lines[json.operationName ? 1 : 0].split('(')[0].split('{')[0];
|
|
43
|
-
return json;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
logger.debug(error, 'unable to parse GQL');
|
|
46
|
-
}
|
|
47
|
-
return undefined;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
20
|
// TODO add proper web proxy mode
|
|
51
21
|
|
|
52
22
|
export class Mock {
|
|
53
23
|
public readonly mockUuid: string;
|
|
54
|
-
protected options: Stuntman.
|
|
55
|
-
protected mockApp: express.Express;
|
|
24
|
+
protected options: Stuntman.Config;
|
|
25
|
+
protected mockApp: express.Express | null = null;
|
|
56
26
|
protected MOCK_DOMAIN_REGEX: RegExp;
|
|
57
27
|
protected URL_PORT_REGEX: RegExp;
|
|
58
28
|
protected server: http.Server | null = null;
|
|
@@ -60,7 +30,6 @@ export class Mock {
|
|
|
60
30
|
protected trafficStore: LRUCache<string, Stuntman.LogEntry>;
|
|
61
31
|
protected ipUtils: IPUtils | null = null;
|
|
62
32
|
private _api: API | null = null;
|
|
63
|
-
private requestHandler: (req: express.Request, res: express.Response) => Promise<void>;
|
|
64
33
|
|
|
65
34
|
get apiServer() {
|
|
66
35
|
if (this.options.api.disabled) {
|
|
@@ -76,7 +45,7 @@ export class Mock {
|
|
|
76
45
|
return getRuleExecutor(this.mockUuid);
|
|
77
46
|
}
|
|
78
47
|
|
|
79
|
-
constructor(options: Stuntman.
|
|
48
|
+
constructor(options: Stuntman.Config) {
|
|
80
49
|
this.mockUuid = uuidv4();
|
|
81
50
|
this.options = options;
|
|
82
51
|
if (this.options.mock.httpsPort && (!this.options.mock.httpsKey || !this.options.mock.httpsCert)) {
|
|
@@ -101,140 +70,151 @@ export class Mock {
|
|
|
101
70
|
? null
|
|
102
71
|
: new IPUtils({ mockUuid: this.mockUuid, externalDns: this.options.mock.externalDns });
|
|
103
72
|
|
|
104
|
-
this.requestHandler =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
73
|
+
this.requestHandler = this.requestHandler.bind(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async requestHandler(req: express.Request, res: express.Response): Promise<void> {
|
|
77
|
+
const ctx: RequestContext | null = RequestContext.get(req);
|
|
78
|
+
const requestUuid = ctx?.uuid || uuidv4();
|
|
79
|
+
const timestamp = Date.now();
|
|
80
|
+
const originalHostname = req.headers.host || req.hostname;
|
|
81
|
+
const unproxiedHostname = req.hostname.replace(this.MOCK_DOMAIN_REGEX, '');
|
|
82
|
+
const isProxiedHostname = originalHostname !== unproxiedHostname;
|
|
83
|
+
const originalRequest = {
|
|
84
|
+
id: requestUuid,
|
|
85
|
+
timestamp,
|
|
86
|
+
url: `${req.protocol}://${req.hostname}${req.originalUrl}`,
|
|
87
|
+
method: req.method,
|
|
88
|
+
rawHeaders: new RawHeaders(...req.rawHeaders),
|
|
89
|
+
...((Buffer.isBuffer(req.body) && { body: req.body.toString('utf-8') }) ||
|
|
90
|
+
(typeof req.body === 'string' && { body: req.body })),
|
|
91
|
+
};
|
|
92
|
+
logger.debug(originalRequest, 'processing request');
|
|
93
|
+
const logContext: Record<string, any> = {
|
|
94
|
+
requestId: originalRequest.id,
|
|
95
|
+
};
|
|
96
|
+
const mockEntry: WithRequiredProperty<Stuntman.LogEntry, 'modifiedRequest'> = {
|
|
97
|
+
originalRequest,
|
|
98
|
+
modifiedRequest: {
|
|
99
|
+
...this.unproxyRequest(req),
|
|
112
100
|
id: requestUuid,
|
|
113
101
|
timestamp,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!isProxiedHostname) {
|
|
134
|
-
this.removeProxyPort(mockEntry.modifiedRequest);
|
|
135
|
-
}
|
|
136
|
-
const matchingRule = await getRuleExecutor(this.mockUuid).findMatchingRule(mockEntry.modifiedRequest);
|
|
137
|
-
if (matchingRule) {
|
|
138
|
-
mockEntry.mockRuleId = matchingRule.id;
|
|
139
|
-
mockEntry.labels = matchingRule.labels;
|
|
140
|
-
if (matchingRule.actions?.mockResponse) {
|
|
141
|
-
const staticResponse =
|
|
142
|
-
typeof matchingRule.actions.mockResponse === 'function'
|
|
143
|
-
? matchingRule.actions.mockResponse(mockEntry.modifiedRequest)
|
|
144
|
-
: matchingRule.actions.mockResponse;
|
|
145
|
-
mockEntry.modifiedResponse = staticResponse;
|
|
146
|
-
logger.debug({ ...logContext, staticResponse }, 'replying with mocked response');
|
|
147
|
-
if (matchingRule.storeTraffic) {
|
|
148
|
-
this.trafficStore.set(requestUuid, mockEntry);
|
|
149
|
-
}
|
|
150
|
-
if (staticResponse.rawHeaders) {
|
|
151
|
-
for (const header of staticResponse.rawHeaders.toHeaderPairs()) {
|
|
152
|
-
res.setHeader(header[0], header[1]);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
res.status(staticResponse.status || 200);
|
|
156
|
-
res.send(staticResponse.body);
|
|
157
|
-
// static response blocks any further processing
|
|
158
|
-
return;
|
|
102
|
+
...(originalRequest.body && { gqlBody: naiveGQLParser(originalRequest.body) }),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
if (!isProxiedHostname) {
|
|
106
|
+
this.removeProxyPort(mockEntry.modifiedRequest);
|
|
107
|
+
}
|
|
108
|
+
const matchingRule = await getRuleExecutor(this.mockUuid).findMatchingRule(mockEntry.modifiedRequest);
|
|
109
|
+
if (matchingRule) {
|
|
110
|
+
mockEntry.mockRuleId = matchingRule.id;
|
|
111
|
+
mockEntry.labels = matchingRule.labels;
|
|
112
|
+
if (matchingRule.actions.mockResponse) {
|
|
113
|
+
const staticResponse =
|
|
114
|
+
typeof matchingRule.actions.mockResponse === 'function'
|
|
115
|
+
? matchingRule.actions.mockResponse(mockEntry.modifiedRequest)
|
|
116
|
+
: matchingRule.actions.mockResponse;
|
|
117
|
+
mockEntry.modifiedResponse = staticResponse;
|
|
118
|
+
logger.debug({ ...logContext, staticResponse }, 'replying with mocked response');
|
|
119
|
+
if (matchingRule.storeTraffic) {
|
|
120
|
+
this.trafficStore.set(requestUuid, mockEntry);
|
|
159
121
|
}
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
122
|
+
if (staticResponse.rawHeaders) {
|
|
123
|
+
for (const header of staticResponse.rawHeaders.toHeaderPairs()) {
|
|
124
|
+
res.setHeader(header[0], header[1]);
|
|
125
|
+
}
|
|
163
126
|
}
|
|
127
|
+
res.status(staticResponse.status || 200);
|
|
128
|
+
res.send(staticResponse.body);
|
|
129
|
+
// static response blocks any further processing
|
|
130
|
+
return;
|
|
164
131
|
}
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
132
|
+
if (matchingRule.actions.modifyRequest) {
|
|
133
|
+
mockEntry.modifiedRequest = matchingRule.actions.modifyRequest(mockEntry.modifiedRequest);
|
|
134
|
+
logger.debug({ ...logContext, modifiedRequest: mockEntry.modifiedRequest }, 'modified original request');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (this.ipUtils && !isProxiedHostname && !this.ipUtils.isIP(originalHostname)) {
|
|
138
|
+
const hostname = originalHostname.split(':')[0]!;
|
|
139
|
+
try {
|
|
140
|
+
const internalIPs = await this.ipUtils.resolveIP(hostname);
|
|
141
|
+
if (this.ipUtils.isLocalhostIP(internalIPs) && this.options.mock.externalDns.length) {
|
|
142
|
+
const externalIPs = await this.ipUtils.resolveIP(hostname, { useExternalDns: true });
|
|
143
|
+
logger.debug({ ...logContext, hostname, externalIPs, internalIPs }, 'switched to external IP');
|
|
144
|
+
mockEntry.modifiedRequest.url = mockEntry.modifiedRequest.url.replace(
|
|
145
|
+
/^(https?:\/\/)[^:/]+/i,
|
|
146
|
+
`$1${externalIPs}`
|
|
147
|
+
);
|
|
180
148
|
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// swallow the exeception, don't think much can be done at this point
|
|
151
|
+
logger.warn({ ...logContext, error }, `error trying to resolve IP for "${hostname}"`);
|
|
181
152
|
}
|
|
153
|
+
}
|
|
182
154
|
|
|
183
|
-
|
|
155
|
+
const originalResponse: Required<Stuntman.Response> = this.options.mock.disableProxy
|
|
156
|
+
? {
|
|
157
|
+
timestamp: Date.now(),
|
|
158
|
+
body: undefined,
|
|
159
|
+
rawHeaders: new RawHeaders(),
|
|
160
|
+
status: 404,
|
|
161
|
+
}
|
|
162
|
+
: await this.proxyRequest(req, mockEntry, logContext);
|
|
184
163
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
164
|
+
logger.debug({ ...logContext, originalResponse }, 'received response');
|
|
165
|
+
mockEntry.originalResponse = originalResponse;
|
|
166
|
+
let modifedResponse: Stuntman.Response = {
|
|
167
|
+
...originalResponse,
|
|
168
|
+
rawHeaders: new RawHeaders(
|
|
169
|
+
...Array.from(originalResponse.rawHeaders.toHeaderPairs()).flatMap(([key, value]) => {
|
|
170
|
+
// 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)
|
|
171
|
+
return [
|
|
172
|
+
key,
|
|
173
|
+
isProxiedHostname
|
|
174
|
+
? value
|
|
175
|
+
: value.replace(
|
|
176
|
+
new RegExp(`(?:^|\\b)(${escapeStringRegexp(unproxiedHostname)})(?:\\b|$)`, 'igm'),
|
|
177
|
+
originalHostname
|
|
178
|
+
),
|
|
179
|
+
];
|
|
180
|
+
})
|
|
181
|
+
),
|
|
182
|
+
};
|
|
183
|
+
if (matchingRule?.actions.modifyResponse) {
|
|
184
|
+
modifedResponse = matchingRule?.actions.modifyResponse(mockEntry.modifiedRequest, originalResponse);
|
|
185
|
+
logger.debug({ ...logContext, modifedResponse }, 'modified response');
|
|
186
|
+
}
|
|
208
187
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
188
|
+
mockEntry.modifiedResponse = modifedResponse;
|
|
189
|
+
if (matchingRule?.storeTraffic) {
|
|
190
|
+
this.trafficStore.set(requestUuid, mockEntry);
|
|
191
|
+
}
|
|
213
192
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
header[0],
|
|
226
|
-
isProxiedHostname ? header[1].replace(unproxiedHostname, originalHostname) : header[1]
|
|
227
|
-
);
|
|
228
|
-
}
|
|
193
|
+
if (modifedResponse.status) {
|
|
194
|
+
res.status(modifedResponse.status);
|
|
195
|
+
}
|
|
196
|
+
if (modifedResponse.rawHeaders) {
|
|
197
|
+
for (const header of modifedResponse.rawHeaders.toHeaderPairs()) {
|
|
198
|
+
// since fetch decompresses responses we need to get rid of some headers
|
|
199
|
+
// TODO maybe could be handled better than just skipping, although express should add these back for new body
|
|
200
|
+
// if (/^content-(?:length|encoding)$/i.test(header[0])) {
|
|
201
|
+
// continue;
|
|
202
|
+
// }
|
|
203
|
+
res.setHeader(header[0], isProxiedHostname ? header[1].replace(unproxiedHostname, originalHostname) : header[1]);
|
|
229
204
|
}
|
|
230
|
-
|
|
231
|
-
|
|
205
|
+
}
|
|
206
|
+
res.end(Buffer.from(modifedResponse.body, 'binary'));
|
|
207
|
+
}
|
|
232
208
|
|
|
209
|
+
init() {
|
|
210
|
+
if (this.mockApp) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
233
213
|
this.mockApp = express();
|
|
234
214
|
// TODO for now request body is just a buffer passed further, not inflated
|
|
235
215
|
this.mockApp.use(express.raw({ type: '*/*' }));
|
|
236
216
|
|
|
237
|
-
this.mockApp.use((req: express.Request,
|
|
217
|
+
this.mockApp.use((req: express.Request, _res: express.Response, next: express.NextFunction) => {
|
|
238
218
|
RequestContext.bind(req, this.mockUuid);
|
|
239
219
|
next();
|
|
240
220
|
});
|
|
@@ -308,6 +288,10 @@ export class Mock {
|
|
|
308
288
|
}
|
|
309
289
|
|
|
310
290
|
public start() {
|
|
291
|
+
this.init();
|
|
292
|
+
if (!this.mockApp) {
|
|
293
|
+
throw new Error('initialization error');
|
|
294
|
+
}
|
|
311
295
|
if (this.server) {
|
|
312
296
|
throw new Error('mock server already started');
|
|
313
297
|
}
|
|
@@ -369,7 +353,7 @@ export class Mock {
|
|
|
369
353
|
host.endsWith(`:${this.options.mock.port}`) ||
|
|
370
354
|
(this.options.mock.httpsPort && host.endsWith(`:${this.options.mock.httpsPort}`))
|
|
371
355
|
) {
|
|
372
|
-
req.rawHeaders.set('host', host.split(':')[0]);
|
|
356
|
+
req.rawHeaders.set('host', host.split(':')[0]!);
|
|
373
357
|
}
|
|
374
358
|
}
|
|
375
359
|
}
|
package/src/ruleExecutor.ts
CHANGED
|
@@ -136,6 +136,7 @@ class RuleExecutor implements Stuntman.RuleExecutorInterface {
|
|
|
136
136
|
} catch (error) {
|
|
137
137
|
logger.error({ ...logContext, ruleId: rule?.id, error }, 'error in rule match function');
|
|
138
138
|
}
|
|
139
|
+
return undefined;
|
|
139
140
|
});
|
|
140
141
|
if (!matchingRule) {
|
|
141
142
|
logger.debug(logContext, 'no matching rule found');
|
|
@@ -207,5 +208,5 @@ export const getRuleExecutor = (mockUuid: string): RuleExecutor => {
|
|
|
207
208
|
[...DEFAULT_RULES, ...CUSTOM_RULES].map((r) => ({ ...r, ttlSeconds: Infinity }))
|
|
208
209
|
);
|
|
209
210
|
}
|
|
210
|
-
return ruleExecutors[mockUuid]
|
|
211
|
+
return ruleExecutors[mockUuid]!;
|
|
211
212
|
};
|
package/src/rules/index.ts
CHANGED
|
@@ -3,19 +3,19 @@ import glob from 'glob';
|
|
|
3
3
|
import * as tsImport from 'ts-import';
|
|
4
4
|
import { catchAllRule } from './catchAll';
|
|
5
5
|
import { echoRule } from './echo';
|
|
6
|
-
import {
|
|
6
|
+
import { stuntmanConfig, logger } from '@stuntman/shared';
|
|
7
7
|
import type * as Stuntman from '@stuntman/shared';
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_RULES: Stuntman.DeployedRule[] = [catchAllRule, echoRule];
|
|
10
10
|
export const CUSTOM_RULES: Stuntman.DeployedRule[] = [];
|
|
11
11
|
|
|
12
12
|
const loadAdditionalRules = () => {
|
|
13
|
-
if (!
|
|
14
|
-
logger.debug({ rulesPath:
|
|
13
|
+
if (!stuntmanConfig.mock.rulesPath || !fs.existsSync(stuntmanConfig.mock.rulesPath)) {
|
|
14
|
+
logger.debug({ rulesPath: stuntmanConfig.mock.rulesPath }, `additional rules directory not found`);
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
logger.debug({ rulesPath:
|
|
18
|
-
const filePaths = glob.sync('*.[tj]s', { absolute: true, cwd:
|
|
17
|
+
logger.debug({ rulesPath: stuntmanConfig.mock.rulesPath }, `loading additional rules`);
|
|
18
|
+
const filePaths = glob.sync('*.[tj]s', { absolute: true, cwd: stuntmanConfig.mock.rulesPath });
|
|
19
19
|
for (const filePath of filePaths) {
|
|
20
20
|
// TODO add .ts rule support
|
|
21
21
|
try {
|
package/src/storage.ts
CHANGED
|
@@ -28,12 +28,12 @@ export const getTrafficStore = (key: string, options?: Stuntman.StorageConfig) =
|
|
|
28
28
|
sizeCalculation: (value) => sizeof(value),
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
return trafficStoreInstances[key]
|
|
31
|
+
return trafficStoreInstances[key]!;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
export const getDnsResolutionCache = (key: string) => {
|
|
35
35
|
if (!(key in dnsResolutionCacheInstances)) {
|
|
36
36
|
dnsResolutionCacheInstances[key] = new LRUCache<string, string>(DNS_CACHE_OPTIONS);
|
|
37
37
|
}
|
|
38
|
-
return dnsResolutionCacheInstances[key]
|
|
38
|
+
return dnsResolutionCacheInstances[key]!;
|
|
39
39
|
};
|
package/dist/api/api.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import http from 'http';
|
|
3
|
-
import { NextFunction, Request, Response, Express as ExpressServer } from 'express';
|
|
4
|
-
import type * as Stuntman from '@stuntman/shared';
|
|
5
|
-
import LRUCache from 'lru-cache';
|
|
6
|
-
type ApiOptions = Stuntman.ApiConfig & {
|
|
7
|
-
mockUuid: string;
|
|
8
|
-
};
|
|
9
|
-
export declare class API {
|
|
10
|
-
protected options: Required<ApiOptions>;
|
|
11
|
-
protected apiApp: ExpressServer;
|
|
12
|
-
trafficStore: LRUCache<string, Stuntman.LogEntry>;
|
|
13
|
-
server: http.Server | null;
|
|
14
|
-
auth: (req: Request, type: 'read' | 'write') => void;
|
|
15
|
-
authReadOnly: (req: Request, res: Response, next: NextFunction) => void;
|
|
16
|
-
authReadWrite: (req: Request, res: Response, next: NextFunction) => void;
|
|
17
|
-
constructor(options: ApiOptions, webGuiOptions?: Stuntman.WebGuiConfig);
|
|
18
|
-
private initWebGui;
|
|
19
|
-
start(): void;
|
|
20
|
-
stop(): void;
|
|
21
|
-
}
|
|
22
|
-
export {};
|
package/dist/api/api.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.API = void 0;
|
|
7
|
-
const express_1 = __importDefault(require("express"));
|
|
8
|
-
const uuid_1 = require("uuid");
|
|
9
|
-
const storage_1 = require("../storage");
|
|
10
|
-
const ruleExecutor_1 = require("../ruleExecutor");
|
|
11
|
-
const shared_1 = require("@stuntman/shared");
|
|
12
|
-
const requestContext_1 = __importDefault(require("../requestContext"));
|
|
13
|
-
const serialize_javascript_1 = __importDefault(require("serialize-javascript"));
|
|
14
|
-
const validators_1 = require("./validators");
|
|
15
|
-
const utils_1 = require("./utils");
|
|
16
|
-
const API_KEY_HEADER = 'x-api-key';
|
|
17
|
-
class API {
|
|
18
|
-
constructor(options, webGuiOptions) {
|
|
19
|
-
this.server = null;
|
|
20
|
-
if (!options.apiKeyReadOnly !== !options.apiKeyReadWrite) {
|
|
21
|
-
throw new Error('apiKeyReadOnly and apiKeyReadWrite options need to be set either both or none');
|
|
22
|
-
}
|
|
23
|
-
this.options = options;
|
|
24
|
-
this.trafficStore = (0, storage_1.getTrafficStore)(this.options.mockUuid);
|
|
25
|
-
this.apiApp = (0, express_1.default)();
|
|
26
|
-
this.apiApp.use(express_1.default.json());
|
|
27
|
-
this.apiApp.use(express_1.default.text());
|
|
28
|
-
this.auth = (req, type) => {
|
|
29
|
-
if (!this.options.apiKeyReadOnly && !this.options.apiKeyReadWrite) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const hasValidReadKey = req.header(API_KEY_HEADER) === this.options.apiKeyReadOnly;
|
|
33
|
-
const hasValidWriteKey = req.header(API_KEY_HEADER) === this.options.apiKeyReadWrite;
|
|
34
|
-
const hasValidKey = type === 'read' ? hasValidReadKey || hasValidWriteKey : hasValidWriteKey;
|
|
35
|
-
if (!hasValidKey) {
|
|
36
|
-
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.UNAUTHORIZED, message: 'unauthorized' });
|
|
37
|
-
}
|
|
38
|
-
return;
|
|
39
|
-
};
|
|
40
|
-
this.authReadOnly = (req, res, next) => {
|
|
41
|
-
this.auth(req, 'read');
|
|
42
|
-
next();
|
|
43
|
-
};
|
|
44
|
-
this.authReadWrite = (req, res, next) => {
|
|
45
|
-
this.auth(req, 'write');
|
|
46
|
-
next();
|
|
47
|
-
};
|
|
48
|
-
this.apiApp.use((req, res, next) => {
|
|
49
|
-
requestContext_1.default.bind(req, this.options.mockUuid);
|
|
50
|
-
next();
|
|
51
|
-
});
|
|
52
|
-
this.apiApp.get('/rule', this.authReadOnly, async (req, res) => {
|
|
53
|
-
res.send((0, shared_1.stringify)(await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).getRules()));
|
|
54
|
-
});
|
|
55
|
-
this.apiApp.get('/rule/:ruleId', this.authReadOnly, async (req, res) => {
|
|
56
|
-
res.send((0, shared_1.stringify)(await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).getRule(req.params.ruleId)));
|
|
57
|
-
});
|
|
58
|
-
this.apiApp.get('/rule/:ruleId/disable', this.authReadWrite, (req, res) => {
|
|
59
|
-
(0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).disableRule(req.params.ruleId);
|
|
60
|
-
res.send();
|
|
61
|
-
});
|
|
62
|
-
this.apiApp.get('/rule/:ruleId/enable', this.authReadWrite, (req, res) => {
|
|
63
|
-
(0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).enableRule(req.params.ruleId);
|
|
64
|
-
res.send();
|
|
65
|
-
});
|
|
66
|
-
this.apiApp.post('/rule', this.authReadWrite, async (req, res) => {
|
|
67
|
-
const deserializedRule = (0, utils_1.deserializeRule)(req.body);
|
|
68
|
-
(0, validators_1.validateDeserializedRule)(deserializedRule);
|
|
69
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
70
|
-
// @ts-ignore
|
|
71
|
-
const rule = await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).addRule(deserializedRule);
|
|
72
|
-
res.send((0, shared_1.stringify)(rule));
|
|
73
|
-
});
|
|
74
|
-
this.apiApp.get('/rule/:ruleId/remove', this.authReadWrite, async (req, res) => {
|
|
75
|
-
await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).removeRule(req.params.ruleId);
|
|
76
|
-
res.send();
|
|
77
|
-
});
|
|
78
|
-
this.apiApp.get('/traffic', this.authReadOnly, (req, res) => {
|
|
79
|
-
const serializedTraffic = {};
|
|
80
|
-
for (const [key, value] of this.trafficStore.entries()) {
|
|
81
|
-
serializedTraffic[key] = value;
|
|
82
|
-
}
|
|
83
|
-
res.json(serializedTraffic);
|
|
84
|
-
});
|
|
85
|
-
this.apiApp.get('/traffic/:ruleIdOrLabel', this.authReadOnly, (req, res) => {
|
|
86
|
-
const serializedTraffic = {};
|
|
87
|
-
for (const [key, value] of this.trafficStore.entries()) {
|
|
88
|
-
if (value.mockRuleId === req.params.ruleIdOrLabel || (value.labels || []).includes(req.params.ruleIdOrLabel)) {
|
|
89
|
-
serializedTraffic[key] = value;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
res.json(serializedTraffic);
|
|
93
|
-
});
|
|
94
|
-
if (!(webGuiOptions === null || webGuiOptions === void 0 ? void 0 : webGuiOptions.disabled)) {
|
|
95
|
-
this.apiApp.set('views', __dirname + '/webgui');
|
|
96
|
-
this.apiApp.set('view engine', 'pug');
|
|
97
|
-
this.initWebGui();
|
|
98
|
-
}
|
|
99
|
-
this.apiApp.all(/.*/, (req, res) => res.status(404).send());
|
|
100
|
-
this.apiApp.use((error, req, res) => {
|
|
101
|
-
const ctx = requestContext_1.default.get(req);
|
|
102
|
-
const uuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)();
|
|
103
|
-
if (error instanceof shared_1.AppError && error.isOperational && res) {
|
|
104
|
-
shared_1.logger.error(error);
|
|
105
|
-
res.status(error.httpCode).json({
|
|
106
|
-
error: { message: error.message, httpCode: error.httpCode, stack: error.stack },
|
|
107
|
-
});
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
shared_1.logger.error({ message: error.message, stack: error.stack, name: error.name, uuid }, 'unexpected error');
|
|
111
|
-
if (res) {
|
|
112
|
-
res.status(shared_1.HttpCode.INTERNAL_SERVER_ERROR).json({
|
|
113
|
-
error: { message: error.message, httpCode: shared_1.HttpCode.INTERNAL_SERVER_ERROR, uuid },
|
|
114
|
-
});
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
// eslint-disable-next-line no-console
|
|
118
|
-
console.log('API server encountered a critical error. Exiting');
|
|
119
|
-
process.exit(1);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
initWebGui() {
|
|
123
|
-
this.apiApp.get('/webgui/rules', this.authReadOnly, async (req, res) => {
|
|
124
|
-
const rules = {};
|
|
125
|
-
for (const rule of await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).getRules()) {
|
|
126
|
-
rules[rule.id] = (0, serialize_javascript_1.default)((0, utils_1.liveRuleToRule)(rule), { unsafe: true });
|
|
127
|
-
}
|
|
128
|
-
res.render('rules', { rules: (0, utils_1.escapedSerialize)(rules), INDEX_DTS: shared_1.INDEX_DTS, ruleKeys: Object.keys(rules) });
|
|
129
|
-
});
|
|
130
|
-
this.apiApp.get('/webgui/traffic', this.authReadOnly, async (req, res) => {
|
|
131
|
-
const serializedTraffic = [];
|
|
132
|
-
for (const value of this.trafficStore.values()) {
|
|
133
|
-
serializedTraffic.push(value);
|
|
134
|
-
}
|
|
135
|
-
res.render('traffic', {
|
|
136
|
-
traffic: JSON.stringify(serializedTraffic.sort((a, b) => b.originalRequest.timestamp - a.originalRequest.timestamp)),
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
// TODO make webui way better and safer, nicer formatting, eslint/prettier, blackjack and hookers
|
|
140
|
-
this.apiApp.post('/webgui/rules/unsafesave', this.authReadWrite, async (req, res) => {
|
|
141
|
-
const rule = new Function(req.body)();
|
|
142
|
-
if (!rule ||
|
|
143
|
-
!rule.id ||
|
|
144
|
-
typeof rule.matches !== 'function' ||
|
|
145
|
-
typeof rule.ttlSeconds !== 'number' ||
|
|
146
|
-
rule.ttlSeconds > shared_1.MAX_RULE_TTL_SECONDS) {
|
|
147
|
-
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'Invalid rule' });
|
|
148
|
-
}
|
|
149
|
-
await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).addRule({
|
|
150
|
-
id: rule.id,
|
|
151
|
-
matches: rule.matches,
|
|
152
|
-
ttlSeconds: rule.ttlSeconds,
|
|
153
|
-
...(rule.actions && {
|
|
154
|
-
actions: {
|
|
155
|
-
...(rule.actions.mockResponse
|
|
156
|
-
? { mockResponse: rule.actions.mockResponse }
|
|
157
|
-
: { modifyRequest: rule.actions.modifyRequest, modifyResponse: rule.actions.modifyResponse }),
|
|
158
|
-
},
|
|
159
|
-
}),
|
|
160
|
-
...(rule.disableAfterUse !== undefined && { disableAfterUse: rule.disableAfterUse }),
|
|
161
|
-
...(rule.isEnabled !== undefined && { isEnabled: rule.isEnabled }),
|
|
162
|
-
...(rule.labels !== undefined && { labels: rule.labels }),
|
|
163
|
-
...(rule.priority !== undefined && { priority: rule.priority }),
|
|
164
|
-
...(rule.removeAfterUse !== undefined && { removeAfterUse: rule.removeAfterUse }),
|
|
165
|
-
...(rule.storeTraffic !== undefined && { storeTraffic: rule.storeTraffic }),
|
|
166
|
-
}, true);
|
|
167
|
-
res.send();
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
start() {
|
|
171
|
-
if (this.server) {
|
|
172
|
-
throw new Error('mock server already started');
|
|
173
|
-
}
|
|
174
|
-
this.server = this.apiApp.listen(this.options.port, () => {
|
|
175
|
-
shared_1.logger.info(`API listening on ${this.options.port}`);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
stop() {
|
|
179
|
-
if (!this.server) {
|
|
180
|
-
throw new Error('mock server not started');
|
|
181
|
-
}
|
|
182
|
-
this.server.close((error) => {
|
|
183
|
-
shared_1.logger.warn(error, 'problem closing server');
|
|
184
|
-
this.server = null;
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
exports.API = API;
|
package/dist/api/utils.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type * as Stuntman from '@stuntman/shared';
|
|
2
|
-
export declare const deserializeRule: (serializedRule: Stuntman.SerializedRule) => Stuntman.Rule;
|
|
3
|
-
export declare const escapedSerialize: (obj: any) => string;
|
|
4
|
-
export declare const liveRuleToRule: (liveRule: Stuntman.LiveRule) => Stuntman.Rule;
|