Compare commits

..

23 Commits

Author SHA1 Message Date
Louis Lam
9c4c60e270 Check NFS 2024-07-06 23:56:56 +08:00
Louis Lam
6e30f71947 Follow up the Apprise issue (#4880) 2024-06-24 19:49:37 +08:00
Louis Lam
953058c6a5 Drop out the testing repo from apt (#4878) 2024-06-24 16:20:12 +08:00
Louis Lam
85c67b6866 Revert "Switch back to TryGhost/node-sqlite3 from louislam/node-sqlite3" (#4879) 2024-06-24 15:52:07 +08:00
Louis Lam
83969d2112 Update dependencies and embed axios-ntlm 1.3.0 into the project (#4877) 2024-06-24 02:08:39 +08:00
Louis Lam
dc15443716 Revert "Fix: Use retryInterval when a monitor is DOWN" (#4875) 2024-06-23 23:46:52 +08:00
Frank Elsinga
39c1283ba6 New notification provider: Threema Gateway (#4854) 2024-06-17 16:55:45 +02:00
booooza
add0ef7be0 Update threema provider translations to better match threema terminology 2024-06-17 10:16:38 +02:00
booooza
0960ec62b7 Adjust Threema notification view to conform with project standards 2024-06-17 08:46:08 +02:00
booooza
39b0c62c1d Refactor error handling to improve clarity and maintainability 2024-06-17 08:34:53 +02:00
booooza
2e5e103434 New notification provider: Threema Gateway 2024-06-14 12:25:19 +02:00
Frank Elsinga
fbf7b77ceb improved zoho-cliq message format (#4848) 2024-06-13 17:10:46 +02:00
Frank Elsinga
9f563adc1a fixed formatting mistakes 2024-06-13 17:04:38 +02:00
Frank Elsinga
c9132adfc7 made sure that the address extraction is extracted into its own method 2024-06-13 17:00:11 +02:00
Francesco M
bd95ccdc64 Update server/notification-providers/zoho-cliq.js
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-06-12 18:14:05 +02:00
Francesco M
82fb7b2816 improved zoho-cliq message format:
- show PORT (cloned switchcase from discord,squadcast,servenio)
- show monitorName in the first row to get on point also from messagge preview notification
- avoid duplication of monitorName and monitorName in the message content
2024-06-12 08:46:48 +02:00
Frank Elsinga
bc25b719db removed some monitor drift in terms of code comments (#4820) 2024-06-07 16:03:12 +02:00
Frank Elsinga
b6cd21c71a Removed where I was wrong about imports 2024-06-07 15:54:28 +02:00
Frank Elsinga
d74facded6 removed some monitor drift in terms of imports and documentation 2024-06-04 05:01:53 +02:00
Frank Elsinga
4794f9eb0b Removed the last reminents of cypress (#4819) 2024-06-04 03:09:52 +02:00
Frank Elsinga
77d82ec30f removed the last reminents of cypress 2024-06-04 03:02:34 +02:00
Frank Elsinga
c7b83e729b Fixed smspartner not having fully working translations (#4816) 2024-06-04 01:28:33 +02:00
Frank Elsinga
f43fe53d28 Fixed smspartner not having fully working translations 2024-06-04 01:10:20 +02:00
38 changed files with 4084 additions and 2327 deletions

View File

@@ -1,7 +1,6 @@
/.idea
/node_modules
/data*
/cypress
/out
/test
/kubernetes

View File

@@ -1,8 +1,7 @@
module.exports = {
ignorePatterns: [
"test/*.js",
"test/cypress",
"server/modules/apicache/*",
"server/modules/*",
"src/util.js"
],
root: true,

View File

@@ -1,28 +0,0 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
projectId: "vyjuem",
e2e: {
experimentalStudio: true,
setupNodeEvents(on, config) {
},
fixturesFolder: "test/cypress/fixtures",
screenshotsFolder: "test/cypress/screenshots",
videosFolder: "test/cypress/videos",
downloadsFolder: "test/cypress/downloads",
supportFile: "test/cypress/support/e2e.js",
baseUrl: "http://localhost:3002",
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
viewportWidth: 1920,
viewportHeight: 1080,
specPattern: [
"test/cypress/e2e/setup.cy.js",
"test/cypress/e2e/**/*.js"
],
},
env: {
baseUrl: "http://localhost:3002",
},
});

View File

@@ -1,10 +0,0 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
supportFile: false,
specPattern: [
"test/cypress/unit/**/*.js"
],
}
});

View File

@@ -1,14 +0,0 @@
exports.up = function (knex) {
// Insert column for custom HTML code
return knex.schema
.alterTable("status_page", function (table) {
table.text("custom_html").nullable().defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("status_page", function (table) {
table.dropColumn("custom_html");
});
};

View File

@@ -3,7 +3,6 @@ FROM node:20-bookworm-slim AS base2-slim
ARG TARGETPLATFORM
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
# apprise = for notifications (From testing repo)
# sqlite3 = for debugging
# iputils-ping = for ping
# util-linux = for setpriv (Should be dropped in 2.0.0?)
@@ -12,10 +11,10 @@ ARG TARGETPLATFORM
# ca-certificates = keep the cert up-to-date
# sudo = for start service nscd with non-root user
# nscd = for better DNS caching
RUN echo "deb http://deb.debian.org/debian testing main" >> /etc/apt/sources.list && \
apt update && \
apt --yes --no-install-recommends -t testing install apprise sqlite3 ca-certificates && \
apt --yes --no-install-recommends -t stable install \
RUN apt update && \
apt --yes --no-install-recommends install \
sqlite3 \
ca-certificates \
iputils-ping \
util-linux \
dumb-init \
@@ -25,6 +24,15 @@ RUN echo "deb http://deb.debian.org/debian testing main" >> /etc/apt/sources.lis
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
# apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867)
# Switching to testing repo is no longer working, as the testing repo is not bookworm anymore.
# python3-paho-mqtt (#4859)
RUN curl http://ftp.debian.org/debian/pool/main/a/apprise/apprise_1.8.0-2_all.deb --output apprise.deb && \
apt update && \
apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \
rm -rf /var/lib/apt/lists/* && \
rm -f apprise.deb && \
apt --yes autoremove
# Install cloudflared
RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
@@ -42,7 +50,9 @@ COPY ./docker/etc/sudoers /etc/sudoers
# Full Base Image
# MariaDB, Chromium and fonts
FROM base2-slim AS base2
# Make sure to reuse the slim image here. Uncomment the above line if you want to build it from scratch.
# FROM base2-slim AS base2
FROM louislam/uptime-kuma:base2-slim AS base2
ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1
RUN apt update && \
apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \

5343
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -76,10 +76,10 @@
"dependencies": {
"@grpc/grpc-js": "~1.7.3",
"@louislam/ping": "~0.4.4-mod.1",
"@louislam/sqlite3": "15.1.6",
"@vvo/tzdb": "^6.125.0",
"args-parser": "~1.3.0",
"axios": "~0.28.1",
"axios-ntlm": "1.3.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
"chardet": "~1.4.0",
@@ -91,6 +91,7 @@
"compression": "~1.7.4",
"croner": "~6.0.5",
"dayjs": "~1.11.5",
"dev-null": "^0.1.1",
"dotenv": "~16.0.3",
"express": "~4.19.2",
"express-basic-auth": "~1.2.1",
@@ -115,7 +116,7 @@
"mitt": "~3.0.1",
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mssql": "~11.0.0",
"mysql2": "~3.9.6",
"nanoid": "~3.3.4",
"node-cloudflared-tunnel": "~1.0.9",
@@ -136,10 +137,9 @@
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"semver": "~7.5.4",
"socket.io": "~4.6.1",
"socket.io-client": "~4.6.1",
"socket.io": "~4.7.5",
"socket.io-client": "~4.7.5",
"socks-proxy-agent": "6.1.1",
"sqlite3": "~5.1.7",
"tar": "~6.2.1",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",

View File

@@ -223,8 +223,20 @@ class Database {
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
}
// Check if Database.sqlitePath is on NFS
if (fs.existsSync(Database.sqlitePath)) {
let stats = fs.statSync(Database.sqlitePath);
log.debug("server", "SQLite database inode: " + stats.ino);
if (stats.ino === 0) {
log.error("server", "It seems that the database is on a network drive (NFS). Uptime Kuma will be UNSTABLE and the database will be CORRUPTED. Please use a local disk.");
}
}
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => require("@louislam/sqlite3");
config = {
client: "sqlite3",
client: Dialect,
connection: {
filename: Database.sqlitePath,
acquireConnectionTimeout: acquireConnectionTimeout,

View File

@@ -897,6 +897,7 @@ class Monitor extends BeanModel {
retries = 0;
} catch (error) {
if (error?.name === "CanceledError") {
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
} else {
@@ -969,7 +970,6 @@ class Monitor extends BeanModel {
} else if (bean.status === MAINTENANCE) {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
} else {
beatInterval = this.retryInterval;
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
}

View File

@@ -66,10 +66,6 @@ class StatusPage extends BeanModel {
head.append($(escapedGoogleAnalyticsScript));
}
if (process.env.UPTIME_KUMA_ALLOW_CUSTOM_HTML === "1") {
head.append(statusPage.customHtml);
}
// OG Meta Tags
let ogTitle = $("<meta property=\"og:title\" content=\"\" />").attr("content", statusPage.title);
head.append(ogTitle);
@@ -251,7 +247,6 @@ class StatusPage extends BeanModel {
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
customHtml: this.custom_html
};
}
@@ -275,7 +270,6 @@ class StatusPage extends BeanModel {
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
customHtml: this.custom_html
};
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 CatButtes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,77 @@
'use strict';
// Original file https://raw.githubusercontent.com/elasticio/node-ntlm-client/master/lib/flags.js
module.exports.NTLMFLAG_NEGOTIATE_UNICODE = 1 << 0;
/* Indicates that Unicode strings are supported for use in security buffer
data. */
module.exports.NTLMFLAG_NEGOTIATE_OEM = 1 << 1;
/* Indicates that OEM strings are supported for use in security buffer data. */
module.exports.NTLMFLAG_REQUEST_TARGET = 1 << 2;
/* Requests that the server's authentication realm be included in the Type 2
message. */
/* unknown (1<<3) */
module.exports.NTLMFLAG_NEGOTIATE_SIGN = 1 << 4;
/* Specifies that authenticated communication between the client and server
should carry a digital signature (message integrity). */
module.exports.NTLMFLAG_NEGOTIATE_SEAL = 1 << 5;
/* Specifies that authenticated communication between the client and server
should be encrypted (message confidentiality). */
module.exports.NTLMFLAG_NEGOTIATE_DATAGRAM_STYLE = 1 << 6;
/* Indicates that datagram authentication is being used. */
module.exports.NTLMFLAG_NEGOTIATE_LM_KEY = 1 << 7;
/* Indicates that the LAN Manager session key should be used for signing and
sealing authenticated communications. */
module.exports.NTLMFLAG_NEGOTIATE_NETWARE = 1 << 8;
/* unknown purpose */
module.exports.NTLMFLAG_NEGOTIATE_NTLM_KEY = 1 << 9;
/* Indicates that NTLM authentication is being used. */
/* unknown (1<<10) */
module.exports.NTLMFLAG_NEGOTIATE_ANONYMOUS = 1 << 11;
/* Sent by the client in the Type 3 message to indicate that an anonymous
context has been established. This also affects the response fields. */
module.exports.NTLMFLAG_NEGOTIATE_DOMAIN_SUPPLIED = 1 << 12;
/* Sent by the client in the Type 1 message to indicate that a desired
authentication realm is included in the message. */
module.exports.NTLMFLAG_NEGOTIATE_WORKSTATION_SUPPLIED = 1 << 13;
/* Sent by the client in the Type 1 message to indicate that the client
workstation's name is included in the message. */
module.exports.NTLMFLAG_NEGOTIATE_LOCAL_CALL = 1 << 14;
/* Sent by the server to indicate that the server and client are on the same
machine. Implies that the client may use a pre-established local security
context rather than responding to the challenge. */
module.exports.NTLMFLAG_NEGOTIATE_ALWAYS_SIGN = 1 << 15;
/* Indicates that authenticated communication between the client and server
should be signed with a "dummy" signature. */
module.exports.NTLMFLAG_TARGET_TYPE_DOMAIN = 1 << 16;
/* Sent by the server in the Type 2 message to indicate that the target
authentication realm is a domain. */
module.exports.NTLMFLAG_TARGET_TYPE_SERVER = 1 << 17;
/* Sent by the server in the Type 2 message to indicate that the target
authentication realm is a server. */
module.exports.NTLMFLAG_TARGET_TYPE_SHARE = 1 << 18;
/* Sent by the server in the Type 2 message to indicate that the target
authentication realm is a share. Presumably, this is for share-level
authentication. Usage is unclear. */
module.exports.NTLMFLAG_NEGOTIATE_NTLM2_KEY = 1 << 19;
/* Indicates that the NTLM2 signing and sealing scheme should be used for
protecting authenticated communications. */
module.exports.NTLMFLAG_REQUEST_INIT_RESPONSE = 1 << 20;
/* unknown purpose */
module.exports.NTLMFLAG_REQUEST_ACCEPT_RESPONSE = 1 << 21;
/* unknown purpose */
module.exports.NTLMFLAG_REQUEST_NONNT_SESSION_KEY = 1 << 22;
/* unknown purpose */
module.exports.NTLMFLAG_NEGOTIATE_TARGET_INFO = 1 << 23;
/* Sent by the server in the Type 2 message to indicate that it is including a
Target Information block in the message. */
/* unknown (1<24) */
/* unknown (1<25) */
/* unknown (1<26) */
/* unknown (1<27) */
/* unknown (1<28) */
module.exports.NTLMFLAG_NEGOTIATE_128 = 1 << 29;
/* Indicates that 128-bit encryption is supported. */
module.exports.NTLMFLAG_NEGOTIATE_KEY_EXCHANGE = 1 << 30;
/* Indicates that the client will provide an encrypted master key in
the "Session Key" field of the Type 3 message. */
module.exports.NTLMFLAG_NEGOTIATE_56 = 1 << 31;
//# sourceMappingURL=flags.js.map

View File

@@ -0,0 +1,122 @@
'use strict';
// Original source at https://github.com/elasticio/node-ntlm-client/blob/master/lib/hash.js
var crypto = require('crypto');
function createLMResponse(challenge, lmhash) {
var buf = new Buffer.alloc(24), pwBuffer = new Buffer.alloc(21).fill(0);
lmhash.copy(pwBuffer);
calculateDES(pwBuffer.slice(0, 7), challenge).copy(buf);
calculateDES(pwBuffer.slice(7, 14), challenge).copy(buf, 8);
calculateDES(pwBuffer.slice(14), challenge).copy(buf, 16);
return buf;
}
function createLMHash(password) {
var buf = new Buffer.alloc(16), pwBuffer = new Buffer.alloc(14), magicKey = new Buffer.from('KGS!@#$%', 'ascii');
if (password.length > 14) {
buf.fill(0);
return buf;
}
pwBuffer.fill(0);
pwBuffer.write(password.toUpperCase(), 0, 'ascii');
return Buffer.concat([
calculateDES(pwBuffer.slice(0, 7), magicKey),
calculateDES(pwBuffer.slice(7), magicKey)
]);
}
function calculateDES(key, message) {
var desKey = new Buffer.alloc(8);
desKey[0] = key[0] & 0xFE;
desKey[1] = ((key[0] << 7) & 0xFF) | (key[1] >> 1);
desKey[2] = ((key[1] << 6) & 0xFF) | (key[2] >> 2);
desKey[3] = ((key[2] << 5) & 0xFF) | (key[3] >> 3);
desKey[4] = ((key[3] << 4) & 0xFF) | (key[4] >> 4);
desKey[5] = ((key[4] << 3) & 0xFF) | (key[5] >> 5);
desKey[6] = ((key[5] << 2) & 0xFF) | (key[6] >> 6);
desKey[7] = (key[6] << 1) & 0xFF;
for (var i = 0; i < 8; i++) {
var parity = 0;
for (var j = 1; j < 8; j++) {
parity += (desKey[i] >> j) % 2;
}
desKey[i] |= (parity % 2) === 0 ? 1 : 0;
}
var des = crypto.createCipheriv('DES-ECB', desKey, '');
return des.update(message);
}
function createNTLMResponse(challenge, ntlmhash) {
var buf = new Buffer.alloc(24), ntlmBuffer = new Buffer.alloc(21).fill(0);
ntlmhash.copy(ntlmBuffer);
calculateDES(ntlmBuffer.slice(0, 7), challenge).copy(buf);
calculateDES(ntlmBuffer.slice(7, 14), challenge).copy(buf, 8);
calculateDES(ntlmBuffer.slice(14), challenge).copy(buf, 16);
return buf;
}
function createNTLMHash(password) {
var md4sum = crypto.createHash('md4');
md4sum.update(new Buffer.from(password, 'ucs2'));
return md4sum.digest();
}
function createNTLMv2Hash(ntlmhash, username, authTargetName) {
var hmac = crypto.createHmac('md5', ntlmhash);
hmac.update(new Buffer.from(username.toUpperCase() + authTargetName, 'ucs2'));
return hmac.digest();
}
function createLMv2Response(type2message, username, ntlmhash, nonce, targetName) {
var buf = new Buffer.alloc(24), ntlm2hash = createNTLMv2Hash(ntlmhash, username, targetName), hmac = crypto.createHmac('md5', ntlm2hash);
//server challenge
type2message.challenge.copy(buf, 8);
//client nonce
buf.write(nonce || createPseudoRandomValue(16), 16, 'hex');
//create hash
hmac.update(buf.slice(8));
var hashedBuffer = hmac.digest();
hashedBuffer.copy(buf);
return buf;
}
function createNTLMv2Response(type2message, username, ntlmhash, nonce, targetName) {
var buf = new Buffer.alloc(48 + type2message.targetInfo.buffer.length), ntlm2hash = createNTLMv2Hash(ntlmhash, username, targetName), hmac = crypto.createHmac('md5', ntlm2hash);
//the first 8 bytes are spare to store the hashed value before the blob
//server challenge
type2message.challenge.copy(buf, 8);
//blob signature
buf.writeUInt32BE(0x01010000, 16);
//reserved
buf.writeUInt32LE(0, 20);
//timestamp
//TODO: we are loosing precision here since js is not able to handle those large integers
// maybe think about a different solution here
// 11644473600000 = diff between 1970 and 1601
var timestamp = ((Date.now() + 11644473600000) * 10000).toString(16);
var timestampLow = Number('0x' + timestamp.substring(Math.max(0, timestamp.length - 8)));
var timestampHigh = Number('0x' + timestamp.substring(0, Math.max(0, timestamp.length - 8)));
buf.writeUInt32LE(timestampLow, 24, false);
buf.writeUInt32LE(timestampHigh, 28, false);
//random client nonce
buf.write(nonce || createPseudoRandomValue(16), 32, 'hex');
//zero
buf.writeUInt32LE(0, 40);
//complete target information block from type 2 message
type2message.targetInfo.buffer.copy(buf, 44);
//zero
buf.writeUInt32LE(0, 44 + type2message.targetInfo.buffer.length);
hmac.update(buf.slice(8));
var hashedBuffer = hmac.digest();
hashedBuffer.copy(buf);
return buf;
}
function createPseudoRandomValue(length) {
var str = '';
while (str.length < length) {
str += Math.floor(Math.random() * 16).toString(16);
}
return str;
}
module.exports = {
createLMHash: createLMHash,
createNTLMHash: createNTLMHash,
createLMResponse: createLMResponse,
createNTLMResponse: createNTLMResponse,
createLMv2Response: createLMv2Response,
createNTLMv2Response: createNTLMv2Response,
createPseudoRandomValue: createPseudoRandomValue
};
//# sourceMappingURL=hash.js.map

View File

@@ -0,0 +1,220 @@
'use strict';
// Original file https://raw.githubusercontent.com/elasticio/node-ntlm-client/master/lib/ntlm.js
var os = require('os'), flags = require('./flags'), hash = require('./hash');
var NTLMSIGNATURE = "NTLMSSP\0";
function createType1Message(workstation, target) {
var dataPos = 32, pos = 0, buf = new Buffer.alloc(1024);
workstation = workstation === undefined ? os.hostname() : workstation;
target = target === undefined ? '' : target;
//signature
buf.write(NTLMSIGNATURE, pos, NTLMSIGNATURE.length, 'ascii');
pos += NTLMSIGNATURE.length;
//message type
buf.writeUInt32LE(1, pos);
pos += 4;
//flags
buf.writeUInt32LE(flags.NTLMFLAG_NEGOTIATE_OEM |
flags.NTLMFLAG_REQUEST_TARGET |
flags.NTLMFLAG_NEGOTIATE_NTLM_KEY |
flags.NTLMFLAG_NEGOTIATE_NTLM2_KEY |
flags.NTLMFLAG_NEGOTIATE_ALWAYS_SIGN, pos);
pos += 4;
//domain security buffer
buf.writeUInt16LE(target.length, pos);
pos += 2;
buf.writeUInt16LE(target.length, pos);
pos += 2;
buf.writeUInt32LE(target.length === 0 ? 0 : dataPos, pos);
pos += 4;
if (target.length > 0) {
dataPos += buf.write(target, dataPos, 'ascii');
}
//workstation security buffer
buf.writeUInt16LE(workstation.length, pos);
pos += 2;
buf.writeUInt16LE(workstation.length, pos);
pos += 2;
buf.writeUInt32LE(workstation.length === 0 ? 0 : dataPos, pos);
pos += 4;
if (workstation.length > 0) {
dataPos += buf.write(workstation, dataPos, 'ascii');
}
return 'NTLM ' + buf.toString('base64', 0, dataPos);
}
function decodeType2Message(str) {
if (str === undefined) {
throw new Error('Invalid argument');
}
//convenience
if (Object.prototype.toString.call(str) !== '[object String]') {
if (str.hasOwnProperty('headers') && str.headers.hasOwnProperty('www-authenticate')) {
str = str.headers['www-authenticate'];
}
else {
throw new Error('Invalid argument');
}
}
var ntlmMatch = /^NTLM ([^,\s]+)/.exec(str);
if (ntlmMatch) {
str = ntlmMatch[1];
}
var buf = new Buffer.from(str, 'base64'), obj = {};
//check signature
if (buf.toString('ascii', 0, NTLMSIGNATURE.length) !== NTLMSIGNATURE) {
throw new Error('Invalid message signature: ' + str);
}
//check message type
if (buf.readUInt32LE(NTLMSIGNATURE.length) !== 2) {
throw new Error('Invalid message type (no type 2)');
}
//read flags
obj.flags = buf.readUInt32LE(20);
obj.encoding = (obj.flags & flags.NTLMFLAG_NEGOTIATE_OEM) ? 'ascii' : 'ucs2';
obj.version = (obj.flags & flags.NTLMFLAG_NEGOTIATE_NTLM2_KEY) ? 2 : 1;
obj.challenge = buf.slice(24, 32);
//read target name
obj.targetName = (function () {
var length = buf.readUInt16LE(12);
//skipping allocated space
var offset = buf.readUInt32LE(16);
if (length === 0) {
return '';
}
if ((offset + length) > buf.length || offset < 32) {
throw new Error('Bad type 2 message');
}
return buf.toString(obj.encoding, offset, offset + length);
})();
//read target info
if (obj.flags & flags.NTLMFLAG_NEGOTIATE_TARGET_INFO) {
obj.targetInfo = (function () {
var info = {};
var length = buf.readUInt16LE(40);
//skipping allocated space
var offset = buf.readUInt32LE(44);
var targetInfoBuffer = new Buffer.alloc(length);
buf.copy(targetInfoBuffer, 0, offset, offset + length);
if (length === 0) {
return info;
}
if ((offset + length) > buf.length || offset < 32) {
throw new Error('Bad type 2 message');
}
var pos = offset;
while (pos < (offset + length)) {
var blockType = buf.readUInt16LE(pos);
pos += 2;
var blockLength = buf.readUInt16LE(pos);
pos += 2;
if (blockType === 0) {
//reached the terminator subblock
break;
}
var blockTypeStr = void 0;
switch (blockType) {
case 1:
blockTypeStr = 'SERVER';
break;
case 2:
blockTypeStr = 'DOMAIN';
break;
case 3:
blockTypeStr = 'FQDN';
break;
case 4:
blockTypeStr = 'DNS';
break;
case 5:
blockTypeStr = 'PARENT_DNS';
break;
default:
blockTypeStr = '';
break;
}
if (blockTypeStr) {
info[blockTypeStr] = buf.toString('ucs2', pos, pos + blockLength);
}
pos += blockLength;
}
return {
parsed: info,
buffer: targetInfoBuffer
};
})();
}
return obj;
}
function createType3Message(type2Message, username, password, workstation, target) {
var dataPos = 52, buf = new Buffer.alloc(1024);
if (workstation === undefined) {
workstation = os.hostname();
}
if (target === undefined) {
target = type2Message.targetName;
}
//signature
buf.write(NTLMSIGNATURE, 0, NTLMSIGNATURE.length, 'ascii');
//message type
buf.writeUInt32LE(3, 8);
if (type2Message.version === 2) {
dataPos = 64;
var ntlmHash = hash.createNTLMHash(password), nonce = hash.createPseudoRandomValue(16), lmv2 = hash.createLMv2Response(type2Message, username, ntlmHash, nonce, target), ntlmv2 = hash.createNTLMv2Response(type2Message, username, ntlmHash, nonce, target);
//lmv2 security buffer
buf.writeUInt16LE(lmv2.length, 12);
buf.writeUInt16LE(lmv2.length, 14);
buf.writeUInt32LE(dataPos, 16);
lmv2.copy(buf, dataPos);
dataPos += lmv2.length;
//ntlmv2 security buffer
buf.writeUInt16LE(ntlmv2.length, 20);
buf.writeUInt16LE(ntlmv2.length, 22);
buf.writeUInt32LE(dataPos, 24);
ntlmv2.copy(buf, dataPos);
dataPos += ntlmv2.length;
}
else {
var lmHash = hash.createLMHash(password), ntlmHash = hash.createNTLMHash(password), lm = hash.createLMResponse(type2Message.challenge, lmHash), ntlm = hash.createNTLMResponse(type2Message.challenge, ntlmHash);
//lm security buffer
buf.writeUInt16LE(lm.length, 12);
buf.writeUInt16LE(lm.length, 14);
buf.writeUInt32LE(dataPos, 16);
lm.copy(buf, dataPos);
dataPos += lm.length;
//ntlm security buffer
buf.writeUInt16LE(ntlm.length, 20);
buf.writeUInt16LE(ntlm.length, 22);
buf.writeUInt32LE(dataPos, 24);
ntlm.copy(buf, dataPos);
dataPos += ntlm.length;
}
//target name security buffer
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? target.length : target.length * 2, 28);
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? target.length : target.length * 2, 30);
buf.writeUInt32LE(dataPos, 32);
dataPos += buf.write(target, dataPos, type2Message.encoding);
//user name security buffer
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? username.length : username.length * 2, 36);
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? username.length : username.length * 2, 38);
buf.writeUInt32LE(dataPos, 40);
dataPos += buf.write(username, dataPos, type2Message.encoding);
//workstation name security buffer
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? workstation.length : workstation.length * 2, 44);
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? workstation.length : workstation.length * 2, 46);
buf.writeUInt32LE(dataPos, 48);
dataPos += buf.write(workstation, dataPos, type2Message.encoding);
if (type2Message.version === 2) {
//session key security buffer
buf.writeUInt16LE(0, 52);
buf.writeUInt16LE(0, 54);
buf.writeUInt32LE(0, 56);
//flags
buf.writeUInt32LE(type2Message.flags, 60);
}
return 'NTLM ' + buf.toString('base64', 0, dataPos);
}
module.exports = {
createType1Message: createType1Message,
decodeType2Message: decodeType2Message,
createType3Message: createType3Message
};
//# sourceMappingURL=ntlm.js.map

View File

@@ -0,0 +1,127 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NtlmClient = void 0;
var axios_1 = __importDefault(require("axios"));
var ntlm = __importStar(require("./ntlm"));
var https = __importStar(require("https"));
var http = __importStar(require("http"));
var dev_null_1 = __importDefault(require("dev-null"));
/**
* @param credentials An NtlmCredentials object containing the username and password
* @param AxiosConfig The Axios config for the instance you wish to create
*
* @returns This function returns an axios instance configured to use the provided credentials
*/
function NtlmClient(credentials, AxiosConfig) {
var _this = this;
var config = AxiosConfig !== null && AxiosConfig !== void 0 ? AxiosConfig : {};
if (!config.httpAgent) {
config.httpAgent = new http.Agent({ keepAlive: true });
}
if (!config.httpsAgent) {
config.httpsAgent = new https.Agent({ keepAlive: true });
}
var client = axios_1.default.create(config);
client.interceptors.response.use(function (response) {
return response;
}, function (err) { return __awaiter(_this, void 0, void 0, function () {
var error, t1Msg, t2Msg, t3Msg, stream_1;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
error = err.response;
if (!(error && error.status === 401
&& error.headers['www-authenticate']
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
// This length check is a hack because SharePoint is awkward and will
// include the Negotiate option when responding with the T2 message
// There is nore we could do to ensure we are processing correctly,
// but this is the easiest option for now
if (error.headers['www-authenticate'].length < 50) {
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
error.config.headers["Authorization"] = t1Msg;
}
else {
t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
error.config.headers["X-retry"] = "false";
error.config.headers["Authorization"] = t3Msg;
}
if (!(error.config.responseType === "stream")) return [3 /*break*/, 2];
stream_1 = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data;
if (!(stream_1 && !stream_1.readableEnded)) return [3 /*break*/, 2];
return [4 /*yield*/, new Promise(function (resolve) {
stream_1.pipe((0, dev_null_1.default)());
stream_1.once('close', resolve);
})];
case 1:
_b.sent();
_b.label = 2;
case 2: return [2 /*return*/, client(error.config)];
case 3: throw err;
}
});
}); });
return client;
}
exports.NtlmClient = NtlmClient;
//# sourceMappingURL=ntlmClient.js.map

View File

@@ -5,7 +5,6 @@ const { dnsResolve } = require("../util-server");
const { R } = require("redbean-node");
class DnsMonitorType extends MonitorType {
name = "dns";
/**

View File

@@ -4,7 +4,6 @@ const { MongoClient } = require("mongodb");
const jsonata = require("jsonata");
class MongodbMonitorType extends MonitorType {
name = "mongodb";
/**
@@ -49,8 +48,7 @@ class MongodbMonitorType extends MonitorType {
* Connect to and run MongoDB command on a MongoDB database
* @param {string} connectionString The database connection string
* @param {object} command MongoDB command to run on the database
* @returns {Promise<(string[] | object[] | object)>} Response from
* server
* @returns {Promise<(string[] | object[] | object)>} Response from server
*/
async runMongodbCommand(connectionString, command) {
let client = await MongoClient.connect(connectionString);

View File

@@ -11,7 +11,6 @@ class MonitorType {
async check(monitor, heartbeat, server) {
throw new Error("You need to override check()");
}
}
module.exports = {

View File

@@ -4,15 +4,10 @@ const mqtt = require("mqtt");
const jsonata = require("jsonata");
class MqttMonitorType extends MonitorType {
name = "mqtt";
/**
* Run the monitoring check on the MQTT monitor
* @param {Monitor} monitor Monitor to check
* @param {Heartbeat} heartbeat Monitor heartbeat to update
* @param {UptimeKumaServer} server Uptime Kuma server
* @returns {Promise<void>}
* @inheritdoc
*/
async check(monitor, heartbeat, server) {
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {

View File

@@ -2,23 +2,13 @@ const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const childProcessAsync = require("promisify-child-process");
/**
* 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.
* @returns {Promise<void>}
* @throws Error if checking Tailscale ping encounters any error
* @inheritdoc
*/
async check(monitor, heartbeat) {
async check(monitor, heartbeat, _server) {
try {
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
this.parseTailscaleOutput(tailscaleOutput, heartbeat);

View File

@@ -33,26 +33,6 @@ class Discord extends NotificationProvider {
return okMsg;
}
let address;
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "gamedig":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
address = monitorJSON["url"];
break;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] === DOWN) {
let discorddowndata = {
@@ -68,7 +48,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
value: this.extractAdress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,
@@ -105,7 +85,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
value: this.extractAdress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,

View File

@@ -19,6 +19,36 @@ class NotificationProvider {
throw new Error("Have to override Notification.send(...)");
}
/**
* Extracts the address from a monitor JSON object based on its type.
* @param {?object} monitorJSON Monitor details (For Up/Down only)
* @returns {string} The extracted address based on the monitor type.
*/
extractAdress(monitorJSON) {
if (!monitorJSON) {
return "";
}
switch (monitorJSON["type"]) {
case "push":
return "Heartbeat";
case "ping":
return monitorJSON["hostname"];
case "port":
case "dns":
case "gamedig":
case "steam":
if (monitorJSON["port"]) {
return monitorJSON["hostname"] + ":" + monitorJSON["port"];
}
return monitorJSON["hostname"];
default:
if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) {
return monitorJSON["url"];
}
return "";
}
}
/**
* Throws an error
* @param {any} error The error to throw

View File

@@ -32,28 +32,7 @@ class SevenIO extends NotificationProvider {
return okMsg;
}
let address = "";
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "gamedig":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) {
address = monitorJSON["url"];
}
break;
}
let address = this.extractAdress(monitorJSON);
if (address !== "") {
address = `(${address}) `;
}

View File

@@ -93,12 +93,7 @@ class SMTP extends NotificationProvider {
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
}
monitorHostnameOrURL = this.extractAdress(monitorJSON);
}
let serviceStatus = "⚠️ Test";

View File

@@ -34,25 +34,7 @@ class Squadcast extends NotificationProvider {
data.status = "resolve";
}
let address;
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
address = monitorJSON["url"];
break;
}
data.tags["AlertAddress"] = address;
data.tags["AlertAddress"] = this.extractAdress(monitorJSON);
monitorJSON["tags"].forEach(tag => {
data.tags[tag["name"]] = {

View File

@@ -216,21 +216,6 @@ class Teams extends NotificationProvider {
return okMsg;
}
let monitorUrl;
switch (monitorJSON["type"]) {
case "http":
case "keywork":
monitorUrl = monitorJSON["url"];
break;
case "docker":
monitorUrl = monitorJSON["docker_host"];
break;
default:
monitorUrl = monitorJSON["hostname"];
break;
}
const baseURL = await setting("primaryBaseURL");
let dashboardUrl;
if (baseURL) {
@@ -240,7 +225,7 @@ class Teams extends NotificationProvider {
const payload = this._notificationPayloadFactory({
heartbeatJSON: heartbeatJSON,
monitorName: monitorJSON.name,
monitorUrl: monitorUrl,
monitorUrl: this.extractAdress(monitorJSON),
dashboardUrl: dashboardUrl,
});

View File

@@ -0,0 +1,77 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Threema extends NotificationProvider {
name = "threema";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const url = "https://msgapi.threema.ch/send_simple";
const config = {
headers: {
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
}
};
const data = {
from: notification.threemaSenderIdentity,
secret: notification.threemaSecret,
text: msg
};
switch (notification.threemaRecipientType) {
case "identity":
data.to = notification.threemaRecipient;
break;
case "phone":
data.phone = notification.threemaRecipient;
break;
case "email":
data.email = notification.threemaRecipient;
break;
default:
throw new Error(`Unsupported recipient type: ${notification.threemaRecipientType}`);
}
try {
await axios.post(url, new URLSearchParams(data), config);
return "Threema notification sent successfully.";
} catch (error) {
const errorMessage = this.handleApiError(error);
this.throwGeneralAxiosError(errorMessage);
}
}
/**
* Handle Threema API errors
* @param {any} error The error to handle
* @returns {string} Additional error context
*/
handleApiError(error) {
if (!error.response) {
return error.message;
}
switch (error.response.status) {
case 400:
return "Invalid recipient identity or account not set up for basic mode (400).";
case 401:
return "Incorrect API identity or secret (401).";
case 402:
return "No credits remaining (402).";
case 404:
return "Recipient not found (404).";
case 413:
return "Message is too long (413).";
case 500:
return "Temporary internal server error (500).";
default:
return error.message;
}
}
}
module.exports = Threema;

View File

@@ -13,9 +13,9 @@ class ZohoCliq extends NotificationProvider {
*/
_statusMessageFactory = (status, monitorName) => {
if (status === DOWN) {
return `🔴 Application [${monitorName}] went down\n`;
return `🔴 [${monitorName}] went down\n`;
} else if (status === UP) {
return ` Application [${monitorName}] is back online\n`;
return `### ✅ [${monitorName}] is back online\n`;
}
return "Notification\n";
};
@@ -46,16 +46,11 @@ class ZohoCliq extends NotificationProvider {
monitorUrl,
}) => {
const payload = [];
payload.push("### Uptime Kuma\n");
payload.push(this._statusMessageFactory(status, monitorName));
payload.push(`*Description:* ${monitorMessage}`);
if (monitorName) {
payload.push(`*Monitor:* ${monitorName}`);
}
if (monitorUrl && monitorUrl !== "https://") {
payload.push(`*URL:* [${monitorUrl}](${monitorUrl})`);
payload.push(`*URL:* ${monitorUrl}`);
}
return payload;
@@ -87,24 +82,10 @@ class ZohoCliq extends NotificationProvider {
return okMsg;
}
let url;
switch (monitorJSON["type"]) {
case "http":
case "keywork":
url = monitorJSON["url"];
break;
case "docker":
url = monitorJSON["docker_host"];
break;
default:
url = monitorJSON["hostname"];
break;
}
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: url,
monitorUrl: this.extractAdress(monitorJSON),
status: heartbeatJSON.status
});

View File

@@ -51,6 +51,7 @@ const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push");
const Telegram = require("./notification-providers/telegram");
const Threema = require("./notification-providers/threema");
const Twilio = require("./notification-providers/twilio");
const Splunk = require("./notification-providers/splunk");
const Webhook = require("./notification-providers/webhook");
@@ -133,6 +134,7 @@ class Notification {
new Teams(),
new TechulusPush(),
new Telegram(),
new Threema(),
new Twilio(),
new Splunk(),
new Webhook(),

View File

@@ -101,11 +101,10 @@ module.exports.statusPageSocketHandler = (socket) => {
if (!statusPage) {
throw new Error("No slug?");
}
const config = await statusPage.toJSON();
config.allowEditingCustomHtml = import.meta.env.UPTIME_KUMA_ALLOW_CUSTOM_HTML === "1";
callback({
ok: true,
config,
config: await statusPage.toJSON(),
});
} catch (error) {
callback({
@@ -168,9 +167,6 @@ module.exports.statusPageSocketHandler = (socket) => {
statusPage.show_certificate_expiry = config.showCertificateExpiry;
statusPage.modified_date = R.isoDateTime();
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
if (process.env.UPTIME_KUMA_ALLOW_CUSTOM_HTML === "1") {
statusPage.custom_html = config.customHtml;
}
await R.store(statusPage);

View File

@@ -11,7 +11,7 @@ const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2");
const { NtlmClient } = require("axios-ntlm");
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
const { Settings } = require("./settings");
const grpc = require("@grpc/grpc-js");
const protojs = require("protobufjs");

View File

@@ -152,6 +152,7 @@ export default {
"stackfield": "Stackfield",
"teams": "Microsoft Teams",
"telegram": "Telegram",
"threema": "Threema",
"twilio": "Twilio",
"Splunk": "Splunk",
"webhook": "Webhook",

View File

@@ -3,7 +3,7 @@
<label for="smspartner-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="smspartner-key" v-model="$parent.notification.smspartnerApikey" :required="true" autocomplete="new-password"></HiddenInput>
<div class="form-text">
<i18n-t keypath="smspartnerApiurl" as="div" class="form-text">
<i18n-t keypath="smspartnerApiurl" tag="div" class="form-text">
<a href="https://my.smspartner.fr/dashboard/api" target="_blank">my.smspartner.fr/dashboard/api</a>
</i18n-t>
</div>
@@ -12,7 +12,7 @@
<label for="smspartner-phone-number" class="form-label">{{ $t("smspartnerPhoneNumber") }}</label>
<input id="smspartner-phone-number" v-model="$parent.notification.smspartnerPhoneNumber" type="text" minlength="3" maxlength="20" pattern="^[\d+,]+$" class="form-control" required>
<div class="form-text">
<i18n-t keypath="smspartnerPhoneNumberHelptext" as="div" class="form-text">
<i18n-t keypath="smspartnerPhoneNumberHelptext" tag="div" class="form-text">
<code>+336xxxxxxxx</code>
<code>+496xxxxxxxx</code>
<code>,</code>

View File

@@ -0,0 +1,87 @@
<template>
<div class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipientType") }}</label>
<select
id="threema-recipient" v-model="$parent.notification.threemaRecipientType" required
class="form-select"
>
<option value="identity">{{ $t("threemaRecipientTypeIdentity") }}</option>
<option value="phone">{{ $t("threemaRecipientTypePhone") }}</option>
<option value="email">{{ $t("threemaRecipientTypeEmail") }}</option>
</select>
</div>
<div v-if="$parent.notification.threemaRecipientType === 'identity'" class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypeIdentity") }}</label>
<input
id="threema-recipient"
v-model="$parent.notification.threemaRecipient"
class="form-control"
minlength="8"
maxlength="8"
pattern="[A-Z0-9]{8}"
required
type="text"
>
<div class="form-text">
<p>{{ $t("threemaRecipientTypeIdentityFormat") }}</p>
</div>
</div>
<div v-else-if="$parent.notification.threemaRecipientType === 'phone'" class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypePhone") }}</label>
<input
id="threema-recipient"
v-model="$parent.notification.threemaRecipient"
class="form-control"
maxlength="15"
pattern="\d{1,15}"
required
type="text"
>
<div class="form-text">
<p>{{ $t("threemaRecipientTypePhoneFormat") }}</p>
</div>
</div>
<div v-else-if="$parent.notification.threemaRecipientType === 'email'" class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypeEmail") }}</label>
<input
id="threema-recipient"
v-model="$parent.notification.threemaRecipient"
class="form-control"
maxlength="254"
required
type="email"
>
</div>
<div class="mb-3">
<label class="form-label" for="threema-sender">{{ $t("threemaSenderIdentity") }}</label>
<input
id="threema-sender"
v-model="$parent.notification.threemaSenderIdentity"
class="form-control"
minlength="8"
maxlength="8"
pattern="^\*[A-Z0-9]{7}$"
required
type="text"
>
<div class="form-text">
<p>{{ $t("threemaSenderIdentityFormat") }}</p>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="threema-secret">{{ $t("threemaApiAuthenticationSecret") }}</label>
<HiddenInput
id="threema-secret" v-model="$parent.notification.threemaSecret" required
autocomplete="false"
></HiddenInput>
</div>
<i18n-t class="form-text" keypath="wayToGetThreemaGateway" tag="div">
<a href="https://threema.ch/en/gateway" target="_blank">{{ $t("here") }}</a>
</i18n-t>
<i18n-t class="form-text" keypath="threemaBasicModeInfo" tag="div">
<a href="https://gateway.threema.ch/en/developer/api" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</template>
<script lang="ts" setup>
import HiddenInput from "../HiddenInput.vue";
</script>

View File

@@ -52,6 +52,7 @@ import STMP from "./SMTP.vue";
import Teams from "./Teams.vue";
import TechulusPush from "./TechulusPush.vue";
import Telegram from "./Telegram.vue";
import Threema from "./Threema.vue";
import Twilio from "./Twilio.vue";
import Webhook from "./Webhook.vue";
import WeCom from "./WeCom.vue";
@@ -119,6 +120,7 @@ const NotificationFormList = {
"stackfield": Stackfield,
"teams": Teams,
"telegram": Telegram,
"threema": Threema,
"twilio": Twilio,
"Splunk": Splunk,
"webhook": Webhook,

View File

@@ -776,9 +776,6 @@
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
"Custom Monitor Type": "Custom Monitor Type",
"Google Analytics ID": "Google Analytics ID",
"Custom HTML": "Custom HTML",
"customHtmlEnvVarDisabled": "environment variable {allow_custom_html} must be set to inject html to the head",
"customHtmlEnvVarEnabled": "Because the environment variable {allow_custom_html} is set, arbitrary html can be injected into the head. Make sure to remove the environment variable after use",
"Edit Tag": "Edit Tag",
"Server Address": "Server Address",
"Learn More": "Learn More",
@@ -945,5 +942,17 @@
"Allow Long SMS": "Allow Long SMS",
"cellsyntSplitLongMessages": "Split long messages into up to 6 parts. 153 x 6 = 918 characters.",
"max 15 digits": "max 15 digits",
"max 11 alphanumeric characters": "max 11 alphanumeric characters"
"max 11 alphanumeric characters": "max 11 alphanumeric characters",
"wayToGetThreemaGateway": "You can register for Threema Gateway {0}.",
"threemaRecipient": "Recipient",
"threemaRecipientType": "Recipient Type",
"threemaRecipientTypeIdentity": "Threema-ID",
"threemaRecipientTypeIdentityFormat": "8 characters",
"threemaRecipientTypePhone": "Phone Number",
"threemaRecipientTypePhoneFormat": "E.164, without leading +",
"threemaRecipientTypeEmail": "Email Address",
"threemaSenderIdentity": "Gateway-ID",
"threemaSenderIdentityFormat": "8 characters, usually starts with *",
"threemaApiAuthenticationSecret": "Gateway-ID Secret",
"threemaBasicModeInfo": "Note: This integration uses Threema Gateway in basic mode (server-based encryption). Further details can be found {0}."
}

View File

@@ -104,22 +104,6 @@
<prism-editor v-model="config.customCSS" class="css-editor" :highlight="highlighter" line-numbers></prism-editor>
</div>
<!-- Custom HTML -->
<div class="my-3">
<div class="mb-1">{{ $t("Custom HTML") }}</div>
<prism-editor v-model="config.customHtml" class="css-editor" :highlight="highlighter" line-numbers :readonly="!config.allowEditingCustomHtml"></prism-editor>
<i18n-t v-if="config.allowEditingCustomHtml" tag="div" class="form-text" keypath="customHtmlEnvVarEnabled">
<template #allow_custom_html>
<code>UPTIME_KUMA_ALLOW_CUSTOM_HTML</code>
</template>
</i18n-t>
<i18n-t v-else tag="div" class="form-text" keypath="customHtmlEnvVarDisabled">
<template #allow_custom_html>
<code>UPTIME_KUMA_ALLOW_CUSTOM_HTML=1</code>
</template>
</i18n-t>
</div>
<div class="danger-zone">
<button class="btn btn-danger me-2" @click="deleteDialog">
<font-awesome-icon icon="trash" />