Compare commits

...

8 Commits

13 changed files with 188 additions and 33 deletions

View File

@@ -50,11 +50,29 @@ export default class URL extends DatabaseProperty {
this._protocol = v;
}
private _username: string | null = null;
public get username(): string | null {
return this._username;
}
public set username(v: string | null) {
this._username = v;
}
private _password: string | null = null;
public get password(): string | null {
return this._password;
}
public set password(v: string | null) {
this._password = v;
}
public constructor(
protocol: Protocol,
hostname: Hostname | string | Email,
route?: Route,
queryString?: string,
username?: string | null,
password?: string | null,
) {
super();
@@ -89,14 +107,33 @@ export default class URL extends DatabaseProperty {
}
}
}
this.username = username || null;
this.password = password || null;
}
public isHttps(): boolean {
return this.protocol === Protocol.HTTPS;
public getUsername(): string | null {
return this.username;
}
public getPassword(): string | null {
return this.password;
}
public override toString(): string {
let urlString: string = `${this.protocol}${this.hostname || this.email}`;
let urlString: string = `${this.protocol}`;
// Add auth if present
if (this.username) {
urlString += `${this.username}`;
if (this.password) {
urlString += `:${this.password}`;
}
urlString += `@`;
}
urlString += `${this.hostname || this.email}`;
if (!this.email && !urlString.startsWith("mailto:")) {
if (this.route && this.route.toString().startsWith("/")) {
if (urlString.endsWith("/")) {
@@ -130,37 +167,47 @@ export default class URL extends DatabaseProperty {
public static fromString(url: string): URL {
let protocol: Protocol = Protocol.HTTPS;
let username: string | null = null;
let password: string | null = null;
if (url.startsWith("https://")) {
protocol = Protocol.HTTPS;
url = url.replace("https://", "");
}
if (url.startsWith("http://")) {
} else if (url.startsWith("http://")) {
protocol = Protocol.HTTP;
url = url.replace("http://", "");
}
if (url.startsWith("wss://")) {
} else if (url.startsWith("wss://")) {
protocol = Protocol.WSS;
url = url.replace("wss://", "");
}
if (url.startsWith("ws://")) {
} else if (url.startsWith("ws://")) {
protocol = Protocol.WS;
url = url.replace("ws://", "");
}
if (url.startsWith("mongodb://")) {
} else if (url.startsWith("mongodb://")) {
protocol = Protocol.MONGO_DB;
url = url.replace("mongodb://", "");
}
if (url.startsWith("mailto:")) {
} else if (url.startsWith("mailto:")) {
protocol = Protocol.MAIL;
url = url.replace("mailto:", "");
}
// Parse auth if present (username:password@)
if (url.includes('@')) {
const parts = url.split('@');
if (parts.length > 1 && parts[0] && parts[1]) {
const authPart = parts[0];
if (authPart.includes(':')) {
const authSplit = authPart.split(':');
if (authSplit.length >= 2 && authSplit[0] && authSplit[1]) {
username = decodeURIComponent(authSplit[0]);
password = decodeURIComponent(authSplit[1]);
}
} else {
username = decodeURIComponent(authPart);
}
url = parts[1]; // Remove auth part from URL
}
}
const hostname: Hostname = new Hostname(url.split("/")[0] || "");
let route: Route | undefined;
@@ -173,7 +220,7 @@ export default class URL extends DatabaseProperty {
const queryString: string | undefined = url.split("?")[1] || "";
return new URL(protocol, hostname, route, queryString);
return new URL(protocol, hostname, route, queryString, username, password);
}
public removeQueryString(): URL {

View File

@@ -1,6 +1,7 @@
import HTTPMethod from "./API/HTTPMethod";
import Headers from "./API/Headers";
import URL from "./API/URL";
import Protocol from "./API/Protocol";
import Dictionary from "./Dictionary";
import HTML from "./Html";
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
@@ -22,6 +23,7 @@ export default class WebsiteRequest {
timeout?: number | undefined;
isHeadRequest?: boolean | undefined;
doNotFollowRedirects?: boolean | undefined;
proxyUrl?: URL | undefined;
},
): Promise<WebsiteResponse> {
const axiosOptions: AxiosRequestConfig = {
@@ -41,6 +43,24 @@ export default class WebsiteRequest {
axiosOptions.maxRedirects = 0;
}
if (options.proxyUrl) {
axiosOptions.proxy = {
host: options.proxyUrl.hostname.hostname,
port: options.proxyUrl.hostname.port?.toNumber() || 80,
protocol: options.proxyUrl.protocol === Protocol.HTTPS ? 'https' : 'http',
};
// Handle auth if present in URL
const username = options.proxyUrl.getUsername();
const password = options.proxyUrl.getPassword();
if (username && password) {
axiosOptions.proxy.auth = {
username: decodeURIComponent(username),
password: decodeURIComponent(password),
};
}
}
// use axios to fetch an HTML page
let response: AxiosResponse | null = null;

View File

@@ -19,6 +19,23 @@ export interface RequestOptions {
exponentialBackoff?: boolean | undefined;
timeout?: number | undefined;
doNotFollowRedirects?: boolean | undefined;
proxyUrl?: URL | undefined;
}
// Declare AggregateError interface for environments where it's not available
interface AggregateError extends Error {
errors: Error[];
}
// Type guard for AggregateError
function isAggregateError(error: unknown): error is AggregateError {
return (
error instanceof Error &&
'name' in error &&
(error as any).name === 'AggregateError' &&
'errors' in error &&
Array.isArray((error as any).errors)
);
}
export default class API {
@@ -169,7 +186,7 @@ export default class API {
return Promise.resolve(headers);
}
public static getDefaultHeaders(_props?: any): Headers {
public static getDefaultHeaders(_props?: unknown): Headers {
const defaultHeaders: Headers = {
"Access-Control-Allow-Origin": "*",
Accept: "application/json",
@@ -400,6 +417,24 @@ export default class API {
axiosOptions.maxRedirects = 0;
}
if (options?.proxyUrl) {
axiosOptions.proxy = {
host: options.proxyUrl.hostname.hostname,
port: options.proxyUrl.hostname.port?.toNumber() || 80,
protocol: options.proxyUrl.protocol === Protocol.HTTPS ? 'https' : 'http',
};
// Handle auth if present in URL
const username = options.proxyUrl.getUsername();
const password = options.proxyUrl.getPassword();
if (username && password) {
axiosOptions.proxy.auth = {
username: decodeURIComponent(username),
password: decodeURIComponent(password),
};
}
}
result = await axios(axiosOptions);
break;
@@ -471,12 +506,8 @@ export default class API {
}
// Handle AggregateError by extracting the underlying error messages
if (
error &&
(error as any).name === "AggregateError" &&
(error as any).errors
) {
const aggregateErrors: Error[] = (error as any).errors as Error[];
if (isAggregateError(error)) {
const aggregateErrors: Error[] = (error as AggregateError).errors as Error[];
const errorMessages: string[] = aggregateErrors
.map((err: Error) => {
return err.message || err.toString();

View File

@@ -16,6 +16,18 @@ docker run --name oneuptime-probe --network host -e PROBE_KEY=<probe-key> -e PRO
If you are self hosting OneUptime, you can change `ONEUPTIME_URL` to your custom self hosted instance.
#### Proxy Configuration
If your network requires outgoing requests to go through a proxy, you can configure the probe to use a proxy server by setting the `PROXY_URL` environment variable. This is useful for monitoring resources that require proxy access.
The `PROXY_URL` should be in the format: `http://proxy.example.com:8080` or `https://user:pass@proxy.example.com:8080` for authenticated proxies.
Example with proxy:
```
docker run --name oneuptime-probe --network host -e PROBE_KEY=<probe-key> -e PROBE_ID=<probe-id> -e ONEUPTIME_URL=https://oneuptime.com -e PROXY_URL=http://proxy.example.com:8080 -d oneuptime/probe:release
```
#### Docker Compose
You can also run the probe using docker-compose. Create a `docker-compose.yml` file with the following content:
@@ -31,6 +43,7 @@ services:
- PROBE_KEY=<probe-key>
- PROBE_ID=<probe-id>
- ONEUPTIME_URL=https://oneuptime.com
- PROXY_URL=http://proxy.example.com:8080 # Optional: Proxy URL for monitoring requests
network_mode: host
restart: always
```
@@ -70,6 +83,8 @@ spec:
value: "<probe-id>"
- name: ONEUPTIME_URL
value: "https://oneuptime.com"
- name: PROXY_URL
value: "http://proxy.example.com:8080" # Optional: Proxy URL for monitoring requests
```
Then run the following command:

View File

@@ -99,6 +99,10 @@ spec:
{{- end }}
- name: PROBE_MONITOR_FETCH_LIMIT
value: {{ $val.monitorFetchLimit | squote }}
{{- if $val.proxyUrl }}
- name: PROXY_URL
value: {{ $val.proxyUrl }}
{{- end }}
{{- if $val.disableTelemetryCollection }}
- name: DISABLE_TELEMETRY
value: {{ $val.disableTelemetryCollection | quote }}

View File

@@ -200,6 +200,7 @@ probes:
monitoringWorkers: 3
monitorFetchLimit: 10
monitorRetryLimit: 3
proxyUrl: ""
key:
replicaCount: 1
syntheticMonitorScriptTimeoutInMs: 60000

View File

@@ -60,6 +60,10 @@ export const PROBE_MONITOR_FETCH_LIMIT: number = monitorFetchLimit;
export const HOSTNAME: string = process.env["HOSTNAME"] || "localhost";
export const PROXY_URL: URL | null = process.env["PROXY_URL"]
? URL.fromString(process.env["PROXY_URL"])
: null;
export const PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS: number = process.env[
"PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS"
]

View File

@@ -11,6 +11,7 @@ import PositiveNumber from "Common/Types/PositiveNumber";
import Sleep from "Common/Types/Sleep";
import API from "Common/Utils/API";
import logger from "Common/Server/Utils/Logger";
import { PROXY_URL } from "../../../Config";
export interface APIResponse {
url: URL;
@@ -69,6 +70,7 @@ export default class ApiMonitor {
{
timeout: options.timeout?.toNumber() || 5000,
doNotFollowRedirects: options.doNotFollowRedirects || false,
proxyUrl: PROXY_URL || undefined,
},
);
@@ -87,6 +89,7 @@ export default class ApiMonitor {
{
timeout: options.timeout?.toNumber() || 5000,
doNotFollowRedirects: options.doNotFollowRedirects || false,
proxyUrl: PROXY_URL || undefined,
},
);
}

View File

@@ -1,4 +1,4 @@
import { PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS } from "../../../Config";
import { PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS, PROXY_URL } from "../../../Config";
import BadDataException from "Common/Types/Exception/BadDataException";
import ReturnResult from "Common/Types/IsolatedVM/ReturnResult";
import BrowserType from "Common/Types/Monitor/SyntheticMonitors/BrowserType";
@@ -17,6 +17,13 @@ export interface SyntheticMonitorOptions {
script: string;
}
interface BrowserLaunchOptions {
executablePath: string;
proxy?: {
server: string;
};
}
export default class SyntheticMonitor {
public static async execute(
options: SyntheticMonitorOptions,
@@ -132,8 +139,8 @@ export default class SyntheticMonitor {
}
scriptResult.screenshots[screenshotName] = (
result.returnValue.screenshots[screenshotName] as any
).toString("base64"); // convert screennshots to base 64
result.returnValue.screenshots[screenshotName] as Buffer
).toString("base64"); // convert screenshots to base 64
}
}
@@ -272,16 +279,32 @@ export default class SyntheticMonitor {
let browser: Browser | null = null;
if (data.browserType === BrowserType.Chromium) {
browser = await chromium.launch({
const launchOptions: BrowserLaunchOptions = {
executablePath: await this.getChromeExecutablePath(),
});
};
if (PROXY_URL) {
launchOptions.proxy = {
server: PROXY_URL.toString(),
};
}
browser = await chromium.launch(launchOptions);
page = await browser.newPage();
}
if (data.browserType === BrowserType.Firefox) {
browser = await firefox.launch({
const launchOptions: BrowserLaunchOptions = {
executablePath: await this.getFirefoxExecutablePath(),
});
};
if (PROXY_URL) {
launchOptions.proxy = {
server: PROXY_URL.toString(),
};
}
browser = await firefox.launch(launchOptions);
page = await browser.newPage();
}

View File

@@ -11,6 +11,7 @@ import WebsiteRequest, { WebsiteResponse } from "Common/Types/WebsiteRequest";
import API from "Common/Utils/API";
import logger from "Common/Server/Utils/Logger";
import { AxiosError } from "axios";
import { PROXY_URL } from "../../../Config";
export interface ProbeWebsiteResponse {
url: URL;
@@ -64,6 +65,7 @@ export default class WebsiteMonitor {
isHeadRequest: options.isHeadRequest,
timeout: options.timeout?.toNumber() || 5000,
doNotFollowRedirects: options.doNotFollowRedirects || false,
proxyUrl: PROXY_URL || undefined,
});
if (
@@ -76,6 +78,7 @@ export default class WebsiteMonitor {
isHeadRequest: false,
timeout: options.timeout?.toNumber() || 5000,
doNotFollowRedirects: options.doNotFollowRedirects || false,
proxyUrl: PROXY_URL || undefined,
});
}

View File

@@ -177,6 +177,7 @@ GLOBAL_PROBE_1_ONEUPTIME_URL=http://localhost
GLOBAL_PROBE_1_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS=60000
GLOBAL_PROBE_1_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS=60000
GLOBAL_PROBE_1_PORT=3874
GLOBAL_PROBE_1_PROXY_URL= # Optional: Proxy URL for monitoring requests
GLOBAL_PROBE_2_NAME="Probe-2"
GLOBAL_PROBE_2_DESCRIPTION="Global probe to monitor oneuptime resources"
@@ -186,6 +187,7 @@ GLOBAL_PROBE_2_ONEUPTIME_URL=http://localhost
GLOBAL_PROBE_2_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS=60000
GLOBAL_PROBE_2_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS=60000
GLOBAL_PROBE_2_PORT=3875
GLOBAL_PROBE_2_PROXY_URL= # Optional: Proxy URL for monitoring requests
SMS_DEFAULT_COST_IN_CENTS=
CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE=

View File

@@ -362,6 +362,7 @@ services:
PROBE_MONITOR_FETCH_LIMIT: ${GLOBAL_PROBE_1_MONITOR_FETCH_LIMIT}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_PROBE}
PORT: ${GLOBAL_PROBE_1_PORT}
PROXY_URL: ${GLOBAL_PROBE_1_PROXY_URL}
logging:
driver: "local"
options:
@@ -382,6 +383,7 @@ services:
PROBE_MONITOR_FETCH_LIMIT: ${GLOBAL_PROBE_2_MONITOR_FETCH_LIMIT}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_PROBE}
PORT: ${GLOBAL_PROBE_2_PORT}
PROXY_URL: ${GLOBAL_PROBE_2_PROXY_URL}
logging:
driver: "local"
options: