diff --git a/EpinelPS/Controllers/AdminApiController.cs b/EpinelPS/Controllers/AdminApiController.cs index 537e3c0..e104c78 100644 --- a/EpinelPS/Controllers/AdminApiController.cs +++ b/EpinelPS/Controllers/AdminApiController.cs @@ -63,7 +63,7 @@ namespace EpinelPS.Controllers } [HttpPost("RunCmd")] - public RunCmdResponse RunCmd([FromBody] RunCmdRequest req) + public async Task RunCmd([FromBody] RunCmdRequest req) { if (!AdminController.CheckAuth(HttpContext)) return new RunCmdResponse() { error = "bad token" }; @@ -124,6 +124,10 @@ namespace EpinelPS.Controllers var s = req.p2.Split("-"); return AdminCommands.AddItem(user, int.Parse(s[0]), int.Parse(s[1])); } + case "updateServer": + { + return await AdminCommands.UpdateResources(); + } } return new RunCmdResponse() { error = "Not implemented" }; } diff --git a/EpinelPS/EpinelPS.csproj b/EpinelPS/EpinelPS.csproj index d587404..292c1ea 100644 --- a/EpinelPS/EpinelPS.csproj +++ b/EpinelPS/EpinelPS.csproj @@ -22,10 +22,10 @@ - + - - + + diff --git a/EpinelPS/LobbyServer/Event/EnterEventField.cs b/EpinelPS/LobbyServer/Event/EnterEventField.cs index 324a66f..0f0f214 100644 --- a/EpinelPS/LobbyServer/Event/EnterEventField.cs +++ b/EpinelPS/LobbyServer/Event/EnterEventField.cs @@ -13,7 +13,8 @@ namespace EpinelPS.LobbyServer.Event ResEnterEventField response = new() { - Field = new() + Field = new(), + Json = "{}" }; // Retrieve collected objects diff --git a/EpinelPS/Program.cs b/EpinelPS/Program.cs index e0a6763..0f833a9 100644 --- a/EpinelPS/Program.cs +++ b/EpinelPS/Program.cs @@ -21,7 +21,7 @@ namespace EpinelPS { Console.WriteLine($"EpinelPS v{Assembly.GetExecutingAssembly().GetName().Version} - https://github.com/EpinelPS/EpinelPS/"); Console.WriteLine("This software is licensed under the AGPL-3.0 License"); - Console.WriteLine("Targeting game version " + GameConfig.Root.GameMaxVer); + Console.WriteLine("Targeting game version " + GameConfig.Root.TargetVersion); GameData.Instance.GetAllCostumes(); // force static data to be loaded @@ -101,6 +101,7 @@ namespace EpinelPS app.MapGet("/prdenv/{**all}", AssetDownloadUtil.HandleReq); app.MapGet("/PC/{**all}", AssetDownloadUtil.HandleReq); app.MapGet("/media/{**all}", AssetDownloadUtil.HandleReq); + app.MapPost("/rqd/sync", HandleRqd); // NOTE: pub prefixes shows public (production server), local is local server (does not have any effect), dev is development server, etc. // It does not have any effect, except for the publisher server, which adds a watermark? @@ -189,6 +190,11 @@ namespace EpinelPS } } + private static async Task HandleRqd(HttpContext context) + { + + } + private static void CliLoop() { ulong selectedUser = 0; diff --git a/EpinelPS/Utils/AdminCommands.cs b/EpinelPS/Utils/AdminCommands.cs index 3b4562b..4c83bb4 100644 --- a/EpinelPS/Utils/AdminCommands.cs +++ b/EpinelPS/Utils/AdminCommands.cs @@ -1,15 +1,59 @@ +using System.Net; +using System.Net.Security; +using System.Net.Sockets; using DnsClient; using EpinelPS.Data; using EpinelPS.Database; using EpinelPS.LobbyServer.Stage; using EpinelPS.Models.Admin; -using System.Net; +using Google.Protobuf; namespace EpinelPS.Utils { public class AdminCommands { + private static HttpClient client; + private static string serverUrl = "global-lobby.nikke-kr.com"; + private static string connectingServer = serverUrl; + private static string? serverIp; + private static string? staticDataUrl; + private static string? resourcesUrl; + static AdminCommands() + { + // Use TLS 1.1 so that tencents cloudflare knockoff wont complain + var handler = new SocketsHttpHandler + { + ConnectCallback = async (context, cancellationToken) => + { + var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + try + { + await socket.ConnectAsync(context.DnsEndPoint, cancellationToken); + + var sslStream = new SslStream(new NetworkStream(socket, ownsSocket: true)); + + // When using HTTP/2, you must also keep in mind to set options like ApplicationProtocols + await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + { + TargetHost = connectingServer, + EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls11 + + }, cancellationToken); + + return sslStream; + } + catch + { + socket.Dispose(); + throw; + } + } + }; + + client = new(handler); + client.DefaultRequestHeaders.Add("Accept", "application/octet-stream+protobuf"); + } public static RunCmdResponse CompleteStage(ulong userId, string input2) { var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == userId); @@ -327,5 +371,72 @@ namespace EpinelPS.Utils JsonDb.Save(); return RunCmdResponse.OK; } + + internal static async Task UpdateResources() + { + Logging.WriteLine("updating static data and resource info...", LogType.Info); + if (serverIp == null || staticDataUrl == null || resourcesUrl == null) + { + serverIp = await AssetDownloadUtil.GetIpAsync(serverUrl); + staticDataUrl = $"https://{serverIp}/v1/staticdatapack"; + resourcesUrl = $"https://{serverIp}/v1/resourcehosts2"; + } + + if (serverIp == null) + return new RunCmdResponse() { error = "failed to get real server ip, check internet connection" }; + + // Get latest static data info from server + ResStaticDataPackInfo? staticData = await FetchProtobuf(staticDataUrl); + if (staticData == null) + { + Logging.WriteLine("failed to fetch static data", LogType.Error); + return new RunCmdResponse() { error = "failed to fetch static data"}; + } + + ResGetResourceHosts2? resources = await FetchProtobuf(resourcesUrl); + if (resources == null) + { + Logging.WriteLine("failed to fetch resource data", LogType.Error); + return new RunCmdResponse() { error = "failed to fetch resource data" }; + } + + GameConfig.Root.ResourceBaseURL = resources.BaseUrl; + GameConfig.Root.StaticData.Salt1 = staticData.Salt1.ToBase64(); + GameConfig.Root.StaticData.Salt2 = staticData.Salt2.ToBase64(); + GameConfig.Root.StaticData.Version = staticData.Version; + GameConfig.Root.StaticData.Url = staticData.Url; + GameConfig.Save(); + + return RunCmdResponse.OK; + } + + private static async Task FetchProtobuf(string url) where T : IMessage, new() + { + ByteArrayContent staticDataContent = new([]); + client.DefaultRequestHeaders.Host = serverUrl; + staticDataContent.Headers.Add("Content-Type", "application/octet-stream+protobuf"); + connectingServer = serverUrl; + HttpResponseMessage? staticDataHttpResponse = await client.PostAsync(url, staticDataContent); + if (staticDataHttpResponse == null) + { + Console.WriteLine($"failed to post {url}"); + return default(T); + } + + if (!staticDataHttpResponse.IsSuccessStatusCode) + { + Console.WriteLine($"POST {url} failed with {staticDataHttpResponse.StatusCode}"); + return default(T); + } + + byte[] staticDataHttpResponseBytes = await staticDataHttpResponse.Content.ReadAsByteArrayAsync(); + + // Parse response + T response = new(); + response.MergeFrom(new CodedInputStream(staticDataHttpResponseBytes)); + + + return response; + } } } diff --git a/EpinelPS/Utils/AssetDownloadUtil.cs b/EpinelPS/Utils/AssetDownloadUtil.cs index 2ff2e7c..d03dec8 100644 --- a/EpinelPS/Utils/AssetDownloadUtil.cs +++ b/EpinelPS/Utils/AssetDownloadUtil.cs @@ -26,7 +26,7 @@ namespace EpinelPS.Utils if (CloudIp == null) { - CloudIp = await GetCloudIpAsync(); + CloudIp = await GetIpAsync("cloud.nikke-kr.com"); } var requestUri = new Uri("https://" + CloudIp + "/" + rawUrl); @@ -69,13 +69,13 @@ namespace EpinelPS.Utils context.Response.StatusCode = 404; } - private static async Task GetCloudIpAsync() + public static async Task GetIpAsync(string query) { var lookup = new LookupClient(); - var result = await lookup.QueryAsync("cloud.nikke-kr.com", QueryType.A); + var result = await lookup.QueryAsync(query, QueryType.A); var record = result.Answers.ARecords().FirstOrDefault(); - var ip = record?.Address ?? throw new Exception("Failed to find IP address of cloud.nikke-kr.com, check your internet connection."); + var ip = record?.Address ?? throw new Exception($"Failed to find IP address of {query}, check your internet connection."); return ip.ToString(); } diff --git a/EpinelPS/Utils/GameConfig.cs b/EpinelPS/Utils/GameConfig.cs index aa9f5e8..da8d49e 100644 --- a/EpinelPS/Utils/GameConfig.cs +++ b/EpinelPS/Utils/GameConfig.cs @@ -60,5 +60,13 @@ namespace EpinelPS.Utils return _root; } } + + internal static void Save() + { + if (Root != null) + { + File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json", JsonConvert.SerializeObject(Root, Formatting.Indented)); + } + } } } diff --git a/EpinelPS/Views/Admin/Configuration.cshtml b/EpinelPS/Views/Admin/Configuration.cshtml index 1fd5180..3212f57 100644 --- a/EpinelPS/Views/Admin/Configuration.cshtml +++ b/EpinelPS/Views/Admin/Configuration.cshtml @@ -15,6 +15,9 @@ + + +
diff --git a/EpinelPS/gameconfig.json b/EpinelPS/gameconfig.json index 20fb5f6..0a1e40b 100644 --- a/EpinelPS/gameconfig.json +++ b/EpinelPS/gameconfig.json @@ -1,17 +1,12 @@ { - // Asset Urls for game version 134.8.13 - // Extracted from POST https://global-lobby.nikke-kr.com/v1/staticdatapack -"StaticData": { - "Url": "https://cloud.nikke-kr.com/prdenv/134-cd14ebd38f/staticdata/data/qa-250612-06b/410113/StaticData.pack", - "Version": "data/qa-250612-06b/410113", - "Salt1": "4JgIjnC3wkpnk2B9nqvSd5Ovd0LxiKEd2u5UOKjPPVM=", - "Salt2": "8505DAwa5p0ojKq91jhX5ktAOOL+JoSh1H6XhGrQfts=" - }, - - // Extracted from POST https://global-lobby.nikke-kr.com"/v1/resourcehosts2 - "ResourceBaseURL": "https://cloud.nikke-kr.com/prdenv/134-bac9cf1d39/{Platform}", - - // Allow all versions - "GameMinVer": "100.0.1", - "GameMaxVer": "150.0.2" + "StaticData": { + "Url": "https://cloud.nikke-kr.com/prdenv/134-cd14ebd38f/staticdata/data/qa-250612-06b/410113/StaticData.pack", + "Version": "data/qa-250612-06b/410113", + "Salt1": "4JgIjnC3wkpnk2B9nqvSd5Ovd0LxiKEd2u5UOKjPPVM=", + "Salt2": "8505DAwa5p0ojKq91jhX5ktAOOL+JoSh1H6XhGrQfts=" + }, + "ResourceBaseURL": "https://cloud.nikke-kr.com/prdenv/134-bac9cf1d39/{Platform}", + "GameMinVer": "100.0.1", + "GameMaxVer": "150.0.2", + "TargetVersion": "134.8.13" } \ No newline at end of file diff --git a/EpinelPS/wwwroot/admin/assets/i18n/en.json b/EpinelPS/wwwroot/admin/assets/i18n/en.json index 807318d..80a8986 100644 --- a/EpinelPS/wwwroot/admin/assets/i18n/en.json +++ b/EpinelPS/wwwroot/admin/assets/i18n/en.json @@ -204,7 +204,9 @@ "config": { "server": { "title": "Server Configuration", - "logLevel": "Log Level:" + "logLevel": "Log Level:", + "updateResources": "Update Resources", + "updateResourcesHint": "Downloads asset and static data info from official server." }, "database": { "title": "Database Configuration", diff --git a/EpinelPS/wwwroot/admin/assets/i18n/ja.json b/EpinelPS/wwwroot/admin/assets/i18n/ja.json index 06c0e15..99daac0 100644 --- a/EpinelPS/wwwroot/admin/assets/i18n/ja.json +++ b/EpinelPS/wwwroot/admin/assets/i18n/ja.json @@ -204,7 +204,9 @@ "config": { "server": { "title": "サーバー設定", - "logLevel": "ログレベル:" + "logLevel": "ログレベル:", + "updateResources": "リソースを更新する", + "updateResourcesHint": "公式サーバーからアセットと静的データ情報をダウンロードします。" }, "database": { "title": "データベース設定", diff --git a/EpinelPS/wwwroot/admin/assets/i18n/ko.json b/EpinelPS/wwwroot/admin/assets/i18n/ko.json index 5f3b9a1..4ea5466 100644 --- a/EpinelPS/wwwroot/admin/assets/i18n/ko.json +++ b/EpinelPS/wwwroot/admin/assets/i18n/ko.json @@ -204,7 +204,9 @@ "config": { "server": { "title": "서버 구성", - "logLevel": "로그 레벨:" + "logLevel": "로그 레벨:", + "updateResources": "리소스 업데이트", + "updateResourcesHint": "공식 서버에서 자산 및 정적 데이터 정보를 다운로드합니다." }, "database": { "title": "데이터베이스 구성", diff --git a/EpinelPS/wwwroot/admin/assets/i18n/zh.json b/EpinelPS/wwwroot/admin/assets/i18n/zh.json index 689eb7a..4e6ab55 100644 --- a/EpinelPS/wwwroot/admin/assets/i18n/zh.json +++ b/EpinelPS/wwwroot/admin/assets/i18n/zh.json @@ -204,7 +204,9 @@ "config": { "server": { "title": "服务器配置", - "logLevel": "日志级别:" + "logLevel": "日志级别:", + "updateResources": "Update Resources", + "updateResourcesHint": "Downloads asset and static data info from official server." }, "database": { "title": "数据库配置", diff --git a/EpinelPS/wwwroot/admin/assets/js/site.js b/EpinelPS/wwwroot/admin/assets/js/site.js index bb53d0a..5328e5d 100644 --- a/EpinelPS/wwwroot/admin/assets/js/site.js +++ b/EpinelPS/wwwroot/admin/assets/js/site.js @@ -1,7 +1,7 @@ // Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification // for details on configuring this project to bundle and minify static web assets. -// NIKKE管理控制台API通用工具函数 +// EpinelPS管理控制台API通用工具函数 function runCmd(cmdName, cb, p1, p2) { @@ -28,9 +28,9 @@ function runSimpleCmd(cmdName, p1, p2) { runCmd(cmdName, function(json){ if (json.ok) - alert("操作已完成"); + alert("Operation completed"); else - alert("错误: " + json.error); + alert("Error: " + json.error); }, p1, p2); }