summary refs log tree commit diff
path: root/ReferenceClientProxyImplementation
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--ReferenceClientProxyImplementation.slnx3
-rw-r--r--ReferenceClientProxyImplementation/.gitignore9
-rw-r--r--ReferenceClientProxyImplementation/Configuration/AssetCacheConfig.cs15
-rw-r--r--ReferenceClientProxyImplementation/Configuration/ProxyConfiguration.cs8
-rw-r--r--ReferenceClientProxyImplementation/Configuration/TestClientConfig.cs13
-rw-r--r--ReferenceClientProxyImplementation/Configuration/TestClientDebug.cs7
-rw-r--r--ReferenceClientProxyImplementation/Configuration/TestClientPatchOptions.cs7
-rw-r--r--ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs72
-rw-r--r--ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs.bak124
-rw-r--r--ReferenceClientProxyImplementation/Controllers/ErrorReportingProxy.cs34
-rw-r--r--ReferenceClientProxyImplementation/Controllers/FrontendController.cs77
-rw-r--r--ReferenceClientProxyImplementation/Controllers/StaticController.cs15
-rw-r--r--ReferenceClientProxyImplementation/Helpers/HtmlUtils.cs16
-rw-r--r--ReferenceClientProxyImplementation/Helpers/Resolvers.cs80
-rw-r--r--ReferenceClientProxyImplementation/Patches/Constants/Constant.cs6
-rw-r--r--ReferenceClientProxyImplementation/Patches/IPatch.cs8
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs91
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs72
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs22
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs36
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs32
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs21
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs21
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs26
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs26
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs33
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs79
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs16
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs32
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs34
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs32
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs56
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs27
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs19
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs26
-rw-r--r--ReferenceClientProxyImplementation/Program.cs107
-rw-r--r--ReferenceClientProxyImplementation/Properties/launchSettings.json32
-rw-r--r--ReferenceClientProxyImplementation/ReferenceClientProxyImplementation.csproj34
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/checkLocale.js47
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/dff87c953f43b561d71fbcfe8a93a79a.png0
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/endpoints.json115
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/features.json26
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/fosscord-login.css71
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/fosscord.css44
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/plugins/.gitkeep0
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/preload-plugins/autoRegister.js62
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/tools/rightsCalculator.js17
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/user.css1
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/widget/banner1.pngbin0 -> 5950 bytes
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/widget/banner2.pngbin0 -> 3756 bytes
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/widget/banner3.pngbin0 -> 5342 bytes
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/widget/banner4.pngbin0 -> 13105 bytes
-rw-r--r--ReferenceClientProxyImplementation/Resources/Assets/widget/shield.pngbin0 -> 726 bytes
-rw-r--r--ReferenceClientProxyImplementation/Resources/Pages/developers.html52
-rw-r--r--ReferenceClientProxyImplementation/Resources/Pages/index-template.html75
-rw-r--r--ReferenceClientProxyImplementation/Resources/Pages/index.html130
-rw-r--r--ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDataLog.html7
-rw-r--r--ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDumper.html22
-rw-r--r--ReferenceClientProxyImplementation/Services/BuildDownloadService.cs76
-rw-r--r--ReferenceClientProxyImplementation/Services/ClientStoreService.cs62
-rw-r--r--ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs3
-rw-r--r--ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs14
-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
-rw-r--r--ReferenceClientProxyImplementation/Tasks/Tasks.cs28
-rw-r--r--ReferenceClientProxyImplementation/appsettings.Development.json62
-rw-r--r--ReferenceClientProxyImplementation/appsettings.json9
-rwxr-xr-xReferenceClientProxyImplementation/formatCache.sh5
70 files changed, 2560 insertions, 0 deletions
diff --git a/ReferenceClientProxyImplementation.slnx b/ReferenceClientProxyImplementation.slnx
new file mode 100644

