@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Andrzej Pasterczyk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,23 @@
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
+ }
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,19 @@
1
+ /// <reference types="node" />
2
+ import http from 'http';
3
+ import { 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
+ constructor(options: ApiOptions, webGuiOptions?: Stuntman.WebGuiConfig);
15
+ private initWebGui;
16
+ start(): void;
17
+ stop(): void;
18
+ }
19
+ export {};
@@ -0,0 +1,162 @@
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 validatiors_1 = require("./validatiors");
15
+ const utils_1 = require("./utils");
16
+ class API {
17
+ constructor(options, webGuiOptions) {
18
+ this.server = null;
19
+ this.options = options;
20
+ this.trafficStore = (0, storage_1.getTrafficStore)(this.options.mockUuid);
21
+ this.apiApp = (0, express_1.default)();
22
+ this.apiApp.use(express_1.default.json());
23
+ this.apiApp.use(express_1.default.text());
24
+ this.apiApp.use((req, res, next) => {
25
+ requestContext_1.default.bind(req, this.options.mockUuid);
26
+ next();
27
+ });
28
+ this.apiApp.get('/rule', async (req, res) => {
29
+ res.send((0, shared_1.stringify)(await ruleExecutor_1.ruleExecutor.getRules()));
30
+ });
31
+ this.apiApp.get('/rule/:ruleId', async (req, res) => {
32
+ res.send((0, shared_1.stringify)(await ruleExecutor_1.ruleExecutor.getRule(req.params.ruleId)));
33
+ });
34
+ this.apiApp.get('/rule/:ruleId/disable', (req, res) => {
35
+ ruleExecutor_1.ruleExecutor.disableRule(req.params.ruleId);
36
+ res.send();
37
+ });
38
+ this.apiApp.get('/rule/:ruleId/enable', (req, res) => {
39
+ ruleExecutor_1.ruleExecutor.enableRule(req.params.ruleId);
40
+ res.send();
41
+ });
42
+ this.apiApp.post('/rule', async (req, res) => {
43
+ const deserializedRule = (0, utils_1.deserializeRule)(req.body);
44
+ (0, validatiors_1.validateDeserializedRule)(deserializedRule);
45
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
46
+ // @ts-ignore
47
+ const rule = await ruleExecutor_1.ruleExecutor.addRule(deserializedRule);
48
+ res.send((0, shared_1.stringify)(rule));
49
+ });
50
+ this.apiApp.get('/rule/:ruleId/remove', async (req, res) => {
51
+ await ruleExecutor_1.ruleExecutor.removeRule(req.params.ruleId);
52
+ res.send();
53
+ });
54
+ this.apiApp.get('/traffic', (req, res) => {
55
+ const serializedTraffic = {};
56
+ for (const [key, value] of this.trafficStore.entries()) {
57
+ serializedTraffic[key] = value;
58
+ }
59
+ res.json(serializedTraffic);
60
+ });
61
+ this.apiApp.get('/traffic/:ruleIdOrLabel', (req, res) => {
62
+ const serializedTraffic = {};
63
+ for (const [key, value] of this.trafficStore.entries()) {
64
+ if (value.mockRuleId === req.params.ruleIdOrLabel || (value.labels || []).includes(req.params.ruleIdOrLabel)) {
65
+ serializedTraffic[key] = value;
66
+ }
67
+ }
68
+ res.json(serializedTraffic);
69
+ });
70
+ this.apiApp.use((error, req, res, _next) => {
71
+ const ctx = requestContext_1.default.get(req);
72
+ const uuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)();
73
+ if (error instanceof shared_1.AppError && error.isOperational && res) {
74
+ shared_1.logger.error(error);
75
+ res.status(error.httpCode).json({
76
+ error: { message: error.message, httpCode: error.httpCode, stack: error.stack },
77
+ });
78
+ return;
79
+ }
80
+ shared_1.logger.error({ ...error, uuid }, 'Unexpected error');
81
+ if (res) {
82
+ res.status(shared_1.HttpCode.INTERNAL_SERVER_ERROR).json({
83
+ error: { message: error.message, httpCode: shared_1.HttpCode.INTERNAL_SERVER_ERROR, uuid },
84
+ });
85
+ return;
86
+ }
87
+ console.log('Application encountered a critical error. Exiting');
88
+ process.exit(1);
89
+ });
90
+ this.apiApp.set('views', __dirname + '/webgui');
91
+ this.apiApp.set('view engine', 'pug');
92
+ if (!(webGuiOptions === null || webGuiOptions === void 0 ? void 0 : webGuiOptions.disabled)) {
93
+ this.initWebGui();
94
+ }
95
+ }
96
+ initWebGui() {
97
+ this.apiApp.get('/webgui/rules', async (req, res) => {
98
+ const rules = {};
99
+ for (const rule of await ruleExecutor_1.ruleExecutor.getRules()) {
100
+ rules[rule.id] = (0, serialize_javascript_1.default)((0, utils_1.liveRuleToRule)(rule), { unsafe: true });
101
+ }
102
+ res.render('rules', { rules: (0, utils_1.escapedSerialize)(rules), INDEX_DTS: shared_1.INDEX_DTS, ruleKeys: Object.keys(rules) });
103
+ });
104
+ this.apiApp.get('/webgui/traffic', async (req, res) => {
105
+ const serializedTraffic = [];
106
+ for (const value of this.trafficStore.values()) {
107
+ serializedTraffic.push(value);
108
+ }
109
+ res.render('traffic', {
110
+ traffic: JSON.stringify(serializedTraffic.sort((a, b) => b.originalRequest.timestamp - a.originalRequest.timestamp)),
111
+ });
112
+ });
113
+ // TODO make webui way better and safer, nicer formatting, eslint/prettier, blackjack and hookers
114
+ this.apiApp.post('/webgui/rules/unsafesave', async (req, res) => {
115
+ const rule = new Function(req.body)();
116
+ if (!rule ||
117
+ !rule.id ||
118
+ typeof rule.matches !== 'function' ||
119
+ typeof rule.ttlSeconds !== 'number' ||
120
+ rule.ttlSeconds > shared_1.MAX_RULE_TTL_SECONDS) {
121
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'Invalid rule' });
122
+ }
123
+ await ruleExecutor_1.ruleExecutor.addRule({
124
+ id: rule.id,
125
+ matches: rule.matches,
126
+ ttlSeconds: rule.ttlSeconds,
127
+ ...(rule.actions && {
128
+ actions: {
129
+ ...(rule.actions.mockResponse
130
+ ? { mockResponse: rule.actions.mockResponse }
131
+ : { modifyRequest: rule.actions.modifyRequest, modifyResponse: rule.actions.modifyResponse }),
132
+ },
133
+ }),
134
+ ...(rule.disableAfterUse !== undefined && { disableAfterUse: rule.disableAfterUse }),
135
+ ...(rule.isEnabled !== undefined && { isEnabled: rule.isEnabled }),
136
+ ...(rule.labels !== undefined && { labels: rule.labels }),
137
+ ...(rule.priority !== undefined && { priority: rule.priority }),
138
+ ...(rule.removeAfterUse !== undefined && { removeAfterUse: rule.removeAfterUse }),
139
+ ...(rule.storeTraffic !== undefined && { storeTraffic: rule.storeTraffic }),
140
+ }, true);
141
+ res.send();
142
+ });
143
+ }
144
+ start() {
145
+ if (this.server) {
146
+ throw new Error('mock server already started');
147
+ }
148
+ this.server = this.apiApp.listen(this.options.port, () => {
149
+ shared_1.logger.info(`API listening on ${this.options.port}`);
150
+ });
151
+ }
152
+ stop() {
153
+ if (!this.server) {
154
+ throw new Error('mock server not started');
155
+ }
156
+ this.server.close((error) => {
157
+ shared_1.logger.warn(error, 'problem closing server');
158
+ this.server = null;
159
+ });
160
+ }
161
+ }
162
+ exports.API = API;
@@ -0,0 +1,4 @@
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;
@@ -0,0 +1,69 @@
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.liveRuleToRule = exports.escapedSerialize = exports.deserializeRule = void 0;
7
+ const serialize_javascript_1 = __importDefault(require("serialize-javascript"));
8
+ const shared_1 = require("@stuntman/shared");
9
+ const validatiors_1 = require("./validatiors");
10
+ // TODO
11
+ const deserializeRule = (serializedRule) => {
12
+ shared_1.logger.debug(serializedRule, 'attempt to deserialize rule');
13
+ (0, validatiors_1.validateSerializedRuleProperties)(serializedRule);
14
+ const rule = {
15
+ id: serializedRule.id,
16
+ matches: (req) => new Function('____arg0', serializedRule.matches.remoteFn)(req),
17
+ ttlSeconds: serializedRule.ttlSeconds,
18
+ ...(serializedRule.disableAfterUse !== undefined && { disableAfterUse: serializedRule.disableAfterUse }),
19
+ ...(serializedRule.removeAfterUse !== undefined && { removeAfterUse: serializedRule.removeAfterUse }),
20
+ ...(serializedRule.labels !== undefined && { labels: serializedRule.labels }),
21
+ ...(serializedRule.priority !== undefined && { priority: serializedRule.priority }),
22
+ ...(serializedRule.isEnabled !== undefined && { isEnabled: serializedRule.isEnabled }),
23
+ ...(serializedRule.storeTraffic !== undefined && { storeTraffic: serializedRule.storeTraffic }),
24
+ };
25
+ if (serializedRule.actions) {
26
+ // TODO store the original localFn and variables sent from client for web UI editing maybe
27
+ if (serializedRule.actions.mockResponse) {
28
+ rule.actions = {
29
+ mockResponse: 'remoteFn' in serializedRule.actions.mockResponse
30
+ ? (req) => {
31
+ var _a;
32
+ return new Function('____arg0', ((_a = serializedRule.actions) === null || _a === void 0 ? void 0 : _a.mockResponse).remoteFn)(req);
33
+ }
34
+ : serializedRule.actions.mockResponse,
35
+ };
36
+ }
37
+ else {
38
+ rule.actions = {};
39
+ if (serializedRule.actions.modifyRequest) {
40
+ rule.actions.modifyRequest = (req) => {
41
+ var _a;
42
+ return new Function('____arg0', ((_a = serializedRule.actions) === null || _a === void 0 ? void 0 : _a.modifyRequest).remoteFn)(req);
43
+ };
44
+ }
45
+ if (serializedRule.actions.modifyResponse) {
46
+ rule.actions.modifyResponse = (req, res) => {
47
+ var _a;
48
+ return new Function('____arg0', '____arg1', ((_a = serializedRule.actions) === null || _a === void 0 ? void 0 : _a.modifyResponse).remoteFn)(req, res);
49
+ };
50
+ }
51
+ }
52
+ }
53
+ shared_1.logger.debug(rule, 'deserialized rule');
54
+ return rule;
55
+ };
56
+ exports.deserializeRule = deserializeRule;
57
+ const escapedSerialize = (obj) => (0, serialize_javascript_1.default)(obj).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, "\\n'\n+ '");
58
+ exports.escapedSerialize = escapedSerialize;
59
+ const liveRuleToRule = (liveRule) => {
60
+ const ruleClone = { ...liveRule };
61
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
62
+ // @ts-ignore
63
+ delete ruleClone.counter;
64
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
65
+ // @ts-ignore
66
+ delete ruleClone.createdTimestamp;
67
+ return ruleClone;
68
+ };
69
+ exports.liveRuleToRule = liveRuleToRule;
@@ -0,0 +1,3 @@
1
+ import type * as Stuntman from '@stuntman/shared';
2
+ export declare const validateSerializedRuleProperties: (rule: Stuntman.SerializedRule) => void;
3
+ export declare const validateDeserializedRule: (deserializedRule: Stuntman.Rule) => void;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateDeserializedRule = exports.validateSerializedRuleProperties = void 0;
4
+ const shared_1 = require("@stuntman/shared");
5
+ const validateSerializedRuleProperties = (rule) => {
6
+ var _a;
7
+ if (!rule) {
8
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid serialized rule' });
9
+ }
10
+ if (typeof rule.id !== 'string') {
11
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.id' });
12
+ }
13
+ if (typeof rule.matches !== 'object' || !('remoteFn' in rule.matches) || typeof rule.matches.remoteFn !== 'string') {
14
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.matches' });
15
+ }
16
+ if (rule.priority && typeof rule.priority !== 'number') {
17
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.priority' });
18
+ }
19
+ if (typeof rule.actions !== 'undefined') {
20
+ if (typeof rule.actions !== 'object') {
21
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions' });
22
+ }
23
+ if (typeof rule.actions.mockResponse !== 'undefined') {
24
+ if (typeof rule.actions.mockResponse !== 'object') {
25
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
26
+ }
27
+ if ('remoteFn' in rule.actions.mockResponse && typeof rule.actions.mockResponse.remoteFn !== 'string') {
28
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
29
+ }
30
+ else if ('status' in rule.actions.mockResponse) {
31
+ if (typeof rule.actions.mockResponse.status !== 'number') {
32
+ throw new shared_1.AppError({
33
+ httpCode: shared_1.HttpCode.BAD_REQUEST,
34
+ message: 'invalid rule.actions.mockResponse.status',
35
+ });
36
+ }
37
+ if (typeof rule.actions.mockResponse.rawHeaders !== 'undefined' &&
38
+ (!Array.isArray(rule.actions.mockResponse.rawHeaders) ||
39
+ rule.actions.mockResponse.rawHeaders.some((header) => typeof header !== 'string'))) {
40
+ throw new shared_1.AppError({
41
+ httpCode: shared_1.HttpCode.BAD_REQUEST,
42
+ message: 'invalid rule.actions.mockResponse.rawHeaders',
43
+ });
44
+ }
45
+ if (typeof rule.actions.mockResponse.body !== 'undefined' && typeof rule.actions.mockResponse.body !== 'string') {
46
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse.body' });
47
+ }
48
+ }
49
+ }
50
+ if (typeof rule.actions.modifyRequest !== 'undefined' && typeof rule.actions.modifyRequest.remoteFn !== 'string') {
51
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyRequest' });
52
+ }
53
+ if (typeof rule.actions.modifyResponse !== 'undefined' && typeof rule.actions.modifyResponse.remoteFn !== 'string') {
54
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyResponse' });
55
+ }
56
+ }
57
+ if (typeof rule.disableAfterUse !== 'undefined' &&
58
+ typeof rule.disableAfterUse !== 'boolean' &&
59
+ (typeof rule.disableAfterUse !== 'number' || rule.disableAfterUse < 0)) {
60
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.disableAfterUse' });
61
+ }
62
+ if (typeof rule.removeAfterUse !== 'undefined' &&
63
+ typeof rule.removeAfterUse !== 'boolean' &&
64
+ (typeof rule.removeAfterUse !== 'number' || rule.removeAfterUse < 0)) {
65
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.removeAfterUse' });
66
+ }
67
+ if (typeof rule.labels !== 'undefined' &&
68
+ (!Array.isArray(rule.labels) || rule.labels.some((label) => typeof label !== 'string'))) {
69
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.labels' });
70
+ }
71
+ if (((_a = rule.actions) === null || _a === void 0 ? void 0 : _a.mockResponse) && rule.actions.modifyResponse) {
72
+ throw new shared_1.AppError({
73
+ httpCode: shared_1.HttpCode.BAD_REQUEST,
74
+ message: 'rule.actions.mockResponse and rule.actions.modifyResponse are mutually exclusive',
75
+ });
76
+ }
77
+ if (!rule.ttlSeconds || rule.ttlSeconds < shared_1.MIN_RULE_TTL_SECONDS || rule.ttlSeconds > shared_1.MAX_RULE_TTL_SECONDS) {
78
+ throw new shared_1.AppError({
79
+ httpCode: shared_1.HttpCode.BAD_REQUEST,
80
+ message: `rule.ttlSeconds should be within ${shared_1.MIN_RULE_TTL_SECONDS} and ${shared_1.MAX_RULE_TTL_SECONDS}`,
81
+ });
82
+ }
83
+ if (typeof rule.isEnabled !== 'undefined' && typeof rule.isEnabled !== 'boolean') {
84
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.isEnabled' });
85
+ }
86
+ if (typeof rule.storeTraffic !== 'undefined' && typeof rule.storeTraffic !== 'boolean') {
87
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.storeTraffic' });
88
+ }
89
+ };
90
+ exports.validateSerializedRuleProperties = validateSerializedRuleProperties;
91
+ const validateDeserializedRule = (deserializedRule) => {
92
+ // TODO validate other functions ?
93
+ let matchValidationResult;
94
+ try {
95
+ matchValidationResult = deserializedRule.matches({
96
+ id: 'validation',
97
+ method: 'GET',
98
+ rawHeaders: new shared_1.RawHeaders(),
99
+ timestamp: Date.now(),
100
+ url: 'http://dummy.invalid/',
101
+ });
102
+ }
103
+ catch (error) {
104
+ shared_1.logger.error({ ruleId: deserializedRule.id }, error);
105
+ throw new shared_1.AppError({
106
+ httpCode: shared_1.HttpCode.UNPROCESSABLE_ENTITY,
107
+ message: 'match function returned invalid value',
108
+ });
109
+ }
110
+ if (matchValidationResult !== true &&
111
+ matchValidationResult !== false &&
112
+ matchValidationResult.result !== true &&
113
+ matchValidationResult.result !== false) {
114
+ shared_1.logger.error({ ruleId: deserializedRule.id, matchValidationResult }, 'match function retruned invalid value');
115
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.UNPROCESSABLE_ENTITY, message: 'match function returned invalid value' });
116
+ }
117
+ };
118
+ exports.validateDeserializedRule = validateDeserializedRule;
@@ -0,0 +1,145 @@
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
+ button#newRule(type='button', onclick='window.newRule()') New rule
10
+ button#saveRule(type='button', onclick='window.saveRule()', disabled) Save rule
11
+ div(style='width: 100%; overflow: hidden')
12
+ div(style='width: 230px; float: left')
13
+ h3 Rules
14
+ ul#ruleKeys.no-bullets
15
+ each ruleId in ruleKeys
16
+ li
17
+ button.rule(
18
+ type='button',
19
+ onclick='window.setRuleModel(this.getAttribute("data-rule-id"))',
20
+ data-rule-id=ruleId
21
+ )= ruleId
22
+ div(style='margin-left: 240px')
23
+ #container(style='height: 400px')
24
+ script.
25
+ const uuidv4 = () => {
26
+ function getRandomSymbol(symbol) {
27
+ var array;
28
+
29
+ if (symbol === 'y') {
30
+ array = ['8', '9', 'a', 'b'];
31
+ return array[Math.floor(Math.random() * array.length)];
32
+ }
33
+
34
+ array = new Uint8Array(1);
35
+ window.crypto.getRandomValues(array);
36
+ return (array[0] % 16).toString(16);
37
+ }
38
+
39
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, getRandomSymbol);
40
+ };
41
+ require.config({
42
+ paths: {
43
+ vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.35.0/min/vs',
44
+ },
45
+ });
46
+ require(['vs/editor/editor.main'], function () {
47
+ monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true);
48
+ monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
49
+ noSemanticValidation: false,
50
+ noSyntaxValidation: false,
51
+ });
52
+
53
+ // compiler options
54
+ monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
55
+ target: monaco.languages.typescript.ScriptTarget.ES6,
56
+ allowNonTsExtensions: true,
57
+ noLib: true,
58
+ moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
59
+ module: monaco.languages.typescript.ModuleKind.CommonJS,
60
+ noEmit: true,
61
+ checkJs: true,
62
+ allowJs: true,
63
+ isolatedModules: true,
64
+ typeRoots: ['node_modules/@types'],
65
+ });
66
+
67
+ monaco.languages.typescript.typescriptDefaults.addExtraLib(`!{INDEX_DTS}`, 'file:///node_modules/@types/stuntman/index.d.ts');
68
+ const models = {};
69
+ const rules = eval('(!{rules})');
70
+ for (const ruleId of Object.keys(rules)) {
71
+ models[ruleId] = monaco.editor.createModel("import type * as Stuntman from 'stuntman';\n\nvar STUNTMAN_RULE: Stuntman.Rule = " + rules[ruleId] + ';', 'typescript', `file:///${ruleId}.ts`);
72
+ }
73
+ const editor = monaco.editor.create(document.getElementById('container'), {
74
+ theme: 'vs-dark',
75
+ autoIndent: true,
76
+ formatOnPaste: true,
77
+ formatOnType: true,
78
+ automaticLayout: true,
79
+ autoIndent: true,
80
+ tabSize: 2,
81
+ });
82
+ editor.onDidChangeModel((event) => {
83
+ const isInternal = /^file:\/\/\/internal\/.+/.test(event.newModelUrl);
84
+ if (isInternal) {
85
+ document.getElementById('saveRule').setAttribute('disabled', 'true');
86
+ } else {
87
+ document.getElementById('saveRule').removeAttribute('disabled');
88
+ }
89
+
90
+ setTimeout(() => {
91
+ editor.getAction('editor.action.formatDocument').run();
92
+ }, 100);
93
+ });
94
+
95
+ window.setRuleModel = (ruleId) => {
96
+ if (history.pushState) {
97
+ const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?ruleId=${encodeURIComponent(ruleId)}`;
98
+ window.history.pushState({ path: newUrl }, '', newUrl);
99
+ }
100
+ editor.setModel(models[ruleId]);
101
+ };
102
+
103
+ const urlSearchParams = new URLSearchParams(window.location.search);
104
+ if (urlSearchParams.has('ruleId') && urlSearchParams.get('ruleId') in models) {
105
+ editor.setModel(models[urlSearchParams.get('ruleId')]);
106
+ }
107
+
108
+ window.saveRule = () => {
109
+ document.getElementById('saveRule').setAttribute('disabled', 'true');
110
+ const modelUri = editor.getModel().uri;
111
+ const result = monaco.languages.typescript.getTypeScriptWorker();
112
+ result.then((worker) => {
113
+ worker(modelUri).then(function (client) {
114
+ client.getEmitOutput(modelUri.toString()).then((output) => {
115
+ const ruleFunctionText = output.outputFiles[0].text.replace(/^export .+$/im, '');
116
+ const newId = new Function(ruleFunctionText + '\n return STUNTMAN_RULE;')().id;
117
+ fetch('/webgui/rules/unsafesave', {
118
+ method: 'POST',
119
+ headers: { 'content-type': 'text/plain' },
120
+ body: ruleFunctionText + '\n return STUNTMAN_RULE;',
121
+ }).then((response) => {
122
+ if (response.ok) {
123
+ window.location.reload();
124
+ return;
125
+ }
126
+ alert('Error when saving rule');
127
+ document.getElementById('saveRule').removeAttribute('disabled');
128
+ });
129
+ });
130
+ });
131
+ });
132
+ };
133
+
134
+ window.newRule = () => {
135
+ const ruleId = uuidv4();
136
+ const emptyRule = `import type * as Stuntman from \'stuntman\';\n\nvar STUNTMAN_RULE: Stuntman.Rule = { id: '${ruleId}', matches: (req: Stuntman.Request) => true, ttlSeconds: 600 };`;
137
+ models[ruleId] = monaco.editor.createModel(emptyRule, 'typescript', `file:///${ruleId}.ts`);
138
+ const ruleKeyNode = document.getElementById('ruleKeys').firstChild;
139
+ const ruleKeyNodeClone = ruleKeyNode.cloneNode(true);
140
+ ruleKeyNodeClone.getElementsByTagName('button')[0].setAttribute('data-rule-id', ruleId);
141
+ ruleKeyNodeClone.getElementsByTagName('button')[0].innerText = ruleId;
142
+ document.getElementById('ruleKeys').appendChild(ruleKeyNodeClone);
143
+ window.setRuleModel(ruleId);
144
+ };
145
+ });
@@ -0,0 +1,28 @@
1
+ div#container {
2
+ resize: vertical;
3
+ overflow: auto;
4
+ }
5
+
6
+ body {
7
+ font-family: Menlo, Monaco, 'Courier New', monospace;
8
+ }
9
+
10
+ button.rule {
11
+ font-family: Menlo, Monaco, 'Courier New', monospace;
12
+ background: none !important;
13
+ border: none;
14
+ padding: 0 !important;
15
+ color: #aaa;
16
+ text-decoration: underline;
17
+ cursor: pointer;
18
+ margin-top: 10px;
19
+ font-size: x-small;
20
+ text-align: left;
21
+ }
22
+
23
+ ul.no-bullets {
24
+ list-style-type: none; /* Remove bullets */
25
+ padding: 0; /* Remove padding */
26
+ margin: 0; /* Remove margins */
27
+ text-align: left;
28
+ }