Merge branch 'patch-6' of https://github.com/andreasbrett/uptime-kuma into patch-6

This commit is contained in:
Andreas Brett 2021-10-25 01:34:06 +02:00
commit 4fc2603818
10 changed files with 225 additions and 20 deletions

23
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,23 @@
# Description
Fixes #(issue)
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] User Interface
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Translation update
- [ ] This change requires a documentation update
## Checklist
- [ ] My code follows the style guidelines of this project
- [ ] I ran ESLint and other linters for modified files
- [ ] I have performed a self-review of my own code and test it
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] My changes generate no new warnings
- [ ] My code needed automated testing. I have added them
## Screenshots (if any)

View file

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [macos-latest, ubuntu-latest, windows-latest] os: [macos-latest, ubuntu-latest, windows-latest]
node-version: [14.x, 16.x] node-version: [14.x, 16.x, 17.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:

View file

@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE user
ADD twofa_last_token VARCHAR(6);
COMMIT;

View file

@ -1,8 +1,9 @@
const basicAuth = require("express-basic-auth") const basicAuth = require("express-basic-auth");
const passwordHash = require("./password-hash"); const passwordHash = require("./password-hash");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { setting } = require("./util-server"); const { setting } = require("./util-server");
const { debug } = require("../src/util"); const { debug } = require("../src/util");
const { loginRateLimiter } = require("./rate-limiter");
/** /**
* *
@ -13,7 +14,7 @@ const { debug } = require("../src/util");
exports.login = async function (username, password) { exports.login = async function (username, password) {
let user = await R.findOne("user", " username = ? AND active = 1 ", [ let user = await R.findOne("user", " username = ? AND active = 1 ", [
username, username,
]) ]);
if (user && passwordHash.verify(password, user.password)) { if (user && passwordHash.verify(password, user.password)) {
// Upgrade the hash to bcrypt // Upgrade the hash to bcrypt
@ -27,21 +28,30 @@ exports.login = async function (username, password) {
} }
return null; return null;
} };
function myAuthorizer(username, password, callback) { function myAuthorizer(username, password, callback) {
setting("disableAuth").then((result) => { setting("disableAuth").then((result) => {
if (result) { if (result) {
callback(null, true) callback(null, true);
} else { } else {
exports.login(username, password).then((user) => { // Login Rate Limit
callback(null, user != null) loginRateLimiter.pass(null, 0).then((pass) => {
}) if (pass) {
} exports.login(username, password).then((user) => {
}) callback(null, user != null);
if (user == null) {
loginRateLimiter.removeTokens(1);
}
});
} else {
callback(null, false);
}
});
}
});
} }
exports.basicAuth = basicAuth({ exports.basicAuth = basicAuth({

View file

@ -50,6 +50,7 @@ class Database {
"patch-group-table.sql": true, "patch-group-table.sql": true,
"patch-monitor-push_token.sql": true, "patch-monitor-push_token.sql": true,
"patch-http-monitor-method-body-and-headers.sql": true, "patch-http-monitor-method-body-and-headers.sql": true,
"patch-2fa-invalidate-used-token.sql": true,
} }
/** /**

39
server/rate-limiter.js Normal file
View file

@ -0,0 +1,39 @@
const { RateLimiter } = require("limiter");
const { debug } = require("../src/util");
class KumaRateLimiter {
constructor(config) {
this.errorMessage = config.errorMessage;
this.rateLimiter = new RateLimiter(config);
}
async pass(callback, num = 1) {
const remainingRequests = await this.removeTokens(num);
debug("Rate Limit (remainingRequests):" + remainingRequests);
if (remainingRequests < 0) {
if (callback) {
callback({
ok: false,
msg: this.errorMessage,
});
}
return false;
}
return true;
}
async removeTokens(num = 1) {
return await this.rateLimiter.removeTokens(num);
}
}
const loginRateLimiter = new KumaRateLimiter({
tokensPerInterval: 20,
interval: "minute",
fireImmediately: true,
errorMessage: "Too frequently, try again later."
});
module.exports = {
loginRateLimiter
};

View file

@ -52,6 +52,7 @@ const Database = require("./database");
debug("Importing Background Jobs"); debug("Importing Background Jobs");
const { initBackgroundJobs } = require("./jobs"); const { initBackgroundJobs } = require("./jobs");
const { loginRateLimiter } = require("./rate-limiter");
const { basicAuth } = require("./auth"); const { basicAuth } = require("./auth");
const { login } = require("./auth"); const { login } = require("./auth");
@ -281,12 +282,17 @@ exports.entryPage = "dashboard";
socket.on("login", async (data, callback) => { socket.on("login", async (data, callback) => {
console.log("Login"); console.log("Login");
// Login Rate Limit
if (! await loginRateLimiter.pass(callback)) {
return;
}
let user = await login(data.username, data.password); let user = await login(data.username, data.password);
if (user) { if (user) {
afterLogin(socket, user); afterLogin(socket, user);
if (user.twofaStatus == 0) { if (user.twofa_status == 0) {
callback({ callback({
ok: true, ok: true,
token: jwt.sign({ token: jwt.sign({
@ -295,7 +301,7 @@ exports.entryPage = "dashboard";
}); });
} }
if (user.twofaStatus == 1 && !data.token) { if (user.twofa_status == 1 && !data.token) {
callback({ callback({
tokenRequired: true, tokenRequired: true,
}); });
@ -304,7 +310,13 @@ exports.entryPage = "dashboard";
if (data.token) { if (data.token) {
let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts); let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts);
if (verify && verify.delta == 0) { if (user.twofa_last_token !== data.token && verify) {
await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
data.token,
socket.userID,
]);
callback({ callback({
ok: true, ok: true,
token: jwt.sign({ token: jwt.sign({
@ -422,7 +434,7 @@ exports.entryPage = "dashboard";
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts); let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
if (verify && verify.delta == 0) { if (user.twofa_last_token !== token && verify) {
callback({ callback({
ok: true, ok: true,
valid: true, valid: true,

View file

@ -197,5 +197,112 @@ export default {
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"Primary Base URL": "Primär URL",
"Push URL": "Push URL",
needPushEvery: "Du solltest diese URL alle {0} Sekunden aufrufen.",
pushOptionalParams: "Optionale Parameter: {0}",
defaultNotificationName: "Meine {notification} Alarm ({number})",
here: "hier",
Required: "Erforderlich",
"Bot Token": "Bot Token",
wayToGetTelegramToken: "Hier kannst du einen Token erhalten {0}.",
"Chat ID": "Chat ID",
supportTelegramChatID: "Unterstützt Direkt Chat / Gruppe / Kanal Chat-ID's",
wayToGetTelegramChatID: "Du kannst die Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id: zu sehen.",
"YOUR BOT TOKEN HERE": "HIER DEIN BOT TOKEN",
chatIDNotFound: "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
"Post URL": "Post URL",
"Content Type": "Content Type",
webhookJsonDesc: "{0} ist gut für alle modernen HTTP-Server sowie Express.js",
webhookFormDataDesc: "{multipart} ist gut für PHP. Die JSON muss mit {decodeFunction} geparst werden",
secureOptionNone: "Keine / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "TLS-Fehler ignorieren",
"From Email": "Von Email",
emailCustomSubject: "Benutzerdefinierter Betreff",
"To Email": "Zu Email",
smtpCC: "CC",
smtpBCC: "BCC",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Du kannst diesen erhalten, indem du zu den Servereinstellungen gehst -> Integrationen -> Neuer Webhook",
"Bot Display Name": "Bot-Anzeigename",
"Prefix Custom Message": "Benutzerdefinierter Nachrichten Präfix",
"Hello @everyone is...": "Hallo {'@'}everyone ist...",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "Hier erfährst du, wie eine Webhook-URL erstellt werden kann {0}.",
Number: "Nummer",
Recipients: "Empfänger",
needSignalAPI: "Es wird ein Signal Client mit REST-API benötigt.",
wayToCheckSignalURL: "Du kannst diese URL aufrufen, um zu sehen, wie du eine einrichtest:",
signalImportant: "WICHTIG: Gruppen und Nummern können in Empfängern nicht gemischt werden!",
"Application Token": "Anwendungs Token",
"Server URL": "Server URL",
Priority: "Priorität",
"Icon Emoji": "Icon Emoji",
"Channel Name": "Kanalname",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Weitere Informationen zu Webhooks auf: {0}",
aboutChannelName: "Gebe den Kanalnamen ein auf {0} Feld Kanalname, wenn du den Webhook-Kanal umgehen möchtest. Ex: #other-channel",
aboutKumaURL: "Wenn das Feld für die Uptime Kuma URL leer gelassen wird, wird es standardmäßig die GitHub Projekt Seite verwenden.",
emojiCheatSheet: "Emoji Cheat Sheet: {0}",
"User Key": "Benutzerschlüssel",
Device: "Gerät",
"Message Title": "Nachrichtentitel",
"Notification Sound": "Benachrichtigungston",
"More info on:": "Mehr Infos auf: {0}",
pushoverDesc1: "Notfallpriorität (2) hat Standardmäßig 30 Sekunden Auszeit, zwischen den Versuchen und läuft nach 1 Stunde ab.",
pushoverDesc2: "Fülle das Geräte Feld aus, wenn du Benachrichtigungen an verschiedene Geräte senden möchtest.",
"SMS Type": "SMS Typ",
octopushTypePremium: "Premium (Schnell - zur Benachrichtigung empfohlen)",
octopushTypeLowCost: "Kostengünstig (Langsam - manchmal vom Betreiber gesperrt)",
checkPrice: "Prüfe {0} Preise:",
octopushLegacyHint: "Verwendest du die Legacy-Version von Octopush (2011-2020) oder die neue Version?",
"Check octopush prices": "Überprüfe die Oktopush Preise {0}.",
octopushPhoneNumber: "Telefonnummer (Internationales Format, z.B : +49612345678) ",
octopushSMSSender: "Name des SMS-Absenders : 3-11 alphanumerische Zeichen und Leerzeichen (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Geräte ID",
"Apprise URL": "Apprise URL",
"Example:": "Beispiel: {0}",
"Read more:": "Weiterlesen: {0}",
"Status:": "Status: {0}",
"Read more": "Weiterlesen",
appriseInstalled: "Apprise ist installiert.",
appriseNotInstalled: "Apprise ist nicht installiert. {0}",
"Access Token": "Access Token",
"Channel access token": "Channel access token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Basic Settings",
"User ID": "User ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "Rufe zuerst {0} auf, erstelle dann einen Provider und Channel (Messaging API). Als nächstes kannst du den Channel access token und die User ID aus den oben genannten Menüpunkten abrufen.",
"Icon URL": "Icon URL",
aboutIconURL: "Du kannst einen Link zu einem Bild in 'Icon URL' übergeben um das Standardprofilbild zu überschreiben. Wird nicht verwendet, wenn ein Icon Emoji gesetzt ist.",
aboutMattermostChannelName: "Du kannst den Standardkanal, auf dem der Webhook postet überschreiben, indem der Kanalnamen in das Feld 'Channel Name' eingeben wird. Dies muss in den Mattermost Webhook-Einstellungen aktiviert werden. Ex: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - billig, aber langsam und oft überladen. Nur auf polnische Empfänger beschränkt.",
promosmsTypeFlash: "SMS FLASH - Die Nachricht wird automatisch auf dem Empfängergerät angezeigt. Nur auf polnische Empfänger beschränkt.",
promosmsTypeFull: "SMS FULL - Premium Stufe von SMS, es kann der Absendernamen verwendet werden (Der Name musst zuerst registriert werden). Zuverlässig für Warnungen.",
promosmsTypeSpeed: "SMS SPEED - Höchste Priorität im System. Sehr schnell und zuverlässig, aber teuer (Ungefähr das doppelte von SMS FULL).",
promosmsPhoneNumber: "Phone number (Für polnische Empfänger können die Vorwahlen übersprungen werden)",
promosmsSMSSender: "Name des SMS-Absenders : vorregistrierter Name oder einer der Standardwerte: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu Webhook URL",
matrixHomeserverURL: "Heimserver URL (mit http(s):// und optionalen Ports)",
"Internal Room Id": "Interne Raum-ID",
matrixDesc1: "Die interne Raum-ID findest du im erweiterten Bereich der Raumeinstellungen im Matrix-Client. Es sollte es aussehen wie z.B. !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Es wird dringend empfohlen, dass ein neuen Benutzer erstellt wird und nicht den Zugriffstoken deines eigenen Matrix-Benutzers verwendest. Anderfalls ermöglicht es vollen Zugriff auf dein Konto und alle Räume, denen du beigetreten bist. Erstelle stattdessen einen neuen Benutzer und lade ihn nur in den Raum ein, in dem du die Benachrichtigung erhalten möchtest. Du kannst den Zugriffstoken erhalten, indem du folgendes ausführst {0}",
Method: "Method",
Body: "Body",
Headers: "Headers",
PushUrl: "Push URL",
HeadersInvalidFormat: "Die Header ist kein gültiges JSON: ",
BodyInvalidFormat: "Der Body ist kein gültiges JSON: ",
"Monitor History": "Monitor Verlauf:",
clearDataOlderThan: "Bewahre die Monitor-Verlaufsdaten für {0} Tage auf.",
PasswordsDoNotMatch: "Passwörter stimmen nicht überein.",
records: "Einträge",
"One record": "Ein Eintrag",
"Showing {from} to {to} of {count} records": "Zeige {from} zu {to} von {count} Einträge",
steamApiKeyDescription: "Um einen Steam Game Server zu überwachen, wird ein Steam Web-API-Schlüssel benötigt. Dieser kann hier registriert werden: ",
"Current User": "Aktueller Benutzer", "Current User": "Aktueller Benutzer",
}; };

View file

@ -271,8 +271,8 @@ export default {
"Messaging API": "API Wiadomości", "Messaging API": "API Wiadomości",
wayToGetLineChannelToken: "Najpierw uzyskaj dostęp do {0}, utwórz dostawcę i kanał (Messaging API), a następnie możesz uzyskać token dostępu do kanału i identyfikator użytkownika z wyżej wymienionych pozycji menu.", wayToGetLineChannelToken: "Najpierw uzyskaj dostęp do {0}, utwórz dostawcę i kanał (Messaging API), a następnie możesz uzyskać token dostępu do kanału i identyfikator użytkownika z wyżej wymienionych pozycji menu.",
"Icon URL": "Adres Ikony", "Icon URL": "Adres Ikony",
aboutIconURL: "You can provide a link to a picture in \"Icon URL\" to override the default profile picture. Will not be used if Icon Emoji is set.", aboutIconURL: "Możesz podać link do zdjęcia w \"Adres URL ikony\", aby zastąpić domyślne zdjęcie profilowe. Nie będzie używany, jeśli ustawiona jest ikona Emoji.",
aboutMattermostChannelName: "You can override the default channel that webhook posts to by entering the channel name into \"Channel Name\" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel", aboutMattermostChannelName: "Możesz zastąpić domyślny kanał, na którym publikowane są posty webhooka, wpisując nazwę kanału w polu \"Nazwa Kanału\". Należy to włączyć w ustawieniach webhooka Mattermost. Np.: #inny-kanał",
"matrix": "Matrix", "matrix": "Matrix",
promosmsTypeEco: "SMS ECO - Tanie, lecz wolne. Dostępne tylko w Polsce", promosmsTypeEco: "SMS ECO - Tanie, lecz wolne. Dostępne tylko w Polsce",
promosmsTypeFlash: "SMS FLASH - Wiadomość automatycznie wyświetli się na urządzeniu. Dostępne tylko w Polsce.", promosmsTypeFlash: "SMS FLASH - Wiadomość automatycznie wyświetli się na urządzeniu. Dostępne tylko w Polsce.",

View file

@ -340,11 +340,17 @@ export default {
}, },
bodyPlaceholder() { bodyPlaceholder() {
return "{\n\t\"id\": 124357,\n\t\"username\": \"admin\",\n\t\"password\": \"myAdminPassword\"\n}"; return `Example:
{
"key": "value"
}`;
}, },
headersPlaceholder() { headersPlaceholder() {
return "{\n\t\"Authorization\": \"Bearer abc123\",\n\t\"Content-Type\": \"application/json\"\n}"; return `Example:
{
"HeaderName": "HeaderValue"
}`;
} }
}, },