Upload via GUI (26 Dateien)
This commit is contained in:
@@ -2,6 +2,7 @@ package de.deskhub.minecraft.bukkit;
|
||||
|
||||
import de.deskhub.minecraft.core.HeartbeatFile;
|
||||
import de.deskhub.minecraft.core.OpenTickets;
|
||||
import de.deskhub.minecraft.core.RestartNotice;
|
||||
import de.deskhub.minecraft.core.Settings;
|
||||
import de.deskhub.minecraft.core.Template;
|
||||
import de.deskhub.minecraft.core.TicketClient;
|
||||
@@ -65,6 +66,12 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor {
|
||||
public void onDisable() {
|
||||
if (beatTask != null) beatTask.cancel();
|
||||
long now = System.currentTimeMillis();
|
||||
// Zuerst dem Proxy sagen, dass der Neustart geplant ist (er wartet dann länger, bevor er ein Ticket anlegt)
|
||||
if (settings != null && settings.announcesRestarts()) {
|
||||
String err = RestartNotice.send(settings.proxyNotifyHost, settings.proxyNotifyPort, RestartNotice.build(settings.proxyNotifySecret, settings.proxyNotifyName, now), 1500);
|
||||
if (err == null) getLogger().info("Proxy über das geplante Herunterfahren informiert.");
|
||||
else getLogger().warning("Proxy konnte nicht über das Herunterfahren informiert werden: " + err);
|
||||
}
|
||||
try {
|
||||
if (heartbeat != null) heartbeat.markClean(now);
|
||||
} catch (IOException e) {
|
||||
@@ -161,6 +168,7 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor {
|
||||
sender.sendMessage("§7Abteilung: §f" + idText(settings.departmentId) + " §7Priorität: §f" + idText(settings.priorityId));
|
||||
sender.sendMessage("§7Konfiguration: " + (settings.problems().isEmpty() ? "§aOK" : "§c" + settings.problems().get(0)));
|
||||
sender.sendMessage("§7Absturz-Ticket: §f" + (settings.ticketOnCrash ? "an" : "aus") + " §7Herunterfahren-Ticket: §f" + (settings.ticketOnShutdown ? "an" : "aus"));
|
||||
sender.sendMessage("§7Meldung an den Proxy beim Herunterfahren: §f" + (settings.announcesRestarts() ? "an (" + settings.proxyNotifyHost + ":" + settings.proxyNotifyPort + ", Name " + settings.proxyNotifyName + ")" : "aus"));
|
||||
} else if (sub.equals("test")) {
|
||||
if (!settings.problems().isEmpty()) {
|
||||
sender.sendMessage("§cKein Test möglich: " + settings.problems().get(0));
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.deskhub.minecraft.bungee;
|
||||
|
||||
import de.deskhub.minecraft.core.OpenTickets;
|
||||
import de.deskhub.minecraft.core.OutageTracker;
|
||||
import de.deskhub.minecraft.core.RestartNotice;
|
||||
import de.deskhub.minecraft.core.Settings;
|
||||
import de.deskhub.minecraft.core.Template;
|
||||
import de.deskhub.minecraft.core.TicketClient;
|
||||
@@ -21,11 +22,16 @@ import net.md_5.bungee.config.YamlConfiguration;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
@@ -39,6 +45,9 @@ public final class BungeePlugin extends Plugin {
|
||||
private OutageTracker tracker;
|
||||
private OpenTickets openTickets;
|
||||
private ScheduledTask watchdog;
|
||||
private ServerSocket notifySocket;
|
||||
/** Gleichzeitig bearbeitete Meldungen: Mehr werden sofort abgewiesen (schützt vor Verbindungsfluten auf dem offenen Anschluss). */
|
||||
private final AtomicInteger activeNotices = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -49,6 +58,101 @@ public final class BungeePlugin extends Plugin {
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (watchdog != null) watchdog.cancel();
|
||||
stopNotifyListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lauscht auf Meldungen der Server über geplante Neustarts (nur wenn monitor.notify-port und monitor.notify-secret gesetzt sind).
|
||||
* Jede Meldung ist mit dem gemeinsamen Geheimnis signiert und muss frisch sein; der Servername muss in der Proxy-Konfiguration stehen.
|
||||
*/
|
||||
private void startNotifyListener() {
|
||||
stopNotifyListener();
|
||||
if (!settings.listensForRestartNotices() || settings.plannedRestartGraceMinutes <= 0) return;
|
||||
final ServerSocket ss;
|
||||
try {
|
||||
ss = new ServerSocket();
|
||||
ss.setReuseAddress(true);
|
||||
ss.bind(new InetSocketAddress(settings.notifyBind, settings.notifyPort), 16);
|
||||
} catch (IOException e) {
|
||||
getLogger().warning("Meldungen über geplante Neustarts sind nicht möglich (Port " + settings.notifyPort + "): " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
notifySocket = ss;
|
||||
getLogger().info("Nimmt Meldungen über geplante Neustarts an (" + settings.notifyBind + ":" + settings.notifyPort + ").");
|
||||
Thread t = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!ss.isClosed()) {
|
||||
try {
|
||||
final Socket client = ss.accept();
|
||||
if (activeNotices.incrementAndGet() > 8) {
|
||||
activeNotices.decrementAndGet();
|
||||
try {
|
||||
client.close();
|
||||
} catch (IOException ignored) {
|
||||
// abgewiesen
|
||||
}
|
||||
continue;
|
||||
}
|
||||
getProxy().getScheduler().runAsync(BungeePlugin.this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handleNotice(client);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
if (ss.isClosed()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, "DeskHub-Minecraft-Notify");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void stopNotifyListener() {
|
||||
if (notifySocket != null) {
|
||||
try {
|
||||
notifySocket.close();
|
||||
} catch (IOException ignored) {
|
||||
// wird ohnehin beendet
|
||||
}
|
||||
notifySocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleNotice(Socket client) {
|
||||
String answer = "ERR\n";
|
||||
try {
|
||||
client.setSoTimeout(2000);
|
||||
// höchstens eine kurze Zeile lesen
|
||||
java.io.InputStream in = client.getInputStream();
|
||||
byte[] buf = new byte[RestartNotice.MAX_LINE_BYTES];
|
||||
int n = 0;
|
||||
int b;
|
||||
while (n < buf.length && (b = in.read()) != -1 && b != '\n') buf[n++] = (byte) b;
|
||||
String line = new String(buf, 0, n, "UTF-8");
|
||||
String server = RestartNotice.verify(settings.notifySecret, line, System.currentTimeMillis(), 60000L);
|
||||
if (server != null && getProxy().getServers().containsKey(server)) {
|
||||
tracker.markPlanned(server, System.currentTimeMillis());
|
||||
getLogger().info("Server \"" + server + "\" meldet einen geplanten Neustart: Ticket frühestens nach " + settings.plannedRestartGraceMinutes + " Minuten.");
|
||||
answer = "OK\n";
|
||||
} else if (server != null) {
|
||||
getLogger().warning("Meldung für den unbekannten Server \"" + server + "\" ignoriert (Name stimmt nicht mit der Proxy-Konfiguration überein).");
|
||||
}
|
||||
OutputStream out = client.getOutputStream();
|
||||
out.write(answer.getBytes("UTF-8"));
|
||||
out.flush();
|
||||
} catch (IOException ignored) {
|
||||
// Verbindung abgebrochen: nichts zu tun
|
||||
} finally {
|
||||
activeNotices.decrementAndGet();
|
||||
try {
|
||||
client.close();
|
||||
} catch (IOException ignored) {
|
||||
// nichts zu tun
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void load() {
|
||||
@@ -72,7 +176,8 @@ public final class BungeePlugin extends Plugin {
|
||||
}
|
||||
client = new TicketClient(settings, getDescription().getVersion());
|
||||
openTickets = new OpenTickets(new File(getDataFolder(), "offene-tickets.properties"));
|
||||
tracker = new OutageTracker(settings.monitorFailures, settings.cooldownMinutes, settings.ignoreNeverOnline);
|
||||
tracker = new OutageTracker(settings.monitorFailures, settings.cooldownMinutes, settings.ignoreNeverOnline, settings.restartGraceMinutes, settings.plannedRestartGraceMinutes);
|
||||
startNotifyListener();
|
||||
for (String w : settings.warnings()) getLogger().warning(w);
|
||||
List<String> problems = settings.problems();
|
||||
if (!problems.isEmpty()) {
|
||||
@@ -178,7 +283,7 @@ public final class BungeePlugin extends Plugin {
|
||||
send(sender, "§7DeskHub: §f" + (settings.url.isEmpty() ? "nicht eingestellt" : settings.url));
|
||||
send(sender, "§7Abteilung: §f" + idText(settings.departmentId) + " §7Priorität: §f" + idText(settings.priorityId));
|
||||
send(sender, "§7Konfiguration: " + (settings.problems().isEmpty() ? "§aOK" : "§c" + settings.problems().get(0)));
|
||||
send(sender, "§7Prüfung alle §f" + settings.monitorIntervalSeconds + " s§7, Ticket nach §f" + settings.monitorFailures + " §7Fehlschlägen, Wartezeit §f" + settings.cooldownMinutes + " Min.");
|
||||
send(sender, "§7Prüfung alle §f" + settings.monitorIntervalSeconds + " s§7, Ticket nach §f" + settings.monitorFailures + " §7Fehlschlägen, Wartezeit §f" + settings.cooldownMinutes + " Min.§7, Neustart-Schonfrist §f" + settings.restartGraceMinutes + " Min. §7(geplant: §f" + settings.plannedRestartGraceMinutes + " Min.§7, Meldungen: §f" + (notifySocket != null ? "an, Anschluss " + settings.notifyPort : "aus") + "§7)");
|
||||
Map<String, Boolean> snap = tracker.snapshot();
|
||||
if (snap.isEmpty()) send(sender, "§7Noch keine Prüfung durchgeführt.");
|
||||
for (Map.Entry<String, Boolean> e : snap.entrySet()) send(sender, " §7- §f" + e.getKey() + ": " + (e.getValue() ? "§aonline" : "§coffline"));
|
||||
|
||||
@@ -8,6 +8,10 @@ import java.util.Map;
|
||||
* Entscheidet anhand der Prüfergebnisse (online/offline) je Server, wann ein Ticket angelegt wird: erst nach mehreren Fehlschlägen in Folge
|
||||
* (kein Ticket bei einem einzelnen Aussetzer), höchstens einmal pro Ausfall und höchstens einmal je Wartezeit (kein Ticket-Gewitter, wenn
|
||||
* ein Server ständig an- und ausgeht). Server, die seit dem Start nie online waren, werden auf Wunsch ignoriert.
|
||||
*
|
||||
* Ein geplanter Neustart ist von außen nicht von einem Absturz zu unterscheiden. Deshalb gibt es eine Schonfrist: Ein Ticket entsteht erst,
|
||||
* wenn der Server mindestens so lange nicht erreichbar ist (Neustarts dauern meist ein bis drei Minuten). Meldet der Server vorher selbst
|
||||
* einen geplanten Neustart (RestartNotice), gilt statt dessen die längere Frist für geplante Neustarts.
|
||||
*/
|
||||
public final class OutageTracker {
|
||||
public enum Decision { NONE, OPEN_TICKET, RECOVERED }
|
||||
@@ -19,14 +23,27 @@ public final class OutageTracker {
|
||||
long lastTicketAt = -1;
|
||||
long offlineSince;
|
||||
boolean online;
|
||||
long plannedUntil;
|
||||
}
|
||||
|
||||
private final int failuresNeeded;
|
||||
private final long cooldownMillis;
|
||||
private final boolean ignoreNeverOnline;
|
||||
private final long graceMillis;
|
||||
private final long plannedGraceMillis;
|
||||
private final Map<String, State> states = new HashMap<String, State>();
|
||||
|
||||
public OutageTracker(int failuresNeeded, int cooldownMinutes, boolean ignoreNeverOnline) {
|
||||
this(failuresNeeded, cooldownMinutes, ignoreNeverOnline, 0);
|
||||
}
|
||||
|
||||
public OutageTracker(int failuresNeeded, int cooldownMinutes, boolean ignoreNeverOnline, int graceMinutes) {
|
||||
this(failuresNeeded, cooldownMinutes, ignoreNeverOnline, graceMinutes, 0);
|
||||
}
|
||||
|
||||
public OutageTracker(int failuresNeeded, int cooldownMinutes, boolean ignoreNeverOnline, int graceMinutes, int plannedGraceMinutes) {
|
||||
this.plannedGraceMillis = Math.max(0, plannedGraceMinutes) * 60000L;
|
||||
this.graceMillis = Math.max(0, graceMinutes) * 60000L;
|
||||
this.failuresNeeded = Math.max(1, failuresNeeded);
|
||||
this.cooldownMillis = Math.max(0, cooldownMinutes) * 60000L;
|
||||
this.ignoreNeverOnline = ignoreNeverOnline;
|
||||
@@ -45,12 +62,13 @@ public final class OutageTracker {
|
||||
s.failures = 0;
|
||||
s.outageOpen = false;
|
||||
s.offlineSince = 0;
|
||||
s.plannedUntil = 0;
|
||||
return recovered ? Decision.RECOVERED : Decision.NONE;
|
||||
}
|
||||
if (ignoreNeverOnline && !s.seenOnline) return Decision.NONE;
|
||||
if (s.failures == 0) s.offlineSince = nowMillis;
|
||||
s.failures++;
|
||||
if (s.failures >= failuresNeeded && !s.outageOpen) {
|
||||
if (s.failures >= failuresNeeded && !s.outageOpen && nowMillis - s.offlineSince >= graceMillis && nowMillis >= s.plannedUntil) {
|
||||
s.outageOpen = true;
|
||||
if (s.lastTicketAt < 0 || nowMillis - s.lastTicketAt >= cooldownMillis) {
|
||||
s.lastTicketAt = nowMillis;
|
||||
@@ -60,6 +78,23 @@ public final class OutageTracker {
|
||||
return Decision.NONE;
|
||||
}
|
||||
|
||||
/** Der Server hat einen geplanten Neustart gemeldet: Bis zur Frist für geplante Neustarts gibt es kein Ticket. */
|
||||
public synchronized void markPlanned(String server, long nowMillis) {
|
||||
if (plannedGraceMillis <= 0) return;
|
||||
State s = states.get(server);
|
||||
if (s == null) {
|
||||
s = new State();
|
||||
states.put(server, s);
|
||||
}
|
||||
s.plannedUntil = nowMillis + plannedGraceMillis;
|
||||
}
|
||||
|
||||
/** Ob für den Server gerade ein geplanter Neustart gemeldet ist. */
|
||||
public synchronized boolean isPlanned(String server, long nowMillis) {
|
||||
State s = states.get(server);
|
||||
return s != null && nowMillis < s.plannedUntil;
|
||||
}
|
||||
|
||||
/** Zeitpunkt des ersten fehlgeschlagenen Prüfens des aktuellen Ausfalls (0 = Server ist online). */
|
||||
public synchronized long offlineSince(String server) {
|
||||
State s = states.get(server);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package de.deskhub.minecraft.core;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
/**
|
||||
* Meldung "Dieser Server wird jetzt geplant heruntergefahren" vom Server (Spigot/Paper) an den BungeeCord-Proxy. Der Proxy kann von
|
||||
* außen einen Neustart nicht von einem Absturz unterscheiden; mit dieser Meldung gibt er dem Server mehr Zeit, bevor er ein Ticket anlegt.
|
||||
*
|
||||
* Eine Zeile über TCP: {@code DHMC1 <Servername> <Zeit in Millisekunden> <HMAC-SHA256 als Hex>}. Der Servername steht URL-kodiert darin;
|
||||
* signiert werden Servername und Zeit mit einem gemeinsamen Geheimnis. Der Proxy nimmt nur Meldungen mit richtiger Signatur und frischer
|
||||
* Zeit an (kein Nachspielen alter Meldungen).
|
||||
*/
|
||||
public final class RestartNotice {
|
||||
private static final Charset UTF8 = Charset.forName("UTF-8");
|
||||
public static final int MAX_LINE_BYTES = 512;
|
||||
|
||||
private RestartNotice() {
|
||||
}
|
||||
|
||||
/** Die Zeile, die der Server sendet (mit Zeilenumbruch am Ende). */
|
||||
public static String build(String secret, String server, long nowMillis) {
|
||||
String name = encode(server);
|
||||
return "DHMC1 " + name + " " + nowMillis + " " + sign(secret, name, nowMillis) + "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft eine empfangene Zeile.
|
||||
*
|
||||
* @return der Servername, oder null, wenn die Zeile fehlerhaft, die Signatur falsch oder die Zeit zu alt bzw. zu neu ist
|
||||
*/
|
||||
public static String verify(String secret, String line, long nowMillis, long maxSkewMillis) {
|
||||
if (secret == null || secret.isEmpty() || line == null) return null;
|
||||
String[] p = line.trim().split(" ");
|
||||
if (p.length != 4 || !"DHMC1".equals(p[0])) return null;
|
||||
long ts;
|
||||
try {
|
||||
ts = Long.parseLong(p[2]);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
if (Math.abs(nowMillis - ts) > maxSkewMillis) return null;
|
||||
byte[] expected = sign(secret, p[1], ts).getBytes(UTF8);
|
||||
if (!MessageDigest.isEqual(expected, p[3].toLowerCase().getBytes(UTF8))) return null;
|
||||
return decode(p[1]);
|
||||
}
|
||||
|
||||
/** Sendet die Meldung (blockierend, mit kurzer Zeitgrenze). @return null bei Erfolg, sonst der Grund des Fehlschlags */
|
||||
public static String send(String host, int port, String line, int timeoutMillis) {
|
||||
Socket s = new Socket();
|
||||
try {
|
||||
s.connect(new InetSocketAddress(host, port), timeoutMillis);
|
||||
s.setSoTimeout(timeoutMillis);
|
||||
OutputStream out = s.getOutputStream();
|
||||
out.write(line.getBytes(UTF8));
|
||||
out.flush();
|
||||
// Auf die Bestätigung des Proxys warten (eine Zeile), damit die Meldung sicher angekommen ist
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream(), UTF8));
|
||||
String ack = in.readLine();
|
||||
return ack != null && ack.startsWith("OK") ? null : "Proxy hat die Meldung abgelehnt (" + (ack == null ? "keine Antwort" : ack) + ")";
|
||||
} catch (IOException e) {
|
||||
return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
|
||||
} finally {
|
||||
try {
|
||||
s.close();
|
||||
} catch (IOException ignored) {
|
||||
// nichts mehr zu tun
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String sign(String secret, String encodedName, long ts) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secret.getBytes(UTF8), "HmacSHA256"));
|
||||
byte[] raw = mac.doFinal((encodedName + "|" + ts).getBytes(UTF8));
|
||||
StringBuilder sb = new StringBuilder(raw.length * 2);
|
||||
for (byte b : raw) sb.append(String.format("%02x", b & 0xff));
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String encode(String s) {
|
||||
try {
|
||||
return URLEncoder.encode(s, "UTF-8");
|
||||
} catch (java.io.UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String decode(String s) {
|
||||
try {
|
||||
return URLDecoder.decode(s, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,21 @@ public final class Settings {
|
||||
public final int monitorFailures;
|
||||
public final int cooldownMinutes;
|
||||
public final boolean ignoreNeverOnline;
|
||||
public final int restartGraceMinutes;
|
||||
public final int plannedRestartGraceMinutes;
|
||||
public final int notifyPort;
|
||||
public final String notifyBind;
|
||||
public final String notifySecret;
|
||||
public final List<String> monitorServers;
|
||||
|
||||
public final int heartbeatSeconds;
|
||||
public final boolean ticketOnCrash;
|
||||
public final boolean ticketOnShutdown;
|
||||
public final String serverName;
|
||||
public final String proxyNotifyHost;
|
||||
public final int proxyNotifyPort;
|
||||
public final String proxyNotifyName;
|
||||
public final String proxyNotifySecret;
|
||||
|
||||
public Settings(ConfigSource c) {
|
||||
String u = c.getString("deskhub.url", "").trim();
|
||||
@@ -74,6 +83,11 @@ public final class Settings {
|
||||
this.monitorFailures = clamp(c.getInt("monitor.failures-before-ticket", 3), 1, 100);
|
||||
this.cooldownMinutes = clamp(c.getInt("monitor.cooldown-minutes", 30), 0, 10080);
|
||||
this.ignoreNeverOnline = c.getBoolean("monitor.ignore-never-online", true);
|
||||
this.restartGraceMinutes = clamp(c.getInt("monitor.restart-grace-minutes", 5), 0, 120);
|
||||
this.plannedRestartGraceMinutes = clamp(c.getInt("monitor.planned-restart-grace-minutes", 15), 0, 240);
|
||||
this.notifyPort = clamp(c.getInt("monitor.notify-port", 0), 0, 65535);
|
||||
this.notifyBind = c.getString("monitor.notify-bind", "0.0.0.0").trim();
|
||||
this.notifySecret = c.getString("monitor.notify-secret", "").trim();
|
||||
List<String> servers = new ArrayList<String>();
|
||||
for (String s : c.getStringList("monitor.servers")) if (s != null && !s.trim().isEmpty()) servers.add(s.trim());
|
||||
this.monitorServers = Collections.unmodifiableList(servers);
|
||||
@@ -82,6 +96,33 @@ public final class Settings {
|
||||
this.ticketOnCrash = c.getBoolean("backend.ticket-on-crash", true);
|
||||
this.ticketOnShutdown = c.getBoolean("backend.ticket-on-shutdown", false);
|
||||
this.serverName = clip(c.getString("backend.server-name", "").trim(), 60);
|
||||
String target = c.getString("backend.proxy-notify", "").trim();
|
||||
String host = "";
|
||||
int port = 0;
|
||||
int colon = target.lastIndexOf(':');
|
||||
if (colon > 0) {
|
||||
host = target.substring(0, colon).trim();
|
||||
try {
|
||||
port = Integer.parseInt(target.substring(colon + 1).trim());
|
||||
} catch (NumberFormatException e) {
|
||||
host = "";
|
||||
}
|
||||
}
|
||||
boolean validTarget = !host.isEmpty() && port >= 1 && port <= 65535;
|
||||
this.proxyNotifyHost = validTarget ? host : "";
|
||||
this.proxyNotifyPort = validTarget ? port : 0;
|
||||
this.proxyNotifyName = clip(c.getString("backend.proxy-server-name", "").trim(), 60);
|
||||
this.proxyNotifySecret = c.getString("backend.proxy-secret", "").trim();
|
||||
}
|
||||
|
||||
/** Ob der Proxy auf Meldungen über geplante Neustarts lauscht (Anschluss und Geheimnis eingestellt). */
|
||||
public boolean listensForRestartNotices() {
|
||||
return notifyPort > 0 && !notifySecret.isEmpty();
|
||||
}
|
||||
|
||||
/** Ob dieser Server geplante Neustarts an den Proxy meldet. */
|
||||
public boolean announcesRestarts() {
|
||||
return !proxyNotifyHost.isEmpty() && !proxyNotifySecret.isEmpty() && !proxyNotifyName.isEmpty();
|
||||
}
|
||||
|
||||
/** Probleme, die das Anlegen von Tickets verhindern (leer = alles in Ordnung). */
|
||||
@@ -98,6 +139,9 @@ public final class Settings {
|
||||
/** Hinweise, die das Anlegen nicht verhindern. */
|
||||
public List<String> warnings() {
|
||||
List<String> w = new ArrayList<String>();
|
||||
if (notifyPort > 0 && notifySecret.isEmpty()) w.add("monitor.notify-port ist gesetzt, aber monitor.notify-secret fehlt: Meldungen über geplante Neustarts werden nicht angenommen.");
|
||||
if (notifySecret.length() > 0 && notifySecret.length() < 16) w.add("monitor.notify-secret ist kurz: bitte mindestens 16 zufällige Zeichen verwenden.");
|
||||
if (!proxyNotifyHost.isEmpty() && (proxyNotifySecret.isEmpty() || proxyNotifyName.isEmpty())) w.add("backend.proxy-notify ist gesetzt, aber backend.proxy-secret oder backend.proxy-server-name fehlt: Neustarts werden nicht gemeldet.");
|
||||
if (url.startsWith("http://")) w.add("deskhub.url verwendet http: der API-Schlüssel würde unverschlüsselt übertragen. Bitte https:// verwenden.");
|
||||
return w;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,17 @@ monitor:
|
||||
interval-seconds: 30
|
||||
# Wie oft die Prüfung hintereinander fehlschlagen muss, bevor ein Ticket entsteht (verhindert Tickets bei kurzen Aussetzern)
|
||||
failures-before-ticket: 3
|
||||
# Schonfrist für Neustarts in Minuten: Ein Ticket entsteht erst, wenn der Server mindestens so lange nicht erreichbar ist.
|
||||
# Der Proxy kann einen geplanten Neustart nicht von einem Absturz unterscheiden. Stelle hier etwas mehr ein, als dein Server zum Neustarten braucht
|
||||
# (meist 2 bis 5). 0 = keine Schonfrist (Ticket gleich nach den Fehlschlägen oben).
|
||||
restart-grace-minutes: 5
|
||||
# Meldet ein Server selbst einen geplanten Neustart (siehe backend.proxy-notify), gilt statt dessen diese längere Frist in Minuten.
|
||||
planned-restart-grace-minutes: 15
|
||||
# Meldungen der Server über geplante Neustarts annehmen: TCP-Anschluss und gemeinsames Geheimnis (mindestens 16 zufällige Zeichen).
|
||||
# notify-port 0 = aus. Der Anschluss sollte nur aus dem Netz eurer Server erreichbar sein (Firewall); ohne richtige Signatur wird nichts angenommen.
|
||||
notify-port: 0
|
||||
notify-bind: "0.0.0.0"
|
||||
notify-secret: ""
|
||||
# Frühestens nach so vielen Minuten gibt es für denselben Server wieder ein Ticket (verhindert Ticket-Flut, wenn ein Server ständig ausfällt)
|
||||
cooldown-minutes: 30
|
||||
# true: Server, die seit dem Start des Proxys noch nie online waren, werden ignoriert (z. B. absichtlich abgeschaltete Server)
|
||||
@@ -83,6 +94,13 @@ backend:
|
||||
# Ticket beim nächsten Start, wenn der Server zuvor abgestürzt oder hart beendet worden war
|
||||
ticket-on-crash: true
|
||||
# Ticket auch beim normalen Herunterfahren (/stop, Neustart). Meist unerwünscht, deshalb aus.
|
||||
# Gilt nur für das Plugin auf dem Server (Spigot/Paper), NICHT für den BungeeCord-Proxy: Dort gilt die Schonfrist oben (monitor.restart-grace-minutes).
|
||||
ticket-on-shutdown: false
|
||||
# Name des Servers in den Tickets. Leer = "Minecraft-Server (Port 25565)"
|
||||
server-name: ""
|
||||
# Dem BungeeCord-Proxy beim Herunterfahren melden, dass der Neustart geplant ist (dann wartet er länger, bevor er ein Ticket anlegt).
|
||||
# proxy-notify: Adresse und notify-port des Proxys, z. B. "10.0.0.5:25590". proxy-secret: dasselbe Geheimnis wie monitor.notify-secret.
|
||||
# proxy-server-name: Name dieses Servers in der config.yml des Proxys (Abschnitt servers), z. B. "lobby".
|
||||
proxy-notify: ""
|
||||
proxy-secret: ""
|
||||
proxy-server-name: ""
|
||||
|
||||
@@ -91,6 +91,135 @@ class CoreTest {
|
||||
assertEquals("3 Tage 2 Std.", Template.formatDuration((3 * 24 + 2) * 3600000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void plannedRestartNoticeGivesLongerGrace() {
|
||||
OutageTracker t = new OutageTracker(3, 30, true, 5, 15);
|
||||
long now = 1_000_000;
|
||||
assertEquals(OutageTracker.Decision.NONE, t.report("lobby", true, now));
|
||||
// Server meldet einen geplanten Neustart und ist dann 12 Minuten weg: kein Ticket (Frist für geplante Neustarts: 15 Minuten)
|
||||
t.markPlanned("lobby", now);
|
||||
assertTrue(t.isPlanned("lobby", now + 1000));
|
||||
for (int i = 0; i < 24; i++) assertEquals(OutageTracker.Decision.NONE, t.report("lobby", false, now += 30000));
|
||||
assertEquals(OutageTracker.Decision.NONE, t.report("lobby", true, now += 30000));
|
||||
assertFalse(t.isPlanned("lobby", now + 1000), "nach der Rückkehr gilt die Meldung nicht mehr");
|
||||
// Bleibt der Server nach der geplanten Frist weg, gibt es das Ticket doch
|
||||
t.markPlanned("lobby", now);
|
||||
int tickets = 0;
|
||||
for (int i = 0; i < 40; i++) if (t.report("lobby", false, now += 30000) == OutageTracker.Decision.OPEN_TICKET) tickets++;
|
||||
assertEquals(1, tickets, "nach 15 Minuten ohne Rückkehr kommt das Ticket");
|
||||
// Ohne Meldung gilt nur die kurze Schonfrist
|
||||
OutageTracker u = new OutageTracker(3, 30, true, 5, 15);
|
||||
u.report("a", true, now);
|
||||
int t2 = 0;
|
||||
for (int i = 0; i < 12; i++) if (u.report("a", false, now += 30000) == OutageTracker.Decision.OPEN_TICKET) t2++;
|
||||
assertEquals(1, t2);
|
||||
// Frist für geplante Neustarts 0 = Meldungen wirken nicht
|
||||
OutageTracker v = new OutageTracker(3, 30, true, 5, 0);
|
||||
v.report("b", true, now);
|
||||
v.markPlanned("b", now);
|
||||
assertFalse(v.isPlanned("b", now + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartNoticeIsSignedAndFresh() {
|
||||
long now = 1_700_000_000_000L;
|
||||
String secret = "ein-langes-gemeinsames-geheimnis";
|
||||
String line = RestartNotice.build(secret, "Lobby Süd", now);
|
||||
assertTrue(line.startsWith("DHMC1 ") && line.endsWith("\n"));
|
||||
assertEquals("Lobby Süd", RestartNotice.verify(secret, line, now + 5000, 60000L));
|
||||
assertNull(RestartNotice.verify("falsches-geheimnis-xxxxxxxx", line, now, 60000L), "falsches Geheimnis");
|
||||
assertNull(RestartNotice.verify(secret, line, now + 120000L, 60000L), "zu alt (Nachspielen)");
|
||||
assertNull(RestartNotice.verify(secret, line, now - 120000L, 60000L), "aus der Zukunft");
|
||||
assertNull(RestartNotice.verify(secret, line.replace("Lobby", "Hub"), now, 60000L), "veränderter Servername");
|
||||
assertNull(RestartNotice.verify(secret, "DHMC1 lobby abc def", now, 60000L), "fehlerhafte Zeit");
|
||||
assertNull(RestartNotice.verify(secret, "Hallo Welt", now, 60000L), "kein Protokoll");
|
||||
assertNull(RestartNotice.verify("", line, now, 60000L), "ohne Geheimnis wird nichts angenommen");
|
||||
assertNull(RestartNotice.verify(secret, null, now, 60000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartNoticeTravelsOverTcp() throws Exception {
|
||||
final String secret = "ein-langes-gemeinsames-geheimnis";
|
||||
final java.net.ServerSocket ss = new java.net.ServerSocket(0, 1, java.net.InetAddress.getByName("127.0.0.1"));
|
||||
final String[] got = new String[1];
|
||||
Thread th = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
java.net.Socket c = ss.accept();
|
||||
java.io.BufferedReader r = new java.io.BufferedReader(new java.io.InputStreamReader(c.getInputStream(), "UTF-8"));
|
||||
got[0] = RestartNotice.verify(secret, r.readLine(), System.currentTimeMillis(), 60000L);
|
||||
c.getOutputStream().write((got[0] != null ? "OK\n" : "ERR\n").getBytes("UTF-8"));
|
||||
c.getOutputStream().flush();
|
||||
c.close();
|
||||
} catch (Exception ignored) {
|
||||
// Test schlägt über got[0] fehl
|
||||
}
|
||||
}
|
||||
});
|
||||
th.start();
|
||||
String err = RestartNotice.send("127.0.0.1", ss.getLocalPort(), RestartNotice.build(secret, "lobby", System.currentTimeMillis()), 3000);
|
||||
th.join(3000);
|
||||
ss.close();
|
||||
assertNull(err, "Senden gelingt: " + err);
|
||||
assertEquals("lobby", got[0]);
|
||||
// Nicht erreichbarer Proxy: Fehlermeldung statt Absturz
|
||||
assertNotNull(RestartNotice.send("127.0.0.1", 1, RestartNotice.build(secret, "lobby", System.currentTimeMillis()), 500));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartNoticeSettings() {
|
||||
Map<String, Object> m = valid("https://support.example.de");
|
||||
m.put("monitor.notify-port", 25590);
|
||||
m.put("monitor.notify-secret", "ein-langes-gemeinsames-geheimnis");
|
||||
m.put("backend.proxy-notify", "10.0.0.5:25590");
|
||||
m.put("backend.proxy-secret", "ein-langes-gemeinsames-geheimnis");
|
||||
m.put("backend.proxy-server-name", "lobby");
|
||||
Settings s = new Settings(cfg(m));
|
||||
assertTrue(s.listensForRestartNotices());
|
||||
assertTrue(s.announcesRestarts());
|
||||
assertEquals("10.0.0.5", s.proxyNotifyHost);
|
||||
assertEquals(25590, s.proxyNotifyPort);
|
||||
assertEquals(15, s.plannedRestartGraceMinutes);
|
||||
assertTrue(s.warnings().isEmpty(), s.warnings().toString());
|
||||
// Standard: alles aus
|
||||
Settings d = new Settings(cfg(valid("https://support.example.de")));
|
||||
assertFalse(d.listensForRestartNotices());
|
||||
assertFalse(d.announcesRestarts());
|
||||
// Unvollständig: Warnung
|
||||
Map<String, Object> bad = valid("https://support.example.de");
|
||||
bad.put("backend.proxy-notify", "10.0.0.5:25590");
|
||||
assertFalse(new Settings(cfg(bad)).announcesRestarts());
|
||||
assertFalse(new Settings(cfg(bad)).warnings().isEmpty());
|
||||
Map<String, Object> badPort = valid("https://support.example.de");
|
||||
badPort.put("backend.proxy-notify", "ohne-port");
|
||||
assertEquals("", new Settings(cfg(badPort)).proxyNotifyHost);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartGracePreventsTicketForPlannedRestart() {
|
||||
OutageTracker t = new OutageTracker(3, 30, true, 5);
|
||||
long now = 1_000_000;
|
||||
assertEquals(OutageTracker.Decision.NONE, t.report("lobby", true, now));
|
||||
// Neustart: 4 Minuten nicht erreichbar (alle 30 s geprüft), dann wieder online: kein Ticket, auch keine "Wiederhergestellt"-Meldung
|
||||
for (int i = 0; i < 8; i++) assertEquals(OutageTracker.Decision.NONE, t.report("lobby", false, now += 30000));
|
||||
assertEquals(OutageTracker.Decision.NONE, t.report("lobby", true, now += 30000));
|
||||
// echter Ausfall: nach 5 Minuten gibt es das Ticket, nur einmal
|
||||
OutageTracker.Decision last = OutageTracker.Decision.NONE;
|
||||
int tickets = 0;
|
||||
for (int i = 0; i < 14; i++) {
|
||||
last = t.report("lobby", false, now += 30000);
|
||||
if (last == OutageTracker.Decision.OPEN_TICKET) tickets++;
|
||||
}
|
||||
assertEquals(1, tickets);
|
||||
assertEquals(OutageTracker.Decision.RECOVERED, t.report("lobby", true, now += 30000));
|
||||
// ohne Schonfrist wie bisher nach den Fehlschlägen
|
||||
OutageTracker none = new OutageTracker(3, 30, true, 0);
|
||||
none.report("a", true, now);
|
||||
none.report("a", false, now += 1000);
|
||||
none.report("a", false, now += 1000);
|
||||
assertEquals(OutageTracker.Decision.OPEN_TICKET, none.report("a", false, now += 1000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outageTrackerRules() {
|
||||
OutageTracker t = new OutageTracker(3, 30, true);
|
||||
|
||||
Reference in New Issue
Block a user