diff --git a/src/Prospect.Client.Config/Program.cs b/src/Prospect.Client.Config/Program.cs index 3df71cd..e3935da 100644 --- a/src/Prospect.Client.Config/Program.cs +++ b/src/Prospect.Client.Config/Program.cs @@ -158,28 +158,56 @@ internal static class Program // ---- Certificate ------------------------------------------------------- private static void ImportCert() { - Console.Write("Chemin du certificat (.crt) : "); - var path = Console.ReadLine()?.Trim().Trim('"'); - if (string.IsNullOrEmpty(path) || !File.Exists(path)) { Console.WriteLine("⚠ Fichier introuvable."); return; } - - if (OperatingSystem.IsWindows()) + // Prefer fetching the cert live from the server the client points at (backend.txt), so + // it's always the current one; fall back to a .crt file. + X509Certificate2? cert = null; + var target = ReadBackendUrl(); + if (target != null && TryFetchServerCert(target, out cert)) + Console.WriteLine($"Certificat récupéré depuis {target}"); + else { - ImportCertWindows(path); - return; + Console.Write("Chemin d'un certificat (.crt) : "); + var p = Console.ReadLine()?.Trim().Trim('"'); + if (string.IsNullOrEmpty(p) || !File.Exists(p)) { Console.WriteLine("⚠ Annulé."); return; } + try { cert = new X509Certificate2(p); } catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); return; } } + if (cert == null) { Console.WriteLine("⚠ Aucun certificat."); return; } + Console.WriteLine($" empreinte : {cert.Thumbprint}"); - // Linux/Proton : le certificat doit être approuvé DANS le préfixe Wine du jeu. - var prefix = FindProtonPrefix() ?? $"~/.steam/steam/steamapps/compatdata/{AppId}/pfx"; - Console.WriteLine("Sous Proton/Wine, ajoute le certificat au préfixe du jeu (wine requis) :"); - Console.WriteLine($" WINEPREFIX=\"{prefix}\" wine certutil -addstore -f Root \"{path}\""); + if (OperatingSystem.IsWindows()) ImportCertWindows(cert); + else ImportCertProton(cert); + } + + private static string? ReadBackendUrl() + { + if (string.IsNullOrEmpty(_settings.GameFolder)) return null; + var f = Path.Combine(_settings.GameFolder, "backend.txt"); + try { return File.Exists(f) ? File.ReadAllText(f).Trim() : null; } catch { return null; } + } + + private static bool TryFetchServerCert(string url, out X509Certificate2? cert) + { + cert = null; + try + { + var uri = new Uri(url); + using var tcp = new System.Net.Sockets.TcpClient(); + tcp.Connect(uri.Host, uri.Port <= 0 ? 443 : uri.Port); + X509Certificate2? captured = null; + using var ssl = new System.Net.Security.SslStream(tcp.GetStream(), false, + (_, c, _, _) => { if (c != null) captured = new X509Certificate2(c.Export(X509ContentType.Cert)); return true; }); + ssl.AuthenticateAsClient(uri.Host); + cert = captured; + return cert != null; + } + catch { return false; } } [System.Runtime.Versioning.SupportedOSPlatform("windows")] - private static void ImportCertWindows(string path) + private static void ImportCertWindows(X509Certificate2 cert) { try { - using var cert = new X509Certificate2(path); using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadWrite); store.Add(cert); @@ -188,16 +216,132 @@ internal static class Program catch (Exception ex) { Console.WriteLine("✖ Import : " + ex.Message); } } - private static string? FindProtonPrefix() + // Trust the cert inside the game's Proton/Wine prefix by writing the serialized cert blob + // into the prefix Root store via `wine reg import` (the wine `certutil` tool is unreliable + // under Proton). The prefix is chosen explicitly, so it works even though the client runs + // as a non-Steam shortcut (whose appid isn't the game's 868270). + private static void ImportCertProton(X509Certificate2 cert) + { + var wine = FindProtonWine(); + if (wine == null) { Console.WriteLine("⚠ Aucun wine Proton trouvé (Steam/Proton installé ?)."); return; } + var prefix = SelectPrefix(); + if (prefix == null) return; + + // Wine serialized cert blob: SHA1-hash property (id 3) + encoded cert property (id 0x20). + var der = cert.RawData; + var sha1 = cert.GetCertHash(); + byte[] blob; + using (var ms = new MemoryStream()) + { + void Prop(uint id, byte[] data) + { + ms.Write(BitConverter.GetBytes(id)); + ms.Write(BitConverter.GetBytes(1u)); + ms.Write(BitConverter.GetBytes((uint)data.Length)); + ms.Write(data); + } + Prop(3, sha1); + Prop(0x20, der); + blob = ms.ToArray(); + } + var thumb = Convert.ToHexString(sha1); + var hex = string.Join(",", blob.Select(b => b.ToString("x2"))); + var regFile = Path.Combine(Path.GetTempPath(), "prospect-cert.reg"); + File.WriteAllText(regFile, + "REGEDIT4\r\n\r\n" + + $"[HKEY_LOCAL_MACHINE\\Software\\Microsoft\\SystemCertificates\\Root\\Certificates\\{thumb}]\r\n" + + $"\"Blob\"=hex:{hex}\r\n"); + + Console.WriteLine("⚠ Ferme complètement le jeu avant de continuer (sinon le registre ne sera pas pris en compte)."); + Console.Write("Appuie sur Entrée pour importer… "); + Console.ReadLine(); + + var wineserver = Path.Combine(Path.GetDirectoryName(wine)!, "wineserver"); + var regWinePath = "Z:" + regFile.Replace('/', '\\'); // Z: maps to / inside the prefix + RunWine(wineserver, "-k", prefix); // stop any running wineserver (flush) + RunWine(wine, $"reg import \"{regWinePath}\"", prefix); + RunWine(wineserver, "-k", prefix); // flush the change to disk + + var sysreg = Path.Combine(prefix, "system.reg"); + var ok = File.Exists(sysreg) && File.ReadAllText(sysreg).Contains(thumb, StringComparison.OrdinalIgnoreCase); + Console.WriteLine(ok + ? $"✔ Certificat importé dans le préfixe : {prefix}\n Relance le jeu." + : "✖ Import non confirmé — assure-toi que le jeu est fermé et réessaie."); + } + + private static void RunWine(string exe, string args, string prefix) + { + try + { + var psi = new ProcessStartInfo(exe, args) + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + psi.Environment["WINEPREFIX"] = prefix; + psi.Environment["WINEDEBUG"] = "-all"; + using var p = Process.Start(psi); + p?.WaitForExit(20000); + } + catch (Exception ex) { Console.WriteLine(" (wine) " + ex.Message); } + } + + private static string? FindProtonWine() + { + foreach (var root in SteamRoots()) + { + var common = Path.Combine(root, "steamapps", "common"); + if (!Directory.Exists(common)) continue; + var candidates = Directory.GetDirectories(common, "Proton*") + .Select(d => Path.Combine(d, "files", "bin", "wine")) + .Where(File.Exists) + .OrderByDescending(w => w.Contains("Experimental")) + .ThenByDescending(File.GetLastWriteTimeUtc) + .ToList(); + if (candidates.Count > 0) return candidates[0]; + } + return null; + } + + private static string? SelectPrefix() + { + if (!string.IsNullOrEmpty(_settings.WinePrefix) && Directory.Exists(_settings.WinePrefix)) + return _settings.WinePrefix; + + var prefixes = new List(); + foreach (var root in SteamRoots()) + { + var cd = Path.Combine(root, "steamapps", "compatdata"); + if (!Directory.Exists(cd)) continue; + foreach (var d in Directory.GetDirectories(cd)) + { + var pfx = Path.Combine(d, "pfx"); + if (File.Exists(Path.Combine(pfx, "system.reg"))) prefixes.Add(Path.GetFullPath(pfx)); + } + } + prefixes = prefixes.Distinct() + .OrderByDescending(p => File.GetLastWriteTimeUtc(Path.Combine(p, "system.reg"))) + .ToList(); + if (prefixes.Count == 0) { Console.WriteLine("⚠ Aucun préfixe Proton trouvé."); return null; } + + Console.WriteLine("Préfixes Proton détectés (le plus récemment utilisé en premier = sûrement ton jeu) :"); + for (var i = 0; i < prefixes.Count; i++) + Console.WriteLine($" {i + 1}) {prefixes[i]}"); + Console.Write("Numéro du préfixe (Entrée = 1) : "); + var raw = Console.ReadLine()?.Trim(); + var idx = string.IsNullOrEmpty(raw) ? 0 : (int.TryParse(raw, out var n) ? n - 1 : -1); + if (idx < 0 || idx >= prefixes.Count) { Console.WriteLine("⚠ Choix invalide."); return null; } + _settings.WinePrefix = prefixes[idx]; + SaveSettings(); + return _settings.WinePrefix; + } + + private static IEnumerable SteamRoots() { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - foreach (var dir in new[] - { - Path.Combine(home, ".steam", "steam", "steamapps", "compatdata", AppId, "pfx"), - Path.Combine(home, ".local", "share", "Steam", "steamapps", "compatdata", AppId, "pfx"), - }) - if (Directory.Exists(dir)) return dir; - return null; + yield return Path.Combine(home, ".steam", "steam"); + yield return Path.Combine(home, ".local", "share", "Steam"); } // ---- Launch ------------------------------------------------------------ @@ -255,5 +399,6 @@ internal static class Program private sealed class Settings { public string GameFolder { get; set; } = ""; + public string WinePrefix { get; set; } = ""; } }