diff --git a/Server/db/models/Monitor.js b/Server/db/models/Monitor.js index 90cbb1d62..b67666588 100644 --- a/Server/db/models/Monitor.js +++ b/Server/db/models/Monitor.js @@ -38,6 +38,9 @@ const MonitorSchema = mongoose.Schema( "distributed_http", ], }, + testScripts: { + type: String, + }, url: { type: String, required: true, diff --git a/Server/package-lock.json b/Server/package-lock.json index 67399c8a1..29840977d 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -19,6 +19,7 @@ "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", + "isolated-vm": "^5.0.3", "joi": "^17.13.1", "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", @@ -2642,7 +2643,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -2658,7 +2658,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2677,6 +2676,14 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3332,6 +3339,14 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -3822,6 +3837,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4205,6 +4225,11 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/ioredis": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.2.tgz", @@ -4347,6 +4372,18 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isolated-vm": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/isolated-vm/-/isolated-vm-5.0.3.tgz", + "integrity": "sha512-GNqX0j7dkwdaNQfFogLLb/tSuPZbXtKlk5ldaJ084ngjaW9/bn34x9FQFL856p20KSZoubIIummmiJf+2hzhCw==", + "hasInstallScript": true, + "dependencies": { + "prebuild-install": "^7.1.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/isomorphic-unfetch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", @@ -5693,6 +5730,11 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5772,6 +5814,17 @@ "node": ">=16" } }, + "node_modules/node-abi": { + "version": "3.73.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.73.0.tgz", + "integrity": "sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -6858,6 +6911,31 @@ "node": ">=12" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7025,6 +7103,28 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7457,6 +7557,49 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -7973,6 +8116,17 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", diff --git a/Server/package.json b/Server/package.json index 873f8ce67..b9678ad7d 100644 --- a/Server/package.json +++ b/Server/package.json @@ -26,6 +26,7 @@ "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", + "isolated-vm": "^5.0.3", "joi": "^17.13.1", "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 54edf39f9..4bce00f5c 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -1,4 +1,5 @@ import { errorMessages, successMessages } from "../utils/messages.js"; +import runInSandbox from "../utils/sandbox.js"; const SERVICE_NAME = "NetworkService"; /** @@ -135,6 +136,7 @@ class NetworkService { type: job.data.type, responseTime, payload: response?.data, + code: response.status, }; if (error) { @@ -144,8 +146,14 @@ class NetworkService { httpResponse.message = this.http.STATUS_CODES[code] || "Network Error"; return httpResponse; } - httpResponse.status = true; - httpResponse.code = response.status; + + const testScripts = job.data.testScripts; + if (testScripts) { + const status = await runInSandbox(testScripts, response?.data); + httpResponse.status = !!status; + } else { + httpResponse.status = true; + } httpResponse.message = this.http.STATUS_CODES[response.status]; return httpResponse; } catch (error) { diff --git a/Server/utils/sandbox.js b/Server/utils/sandbox.js new file mode 100644 index 000000000..6ea0e849c --- /dev/null +++ b/Server/utils/sandbox.js @@ -0,0 +1,55 @@ +import ivm from 'isolated-vm'; + +export class TimeoutError extends Error { + constructor(message) { super(message); } +} + +export class UserScriptError extends Error { + constructor(message, row, col) { + super(message); + this.row = row; + this.col = col; + } +} + +export default async function(code, param) { + // Create a new isolate limited to 128MB + const isolate = new ivm.Isolate({ memoryLimit: 128 }); + + // Create a new context within this isolate. Each context has its own copy of all the builtin Objects. + // So for instance if one context does Object.prototype.foo = 1 this would not affect any other contexts. + const context = isolate.createContextSync(); + + // Complete code to be executed by `evalClosure`. + // Add code to get the argument as the `res`, so that `res` can be used in the user code. + // Example: + // return res.code === 200; + // No spaces or line breaks, preserve the error location. + const script = + `const res = $0; +try { +${code} +} catch (e) { +const lines = e.stack.split("\\n"); +const [, row, col] = lines[lines.length - 1].match(/\\sat\\s\\\\:(\\d)\\:(\\d)/); +throw new Error("UserScriptError:" + row + ":" + col + ":" + e.message); +} +`; + + // Compiles and runs code as if it were inside a function, similar to the seldom-used new Function(code) constructor. + // The function will return a Promise while the work runs in a separate thread pool. + // After the timeout, the thread will be automatically terminated, and we do not need to handle it. + return context.evalClosure(script, [param], { timeout: 1000, arguments: { copy: true } }).catch(e => { + if (e.message.startsWith('UserScriptError')) { + // User script error, display error location in user script. + const [, row, col, ...messages] = e.message.split(':'); + throw new UserScriptError("UserScriptError: " + messages.join(':'), row - 2, col); + } else if (e.message === 'Script execution timed out.') { + // Timeout + throw new TimeoutError(e.message); + } else { + // Other runtime error + throw e; + } + }); +} \ No newline at end of file diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 8f366a113..900914333 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -194,6 +194,7 @@ const createMonitorBodyValidation = joi.object({ }), notifications: joi.array().items(joi.object()), secret: joi.string(), + testScripts: joi.string(), }); const editMonitorBodyValidation = joi.object({ @@ -202,6 +203,7 @@ const editMonitorBodyValidation = joi.object({ interval: joi.number(), notifications: joi.array().items(joi.object()), secret: joi.string(), + testScripts: joi.string(), }); const pauseMonitorParamValidation = joi.object({