@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.
Files changed (43) hide show
  1. package/README.md +3 -3
  2. package/package.json +8 -6
  3. package/src/api/api.ts +69 -48
  4. package/src/api/utils.ts +26 -26
  5. package/src/api/validators.ts +44 -35
  6. package/src/api/webgui/rules.pug +4 -2
  7. package/src/api/webgui/style.css +3 -3
  8. package/src/api/webgui/traffic.pug +1 -0
  9. package/src/bin/stuntman.ts +2 -2
  10. package/src/ipUtils.ts +1 -1
  11. package/src/mock.ts +137 -153
  12. package/src/ruleExecutor.ts +2 -1
  13. package/src/rules/index.ts +5 -5
  14. package/src/storage.ts +2 -2
  15. package/dist/api/api.d.ts +0 -22
  16. package/dist/api/api.js +0 -188
  17. package/dist/api/utils.d.ts +0 -4
  18. package/dist/api/utils.js +0 -69
  19. package/dist/api/validators.d.ts +0 -3
  20. package/dist/api/validators.js +0 -118
  21. package/dist/api/webgui/rules.pug +0 -145
  22. package/dist/api/webgui/style.css +0 -28
  23. package/dist/api/webgui/traffic.pug +0 -37
  24. package/dist/bin/stuntman.d.ts +0 -2
  25. package/dist/bin/stuntman.js +0 -7
  26. package/dist/index.d.ts +0 -1
  27. package/dist/index.js +0 -5
  28. package/dist/ipUtils.d.ts +0 -17
  29. package/dist/ipUtils.js +0 -101
  30. package/dist/mock.d.ts +0 -30
  31. package/dist/mock.js +0 -321
  32. package/dist/requestContext.d.ts +0 -9
  33. package/dist/requestContext.js +0 -18
  34. package/dist/ruleExecutor.d.ts +0 -22
  35. package/dist/ruleExecutor.js +0 -187
  36. package/dist/rules/catchAll.d.ts +0 -2
  37. package/dist/rules/catchAll.js +0 -15
  38. package/dist/rules/echo.d.ts +0 -2
  39. package/dist/rules/echo.js +0 -15
  40. package/dist/rules/index.d.ts +0 -3
  41. package/dist/rules/index.js +0 -70
  42. package/dist/storage.d.ts +0 -4
  43. 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.ServerConfig;
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.ServerConfig) {
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 = async (req: express.Request, res: express.Response): Promise<void> => {
105
- const ctx: RequestContext | null = RequestContext.get(req);
106
- const requestUuid = ctx?.uuid || uuidv4();
107
- const timestamp = Date.now();
108
- const originalHostname = req.headers.host || req.hostname;
109
- const unproxiedHostname = req.hostname.replace(this.MOCK_DOMAIN_REGEX, '');
110
- const isProxiedHostname = originalHostname !== unproxiedHostname;
111
- const originalRequest = {
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
- url: `${req.protocol}://${req.hostname}${req.originalUrl}`,
115
- method: req.method,
116
- rawHeaders: new RawHeaders(...req.rawHeaders),
117
- ...((Buffer.isBuffer(req.body) && { body: req.body.toString('utf-8') }) ||
118
- (typeof req.body === 'string' && { body: req.body })),
119
- };
120
- logger.debug(originalRequest, 'processing request');
121
- const logContext: Record<string, any> = {
122
- requestId: originalRequest.id,
123
- };
124
- const mockEntry: WithRequiredProperty<Stuntman.LogEntry, 'modifiedRequest'> = {
125
- originalRequest,
126
- modifiedRequest: {
127
- ...this.unproxyRequest(req),
128
- id: requestUuid,
129
- timestamp,
130
- ...(originalRequest.body && { gqlBody: naiveGQLParser(originalRequest.body) }),
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 (matchingRule.actions?.modifyRequest) {
161
- mockEntry.modifiedRequest = matchingRule.actions?.modifyRequest(mockEntry.modifiedRequest);
162
- logger.debug({ ...logContext, modifiedRequest: mockEntry.modifiedRequest }, 'modified original request');
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 (this.ipUtils && !isProxiedHostname && !this.ipUtils.isIP(originalHostname)) {
166
- const hostname = originalHostname.split(':')[0];
167
- try {
168
- const internalIPs = await this.ipUtils.resolveIP(hostname);
169
- if (this.ipUtils.isLocalhostIP(internalIPs) && this.options.mock.externalDns.length) {
170
- const externalIPs = await this.ipUtils.resolveIP(hostname, { useExternalDns: true });
171
- logger.debug({ ...logContext, hostname, externalIPs, internalIPs }, 'switched to external IP');
172
- mockEntry.modifiedRequest.url = mockEntry.modifiedRequest.url.replace(
173
- /^(https?:\/\/)[^:/]+/i,
174
- `$1${externalIPs}`
175
- );
176
- }
177
- } catch (error) {
178
- // swallow the exeception, don't think much can be done at this point
179
- logger.warn({ ...logContext, error }, `error trying to resolve IP for "${hostname}"`);
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
- const originalResponse = await this.proxyRequest(req, mockEntry, logContext);
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
- logger.debug({ ...logContext, originalResponse }, 'received response');
186
- mockEntry.originalResponse = originalResponse;
187
- let modifedResponse: Stuntman.Response = {
188
- ...originalResponse,
189
- rawHeaders: new RawHeaders(
190
- ...Array.from(originalResponse.rawHeaders.toHeaderPairs()).flatMap(([key, value]) => {
191
- // 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)
192
- return [
193
- key,
194
- isProxiedHostname
195
- ? value
196
- : value.replace(
197
- new RegExp(`(?:^|\\b)(${unproxiedHostname.replace('.', '\\.')})(?:\\b|$)`, 'igm'),
198
- originalHostname
199
- ),
200
- ];
201
- })
202
- ),
203
- };
204
- if (matchingRule?.actions?.modifyResponse) {
205
- modifedResponse = matchingRule?.actions?.modifyResponse(mockEntry.modifiedRequest, originalResponse);
206
- logger.debug({ ...logContext, modifedResponse }, 'modified response');
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
- mockEntry.modifiedResponse = modifedResponse;
210
- if (matchingRule?.storeTraffic) {
211
- this.trafficStore.set(requestUuid, mockEntry);
212
- }
188
+ mockEntry.modifiedResponse = modifedResponse;
189
+ if (matchingRule?.storeTraffic) {
190
+ this.trafficStore.set(requestUuid, mockEntry);
191
+ }
213
192
 
214
- if (modifedResponse.status) {
215
- res.status(modifedResponse.status);
216
- }
217
- if (modifedResponse.rawHeaders) {
218
- for (const header of modifedResponse.rawHeaders.toHeaderPairs()) {
219
- // since fetch decompresses responses we need to get rid of some headers
220
- // TODO maybe could be handled better than just skipping, although express should add these back for new body
221
- // if (/^content-(?:length|encoding)$/i.test(header[0])) {
222
- // continue;
223
- // }
224
- res.setHeader(
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
- res.end(Buffer.from(modifedResponse.body, 'binary'));
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, res: express.Response, next: express.NextFunction) => {
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
  }
@@ -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
  };
@@ -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 { serverConfig, logger } from '@stuntman/shared';
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 (!serverConfig.mock.rulesPath || !fs.existsSync(serverConfig.mock.rulesPath)) {
14
- logger.debug({ rulesPath: serverConfig.mock.rulesPath }, `additional rules directory not found`);
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: serverConfig.mock.rulesPath }, `loading additional rules`);
18
- const filePaths = glob.sync('*.[tj]s', { absolute: true, cwd: serverConfig.mock.rulesPath });
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;
@@ -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;