summary refs log tree commit diff
path: root/ReferenceClientProxyImplementation/Tasks/Startup
diff options
context:
space:
mode:
Diffstat (limited to 'ReferenceClientProxyImplementation/Tasks/Startup')
-rw-r--r--ReferenceClientProxyImplementation/Tasks/Startup/BuildClientTask.cs39
-rw-r--r--ReferenceClientProxyImplementation/Tasks/Startup/InitClientStoreTask.cs173
-rw-r--r--ReferenceClientProxyImplementation/Tasks/Startup/PatchClientAssetsTask.cs37
-rw-r--r--ReferenceClientProxyImplementation/Tasks/Startup/SanitiseConfigPathsTask.cs15
4 files changed, 264 insertions, 0 deletions
diff --git a/ReferenceClientProxyImplementation/Tasks/Startup/BuildClientTask.cs b/ReferenceClientProxyImplementation/Tasks/Startup/BuildClientTask.cs
new file mode 100644

index 0000000..8718fc5 --- /dev/null +++ b/ReferenceClientProxyImplementation/Tasks/Startup/BuildClientTask.cs
@@ -0,0 +1,39 @@ +// using ReferenceClientProxyImplementation.Configuration; +// using ReferenceClientProxyImplementation.Helpers; +// +// namespace ReferenceClientProxyImplementation.Tasks.Startup; +// +// public class BuildClientTask(ProxyConfiguration proxyConfig) : ITask { +// public int GetOrder() => 10; +// +// public string GetName() => "Build updated test client"; +// +// public async Task Execute() { +// var hc = new HttpClient(); +// if (proxyConfig.AssetCache.WipeOnStartup) { +// Directory.Delete(proxyConfig.AssetCache.DiskCachePath, true); +// Directory.CreateDirectory(proxyConfig.AssetCache.DiskCachePath); +// } +// +// // if (!proxyConfig.TestClient.Enabled || +// // !proxyConfig.TestClient.UseLatest) { +// // Console.WriteLine("[Client Updater] Test client is disabled or not set to use latest version, skipping!"); +// return; +// // } +// +// Console.WriteLine("[Client updater] Fetching client"); +// var client = HtmlUtils.CleanupHtml(await hc.GetStringAsync("https://canary.discord.com/channels/@me")); +// Console.WriteLine("[Client updater] Building client..."); +// var target = File.ReadAllText("Resources/Pages/index-template.html"); +// var lines = client.Split("\n"); +// target = target.Replace("<!--prefetch_script-->", +// string.Join("\n", lines.Where(x => x.Contains("link rel=\"prefetch\" as=\"script\"")))); +// target = target.Replace("<!--client_script-->", +// string.Join("\n", lines.Where(x => x.Contains("<script src=")))); +// target = target.Replace("<!--client_css-->", +// string.Join("\n", lines.Where(x => x.Contains("link rel=\"stylesheet\"")))); +// target = target.Replace("integrity", "hashes"); +// File.WriteAllText("Resources/Pages/index-updated.html", target); +// Console.WriteLine("[Client updater] Finished building client!"); +// } +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Tasks/Startup/InitClientStoreTask.cs b/ReferenceClientProxyImplementation/Tasks/Startup/InitClientStoreTask.cs new file mode 100644
index 0000000..4aeab96 --- /dev/null +++ b/ReferenceClientProxyImplementation/Tasks/Startup/InitClientStoreTask.cs
@@ -0,0 +1,173 @@ +using System.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; +using ArcaneLibs.Extensions; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Tasks.Startup; + +public partial class InitClientStoreService(ProxyConfiguration proxyConfig) : ITask { + public int GetOrder() => 0; + + public string GetName() => "Get client revision"; + + public async Task Execute() { + switch (proxyConfig.TestClient.Revision) { + case "canary": + proxyConfig.TestClient.RevisionBaseUrl = "https://canary.discord.com"; + proxyConfig.TestClient.RevisionPath = await GetRevisionPathFromUrl("canary", "https://canary.discord.com/app"); + break; + case "ptb": + proxyConfig.TestClient.RevisionBaseUrl = "https://ptb.discord.com"; + proxyConfig.TestClient.RevisionPath = await GetRevisionPathFromUrl("ptb", "https://ptb.discord.com/app"); + break; + case "stable": + proxyConfig.TestClient.RevisionBaseUrl = "https://discord.com"; + proxyConfig.TestClient.RevisionPath = await GetRevisionPathFromUrl("stable", "https://discord.com/app"); + break; + default: + if (proxyConfig.TestClient.RevisionPath == null) { + throw new Exception("Test client revision path is not set!"); + } + + break; + } + } + + private async Task<string> GetRevisionPathFromUrl(string rev, string url) { + using var hc = new HttpClient(); + using var response = await hc.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + var normalisedContent = StripNonces(content); + var hash = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(normalisedContent)); + var knownHashes = await GetKnownRevisionHashes("src/app.html"); + var currentRevisionFilePath = Path.Combine(proxyConfig.AssetCache.DiskCacheBaseDirectory, "currentRevision"); + var previousRevision = Path.Exists(currentRevisionFilePath) ? await File.ReadAllTextAsync(currentRevisionFilePath) : ""; + var revisionName = rev; + + if (knownHashes.Any(x => x.Value.SequenceEqual(hash))) { + Console.WriteLine($"[InitClientStoreTask] Found known revision '{rev}' with hash {hash.AsHexString().Replace(" ", "")}!"); + revisionName = knownHashes.First(x => x.Value.SequenceEqual(hash)).Key; + } + else { + Console.WriteLine($"[InitClientStoreTask] No known revision found for hash {hash.AsHexString().Replace(" ", "")}, creating new revision directory!"); + if (response.Headers.Contains("X-Build-Id")) { + revisionName = "buildId_" + response.Headers.GetValues("X-Build-Id").FirstOrDefault(); + Console.WriteLine("[InitClientStoreTask] Using build ID from X-Build-Id header: " + revisionName); + } + } + + var revisionPath = Path.Combine(proxyConfig.AssetCache.DiskCacheBaseDirectory, revisionName); + Console.WriteLine($"[InitClientStoreTask] Saving revision '{revisionName}' to {revisionPath}..."); + PrepareRevisionDirectory(revisionPath); + await File.WriteAllTextAsync(Path.Combine(revisionPath, "src", "app.html"), content); + await File.WriteAllTextAsync(Path.Combine(proxyConfig.AssetCache.DiskCacheBaseDirectory, "currentRevision"), revisionName); + + //also download dev page + using var devResponse = await hc.GetAsync(url.Replace("/app", "/developers/applications")); + var devContent = await devResponse.Content.ReadAsStringAsync(); + await File.WriteAllTextAsync(Path.Combine(revisionPath, "src", "developers.html"), devContent); + + //...and popout + using var popoutResponse = await hc.GetAsync(url.Replace("/app", "/popout")); + var popoutContent = await popoutResponse.Content.ReadAsStringAsync(); + await File.WriteAllTextAsync(Path.Combine(revisionPath, "src", "popout.html"), popoutContent); + + if (proxyConfig.AssetCache.DitchPatchedOnStartup) { + Directory.Delete(Path.Combine(revisionPath, "patched"), true); + Directory.CreateDirectory(Path.Combine(revisionPath, "patched")); + } + + if (previousRevision != revisionName || true) { + foreach (var argv in proxyConfig.AssetCache.ExecOnRevisionChange) { + try { + var psi = new ProcessStartInfo(argv[0], argv[1..].Select(a => a.Replace("{revisionPath}", revisionPath))) { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + using var process = Process.Start(psi); + if (process != null) { + _ = process.StandardOutput.ReadToEndAsync(); + _ = process.StandardError.ReadToEndAsync(); + Console.WriteLine($"[InitClientStoreTask] Executing post-revision change command: {argv[0]} {string.Join(" ", argv[1..])}"); + } + else { + Console.WriteLine($"[InitClientStoreTask] Failed to start post-revision change command: {argv[0]} {string.Join(" ", argv[1..])}"); + } + } + catch (Exception e) { + Console.WriteLine($"[InitClientStoreTask] Failed to start post-revision change command: {argv[0]} {string.Join(" ", argv[1..])}\n{e}"); + } + } + } + + return revisionPath; + } + + private static void PrepareRevisionDirectory(string revisionPath, bool dropPatched = false) { + Directory.CreateDirectory(revisionPath); + Directory.CreateDirectory(Path.Combine(revisionPath, "src")); + Directory.CreateDirectory(Path.Combine(revisionPath, "formatted")); + Directory.CreateDirectory(Path.Combine(revisionPath, "patched")); + } + + private async Task<Dictionary<string, byte[]>> GetKnownRevisionHashes(string file) { + if (!Directory.Exists(proxyConfig.AssetCache.DiskCacheBaseDirectory)) + Directory.CreateDirectory(proxyConfig.AssetCache.DiskCacheBaseDirectory); + + var revisionHashTasks = Directory + .GetDirectories(proxyConfig.AssetCache.DiskCacheBaseDirectory) + .Select(dir => GetKnownRevisionHash(dir, file)); + + var revisionHashes = await Task.WhenAll(revisionHashTasks); + return revisionHashes + .OfType<(string RevisionId, byte[] Hash)>() + .ToDictionary( + x => x.RevisionId, + x => x.Hash + ); + } + + private async Task<(string RevisionId, byte[] Hash)?> GetKnownRevisionHash(string dir, string file) { + var hashFile = Path.Combine(dir, file); + if (File.Exists(hashFile)) { + var content = StripNonces(await File.ReadAllTextAsync(hashFile)); + var hash = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(content)); + var result = (new DirectoryInfo(dir).Name, hash); + return result; + } + + Console.WriteLine($"[InitClientStoreTask] '{file}' not found in client revision directory '{dir}'!"); + return null; + } + + private static string StripNonces(string content) => + // most specific first + HtmlScriptIntegrityRegex().Replace( + HtmlScriptNonceRegex().Replace( + JsElementNonceRegex().Replace( + CFParamsRegex().Replace( + content, + "" + ), + "" + ), + ""), + "" + ); + + [GeneratedRegex("nonce=\"[a-zA-Z0-9+/=]+\"")] + private static partial Regex HtmlScriptNonceRegex(); + + [GeneratedRegex(@"\sintegrity=""[a-zA-Z0-9+/=\-\s]+""")] + private static partial Regex HtmlScriptIntegrityRegex(); + + [GeneratedRegex("\\w.nonce='[a-zA-Z0-9+/=]+';")] + private static partial Regex JsElementNonceRegex(); + + [GeneratedRegex( + @"var\s+\w+\s*=\s*b\.createElement\('script'\);\s*\w+\.nonce='[a-zA-Z0-9+/=]+'\s*;\s*\w+\.innerHTML=""window\.__CF\$cv\$\w+=\{r:'[a-zA-Z0-9+/=]+',t:'[a-zA-Z0-9+/=]+'\};var\s+\w+=document\.createElement\('script'\);\s*\w+\.nonce='[a-zA-Z0-9+/=]+'\s*;\s*\w+\.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document\.getElementsByTagName\('head'\)\[0\]\.appendChild\(\w+\);")] + public static partial Regex CFParamsRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Tasks/Startup/PatchClientAssetsTask.cs b/ReferenceClientProxyImplementation/Tasks/Startup/PatchClientAssetsTask.cs new file mode 100644
index 0000000..60e7ddc --- /dev/null +++ b/ReferenceClientProxyImplementation/Tasks/Startup/PatchClientAssetsTask.cs
@@ -0,0 +1,37 @@ +// using ReferenceClientProxyImplementation.Configuration; +// +// namespace ReferenceClientProxyImplementation.Tasks.Startup; +// +// public class PatchClientAssetsTask(ProxyConfiguration proxyConfig) : ITask { +// public int GetOrder() => 100; +// +// public string GetName() => "Patch client assets"; +// +// public async Task Execute() { +// // foreach (var file in Directory.GetFiles(proxyConfig.AssetCache.DiskCachePath).Where(x => x.EndsWith(".js"))) +// // { +// // var start = DateTime.Now; +// // if(proxyConfig.Logging.LogClientPatching) Console.Write($"[Client Patcher] Patching file {file}..."); +// // var contents = File.ReadAllText(file); +// // contents = AssetsController.PatchClient(contents); +// // File.WriteAllText(file, contents); +// // if(proxyConfig.Logging.LogClientPatching) Console.WriteLine($" Done in {DateTime.Now - start}!"); +// // } +// // +// // if (proxyConfig.Debug.ReformatAssets) +// // { +// // Console.WriteLine("[Client Patcher] Reformatting assets..."); +// // foreach (var file in Directory.GetFiles(proxyConfig.AssetCache.DiskCachePath)) +// // { +// // var target = file.Replace(proxyConfig.AssetCache.DiskCachePath, proxyConfig.TestClient.DebugOptions.FormattedAssetPath); +// // if(!File.Exists(target)) +// // File.Copy(file, target, false); +// // } +// // +// // Process.Start("npx", "prettier -w " + proxyConfig.Debug.FormattedAssetPath).WaitForExit(); +// // Console.WriteLine("[Client Patcher] Done!"); +// // if (proxyConfig.Debug.OpenFormattedDirAfterReformat) +// // Process.Start(proxyConfig.Debug.OpenFormattedDirCommand.Command, proxyConfig.Debug.OpenFormattedDirCommand.Args.Replace("$dir", proxyConfig.Debug.FormattedAssetPath)); +// // } +// } +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Tasks/Startup/SanitiseConfigPathsTask.cs b/ReferenceClientProxyImplementation/Tasks/Startup/SanitiseConfigPathsTask.cs new file mode 100644
index 0000000..2534362 --- /dev/null +++ b/ReferenceClientProxyImplementation/Tasks/Startup/SanitiseConfigPathsTask.cs
@@ -0,0 +1,15 @@ +using ArcaneLibs.Extensions; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Tasks.Startup; + +public partial class SanitiseConfigPathTask(ProxyConfiguration proxyConfig) : ITask { + public int GetOrder() => int.MinValue; + + public string GetName() => "Sanitise config path"; + + public async Task Execute() { + // proxyConfig.AssetCache.DiskCacheBaseDirectory = Path.GetFullPath(proxyConfig.AssetCache.DiskCacheBaseDirectory); + Console.WriteLine(proxyConfig.ToJson()); + } +} \ No newline at end of file