Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 122 additions & 9 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { DockerHost } = require("../docker");
const Gamedig = require("gamedig");
const net = require("net");
const tls = require("tls");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const { UptimeCalculator } = require("../uptime-calculator");
Expand Down Expand Up @@ -63,13 +65,16 @@ class Monitor extends BeanModel {
if (showTags) {
obj.tags = await this.getTags();
}

if (certExpiry && (this.type === "http" || this.type === "keyword" || this.type === "json-query") && this.getURLProtocol() === "https:") {
if (certExpiry &&
(
(this.type === "http" || this.type === "keyword" || this.type === "json-query") && this.getURLProtocol() === "https:"
) ||
(this.type === "smtp" && this.smtpSecurity) // NEW: SMTP SSL or STARTTLS
) {
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
obj.validCert = validCert;
}

return obj;
}

Expand Down Expand Up @@ -355,7 +360,7 @@ class Monitor extends BeanModel {

let beatInterval = this.interval;

if (! beatInterval) {
if (!beatInterval) {
beatInterval = 1;
}

Expand Down Expand Up @@ -619,6 +624,112 @@ class Monitor extends BeanModel {
}

}
} else if (this.type === "smtp") {
// SMTP monitor: supports SMTPS (secure), STARTTLS, and plain SMTP.
// Checks TLS certificates and sets status, message, and ping time.
const startTime = dayjs().valueOf();
let smtpTlsInfo = {};

try {
if (this.smtpSecurity === "secure") {
const tlsSocket = tls.connect({
host: this.hostname,
port: this.port || 465,
servername: this.hostname,
rejectUnauthorized: !this.getIgnoreTls(),
});

await new Promise((resolve, reject) => {
tlsSocket.on("secureConnect", async () => {
try {
tlsSocket.write("EHLO uptime-kuma\r\n");

smtpTlsInfo = checkCertificate(tlsSocket);
smtpTlsInfo.valid = tlsSocket.authorized || false;
await this.handleTlsInfo(smtpTlsInfo);

tlsSocket.end();
bean.msg = "SMTPS connection established";
bean.status = UP;
resolve();
} catch (err) {
tlsSocket.destroy();
reject(err);
}
});

tlsSocket.on("error", (err) => {
tlsSocket.destroy();
reject(err);
});
});

} else if (this.smtpSecurity === "starttls") {
const socket = net.connect({ host: this.hostname,
port: this.port || 587 }, () => {
socket.write("EHLO uptime-kuma\r\n");
});

await new Promise((resolve, reject) => {
let buffer = "";

socket.on("data", async (data) => {
buffer += data.toString();
let lines = buffer.split("\r\n");
buffer = lines.pop() || "";

for (const line of lines) {
if (line.includes("STARTTLS")) {
socket.write("STARTTLS\r\n");
}

if (line.startsWith("220") && line.toLowerCase().includes("start tls")) {
const tlsSocket = tls.connect({
socket,
servername: this.hostname,
rejectUnauthorized: !this.getIgnoreTls(),
});

tlsSocket.on("secureConnect", async () => {
try {
tlsSocket.write("EHLO uptime-kuma\r\n");

smtpTlsInfo = checkCertificate(tlsSocket);
smtpTlsInfo.valid = tlsSocket.authorized || false;
await this.handleTlsInfo(smtpTlsInfo);

tlsSocket.end();
bean.msg = "SMTP STARTTLS connection established";
bean.status = UP;
resolve();
} catch (err) {
tlsSocket.destroy();
reject(err);
}
});

tlsSocket.on("error", (err) => {
tlsSocket.destroy();
reject(err);
});
}
}
});

socket.on("error", reject);
});

} else {
bean.msg = "SMTP connection established (no TLS)";
bean.status = UP;
}

bean.ping = dayjs().valueOf() - startTime;

} catch (e) {
bean.msg = "SMTP check failed: " + e.message;
bean.status = DOWN;
}

} else if (this.type === "port") {
bean.ping = await tcping(this.hostname, this.port);
Expand Down Expand Up @@ -993,7 +1104,7 @@ class Monitor extends BeanModel {

previousBeat = bean;

if (! this.isStop) {
if (!this.isStop) {
log.debug("monitor", `[${this.name}] SetTimeout for next check.`);

let intervalRemainingMs = Math.max(
Expand Down Expand Up @@ -1022,7 +1133,7 @@ class Monitor extends BeanModel {
UptimeKumaServer.errorLog(e, false);
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");

if (! this.isStop) {
if (!this.isStop) {
log.info("monitor", "Try to restart the monitor");
this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000);
}
Expand Down Expand Up @@ -1074,7 +1185,8 @@ class Monitor extends BeanModel {
let oauth2AuthHeader = {
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
};
options.headers = { ...(options.headers),
options.headers = {
...(options.headers),
...(oauth2AuthHeader)
};

Expand Down Expand Up @@ -1335,7 +1447,8 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
const heartbeatJSON = bean.toJSON();
const monitorData = [{ id: monitor.id,
const monitorData = [{
id: monitor.id,
active: monitor.active,
name: monitor.name
}];
Expand Down Expand Up @@ -1380,7 +1493,7 @@ class Monitor extends BeanModel {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this);

if (! notificationList.length > 0) {
if (!notificationList.length > 0) {
// fail fast. If no notification is set, all the following checks can be skipped.
log.debug("monitor", "No notification, no need to send cert notification");
return;
Expand Down
10 changes: 5 additions & 5 deletions src/modules/dayjs/plugin/timezone/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { PluginFunc, ConfigType } from 'dayjs/esm'
import { PluginFunc, ConfigType } from "dayjs/esm";

declare const plugin: PluginFunc
declare const plugin: PluginFunc;
export = plugin

declare module 'dayjs/esm' {
declare module "dayjs/esm" {
interface Dayjs {
tz(timezone?: string, keepLocalTime?: boolean): Dayjs
offsetName(type?: 'short' | 'long'): string | undefined
offsetName(type?: "short" | "long"): string | undefined
}

interface DayjsTimezone {
Expand All @@ -16,5 +16,5 @@ declare module 'dayjs/esm' {
setDefault(timezone?: string): void
}

const tz: DayjsTimezone
const tz: DayjsTimezone;
}
2 changes: 1 addition & 1 deletion src/pages/EditMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@

<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>

<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
<div v-if="monitor.type === 'http' ||monitor.type === 'smtp'|| monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox" :disabled="monitor.ignoreTls">
<label class="form-check-label" for="expiry-notification">
{{ $t("Certificate Expiry Notification") }}
Expand Down
Loading