@stuntman/server 0.1.0 → 0.1.1

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 ADDED
@@ -0,0 +1,121 @@
1
+ # Stuntman
2
+
3
+ ## Building from source
4
+
5
+ ### Prerequisites
6
+
7
+ * [pnpm](https://github.com/pnpm/pnpm) package manager
8
+ * [nvm](https://github.com/nvm-sh/nvm) node version manager (optional)
9
+
10
+ ```bash
11
+ nvm use
12
+ pnpm install --frozen-lockfile
13
+ pnpm build
14
+ ```
15
+
16
+ ### Start server
17
+
18
+ ```bash
19
+ pnpm stuntman
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ Stuntman uses [config](https://github.com/node-config/node-config)
25
+ You can create `config/default.json` with settings of your liking matching `ServerConfig` type
26
+
27
+ ## Running as a package
28
+
29
+ ### Install with package manager of your choice
30
+
31
+ ```bash
32
+ npm install @stuntman/server
33
+ yarn add @stuntman/server
34
+ pnpm add @stuntman/server
35
+ ```
36
+
37
+ ### Run from bin
38
+
39
+ ```bash
40
+ stuntman
41
+ yarn stuntman
42
+ node ./node_modules/.bin/stuntman
43
+ ```
44
+
45
+ ### Run programatically
46
+
47
+ ```ts
48
+ import { Mock } from '../mock';
49
+ import { serverConfig } from '@stuntman/shared';
50
+
51
+ const mock = new Mock(serverConfig);
52
+
53
+ mock.start();
54
+ ```
55
+
56
+ ### Point domain to localhost
57
+
58
+ Add some domains with `.stuntman` suffix (or `.stuntmanhttp` / `.stuntmanhttps` depending where you want to direct the traffic in proxy mode) to your `/etc/hosts` for example
59
+
60
+ ```text
61
+ 127.0.0.1 www.example.com.stuntman
62
+ ```
63
+
64
+ ### Try in browser
65
+
66
+ go to your browser and visit `http://www.example.com.stuntman:2015/` to see the proxied page
67
+ for local playground you can also use `http://www.example.com.localhost:2015`
68
+
69
+ ### Take a look at client
70
+
71
+ Take a look at `./src/clientTestExample.ts`, you can use it to set up some rules
72
+
73
+ Mind the scope of `Stuntman.RemotableFunction` like `matches`, `modifyRequest`, `modifyResponse`.
74
+ `Stuntman.RemotableFunction.localFn` contains the function, but since it'll be executed on a remote mock server it cannot access any variables outside it's body. In order to pass variable values into the function use `Stuntman.RemotableFunction.variables` for example:
75
+
76
+ ```ts
77
+ matches: {
78
+ localFn: (req) => {
79
+ // you might need to ignore typescript errors about undefined variables in this scope
80
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
81
+ // @ts-ignore
82
+ return /http:\/\/[^/]+\/somepath$/.test(req.url) && req.url.includes(`?someparam=${myVar}`);
83
+ },
84
+ localVariables: { myVar: 'myValue' },
85
+ }
86
+ ```
87
+
88
+ You can build the rules using fluentish `ruleBuilder`
89
+
90
+ ```ts
91
+ import { Client } from './apiClient';
92
+ import { ruleBuilder } from './ruleBuilder';
93
+
94
+ const client = new Client();
95
+
96
+ const uniqueQaUserEmail = 'unique_qa_email@example.com';
97
+ const rule = ruleBuilder()
98
+ .limitedUse(2)
99
+ .onRequestToHostname('example.com')
100
+ .withSearchParam('user', uniqueQaUserEmail)
101
+ .mockResponse({
102
+ localFn: (req) => {
103
+ if (JSON.parse(req.body).email !== uniqueQaUserEmail) {
104
+ return {
105
+ status: 500,
106
+ };
107
+ }
108
+ return { status: 201 };
109
+ },
110
+ localVariables: { uniqueQaUserEmail },
111
+ });
112
+
113
+ client.addRule(rule).then((x) => console.log(x));
114
+ ```
115
+
116
+ ### Take a look at PoC of WebGUI
117
+
118
+ ....just don't look to closely, it's very much incomplete and hacky
119
+
120
+ * http://stuntman:1985/webgui/rules - rule viewer/editor
121
+ * http://stuntman:1985/webgui/traffic - traffic viewer for the rules that store traffic
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@stuntman/server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Stuntman - HTTP proxy / mock server with API",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/andrzej-woof/stuntman.git"
9
9
  },
10
+ "homepage": "https://github.com/andrzej-woof/stuntman#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/andrzej-woof/stuntman/issues"
13
+ },
10
14
  "keywords": [
11
15
  "proxy",
12
16
  "mock",
@@ -28,7 +32,7 @@
28
32
  "author": "Andrzej Pasterczyk",
29
33
  "license": "MIT",
30
34
  "dependencies": {
31
- "@stuntman/shared": "^0.1.0",
35
+ "@stuntman/shared": "^0.1.1",
32
36
  "await-lock": "2.2.2",
33
37
  "express": "5.0.0-beta.1",
34
38
  "lru-cache": "7.16.0",
@@ -45,6 +49,12 @@
45
49
  "bin": {
46
50
  "stuntman": "./dist/bin/stuntman.js"
47
51
  },
52
+ "files": [
53
+ "dist/",
54
+ "README.md",
55
+ "LICENSE",
56
+ "CHANGELOG.md"
57
+ ],
48
58
  "scripts": {
49
59
  "test": "echo \"Error: no test specified\" && exit 1",
50
60
  "clean": "rm -fr dist",
@@ -1,23 +0,0 @@
1
- {
2
- "stuntman": {
3
- "mock": {
4
- "domain": "stuntman",
5
- "port": 2015,
6
- "timeout": 60000,
7
- "externalDns": ["8.8.8.8", "1.1.1.1"]
8
- },
9
- "api": {
10
- "port": 1985,
11
- "disabled": false
12
- },
13
- "webgui": {
14
- "disabled": false
15
- },
16
- "storage": {
17
- "traffic": {
18
- "limitCount": 500,
19
- "limitSize": 524288000
20
- }
21
- }
22
- }
23
- }
package/config/test.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "stuntman": {
3
- "mock": {
4
- "domain": "stuntman",
5
- "port": 80,
6
- "httpsPort": 443,
7
- "httpsCert": "-----BEGIN CERTIFICATE-----\nMIIC7jCCAdYCCQDHj59tQDx5iTANBgkqhkiG9w0BAQsFADA5MREwDwYDVQQKDAhz\ndHVudG1hbjERMA8GA1UECwwIc3R1bnRtYW4xETAPBgNVBAMMCHN0dW50bWFuMB4X\nDTIzMDIxNjE1MzQzNFoXDTI0MDIxNjE1MzQzNFowOTERMA8GA1UECgwIc3R1bnRt\nYW4xETAPBgNVBAsMCHN0dW50bWFuMREwDwYDVQQDDAhzdHVudG1hbjCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK23MCp+grcLiyFzxvSU9iOFReIKZRVN\n0+DTlxl/sN4pvYlt7Hji7n/Yh55m7ACF1j8LRjaU6MOYIGofF4lbgA3nEbZVNJoH\nAtlSjk/JZc4LnDFinAWWxna2FpwrpfEnknIJ2B7fjtk5dM/WzSMn1MdPiC9V9Ee0\nPFe0PlpFl/hQSd4/VXfLxNy3bzW5AXa5CuTVRaEmts21TbL4VYe6KNPMkbTe+NJh\nwBrwVqS5lB3Z5racxOn5Dw5g5NuHgSA6LvUxdKhdkPs7y7e87XADadakibd9u02j\nimJwQih31O4rPJINLDYhVj5muyPGw9lpxEQ7UthxRxuzodm4F+5ZM1ECAwEAATAN\nBgkqhkiG9w0BAQsFAAOCAQEAhqISsPYrM+G37vw8I6YCWNSW0dJrpvfNpiz6oXal\nicIxOJz06qg0HsEXoWhdneo9PSA66KAmdcTplwPJtZ486izwD3F46+TZLkesOuCS\nDW9ihEPY5XPyjZDSz2J4EwBD4pH0AFeXSVFDIyXCSoWypSKjSq5lm7hOQuCOLkkm\ntlsptc4R3MGuvNYKSDBvxCjTy76jlXpMWINdVV18M4bVmRnVj+vYlQbYP5tCYGUm\nnzlFVi0dCLdvS2LGiKhARLQILP9YzC86a9UDPyWs703Zvqm5cnknCLEpjaR8dhd8\njAcDPHUe1RkR8wGrGwkkrIQfe8r8ovEylJgLT8HtNLqEXg==\n-----END CERTIFICATE-----\n",
8
- "httpsKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArbcwKn6CtwuLIXPG9JT2I4VF4gplFU3T4NOXGX+w3im9iW3s\neOLuf9iHnmbsAIXWPwtGNpTow5ggah8XiVuADecRtlU0mgcC2VKOT8llzgucMWKc\nBZbGdrYWnCul8SeScgnYHt+O2Tl0z9bNIyfUx0+IL1X0R7Q8V7Q+WkWX+FBJ3j9V\nd8vE3LdvNbkBdrkK5NVFoSa2zbVNsvhVh7oo08yRtN740mHAGvBWpLmUHdnmtpzE\n6fkPDmDk24eBIDou9TF0qF2Q+zvLt7ztcANp1qSJt327TaOKYnBCKHfU7is8kg0s\nNiFWPma7I8bD2WnERDtS2HFHG7Oh2bgX7lkzUQIDAQABAoIBAH+L7GKXBvTNFfeW\n4XK9WMgV14yzIyr0POhrkxrWxY8pSI/6VNEhlgnqexET8p4jpn4dkg0LYqgSL2Kb\nt5VTyH7stPWSNBAPq8jTM8hjUEtr/N/JzlLQNKH+6jT6W1noOz9d+QAaFvFpnVnp\nFi+E1FcPDyfqTXTEYjXnEo0HYiCf5RAIw64VYRR3OfmCWFHjwz3sDbhvTW1bYfZA\nddwViTIoELfebF3cCLg4zWVkyCeZRpmbRJaeyttrqgOLbjD6tn7SFkZVJ8v4BBoK\nZSdRaFrzPrRxBYcLRbXlIaNp0QeM/NkSBZIg63vhwZydR+Y3wDE4mCzZ8UqiOyLC\nGIdHky0CgYEA1yY1WJ1ubOB67w4dckWMw9SOw00AP/i5o0j4kFz8nkcWhx8W6rJW\nrZrPq6yAZ6ffzR4aIwrq0W22nHB20sOvamO7UH7TzG3BuM787ChMqB0bEy98hKHZ\nHTAOKyqG9A50N+QNicUS9gXDWk67/i3j19bw8rLWMrNsQDM9SZHYvaMCgYEAzrMB\nz+ofYQ04z7gIKmlOK+lG9pT3JyNVdnLnHFhil4Q4AKMDDIsoKkDqjE2jJeTNzE+G\nIGlZyiBK6sArTJdNthrvrJLmvJJfVEGWpSnShNxDf+gzIJeUoA/TCJvUac1CXd8g\nHwnhR3Dp1I3SZwm32Hig/vzxe8Dd+YONPoNm8nsCgYBJ1pcgXodzXmdSe+mnOi9h\nViXY6ShYzCgJ3hVQllksiQE2Rnk6+xG8axEyvfUjnf21C8u0kx6b2ad+cSqWkwo0\n3R2ANsbBtjlyD7fF5N7KI5MTNozpiBJXbhKuxd2jDQLd26q5yaUEQl4VNEhYp682\neFIhOTdCF0njjrJN+XwFOQKBgF6j2aWQBhQS0LtTAPIiSzeR1PscE9notL3KOIVi\n9ql3UYkBGmlI4fgOxxW8ioHUNGJi2v/GHOWOSZ8Yo/qqoFtMFAdJL7qRrnJOoaI3\n9vr8Oy+6aoZ2wQdUl4SujOBwqf1/Jx7vECX8ziOTWA3zhijoepalzA+krD4NfMNt\nuNo3AoGBAISjSwEUrpR3II4uj8UuPZaVFNvACujaJLWnKcGwFvQsn/2GTcfzdzm/\nTxwHwpRZLJhFFpboFGVW5pX7g9leqdZERGlqPpTkCSAiQ0eFRbHsIhX7TS9VyYMz\n7iLq9dccEn5DDnUVXWkZxz2h0yG8/nTlNGli0BL2O+DEWZ9b32xE\n-----END RSA PRIVATE KEY-----\n",
9
- "timeout": 60000,
10
- "externalDns": ["8.8.8.8", "1.1.1.1"]
11
- },
12
- "api": {
13
- "port": 1985,
14
- "disabled": false
15
- },
16
- "webgui": {
17
- "disabled": false
18
- },
19
- "storage": {
20
- "traffic": {
21
- "limitCount": 500,
22
- "limitSize": 524288000
23
- }
24
- }
25
- }
26
- }
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
- };
@@ -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
- };