using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; namespace R0Installer { public static class ConfigManager { private const string BASE_URL = "https://a-p-i.r0csgo.com"; private const string INSTALLER_CONFIG_API = "/v3/config/installer"; private const string PROJECT_CONFIG_API = "/v3/config/project/{0}"; private const string VERSION_INDEX_API = "/v3/version/index?project={0}"; private const string USER_AGENT = "r0_installer"; private static string installerConfigCache = null; private static string projectConfigCache = null; private static string projectConfigCacheId = null; #region HTTP private static string HttpGet(string url) { using (WebClient client = new WebClient()) { client.Encoding = Encoding.UTF8; client.Headers.Add("User-Agent", USER_AGENT); return client.DownloadString(url); } } private static async Task HttpGetAsync(string url) { using (WebClient client = new WebClient()) { client.Encoding = Encoding.UTF8; client.Headers.Add("User-Agent", USER_AGENT); return await client.DownloadStringTaskAsync(url); } } #endregion #region JSON helpers public static string ExtractJsonValue(string json, string key) { string searchKey = String.Format("\"{0}\":", key); int startIndex = json.IndexOf(searchKey); if (startIndex < 0) return ""; startIndex += searchKey.Length; while (startIndex < json.Length && (json[startIndex] == ' ' || json[startIndex] == '\n' || json[startIndex] == '\r' || json[startIndex] == '\t')) startIndex++; if (startIndex >= json.Length) return ""; if (json[startIndex] == '"') { startIndex++; int endIndex = json.IndexOf('"', startIndex); if (endIndex < 0) return ""; return json.Substring(startIndex, endIndex - startIndex); } else { int endIndex = startIndex; while (endIndex < json.Length && json[endIndex] != ',' && json[endIndex] != '}' && json[endIndex] != ']') endIndex++; return json.Substring(startIndex, endIndex - startIndex).Trim(); } } public static bool ExtractJsonBool(string json, string key) { string value = ExtractJsonValue(json, key); return value.ToLower() == "true"; } public static int ExtractJsonInt(string json, string key, int defaultValue) { string value = ExtractJsonValue(json, key); int result; if (int.TryParse(value, out result)) return result; return defaultValue; } public static string ExtractNestedJsonValue(string json, string parentKey, string childKey) { string searchParentKey = String.Format("\"{0}\":", parentKey); int parentStartIndex = json.IndexOf(searchParentKey); if (parentStartIndex < 0) return ""; parentStartIndex += searchParentKey.Length; while (parentStartIndex < json.Length && (json[parentStartIndex] == ' ' || json[parentStartIndex] == '\n' || json[parentStartIndex] == '\r')) parentStartIndex++; if (parentStartIndex >= json.Length || json[parentStartIndex] != '{') return ""; int braceCount = 1; int parentEndIndex = parentStartIndex + 1; while (parentEndIndex < json.Length && braceCount > 0) { if (json[parentEndIndex] == '{') braceCount++; else if (json[parentEndIndex] == '}') braceCount--; parentEndIndex++; } string parentJson = json.Substring(parentStartIndex, parentEndIndex - parentStartIndex); return ExtractJsonValue(parentJson, childKey); } public static string ExtractJsonObject(string json, string key) { string searchKey = String.Format("\"{0}\":", key); int startIndex = json.IndexOf(searchKey); if (startIndex < 0) return ""; startIndex += searchKey.Length; while (startIndex < json.Length && (json[startIndex] == ' ' || json[startIndex] == '\n' || json[startIndex] == '\r')) startIndex++; if (startIndex >= json.Length) return ""; if (json[startIndex] == '{') { int braceCount = 1; int endIndex = startIndex + 1; while (endIndex < json.Length && braceCount > 0) { if (json[endIndex] == '{') braceCount++; else if (json[endIndex] == '}') braceCount--; endIndex++; } return json.Substring(startIndex, endIndex - startIndex); } return ""; } public static string ExtractJsonArray(string json, string key) { string searchKey = String.Format("\"{0}\":", key); int startIndex = json.IndexOf(searchKey); if (startIndex < 0) return ""; startIndex += searchKey.Length; while (startIndex < json.Length && (json[startIndex] == ' ' || json[startIndex] == '\n' || json[startIndex] == '\r')) startIndex++; if (startIndex >= json.Length || json[startIndex] != '[') return ""; int bracketCount = 1; int endIndex = startIndex + 1; while (endIndex < json.Length && bracketCount > 0) { if (json[endIndex] == '[') bracketCount++; else if (json[endIndex] == ']') bracketCount--; endIndex++; } return json.Substring(startIndex, endIndex - startIndex); } public static List ParseJsonObjectArray(string arrayJson) { List objects = new List(); if (string.IsNullOrEmpty(arrayJson) || arrayJson[0] != '[') return objects; string inner = arrayJson.Substring(1, arrayJson.Length - 2); int pos = 0; while (pos < inner.Length) { int objStart = inner.IndexOf('{', pos); if (objStart < 0) break; int braceCount = 1; int objEnd = objStart + 1; while (objEnd < inner.Length && braceCount > 0) { if (inner[objEnd] == '{') braceCount++; else if (inner[objEnd] == '}') braceCount--; objEnd++; } objects.Add(inner.Substring(objStart, objEnd - objStart)); pos = objEnd; } return objects; } public static List ParseJsonStringArray(string arrayJson) { List strings = new List(); if (string.IsNullOrEmpty(arrayJson) || arrayJson[0] != '[') return strings; string inner = arrayJson.Substring(1, arrayJson.Length - 2); int pos = 0; while (pos < inner.Length) { int quoteStart = inner.IndexOf('"', pos); if (quoteStart < 0) break; int quoteEnd = inner.IndexOf('"', quoteStart + 1); if (quoteEnd < 0) break; strings.Add(inner.Substring(quoteStart + 1, quoteEnd - quoteStart - 1)); pos = quoteEnd + 1; } return strings; } #endregion #region Config loading private static string GetCachePath(string name) { return Path.Combine(Path.GetTempPath(), "r0_installer_" + name + ".json"); } private static void SaveCache(string name, string json) { try { File.WriteAllText(GetCachePath(name), json, Encoding.UTF8); } catch (Exception ex) { Debug.WriteLine("缓存写入失败: " + ex.Message); } } private static string LoadCache(string name) { try { string path = GetCachePath(name); if (File.Exists(path)) return File.ReadAllText(path, Encoding.UTF8); } catch (Exception ex) { Debug.WriteLine("缓存读取失败: " + ex.Message); } return null; } // 下载的安装包/运行库统一放到固定的厂商目录下,而不是 %TEMP%。 // 从 %TEMP% 写入并立即执行 EXE 是杀软启发式判定下载器木马的主要特征, // 改用 ProgramData 下的固定目录可显著降低误报,且符合正规安装器的行为。 public static string GetWorkDirectory() { string baseDir = null; try { baseDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); } catch { } if (string.IsNullOrEmpty(baseDir)) baseDir = Path.GetTempPath(); string dir = Path.Combine(baseDir, "R0Arena", "cache"); try { Directory.CreateDirectory(dir); return dir; } catch { return Path.GetTempPath(); } } // 目标 exe 已被占用无法覆盖时,改用同目录下的新 exe 文件名(必须保持 .exe 后缀以便运行)。 public static string ResolveWritableDownloadPath(string preferredPath) { if (string.IsNullOrEmpty(preferredPath)) preferredPath = Path.Combine(GetWorkDirectory(), "download_" + Guid.NewGuid().ToString("N") + ".exe"); if (!File.Exists(preferredPath)) return preferredPath; try { File.Delete(preferredPath); if (!File.Exists(preferredPath)) return preferredPath; } catch { } string dir = Path.GetDirectoryName(preferredPath); if (string.IsNullOrEmpty(dir)) dir = GetWorkDirectory(); string nameWithoutExt = Path.GetFileNameWithoutExtension(preferredPath); string ext = Path.GetExtension(preferredPath); if (string.IsNullOrEmpty(ext)) ext = ".exe"; return Path.Combine(dir, nameWithoutExt + "_" + Guid.NewGuid().ToString("N").Substring(0, 8) + ext); } public static void PrepareDownloadFile(string path) { if (!File.Exists(path)) return; try { File.Delete(path); } catch { } } // 执行前确认下载结果确实是有效文件:避免把服务器返回的错误页 / 半截下载 // 当作程序去运行(既是健壮性,也是“正规更新器”才有的行为)。 public static bool IsDownloadedFileValid(string path) { try { FileInfo fi = new FileInfo(path); if (!fi.Exists || fi.Length < 1) return false; if (path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) { using (FileStream fs = File.OpenRead(path)) { return fs.ReadByte() == 0x4D && fs.ReadByte() == 0x5A; // 'MZ' } } return true; } catch { return false; } } public static string GetInstallerConfig() { if (installerConfigCache != null) return installerConfigCache; try { string json = HttpGet(BASE_URL + INSTALLER_CONFIG_API); string data = ExtractJsonObject(json, "data"); if (!string.IsNullOrEmpty(data)) { installerConfigCache = data; SaveCache("installer_config", data); return data; } } catch (Exception ex) { Debug.WriteLine("获取安装器配置失败: " + ex.Message); } string cached = LoadCache("installer_config"); if (cached != null) { installerConfigCache = cached; return cached; } return null; } public static async Task GetProjectConfig(string projectId) { if (projectConfigCache != null && projectConfigCacheId == projectId) return projectConfigCache; try { string url = BASE_URL + String.Format(PROJECT_CONFIG_API, Uri.EscapeDataString(projectId)); string json = await HttpGetAsync(url); string data = ExtractJsonObject(json, "data"); if (!string.IsNullOrEmpty(data)) { projectConfigCache = data; projectConfigCacheId = projectId; SaveCache("project_" + projectId, data); return data; } } catch (Exception ex) { Debug.WriteLine("获取项目配置失败: " + ex.Message); } string cached = LoadCache("project_" + projectId); if (cached != null) { projectConfigCache = cached; projectConfigCacheId = projectId; return cached; } return null; } public static string GetProjectConfigSync(string projectId) { if (projectConfigCache != null && projectConfigCacheId == projectId) return projectConfigCache; try { string url = BASE_URL + String.Format(PROJECT_CONFIG_API, Uri.EscapeDataString(projectId)); string json = HttpGet(url); string data = ExtractJsonObject(json, "data"); if (!string.IsNullOrEmpty(data)) { projectConfigCache = data; projectConfigCacheId = projectId; SaveCache("project_" + projectId, data); return data; } } catch (Exception ex) { Debug.WriteLine("获取项目配置失败: " + ex.Message); } string cached = LoadCache("project_" + projectId); if (cached != null) { projectConfigCache = cached; projectConfigCacheId = projectId; return cached; } return null; } public static async Task GetVersionIndex(string projectId) { try { string url = BASE_URL + String.Format(VERSION_INDEX_API, Uri.EscapeDataString(projectId)); string json = await HttpGetAsync(url); string data = ExtractJsonObject(json, "data"); return data; } catch (Exception ex) { Debug.WriteLine("获取版本信息失败: " + ex.Message); return null; } } public struct UpdateSummary { public bool HasUpdate; public bool HasSilentField; // 服务端是否在本次响应中显式返回了 silent 字段 public bool Silent; // 该版本是否应静默更新(仅当 HasSilentField 为 true 时有效) } // 轻量预检:在决定“弹界面 / 静默”之前,先同步拿到 has_update 以及(可选的)按版本 silent 标志。 // silent 由服务端在 /v3/update/check 响应中按目标版本下发,从而实现“云端指定哪个版本静默更新”。 public static UpdateSummary GetUpdateSummarySync(string projectId, string checkApiTemplate, string currentVersion) { UpdateSummary summary = new UpdateSummary(); try { string apiUrl = BASE_URL + String.Format(checkApiTemplate, Uri.EscapeDataString(currentVersion)); string json = HttpGet(apiUrl); string data = ExtractJsonObject(json, "data"); string body = string.IsNullOrEmpty(data) ? json : data; summary.HasUpdate = ExtractJsonBool(body, "has_update"); string silentRaw = ExtractJsonValue(body, "silent"); summary.HasSilentField = !string.IsNullOrEmpty(silentRaw); summary.Silent = silentRaw.Trim().ToLower() == "true"; } catch (Exception) { // 网络异常时保守认为有更新(与旧逻辑一致);是否静默回退到本地命令/更新目标配置。 summary.HasUpdate = true; summary.HasSilentField = false; } return summary; } #endregion #region Installer config matching public struct MatchResult { public string ProjectId; public string InstallMode; public string UpdateTarget; public bool Silent; } public static MatchResult MatchFilename(string installerConfig, string exeName) { MatchResult result = new MatchResult(); result.ProjectId = ExtractJsonValue(installerConfig, "default_project_id"); result.InstallMode = ExtractJsonValue(installerConfig, "default_install_mode"); if (string.IsNullOrEmpty(result.InstallMode)) result.InstallMode = "full"; string rulesArray = ExtractJsonArray(installerConfig, "filename_rules"); if (string.IsNullOrEmpty(rulesArray)) return result; List rules = ParseJsonObjectArray(rulesArray); foreach (string rule in rules) { string pattern = ExtractJsonValue(rule, "pattern"); string matchType = ExtractJsonValue(rule, "match_type"); bool caseSensitive = ExtractJsonBool(rule, "case_sensitive"); if (string.IsNullOrEmpty(pattern)) continue; bool matched = false; string nameToCheck = caseSensitive ? exeName : exeName.ToLower(); string patternToCheck = caseSensitive ? pattern : pattern.ToLower(); switch (matchType) { case "starts_with": matched = nameToCheck.StartsWith(patternToCheck); break; case "contains": matched = nameToCheck.Contains(patternToCheck); break; case "exact": matched = nameToCheck == patternToCheck; break; default: matched = nameToCheck.StartsWith(patternToCheck); break; } if (matched) { result.ProjectId = ExtractJsonValue(rule, "project_id"); result.InstallMode = ExtractJsonValue(rule, "install_mode"); return result; } } return result; } public static MatchResult MatchCommand(string installerConfig, string command) { MatchResult result = new MatchResult(); string rulesArray = ExtractJsonArray(installerConfig, "command_rules"); if (string.IsNullOrEmpty(rulesArray)) return result; List rules = ParseJsonObjectArray(rulesArray); foreach (string rule in rules) { string cmd = ExtractJsonValue(rule, "command"); if (cmd.ToLower() == command.ToLower()) { result.ProjectId = ExtractJsonValue(rule, "project_id"); result.UpdateTarget = ExtractJsonValue(rule, "update_target"); result.Silent = ExtractJsonBool(rule, "silent"); return result; } } return result; } public static string GetUpdateConfig(string projectConfig, string updateTarget) { return ExtractJsonObject(projectConfig, updateTarget); } #endregion } }