- 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>
81 lines
4.0 KiB
C#
81 lines
4.0 KiB
C#
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;
|
|
}
|