mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-05-17 00:16:32 +03:00
Co-authored-by: sofia.fernandez <sofia.fernandez@six-group.com>
This commit is contained in:
committed by
GitHub
parent
7d7f12b5b1
commit
2f45b46315
@@ -1,7 +1,7 @@
|
|||||||
const { MonitorType } = require("./monitor-type");
|
const { MonitorType } = require("./monitor-type");
|
||||||
const WebSocket = require("ws");
|
const WebSocket = require("ws");
|
||||||
const { UP } = require("../../src/util");
|
const { UP } = require("../../src/util");
|
||||||
const { checkStatusCode } = require("../util-server");
|
const { checkStatusCode, getOidcTokenClientCredentials } = require("../util-server");
|
||||||
// Define closing error codes https://www.iana.org/assignments/websocket/websocket.xml#close-code-number
|
// Define closing error codes https://www.iana.org/assignments/websocket/websocket.xml#close-code-number
|
||||||
const WS_ERR_CODE = {
|
const WS_ERR_CODE = {
|
||||||
1002: "Protocol error",
|
1002: "Protocol error",
|
||||||
@@ -50,17 +50,77 @@ class WebSocketMonitorType extends MonitorType {
|
|||||||
throw new Error("Unknown Websocket Error");
|
throw new Error("Unknown Websocket Error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the WebSocket options object for authentication and TLS.
|
||||||
|
* Supports basic auth, OAuth2 client credentials, and mTLS.
|
||||||
|
* @param {object} monitor The monitor object for input parameters.
|
||||||
|
* @returns {Promise<object>} The options object to pass to the WebSocket constructor.
|
||||||
|
*/
|
||||||
|
async buildWsOptions(monitor) {
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
const timeoutMs = (monitor.timeout ?? 20) * 1000;
|
||||||
|
options.handshakeTimeout = timeoutMs;
|
||||||
|
|
||||||
|
// Parse custom headers if provided
|
||||||
|
if (monitor.headers) {
|
||||||
|
try {
|
||||||
|
options.headers = JSON.parse(monitor.headers);
|
||||||
|
} catch (e) {
|
||||||
|
// If headers is not valid JSON, ignore it
|
||||||
|
options.headers = {};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options.headers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor.authMethod === "basic") {
|
||||||
|
if (monitor.basic_auth_user || monitor.basic_auth_pass) {
|
||||||
|
const credentials = Buffer.from(
|
||||||
|
`${monitor.basic_auth_user ?? ""}:${monitor.basic_auth_pass ?? ""}`
|
||||||
|
).toString("base64");
|
||||||
|
options.headers.Authorization = `Basic ${credentials}`;
|
||||||
|
}
|
||||||
|
} else if (monitor.authMethod === "oauth2-cc") {
|
||||||
|
if (new Date((monitor.oauthAccessToken?.expires_at || 0) * 1000) <= new Date()) {
|
||||||
|
monitor.oauthAccessToken = await getOidcTokenClientCredentials(
|
||||||
|
monitor.oauth_token_url,
|
||||||
|
monitor.oauth_client_id,
|
||||||
|
monitor.oauth_client_secret,
|
||||||
|
monitor.oauth_scopes,
|
||||||
|
monitor.oauth_audience,
|
||||||
|
monitor.oauth_auth_method
|
||||||
|
);
|
||||||
|
}
|
||||||
|
options.headers.Authorization = `${monitor.oauthAccessToken.token_type} ${monitor.oauthAccessToken.access_token}`;
|
||||||
|
} else if (monitor.authMethod === "mtls") {
|
||||||
|
if (monitor.tlsCert) {
|
||||||
|
options.cert = monitor.tlsCert;
|
||||||
|
}
|
||||||
|
if (monitor.tlsKey) {
|
||||||
|
options.key = monitor.tlsKey;
|
||||||
|
}
|
||||||
|
if (monitor.tlsCa) {
|
||||||
|
options.ca = monitor.tlsCa;
|
||||||
|
}
|
||||||
|
options.rejectUnauthorized = !monitor.getIgnoreTls();
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses the ws Node.js library to establish a connection to target server
|
* Uses the ws Node.js library to establish a connection to target server
|
||||||
* @param {object} monitor The monitor object for input parameters.
|
* @param {object} monitor The monitor object for input parameters.
|
||||||
* @returns {Promise<[ string, int ]>} Array containing a status message and response code
|
* @returns {Promise<[ string, int ]>} Array containing a status message and response code
|
||||||
*/
|
*/
|
||||||
async attemptUpgrade(monitor) {
|
async attemptUpgrade(monitor) {
|
||||||
|
const authOptions = await this.buildWsOptions(monitor);
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const timeoutMs = (monitor.timeout ?? 20) * 1000;
|
|
||||||
// If user inputs subprotocol(s), convert to array, set Sec-WebSocket-Protocol header, timeout in ms. Subprotocol Identifier column: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name
|
// If user inputs subprotocol(s), convert to array, set Sec-WebSocket-Protocol header, timeout in ms. Subprotocol Identifier column: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name
|
||||||
const subprotocol = monitor.wsSubprotocol ? monitor.wsSubprotocol.replace(/\s/g, "").split(",") : undefined;
|
const subprotocol = monitor.wsSubprotocol ? monitor.wsSubprotocol.replace(/\s/g, "").split(",") : undefined;
|
||||||
const ws = new WebSocket(monitor.url, subprotocol, { handshakeTimeout: timeoutMs });
|
const ws = new WebSocket(monitor.url, subprotocol, authOptions);
|
||||||
|
|
||||||
ws.addEventListener("open", (event) => {
|
ws.addEventListener("open", (event) => {
|
||||||
// Immediately close the connection
|
// Immediately close the connection
|
||||||
|
|||||||
@@ -976,6 +976,7 @@
|
|||||||
"Analytics ID": "Analytics ID",
|
"Analytics ID": "Analytics ID",
|
||||||
"Analytics Script URL": "Analytics Script URL",
|
"Analytics Script URL": "Analytics Script URL",
|
||||||
"Edit Tag": "Edit Tag",
|
"Edit Tag": "Edit Tag",
|
||||||
|
"WebSocket Options": "WebSocket Options",
|
||||||
"Server Address": "Server Address",
|
"Server Address": "Server Address",
|
||||||
"Learn More": "Learn More",
|
"Learn More": "Learn More",
|
||||||
"Body Encoding": "Body Encoding",
|
"Body Encoding": "Body Encoding",
|
||||||
|
|||||||
@@ -202,28 +202,43 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Websocket Subprotocol Docs: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name -->
|
<template v-if="monitor.type === 'websocket-upgrade'">
|
||||||
<div v-if="monitor.type === 'websocket-upgrade'" class="my-3">
|
<h2 class="mt-5 mb-2">{{ $t("WebSocket Options") }}</h2>
|
||||||
<label for="ws_subprotocol" class="form-label">{{ $t("Subprotocol(s)") }}</label>
|
|
||||||
<input
|
<!-- Websocket Subprotocol Docs: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name -->
|
||||||
id="ws_subprotocol"
|
<div class="my-3">
|
||||||
v-model="monitor.wsSubprotocol"
|
<label for="ws_subprotocol" class="form-label">{{ $t("Subprotocol(s)") }}</label>
|
||||||
type="text"
|
<input
|
||||||
class="form-control"
|
id="ws_subprotocol"
|
||||||
placeholder="mielecloudconnect,soap"
|
v-model="monitor.wsSubprotocol"
|
||||||
/>
|
type="text"
|
||||||
<i18n-t tag="div" class="form-text" keypath="wsSubprotocolDescription">
|
class="form-control"
|
||||||
<template #documentation>
|
placeholder="mielecloudconnect,soap"
|
||||||
<a
|
/>
|
||||||
href="https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name"
|
<i18n-t tag="div" class="form-text" keypath="wsSubprotocolDescription">
|
||||||
target="_blank"
|
<template #documentation>
|
||||||
rel="noopener noreferrer"
|
<a
|
||||||
>
|
href="https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name"
|
||||||
{{ $t("documentationOf", ["IANA"]) }}
|
target="_blank"
|
||||||
</a>
|
rel="noopener noreferrer"
|
||||||
</template>
|
>
|
||||||
</i18n-t>
|
{{ $t("documentationOf", ["IANA"]) }}
|
||||||
</div>
|
</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Headers -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-headers" class="form-label">{{ $t("Headers") }}</label>
|
||||||
|
<textarea
|
||||||
|
id="ws-headers"
|
||||||
|
v-model="monitor.headers"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="headersPlaceholder"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- gRPC URL -->
|
<!-- gRPC URL -->
|
||||||
<div v-if="monitor.type === 'grpc-keyword'" class="my-3">
|
<div v-if="monitor.type === 'grpc-keyword'" class="my-3">
|
||||||
@@ -2058,6 +2073,176 @@
|
|||||||
{{ $t("Setup Notification") }}
|
{{ $t("Setup Notification") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- WebSocket Authentication -->
|
||||||
|
<template v-if="monitor.type === 'websocket-upgrade'">
|
||||||
|
<h2 class="mt-5 mb-2">{{ $t("Authentication") }}</h2>
|
||||||
|
|
||||||
|
<!-- Auth Method -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-auth-method" class="form-label">{{ $t("Method") }}</label>
|
||||||
|
<select id="ws-auth-method" v-model="monitor.authMethod" class="form-select">
|
||||||
|
<option :value="null">
|
||||||
|
{{ $t("None") }}
|
||||||
|
</option>
|
||||||
|
<option value="basic">
|
||||||
|
{{ $t("HTTP Basic Auth") }}
|
||||||
|
</option>
|
||||||
|
<option value="oauth2-cc">
|
||||||
|
{{ $t("OAuth2: Client Credentials") }}
|
||||||
|
</option>
|
||||||
|
<option value="mtls">mTLS</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="monitor.authMethod === 'basic'">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-basicauth-user" class="form-label">{{ $t("Username") }}</label>
|
||||||
|
<input
|
||||||
|
id="ws-basicauth-user"
|
||||||
|
v-model="monitor.basic_auth_user"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('Username')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-basicauth-pass" class="form-label">{{ $t("Password") }}</label>
|
||||||
|
<HiddenInput
|
||||||
|
id="ws-basicauth-pass"
|
||||||
|
v-model="monitor.basic_auth_pass"
|
||||||
|
autocomplete="new-password"
|
||||||
|
:placeholder="$t('Password')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="monitor.authMethod === 'oauth2-cc'">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-oauth-auth-method" class="form-label">
|
||||||
|
{{ $t("Authentication Method") }}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="ws-oauth-auth-method"
|
||||||
|
v-model="monitor.oauth_auth_method"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<option value="client_secret_basic">
|
||||||
|
{{ $t("Authorization Header") }}
|
||||||
|
</option>
|
||||||
|
<option value="client_secret_post">
|
||||||
|
{{ $t("Form Data Body") }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-oauth-token-url" class="form-label">
|
||||||
|
{{ $t("OAuth Token URL") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="ws-oauth-token-url"
|
||||||
|
v-model="monitor.oauth_token_url"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('OAuth Token URL')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-oauth-client-id" class="form-label">
|
||||||
|
{{ $t("Client ID") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="ws-oauth-client-id"
|
||||||
|
v-model="monitor.oauth_client_id"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('Client ID')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
monitor.oauth_auth_method === 'client_secret_post' ||
|
||||||
|
monitor.oauth_auth_method === 'client_secret_basic'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-oauth-client-secret" class="form-label">
|
||||||
|
{{ $t("Client Secret") }}
|
||||||
|
</label>
|
||||||
|
<HiddenInput
|
||||||
|
id="ws-oauth-client-secret"
|
||||||
|
v-model="monitor.oauth_client_secret"
|
||||||
|
:placeholder="$t('Client Secret')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-oauth-scopes" class="form-label">
|
||||||
|
{{ $t("OAuth Scope") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="ws-oauth-scopes"
|
||||||
|
v-model="monitor.oauth_scopes"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('Optional: Space separated list of scopes')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-oauth-audience" class="form-label">
|
||||||
|
{{ $t("OAuth Audience") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="ws-oauth-audience"
|
||||||
|
v-model="monitor.oauth_audience"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('Optional: The audience to request the JWT for')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="monitor.authMethod === 'mtls'">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-tls-cert" class="form-label">
|
||||||
|
{{ $t("mtls-auth-server-cert-label") }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="ws-tls-cert"
|
||||||
|
v-model="monitor.tlsCert"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('mtls-auth-server-cert-placeholder')"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-tls-key" class="form-label">
|
||||||
|
{{ $t("mtls-auth-server-key-label") }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="ws-tls-key"
|
||||||
|
v-model="monitor.tlsKey"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('mtls-auth-server-key-placeholder')"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="ws-tls-ca" class="form-label">
|
||||||
|
{{ $t("mtls-auth-server-ca-label") }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="ws-tls-ca"
|
||||||
|
v-model="monitor.tlsCa"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('mtls-auth-server-ca-placeholder')"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Proxies -->
|
<!-- Proxies -->
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
|
|||||||
@@ -380,4 +380,47 @@ describe("WebSocket Monitor", {}, () => {
|
|||||||
await websocketMonitor.check(monitor, heartbeat, {});
|
await websocketMonitor.check(monitor, heartbeat, {});
|
||||||
assert.deepStrictEqual(heartbeat, expected);
|
assert.deepStrictEqual(heartbeat, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("buildWsOptions() includes custom headers", async () => {
|
||||||
|
const websocketMonitor = new WebSocketMonitorType();
|
||||||
|
|
||||||
|
const options = await websocketMonitor.buildWsOptions({
|
||||||
|
headers: JSON.stringify({
|
||||||
|
"X-Test": "test-value",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(options.headers, {
|
||||||
|
"X-Test": "test-value",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildWsOptions() ignores invalid custom headers JSON", async () => {
|
||||||
|
const websocketMonitor = new WebSocketMonitorType();
|
||||||
|
|
||||||
|
const options = await websocketMonitor.buildWsOptions({
|
||||||
|
headers: "{ invalid-json",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(options.headers, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildWsOptions() authentication header overrides custom Authorization header", async () => {
|
||||||
|
const websocketMonitor = new WebSocketMonitorType();
|
||||||
|
|
||||||
|
const options = await websocketMonitor.buildWsOptions({
|
||||||
|
headers: JSON.stringify({
|
||||||
|
Authorization: "Bearer custom-token",
|
||||||
|
"X-Test": "test-value",
|
||||||
|
}),
|
||||||
|
authMethod: "basic",
|
||||||
|
basic_auth_user: "user",
|
||||||
|
basic_auth_pass: "pass",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(options.headers, {
|
||||||
|
Authorization: "Basic dXNlcjpwYXNz",
|
||||||
|
"X-Test": "test-value",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user