@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 +121 -0
- package/package.json +12 -2
- package/config/default.json +0 -23
- package/config/test.json +0 -26
- package/src/api/api.ts +0 -194
- package/src/api/utils.ts +0 -69
- package/src/api/validatiors.ts +0 -123
- package/src/api/webgui/rules.pug +0 -145
- package/src/api/webgui/style.css +0 -28
- package/src/api/webgui/traffic.pug +0 -37
- package/src/bin/stuntman.ts +0 -8
- package/src/index.ts +0 -1
- package/src/ipUtils.ts +0 -83
- package/src/mock.ts +0 -349
- package/src/requestContext.ts +0 -23
- package/src/ruleExecutor.ts +0 -193
- package/src/rules/catchAll.ts +0 -14
- package/src/rules/echo.ts +0 -14
- package/src/rules/index.ts +0 -7
- package/src/storage.ts +0 -39
- package/tsconfig.json +0 -16
package/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.
|
|
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.
|
|
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",
|
package/config/default.json
DELETED
|
@@ -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
|
-
};
|
package/src/api/validatiors.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { AppError, HttpCode, MAX_RULE_TTL_SECONDS, MIN_RULE_TTL_SECONDS, logger, RawHeaders } from '@stuntman/shared';
|
|
2
|
-
import type * as Stuntman from '@stuntman/shared';
|
|
3
|
-
|
|
4
|
-
export const validateSerializedRuleProperties = (rule: Stuntman.SerializedRule): void => {
|
|
5
|
-
if (!rule) {
|
|
6
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid serialized rule' });
|
|
7
|
-
}
|
|
8
|
-
if (typeof rule.id !== 'string') {
|
|
9
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.id' });
|
|
10
|
-
}
|
|
11
|
-
if (typeof rule.matches !== 'object' || !('remoteFn' in rule.matches) || typeof rule.matches.remoteFn !== 'string') {
|
|
12
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.matches' });
|
|
13
|
-
}
|
|
14
|
-
if (rule.priority && typeof rule.priority !== 'number') {
|
|
15
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.priority' });
|
|
16
|
-
}
|
|
17
|
-
if (typeof rule.actions !== 'undefined') {
|
|
18
|
-
if (typeof rule.actions !== 'object') {
|
|
19
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions' });
|
|
20
|
-
}
|
|
21
|
-
if (typeof rule.actions.mockResponse !== 'undefined') {
|
|
22
|
-
if (typeof rule.actions.mockResponse !== 'object') {
|
|
23
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
24
|
-
}
|
|
25
|
-
if ('remoteFn' in rule.actions.mockResponse && typeof rule.actions.mockResponse.remoteFn !== 'string') {
|
|
26
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
27
|
-
} else if ('status' in rule.actions.mockResponse) {
|
|
28
|
-
if (typeof rule.actions.mockResponse.status !== 'number') {
|
|
29
|
-
throw new AppError({
|
|
30
|
-
httpCode: HttpCode.BAD_REQUEST,
|
|
31
|
-
message: 'invalid rule.actions.mockResponse.status',
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
if (
|
|
35
|
-
typeof rule.actions.mockResponse.rawHeaders !== 'undefined' &&
|
|
36
|
-
(!Array.isArray(rule.actions.mockResponse.rawHeaders) ||
|
|
37
|
-
rule.actions.mockResponse.rawHeaders.some((header) => typeof header !== 'string'))
|
|
38
|
-
) {
|
|
39
|
-
throw new AppError({
|
|
40
|
-
httpCode: HttpCode.BAD_REQUEST,
|
|
41
|
-
message: 'invalid rule.actions.mockResponse.rawHeaders',
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
if (typeof rule.actions.mockResponse.body !== 'undefined' && typeof rule.actions.mockResponse.body !== 'string') {
|
|
45
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse.body' });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (typeof rule.actions.modifyRequest !== 'undefined' && typeof rule.actions.modifyRequest.remoteFn !== 'string') {
|
|
50
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyRequest' });
|
|
51
|
-
}
|
|
52
|
-
if (typeof rule.actions.modifyResponse !== 'undefined' && typeof rule.actions.modifyResponse.remoteFn !== 'string') {
|
|
53
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyResponse' });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (
|
|
57
|
-
typeof rule.disableAfterUse !== 'undefined' &&
|
|
58
|
-
typeof rule.disableAfterUse !== 'boolean' &&
|
|
59
|
-
(typeof rule.disableAfterUse !== 'number' || rule.disableAfterUse < 0)
|
|
60
|
-
) {
|
|
61
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.disableAfterUse' });
|
|
62
|
-
}
|
|
63
|
-
if (
|
|
64
|
-
typeof rule.removeAfterUse !== 'undefined' &&
|
|
65
|
-
typeof rule.removeAfterUse !== 'boolean' &&
|
|
66
|
-
(typeof rule.removeAfterUse !== 'number' || rule.removeAfterUse < 0)
|
|
67
|
-
) {
|
|
68
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.removeAfterUse' });
|
|
69
|
-
}
|
|
70
|
-
if (
|
|
71
|
-
typeof rule.labels !== 'undefined' &&
|
|
72
|
-
(!Array.isArray(rule.labels) || rule.labels.some((label) => typeof label !== 'string'))
|
|
73
|
-
) {
|
|
74
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.labels' });
|
|
75
|
-
}
|
|
76
|
-
if (rule.actions?.mockResponse && rule.actions.modifyResponse) {
|
|
77
|
-
throw new AppError({
|
|
78
|
-
httpCode: HttpCode.BAD_REQUEST,
|
|
79
|
-
message: 'rule.actions.mockResponse and rule.actions.modifyResponse are mutually exclusive',
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
if (!rule.ttlSeconds || rule.ttlSeconds < MIN_RULE_TTL_SECONDS || rule.ttlSeconds > MAX_RULE_TTL_SECONDS) {
|
|
83
|
-
throw new AppError({
|
|
84
|
-
httpCode: HttpCode.BAD_REQUEST,
|
|
85
|
-
message: `rule.ttlSeconds should be within ${MIN_RULE_TTL_SECONDS} and ${MAX_RULE_TTL_SECONDS}`,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
if (typeof rule.isEnabled !== 'undefined' && typeof rule.isEnabled !== 'boolean') {
|
|
89
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.isEnabled' });
|
|
90
|
-
}
|
|
91
|
-
if (typeof rule.storeTraffic !== 'undefined' && typeof rule.storeTraffic !== 'boolean') {
|
|
92
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.storeTraffic' });
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export const validateDeserializedRule = (deserializedRule: Stuntman.Rule) => {
|
|
97
|
-
// TODO validate other functions ?
|
|
98
|
-
let matchValidationResult: Stuntman.RuleMatchResult;
|
|
99
|
-
try {
|
|
100
|
-
matchValidationResult = deserializedRule.matches({
|
|
101
|
-
id: 'validation',
|
|
102
|
-
method: 'GET',
|
|
103
|
-
rawHeaders: new RawHeaders(),
|
|
104
|
-
timestamp: Date.now(),
|
|
105
|
-
url: 'http://dummy.invalid/',
|
|
106
|
-
});
|
|
107
|
-
} catch (error: any) {
|
|
108
|
-
logger.error({ ruleId: deserializedRule.id }, error);
|
|
109
|
-
throw new AppError({
|
|
110
|
-
httpCode: HttpCode.UNPROCESSABLE_ENTITY,
|
|
111
|
-
message: 'match function returned invalid value',
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
if (
|
|
115
|
-
matchValidationResult !== true &&
|
|
116
|
-
matchValidationResult !== false &&
|
|
117
|
-
matchValidationResult.result !== true &&
|
|
118
|
-
matchValidationResult.result !== false
|
|
119
|
-
) {
|
|
120
|
-
logger.error({ ruleId: deserializedRule.id, matchValidationResult }, 'match function retruned invalid value');
|
|
121
|
-
throw new AppError({ httpCode: HttpCode.UNPROCESSABLE_ENTITY, message: 'match function returned invalid value' });
|
|
122
|
-
}
|
|
123
|
-
};
|