mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-05-09 12:54:21 +02:00
Co-authored-by: sofia.fernandez <sofia.fernandez@six-group.com>
This commit is contained in:
parent
7d7f12b5b1
commit
2f45b46315
4 changed files with 314 additions and 25 deletions
|
|
@ -1,7 +1,7 @@
|
|||
const { MonitorType } = require("./monitor-type");
|
||||
const WebSocket = require("ws");
|
||||
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
|
||||
const WS_ERR_CODE = {
|
||||
1002: "Protocol error",
|
||||
|
|
@ -50,17 +50,77 @@ class WebSocketMonitorType extends MonitorType {
|
|||
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
|
||||
* @param {object} monitor The monitor object for input parameters.
|
||||
* @returns {Promise<[ string, int ]>} Array containing a status message and response code
|
||||
*/
|
||||
async attemptUpgrade(monitor) {
|
||||
const authOptions = await this.buildWsOptions(monitor);
|
||||
|
||||
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
|
||||
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) => {
|
||||
// Immediately close the connection
|
||||
|
|
|
|||
|
|
@ -976,6 +976,7 @@
|
|||
"Analytics ID": "Analytics ID",
|
||||
"Analytics Script URL": "Analytics Script URL",
|
||||
"Edit Tag": "Edit Tag",
|
||||
"WebSocket Options": "WebSocket Options",
|
||||
"Server Address": "Server Address",
|
||||
"Learn More": "Learn More",
|
||||
"Body Encoding": "Body Encoding",
|
||||
|
|
|
|||
|
|
@ -202,28 +202,43 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Websocket Subprotocol Docs: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name -->
|
||||
<div v-if="monitor.type === 'websocket-upgrade'" class="my-3">
|
||||
<label for="ws_subprotocol" class="form-label">{{ $t("Subprotocol(s)") }}</label>
|
||||
<input
|
||||
id="ws_subprotocol"
|
||||
v-model="monitor.wsSubprotocol"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="mielecloudconnect,soap"
|
||||
/>
|
||||
<i18n-t tag="div" class="form-text" keypath="wsSubprotocolDescription">
|
||||
<template #documentation>
|
||||
<a
|
||||
href="https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t("documentationOf", ["IANA"]) }}
|
||||
</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<template v-if="monitor.type === 'websocket-upgrade'">
|
||||
<h2 class="mt-5 mb-2">{{ $t("WebSocket Options") }}</h2>
|
||||
|
||||
<!-- Websocket Subprotocol Docs: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name -->
|
||||
<div class="my-3">
|
||||
<label for="ws_subprotocol" class="form-label">{{ $t("Subprotocol(s)") }}</label>
|
||||
<input
|
||||
id="ws_subprotocol"
|
||||
v-model="monitor.wsSubprotocol"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="mielecloudconnect,soap"
|
||||
/>
|
||||
<i18n-t tag="div" class="form-text" keypath="wsSubprotocolDescription">
|
||||
<template #documentation>
|
||||
<a
|
||||
href="https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t("documentationOf", ["IANA"]) }}
|
||||
</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 -->
|
||||
<div v-if="monitor.type === 'grpc-keyword'" class="my-3">
|
||||
|
|
@ -2058,6 +2073,176 @@
|
|||
{{ $t("Setup Notification") }}
|
||||
</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 -->
|
||||
<div
|
||||
v-if="
|
||||
|
|
|
|||
|
|
@ -380,4 +380,47 @@ describe("WebSocket Monitor", {}, () => {
|
|||
await websocketMonitor.check(monitor, heartbeat, {});
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue