@stuntman/server 0.1.0

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/config/default.json +23 -0
  3. package/config/test.json +26 -0
  4. package/dist/api/api.d.ts +19 -0
  5. package/dist/api/api.js +162 -0
  6. package/dist/api/utils.d.ts +4 -0
  7. package/dist/api/utils.js +69 -0
  8. package/dist/api/validatiors.d.ts +3 -0
  9. package/dist/api/validatiors.js +118 -0
  10. package/dist/api/webgui/rules.pug +145 -0
  11. package/dist/api/webgui/style.css +28 -0
  12. package/dist/api/webgui/traffic.pug +37 -0
  13. package/dist/bin/stuntman.d.ts +2 -0
  14. package/dist/bin/stuntman.js +7 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +5 -0
  17. package/dist/ipUtils.d.ts +17 -0
  18. package/dist/ipUtils.js +101 -0
  19. package/dist/mock.d.ts +27 -0
  20. package/dist/mock.js +308 -0
  21. package/dist/requestContext.d.ts +9 -0
  22. package/dist/requestContext.js +18 -0
  23. package/dist/ruleExecutor.d.ts +21 -0
  24. package/dist/ruleExecutor.js +172 -0
  25. package/dist/rules/catchAll.d.ts +2 -0
  26. package/dist/rules/catchAll.js +15 -0
  27. package/dist/rules/echo.d.ts +2 -0
  28. package/dist/rules/echo.js +15 -0
  29. package/dist/rules/index.d.ts +2 -0
  30. package/dist/rules/index.js +7 -0
  31. package/dist/storage.d.ts +4 -0
  32. package/dist/storage.js +42 -0
  33. package/package.json +58 -0
  34. package/src/api/api.ts +194 -0
  35. package/src/api/utils.ts +69 -0
  36. package/src/api/validatiors.ts +123 -0
  37. package/src/api/webgui/rules.pug +145 -0
  38. package/src/api/webgui/style.css +28 -0
  39. package/src/api/webgui/traffic.pug +37 -0
  40. package/src/bin/stuntman.ts +8 -0
  41. package/src/index.ts +1 -0
  42. package/src/ipUtils.ts +83 -0
  43. package/src/mock.ts +349 -0
  44. package/src/requestContext.ts +23 -0
  45. package/src/ruleExecutor.ts +193 -0
  46. package/src/rules/catchAll.ts +14 -0
  47. package/src/rules/echo.ts +14 -0
  48. package/src/rules/index.ts +7 -0
  49. package/src/storage.ts +39 -0
  50. package/tsconfig.json +16 -0