index 0000000..dc01632 --- /dev/null +++ b/ReferenceClientProxyImplementation.slnx
@@ -0,0 +1,3 @@ +<Solution> + <Project Path="ReferenceClientProxyImplementation/ReferenceClientProxyImplementation.csproj" /> +</Solution> diff --git a/ReferenceClientProxyImplementation/.gitignore b/ReferenceClientProxyImplementation/.gitignore new file mode 100644
index 0000000..0219682 --- /dev/null +++ b/ReferenceClientProxyImplementation/.gitignore
@@ -0,0 +1,9 @@ +downloadCache +server-cs-d9282bd218a35e2399d27e98cdd6f7a0a8552bb7 +cache +cache_formatted +clientRepository +error_reports +prettier +node +biome \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Configuration/AssetCacheConfig.cs b/ReferenceClientProxyImplementation/Configuration/AssetCacheConfig.cs new file mode 100644
index 0000000..7360a0a --- /dev/null +++ b/ReferenceClientProxyImplementation/Configuration/AssetCacheConfig.cs
@@ -0,0 +1,15 @@ +namespace ReferenceClientProxyImplementation.Configuration; + +public class AssetCacheConfig { + public bool MemoryCache { get; set; } = true; + public bool DiskCache { get; set; } = true; + public string DiskCachePath { get; set; } = "cache"; + public bool WipeOnStartup { get; set; } = false; + public string DiskCacheBaseDirectory { get; set; } = "./clientRepository"; + public List<List<string>> ExecOnRevisionChange { get; set; } = []; + public bool DitchPatchedOnStartup { get; set; } = false; + public string BiomePath { get; set; } = "biome"; + public string PrettierPath { get; set; } = "prettier"; + + public string NodePath { get; set; } = "node"; +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Configuration/ProxyConfiguration.cs b/ReferenceClientProxyImplementation/Configuration/ProxyConfiguration.cs new file mode 100644
index 0000000..84afacd --- /dev/null +++ b/ReferenceClientProxyImplementation/Configuration/ProxyConfiguration.cs
@@ -0,0 +1,8 @@ +namespace ReferenceClientProxyImplementation.Configuration; + +public class ProxyConfiguration { + public ProxyConfiguration(IConfiguration configuration) => configuration.GetRequiredSection("ProxyConfiguration").Bind(this); + + public TestClientConfig TestClient { get; set; } = new(); + public AssetCacheConfig AssetCache { get; set; } = new(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Configuration/TestClientConfig.cs b/ReferenceClientProxyImplementation/Configuration/TestClientConfig.cs new file mode 100644
index 0000000..1776eef --- /dev/null +++ b/ReferenceClientProxyImplementation/Configuration/TestClientConfig.cs
@@ -0,0 +1,13 @@ +namespace ReferenceClientProxyImplementation.Configuration; + +public class TestClientConfig { + public TestClientDebug DebugOptions = new(); + public bool Enabled { get; set; } = true; + // public bool UseLatest = true; + public string Revision { get; set; } = "canary"; + public Dictionary<string, object> GlobalEnv { get; set; } = new(); + + // internal + public string RevisionPath { get; set; } = null!; + public string RevisionBaseUrl { get; set; } = null!; +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Configuration/TestClientDebug.cs b/ReferenceClientProxyImplementation/Configuration/TestClientDebug.cs new file mode 100644
index 0000000..b7370b5 --- /dev/null +++ b/ReferenceClientProxyImplementation/Configuration/TestClientDebug.cs
@@ -0,0 +1,7 @@ +namespace ReferenceClientProxyImplementation.Configuration; + +public class TestClientDebug { + public bool DumpWebsocketTraffic = false; + public bool DumpWebsocketTrafficToBrowserConsole = false; + public TestClientPatchOptions PatchOptions = new(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Configuration/TestClientPatchOptions.cs b/ReferenceClientProxyImplementation/Configuration/TestClientPatchOptions.cs new file mode 100644
index 0000000..e95dfa5 --- /dev/null +++ b/ReferenceClientProxyImplementation/Configuration/TestClientPatchOptions.cs
@@ -0,0 +1,7 @@ +namespace ReferenceClientProxyImplementation.Configuration; + +public class TestClientPatchOptions { + public bool GatewayImmediateReconnect = false; + public bool GatewayPlaintext = true; + public bool NoXssWarning = true; +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs b/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs new file mode 100644
index 0000000..8bd78bc --- /dev/null +++ b/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs
@@ -0,0 +1,72 @@ +//#define MEMCACHE +using System.Collections.Frozen; +using ArcaneLibs.Extensions.Streams; +using Microsoft.AspNetCore.Mvc; +using ReferenceClientProxyImplementation.Configuration; +using ReferenceClientProxyImplementation.Services; + +namespace ReferenceClientProxyImplementation.Controllers; + +[Controller] +[Route("/")] +public class AssetsController(ProxyConfiguration proxyConfiguration, ClientStoreService clientStore) : Controller { +#if MEMCACHE + private static FrozenDictionary<string, ReadOnlyMemory<byte>> memCache = new Dictionary<string, ReadOnlyMemory<byte>>().ToFrozenDictionary(); +#endif + + [HttpGet("/assets/{*res:required}")] + public async Task<IActionResult> Asset(string res) { + if (res is "version.staging.json" or "version.internal.json") + res = $"version.{proxyConfiguration.TestClient.Revision}.json"; + else if (res.EndsWith(".map")) { + return NotFound(); + } + + var ext = res.Split(".").Last(); + var contentType = ext switch { + //text types + "html" => "text/html", + "js" => "text/javascript", + "css" => "text/css", + "txt" => "text/plain", + "csv" => "text/csv", + "json" => "application/json", + //image types + "apng" => "image/apng", + "gif" => "image/gif", + "jpg" => "image/jpeg", + "png" => "image/png", + "svg" => "image/svg+xml", + "webp" => "image/webp", + "ico" => "image/x-icon", + //script types + "wasm" => "application/wasm", + _ => "application/octet-stream" + }; + // Response.Headers.ContentType = contentType; +#if MEMCACHE + if (memCache.TryGetValue(res, out var value)) { + // value.Position = 0; + var cms = new MemoryStream(value.ToArray()); + // await value.CopyToAsync(cms); + // ms.Position = 0; + + return new FileStreamResult(cms, contentType); + } +#endif + +#if MEMCACHE + var stream = await clientStore.GetPatchedClientAsset("assets/" + res); + stream.Position = 0; + // var ms = new MemoryStream(stream.ReadToEnd().ToArray(), false); + // memCache = memCache.Append(new KeyValuePair<string, MemoryStream>(res, ms)).ToFrozenDictionary(); + // return new FileStreamResult(ms, contentType); + var mem = new ReadOnlyMemory<byte>(stream.ReadToEnd().ToArray()); + memCache = memCache.Append(new KeyValuePair<string, ReadOnlyMemory<byte>>(res, mem)).ToFrozenDictionary(); + return new FileStreamResult(new MemoryStream(mem.ToArray()), contentType); +#else + return new FileStreamResult(await clientStore.GetPatchedClientAsset("assets/" + res), contentType); +#endif + // return ; + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs.bak b/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs.bak new file mode 100644
index 0000000..ea5909a --- /dev/null +++ b/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs.bak
@@ -0,0 +1,124 @@ +//using System.Collections.Concurrent; +//using System.Text; +//using Microsoft.AspNetCore.Mvc; +//using ReferenceClientProxyImplementation.Configuration; +//using ReferenceClientProxyImplementation.Services; +//using Spacebar.API.Helpers; +// +//namespace ReferenceClientProxyImplementation.Controllers; +// +//[Controller] +//[Route("/")] +//public class AssetsController(ProxyConfiguration proxyConfiguration, ClientStoreService clientStore) : Controller { +// private static readonly ConcurrentDictionary<string, byte[]> cache = new(); +// +// [HttpGet("/assets/{*res:required}")] +// public async Task<object> Asset(string res) { +// var ext = res.Split(".").Last(); +// var contentType = ext switch { +// //text types +// "html" => "text/html", +// "js" => "text/javascript", +// "css" => "text/css", +// "txt" => "text/plain", +// "csv" => "text/csv", +// //image types +// "apng" => "image/apng", +// "gif" => "image/gif", +// "jpg" => "image/jpeg", +// "png" => "image/png", +// "svg" => "image/svg+xml", +// "webp" => "image/webp", +// "ico" => "image/x-icon", +// _ => "application/octet-stream" +// }; +// if (cache.ContainsKey(res)) +// return File(cache[res], contentType); +// +// if (System.IO.File.Exists("./Resources/Assets/" + res)) +// cache.TryAdd(res, await System.IO.File.ReadAllBytesAsync("./Resources/Assets/" + res)); +// else if (System.IO.File.Exists("./cache_formatted/" + res)) +// cache.TryAdd(res, await System.IO.File.ReadAllBytesAsync("./cache_formatted/" + res)); +// else if (System.IO.File.Exists("./cache/" + res)) +// cache.TryAdd(res, await System.IO.File.ReadAllBytesAsync($"{proxyConfiguration.AssetCache.DiskCachePath}/{res}")); +// else { +// if (!Directory.Exists(proxyConfiguration.AssetCache.DiskCachePath)) Directory.CreateDirectory(proxyConfiguration.AssetCache.DiskCachePath); +// if (res.EndsWith(".map")) return NotFound(); +// Console.WriteLine($"[Asset cache] Downloading {"https://discord.com/assets/" + res} -> {proxyConfiguration.AssetCache.DiskCachePath}/{res}"); +// try { +// using (var hc = new HttpClient()) { +// var resp = await hc.GetAsync("https://discord.com/assets/" + res); +// +// if (!resp.IsSuccessStatusCode) return NotFound(); +// //save to file +// var bytes = await resp.Content.ReadAsByteArrayAsync(); +// //check if cloudflare +// if (bytes.Length == 0) { +// Console.WriteLine( +// $"[Asset cache] Cloudflare detected, retrying {"https://discord.com/assets/" + res} -> {proxyConfiguration.AssetCache.DiskCachePath}/{res}"); +// await Task.Delay(1000); +// resp = await hc.GetAsync("https://discord.com/assets/" + res); +// if (!resp.IsSuccessStatusCode) return NotFound(); +// bytes = await resp.Content.ReadAsByteArrayAsync(); +// } +// +// //check if cloudflare html +// /*if (bytes.Length < 1000 && bytes.ToList().Contains<byte[]>(Encoding.UTF8.GetBytes("Cloudflare"))) +// { +// Console.WriteLine($"[Asset cache] Cloudflare detected, retrying {"https://discord.com/assets/" + res} -> ./cache/{res}"); +// await Task.Delay(1000); +// resp = await hc.GetAsync("https://discord.com/assets/" + res); +// if (!resp.IsSuccessStatusCode) return NotFound(); +// bytes = await resp.Content.ReadAsByteArrayAsync(); +// }*/ +// if (res.EndsWith(".js") || res.EndsWith(".css")) { +// //remove sourcemap +// var str = Encoding.UTF8.GetString(bytes); +// str = PatchClient(str); +// bytes = Encoding.UTF8.GetBytes(str); +// } +// +// if (proxyConfiguration.AssetCache.DiskCache) await System.IO.File.WriteAllBytesAsync($"{proxyConfiguration.AssetCache.DiskCachePath}/{res}", bytes); +// cache.TryAdd(res, bytes); +// } +// //await new WebClient().DownloadFileTaskAsync("https://discord.com/assets/" + res, "./cache/" + res); +// //cache.TryAdd(res, await System.IO.File.ReadAllBytesAsync("./cache/" + res)); +// } +// catch (Exception e) { +// Console.WriteLine(e); +// return NotFound(); +// } +// } +// +// if (cache.ContainsKey(res)) { +// var result = cache[res]; +// if (!proxyConfiguration.AssetCache.MemoryCache) cache.TryRemove(res, out _); +// return File(result, contentType); +// } +// +// return NotFound(); +// } +// +// public string PatchClient(string str) { +// var patchOptions = proxyConfiguration.TestClient.DebugOptions.PatchOptions; +// str = str.Replace("//# sourceMappingURL=", "//# disabledSourceMappingURL="); +// str = str.Replace("https://fa97a90475514c03a42f80cd36d147c4@sentry.io/140984", "https://6bad92b0175d41a18a037a73d0cff282@sentry.thearcanebrony.net/12"); +// if (patchOptions.GatewayPlaintext) +// str = str.Replace("e.isDiscordGatewayPlaintextSet=function(){0;return!1};", "e.isDiscordGatewayPlaintextSet = function() { return true };"); +// +// if (patchOptions.NoXssWarning) { +// str = str.Replace("console.log(\"%c\"+n.SELF_XSS_", "console.valueOf(n.SELF_XSS_"); +// str = str.Replace("console.log(\"%c\".concat(n.SELF_XSS_", "console.valueOf(console.valueOf(n.SELF_XSS_"); +// } +// +// if (patchOptions.GatewayImmediateReconnect) str = str.Replace("nextReconnectIsImmediate=!1", "nextReconnectIsImmediate = true"); +// +// return str; +// } +// +// [HttpGet("/robots.txt")] +// public object Robots() => Resolvers.ReturnFile("./Resources/robots.txt"); +// +// [HttpGet("/favicon.ico")] +// public object Favicon() => Resolvers.ReturnFile("./Resources/RunData/favicon.png"); +//} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Controllers/ErrorReportingProxy.cs b/ReferenceClientProxyImplementation/Controllers/ErrorReportingProxy.cs new file mode 100644
index 0000000..acc186b --- /dev/null +++ b/ReferenceClientProxyImplementation/Controllers/ErrorReportingProxy.cs
@@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; + +namespace ReferenceClientProxyImplementation.Controllers; + +[Controller] +[Route("/")] +public class ErrorReportingProxy : Controller { + [HttpPost("/error-reporting-proxy/web")] + public async Task<ActionResult> HandleErrorReport() { + // read body as string + var body = await new StreamReader(Request.Body).ReadToEndAsync(); + var lines = body.Split('\n'); + var data = new JsonObject() { + ["eventInfo"] = JsonSerializer.Deserialize<JsonObject>(lines[0]), + ["typeInfo"] = JsonSerializer.Deserialize<JsonObject>(lines[1]), + ["stackTrace"] = JsonSerializer.Deserialize<JsonObject>(lines[2]), + }; + + if (lines.Length > 3) + for (var i = 3; i < lines.Length; i++) { + data[$"unk_line_{i}"] = JsonSerializer.Deserialize<JsonValue>(lines[i]); + } + + if (!System.IO.Directory.Exists("error_reports")) + System.IO.Directory.CreateDirectory("error_reports"); + await System.IO.File.WriteAllTextAsync($"error_reports/web_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.json", data.ToJsonString(new JsonSerializerOptions { + WriteIndented = true + })); + + return NoContent(); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Controllers/FrontendController.cs b/ReferenceClientProxyImplementation/Controllers/FrontendController.cs new file mode 100644
index 0000000..b12996d --- /dev/null +++ b/ReferenceClientProxyImplementation/Controllers/FrontendController.cs
@@ -0,0 +1,77 @@ +using Microsoft.AspNetCore.Mvc; +using ReferenceClientProxyImplementation.Configuration; +using ReferenceClientProxyImplementation.Helpers; +using ReferenceClientProxyImplementation.Patches.Implementations; + +namespace ReferenceClientProxyImplementation.Controllers; + +[Controller] +[Route("/")] +public class FrontendController(ProxyConfiguration proxyConfiguration, PatchSet patches) : Controller { + [HttpGet] + [HttpGet("/app")] + [HttpGet("/login")] + [HttpGet("/register")] + [HttpGet("/channels/@me")] + [HttpGet("/channels/{*_}")] + [HttpGet("/shop")] + [HttpGet("/app/{*_}")] + [HttpGet("/open")] + [HttpGet("/settings/{*_}")] + [HttpGet("/action/{*_}")] + [HttpGet("/library/{*_}")] + public async Task<Stream> Home() { + var patchedPath = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "patched", "app.html"); + if (!System.IO.File.Exists(patchedPath)) { + var path = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "src", "app.html"); + var patchedContent = await patches.ApplyPatches("app.html", await System.IO.File.ReadAllBytesAsync(path)); + Directory.CreateDirectory(Path.GetDirectoryName(patchedPath)!); + await System.IO.File.WriteAllBytesAsync(patchedPath, patchedContent); + } + + return System.IO.File.OpenRead(patchedPath); + // return null; + // if (!proxyConfiguration.TestClient.Enabled) + // return NotFound("Test client is disabled"); + // var html = await System.IO.File.ReadAllTextAsync(proxyConfiguration.TestClient.UseLatest ? "Resources/Pages/index-updated.html" : "Resources/Pages/index.html"); + // + // //inject debug utilities + // var debugOptions = proxyConfiguration.TestClient.DebugOptions; + // if (debugOptions.DumpWebsocketTrafficToBrowserConsole) + // html = html.Replace("<!-- preload plugin marker -->", + // await System.IO.File.ReadAllTextAsync("Resources/Private/Injections/WebSocketDataLog.html") + "\n<!-- preload plugin marker -->"); + // if (debugOptions.DumpWebsocketTraffic) + // html = html.Replace("<!-- preload plugin marker -->", + // await System.IO.File.ReadAllTextAsync("Resources/Private/Injections/WebSocketDumper.html") + "\n<!-- preload plugin marker -->"); + // + // return File(Encoding.UTF8.GetBytes(html), "text/html"); + } + + [HttpGet("/developers")] + [HttpGet("/developers/{*_}")] + public async Task<object> Developers() { + var patchedPath = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "patched", "developers.html"); + if (!System.IO.File.Exists(patchedPath)) { + var path = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "src", "developers.html"); + var patchedContent = await patches.ApplyPatches("developers.html", await System.IO.File.ReadAllBytesAsync(path)); + Directory.CreateDirectory(Path.GetDirectoryName(patchedPath)!); + await System.IO.File.WriteAllBytesAsync(patchedPath, patchedContent); + } + + return System.IO.File.OpenRead(patchedPath); + } + + [HttpGet("/popout")] + [HttpGet("/popout/{*_}")] + public async Task<object> Popout() { + var patchedPath = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "patched", "popout.html"); + if (!System.IO.File.Exists(patchedPath)) { + var path = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "src", "popout.html"); + var patchedContent = await patches.ApplyPatches("popout.html", await System.IO.File.ReadAllBytesAsync(path)); + Directory.CreateDirectory(Path.GetDirectoryName(patchedPath)!); + await System.IO.File.WriteAllBytesAsync(patchedPath, patchedContent); + } + + return System.IO.File.OpenRead(patchedPath); + } +} diff --git a/ReferenceClientProxyImplementation/Controllers/StaticController.cs b/ReferenceClientProxyImplementation/Controllers/StaticController.cs new file mode 100644
index 0000000..a2ecf2c --- /dev/null +++ b/ReferenceClientProxyImplementation/Controllers/StaticController.cs
@@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using ReferenceClientProxyImplementation.Helpers; + +namespace ReferenceClientProxyImplementation.Controllers; + +[Controller] +[Route("/")] +public class StaticController : Controller { + [HttpGet("/resources/{*res:required}")] + public object Resource(string res) { + if (System.IO.File.Exists("./Resources/Static/" + res)) + return Resolvers.ReturnFile("./Resources/Static/" + res); + return new RedirectResult("https://discord.gg/assets/" + res); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Helpers/HtmlUtils.cs b/ReferenceClientProxyImplementation/Helpers/HtmlUtils.cs new file mode 100644
index 0000000..a8ee447 --- /dev/null +++ b/ReferenceClientProxyImplementation/Helpers/HtmlUtils.cs
@@ -0,0 +1,16 @@ +using AngleSharp.Html; +using AngleSharp.Html.Parser; + +namespace ReferenceClientProxyImplementation.Helpers; + +public class HtmlUtils { + public static string CleanupHtml(string input) { + var parser = new HtmlParser(); + + var document = parser.ParseDocument(input); + + var sw = new StringWriter(); + document.ToHtml(sw, new PrettyMarkupFormatter()); + return sw.ToString(); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Helpers/Resolvers.cs b/ReferenceClientProxyImplementation/Helpers/Resolvers.cs new file mode 100644
index 0000000..813ffcb --- /dev/null +++ b/ReferenceClientProxyImplementation/Helpers/Resolvers.cs
@@ -0,0 +1,80 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +namespace ReferenceClientProxyImplementation.Helpers; + +public static class Resolvers { + private static readonly string Navbar = File.Exists("Resources/Parts/Navbar.html") ? File.ReadAllText("Resources/Parts/Navbar.html") : "Navbar not found!"; + + public static object ReturnFile(string path) { + if (!File.Exists(path)) return new NotFoundObjectResult("File doesn't exist!"); + var ext = path.Split(".").Last(); + var contentType = ext switch { + //text types + "html" => "text/html", + "js" => "text/javascript", + "css" => "text/css", + "txt" => "text/plain", + "csv" => "text/csv", + //image types + "apng" => "image/apng", + "gif" => "image/gif", + "jpg" => "image/jpeg", + "png" => "image/png", + "svg" => "image/svg+xml", + "webp" => "image/webp", + "ico" => "image/x-icon", + _ => "application/octet-stream" + }; + switch (ext) { + case "html": + return new ContentResult { + ContentType = contentType, + Content = File.ReadAllText(path) + }; + case "js": + case "css": + case "txt": + case "csv": + case "svg": + return new ContentResult { + ContentType = contentType, + Content = File.ReadAllText(path) + }; + case "png": + case "webp": + case "jpg": + case "gif": + case "apng": + case "7z": + case "gz": + case "tar": + case "rar": + case "zip": + case "webm": + case "woff": + case "jar": + case "mp3": + case "mp4": + return new PhysicalFileResult(Path.GetFullPath(path), contentType); + default: + Console.WriteLine($"Unsupported filetype: {ext} ({path})"); + return new PhysicalFileResult(Path.GetFullPath(path), "application/octet-stream"); + } + } + + public static object ReturnFileWithVars(string path, Dictionary<string, object>? customVars = null) { + if (!File.Exists(path)) return new NotFoundObjectResult(Debugger.IsAttached ? $"File {path} doesn't exist!" : "File doesn't exist!"); + var result = ReturnFile(path); + if (result.GetType() != typeof(ContentResult)) return result; + var contentResult = (ContentResult)result; + contentResult.Content = contentResult.Content?.Replace("$NAVBAR", Navbar); + if (customVars != null) + foreach (var (key, value) in customVars) + contentResult.Content = contentResult.Content?.Replace(key, value.ToString()); + + result = contentResult; + + return result; + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Constants/Constant.cs b/ReferenceClientProxyImplementation/Patches/Constants/Constant.cs new file mode 100644
index 0000000..09a00cc --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Constants/Constant.cs
@@ -0,0 +1,6 @@ +namespace ReferenceClientProxyImplementation.Patches.Constants; + +public class Constant { + public required string SourceValue { get; set; } + public required string TargetValue { get; set; } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/IPatch.cs b/ReferenceClientProxyImplementation/Patches/IPatch.cs new file mode 100644
index 0000000..92cab93 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/IPatch.cs
@@ -0,0 +1,8 @@ +namespace ReferenceClientProxyImplementation.Patches; + +public interface IPatch { + public int GetOrder(); + public string GetName(); + public bool Applies(string relativeName, byte[] content); + public Task<byte[]> Execute(string relativeName, byte[] content); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs new file mode 100644
index 0000000..649625f --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs
@@ -0,0 +1,91 @@ +using System.Diagnostics; +using ArcaneLibs; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations; + +public partial class FormatJsFilePatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => -100; + + public string GetName() => "Format JS file"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js") || relativeName.EndsWith(".css") || relativeName.EndsWith(".html"); + + public async Task<byte[]> Execute(string relativeName, byte[] content) { + var cachePath = Path.Combine(config.TestClient.RevisionPath, "formatted", relativeName); + if (File.Exists(cachePath)) { + Console.WriteLine($"Using cached formatted file for {relativeName}"); + return await File.ReadAllBytesAsync(cachePath); + } + + // temporary: add some newlines + var stringContent = System.Text.Encoding.UTF8.GetString(content); + // stringContent = stringContent.Replace("function(){", "function() {\n"); + content = System.Text.Encoding.UTF8.GetBytes(stringContent); + + + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); + var tmpPath = $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{Random.Shared.NextInt64()}_{Path.GetFileName(relativeName)}"; + await File.WriteAllBytesAsync(tmpPath, content); + var sw = Stopwatch.StartNew(); + ProcessStartInfo psi; + + // Biome doesn't support HTML and struggles with upstream emitting Sass directives + if (relativeName.EndsWith(".html") || relativeName.EndsWith(".css")) + psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + else + psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") { + // psi = new ProcessStartInfo(config.AssetCache.BiomePath, $"format --write --line-width 240 --files-max-size=100000000 {tmpPath}") { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) { + throw new InvalidOperationException("Failed to start the formatting process."); + } + + // var stdout = await process.StandardOutput.ReadToEndAsync(); + // var stderr = await process.StandardError.ReadToEndAsync(); + // await process.WaitForExitAsync(); + + Dictionary<ulong, string> stdoutLines = new(); + Dictionary<ulong, string> stderrLines = new(); + + process.OutputDataReceived += (sender, args) => { + if (args.Data != null) { + stdoutLines[(ulong)sw.ElapsedMilliseconds] = args.Data; + Console.Write("O"); + } + }; + process.ErrorDataReceived += (sender, args) => { + if (args.Data != null) { + stderrLines[(ulong)sw.ElapsedMilliseconds] = args.Data; + Console.Write("E"); + } + }; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(); + + Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + + if (process.ExitCode != 0) { + Console.WriteLine($"Failed to format {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + Console.WriteLine("Standard Output:\n" + string.Join("\n", stdoutLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}"))); + Console.WriteLine("Standard Error:\n" + string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}"))); + throw new Exception($"Failed to exec({psi.FileName} {psi.Arguments}): {string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => kv.Value))}"); + } + + var result = await File.ReadAllBytesAsync(tmpPath); + File.Move(tmpPath, cachePath); + return result; + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs new file mode 100644
index 0000000..877a31a --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs
@@ -0,0 +1,72 @@ +using System.Diagnostics; +using ArcaneLibs; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations; + +public partial class FormatHtmlCssPatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => -100; + + public string GetName() => "Format HTML/CSS file"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".css") || relativeName.EndsWith(".html"); + + public async Task<byte[]> Execute(string relativeName, byte[] content) { + var cachePath = Path.Combine(config.TestClient.RevisionPath, "formatted", relativeName); + if (File.Exists(cachePath)) { + Console.WriteLine($"Using cached formatted file for {relativeName}"); + return await File.ReadAllBytesAsync(cachePath); + } + + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); + var tmpPath = $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{Random.Shared.NextInt64()}_{Path.GetFileName(relativeName)}"; + await File.WriteAllBytesAsync(tmpPath, content); + var sw = Stopwatch.StartNew(); + ProcessStartInfo psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) { + throw new InvalidOperationException("Failed to start the formatting process."); + } + + // var stdout = await process.StandardOutput.ReadToEndAsync(); + // var stderr = await process.StandardError.ReadToEndAsync(); + // await process.WaitForExitAsync(); + + Dictionary<ulong, string> stdoutLines = new(); + Dictionary<ulong, string> stderrLines = new(); + + while (!process.HasExited) { + while (!process.StandardOutput.EndOfStream) { + var line = await process.StandardOutput.ReadLineAsync(); + if (line == null) continue; + stdoutLines[(ulong)sw.ElapsedMilliseconds] = line; + Console.Write("O"); + } + + while (!process.StandardError.EndOfStream) { + var line = await process.StandardError.ReadLineAsync(); + if (line == null) continue; + stderrLines[(ulong)sw.ElapsedMilliseconds] = line; + Console.Write("E"); + } + } + + // Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + + if (process.ExitCode != 0) { + Console.WriteLine($"Failed to format {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + Console.WriteLine("Standard Output:\n" + string.Join("\n", stdoutLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}"))); + Console.WriteLine("Standard Error:\n" + string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}"))); + throw new Exception($"Failed to format file {relativeName}: {string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => kv.Value))}"); + } + + var result = await File.ReadAllBytesAsync(tmpPath); + File.Move(tmpPath, cachePath); + return result; + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs new file mode 100644
index 0000000..dc54850 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs
@@ -0,0 +1,22 @@ +using System.Text; +using System.Text.RegularExpressions; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.HTMLPatches; + +public class GlobalEnvPatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Patch GLOBAL_ENV"; + public bool Applies(string relativeName, byte[] content) => relativeName is "app.html" or "developers.html" or "popout.html"; + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + foreach(var (key, value) in config.TestClient.GlobalEnv) { + stringContent = new Regex($"{key}: \".*?\"").Replace(stringContent, $"{key}: \"{value}\""); + } + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs new file mode 100644
index 0000000..6c5f312 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs
@@ -0,0 +1,36 @@ +using System.Text; +using System.Text.RegularExpressions; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.HTMLPatches; + +public partial class StripNoncesPatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => 0; + + public string GetName() => "Strip nonces/integrity from html"; + public bool Applies(string relativeName, byte[] content) => relativeName is "app.html" or "developers.html" or "popout.html"; + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + stringContent = HtmlScriptIntegrityRegex().Replace( + HtmlScriptNonceRegex().Replace( + JsElementNonceRegex().Replace( + stringContent, + "" + ), + "" + ), + "" + ); + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex("\\snonce=\"[a-zA-Z0-9+/=]+\"")] + private static partial Regex HtmlScriptNonceRegex(); + + [GeneratedRegex("\\w.nonce='[a-zA-Z0-9+/=]+';")] + private static partial Regex JsElementNonceRegex(); + + [GeneratedRegex(@"\sintegrity=""[a-zA-Z0-9+/=\-\s]+""")] + private static partial Regex HtmlScriptIntegrityRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs new file mode 100644
index 0000000..6e7f7a2 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs
@@ -0,0 +1,32 @@ +using System.Text; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public class ApiProtocolPatch : IPatch { + public int GetOrder() => 0; + + public string GetName() => "API: Use GLOBAL_ENV.API_PROTOCOL instead of hardcoded https:"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + // TODO: regex + stringContent = stringContent + .Replace( + "return \"https:\" + window.GLOBAL_ENV.API_ENDPOINT + (e ? \"/v\".concat(window.GLOBAL_ENV.API_VERSION) : \"\");", + "return window.GLOBAL_ENV.API_PROTOCOL + window.GLOBAL_ENV.API_ENDPOINT + (e ? \"/v\".concat(window.GLOBAL_ENV.API_VERSION) : \"\");" + ) + .Replace( + "api_endpoint: \"\".concat(\"https:\").concat(window.GLOBAL_ENV.API_ENDPOINT)", + "api_endpoint: window.GLOBAL_ENV.API_PROTOCOL.concat(window.GLOBAL_ENV.API_ENDPOINT)" + ) + .Replace( + "f = null != d ? \"https://\".concat(d) : location.protocol + window.GLOBAL_ENV.API_ENDPOINT,", + "f = null != d ? window.GLOBAL_ENV.API_PROTOCOL.concat(d) : location.protocol + window.GLOBAL_ENV.API_ENDPOINT," + ) + ; + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs new file mode 100644
index 0000000..152a3f3 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs
@@ -0,0 +1,21 @@ +using System.Text; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public class BooleanPatch : IPatch { + public int GetOrder() => 0; + + public string GetName() => "Use real booleans in JS files"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = stringContent + .Replace("return!", "return !") + .Replace("!0", "true") + .Replace("!1", "false"); + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs new file mode 100644
index 0000000..fe73c8e --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs
@@ -0,0 +1,21 @@ +// using System.Text; +// using System.Text.RegularExpressions; +// +// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; +// +// public partial class BooleanPropagationPatch : IPatch { +// public int GetOrder() => 3; +// +// public string GetName() => "Patch pointless boolean comparisons in JS"; +// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); +// +// public async Task<byte[]> Execute(string relativePath, byte[] content) { +// var stringContent = Encoding.UTF8.GetString(content); +// +// stringContent = stringContent +// .Replace(" && true", "").Replace(" || false", "").Replace("false || ", "") +// ; +// +// return Encoding.UTF8.GetBytes(stringContent); +// } +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs new file mode 100644
index 0000000..c44bf95 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs
@@ -0,0 +1,26 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class DisableSciencePatch : IPatch { + public int GetOrder() => 0; + + public string GetName() => @"JS(web): Disable /science calls"; + public bool Applies(string relativeName, byte[] content) => relativeName.StartsWith("assets/web.") && relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + var match = HandleTrackDefinitionRegex().Match(stringContent); + stringContent = stringContent.Insert(match.Index + match.Length, @" + return (new Promise(() => { }), false); // ReferenceClientProxyImplementation: Disable /science calls + "); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@".\.handleTrack = function \(.\) \{", RegexOptions.Compiled)] + private static partial Regex HandleTrackDefinitionRegex(); + +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs new file mode 100644
index 0000000..9eed00c --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs
@@ -0,0 +1,26 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class ExpandUnicodeEscapesPatch : IPatch { + public int GetOrder() => 0; + + public string GetName() => @"JS: expand \x?? to \u00??"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = XToURegex().Replace( + stringContent, + m => $"\\u00{m.Groups[1].Value}" + ); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@"\\x([0-9A-Fa-f]{2})", RegexOptions.Compiled)] + private static partial Regex XToURegex(); + +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs new file mode 100644
index 0000000..b741f56 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs
@@ -0,0 +1,33 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class IsStaffPatch : IPatch { + public int GetOrder() => 2; + + public string GetName() => "Patch isStaff/isStaffPersonal in JS"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string relativePath, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = IsNullableStaffRegex().Replace( + stringContent, + m => $"{m.Groups[1].Value}!!{m.Groups[2].Value}" + ); + + stringContent = IsStaffRegex().Replace( + stringContent, + m => $"{m.Groups[1].Value}true" + ); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@"(\W)(\w|this|\w\.user)\.isStaff(Personal)?\(\)", RegexOptions.Compiled)] + private static partial Regex IsStaffRegex(); + + [GeneratedRegex(@"(\W)(\w|this|\w\.user)\?\.isStaff(Personal)?\(\)", RegexOptions.Compiled)] + private static partial Regex IsNullableStaffRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs new file mode 100644
index 0000000..b5e7d77 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs
@@ -0,0 +1,79 @@ +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class JsonParseMultilinePatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Patch null-coalescing expressions in JS"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string relativePath, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + var matches = JsonParseRegex().Matches(stringContent); + Console.WriteLine($"Found {matches.Count} JSON.parse calls in {relativePath}"); + + + + await Parallel.ForEachAsync(matches, async (match, ct) => { + string formattedJson = match.Groups[1].Value; + try { + var jsonElement = JsonSerializer.Deserialize<JsonElement>(formattedJson.Replace("\\", "\\\\") + "waef"); + formattedJson = JsonSerializer.Serialize(jsonElement, new JsonSerializerOptions { WriteIndented = true }); + } catch (JsonException je) { + // Console.WriteLine($"STJ: Failed to parse JSON in {relativePath} at index {match.Index}: {je.Message}"); // intentinally broken + try { + formattedJson = await formatJsonWithNodejs(relativePath, match, ct); + } catch (Exception e) { + Console.WriteLine($"Node.js: Failed to parse JSON in {relativePath} at index {match.Index}: {e.Message}"); + return; + } + } + + lock (matches) stringContent = stringContent.Replace(match.Value, $"JSON.parse(`{formattedJson.Replace("\\", "\\\\")}`);"); + }); + + return Encoding.UTF8.GetBytes(stringContent); + } + + private async Task<string> formatJsonWithNodejs(string relativePath, Match match, CancellationToken ct) { + // Extract the JSON string from the match + var id = "dcp_" + Path.GetFileName(relativePath).Replace('.', '_') + "_" + match.Index; + await File.WriteAllTextAsync($"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js", $"console.log(JSON.stringify(JSON.parse(`{match.Groups[1].Value.Replace("`", "\\\\\\`")}`), null, 2))"); + var sw = Stopwatch.StartNew(); + + var psi = new ProcessStartInfo(config.AssetCache.NodePath, $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; + + using var process = Process.Start(psi); + if (process == null) { + throw new InvalidOperationException("Failed to start the formatting process."); + } + + var stdout = await process.StandardOutput.ReadToEndAsync(); + var stderr = await process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + // Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + + if (process.ExitCode != 0) { + Console.WriteLine($"Failed to run {Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + Console.WriteLine("Standard Output: " + stdout); + Console.WriteLine("Standard Error: " + stderr); + throw new Exception($"Failed to execute {Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js: {stderr}"); + } + + var formattedJson = stdout.Trim(); + Console.WriteLine($"Parsed JSON({id}) in {sw.ElapsedMilliseconds}ms: {formattedJson.Length} bytes"); + // stringContent = stringContent.Replace(match.Value, $"JSON.parse(`{formattedJson.Replace("\\n", "\\\\n")}`);"); + await File.WriteAllTextAsync($"{config.TestClient.RevisionPath}/patched/assets/{Path.GetFileName(relativePath)}-{match.Index}.json", formattedJson); + return formattedJson; + } + + [GeneratedRegex(@"JSON\.parse\(\n\s*'(.*?)',?\s*\);", RegexOptions.Compiled | RegexOptions.Multiline)] + private static partial Regex JsonParseRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs new file mode 100644
index 0000000..a94e312 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs
@@ -0,0 +1,16 @@ +using System.Text; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public class KnownConstantsPatch : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Use named constants"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs new file mode 100644
index 0000000..e7f78a0 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs
@@ -0,0 +1,32 @@ +// using System.Text; +// using System.Text.RegularExpressions; +// +// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; +// +// public partial class LegacyJsPathces : IPatch { +// public int GetOrder() => 1; +// +// public string GetName() => "Patch deprecated JS constructs"; +// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); +// +// public async Task<byte[]> Execute(string relativePath, byte[] content) { +// var stringContent = Encoding.UTF8.GetString(content); +// +// while(MozInputSourceRegex().IsMatch(stringContent)) { +// var match = MozInputSourceRegex().Match(stringContent); +// var replacement = match.Groups[1].Value switch { +// "0" => "", +// "1" => "mouse", +// "2" => "pen", +// "3" => "pen", +// "4" => "touch", +// _ => throw new InvalidOperationException("Unreachable") +// }; +// } +// +// return Encoding.UTF8.GetBytes(stringContent); +// } +// +// [GeneratedRegex(@"([0-6]) === (\w).mozInputSource", RegexOptions.Compiled)] +// private static partial Regex MozInputSourceRegex(); +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs new file mode 100644
index 0000000..2005c4c --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs
@@ -0,0 +1,34 @@ +// using System.Text; +// using System.Text.RegularExpressions; +// +// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; +// +// public partial class LogErrorContextPatch : IPatch { +// public int GetOrder() => 2; +// +// public string GetName() => "Patch assertions to log more context"; +// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); +// +// public async Task<byte[]> Execute(string relativePath, byte[] content) { +// var stringContent = Encoding.UTF8.GetString(content); +// +// stringContent = NotNullAssertionRegex().Replace( +// stringContent, +// m => { +// var methodName = m.Groups[1].Value; +// var objectName = m.Groups[2].Value; +// var message = m.Groups[3].Value; +// Console.WriteLine($"Patching not-null assertion in {relativePath}: {methodName} - {message}"); +// +// return $@"{methodName}()(null != {objectName}, ""{message} - Context: "" + JSON.stringify({objectName}))"; +// } +// ); +// +// return Encoding.UTF8.GetBytes(stringContent); +// } +// +// // null assertion: u()(null != o, "PrivateChannel.renderAvatar: Invalid prop configuration - no user or channel"); +// // capture: method name, object name, message +// [GeneratedRegex(@"([a-zA-Z0-9_]+)\(\)\(\s*null != ([a-zA-Z0-9_]+),\s*""([^""]+)""\s*\)", RegexOptions.Compiled)] +// private static partial Regex NotNullAssertionRegex(); +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs new file mode 100644
index 0000000..98156c8 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs
@@ -0,0 +1,32 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class NullCoalescingPatch : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Patch null-coalescing expressions in JS"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string relativePath, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = NullCoalescingRegex().Replace( + stringContent, + m => $"{m.Groups[1].Value}?.{m.Groups[2].Value}" + ); + // stringContent = ParenNullCheckRegex().Replace( + // stringContent, + // m => $"{m.Groups[1].Value} == null" + // ); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@"null == ([a-zA-Z0-9_]+?) \? undefined : \1\.([a-zA-Z0-9_]+?)", RegexOptions.Compiled)] + private static partial Regex NullCoalescingRegex(); + + [GeneratedRegex(@"\(([^()]+?)\) == null", RegexOptions.Compiled)] + private static partial Regex ParenNullCheckRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs new file mode 100644
index 0000000..bab0756 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs
@@ -0,0 +1,56 @@ +// using System.Text; +// using System.Text.RegularExpressions; +// using ReferenceClientProxyImplementation.Services; +// +// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; +// +// public partial class PrefetchAssetsPatch(IServiceProvider sp) : IPatch { +// public int GetOrder() => 1000000; +// +// public string GetName() => "Prefetch assets"; +// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); +// +// private static SemaphoreSlim ss = new(2, 2); +// private static HashSet<string> alreadyKnownAssets = new(); +// +// public async Task<byte[]> Execute(string relativePath, byte[] content) { +// // Can't inject service due to loop +// var stringContent = Encoding.UTF8.GetString(content); +// var matches = PrefetchAssetsRegex().Matches(stringContent); +// +// Console.WriteLine($"Found {matches.Count} prefetch assets in {relativePath}"); +// if (matches.Count == 0) { +// return content; // No matches found, return original content +// } +// +// var clientStore = sp.GetRequiredService<ClientStoreService>(); +// +// var newAssets = matches +// .Select(x => x.Groups[1].Value) +// .Distinct() +// .Where(x => !clientStore.HasRawAsset(x) && alreadyKnownAssets.Add(x)); +// +// var tasks = newAssets +// .Select(async match => { +// await ss.WaitAsync(); +// Console.WriteLine($"Discovered prefetch asset in {relativePath}: {match}"); +// // var patches = sp.GetRequiredService<PatchSet>(); +// var res = await clientStore.GetOrDownloadRawAsset(match); +// await res.DisposeAsync(); +// ss.Release(); +// Console.WriteLine($"Prefetched asset {match} in {relativePath}"); +// }).ToList(); +// +// if (tasks.Count == 0) { +// Console.WriteLine($"No new prefetch assets found in {relativePath}, returning original content."); +// return content; // No new assets to prefetch, return original content +// } +// +// await Task.WhenAny(tasks); +// +// return content; +// } +// +// [GeneratedRegex(@".\.exports = ""((?:[a-z\d/\.]*?)\.\w{2,4})""", RegexOptions.Compiled)] +// private static partial Regex PrefetchAssetsRegex(); +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs new file mode 100644
index 0000000..9d819c7 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs
@@ -0,0 +1,27 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class Void0Patch : IPatch { + public int GetOrder() => 0; + + public string GetName() => "Use literal undefined instead of void 0"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = stringContent + .Replace("void 0", "undefined"); + stringContent = VoidFunctionRegex().Replace( + stringContent, + m => $"{m.Groups[1].Value}(" + ); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@"\(0, ([a-zA-Z0-9_.$]+?)\)\(", RegexOptions.Compiled)] + private static partial Regex VoidFunctionRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs new file mode 100644
index 0000000..277bf8a --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs
@@ -0,0 +1,19 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class WhileTruePatch : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Patch while(true) expressions in JS"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string relativePath, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = stringContent.Replace("for (;;)", "while (true)"); + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs b/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs new file mode 100644
index 0000000..c3dba59 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs
@@ -0,0 +1,26 @@ +namespace ReferenceClientProxyImplementation.Patches.Implementations; + +public class PatchSet(IServiceProvider sp) { + public List<IPatch> Patches { get; } = sp.GetServices<IPatch>().OrderBy(x => x.GetOrder()).ToList(); + + public async Task<byte[]> ApplyPatches(string relativeName, byte[] content) { + var i = 0; + var patches = Patches + .Where(p => p.Applies(relativeName, content)) + .OrderBy(p => p.GetOrder()) + .ToList(); + foreach (var patch in patches) { + if (patch.Applies(relativeName, content)) { + var defaultColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.Write("==> "); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($"Running task {++i}/{patches.Count}: {patch.GetName()} (Type<{patch.GetType().Name}>)"); + Console.ForegroundColor = defaultColor; + content = await patch.Execute(relativeName, content); + } + } + + return content; + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Program.cs b/ReferenceClientProxyImplementation/Program.cs new file mode 100644
index 0000000..c35248b --- /dev/null +++ b/ReferenceClientProxyImplementation/Program.cs
@@ -0,0 +1,107 @@ +using ArcaneLibs; +using Microsoft.AspNetCore.HttpLogging; +using Microsoft.Extensions.Hosting.Systemd; +using ReferenceClientProxyImplementation.Configuration; +using ReferenceClientProxyImplementation.Patches; +using ReferenceClientProxyImplementation.Patches.Implementations; +using ReferenceClientProxyImplementation.Services; +using ReferenceClientProxyImplementation.Tasks; + +// using Spacebar.API.Tasks.Startup; + +if (!Directory.Exists("cache_formatted")) Directory.CreateDirectory("cache_formatted"); +if (!Directory.Exists("cache")) Directory.CreateDirectory("cache"); +/*foreach (var file in Directory.GetFiles("cache").Where(x => x.EndsWith(".js"))) +{ + //JsFormatter.FormatJsFile(File.OpenRead(file), File.OpenWrite(file.Replace("cache", "cache_formatted"))); +}*/ + +/*var processes = Directory.GetFiles("cache").Where(x => x.EndsWith(".js")).Select(file => JsFormatter.SafeFormat(file, file.Replace("cache", "cache_formatted"))).ToList(); +while (processes.Any(x => !x.HasExited)) +{ + Thread.Sleep(100); +}*/ + +//Environment.Exit(0); +// Tasks.RunStartup(); + +var builder = WebApplication.CreateBuilder(args); + +builder.Host.ConfigureHostOptions(host => { + host.ServicesStartConcurrently = true; + host.ServicesStopConcurrently = true; + host.ShutdownTimeout = TimeSpan.FromSeconds(5); +}); + +// builder.Services.AddHostedService<TemporaryTestJob>(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddHttpLogging(o => { o.LoggingFields = HttpLoggingFields.All; }); +builder.Services.AddLogging(o => { + if (SystemdHelpers.IsSystemdService()) + o.AddSystemdConsole(); + else o.AddConsole(); +}); + +builder.Services.AddSingleton<ProxyConfiguration>(); +// builder.Services.AddSingleton<BuildDownloadService>(); + +foreach (var taskType in ClassCollector<ITask>.ResolveFromAllAccessibleAssemblies()) +{ + builder.Services.AddSingleton(typeof(ITask), taskType); +} +builder.Services.AddHostedService<Tasks>(); + +foreach (var taskType in ClassCollector<IPatch>.ResolveFromAllAccessibleAssemblies()) +{ + builder.Services.AddSingleton(typeof(IPatch), taskType); +} +builder.Services.AddSingleton<PatchSet>(); + +builder.Services.AddSingleton<ClientStoreService>(); + +var app = builder.Build(); + +// +if (app.Environment.IsDevelopment()) { + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpLogging(); +app.UseRouting(); +// app.UseSentryTracing(); + +// app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + +app.Use((context, next) => { + context.Response.Headers["Content-Type"] += "; charset=utf-8"; + context.Response.Headers["Access-Control-Allow-Origin"] = "*"; + return next.Invoke(); +}); +app.UseCors("*"); + +app.MapControllers(); +app.UseDeveloperExceptionPage(); + +// +app.UseEndpoints(endpoints => { endpoints.MapControllerRoute("default", "{controller=FrontendController}/{action=Index}/{id?}"); }); + +Console.WriteLine("Starting web server!"); +if (args.Contains("--exit-on-modified")) { + Console.WriteLine("[WARN] --exit-on-modified enabled, exiting on source file change!"); + new FileSystemWatcher { + Path = Environment.CurrentDirectory, + Filter = "*.cs", + NotifyFilter = NotifyFilters.LastWrite, + EnableRaisingEvents = true + }.Changed += async (sender, args) => { + Console.WriteLine("Source modified. Exiting..."); + await app.StopAsync(); + Environment.Exit(0); + }; +} + +app.Run(); \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Properties/launchSettings.json b/ReferenceClientProxyImplementation/Properties/launchSettings.json new file mode 100644
index 0000000..3cac55a --- /dev/null +++ b/ReferenceClientProxyImplementation/Properties/launchSettings.json
@@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:60588", + "sslPort": 44301 + } + }, + "profiles": { + "Spacebar.API": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:2000;https://localhost:2001;http://localhost:2002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "CONFIG_PATH": "../Config.json" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ReferenceClientProxyImplementation/ReferenceClientProxyImplementation.csproj b/ReferenceClientProxyImplementation/ReferenceClientProxyImplementation.csproj new file mode 100644
index 0000000..14bdb55 --- /dev/null +++ b/ReferenceClientProxyImplementation/ReferenceClientProxyImplementation.csproj
@@ -0,0 +1,34 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net10.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + <EnableDefaultContentItems>false</EnableDefaultContentItems> + <LangVersion>preview</LangVersion> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)'=='Release'"> + <DebugSymbols>False</DebugSymbols> + <DebugType>None</DebugType> + <Optimize>True</Optimize> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="AngleSharp" Version="1.3.0"/> + <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20250630-114950"/> + <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6"/> + <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.6"/> + <PackageReference Include="Sentry.AspNetCore" Version="5.11.2"/> + <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1"/> + </ItemGroup> + + <ItemGroup> + <Compile Remove="cache\**"/> + <Compile Remove="cache_formatted\**"/> + <Compile Remove="server-cs-d9282bd218a35e2399d27e98cdd6f7a0a8552bb7\**"/> + </ItemGroup> + <ItemGroup> + <Content Include="Resources\**" CopyToOutputDirectory="Always"/> + </ItemGroup> + +</Project> diff --git a/ReferenceClientProxyImplementation/Resources/Assets/checkLocale.js b/ReferenceClientProxyImplementation/Resources/Assets/checkLocale.js new file mode 100644
index 0000000..2f46120 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/checkLocale.js
@@ -0,0 +1,47 @@ +const localStorage = window.localStorage; +// TODO: remote auth +// window.GLOBAL_ENV.REMOTE_AUTH_ENDPOINT = window.GLOBAL_ENV.GATEWAY_ENDPOINT.replace(/wss?:/, ""); +localStorage.setItem("gatewayURL", window.GLOBAL_ENV.GATEWAY_ENDPOINT); +localStorage.setItem( + "DeveloperOptionsStore", + `{"trace":false,"canary":false,"logGatewayEvents":true,"logOverlayEvents":true,"logAnalyticsEvents":true,"sourceMapsEnabled":false,"axeEnabled":false}` +); + +const supportedLocales = [ + "bg", + "cs", + "da", + "de", + "el", + "en-GB", + "es-ES", + "fi", + "fr", + "hi", + "hr", + "hu", + "it", + "ja", + "ko", + "lt", + "nl", + "no", + "pl", + "pt-BR", + "ro", + "ru", + "sv-SE", + "th", + "tr", + "uk", + "vi", + "zh-CN", + "zh-TW" +]; + +const settings = JSON.parse(localStorage.getItem("UserSettingsStore")); +if (settings && !supportedLocales.includes(settings.locale)) { + // fix client locale wrong and client not loading at all + settings.locale = "en-US"; + localStorage.setItem("UserSettingsStore", JSON.stringify(settings)); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Resources/Assets/dff87c953f43b561d71fbcfe8a93a79a.png b/ReferenceClientProxyImplementation/Resources/Assets/dff87c953f43b561d71fbcfe8a93a79a.png new file mode 100644
index 0000000..e69de29 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/dff87c953f43b561d71fbcfe8a93a79a.png
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/endpoints.json b/ReferenceClientProxyImplementation/Resources/Assets/endpoints.json new file mode 100644
index 0000000..d5478e7 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/endpoints.json
@@ -0,0 +1,115 @@ +{ + "USER_CHANNELS": "/users/@me/channels", + "USER_ACTIVITY_STATISTICS": "/users/@me/activities/statistics/applications", + "ACTIVITIES": "/activities", + "LOBBIES": "/lobbies", + "LOBBY_SEARCH": "/lobbies/search", + "NETWORKING_TOKEN": "/networking/token", + "USER_GAMES_NOTIFICATIONS": "/users/@me/settings/game-notifications", + "USER_GAMES_NOTIFICATIONS_OVERRIDES": "/users/@me/settings/game-notifications/overrides", + "UNVERIFIED_APPLICATIONS": "/unverified-applications", + "UNVERIFIED_APPLICATIONS_ICONS": "/unverified-applications/icons", + "BULK_ACK": "/read-states/ack-bulk", + "GUILDS": "/guilds", + "CHANNELS": "/channels", + "TUTORIAL_INDICATORS": "/tutorial/indicators", + "TUTORIAL_INDICATORS_SUPPRESS": "/tutorial/indicators/suppress", + "USERS": "/users", + "ME": "/users/@me", + "DELETE_ACCOUNT": "/users/@me/delete", + "DISABLE_ACCOUNT": "/users/@me/disable", + "DEVICES": "/users/@me/devices", + "SETTINGS": "/users/@me/settings", + "SETTINGS_CONSENT": "/users/@me/consent", + "PHONE": "/users/@me/phone", + "VERIFY_PHONE": "/users/@me/phone/verify", + "VERIFY_PHONE_NEW": "/phone-verifications/verify", + "RESEND_PHONE": "/phone-verifications/resend", + "CONNECTIONS": "/users/@me/connections", + "CONNECTION_SYNC_CONTACTS": "/users/@me/connections/contacts/@me/external-friend-list-entries", + "NOTES": "/users/@me/notes", + "MENTIONS": "/users/@me/mentions", + "CAPTCHA": "/users/@me/captcha/verify", + "EXPERIMENTS": "/experiments", + "LOGIN": "/auth/login", + "LOGIN_MFA": "/auth/mfa/totp", + "LOGIN_SMS": "/auth/mfa/sms", + "LOGIN_SMS_SEND": "/auth/mfa/sms/send", + "REMOTE_AUTH_INITIALIZE": "/users/@me/remote-auth", + "REMOTE_AUTH_CANCEL": "/users/@me/remote-auth/cancel", + "REMOTE_AUTH_FINISH": "/users/@me/remote-auth/finish", + "LOGOUT": "/auth/logout", + "REGISTER": "/auth/register", + "REGISTER_PHONE": "/auth/register/phone", + "TRACK": "/science", + "SSO": "/sso", + "VERIFY": "/auth/verify", + "AUTHORIZE_IP": "/auth/authorize-ip", + "VERIFY_RESEND": "/auth/verify/resend", + "FORGOT_PASSWORD": "/auth/forgot", + "RESET_PASSWORD": "/auth/reset", + "ICE": "/voice/ice", + "REPORT": "/report", + "REPORT_V2": "/reports", + "REPORT_OPTIONS": "/report/options", + "INTEGRATIONS": "/integrations", + "GATEWAY": "/gateway", + "APPLICATIONS_DETECTABLE": "/applications/detectable", + "OAUTH2_AUTHORIZE": "/oauth2/authorize", + "OAUTH2_AUTHORIZE_WEBHOOK_CHANNELS": "/oauth2/authorize/webhook-channels", + "OAUTH2_CURRENT_AUTH": "/oauth2/@me", + "OAUTH2_TOKENS": "/oauth2/tokens", + "OAUTH2_WHITELIST_ACCEPT": "/oauth2/whitelist/accept", + "MFA_TOTP_ENABLE": "/users/@me/mfa/totp/enable", + "MFA_TOTP_DISABLE": "/users/@me/mfa/totp/disable", + "MFA_SMS_ENABLE": "/users/@me/mfa/sms/enable", + "MFA_SMS_DISABLE": "/users/@me/mfa/sms/disable", + "MFA_CODES": "/users/@me/mfa/codes", + "DISABLE_EMAIL_NOTIFICATIONS": "/users/disable-email-notifications", + "GUILD_PREMIUM_SUBSCRIPTION_COOLDOWN": "/users/@me/guilds/premium/subscriptions/cooldown", + "USER_GUILD_PREMIUM_SUBSCRIPTIONS": "/users/@me/guilds/premium/subscriptions", + "USER_PREMIUM_GUILD_SUBSCRIPTION_SLOTS": "/users/@me/guilds/premium/subscription-slots", + "BILLING_STRIPE_SETUP_INTENT_SECRET": "/users/@me/billing/stripe/setup-intents", + "BILLING_PAYMENT_SOURCES": "/users/@me/billing/payment-sources", + "BILLING_PAYMENTS": "/users/@me/billing/payments", + "BILLING_BRAINTREE_POPUP_BRIDGE": "/billing/braintree/popup-bridge", + "BILLING_BRAINTREE_POPUP_BRIDGE_CALLBACK": "/billing/braintree/popup-bridge/callback", + "BILLING_SUBSCRIPTIONS": "/users/@me/billing/subscriptions", + "BILLING_APPLY_APPLE_RECEIPT": "/billing/apple/apply-receipt", + "BILLING_INVOICE_PREVIEW": "/users/@me/billing/invoices/preview", + "USER_AGREEMENTS": "/users/@me/agreements", + "HANDOFF": "/auth/handoff", + "HANDOFF_EXCHANGE": "/auth/handoff/exchange", + "LIBRARY": "/users/@me/library", + "AUTH_CONSENT_REQUIRED": "/auth/consent-required", + "USER_HARVEST": "/users/@me/harvest", + "APPLICATION_BRANCHES": "/branches", + "APPLICATIONS_PUBLIC": "/applications/public", + "APPLICATIONS_TRENDING": "/applications/trending/global", + "STORE_PUBLISHED_LISTINGS_APPLICATIONS": "/store/published-listings/applications", + "STORE_PUBLISHED_LISTINGS_SKUS": "/store/published-listings/skus", + "ENTITLEMENTS_GIFTABLE": "/users/@me/entitlements/gifts", + "PROMOTIONS": "/promotions", + "PROMOTION_ACK": "/promotions/ack", + "HYPESQUAD_ONLINE": "/hypesquad/online", + "GIFS_SEARCH": "/gifs/search", + "GIFS_TRENDING": "/gifs/trending", + "GIFS_TRENDING_GIFS": "/gifs/trending-gifs", + "GIFS_SELECT": "/gifs/select", + "GIFS_SUGGEST": "/gifs/suggest", + "GIFS_TRENDING_SEARCH": "/gifs/trending-search", + "USER_GIFT_CODE_CREATE": "/users/@me/entitlements/gift-codes", + "USER_GIFT_CODES": "/users/@me/entitlements/gift-codes", + "GUILD_DISCOVERY": "/discoverable-guilds", + "GUILD_DISCOVERY_CATEGORIES": "/discovery/categories", + "GUILD_DISCOVERY_VALID_TERM": "/discovery/valid-term", + "USER_AFFINITIES": "/users/@me/affinities/users", + "GUILD_AFFINITIES": "/users/@me/affinities/guilds", + "XBOX_GAME_PASS_PROMOTION": "/promotions/xbox-game-pass", + "XBOX_GAME_PASS_PROMOTION_REDEEM": "/promotions/xbox-game-pass/redeem", + "FUNIMATION_PROMOTION": "/promotions/funimation", + "PARTNERS_CONNECTIONS": "/partners/connections", + "PARTNERS_APPLY": "/partners/apply", + "USER_STICKER_PACKS": "/users/@me/sticker-packs", + "INTERACTIONS": "/interactions" +} diff --git a/ReferenceClientProxyImplementation/Resources/Assets/features.json b/ReferenceClientProxyImplementation/Resources/Assets/features.json new file mode 100644
index 0000000..1e1ebb1 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/features.json
@@ -0,0 +1,26 @@ +[ + "ANIMATED_ICON", + "BANNER", + "COMMERCE", + "COMMUNITY", + "DISCOVERABLE", + "DISCOVERABLE_DISABLED", + "ENABLED_DISCOVERABLE_BEFORE", + "HUB", + "INVITE_SPLASH", + "MONETIZATION_ENABLED", + "MORE_EMOJI", + "MORE_STICKERS", + "NEWS", + "PARTNERED", + "PREVIEW_ENABLED", + "PRIVATE_THREADS", + "SEVEN_DAY_THREAD_ARCHIVE", + "THREE_DAY_THREAD_ARCHIVE", + "THREADS_ENABLED", + "TICKETED_EVENTS_ENABLED", + "VANITY_URL", + "VERIFIED", + "VIP_REGIONS", + "WELCOME_SCREEN_ENABLED" +] diff --git a/ReferenceClientProxyImplementation/Resources/Assets/fosscord-login.css b/ReferenceClientProxyImplementation/Resources/Assets/fosscord-login.css new file mode 100644
index 0000000..37bb111 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/fosscord-login.css
@@ -0,0 +1,71 @@ +/* replace tos acceptance popup */ +#app-mount > div:nth-child(7) > div > div > div.tooltipContent-bqVLWK { + visibility: hidden; +} + +#app-mount > div:nth-child(7) > div > div > div.tooltipContent-bqVLWK::after { + visibility: visible; + display: block; + content: "You need to agree to this instance's rules to continue"; + margin-top: -32px; +} + +/* replace login header */ +#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.mainLoginContainer-1ddwnR > h3 { + visibility: hidden; +} + +h3.title-jXR8lp.marginBottom8-AtZOdT.base-1x0h_U.size24-RIRrxO::after { + margin-top: -32px; + content: "Welcome to Spacebar!"; + visibility: visible; + display: block; +} + +/* Logo in top left when bg removed */ +#app-mount > div.app-1q1i1E > div > a { + /* replace me: original dimensions: 130x36 */ + background: url(https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Spacebar-Wordmark-Gradient.svg); + width: 130px; + height: 23px; + background-size: contain; +} + +/* replace TOS text */ + +#app-mount +> div.app-1q1i1E +> div +> div +> div +> form +> div +> div +> div.flex-1xMQg5.flex-1O1GKY.horizontal-1ae9ci.horizontal-2EEEnY.flex-1O1GKY.directionRow-3v3tfG.justifyStart-2NDFzi.alignCenter-1dQNNs.noWrap-3jynv6.marginTop20-3TxNs6 +> label +> div.label-cywgfr.labelClickable-11AuB8.labelForward-1wfipV +> * { + visibility: hidden; +} + +#app-mount +> div.app-1q1i1E +> div +> div +> div +> form +> div +> div +> div.flex-1xMQg5.flex-1O1GKY.horizontal-1ae9ci.horizontal-2EEEnY.flex-1O1GKY.directionRow-3v3tfG.justifyStart-2NDFzi.alignCenter-1dQNNs.noWrap-3jynv6.marginTop20-3TxNs6 +> label +> div.label-cywgfr.labelClickable-11AuB8.labelForward-1wfipV::after { + visibility: visible; + content: "I have read and agree with the rules set by this instance."; + display: block; + margin-top: -16px; +} + +/* shrink login box to same size as register */ +.authBoxExpanded-2jqaBe { + width: 480px !important; +} diff --git a/ReferenceClientProxyImplementation/Resources/Assets/fosscord.css b/ReferenceClientProxyImplementation/Resources/Assets/fosscord.css new file mode 100644
index 0000000..283c29f --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/fosscord.css
@@ -0,0 +1,44 @@ +/* loading spinner */ +#app-mount > div.app-1q1i1E > div.container-16j22k.fixClipping-3qAKRb > div.content-1-zrf2 > video { + filter: opacity(1); + background: url("http://www.clipartbest.com/cliparts/7ca/6Rr/7ca6RrLAi.gif"); + background-size: contain; + /* width: 64px; + height: 64px; */ + padding-bottom: 64px; + background-repeat: no-repeat; +} + +/* home button icon */ +#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div { + background-image: url(https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Spacebar-Icon-Rounded-Subtract.svg); + background-size: contain; + border-radius: 50%; +} + +#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div, #app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div:hover { + background-color: white; +} + +/* Login QR */ +#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.transitionGroup-aR7y1d.qrLogin-1AOZMt, +#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.verticalSeparator-3huAjp, + /* Remove login bg */ +#app-mount > div.app-1q1i1E > div > svg, + /* Download bar */ +#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.notice-3bPHh-.colorDefault-22HBa0, + /* Connection problem links */ +#app-mount > div.app-1q1i1E > div.container-16j22k.fixClipping-3qAKRb > div.problems-3mgf6w.slideIn-sCvzGz > div:nth-child(2), + /* Downloads button */ +#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div:nth-child(7) > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div, +#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div:nth-child(6) > div, + /* help button */ +#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.content-98HsJk > div.chat-3bRxxu > section > div.toolbar-1t6TWx > a, + /* download button start of guild */ +#chat-messages-899316648933185083 > div > div > div:nth-child(5), + /* Thread permissions etc popups */ +#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.content-98HsJk > div.sidebar-2K8pFh.hasNotice-1XRy4h > nav > div.container-3O_wAf, + /* home button icon */ +#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div > svg { + display: none; +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Resources/Assets/plugins/.gitkeep b/ReferenceClientProxyImplementation/Resources/Assets/plugins/.gitkeep new file mode 100644
index 0000000..e69de29 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/plugins/.gitkeep
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/preload-plugins/autoRegister.js b/ReferenceClientProxyImplementation/Resources/Assets/preload-plugins/autoRegister.js new file mode 100644
index 0000000..15ef582 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/preload-plugins/autoRegister.js
@@ -0,0 +1,62 @@ +// Auto register guest account: +const prefix = [ + "mysterious", + "adventurous", + "courageous", + "precious", + "cynical", + "flamer ", + "despicable", + "suspicious", + "gorgeous", + "impeccable", + "lovely", + "stunning", + "keyed", + "phoned", + "glorious", + "amazing", + "strange", + "arcane" +]; +const suffix = [ + "Anonymous", + "Boy", + "Lurker", + "Keyhitter", + "User", + "Enjoyer", + "Hunk", + "Coolstar", + "Wrestling", + "TylerTheCreator", + "Ad", + "Gamer", + "Games", + "Programmer" +]; + +Array.prototype.random = function () { + return this[Math.floor(Math.random() * this.length)]; +}; + +function _generateName() { + return `${prefix.random()}${suffix.random()}`; +} + +var token = JSON.parse(localStorage.getItem("token")); +if (!token && location.pathname !== "/login" && location.pathname !== "/register") { + fetch(`${window.GLOBAL_ENV.API_ENDPOINT}/auth/register`, { + method: "POST", + headers: {"content-type": "application/json"}, + body: JSON.stringify({username: `${_generateName()}`, consent: true}) //${Date.now().toString().slice(-4)} + }) + .then((x) => x.json()) + .then((x) => { + localStorage.setItem("token", `"${x.token}"`); + if (!window.localStorage) { + // client already loaded -> need to reload to apply the newly registered user token + location.reload(); + } + }); +} diff --git a/ReferenceClientProxyImplementation/Resources/Assets/tools/rightsCalculator.js b/ReferenceClientProxyImplementation/Resources/Assets/tools/rightsCalculator.js new file mode 100644
index 0000000..c324891 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/tools/rightsCalculator.js
@@ -0,0 +1,17 @@ +String.prototype.replaceAt = function (index, replacement) { + return this.substring(0, index) + replacement + this.substring(index + 1); +} + +var legacyRights = 0n; +var modernRights = '0'.repeat(document.getElementsByTagName("input").length); +var configRights = {}; + +function calculate(a) { + console.log(a) + legacyRights += a.checked ? (1n << BigInt(a.value)) : -(1n << BigInt(a.value)) + modernRights = modernRights.replaceAt(a.value, a.checked ? '1' : '0') + configRights[a.name] = a.checked + document.getElementById("legacyRights").innerText = "Legacy rights (fosscord-server-ts): " + legacyRights + document.getElementById("modernRights").innerText = "User rights: " + modernRights + document.getElementById("configRights").value = JSON.stringify(configRights, null, 4) +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Resources/Assets/user.css b/ReferenceClientProxyImplementation/Resources/Assets/user.css new file mode 100644
index 0000000..a7e5c4f --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/user.css
@@ -0,0 +1 @@ +/* Your custom CSS goes here, enjoy! */ \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/banner1.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner1.png new file mode 100644
index 0000000..ed9bd5c --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner1.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/banner2.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner2.png new file mode 100644
index 0000000..90d3713 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner2.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/banner3.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner3.png new file mode 100644
index 0000000..2235189 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner3.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/banner4.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner4.png new file mode 100644
index 0000000..e6bd7b6 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner4.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/shield.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/shield.png new file mode 100644
index 0000000..30277db --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/shield.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Pages/developers.html b/ReferenceClientProxyImplementation/Resources/Pages/developers.html new file mode 100644
index 0000000..9798554 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Pages/developers.html
@@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html class="theme-dark" data-theme="dark"> + <head> + <meta charset="utf-8"/> + <meta content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no" name="viewport"/> + + <link href="/assets/532.03aaeef88460fae60534.css" integrity="" rel="stylesheet"/> + <link href="/assets/07dca80a102d4149e9736d4b162cff6f.ico" rel="icon"/> + <title>Discord Test Client Developer Portal</title> + <meta charset="utf-8" data-react-helmet="true"/> + </head> + + <body> + <div id="app-mount"></div> + <script> + window.GLOBAL_ENV = { + API_VERSION: 9, + API_ENDPOINT: "/api", + WEBAPP_ENDPOINT: "", + CDN_HOST: `${location.hostname}:3003`, + + BRAINTREE_KEY: "production_5st77rrc_49pp2rp4phym7387", + STRIPE_KEY: "pk_live_CUQtlpQUF0vufWpnpUmQvcdi", + MARKETING_ENDPOINT: "//discord.com", + RELEASE_CHANNEL: "stable", + ALGOLIA_KEY: "aca0d7082e4e63af5ba5917d5e96bed0" + }; + GLOBAL_ENV.MEDIA_PROXY_ENDPOINT = location.protocol + "//" + GLOBAL_ENV.CDN_HOST; + const localStorage = window.localStorage; + // TODO: remote auth + // window.GLOBAL_ENV.REMOTE_AUTH_ENDPOINT = window.GLOBAL_ENV.GATEWAY_ENDPOINT.replace(/wss?:/, ""); + localStorage.setItem("gatewayURL", window.GLOBAL_ENV.GATEWAY_ENDPOINT); + localStorage.setItem( + "DeveloperOptionsStore", + JSON.stringify({ + trace: false, + canary: true, + logGatewayEvents: true, + logOverlayEvents: true, + logAnalyticsEvents: true, + sourceMapsEnabled: false, + axeEnabled: true, + bugReporterEnabled: true, + idleStatusIndicatorEnabled: false + }) + ); + </script> + <script integrity="" src="/assets/41fde19fdf180f3d4315.js"></script> + <script integrity="" src="/assets/7b04a3ab10e05dd9054e.js"></script> + <script integrity="" src="/assets/d1f811da193e5648048b.js"></script> + </body> +</html> diff --git a/ReferenceClientProxyImplementation/Resources/Pages/index-template.html b/ReferenceClientProxyImplementation/Resources/Pages/index-template.html new file mode 100644
index 0000000..ef77e22 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Pages/index-template.html
@@ -0,0 +1,75 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"/> + <meta content="width=device-width, initial-scale=1.0" name="viewport"/> + <title>Discord Test Client</title> + <link href="/assets/fosscord.css" rel="stylesheet"/> + <link href="/assets/fosscord-login.css" id="logincss" rel="stylesheet"/> + <link href="/assets/user.css" id="customcss" rel="stylesheet"/> + <!-- preload plugin marker --> + </head> + + <body> + <div id="app-mount"></div> + <script> + window.__OVERLAY__ = /overlay/.test(location.pathname); + window.__BILLING_STANDALONE__ = /^\/billing/.test(location.pathname); + + var xmlhttp = new XMLHttpRequest(); + var url = "/api/_fosscord/v1/global_env"; + xmlhttp.onreadystatechange = function () { + if (this.readyState == 4 && this.status == 200) { + window.GLOBAL_ENV = JSON.parse(this.responseText); + } + } + + xmlhttp.open("GET", url, false); + xmlhttp.send(); + + + const localStorage = window.localStorage; + // TODO: remote auth + // window.GLOBAL_ENV.REMOTE_AUTH_ENDPOINT = window.GLOBAL_ENV.GATEWAY_ENDPOINT.replace(/wss?:/, ""); + localStorage.setItem("gatewayURL", window.GLOBAL_ENV.GATEWAY_ENDPOINT); + localStorage.setItem( + "DeveloperOptionsStore", + JSON.stringify({ + trace: false, + canary: true, + logGatewayEvents: true, + logOverlayEvents: true, + logAnalyticsEvents: true, + sourceMapsEnabled: false, + axeEnabled: true, + bugReporterEnabled: true, + idleStatusIndicatorEnabled: false + }) + ); + + setInterval(() => { + var token = JSON.parse(localStorage.getItem("token")); + if (token) { + var logincss = document.querySelector('#logincss'), + canRemove = logincss ? logincss : ""; + if (canRemove !== "") { + document.querySelector("#logincss").remove(); + canRemove = ""; + } + } + }, 1000) + + const settings = JSON.parse(localStorage.getItem("UserSettingsStore")); + if (settings && settings.locale.length <= 2) { + // fix client locale wrong and client not loading at all + settings.locale = "en-US"; + localStorage.setItem("UserSettingsStore", JSON.stringify(settings)); + } + </script> + <!--prefetch_script--> + <!--client_css--> + <script src="/assets/checkLocale.js"></script> + <!--client_script--> + <!-- plugin marker --> + </body> +</html> diff --git a/ReferenceClientProxyImplementation/Resources/Pages/index.html b/ReferenceClientProxyImplementation/Resources/Pages/index.html new file mode 100644
index 0000000..0b52736 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Pages/index.html
@@ -0,0 +1,130 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"/> + <meta content="width=device-width, initial-scale=1.0" name="viewport"/> + <title>Discord Test Client</title> + <link href="/assets/fosscord.css" rel="stylesheet"/> + <link href="/assets/fosscord-login.css" id="logincss" rel="stylesheet"/> + <link href="/assets/user.css" id="customcss" rel="stylesheet"/> + <!-- preload plugin marker --> + </head> + + <body> + <div id="app-mount"></div> + <script> + window.__OVERLAY__ = /overlay/.test(location.pathname); + window.__BILLING_STANDALONE__ = /^\/billing/.test(location.pathname); + window.GLOBAL_ENV = { + API_ENDPOINT: "/api", + API_VERSION: 9, + GATEWAY_ENDPOINT: `${location.protocol === "https:" ? "wss://" : "ws://"}${location.hostname}:2001`, + WEBAPP_ENDPOINT: "", + CDN_HOST: `${location.hostname}:3003`, + ASSET_ENDPOINT: "", + MEDIA_PROXY_ENDPOINT: "https://media.discordapp.net", + WIDGET_ENDPOINT: `//${location.host}/widget`, + INVITE_HOST: `${location.host}/invite`, + GUILD_TEMPLATE_HOST: "discord.new", + GIFT_CODE_HOST: "discord.gift", + RELEASE_CHANNEL: "staging", + MARKETING_ENDPOINT: "//discord.com", + BRAINTREE_KEY: "production_5st77rrc_49pp2rp4phym7387", + STRIPE_KEY: "pk_live_CUQtlpQUF0vufWpnpUmQvcdi", + NETWORKING_ENDPOINT: "//router.discordapp.net", + RTC_LATENCY_ENDPOINT: "//latency.discord.media/rtc", + ACTIVITY_APPLICATION_HOST: 'discordsays.com', + PROJECT_ENV: "staging", + REMOTE_AUTH_ENDPOINT: "//localhost:3020", + SENTRY_TAGS: {buildId: "75e36d9", buildType: "normal"}, + MIGRATION_SOURCE_ORIGIN: "https://discordapp.com", + MIGRATION_DESTINATION_ORIGIN: "https://discord.com", + HTML_TIMESTAMP: Date.now(), + ALGOLIA_KEY: "aca0d7082e4e63af5ba5917d5e96bed0" + }; + GLOBAL_ENV.MEDIA_PROXY_ENDPOINT = location.protocol + "//" + GLOBAL_ENV.CDN_HOST; + const localStorage = window.localStorage; + // TODO: remote auth + // window.GLOBAL_ENV.REMOTE_AUTH_ENDPOINT = window.GLOBAL_ENV.GATEWAY_ENDPOINT.replace(/wss?:/, ""); + localStorage.setItem("gatewayURL", window.GLOBAL_ENV.GATEWAY_ENDPOINT); + localStorage.setItem( + "DeveloperOptionsStore", + JSON.stringify({ + trace: false, + canary: true, + logGatewayEvents: true, + logOverlayEvents: true, + logAnalyticsEvents: true, + sourceMapsEnabled: false, + axeEnabled: true, + bugReporterEnabled: true, + idleStatusIndicatorEnabled: false + }) + ); + + setInterval(() => { + var token = JSON.parse(localStorage.getItem("token")); + if (token) { + var logincss = document.querySelector('#logincss'), + canRemove = logincss ? logincss : ""; + if (canRemove !== "") { + document.querySelector("#logincss").remove(); + canRemove = ""; + } + } + }, 1000) + + const settings = JSON.parse(localStorage.getItem("UserSettingsStore")); + if (settings && settings.locale.length <= 2) { + // fix client locale wrong and client not loading at all + settings.locale = "en-US"; + localStorage.setItem("UserSettingsStore", JSON.stringify(settings)); + } + </script> + <link as="script" href="/assets/d110af2d26131beb5874.js" rel="prefetch"> + <link as="script" href="/assets/b4499d2a6b9046b1b402.js" rel="prefetch"> + <link as="script" href="/assets/4b58fa778cc38e586a72.js" rel="prefetch"> + <link as="script" href="/assets/2e45b6b321248222f12a.js" rel="prefetch"> + <link as="script" href="/assets/2a6b8d1c4c54837fbc1c.js" rel="prefetch"> + <link as="script" href="/assets/32a385a6252512863141.js" rel="prefetch"> + <link as="script" href="/assets/66fb582b4c0e8351bd05.js" rel="prefetch"> + <link as="script" href="/assets/d5b25e619c7d3c54072a.js" rel="prefetch"> + <link as="script" href="/assets/e03405baa0373b35d236.js" rel="prefetch"> + <link as="script" href="/assets/7ac1c95d059085cc0b95.js" rel="prefetch"> + <link as="script" href="/assets/0778eb7c5d969638777f.js" rel="prefetch"> + <link as="script" href="/assets/d2888bff1a9b35045d7a.js" rel="prefetch"> + <link as="script" href="/assets/46f8b7fcfcafca99414f.js" rel="prefetch"> + <link as="script" href="/assets/e2d458306e3a43a03786.js" rel="prefetch"> + <link as="script" href="/assets/2f6fc578446032c0c0a5.js" rel="prefetch"> + <link as="script" href="/assets/14a17120248ea20823d1.js" rel="prefetch"> + <link as="script" href="/assets/0cfed40d2532db2e560f.js" rel="prefetch"> + <link as="script" href="/assets/f66efd328f8ca3ad6f1d.js" rel="prefetch"> + <link as="script" href="/assets/a1ad32825016ef835b51.js" rel="prefetch"> + <link as="script" href="/assets/916a3fd42b56f5b5b0ff.js" rel="prefetch"> + <link as="script" href="/assets/e4c9aec8e2ccf49c6a43.js" rel="prefetch"> + <link as="script" href="/assets/4eb8ce79510c19e425f0.js" rel="prefetch"> + <link as="script" href="/assets/669ce420727060b05505.js" rel="prefetch"> + <link as="script" href="/assets/90adae2a1e530ee2388a.js" rel="prefetch"> + <link as="script" href="/assets/95785439b23503f2a4bc.js" rel="prefetch"> + <link as="script" href="/assets/4dcc4abc98689f0e601b.js" rel="prefetch"> + <link as="script" href="/assets/50b1f37a5e2c8602463a.js" rel="prefetch"> + <link as="script" href="/assets/0d7b3701d6136866c7ec.js" rel="prefetch"> + <link as="script" href="/assets/d84f21f10b08966b8903.js" rel="prefetch"> + <link as="script" href="/assets/60385fdbdf9f90d8c4c7.js" rel="prefetch"> + <link as="script" href="/assets/7f327522a62dfbc3fa17.js" rel="prefetch"> + <link as="script" href="/assets/50ef6fe50ba0096918d9.js" rel="prefetch"> + <link as="script" href="/assets/6f81e4e1883133e82730.js" rel="prefetch"> + <link as="script" href="/assets/ec34e6ac622f2adf141a.js" rel="prefetch"> + <link as="script" href="/assets/1839152a11550e6304c2.js" rel="prefetch"> + <link as="script" href="/assets/e7ab6471de10e3f701e6.js" rel="prefetch"> + <link as="script" href="/assets/efb278e1216055e3d1a8.js" rel="prefetch"> + <link as="script" href="/assets/70093dfc536e766dbb27.js" rel="prefetch"> + <link href="/assets/40532.bd1a2e262154456bad22.css" rel="stylesheet"> + <script src="/assets/checkLocale.js"></script> + <script src="/assets/81d16d948535e8d77831.js"></script> + <script src="/assets/d110af2d26131beb5874.js"></script> + <script src="/assets/c44d54de4c931fca740e.js"></script> + <script src="/assets/2cc7dd42639b0c3ae762.js"></script> + <!-- plugin marker --> + </body> +</html> diff --git a/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDataLog.html b/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDataLog.html new file mode 100644
index 0000000..ef16e72 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDataLog.html
@@ -0,0 +1,7 @@ +<script> + window.toHexString = function (byteArray) { + return byteArray.reduce((output, elem) => + (output + (elem.toString(16).padStart(2, '0').toUpperCase() + ' ')), + ''); + } +</script> \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDumper.html b/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDumper.html new file mode 100644
index 0000000..14dc989 --- /dev/null +++ b/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDumper.html
@@ -0,0 +1,22 @@ +<script> + window.sockets = []; + var lastBuff = ''; + const nativeWebSocket = window.WebSocket; + window.WebSocket = function (...args) { + console.log("Starting new websocket"); + const socket = new nativeWebSocket(...args); + window.sockets.push(socket); + if (!args[0].includes('spotify')) + socket.addEventListener("message", ev => { + console.log("Dumping message..."); + lastBuff = ev.data; + var dat = new Uint8Array(lastBuff); + if (window.toHexString) console.log(window.toHexString(dat)); + var xhr = new XMLHttpRequest; + xhr.open("POST", "http://localhost:2001/dump/cs", false); + xhr.send(ev.data); + }); + console.log("Websocket hooked!", socket); + return socket; + }; +</script> \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Services/BuildDownloadService.cs b/ReferenceClientProxyImplementation/Services/BuildDownloadService.cs new file mode 100644
index 0000000..364c6c5 --- /dev/null +++ b/ReferenceClientProxyImplementation/Services/BuildDownloadService.cs
@@ -0,0 +1,76 @@ +// using System.Net; +// using AngleSharp.Html.Parser; +// +// namespace ReferenceClientProxyImplementation.Services; +// +// public class BuildDownloadService(ILogger<BuildDownloadService> logger) { +// private static readonly HttpClient hc = new(); +// +// public async Task DownloadBuildFromArchiveOrg(string outputDirectory, DateTime timestamp) { +// // 20150906025145 +// var paddedTimestamp = timestamp.ToString("yyyyMMddHHmmss"); +// await DownloadBuildFromUrl(outputDirectory, $"https://web.archive.org/web/{paddedTimestamp}im_/https://discordapp.com/login"); +// } +// +// public async Task DownloadBuildFromUrl(string outputDirectory, string url) { +// logger.LogInformation("Downloading build from {url} to {outDir}", url, outputDirectory); +// var response = await hc.GetAsync(url); +// if (!response.IsSuccessStatusCode) +// throw new Exception($"Failed to download build from {url}"); +// var html = await response.Content.ReadAsStringAsync(); +// File.WriteAllText(outputDirectory + "/index.html", html); +// var parser = new HtmlParser(); +// var document = parser.ParseDocument(html); +// var assets = document.QuerySelectorAll("link[rel=stylesheet], link[rel=icon], script, img"); +// foreach (var asset in assets) { +// var assetUrl = asset.GetAttribute("href") ?? asset.GetAttribute("src"); +// if (assetUrl == null) +// continue; +// if (assetUrl.StartsWith("//")) { +// logger.LogWarning("Skipping asset {assetUrl} as it is a protocol-relative URL", assetUrl); +// continue; +// } +// +// var assetStream = await GetAssetStream(assetUrl); +// var assetPath = Path.Combine(outputDirectory, assetUrl.TrimStart('/')); +// Console.WriteLine($"Downloading asset {assetUrl} to {assetPath}"); +// Directory.CreateDirectory(Path.GetDirectoryName(assetPath)); +// await using var fs = File.Create(assetPath); +// await assetStream.CopyToAsync(fs); +// } +// +// logger.LogInformation("Downloading build from {url} complete!", url); +// } +// +// public async Task<Stream> GetAssetStream(string asset) { +// asset = asset.Replace("/assets/", ""); +// var urlsToTry = new Stack<string>(new[] { +// $"https://web.archive.org/web/0id_/https://discordapp.com/assets/{asset}", +// $"https://web.archive.org/web/0id_/https://discord.com/assets/{asset}", +// $"https://discord.com/assets/{asset}" +// }); +// while (urlsToTry.TryPop(out var urlToTry)) { +// if (string.IsNullOrWhiteSpace(urlToTry)) continue; +// try { +// var response = await hc.GetAsync(urlToTry, HttpCompletionOption.ResponseHeadersRead); +// if (response.IsSuccessStatusCode) { +// Console.WriteLine($"Got success for asset {asset} from {urlToTry}"); +// return await response.Content.ReadAsStreamAsync(); +// } +// //redirect +// +// if (response.StatusCode == HttpStatusCode.Found) { +// var redirectUrl = response.Headers.Location?.ToString(); +// if (string.IsNullOrWhiteSpace(redirectUrl)) continue; +// urlsToTry.Push(redirectUrl); +// } +// else logger.LogWarning("Failed to download asset {asset} from {urlToTry}", asset, urlToTry); +// } +// catch { +// // ignored +// } +// } +// +// throw new Exception($"Failed to download asset {asset}"); +// } +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Services/ClientStoreService.cs b/ReferenceClientProxyImplementation/Services/ClientStoreService.cs new file mode 100644
index 0000000..6bd7418 --- /dev/null +++ b/ReferenceClientProxyImplementation/Services/ClientStoreService.cs
@@ -0,0 +1,62 @@ +using ArcaneLibs.Extensions.Streams; +using ReferenceClientProxyImplementation.Configuration; +using ReferenceClientProxyImplementation.Patches.Implementations; + +namespace ReferenceClientProxyImplementation.Services; + +public class ClientStoreService(ProxyConfiguration config, PatchSet patches) { + private static readonly HttpClient HttpClient = new(); + + public async Task<Stream> GetPatchedClientAsset(string relativePath) { + if (relativePath.StartsWith("/")) { + relativePath = relativePath[1..]; + } + + var path = Path.Combine(config.TestClient.RevisionPath, "patched", relativePath); + + if (File.Exists(path)) + return File.OpenRead(path); + + var srcAsset = (await GetOrDownloadRawAsset(relativePath)).ReadToEnd().ToArray(); + var result = await patches.ApplyPatches(relativePath, srcAsset); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + if (!result.SequenceEqual(srcAsset)) { + await File.WriteAllBytesAsync(path, result); + return File.OpenRead(path); + } + + Console.WriteLine($"No patches applied for {relativePath}, returning original asset."); + return new MemoryStream(srcAsset); + } + + public async Task<Stream> GetOrDownloadRawAsset(string relativePath) { + relativePath = relativePath.TrimStart('/'); + var assetPath = Path.Combine(config.TestClient.RevisionPath, "src", relativePath); + if (File.Exists(assetPath)) { + Console.WriteLine($"Asset {relativePath} already exists at {assetPath}, returning existing file."); + return File.OpenRead(assetPath); + } + + var url = $"{config.TestClient.RevisionBaseUrl}/{relativePath}"; + var response = await HttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + if (!response.IsSuccessStatusCode) { + Console.WriteLine($"Failed to download asset {relativePath} from {url}, status code: {response.StatusCode}"); + throw new FileNotFoundException($"Asset not found: {relativePath}"); + } + var contentStream = await response.Content.ReadAsStreamAsync(); + Directory.CreateDirectory(Path.GetDirectoryName(assetPath)!); + await using var fileStream = File.Create(assetPath); + await contentStream.CopyToAsync(fileStream); + fileStream.Close(); + contentStream.Close(); + Console.WriteLine($"Downloaded asset {relativePath} to {assetPath}"); + + return File.OpenRead(assetPath); + } + + public bool HasRawAsset(string relativePath) { + relativePath = relativePath.TrimStart('/'); + var assetPath = Path.Combine(config.TestClient.RevisionPath, "src", relativePath); + return File.Exists(assetPath); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs b/ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs new file mode 100644
index 0000000..039fc74 --- /dev/null +++ b/ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs
@@ -0,0 +1,3 @@ +namespace ReferenceClientProxyImplementation.Services; + +public class ModernAssetLocator { } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs b/ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs new file mode 100644
index 0000000..b8998f1 --- /dev/null +++ b/ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs
@@ -0,0 +1,14 @@ +// namespace ReferenceClientProxyImplementation.Services; +// +// public class TemporaryTestJob(BuildDownloadService buildDownloadService) : BackgroundService { +// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { +// Console.WriteLine("Running test job"); +// var outDir = +// "/home/Rory/git/spacebar/server-cs/DevUtils/ReferenceClientProxyImplementation/downloadCache/today/raw/"; +// if (Directory.Exists(outDir)) +// Directory.Delete(outDir, true); +// Directory.CreateDirectory(outDir); +// // await buildDownloadService.DownloadBuildFromArchiveOrg(outDir, new DateTime(2014, 1, 1)); +// await buildDownloadService.DownloadBuildFromUrl(outDir, "https://canary.discord.com/app"); +// } +// } \ No newline at end of file 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 diff --git a/ReferenceClientProxyImplementation/Tasks/Tasks.cs b/ReferenceClientProxyImplementation/Tasks/Tasks.cs new file mode 100644
index 0000000..5038002 --- /dev/null +++ b/ReferenceClientProxyImplementation/Tasks/Tasks.cs
@@ -0,0 +1,28 @@ +namespace ReferenceClientProxyImplementation.Tasks; + +public class Tasks(IServiceProvider serviceProvider) : BackgroundService { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + var defaultColor = Console.ForegroundColor; + var tasks = serviceProvider.GetServices<ITask>().ToList(); + // List<ITask> tasks = new() + // { + // new BuildClientTask(), + // new PatchClientAssetsTask() + // }; + var i = 0; + foreach (var task in tasks.OrderBy(x => x.GetOrder())) { + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.Write("==> "); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($"Running task {++i}/{tasks.Count}: {task.GetName()} (Type<{task.GetType().Name}>)"); + Console.ForegroundColor = defaultColor; + task.Execute(); + } + } +} + +public interface ITask { + public int GetOrder(); + public string GetName(); + public Task Execute(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/appsettings.Development.json b/ReferenceClientProxyImplementation/appsettings.Development.json new file mode 100644
index 0000000..6cb9d8a --- /dev/null +++ b/ReferenceClientProxyImplementation/appsettings.Development.json
@@ -0,0 +1,62 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information", + "Microsoft.AspNetCore.HttpLogging": "Warning", + "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Warning", + "Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor": "Warning", + "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker": "Warning" + } + }, + "ProxyConfiguration": { + "TestClient": { + "Enabled": true, + "Revision": "canary", + // This only does text properties! + "GlobalEnv": { +// "IMAGE_PROXY_ENDPOINTS": "//images-ext-1.discordapp.net,//images-ext-2.discordapp.net", + + + "API_ENDPOINT": "//api.old.server.spacebar.chat/api", + "GATEWAY_ENDPOINT": "wss://gateway.old.server.spacebar.chat", + "MEDIA_PROXY_ENDPOINT": "//cdn.old.server.spacebar.chat", + "IMAGE_PROXY_ENDPOINTS": "//cdn.old.server.spacebar.chat", + "API_PROTOCOL": "https:", + "CDN_HOST": "cdn.old.server.spacebar.chat", + + + //"API_ENDPOINT": "//localhost:3001/api", + //"GATEWAY_ENDPOINT": "ws://localhost:3002", + //"MEDIA_PROXY_ENDPOINT": "//localhost:3003", + //"IMAGE_PROXY_ENDPOINTS": "//localhost:3003", + //"CDN_HOST": "localhost:3003", + //"API_PROTOCOL": "http:", + + + "NODE_ENV": "production", + "PROJECT_ENV": "development", //internal + "RELEASE_CHANNEL": "internal", + "PUBLIC_PATH": "/assets/", + "API_VERSION": 9 + } + }, + "AssetCache": { + "MemoryCache": true, + "DiskCache": true, + "DiskCachePath": "cache", + "DiskCacheBaseDirectory": "./clientRepository", + "DitchPatchedOnStartup": true, + "WipeOnStartup": false, + "ExecOnRevisionChange": [ + ["code", "-n", "{revisionPath}/patched"] + ], + //"BiomePath": "/nix/store/cahp9z8wkr9kazb2qx54ddp1fgjgrrxa-biome-2.2.5/bin/biome", + //"PrettierPath": "/nix/store/6all2hvcnqjwbgi8nhdrffbydv7np5vx-prettier-3.6.2/bin/prettier", + //"NodePath": "/nix/store/cwxxy2is6hb4dcqsx8rhnvwic3hfzpcd-nodejs-22.19.0/bin/node" + "BiomePath": "./biome/bin/biome", + "PrettierPath": "./prettier/bin/prettier", + "NodePath": "./node/bin/node" + } + } +} diff --git a/ReferenceClientProxyImplementation/appsettings.json b/ReferenceClientProxyImplementation/appsettings.json new file mode 100644
index 0000000..23039ed --- /dev/null +++ b/ReferenceClientProxyImplementation/appsettings.json
@@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ReferenceClientProxyImplementation/formatCache.sh b/ReferenceClientProxyImplementation/formatCache.sh new file mode 100755
index 0000000..d041b5d --- /dev/null +++ b/ReferenceClientProxyImplementation/formatCache.sh
@@ -0,0 +1,5 @@ +#!/bin/sh +rm -rfv ./cache_formatted/* +cp -rv ./cache/*.js ./cache_formatted/ +cp -rv ./cache/*.css ./cache_formatted/ +npx prettier --write ./cache_formatted/ \ No newline at end of file