Server 2.10.4: HTTPS for the web panel and hostile client hardening
- Panel over HTTPS on the game port (TLS handshake detected like HTTP); self-signed certificate in data/panel_cert.pfx (fingerprint in the log) or an own PFX (PanelCertificatePath); remote HTTP is redirected to HTTPS (not locally / behind a proxy); cookie Secure over HTTPS; PanelHttps=false turns it off - Panel login: also locked per user name (16 failures / 15 min), failure lists bounded, user names in the log without control characters - panel_users.json, panel_sessions.json, server.json, certificate: owner-only on Linux - Players: names/chat without control or direction characters (no fake log lines), no '<' '>' in names; '* ' system-style chat, msco.* messages (time/weather) and MSCO_INSTALL only from admins; NaN/absurd money changes dropped (could break the shared money for good) - Extra headers: Permissions-Policy, Cross-Origin-Opener-Policy - 11 new tests (165 pass) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace MSCO.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Zertifikat für HTTPS im Web-Panel. Ohne eigenes Zertifikat (PanelCertificatePath) erzeugt der Server einmal ein
|
||||
/// selbst signiertes und speichert es in data/panel_cert.pfx (nur für den Server-Benutzer lesbar). Der Browser warnt
|
||||
/// beim ersten Besuch ("nicht vertrauenswürdig") - die Verbindung ist trotzdem verschlüsselt; den Fingerabdruck zeigt
|
||||
/// die Server-Konsole zum Vergleichen.
|
||||
/// </summary>
|
||||
public static class PanelTls
|
||||
{
|
||||
public const string FileName = "panel_cert.pfx";
|
||||
|
||||
public static X509Certificate2 Load(ServerConfig config, string dataDir, Action<string> log)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(config.PanelCertificatePath))
|
||||
{
|
||||
string path = Path.IsPathRooted(config.PanelCertificatePath) ? config.PanelCertificatePath : Path.Combine(dataDir, config.PanelCertificatePath);
|
||||
var own = X509CertificateLoader.LoadPkcs12FromFile(path, config.PanelCertificatePassword ?? "");
|
||||
log($"Web panel HTTPS certificate: {own.Subject} (from {path})");
|
||||
return own;
|
||||
}
|
||||
|
||||
string file = Path.Combine(dataDir, FileName);
|
||||
X509Certificate2 cert = null;
|
||||
if (File.Exists(file))
|
||||
{
|
||||
try
|
||||
{
|
||||
cert = X509CertificateLoader.LoadPkcs12FromFile(file, null);
|
||||
if (cert.NotAfter < DateTime.Now.AddDays(30)) cert = null; // bald abgelaufen: neu erzeugen
|
||||
}
|
||||
catch { cert = null; }
|
||||
}
|
||||
if (cert == null)
|
||||
{
|
||||
cert = CreateSelfSigned(config);
|
||||
Directory.CreateDirectory(dataDir);
|
||||
File.WriteAllBytes(file, cert.Export(X509ContentType.Pfx));
|
||||
Restrict(file);
|
||||
cert = X509CertificateLoader.LoadPkcs12FromFile(file, null); // aus der Datei laden (Windows braucht einen gespeicherten Schlüssel)
|
||||
log("Web panel: created a self-signed HTTPS certificate (" + FileName + ")");
|
||||
}
|
||||
log("Web panel HTTPS fingerprint (SHA-256): " + Fingerprint(cert));
|
||||
return cert;
|
||||
}
|
||||
|
||||
public static string Fingerprint(X509Certificate2 cert) =>
|
||||
string.Join(":", Convert.ToHexString(cert.GetCertHash(HashAlgorithmName.SHA256)).Chunk(2).Select(c => new string(c)));
|
||||
|
||||
static X509Certificate2 CreateSelfSigned(ServerConfig config)
|
||||
{
|
||||
using var rsa = RSA.Create(3072);
|
||||
var req = new CertificateRequest("CN=MSCO Server Panel", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
|
||||
req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true));
|
||||
req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
|
||||
var san = new SubjectAlternativeNameBuilder();
|
||||
san.AddDnsName("localhost");
|
||||
san.AddIpAddress(IPAddress.Loopback);
|
||||
string publicAddress = (config.PublicAddress ?? "").Trim();
|
||||
if (IPAddress.TryParse(publicAddress, out var ip)) san.AddIpAddress(ip);
|
||||
else if (publicAddress.Length > 0 && Uri.CheckHostName(publicAddress) == UriHostNameType.Dns) san.AddDnsName(publicAddress);
|
||||
req.CertificateExtensions.Add(san.Build());
|
||||
return req.CreateSelfSigned(DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now.AddYears(5));
|
||||
}
|
||||
|
||||
/// <summary>Datei nur für den Benutzer des Servers lesbar (Linux).</summary>
|
||||
public static void Restrict(string file)
|
||||
{
|
||||
if (OperatingSystem.IsWindows()) return;
|
||||
try { File.SetUnixFileMode(file, UnixFileMode.UserRead | UnixFileMode.UserWrite); } catch { }
|
||||
}
|
||||
|
||||
/// <summary>Beginnen die Bytes wie ein TLS-Handshake (ClientHello: 16 03 0x)?</summary>
|
||||
public static bool LooksLikeTls(byte[] b, int n) => n >= 3 && b[0] == 0x16 && b[1] == 0x03 && b[2] <= 0x04;
|
||||
}
|
||||
Reference in New Issue
Block a user