@@ -0,0 +1,37 @@
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
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const mock_1 = require("../mock");
5
+ const shared_1 = require("@stuntman/shared");
6
+ const mock = new mock_1.Mock(shared_1.serverConfig);
7
+ mock.start();
@@ -0,0 +1 @@
1
+ export { Mock as StuntmanMock } from './mock';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StuntmanMock = void 0;
4
+ var mock_1 = require("./mock");
5
+ Object.defineProperty(exports, "StuntmanMock", { enumerable: true, get: function () { return mock_1.Mock; } });
@@ -0,0 +1,17 @@
1
+ /// <reference types="node" />
2
+ import dns from 'node:dns';
3
+ export declare class IPUtils {
4
+ protected dnsResolutionCache: import("lru-cache")<string, string>;
5
+ protected mockUuid: string;
6
+ externalDns: dns.Resolver | null;
7
+ constructor(options: {
8
+ mockUuid: string;
9
+ externalDns?: string[];
10
+ });
11
+ isLocalhostIP(ip: string): boolean;
12
+ private resolveIPs;
13
+ resolveIP(hostname: string, options?: {
14
+ useExternalDns?: true;
15
+ }): Promise<string>;
16
+ isIP(hostname: string): boolean;
17
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.IPUtils = void 0;
27
+ const os_1 = require("os");
28
+ const node_dns_1 = __importStar(require("node:dns"));
29
+ const storage_1 = require("./storage");
30
+ const shared_1 = require("@stuntman/shared");
31
+ const localhostIPs = ['127.0.0.1'];
32
+ const IP_WITH_OPTIONAL_PORT_REGEX = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(:[0-9]+)?$/i;
33
+ for (const nets of Object.values((0, os_1.networkInterfaces)())) {
34
+ if (!nets) {
35
+ continue;
36
+ }
37
+ for (const net of nets) {
38
+ const familyV4Value = typeof net.family === 'string' ? 'IPv4' : 4;
39
+ if (net.family === familyV4Value && !localhostIPs.includes(net.address)) {
40
+ localhostIPs.push(net.address);
41
+ }
42
+ }
43
+ }
44
+ class IPUtils {
45
+ constructor(options) {
46
+ var _a;
47
+ this.externalDns = null;
48
+ this.mockUuid = options.mockUuid;
49
+ if ((_a = options.externalDns) === null || _a === void 0 ? void 0 : _a.length) {
50
+ this.externalDns = new node_dns_1.Resolver();
51
+ this.externalDns.setServers(options.externalDns);
52
+ }
53
+ this.dnsResolutionCache = (0, storage_1.getDnsResolutionCache)(this.mockUuid);
54
+ }
55
+ isLocalhostIP(ip) {
56
+ return localhostIPs.includes(ip);
57
+ }
58
+ async resolveIPs(hostname, options) {
59
+ return new Promise((resolve, reject) => {
60
+ const callback = (error, addresses) => {
61
+ if (error) {
62
+ shared_1.logger.debug({ error, hostname }, 'error resolving hostname');
63
+ reject(error);
64
+ return;
65
+ }
66
+ if (typeof addresses === 'string') {
67
+ shared_1.logger.debug({ ip: [addresses], hostname }, 'resolved hostname');
68
+ resolve([addresses]);
69
+ return;
70
+ }
71
+ if (!addresses || addresses.length === 0) {
72
+ shared_1.logger.debug({ hostname }, 'missing IPs');
73
+ reject(new Error('No addresses found'));
74
+ return;
75
+ }
76
+ shared_1.logger.debug({ ip: addresses, hostname }, 'resolved hostname');
77
+ resolve([addresses[0], ...addresses.slice(1)]);
78
+ };
79
+ if (options === null || options === void 0 ? void 0 : options.useExternalDns) {
80
+ if (!this.externalDns) {
81
+ reject(new Error('external dns servers not set'));
82
+ return;
83
+ }
84
+ this.externalDns.resolve(hostname, callback);
85
+ return;
86
+ }
87
+ node_dns_1.default.lookup(hostname, callback);
88
+ });
89
+ }
90
+ async resolveIP(hostname, options) {
91
+ const cachedIP = this.dnsResolutionCache.get(hostname);
92
+ if (cachedIP) {
93
+ return cachedIP;
94
+ }
95
+ return (await this.resolveIPs(hostname, options))[0];
96
+ }
97
+ isIP(hostname) {
98
+ return IP_WITH_OPTIONAL_PORT_REGEX.test(hostname);
99
+ }
100
+ }
101
+ exports.IPUtils = IPUtils;
package/dist/mock.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import http from 'http';
4
+ import https from 'https';
5
+ import express from 'express';
6
+ import type * as Stuntman from '@stuntman/shared';
7
+ import { IPUtils } from './ipUtils';
8
+ import LRUCache from 'lru-cache';
9
+ import { API } from './api/api';
10
+ export declare class Mock {
11
+ protected mockUuid: string;
12
+ protected options: Stuntman.ServerConfig;
13
+ protected mockApp: express.Express;
14
+ protected MOCK_DOMAIN_REGEX: RegExp;
15
+ protected URL_PORT_REGEX: RegExp;
16
+ protected server: http.Server | null;
17
+ protected serverHttps: https.Server | null;
18
+ protected trafficStore: LRUCache<string, Stuntman.LogEntry>;
19
+ protected ipUtils: IPUtils | null;
20
+ private _api;
21
+ get apiServer(): API | null;
22
+ constructor(options: Stuntman.ServerConfig);
23
+ start(): void;
24
+ stop(): void;
25
+ protected unproxyRequest(req: express.Request): Stuntman.BaseRequest;
26
+ protected removeProxyPort(req: Stuntman.Request): void;
27
+ }
package/dist/mock.js ADDED
@@ -0,0 +1,308 @@
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.Mock = void 0;
7
+ const https_1 = __importDefault(require("https"));
8
+ const express_1 = __importDefault(require("express"));
9
+ const uuid_1 = require("uuid");
10
+ const ruleExecutor_1 = require("./ruleExecutor");
11
+ const storage_1 = require("./storage");
12
+ const shared_1 = require("@stuntman/shared");
13
+ const requestContext_1 = __importDefault(require("./requestContext"));
14
+ const ipUtils_1 = require("./ipUtils");
15
+ const api_1 = require("./api/api");
16
+ const naiveGQLParser = (body) => {
17
+ try {
18
+ let json = undefined;
19
+ try {
20
+ json = JSON.parse(Buffer.isBuffer(body) ? body.toString('utf-8') : body);
21
+ }
22
+ catch (kiss) {
23
+ // and swallow
24
+ }
25
+ if (!(json === null || json === void 0 ? void 0 : json.query) && !(json === null || json === void 0 ? void 0 : json.operationName)) {
26
+ return;
27
+ }
28
+ const lines = json.query
29
+ .split('\n')
30
+ .map((l) => l.replace(/^\s+/g, '').trim())
31
+ .filter((l) => !!l);
32
+ if (/^query /.test(lines[0])) {
33
+ json.type = 'query';
34
+ }
35
+ else if (/^mutation /.test(lines[0])) {
36
+ json.type = 'mutation';
37
+ }
38
+ else {
39
+ throw new Error(`Unable to resolve query type of ${lines[0]}`);
40
+ }
41
+ json.methodName = lines[json.operationName ? 1 : 0].split('(')[0].split('{')[0];
42
+ return json;
43
+ }
44
+ catch (error) {
45
+ shared_1.logger.debug(error, 'unable to parse GQL');
46
+ }
47
+ return undefined;
48
+ };
49
+ // TODO add proper web proxy mode
50
+ class Mock {
51
+ get apiServer() {
52
+ if (this.options.api.disabled) {
53
+ return null;
54
+ }
55
+ if (!this._api) {
56
+ this._api = new api_1.API({ ...this.options.api, mockUuid: this.mockUuid }, this.options.webgui);
57
+ }
58
+ return this._api;
59
+ }
60
+ constructor(options) {
61
+ this.server = null;
62
+ this.serverHttps = null;
63
+ this.ipUtils = null;
64
+ this._api = null;
65
+ this.mockUuid = (0, uuid_1.v4)();
66
+ this.options = options;
67
+ if (this.options.mock.httpsPort && (!this.options.mock.httpsKey || !this.options.mock.httpsCert)) {
68
+ throw new Error('missing https key/cert');
69
+ }
70
+ this.MOCK_DOMAIN_REGEX = new RegExp(`(?:\\.([0-9]+))?\\.(?:(?:${this.options.mock.domain})|(?:localhost))(https?)?(:${this.options.mock.port}${this.options.mock.httpsPort ? `|${this.options.mock.httpsPort}` : ''})?(?:\\b|$)`, 'i');
71
+ this.URL_PORT_REGEX = new RegExp(`^(https?:\\/\\/[^:/]+):(?:${this.options.mock.port}${this.options.mock.httpsPort ? `|${this.options.mock.httpsPort}` : ''})(\\/.*)`, 'i');
72
+ this.trafficStore = (0, storage_1.getTrafficStore)(this.mockUuid, this.options.storage.traffic);
73
+ this.ipUtils =
74
+ !this.options.mock.externalDns || this.options.mock.externalDns.length === 0
75
+ ? null
76
+ : new ipUtils_1.IPUtils({ mockUuid: this.mockUuid, externalDns: this.options.mock.externalDns });
77
+ this.mockApp = (0, express_1.default)();
78
+ // TODO for now request body is just a buffer passed further, not inflated
79
+ this.mockApp.use(express_1.default.raw({ type: '*/*' }));
80
+ this.mockApp.use((req, res, next) => {
81
+ requestContext_1.default.bind(req, this.mockUuid);
82
+ next();
83
+ });
84
+ this.mockApp.all(/.*/, async (req, res) => {
85
+ var _a, _b, _c, _d, _e;
86
+ const ctx = requestContext_1.default.get(req);
87
+ const requestUuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)();
88
+ const timestamp = Date.now();
89
+ const originalHostname = req.hostname;
90
+ const unproxiedHostname = req.hostname.replace(this.MOCK_DOMAIN_REGEX, '');
91
+ const isProxiedHostname = originalHostname !== unproxiedHostname;
92
+ const originalRequest = {
93
+ id: requestUuid,
94
+ timestamp,
95
+ url: `${req.protocol}://${req.hostname}${req.originalUrl}`,
96
+ method: req.method,
97
+ rawHeaders: new shared_1.RawHeaders(...req.rawHeaders),
98
+ ...(Buffer.isBuffer(req.body) && { body: req.body.toString('utf-8') }),
99
+ };
100
+ shared_1.logger.debug(originalRequest, 'processing request');
101
+ const logContext = {
102
+ requestId: originalRequest.id,
103
+ };
104
+ const mockEntry = {
105
+ originalRequest,
106
+ modifiedRequest: {
107
+ ...this.unproxyRequest(req),
108
+ id: requestUuid,
109
+ timestamp,
110
+ ...(originalRequest.body && { gqlBody: naiveGQLParser(originalRequest.body) }),
111
+ },
112
+ };
113
+ if (!isProxiedHostname) {
114
+ this.removeProxyPort(mockEntry.modifiedRequest);
115
+ }
116
+ const matchingRule = await ruleExecutor_1.ruleExecutor.findMatchingRule(mockEntry.modifiedRequest);
117
+ if (matchingRule) {
118
+ mockEntry.mockRuleId = matchingRule.id;
119
+ mockEntry.labels = matchingRule.labels;
120
+ if ((_a = matchingRule.actions) === null || _a === void 0 ? void 0 : _a.mockResponse) {
121
+ const staticResponse = typeof matchingRule.actions.mockResponse === 'function'
122
+ ? matchingRule.actions.mockResponse(mockEntry.modifiedRequest)
123
+ : matchingRule.actions.mockResponse;
124
+ mockEntry.modifiedResponse = staticResponse;
125
+ shared_1.logger.debug({ ...logContext, staticResponse }, 'replying with mocked response');
126
+ if (matchingRule.storeTraffic) {
127
+ this.trafficStore.set(requestUuid, mockEntry);
128
+ }
129
+ if (staticResponse.rawHeaders) {
130
+ for (const header of staticResponse.rawHeaders.toHeaderPairs()) {
131
+ res.setHeader(header[0], header[1]);
132
+ }
133
+ }
134
+ res.status(staticResponse.status || 200);
135
+ res.send(staticResponse.body);
136
+ // static response blocks any further processing
137
+ return;
138
+ }
139
+ if ((_b = matchingRule.actions) === null || _b === void 0 ? void 0 : _b.modifyRequest) {
140
+ mockEntry.modifiedRequest = (_c = matchingRule.actions) === null || _c === void 0 ? void 0 : _c.modifyRequest(mockEntry.modifiedRequest);
141
+ shared_1.logger.debug({ ...logContext, modifiedRequest: mockEntry.modifiedRequest }, 'modified original request');
142
+ }
143
+ }
144
+ if (this.ipUtils && !isProxiedHostname && !this.ipUtils.isIP(originalHostname)) {
145
+ const hostname = originalHostname.split(':')[0];
146
+ try {
147
+ const internalIPs = await this.ipUtils.resolveIP(hostname);
148
+ if (this.ipUtils.isLocalhostIP(internalIPs) && this.options.mock.externalDns.length) {
149
+ const externalIPs = await this.ipUtils.resolveIP(hostname, { useExternalDns: true });
150
+ shared_1.logger.debug({ ...logContext, hostname, externalIPs, internalIPs }, 'switched to external IP');
151
+ mockEntry.modifiedRequest.url = mockEntry.modifiedRequest.url.replace(/^(https?:\/\/)[^:/]+/i, `$1${externalIPs}`);
152
+ }
153
+ }
154
+ catch (error) {
155
+ // swallow the exeception, don't think much can be done at this point
156
+ shared_1.logger.warn({ ...logContext, error }, `error trying to resolve IP for "${hostname}"`);
157
+ }
158
+ }
159
+ let controller = new AbortController();
160
+ const fetchTimeout = setTimeout(() => {
161
+ if (controller) {
162
+ controller.abort(`timeout after ${this.options.mock.timeout}`);
163
+ }
164
+ }, this.options.mock.timeout);
165
+ req.on('close', () => {
166
+ shared_1.logger.debug(logContext, 'remote client canceled the request');
167
+ clearTimeout(fetchTimeout);
168
+ if (controller) {
169
+ controller.abort('remote client canceled the request');
170
+ }
171
+ });
172
+ let targetResponse;
173
+ const hasKeepAlive = !!mockEntry.modifiedRequest.rawHeaders
174
+ .toHeaderPairs()
175
+ .find((h) => /^connection$/.test(h[0]) && /^keep-alive$/.test(h[1]));
176
+ try {
177
+ targetResponse = await fetch(mockEntry.modifiedRequest.url, {
178
+ redirect: 'manual',
179
+ headers: mockEntry.modifiedRequest.rawHeaders
180
+ .toHeaderPairs()
181
+ .filter((h) => !/^connection$/.test(h[0]) && !/^keep-alive$/.test(h[1])),
182
+ body: mockEntry.modifiedRequest.body,
183
+ method: mockEntry.modifiedRequest.method,
184
+ keepalive: !!hasKeepAlive,
185
+ });
186
+ }
187
+ finally {
188
+ controller = null;
189
+ clearTimeout(fetchTimeout);
190
+ }
191
+ const targetResponseBuffer = Buffer.from(await targetResponse.arrayBuffer());
192
+ const originalResponse = {
193
+ timestamp: Date.now(),
194
+ body: targetResponseBuffer.toString('binary'),
195
+ status: targetResponse.status,
196
+ rawHeaders: new shared_1.RawHeaders(...Array.from(targetResponse.headers.entries()).flatMap(([key, value]) => [key, value])),
197
+ };
198
+ shared_1.logger.debug({ ...logContext, originalResponse }, 'received response');
199
+ mockEntry.originalResponse = originalResponse;
200
+ let modifedResponse = {
201
+ ...originalResponse,
202
+ rawHeaders: new shared_1.RawHeaders(...Array.from(targetResponse.headers.entries()).flatMap(([key, value]) => {
203
+ // TODO this replace may be too aggressive and doesn't handle protocol (won't be necessary with a trusted cert and mock serving http+https)
204
+ return [
205
+ key,
206
+ isProxiedHostname
207
+ ? value
208
+ : value.replace(new RegExp(`(?:^|\\b)(${unproxiedHostname.replace('.', '\\.')})(?:\\b|$)`, 'igm'), originalHostname),
209
+ ];
210
+ })),
211
+ };
212
+ if ((_d = matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.actions) === null || _d === void 0 ? void 0 : _d.modifyResponse) {
213
+ modifedResponse = (_e = matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.actions) === null || _e === void 0 ? void 0 : _e.modifyResponse(mockEntry.modifiedRequest, originalResponse);
214
+ shared_1.logger.debug({ ...logContext, modifedResponse }, 'modified response');
215
+ }
216
+ mockEntry.modifiedResponse = modifedResponse;
217
+ if (matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.storeTraffic) {
218
+ this.trafficStore.set(requestUuid, mockEntry);
219
+ }
220
+ if (modifedResponse.status) {
221
+ res.status(modifedResponse.status);
222
+ }
223
+ if (modifedResponse.rawHeaders) {
224
+ for (const header of modifedResponse.rawHeaders.toHeaderPairs()) {
225
+ // since fetch decompresses responses we need to get rid of some headers
226
+ // TODO maybe could be handled better than just skipping, although express should add these back for new body
227
+ if (/^content-(?:length|encoding)$/i.test(header[0])) {
228
+ continue;
229
+ }
230
+ res.setHeader(header[0], header[1]);
231
+ }
232
+ }
233
+ res.end(Buffer.from(modifedResponse.body, 'binary'));
234
+ });
235
+ this.mockApp.use((error, req, res, _next) => {
236
+ const ctx = requestContext_1.default.get(req);
237
+ const uuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)();
238
+ shared_1.logger.error({ ...error, uuid }, 'Unexpected error');
239
+ if (res) {
240
+ res.status(shared_1.HttpCode.INTERNAL_SERVER_ERROR).json({
241
+ error: { message: error.message, httpCode: shared_1.HttpCode.INTERNAL_SERVER_ERROR, uuid },
242
+ });
243
+ return;
244
+ }
245
+ console.log('mock encountered a critical error. exiting');
246
+ process.exit(1);
247
+ });
248
+ }
249
+ start() {
250
+ if (this.server) {
251
+ throw new Error('mock server already started');
252
+ }
253
+ if (this.options.mock.httpsPort) {
254
+ this.serverHttps = https_1.default
255
+ .createServer({
256
+ key: this.options.mock.httpsKey,
257
+ cert: this.options.mock.httpsCert,
258
+ }, this.mockApp)
259
+ .listen(this.options.mock.httpsPort, () => {
260
+ shared_1.logger.info(`Mock listening on ${this.options.mock.domain}:${this.options.mock.httpsPort}`);
261
+ });
262
+ }
263
+ this.server = this.mockApp.listen(this.options.mock.port, () => {
264
+ var _a;
265
+ shared_1.logger.info(`Mock listening on ${this.options.mock.domain}:${this.options.mock.port}`);
266
+ if (!this.options.api.disabled) {
267
+ (_a = this.apiServer) === null || _a === void 0 ? void 0 : _a.start();
268
+ }
269
+ });
270
+ }
271
+ stop() {
272
+ var _a;
273
+ if (!this.server) {
274
+ throw new Error('mock server not started');
275
+ }
276
+ if (!this.options.api.disabled) {
277
+ (_a = this.apiServer) === null || _a === void 0 ? void 0 : _a.stop();
278
+ }
279
+ this.server.close((error) => {
280
+ shared_1.logger.warn(error, 'problem closing server');
281
+ this.server = null;
282
+ });
283
+ }
284
+ unproxyRequest(req) {
285
+ const protocol = (this.MOCK_DOMAIN_REGEX.exec(req.hostname) || [])[2] || req.protocol;
286
+ const port = (this.MOCK_DOMAIN_REGEX.exec(req.hostname) || [])[1] || undefined;
287
+ // TODO unproxied req might fail if there's a signed url :shrug:
288
+ // but then we can probably switch DNS for some particular 3rd party server to point to mock
289
+ // and in mock have a mapping rule for that domain to point directly to some IP :thinking:
290
+ return {
291
+ url: `${protocol}://${req.hostname.replace(this.MOCK_DOMAIN_REGEX, '')}${port ? `:${port}` : ''}${req.originalUrl}`,
292
+ rawHeaders: new shared_1.RawHeaders(...req.rawHeaders.map((h) => h.replace(this.MOCK_DOMAIN_REGEX, ''))),
293
+ method: req.method,
294
+ ...(Buffer.isBuffer(req.body) && { body: req.body.toString('utf-8') }),
295
+ };
296
+ }
297
+ removeProxyPort(req) {
298
+ if (this.URL_PORT_REGEX.test(req.url)) {
299
+ req.url = req.url.replace(this.URL_PORT_REGEX, '$1$2');
300
+ }
301
+ const host = req.rawHeaders.get('host') || '';
302
+ if (host.endsWith(`:${this.options.mock.port}`) ||
303
+ (this.options.mock.httpsPort && host.endsWith(`:${this.options.mock.httpsPort}`))) {
304
+ req.rawHeaders.set('host', host.split(':')[0]);
305
+ }
306
+ }
307
+ }
308
+ exports.Mock = Mock;
@@ -0,0 +1,9 @@
1
+ import type { Request } from 'express';
2
+ export default class RequestContext {
3
+ static _bindings: WeakMap<Request, RequestContext>;
4
+ readonly mockUuid: string;
5
+ readonly uuid: string;
6
+ constructor(mockUuid: string);
7
+ static bind(req: Request, mockUuid: string): void;
8
+ static get(req: Request): RequestContext | null;
9
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const uuid_1 = require("uuid");
4
+ class RequestContext {
5
+ constructor(mockUuid) {
6
+ this.uuid = (0, uuid_1.v4)();
7
+ this.mockUuid = mockUuid;
8
+ }
9
+ static bind(req, mockUuid) {
10
+ const ctx = new RequestContext(mockUuid);
11
+ RequestContext._bindings.set(req, ctx);
12
+ }
13
+ static get(req) {
14
+ return RequestContext._bindings.get(req) || null;
15
+ }
16
+ }
17
+ exports.default = RequestContext;
18
+ RequestContext._bindings = new WeakMap();
@@ -0,0 +1,21 @@
1
+ import type * as Stuntman from '@stuntman/shared';
2
+ declare class RuleExecutor {
3
+ private _rules;
4
+ private get enabledRules();
5
+ constructor(rules?: Stuntman.Rule[]);
6
+ private hasExpired;
7
+ private cleanUpExpired;
8
+ addRule(rule: Stuntman.Rule, overwrite?: boolean): Promise<Stuntman.LiveRule>;
9
+ private _removeRule;
10
+ removeRule(id: string): Promise<void>;
11
+ removeRule(rule: Stuntman.Rule): Promise<void>;
12
+ enableRule(id: string): void;
13
+ enableRule(rule: Stuntman.Rule): void;
14
+ disableRule(id: string): void;
15
+ disableRule(rule: Stuntman.Rule): void;
16
+ findMatchingRule(request: Stuntman.Request): Promise<Stuntman.LiveRule | null>;
17
+ getRules(): Promise<readonly Stuntman.LiveRule[]>;
18
+ getRule(id: string): Promise<Stuntman.LiveRule | undefined>;
19
+ }
20
+ export declare const ruleExecutor: RuleExecutor;
21
+ export {};