Files

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
}
}