mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-09 08:38:37 +08:00
Merge remote-tracking branch 'origin/master' into status-page-expiry
# Conflicts: # src/lang/en.json
This commit is contained in:
95
server/monitor-types/tailscale-ping.js
Normal file
95
server/monitor-types/tailscale-ping.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP, log } = require("../../src/util");
|
||||
const exec = require("child_process").exec;
|
||||
|
||||
/**
|
||||
* A TailscalePing class extends the MonitorType.
|
||||
* It runs Tailscale ping to monitor the status of a specific node.
|
||||
*/
|
||||
class TailscalePing extends MonitorType {
|
||||
|
||||
name = "tailscale-ping";
|
||||
|
||||
/**
|
||||
* Checks the ping status of the URL associated with the monitor.
|
||||
* It then parses the Tailscale ping command output to update the heatrbeat.
|
||||
*
|
||||
* @param {Object} monitor - The monitor object associated with the check.
|
||||
* @param {Object} heartbeat - The heartbeat object to update.
|
||||
* @throws Will throw an error if checking Tailscale ping encounters any error
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
try {
|
||||
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
||||
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
||||
} catch (err) {
|
||||
log.debug("Tailscale", err);
|
||||
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
||||
throw new Error(`Error checking Tailscale ping: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the Tailscale ping command to the given URL.
|
||||
*
|
||||
* @param {string} hostname - The hostname to ping.
|
||||
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
|
||||
* @throws Will throw an error if the command execution encounters any error.
|
||||
*/
|
||||
async runTailscalePing(hostname, interval) {
|
||||
let cmd = `tailscale ping ${hostname}`;
|
||||
|
||||
log.debug("Tailscale", cmd);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeout = interval * 1000 * 0.8;
|
||||
exec(cmd, { timeout: timeout }, (error, stdout, stderr) => {
|
||||
// we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues)
|
||||
if (error) {
|
||||
reject(`Execution error: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
reject(`Error in output: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the output of the Tailscale ping command to update the heartbeat.
|
||||
*
|
||||
* @param {string} tailscaleOutput - The output of the Tailscale ping command.
|
||||
* @param {Object} heartbeat - The heartbeat object to update.
|
||||
* @throws Will throw an eror if the output contains any unexpected string.
|
||||
*/
|
||||
parseTailscaleOutput(tailscaleOutput, heartbeat) {
|
||||
let lines = tailscaleOutput.split("\n");
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.includes("pong from")) {
|
||||
heartbeat.status = UP;
|
||||
let time = line.split(" in ")[1].split(" ")[0];
|
||||
heartbeat.ping = parseInt(time);
|
||||
heartbeat.msg = line;
|
||||
break;
|
||||
} else if (line.includes("timed out")) {
|
||||
throw new Error(`Ping timed out: "${line}"`);
|
||||
// Immediately throws upon "timed out" message, the server is expected to re-call the check function
|
||||
} else if (line.includes("no matching peer")) {
|
||||
throw new Error(`Nonexistant or inaccessible due to ACLs: "${line}"`);
|
||||
} else if (line.includes("is local Tailscale IP")) {
|
||||
throw new Error(`Tailscale only works if used on other machines: "${line}"`);
|
||||
} else if (line !== "") {
|
||||
throw new Error(`Unexpected output: "${line}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TailscalePing,
|
||||
};
|
119
server/notification-providers/nostr.js
Normal file
119
server/notification-providers/nostr.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const { log } = require("../../src/util");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const {
|
||||
relayInit,
|
||||
getPublicKey,
|
||||
getEventHash,
|
||||
getSignature,
|
||||
nip04,
|
||||
nip19
|
||||
} = require("nostr-tools");
|
||||
|
||||
// polyfills for node versions
|
||||
const semver = require("semver");
|
||||
const nodeVersion = process.version;
|
||||
if (semver.lt(nodeVersion, "16.0.0")) {
|
||||
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
|
||||
} else if (semver.lt(nodeVersion, "18.0.0")) {
|
||||
// polyfills for node 16
|
||||
global.crypto = require("crypto");
|
||||
global.WebSocket = require("isomorphic-ws");
|
||||
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
|
||||
crypto.subtle = crypto.webcrypto.subtle;
|
||||
}
|
||||
} else if (semver.lt(nodeVersion, "20.0.0")) {
|
||||
// polyfills for node 18
|
||||
global.crypto = require("crypto");
|
||||
global.WebSocket = require("isomorphic-ws");
|
||||
} else {
|
||||
// polyfills for node 20
|
||||
global.WebSocket = require("isomorphic-ws");
|
||||
}
|
||||
|
||||
class Nostr extends NotificationProvider {
|
||||
name = "nostr";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
// All DMs should have same timestamp
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
|
||||
const senderPrivateKey = await this.getPrivateKey(notification.sender);
|
||||
const senderPublicKey = getPublicKey(senderPrivateKey);
|
||||
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
|
||||
|
||||
// Create NIP-04 encrypted direct message event for each recipient
|
||||
const events = [];
|
||||
for (const recipientPublicKey of recipientsPublicKeys) {
|
||||
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
|
||||
let event = {
|
||||
kind: 4,
|
||||
pubkey: senderPublicKey,
|
||||
created_at: createdAt,
|
||||
tags: [[ "p", recipientPublicKey ]],
|
||||
content: ciphertext,
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = getSignature(event, senderPrivateKey);
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
// Publish events to each relay
|
||||
const relays = notification.relays.split("\n");
|
||||
let successfulRelays = 0;
|
||||
|
||||
// Connect to each relay
|
||||
for (const relayUrl of relays) {
|
||||
const relay = relayInit(relayUrl);
|
||||
try {
|
||||
await relay.connect();
|
||||
successfulRelays++;
|
||||
|
||||
// Publish events
|
||||
for (const event of events) {
|
||||
relay.publish(event);
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
} finally {
|
||||
relay.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Report success or failure
|
||||
if (successfulRelays === 0) {
|
||||
throw Error("Failed to connect to any relays.");
|
||||
}
|
||||
return `${successfulRelays}/${relays.length} relays connected.`;
|
||||
}
|
||||
|
||||
async getPrivateKey(sender) {
|
||||
try {
|
||||
const senderDecodeResult = await nip19.decode(sender);
|
||||
const { data } = senderDecodeResult;
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get private key: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getPublicKeys(recipients) {
|
||||
const recipientsList = recipients.split("\n");
|
||||
const publicKeys = [];
|
||||
for (const recipient of recipientsList) {
|
||||
try {
|
||||
const recipientDecodeResult = await nip19.decode(recipient);
|
||||
const { type, data } = recipientDecodeResult;
|
||||
if (type === "npub") {
|
||||
publicKeys.push(data);
|
||||
} else {
|
||||
throw new Error("not an npub");
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Error decoding recipient: ${error}`);
|
||||
}
|
||||
}
|
||||
return publicKeys;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Nostr;
|
@@ -13,7 +13,7 @@ class SMTP extends NotificationProvider {
|
||||
port: notification.smtpPort,
|
||||
secure: notification.smtpSecure,
|
||||
tls: {
|
||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -21,6 +21,7 @@ const LineNotify = require("./notification-providers/linenotify");
|
||||
const LunaSea = require("./notification-providers/lunasea");
|
||||
const Matrix = require("./notification-providers/matrix");
|
||||
const Mattermost = require("./notification-providers/mattermost");
|
||||
const Nostr = require("./notification-providers/nostr");
|
||||
const Ntfy = require("./notification-providers/ntfy");
|
||||
const Octopush = require("./notification-providers/octopush");
|
||||
const OneBot = require("./notification-providers/onebot");
|
||||
@@ -84,6 +85,7 @@ class Notification {
|
||||
new LunaSea(),
|
||||
new Matrix(),
|
||||
new Mattermost(),
|
||||
new Nostr(),
|
||||
new Ntfy(),
|
||||
new Octopush(),
|
||||
new OneBot(),
|
||||
|
@@ -49,6 +49,7 @@ if (! process.env.NODE_ENV) {
|
||||
}
|
||||
|
||||
log.info("server", "Node Env: " + process.env.NODE_ENV);
|
||||
log.info("server", "Inside Container: " + process.env.UPTIME_KUMA_IS_CONTAINER === "1");
|
||||
|
||||
log.info("server", "Importing Node libraries");
|
||||
const fs = require("fs");
|
||||
@@ -1589,6 +1590,8 @@ let needSetup = false;
|
||||
await shutdownFunction();
|
||||
});
|
||||
|
||||
server.start();
|
||||
|
||||
server.httpServer.listen(port, hostname, () => {
|
||||
if (hostname) {
|
||||
log.info("server", `Listening on ${hostname}:${port}`);
|
||||
|
@@ -10,6 +10,7 @@ const util = require("util");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
const childProcess = require("child_process");
|
||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||
|
||||
/**
|
||||
@@ -99,6 +100,7 @@ class UptimeKumaServer {
|
||||
|
||||
// Set Monitor Types
|
||||
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||
|
||||
this.io = new Server(this.httpServer);
|
||||
}
|
||||
@@ -333,9 +335,49 @@ class UptimeKumaServer {
|
||||
dayjs.tz.setDefault(timezone);
|
||||
}
|
||||
|
||||
/** Stop the server */
|
||||
async stop() {
|
||||
/**
|
||||
* TODO: Listen logic should be moved to here
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async start() {
|
||||
this.startServices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async stop() {
|
||||
this.stopServices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start all system services (e.g. nscd)
|
||||
* For now, only used in Docker
|
||||
*/
|
||||
startServices() {
|
||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||
try {
|
||||
log.info("services", "Starting nscd");
|
||||
childProcess.execSync("sudo service nscd start", { stdio: "pipe" });
|
||||
} catch (e) {
|
||||
log.info("services", "Failed to start nscd");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all system services
|
||||
*/
|
||||
stopServices() {
|
||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||
try {
|
||||
log.info("services", "Stopping nscd");
|
||||
childProcess.execSync("sudo service nscd stop");
|
||||
} catch (e) {
|
||||
log.info("services", "Failed to stop nscd");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,3 +387,4 @@ module.exports = {
|
||||
|
||||
// Must be at the end to avoid circular dependencies
|
||||
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
|
||||
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||
|
Reference in New Issue
Block a user