mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: refactor SyntheticMonitor to use child processes for script execution
- Added isolated-vm dependency for secure script execution. - Replaced direct Playwright usage in SyntheticMonitor with a worker process. - Created SyntheticMonitorWorker to handle script execution in a sandboxed environment. - Implemented proxy configuration handling for worker processes. - Enhanced error handling and logging for script execution results. - Removed unnecessary browser session management from SyntheticMonitor.
This commit is contained in:
@@ -161,6 +161,14 @@ export const ClusterKey: ObjectID = new ObjectID(
|
||||
|
||||
export const HasClusterKey: boolean = Boolean(process.env["ONEUPTIME_SECRET"]);
|
||||
|
||||
export const RegisterProbeKey: ObjectID = new ObjectID(
|
||||
process.env["REGISTER_PROBE_KEY"] || "secret",
|
||||
);
|
||||
|
||||
export const HasRegisterProbeKey: boolean = Boolean(
|
||||
process.env["REGISTER_PROBE_KEY"],
|
||||
);
|
||||
|
||||
export const AppApiHostname: Hostname = Hostname.fromString(
|
||||
`${process.env["SERVER_APP_HOSTNAME"] || "localhost"}:${
|
||||
process.env["APP_PORT"] || 80
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
import GenericObject from "../../../Types/GenericObject";
|
||||
import ReturnResult from "../../../Types/IsolatedVM/ReturnResult";
|
||||
import { JSONObject, JSONValue } from "../../../Types/JSON";
|
||||
import axios from "axios";
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
import { JSONObject } from "../../../Types/JSON";
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import crypto from "crypto";
|
||||
import vm, { Context } from "node:vm";
|
||||
import ivm from "isolated-vm";
|
||||
import CaptureSpan from "../Telemetry/CaptureSpan";
|
||||
|
||||
export default class VMRunner {
|
||||
@@ -16,49 +12,231 @@ export default class VMRunner {
|
||||
options: {
|
||||
timeout?: number;
|
||||
args?: JSONObject | undefined;
|
||||
context?: Dictionary<GenericObject | string> | undefined;
|
||||
};
|
||||
}): Promise<ReturnResult> {
|
||||
const { code, options } = data;
|
||||
const timeout: number = options.timeout || 5000;
|
||||
|
||||
const logMessages: string[] = [];
|
||||
|
||||
let sandbox: Context = {
|
||||
console: {
|
||||
log: (...args: JSONValue[]) => {
|
||||
const isolate: ivm.Isolate = new ivm.Isolate({ memoryLimit: 128 });
|
||||
|
||||
try {
|
||||
const context: ivm.Context = await isolate.createContext();
|
||||
const jail: ivm.Reference<Record<string, unknown>> = context.global;
|
||||
|
||||
// Set up global object
|
||||
await jail.set("global", jail.derefInto());
|
||||
|
||||
// console.log - fire-and-forget callback
|
||||
await jail.set(
|
||||
"_log",
|
||||
new ivm.Callback((...args: string[]) => {
|
||||
logMessages.push(args.join(" "));
|
||||
}),
|
||||
);
|
||||
|
||||
await context.eval(`
|
||||
const console = { log: (...a) => _log(...a.map(v => {
|
||||
try { return typeof v === 'object' ? JSON.stringify(v) : String(v); }
|
||||
catch(_) { return String(v); }
|
||||
}))};
|
||||
`);
|
||||
|
||||
// args - deep copy into isolate
|
||||
if (options.args) {
|
||||
await jail.set(
|
||||
"_args",
|
||||
new ivm.ExternalCopy(options.args).copyInto(),
|
||||
);
|
||||
await context.eval("const args = _args;");
|
||||
} else {
|
||||
await context.eval("const args = {};");
|
||||
}
|
||||
|
||||
// axios (get, post, put, delete) - bridged via applySyncPromise
|
||||
const axiosRef: ivm.Reference<
|
||||
(
|
||||
method: string,
|
||||
url: string,
|
||||
dataOrConfig?: string,
|
||||
) => Promise<string>
|
||||
> = new ivm.Reference(
|
||||
async (
|
||||
method: string,
|
||||
url: string,
|
||||
dataOrConfig?: string,
|
||||
): Promise<string> => {
|
||||
const parsed: JSONObject | undefined = dataOrConfig
|
||||
? (JSON.parse(dataOrConfig) as JSONObject)
|
||||
: undefined;
|
||||
|
||||
let response: AxiosResponse;
|
||||
|
||||
switch (method) {
|
||||
case "get":
|
||||
response = await axios.get(url, parsed);
|
||||
break;
|
||||
case "post":
|
||||
response = await axios.post(url, parsed);
|
||||
break;
|
||||
case "put":
|
||||
response = await axios.put(url, parsed);
|
||||
break;
|
||||
case "delete":
|
||||
response = await axios.delete(url, parsed);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported HTTP method: ${method}`);
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
});
|
||||
},
|
||||
},
|
||||
http: http,
|
||||
https: https,
|
||||
axios: axios,
|
||||
crypto: crypto,
|
||||
setTimeout: setTimeout,
|
||||
clearTimeout: clearTimeout,
|
||||
setInterval: setInterval,
|
||||
...options.context,
|
||||
};
|
||||
);
|
||||
|
||||
if (options.args) {
|
||||
sandbox = {
|
||||
...sandbox,
|
||||
args: options.args,
|
||||
};
|
||||
}
|
||||
await jail.set("_axiosRef", axiosRef);
|
||||
|
||||
vm.createContext(sandbox); // Contextify the object.
|
||||
await context.eval(`
|
||||
const axios = {
|
||||
get: async (url, config) => {
|
||||
const r = await _axiosRef.applySyncPromise(undefined, ['get', url, config ? JSON.stringify(config) : undefined]);
|
||||
return JSON.parse(r);
|
||||
},
|
||||
post: async (url, data) => {
|
||||
const r = await _axiosRef.applySyncPromise(undefined, ['post', url, data ? JSON.stringify(data) : undefined]);
|
||||
return JSON.parse(r);
|
||||
},
|
||||
put: async (url, data) => {
|
||||
const r = await _axiosRef.applySyncPromise(undefined, ['put', url, data ? JSON.stringify(data) : undefined]);
|
||||
return JSON.parse(r);
|
||||
},
|
||||
delete: async (url, config) => {
|
||||
const r = await _axiosRef.applySyncPromise(undefined, ['delete', url, config ? JSON.stringify(config) : undefined]);
|
||||
return JSON.parse(r);
|
||||
},
|
||||
};
|
||||
`);
|
||||
|
||||
const script: string = `(async()=>{
|
||||
// crypto (createHash, createHmac, randomBytes) - bridged via applySync
|
||||
const cryptoRef: ivm.Reference<
|
||||
(op: string, ...args: string[]) => string
|
||||
> = new ivm.Reference((op: string, ...args: string[]): string => {
|
||||
switch (op) {
|
||||
case "createHash": {
|
||||
const [algorithm, inputData, encoding] = args;
|
||||
return crypto
|
||||
.createHash(algorithm!)
|
||||
.update(inputData!)
|
||||
.digest((encoding as crypto.BinaryToTextEncoding) || "hex");
|
||||
}
|
||||
case "createHmac": {
|
||||
const [algorithm, key, inputData, encoding] = args;
|
||||
return crypto
|
||||
.createHmac(algorithm!, key!)
|
||||
.update(inputData!)
|
||||
.digest((encoding as crypto.BinaryToTextEncoding) || "hex");
|
||||
}
|
||||
case "randomBytes": {
|
||||
const [size] = args;
|
||||
return crypto.randomBytes(parseInt(size!)).toString("hex");
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported crypto operation: ${op}`);
|
||||
}
|
||||
});
|
||||
|
||||
await jail.set("_cryptoRef", cryptoRef);
|
||||
|
||||
await context.eval(`
|
||||
const crypto = {
|
||||
createHash: (algorithm) => ({
|
||||
_alg: algorithm, _data: '',
|
||||
update(d) { this._data = d; return this; },
|
||||
digest(enc) { return _cryptoRef.applySync(undefined, ['createHash', this._alg, this._data, enc || 'hex']); }
|
||||
}),
|
||||
createHmac: (algorithm, key) => ({
|
||||
_alg: algorithm, _key: key, _data: '',
|
||||
update(d) { this._data = d; return this; },
|
||||
digest(enc) { return _cryptoRef.applySync(undefined, ['createHmac', this._alg, this._key, this._data, enc || 'hex']); }
|
||||
}),
|
||||
randomBytes: (size) => ({
|
||||
toString(enc) { return _cryptoRef.applySync(undefined, ['randomBytes', String(size)]); }
|
||||
}),
|
||||
};
|
||||
`);
|
||||
|
||||
// setTimeout / sleep - bridged via applySyncPromise
|
||||
const sleepRef: ivm.Reference<(ms: number) => Promise<void>> =
|
||||
new ivm.Reference((ms: number): Promise<void> => {
|
||||
return new Promise((resolve: () => void) => {
|
||||
global.setTimeout(resolve, Math.min(ms, timeout));
|
||||
});
|
||||
});
|
||||
|
||||
await jail.set("_sleepRef", sleepRef);
|
||||
|
||||
await context.eval(`
|
||||
function setTimeout(fn, ms) {
|
||||
_sleepRef.applySyncPromise(undefined, [ms || 0]);
|
||||
if (typeof fn === 'function') fn();
|
||||
}
|
||||
async function sleep(ms) {
|
||||
await _sleepRef.applySyncPromise(undefined, [ms || 0]);
|
||||
}
|
||||
`);
|
||||
|
||||
// Wrap user code in async IIFE and evaluate
|
||||
const wrappedCode: string = `(async () => {
|
||||
${code}
|
||||
})()`;
|
||||
|
||||
const returnVal: any = await vm.runInContext(script, sandbox, {
|
||||
timeout: options.timeout || 5000,
|
||||
}); // run the script
|
||||
// Run with overall timeout covering both CPU and I/O wait
|
||||
const resultPromise: Promise<unknown> = context.eval(wrappedCode, {
|
||||
promise: true,
|
||||
timeout: timeout,
|
||||
});
|
||||
|
||||
return {
|
||||
returnValue: returnVal,
|
||||
logMessages,
|
||||
};
|
||||
const overallTimeout: Promise<never> = new Promise(
|
||||
(
|
||||
_resolve: (value: never) => void,
|
||||
reject: (reason: Error) => void,
|
||||
) => {
|
||||
global.setTimeout(() => {
|
||||
reject(new Error("Script execution timed out"));
|
||||
}, timeout + 5000); // 5s grace period beyond isolate timeout
|
||||
},
|
||||
);
|
||||
|
||||
let returnValue: unknown;
|
||||
|
||||
const result: unknown = await Promise.race([
|
||||
resultPromise,
|
||||
overallTimeout,
|
||||
]);
|
||||
|
||||
// If the result is an ivm.Reference or ExternalCopy, extract the value
|
||||
if (result && typeof result === "object" && "copy" in result) {
|
||||
try {
|
||||
returnValue = (result as ivm.Reference<unknown>).copy();
|
||||
} catch {
|
||||
returnValue = undefined;
|
||||
}
|
||||
} else {
|
||||
returnValue = result;
|
||||
}
|
||||
|
||||
return {
|
||||
returnValue,
|
||||
logMessages,
|
||||
};
|
||||
} finally {
|
||||
if (!isolate.isDisposed) {
|
||||
isolate.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
327
Common/package-lock.json
generated
327
Common/package-lock.json
generated
@@ -62,6 +62,7 @@
|
||||
"formik": "^2.4.6",
|
||||
"history": "^5.3.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"isolated-vm": "^6.0.2",
|
||||
"json2csv": "^5.0.7",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
@@ -6414,6 +6415,55 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
@@ -7102,6 +7152,12 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
@@ -8174,6 +8230,21 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||
@@ -8214,6 +8285,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
|
||||
@@ -8373,7 +8453,6 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -8628,6 +8707,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
|
||||
@@ -9005,6 +9093,15 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect": {
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz",
|
||||
@@ -9466,6 +9563,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
|
||||
@@ -9609,6 +9712,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@@ -10115,6 +10224,12 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inline-style-parser": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
||||
@@ -10650,6 +10765,19 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isolated-vm": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/isolated-vm/-/isolated-vm-6.0.2.tgz",
|
||||
"integrity": "sha512-Qw6AJuagG/VJuh2AIcSWmQPsAArti/L+lKhjXU+lyhYkbt3J57XZr+ZjgfTnOr4NJcY1r3f8f0eePS7MRGp+pg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"prebuild-install": "^7.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
@@ -13760,6 +13888,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
@@ -13818,6 +13958,12 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
|
||||
@@ -13922,6 +14068,12 @@
|
||||
"node": ">= 10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -13948,6 +14100,18 @@
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.87.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
|
||||
"integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
|
||||
"license": "MIT",
|
||||
"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",
|
||||
@@ -14175,7 +14339,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
@@ -14762,6 +14925,32 @@
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"license": "MIT",
|
||||
"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/pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
@@ -14946,6 +15135,16 @@
|
||||
"punycode": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -15166,6 +15365,30 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"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.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
@@ -16555,6 +16778,51 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/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"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/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"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@@ -17522,6 +17790,48 @@
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs/node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||
@@ -17821,6 +18131,18 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/twilio": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/twilio/-/twilio-4.23.0.tgz",
|
||||
@@ -18891,7 +19213,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"formik": "^2.4.6",
|
||||
"history": "^5.3.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"isolated-vm": "^6.0.2",
|
||||
"json2csv": "^5.0.7",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
|
||||
@@ -198,6 +198,18 @@ Usage:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "oneuptime.env.registerProbeKey" }}
|
||||
- name: REGISTER_PROBE_KEY
|
||||
{{- if $.Values.registerProbeKey }}
|
||||
value: {{ $.Values.registerProbeKey }}
|
||||
{{- else }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ printf "%s-%s" $.Release.Name "secrets" }}
|
||||
key: register-probe-key
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "oneuptime.env.runtime" }}
|
||||
|
||||
- name: VAPID_PRIVATE_KEY
|
||||
|
||||
@@ -112,6 +112,7 @@ spec:
|
||||
value: {{ $.Values.probeIngest.disableTelemetryCollection | quote }}
|
||||
- name: PROBE_INGEST_CONCURRENCY
|
||||
value: {{ $.Values.probeIngest.concurrency | squote }}
|
||||
{{- include "oneuptime.env.registerProbeKey" (dict "Values" $.Values "Release" $.Release) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: {{ $.Values.probeIngest.ports.http }}
|
||||
protocol: TCP
|
||||
|
||||
@@ -131,7 +131,7 @@ spec:
|
||||
- name: NO_PROXY
|
||||
value: {{ $val.proxy.noProxy | squote }}
|
||||
{{- end }}
|
||||
{{- include "oneuptime.env.oneuptimeSecret" (dict "Values" $.Values "Release" $.Release) | nindent 12 }}
|
||||
{{- include "oneuptime.env.registerProbeKey" (dict "Values" $.Values "Release" $.Release) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: {{ if and $val.ports $val.ports.http }}{{ $val.ports.http }}{{ else }}3874{{ end }}
|
||||
protocol: TCP
|
||||
|
||||
@@ -17,6 +17,13 @@ stringData:
|
||||
{{- else }}
|
||||
oneuptime-secret: {{ index (lookup "v1" "Secret" $.Release.Namespace (printf "%s-secrets" $.Release.Name)).data "oneuptime-secret" | b64dec }}
|
||||
{{- end }}
|
||||
{{- if .Values.registerProbeKey }}
|
||||
register-probe-key: {{ .Values.registerProbeKey | quote }}
|
||||
{{- else if (index (lookup "v1" "Secret" $.Release.Namespace (printf "%s-secrets" $.Release.Name)).data "register-probe-key") }}
|
||||
register-probe-key: {{ index (lookup "v1" "Secret" $.Release.Namespace (printf "%s-secrets" $.Release.Name)).data "register-probe-key" | b64dec }}
|
||||
{{- else }}
|
||||
register-probe-key: {{ randAlphaNum 32 | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.encryptionSecret }}
|
||||
encryption-secret: {{ .Values.encryptionSecret | quote }}
|
||||
{{- else }}
|
||||
@@ -48,6 +55,11 @@ stringData:
|
||||
{{- else }}
|
||||
oneuptime-secret: {{ randAlphaNum 32 | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.registerProbeKey }}
|
||||
register-probe-key: {{ .Values.registerProbeKey | quote }}
|
||||
{{- else }}
|
||||
register-probe-key: {{ randAlphaNum 32 | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.encryptionSecret }}
|
||||
encryption-secret: {{ .Values.encryptionSecret | quote }}
|
||||
{{- else }}
|
||||
|
||||
@@ -32,6 +32,7 @@ image:
|
||||
# Important: You do need to set this to a long random values if you're using OneUptime in production.
|
||||
# Please set this to string.
|
||||
oneuptimeSecret:
|
||||
registerProbeKey:
|
||||
encryptionSecret:
|
||||
|
||||
# External Secrets
|
||||
|
||||
@@ -15,10 +15,12 @@ import { JSONObject } from "Common/Types/JSON";
|
||||
import ProbeStatusReport from "Common/Types/Probe/ProbeStatusReport";
|
||||
import Sleep from "Common/Types/Sleep";
|
||||
import API from "Common/Utils/API";
|
||||
import { HasClusterKey } from "Common/Server/EnvironmentConfig";
|
||||
import {
|
||||
HasRegisterProbeKey,
|
||||
RegisterProbeKey,
|
||||
} from "Common/Server/EnvironmentConfig";
|
||||
import LocalCache from "Common/Server/Infrastructure/LocalCache";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
|
||||
import ProxyConfig from "../Utils/ProxyConfig";
|
||||
|
||||
export default class Register {
|
||||
@@ -117,7 +119,7 @@ export default class Register {
|
||||
}
|
||||
|
||||
private static async _registerProbe(): Promise<void> {
|
||||
if (HasClusterKey) {
|
||||
if (HasRegisterProbeKey) {
|
||||
const probeRegistrationUrl: URL = URL.fromString(
|
||||
PROBE_INGEST_URL.toString(),
|
||||
).addRoute("/register");
|
||||
@@ -131,7 +133,7 @@ export default class Register {
|
||||
probeKey: PROBE_KEY,
|
||||
probeName: PROBE_NAME,
|
||||
probeDescription: PROBE_DESCRIPTION,
|
||||
clusterKey: ClusterKeyAuthorization.getClusterKey(),
|
||||
registerProbeKey: RegisterProbeKey.toString(),
|
||||
},
|
||||
options: {
|
||||
...ProxyConfig.getRequestProxyAgents(probeRegistrationUrl),
|
||||
@@ -149,7 +151,7 @@ export default class Register {
|
||||
} else {
|
||||
// validate probe.
|
||||
if (!PROBE_ID) {
|
||||
logger.error("PROBE_ID or ONEUPTIME_SECRET should be set");
|
||||
logger.error("PROBE_ID or REGISTER_PROBE_KEY should be set");
|
||||
return process.exit();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS } from "../../../Config";
|
||||
import ProxyConfig from "../../ProxyConfig";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import ReturnResult from "Common/Types/IsolatedVM/ReturnResult";
|
||||
import BrowserType from "Common/Types/Monitor/SyntheticMonitors/BrowserType";
|
||||
import ScreenSizeType from "Common/Types/Monitor/SyntheticMonitors/ScreenSizeType";
|
||||
import SyntheticMonitorResponse from "Common/Types/Monitor/SyntheticMonitors/SyntheticMonitorResponse";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import VMRunner from "Common/Server/Utils/VM/VMRunner";
|
||||
import { Browser, BrowserContext, Page, chromium, firefox } from "playwright";
|
||||
import LocalFile from "Common/Server/Utils/LocalFile";
|
||||
import os from "os";
|
||||
import { ChildProcess, fork } from "child_process";
|
||||
import path from "path";
|
||||
|
||||
export interface SyntheticMonitorOptions {
|
||||
monitorId?: ObjectID | undefined;
|
||||
@@ -20,24 +16,24 @@ export interface SyntheticMonitorOptions {
|
||||
retryCountOnError?: number | undefined;
|
||||
}
|
||||
|
||||
interface BrowserLaunchOptions {
|
||||
executablePath?: string;
|
||||
interface WorkerConfig {
|
||||
script: string;
|
||||
browserType: BrowserType;
|
||||
screenSizeType: ScreenSizeType;
|
||||
timeout: number;
|
||||
proxy?: {
|
||||
server: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
bypass?: string;
|
||||
};
|
||||
args?: string[];
|
||||
headless?: boolean;
|
||||
devtools?: boolean;
|
||||
timeout?: number;
|
||||
username?: string | undefined;
|
||||
password?: string | undefined;
|
||||
} | undefined;
|
||||
}
|
||||
|
||||
interface BrowserSession {
|
||||
browser: Browser;
|
||||
context: BrowserContext;
|
||||
page: Page;
|
||||
interface WorkerResult {
|
||||
logMessages: string[];
|
||||
scriptError?: string | undefined;
|
||||
result?: unknown | undefined;
|
||||
screenshots: Record<string, string>;
|
||||
executionTimeInMS: number;
|
||||
}
|
||||
|
||||
export default class SyntheticMonitor {
|
||||
@@ -111,13 +107,70 @@ export default class SyntheticMonitor {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static getSanitizedEnv(): Record<string, string> {
|
||||
// Only pass safe environment variables to the worker process.
|
||||
// Explicitly exclude all secrets (DATABASE_PASSWORD, REDIS_PASSWORD,
|
||||
// CLICKHOUSE_PASSWORD, ONEUPTIME_SECRET, ENCRYPTION_SECRET, BILLING_PRIVATE_KEY, etc.)
|
||||
const safeKeys: string[] = [
|
||||
"PATH",
|
||||
"HOME",
|
||||
"NODE_ENV",
|
||||
"PLAYWRIGHT_BROWSERS_PATH",
|
||||
"HTTP_PROXY_URL",
|
||||
"http_proxy",
|
||||
"HTTPS_PROXY_URL",
|
||||
"https_proxy",
|
||||
"NO_PROXY",
|
||||
"no_proxy",
|
||||
];
|
||||
|
||||
const env: Record<string, string> = {};
|
||||
|
||||
for (const key of safeKeys) {
|
||||
if (process.env[key]) {
|
||||
env[key] = process.env[key]!;
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
private static getProxyConfig(): WorkerConfig["proxy"] | undefined {
|
||||
if (!ProxyConfig.isProxyConfigured()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const httpsProxyUrl: string | null = ProxyConfig.getHttpsProxyUrl();
|
||||
const httpProxyUrl: string | null = ProxyConfig.getHttpProxyUrl();
|
||||
const proxyUrl: string | null = httpsProxyUrl || httpProxyUrl;
|
||||
|
||||
if (!proxyUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const proxyConfig: WorkerConfig["proxy"] = {
|
||||
server: proxyUrl,
|
||||
};
|
||||
|
||||
try {
|
||||
const parsedUrl: globalThis.URL = new URL(proxyUrl);
|
||||
if (parsedUrl.username && parsedUrl.password) {
|
||||
proxyConfig.username = parsedUrl.username;
|
||||
proxyConfig.password = parsedUrl.password;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse proxy URL for authentication: ${error}`);
|
||||
}
|
||||
|
||||
return proxyConfig;
|
||||
}
|
||||
|
||||
private static async executeByBrowserAndScreenSize(options: {
|
||||
script: string;
|
||||
browserType: BrowserType;
|
||||
screenSizeType: ScreenSizeType;
|
||||
}): Promise<SyntheticMonitorResponse | null> {
|
||||
if (!options) {
|
||||
// this should never happen
|
||||
options = {
|
||||
script: "",
|
||||
browserType: BrowserType.Chromium,
|
||||
@@ -135,385 +188,116 @@ export default class SyntheticMonitor {
|
||||
screenSizeType: options.screenSizeType,
|
||||
};
|
||||
|
||||
let browserSession: BrowserSession | null = null;
|
||||
const timeout: number = PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS;
|
||||
|
||||
const workerConfig: WorkerConfig = {
|
||||
script: options.script,
|
||||
browserType: options.browserType,
|
||||
screenSizeType: options.screenSizeType,
|
||||
timeout: timeout,
|
||||
proxy: this.getProxyConfig(),
|
||||
};
|
||||
|
||||
try {
|
||||
let result: ReturnResult | null = null;
|
||||
|
||||
const startTime: [number, number] = process.hrtime();
|
||||
|
||||
browserSession = await SyntheticMonitor.getPageByBrowserType({
|
||||
browserType: options.browserType,
|
||||
screenSizeType: options.screenSizeType,
|
||||
});
|
||||
|
||||
if (!browserSession) {
|
||||
throw new BadDataException(
|
||||
"Could not create Playwright browser session",
|
||||
);
|
||||
}
|
||||
|
||||
result = await VMRunner.runCodeInSandbox({
|
||||
code: options.script,
|
||||
options: {
|
||||
timeout: PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS,
|
||||
args: {},
|
||||
context: {
|
||||
browser: browserSession.browser,
|
||||
page: browserSession.page,
|
||||
screenSizeType: options.screenSizeType,
|
||||
browserType: options.browserType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const endTime: [number, number] = process.hrtime(startTime);
|
||||
|
||||
const executionTimeInMS: number = Math.ceil(
|
||||
(endTime[0] * 1000000000 + endTime[1]) / 1000000,
|
||||
const workerResult: WorkerResult = await this.forkWorker(
|
||||
workerConfig,
|
||||
timeout,
|
||||
);
|
||||
|
||||
scriptResult.executionTimeInMS = executionTimeInMS;
|
||||
|
||||
scriptResult.logMessages = result.logMessages;
|
||||
|
||||
if (result.returnValue?.screenshots) {
|
||||
if (!scriptResult.screenshots) {
|
||||
scriptResult.screenshots = {};
|
||||
}
|
||||
|
||||
for (const screenshotName in result.returnValue.screenshots) {
|
||||
if (!result.returnValue.screenshots[screenshotName]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if this is of type Buffer. If it is not, continue.
|
||||
|
||||
if (
|
||||
!(result.returnValue.screenshots[screenshotName] instanceof Buffer)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const screenshotBuffer: Buffer = result.returnValue.screenshots[
|
||||
screenshotName
|
||||
] as Buffer;
|
||||
scriptResult.screenshots[screenshotName] =
|
||||
screenshotBuffer.toString("base64"); // convert screenshots to base 64
|
||||
}
|
||||
}
|
||||
|
||||
scriptResult.result = result?.returnValue?.data;
|
||||
scriptResult.logMessages = workerResult.logMessages;
|
||||
scriptResult.scriptError = workerResult.scriptError;
|
||||
scriptResult.result = workerResult.result as typeof scriptResult.result;
|
||||
scriptResult.screenshots = workerResult.screenshots;
|
||||
scriptResult.executionTimeInMS = workerResult.executionTimeInMS;
|
||||
} catch (err: unknown) {
|
||||
logger.error(err);
|
||||
scriptResult.scriptError =
|
||||
(err as Error)?.message || (err as Error).toString();
|
||||
} finally {
|
||||
// Always dispose browser session to prevent zombie processes
|
||||
await SyntheticMonitor.disposeBrowserSession(browserSession);
|
||||
}
|
||||
|
||||
return scriptResult;
|
||||
}
|
||||
|
||||
private static getViewportHeightAndWidth(options: {
|
||||
screenSizeType: ScreenSizeType;
|
||||
}): {
|
||||
height: number;
|
||||
width: number;
|
||||
} {
|
||||
let viewPortHeight: number = 0;
|
||||
let viewPortWidth: number = 0;
|
||||
|
||||
switch (options.screenSizeType) {
|
||||
case ScreenSizeType.Desktop:
|
||||
viewPortHeight = 1080;
|
||||
viewPortWidth = 1920;
|
||||
break;
|
||||
case ScreenSizeType.Mobile:
|
||||
viewPortHeight = 640;
|
||||
viewPortWidth = 360;
|
||||
break;
|
||||
case ScreenSizeType.Tablet:
|
||||
viewPortHeight = 768;
|
||||
viewPortWidth = 1024;
|
||||
break;
|
||||
default:
|
||||
viewPortHeight = 1080;
|
||||
viewPortWidth = 1920;
|
||||
break;
|
||||
}
|
||||
|
||||
return { height: viewPortHeight, width: viewPortWidth };
|
||||
}
|
||||
|
||||
private static getPlaywrightBrowsersPath(): string {
|
||||
return (
|
||||
process.env["PLAYWRIGHT_BROWSERS_PATH"] ||
|
||||
`${os.homedir()}/.cache/ms-playwright`
|
||||
);
|
||||
}
|
||||
|
||||
public static async getChromeExecutablePath(): Promise<string> {
|
||||
const browsersPath: string = this.getPlaywrightBrowsersPath();
|
||||
|
||||
const doesDirectoryExist: boolean =
|
||||
await LocalFile.doesDirectoryExist(browsersPath);
|
||||
if (!doesDirectoryExist) {
|
||||
throw new BadDataException("Chrome executable path not found.");
|
||||
}
|
||||
|
||||
// get list of files in the directory
|
||||
const directories: string[] =
|
||||
await LocalFile.getListOfDirectories(browsersPath);
|
||||
|
||||
if (directories.length === 0) {
|
||||
throw new BadDataException("Chrome executable path not found.");
|
||||
}
|
||||
|
||||
const chromeInstallationName: string | undefined = directories.find(
|
||||
(directory: string) => {
|
||||
return directory.includes("chromium");
|
||||
},
|
||||
);
|
||||
|
||||
if (!chromeInstallationName) {
|
||||
throw new BadDataException("Chrome executable path not found.");
|
||||
}
|
||||
|
||||
const chromeExecutableCandidates: Array<string> = [
|
||||
`${browsersPath}/${chromeInstallationName}/chrome-linux/chrome`,
|
||||
`${browsersPath}/${chromeInstallationName}/chrome-linux64/chrome`,
|
||||
`${browsersPath}/${chromeInstallationName}/chrome64/chrome`,
|
||||
`${browsersPath}/${chromeInstallationName}/chrome/chrome`,
|
||||
];
|
||||
|
||||
for (const executablePath of chromeExecutableCandidates) {
|
||||
if (await LocalFile.doesFileExist(executablePath)) {
|
||||
return executablePath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadDataException("Chrome executable path not found.");
|
||||
}
|
||||
|
||||
public static async getFirefoxExecutablePath(): Promise<string> {
|
||||
const browsersPath: string = this.getPlaywrightBrowsersPath();
|
||||
|
||||
const doesDirectoryExist: boolean =
|
||||
await LocalFile.doesDirectoryExist(browsersPath);
|
||||
if (!doesDirectoryExist) {
|
||||
throw new BadDataException("Firefox executable path not found.");
|
||||
}
|
||||
|
||||
// get list of files in the directory
|
||||
const directories: string[] =
|
||||
await LocalFile.getListOfDirectories(browsersPath);
|
||||
|
||||
if (directories.length === 0) {
|
||||
throw new BadDataException("Firefox executable path not found.");
|
||||
}
|
||||
|
||||
const firefoxInstallationName: string | undefined = directories.find(
|
||||
(directory: string) => {
|
||||
return directory.includes("firefox");
|
||||
},
|
||||
);
|
||||
|
||||
if (!firefoxInstallationName) {
|
||||
throw new BadDataException("Firefox executable path not found.");
|
||||
}
|
||||
|
||||
const firefoxExecutableCandidates: Array<string> = [
|
||||
`${browsersPath}/${firefoxInstallationName}/firefox/firefox`,
|
||||
`${browsersPath}/${firefoxInstallationName}/firefox-linux64/firefox`,
|
||||
`${browsersPath}/${firefoxInstallationName}/firefox64/firefox`,
|
||||
`${browsersPath}/${firefoxInstallationName}/firefox-64/firefox`,
|
||||
];
|
||||
|
||||
for (const executablePath of firefoxExecutableCandidates) {
|
||||
if (await LocalFile.doesFileExist(executablePath)) {
|
||||
return executablePath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadDataException("Firefox executable path not found.");
|
||||
}
|
||||
|
||||
private static async getPageByBrowserType(data: {
|
||||
browserType: BrowserType;
|
||||
screenSizeType: ScreenSizeType;
|
||||
}): Promise<BrowserSession> {
|
||||
const viewport: {
|
||||
height: number;
|
||||
width: number;
|
||||
} = SyntheticMonitor.getViewportHeightAndWidth({
|
||||
screenSizeType: data.screenSizeType,
|
||||
});
|
||||
|
||||
// Prepare browser launch options with proxy support
|
||||
const baseOptions: BrowserLaunchOptions = {};
|
||||
|
||||
// Configure proxy if available
|
||||
if (ProxyConfig.isProxyConfigured()) {
|
||||
const httpsProxyUrl: string | null = ProxyConfig.getHttpsProxyUrl();
|
||||
const httpProxyUrl: string | null = ProxyConfig.getHttpProxyUrl();
|
||||
|
||||
// Prefer HTTPS proxy, fall back to HTTP proxy
|
||||
const proxyUrl: string | null = httpsProxyUrl || httpProxyUrl;
|
||||
|
||||
if (proxyUrl) {
|
||||
baseOptions.proxy = {
|
||||
server: proxyUrl,
|
||||
};
|
||||
|
||||
// Extract username and password if present in proxy URL
|
||||
try {
|
||||
const parsedUrl: globalThis.URL = new URL(proxyUrl);
|
||||
if (parsedUrl.username && parsedUrl.password) {
|
||||
baseOptions.proxy.username = parsedUrl.username;
|
||||
baseOptions.proxy.password = parsedUrl.password;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse proxy URL for authentication: ${error}`);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Synthetic Monitor using proxy: ${proxyUrl} (HTTPS: ${Boolean(httpsProxyUrl)}, HTTP: ${Boolean(httpProxyUrl)})`,
|
||||
private static async forkWorker(
|
||||
config: WorkerConfig,
|
||||
timeout: number,
|
||||
): Promise<WorkerResult> {
|
||||
return new Promise(
|
||||
(
|
||||
resolve: (value: WorkerResult) => void,
|
||||
reject: (reason: Error) => void,
|
||||
) => {
|
||||
// The worker file path. At runtime the compiled JS will be at the same
|
||||
// relative location under the build output directory.
|
||||
const workerPath: string = path.resolve(
|
||||
__dirname,
|
||||
"SyntheticMonitorWorker",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.browserType === BrowserType.Chromium) {
|
||||
const browser: Browser = await chromium.launch({
|
||||
executablePath: await this.getChromeExecutablePath(),
|
||||
...baseOptions,
|
||||
});
|
||||
|
||||
const context: BrowserContext = await browser.newContext({
|
||||
viewport: {
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
},
|
||||
});
|
||||
|
||||
const page: Page = await context.newPage();
|
||||
|
||||
return {
|
||||
browser,
|
||||
context,
|
||||
page,
|
||||
};
|
||||
}
|
||||
|
||||
if (data.browserType === BrowserType.Firefox) {
|
||||
const browser: Browser = await firefox.launch({
|
||||
executablePath: await this.getFirefoxExecutablePath(),
|
||||
...baseOptions,
|
||||
});
|
||||
|
||||
let context: BrowserContext | null = null;
|
||||
|
||||
try {
|
||||
context = await browser.newContext({
|
||||
viewport: {
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
},
|
||||
const child: ChildProcess = fork(workerPath, [], {
|
||||
env: this.getSanitizedEnv(),
|
||||
timeout: timeout + 30000, // fork-level timeout: script timeout + 30s for browser startup/shutdown
|
||||
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||
});
|
||||
|
||||
const page: Page = await context.newPage();
|
||||
let resolved: boolean = false;
|
||||
|
||||
return {
|
||||
browser,
|
||||
context,
|
||||
page,
|
||||
};
|
||||
} catch (error) {
|
||||
await SyntheticMonitor.safeCloseBrowserContext(context);
|
||||
await SyntheticMonitor.safeCloseBrowser(browser);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Explicit kill timer as final safety net
|
||||
const killTimer: ReturnType<typeof setTimeout> = global.setTimeout(
|
||||
() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
child.kill("SIGKILL");
|
||||
reject(
|
||||
new Error(
|
||||
"Synthetic monitor worker killed after timeout",
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
timeout + 60000, // kill after script timeout + 60s
|
||||
);
|
||||
|
||||
throw new BadDataException("Invalid Browser Type.");
|
||||
}
|
||||
child.on("message", (result: WorkerResult) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
global.clearTimeout(killTimer);
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
|
||||
private static async disposeBrowserSession(
|
||||
session: BrowserSession | null,
|
||||
): Promise<void> {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
child.on("error", (err: Error) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
global.clearTimeout(killTimer);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
await SyntheticMonitor.safeClosePage(session.page);
|
||||
await SyntheticMonitor.safeCloseBrowserContexts({
|
||||
browser: session.browser,
|
||||
});
|
||||
await SyntheticMonitor.safeCloseBrowser(session.browser);
|
||||
}
|
||||
child.on("exit", (exitCode: number | null) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
global.clearTimeout(killTimer);
|
||||
if (exitCode !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`Synthetic monitor worker exited with code ${exitCode}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Worker exited cleanly but didn't send a message — shouldn't happen
|
||||
reject(
|
||||
new Error(
|
||||
"Synthetic monitor worker exited without sending results",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private static async safeClosePage(page?: Page | null): Promise<void> {
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!page.isClosed()) {
|
||||
await page.close();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to close Playwright page: ${(error as Error)?.message || error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static async safeCloseBrowserContext(
|
||||
context?: BrowserContext | null,
|
||||
): Promise<void> {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await context.close();
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to close Playwright browser context: ${(error as Error)?.message || error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static async safeCloseBrowser(
|
||||
browser?: Browser | null,
|
||||
): Promise<void> {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (browser.isConnected()) {
|
||||
await browser.close();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to close Playwright browser: ${(error as Error)?.message || error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static async safeCloseBrowserContexts(data: {
|
||||
browser: Browser;
|
||||
}): Promise<void> {
|
||||
if (!data.browser || !data.browser.contexts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contexts: Array<BrowserContext> = data.browser.contexts();
|
||||
|
||||
for (const context of contexts) {
|
||||
await SyntheticMonitor.safeCloseBrowserContext(context);
|
||||
}
|
||||
// Send config to worker via IPC
|
||||
child.send(config);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
339
Probe/Utils/Monitors/MonitorTypes/SyntheticMonitorWorker.ts
Normal file
339
Probe/Utils/Monitors/MonitorTypes/SyntheticMonitorWorker.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
// This script is executed via child_process.fork() with a sanitized environment
|
||||
// It launches Playwright, runs user code with node:vm (safe because env is stripped),
|
||||
// and sends results back via IPC.
|
||||
|
||||
import BrowserType from "Common/Types/Monitor/SyntheticMonitors/BrowserType";
|
||||
import ScreenSizeType from "Common/Types/Monitor/SyntheticMonitors/ScreenSizeType";
|
||||
import vm, { Context } from "node:vm";
|
||||
import { Browser, BrowserContext, Page, chromium, firefox } from "playwright";
|
||||
import LocalFile from "Common/Server/Utils/LocalFile";
|
||||
import os from "os";
|
||||
|
||||
interface WorkerConfig {
|
||||
script: string;
|
||||
browserType: BrowserType;
|
||||
screenSizeType: ScreenSizeType;
|
||||
timeout: number;
|
||||
proxy?: {
|
||||
server: string;
|
||||
username?: string | undefined;
|
||||
password?: string | undefined;
|
||||
} | undefined;
|
||||
}
|
||||
|
||||
interface WorkerResult {
|
||||
logMessages: string[];
|
||||
scriptError?: string | undefined;
|
||||
result?: unknown | undefined;
|
||||
screenshots: Record<string, string>;
|
||||
executionTimeInMS: number;
|
||||
}
|
||||
|
||||
interface ProxyOptions {
|
||||
server: string;
|
||||
username?: string | undefined;
|
||||
password?: string | undefined;
|
||||
}
|
||||
|
||||
function getViewportHeightAndWidth(screenSizeType: ScreenSizeType): {
|
||||
height: number;
|
||||
width: number;
|
||||
} {
|
||||
switch (screenSizeType) {
|
||||
case ScreenSizeType.Desktop:
|
||||
return { height: 1080, width: 1920 };
|
||||
case ScreenSizeType.Mobile:
|
||||
return { height: 640, width: 360 };
|
||||
case ScreenSizeType.Tablet:
|
||||
return { height: 768, width: 1024 };
|
||||
default:
|
||||
return { height: 1080, width: 1920 };
|
||||
}
|
||||
}
|
||||
|
||||
function getPlaywrightBrowsersPath(): string {
|
||||
return (
|
||||
process.env["PLAYWRIGHT_BROWSERS_PATH"] ||
|
||||
`${os.homedir()}/.cache/ms-playwright`
|
||||
);
|
||||
}
|
||||
|
||||
async function getChromeExecutablePath(): Promise<string> {
|
||||
const browsersPath: string = getPlaywrightBrowsersPath();
|
||||
|
||||
const doesDirectoryExist: boolean =
|
||||
await LocalFile.doesDirectoryExist(browsersPath);
|
||||
if (!doesDirectoryExist) {
|
||||
throw new Error("Chrome executable path not found.");
|
||||
}
|
||||
|
||||
const directories: string[] =
|
||||
await LocalFile.getListOfDirectories(browsersPath);
|
||||
|
||||
if (directories.length === 0) {
|
||||
throw new Error("Chrome executable path not found.");
|
||||
}
|
||||
|
||||
const chromeInstallationName: string | undefined = directories.find(
|
||||
(directory: string) => {
|
||||
return directory.includes("chromium");
|
||||
},
|
||||
);
|
||||
|
||||
if (!chromeInstallationName) {
|
||||
throw new Error("Chrome executable path not found.");
|
||||
}
|
||||
|
||||
const candidates: Array<string> = [
|
||||
`${browsersPath}/${chromeInstallationName}/chrome-linux/chrome`,
|
||||
`${browsersPath}/${chromeInstallationName}/chrome-linux64/chrome`,
|
||||
`${browsersPath}/${chromeInstallationName}/chrome64/chrome`,
|
||||
`${browsersPath}/${chromeInstallationName}/chrome/chrome`,
|
||||
];
|
||||
|
||||
for (const executablePath of candidates) {
|
||||
if (await LocalFile.doesFileExist(executablePath)) {
|
||||
return executablePath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Chrome executable path not found.");
|
||||
}
|
||||
|
||||
async function getFirefoxExecutablePath(): Promise<string> {
|
||||
const browsersPath: string = getPlaywrightBrowsersPath();
|
||||
|
||||
const doesDirectoryExist: boolean =
|
||||
await LocalFile.doesDirectoryExist(browsersPath);
|
||||
if (!doesDirectoryExist) {
|
||||
throw new Error("Firefox executable path not found.");
|
||||
}
|
||||
|
||||
const directories: string[] =
|
||||
await LocalFile.getListOfDirectories(browsersPath);
|
||||
|
||||
if (directories.length === 0) {
|
||||
throw new Error("Firefox executable path not found.");
|
||||
}
|
||||
|
||||
const firefoxInstallationName: string | undefined = directories.find(
|
||||
(directory: string) => {
|
||||
return directory.includes("firefox");
|
||||
},
|
||||
);
|
||||
|
||||
if (!firefoxInstallationName) {
|
||||
throw new Error("Firefox executable path not found.");
|
||||
}
|
||||
|
||||
const candidates: Array<string> = [
|
||||
`${browsersPath}/${firefoxInstallationName}/firefox/firefox`,
|
||||
`${browsersPath}/${firefoxInstallationName}/firefox-linux64/firefox`,
|
||||
`${browsersPath}/${firefoxInstallationName}/firefox64/firefox`,
|
||||
`${browsersPath}/${firefoxInstallationName}/firefox-64/firefox`,
|
||||
];
|
||||
|
||||
for (const executablePath of candidates) {
|
||||
if (await LocalFile.doesFileExist(executablePath)) {
|
||||
return executablePath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Firefox executable path not found.");
|
||||
}
|
||||
|
||||
async function launchBrowser(
|
||||
config: WorkerConfig,
|
||||
): Promise<{ browser: Browser; context: BrowserContext; page: Page }> {
|
||||
const viewport: { height: number; width: number } =
|
||||
getViewportHeightAndWidth(config.screenSizeType);
|
||||
|
||||
let proxyOptions: ProxyOptions | undefined;
|
||||
|
||||
if (config.proxy) {
|
||||
proxyOptions = {
|
||||
server: config.proxy.server,
|
||||
};
|
||||
|
||||
if (config.proxy.username && config.proxy.password) {
|
||||
proxyOptions.username = config.proxy.username;
|
||||
proxyOptions.password = config.proxy.password;
|
||||
}
|
||||
}
|
||||
|
||||
let browser: Browser;
|
||||
|
||||
if (config.browserType === BrowserType.Chromium) {
|
||||
const launchOptions: Record<string, unknown> = {
|
||||
executablePath: await getChromeExecutablePath(),
|
||||
};
|
||||
|
||||
if (proxyOptions) {
|
||||
launchOptions["proxy"] = proxyOptions;
|
||||
}
|
||||
|
||||
browser = await chromium.launch(launchOptions);
|
||||
} else if (config.browserType === BrowserType.Firefox) {
|
||||
const launchOptions: Record<string, unknown> = {
|
||||
executablePath: await getFirefoxExecutablePath(),
|
||||
};
|
||||
|
||||
if (proxyOptions) {
|
||||
launchOptions["proxy"] = proxyOptions;
|
||||
}
|
||||
|
||||
browser = await firefox.launch(launchOptions);
|
||||
} else {
|
||||
throw new Error("Invalid Browser Type.");
|
||||
}
|
||||
|
||||
const context: BrowserContext = await browser.newContext({
|
||||
viewport: {
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
},
|
||||
});
|
||||
|
||||
const page: Page = await context.newPage();
|
||||
|
||||
return { browser, context, page };
|
||||
}
|
||||
|
||||
async function run(config: WorkerConfig): Promise<WorkerResult> {
|
||||
const workerResult: WorkerResult = {
|
||||
logMessages: [],
|
||||
scriptError: undefined,
|
||||
result: undefined,
|
||||
screenshots: {},
|
||||
executionTimeInMS: 0,
|
||||
};
|
||||
|
||||
let browser: Browser | null = null;
|
||||
|
||||
try {
|
||||
const startTime: [number, number] = process.hrtime();
|
||||
|
||||
const session: { browser: Browser; context: BrowserContext; page: Page } =
|
||||
await launchBrowser(config);
|
||||
|
||||
browser = session.browser;
|
||||
|
||||
const logMessages: string[] = [];
|
||||
|
||||
const sandbox: Context = {
|
||||
console: {
|
||||
log: (...args: unknown[]) => {
|
||||
logMessages.push(
|
||||
args
|
||||
.map((v: unknown) => {
|
||||
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
||||
})
|
||||
.join(" "),
|
||||
);
|
||||
},
|
||||
},
|
||||
browser: session.browser,
|
||||
page: session.page,
|
||||
screenSizeType: config.screenSizeType,
|
||||
browserType: config.browserType,
|
||||
setTimeout: setTimeout,
|
||||
clearTimeout: clearTimeout,
|
||||
setInterval: setInterval,
|
||||
};
|
||||
|
||||
vm.createContext(sandbox);
|
||||
|
||||
const script: string = `(async()=>{
|
||||
${config.script}
|
||||
})()`;
|
||||
|
||||
const returnVal: unknown = await vm.runInContext(script, sandbox, {
|
||||
timeout: config.timeout,
|
||||
});
|
||||
|
||||
const endTime: [number, number] = process.hrtime(startTime);
|
||||
const executionTimeInMS: number = Math.ceil(
|
||||
(endTime[0] * 1000000000 + endTime[1]) / 1000000,
|
||||
);
|
||||
|
||||
workerResult.executionTimeInMS = executionTimeInMS;
|
||||
workerResult.logMessages = logMessages;
|
||||
|
||||
// Convert screenshots from Buffer to base64
|
||||
const returnObj: Record<string, unknown> =
|
||||
returnVal && typeof returnVal === "object"
|
||||
? (returnVal as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
if (returnObj["screenshots"]) {
|
||||
const screenshots: Record<string, unknown> = returnObj[
|
||||
"screenshots"
|
||||
] as Record<string, unknown>;
|
||||
|
||||
for (const screenshotName in screenshots) {
|
||||
if (!screenshots[screenshotName]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(screenshots[screenshotName] instanceof Buffer)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const screenshotBuffer: Buffer = screenshots[
|
||||
screenshotName
|
||||
] as Buffer;
|
||||
workerResult.screenshots[screenshotName] =
|
||||
screenshotBuffer.toString("base64");
|
||||
}
|
||||
}
|
||||
|
||||
workerResult.result = returnObj["data"];
|
||||
} catch (err: unknown) {
|
||||
workerResult.scriptError =
|
||||
(err as Error)?.message || String(err);
|
||||
} finally {
|
||||
// Close browser
|
||||
if (browser) {
|
||||
try {
|
||||
const contexts: Array<BrowserContext> = browser.contexts();
|
||||
for (const ctx of contexts) {
|
||||
try {
|
||||
await ctx.close();
|
||||
} catch (_e: unknown) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (browser.isConnected()) {
|
||||
await browser.close();
|
||||
}
|
||||
} catch (_e: unknown) {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return workerResult;
|
||||
}
|
||||
|
||||
// Entry point: receive config via IPC message
|
||||
process.on("message", (config: WorkerConfig) => {
|
||||
run(config)
|
||||
.then((result: WorkerResult) => {
|
||||
if (process.send) {
|
||||
process.send(result);
|
||||
}
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (process.send) {
|
||||
process.send({
|
||||
logMessages: [],
|
||||
scriptError: (err as Error)?.message || String(err),
|
||||
result: undefined,
|
||||
screenshots: {},
|
||||
executionTimeInMS: 0,
|
||||
} as WorkerResult);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
|
||||
import { RegisterProbeKey } from "Common/Server/EnvironmentConfig";
|
||||
import ProbeService from "Common/Server/Services/ProbeService";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
@@ -19,7 +19,6 @@ const router: ExpressRouter = Express.getRouter();
|
||||
// Register Global Probe. Custom Probe can be registered via dashboard.
|
||||
router.post(
|
||||
"/register",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
@@ -28,6 +27,23 @@ router.post(
|
||||
try {
|
||||
const data: JSONObject = req.body;
|
||||
|
||||
const registerProbeKey: string | undefined = data[
|
||||
"registerProbeKey"
|
||||
] as string;
|
||||
|
||||
if (
|
||||
!registerProbeKey ||
|
||||
registerProbeKey !== RegisterProbeKey.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Invalid or missing registerProbeKey. Please check REGISTER_PROBE_KEY environment variable.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!data["probeKey"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
|
||||
@@ -22,6 +22,7 @@ CAPTCHA_SECRET_KEY=
|
||||
|
||||
# Secrets - PLEASE CHANGE THESE. Please change these to something random. All of these can be different values.
|
||||
ONEUPTIME_SECRET=please-change-this-to-random-value
|
||||
REGISTER_PROBE_KEY=please-change-this-to-random-value
|
||||
DATABASE_PASSWORD=please-change-this-to-random-value
|
||||
CLICKHOUSE_PASSWORD=please-change-this-to-random-value
|
||||
REDIS_PASSWORD=please-change-this-to-random-value
|
||||
|
||||
@@ -400,7 +400,7 @@ services:
|
||||
network_mode: host
|
||||
environment:
|
||||
ONEUPTIME_URL: ${GLOBAL_PROBE_1_ONEUPTIME_URL}
|
||||
ONEUPTIME_SECRET: ${ONEUPTIME_SECRET}
|
||||
REGISTER_PROBE_KEY: ${REGISTER_PROBE_KEY}
|
||||
PROBE_NAME: ${GLOBAL_PROBE_1_NAME}
|
||||
PROBE_DESCRIPTION: ${GLOBAL_PROBE_1_DESCRIPTION}
|
||||
PROBE_MONITORING_WORKERS: ${GLOBAL_PROBE_1_MONITORING_WORKERS}
|
||||
@@ -424,7 +424,7 @@ services:
|
||||
network_mode: host
|
||||
environment:
|
||||
ONEUPTIME_URL: ${GLOBAL_PROBE_2_ONEUPTIME_URL}
|
||||
ONEUPTIME_SECRET: ${ONEUPTIME_SECRET}
|
||||
REGISTER_PROBE_KEY: ${REGISTER_PROBE_KEY}
|
||||
PROBE_NAME: ${GLOBAL_PROBE_2_NAME}
|
||||
PROBE_DESCRIPTION: ${GLOBAL_PROBE_2_DESCRIPTION}
|
||||
PROBE_MONITORING_WORKERS: ${GLOBAL_PROBE_2_MONITORING_WORKERS}
|
||||
@@ -515,6 +515,7 @@ services:
|
||||
PORT: ${PROBE_INGEST_PORT}
|
||||
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_PROBE_INGEST}
|
||||
PROBE_INGEST_CONCURRENCY: ${PROBE_INGEST_CONCURRENCY}
|
||||
REGISTER_PROBE_KEY: ${REGISTER_PROBE_KEY}
|
||||
logging:
|
||||
driver: "local"
|
||||
options:
|
||||
|
||||
Reference in New Issue
Block a user