Files
MSCO-Dedicated-Server/PanelTls.cs
T
MrMahoandClaude Opus 5.5 4cc71aa079 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>
2026-10-03 18:38:54 +02:00

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;
}