6124d6060c
Co-authored-by: Cursor <cursoragent@cursor.com>
578 lines
21 KiB
C#
578 lines
21 KiB
C#
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<string> 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<string> ParseJsonObjectArray(string arrayJson)
|
|
{
|
|
List<string> objects = new List<string>();
|
|
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<string> ParseJsonStringArray(string arrayJson)
|
|
{
|
|
List<string> strings = new List<string>();
|
|
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<string> 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<string> 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<string> 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<string> 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
|
|
}
|
|
}
|