using System.Data; using System.Text.RegularExpressions; namespace ServiceLib.Handler; public class ConfigHandler { private static readonly string _configRes = Global.ConfigFileName; private static readonly string _tag = "ConfigHandler"; #region ConfigHandler /// /// Load the application configuration file /// If the file exists, deserialize it from JSON /// If not found, create a new Config object with default settings /// Initialize default values for missing configuration sections /// /// Config object containing application settings or null if there's an error public static Config? LoadConfig() { Config? config = null; var result = EmbedUtils.LoadResource(Utils.GetConfigPath(_configRes)); if (result.IsNotEmpty()) { config = JsonUtils.Deserialize(result); } else { if (File.Exists(Utils.GetConfigPath(_configRes))) { Logging.SaveLog("LoadConfig Exception"); return null; } } config ??= new Config(); config.CoreBasicItem ??= new() { LogEnabled = false, Loglevel = "warning", MuxEnabled = false, }; if (config.Inbound == null) { config.Inbound = new List(); InItem inItem = new() { Protocol = EInboundProtocol.socks.ToString(), LocalPort = 10808, UdpEnabled = true, SniffingEnabled = true, RouteOnly = false, }; config.Inbound.Add(inItem); } else { if (config.Inbound.Count > 0) { config.Inbound.First().Protocol = EInboundProtocol.socks.ToString(); } } config.RoutingBasicItem ??= new(); if (config.RoutingBasicItem.DomainStrategy.IsNullOrEmpty()) { config.RoutingBasicItem.DomainStrategy = Global.DomainStrategies.First(); } config.KcpItem ??= new KcpItem { Mtu = 1350, Tti = 50, UplinkCapacity = 12, DownlinkCapacity = 100, ReadBufferSize = 2, WriteBufferSize = 2, Congestion = false }; config.GrpcItem ??= new GrpcItem { IdleTimeout = 60, HealthCheckTimeout = 20, PermitWithoutStream = false, InitialWindowsSize = 0, }; config.TunModeItem ??= new TunModeItem { EnableTun = false, Mtu = 9000, }; config.GuiItem ??= new(); config.MsgUIItem ??= new(); config.UiItem ??= new UIItem() { EnableAutoAdjustMainLvColWidth = true }; config.UiItem.MainColumnItem ??= new(); config.UiItem.WindowSizeItem ??= new(); if (config.UiItem.CurrentLanguage.IsNullOrEmpty()) { config.UiItem.CurrentLanguage = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.CurrentCultureIgnoreCase) ? Global.Languages.First() : Global.Languages[2]; } config.ConstItem ??= new ConstItem(); config.SpeedTestItem ??= new(); if (config.SpeedTestItem.SpeedTestTimeout < 10) { config.SpeedTestItem.SpeedTestTimeout = 10; } if (config.SpeedTestItem.SpeedTestUrl.IsNullOrEmpty()) { config.SpeedTestItem.SpeedTestUrl = Global.SpeedTestUrls.First(); } if (config.SpeedTestItem.SpeedPingTestUrl.IsNullOrEmpty()) { config.SpeedTestItem.SpeedPingTestUrl = Global.SpeedPingTestUrls.First(); } if (config.SpeedTestItem.MixedConcurrencyCount < 1) { config.SpeedTestItem.MixedConcurrencyCount = 5; } config.Mux4RayItem ??= new() { Concurrency = 8, XudpConcurrency = 16, XudpProxyUDP443 = "reject" }; config.Mux4SboxItem ??= new() { Protocol = Global.SingboxMuxs.First(), MaxConnections = 8 }; config.HysteriaItem ??= new() { UpMbps = 100, DownMbps = 100 }; config.ClashUIItem ??= new(); config.SystemProxyItem ??= new(); config.WebDavItem ??= new(); config.CheckUpdateItem ??= new(); config.Fragment4RayItem ??= new() { Packets = "tlshello", Length = "100-200", Interval = "10-20" }; config.GlobalHotkeys ??= new(); if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty()) { config.SystemProxyItem.SystemProxyExceptions = Utils.IsWindows() ? Global.SystemProxyExceptionsWindows : Global.SystemProxyExceptionsLinux; } config.SplitCoreItem ??= new() { EnableSplitCore = false, SplitCoreTypes = new List(), RouteCoreType = ECoreType.Xray }; return config; } /// /// Save the configuration to a file /// First writes to a temporary file, then replaces the original file /// /// Configuration object to be saved /// 0 if successful, -1 if failed public static async Task SaveConfig(Config config) { try { //save temp file var resPath = Utils.GetConfigPath(_configRes); var tempPath = $"{resPath}_temp"; var content = JsonUtils.Serialize(config, true, true); if (content.IsNullOrEmpty()) { return -1; } await File.WriteAllTextAsync(tempPath, content); //rename File.Move(tempPath, resPath, true); } catch (Exception ex) { Logging.SaveLog(_tag, ex); return -1; } return 0; } #endregion ConfigHandler #region Server /// /// Add a server profile to the configuration /// Dispatches the request to the appropriate method based on the config type /// /// Current configuration /// Server profile to add /// Result of the operation (0 if successful, -1 if failed) public static async Task AddServer(Config config, ProfileItem profileItem) { var item = await AppHandler.Instance.GetProfileItem(profileItem.IndexId); if (item is null) { item = profileItem; } else { item.CoreType = profileItem.CoreType; item.Remarks = profileItem.Remarks; item.Address = profileItem.Address; item.Port = profileItem.Port; item.Ports = profileItem.Ports; item.Id = profileItem.Id; item.AlterId = profileItem.AlterId; item.Security = profileItem.Security; item.Flow = profileItem.Flow; item.Network = profileItem.Network; item.HeaderType = profileItem.HeaderType; item.RequestHost = profileItem.RequestHost; item.Path = profileItem.Path; item.StreamSecurity = profileItem.StreamSecurity; item.Sni = profileItem.Sni; item.AllowInsecure = profileItem.AllowInsecure; item.Fingerprint = profileItem.Fingerprint; item.Alpn = profileItem.Alpn; item.PublicKey = profileItem.PublicKey; item.ShortId = profileItem.ShortId; item.SpiderX = profileItem.SpiderX; item.Mldsa65Verify = profileItem.Mldsa65Verify; item.Extra = profileItem.Extra; item.MuxEnabled = profileItem.MuxEnabled; } var ret = item.ConfigType switch { EConfigType.VMess => await AddVMessServer(config, item), EConfigType.Shadowsocks => await AddShadowsocksServer(config, item), EConfigType.SOCKS => await AddSocksServer(config, item), EConfigType.HTTP => await AddHttpServer(config, item), EConfigType.Trojan => await AddTrojanServer(config, item), EConfigType.VLESS => await AddVlessServer(config, item), EConfigType.Hysteria2 => await AddHysteria2Server(config, item), EConfigType.TUIC => await AddTuicServer(config, item), EConfigType.WireGuard => await AddWireguardServer(config, item), EConfigType.Anytls => await AddAnytlsServer(config, item), EConfigType.NaiveProxy => await AddNaiveServer(config, item), EConfigType.Juicity => await AddJuicityServer(config, item), EConfigType.Brook => await AddBrookServer(config, item), EConfigType.Shadowquic => await AddShadowquicServer(config, item), _ => -1, }; return ret; } /// /// Add or edit a VMess server /// Validates and processes VMess-specific settings /// /// Current configuration /// VMess profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddVMessServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.VMess; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Security = profileItem.Security.TrimEx(); profileItem.Network = profileItem.Network.TrimEx(); profileItem.HeaderType = profileItem.HeaderType.TrimEx(); profileItem.RequestHost = profileItem.RequestHost.TrimEx(); profileItem.Path = profileItem.Path.TrimEx(); profileItem.StreamSecurity = profileItem.StreamSecurity.TrimEx(); if (!Global.VmessSecurities.Contains(profileItem.Security)) { return -1; } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Remove multiple servers from the configuration /// /// Current configuration /// List of server profiles to remove /// 0 if successful public static async Task RemoveServers(Config config, List indexes) { var subid = "TempRemoveSubId"; foreach (var item in indexes) { item.Subid = subid; } await SQLiteHelper.Instance.UpdateAllAsync(indexes); await RemoveServersViaSubid(config, subid, false); return 0; } /// /// Clone server profiles /// Creates copies of the specified server profiles with "-clone" appended to the remarks /// /// Current configuration /// List of server profiles to clone /// 0 if successful public static async Task CopyServer(Config config, List indexes) { foreach (var it in indexes) { var item = await AppHandler.Instance.GetProfileItem(it.IndexId); if (item is null) { continue; } var profileItem = JsonUtils.DeepCopy(item); profileItem.IndexId = string.Empty; profileItem.Remarks = $"{item.Remarks}-clone"; if (profileItem.ConfigType == EConfigType.Custom) { profileItem.Address = Utils.GetConfigPath(profileItem.Address); if (await AddCustomServer(config, profileItem, false) == 0) { } } else { await AddServerCommon(config, profileItem, true); } } return 0; } /// /// Set the default server by its index ID /// Updates the configuration to use the specified server as default /// /// Current configuration /// Index ID of the server to set as default /// 0 if successful, -1 if failed public static async Task SetDefaultServerIndex(Config config, string? indexId) { if (indexId.IsNullOrEmpty()) { return -1; } config.IndexId = indexId; await SaveConfig(config); return 0; } /// /// Set a default server from the provided list of profiles /// Ensures there's always a valid default server selected /// /// Current configuration /// List of profile models to choose from /// Result of SetDefaultServerIndex operation public static async Task SetDefaultServer(Config config, List lstProfile) { if (lstProfile.Exists(t => t.IndexId == config.IndexId)) { return 0; } if (await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(t => t.IndexId == config.IndexId) != null) { return 0; } if (lstProfile.Count > 0) { return await SetDefaultServerIndex(config, lstProfile.FirstOrDefault(t => t.Port > 0)?.IndexId); } var item = await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(t => t.Port > 0); return await SetDefaultServerIndex(config, item?.IndexId); } /// /// Get the current default server profile /// If the current default is invalid, selects a new default /// /// Current configuration /// The default profile item or null if none exists public static async Task GetDefaultServer(Config config) { var item = await AppHandler.Instance.GetProfileItem(config.IndexId); if (item is null) { var item2 = await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(); await SetDefaultServerIndex(config, item2?.IndexId); return item2; } return item; } /// /// Move a server in the list to a different position /// Supports moving to top, up, down, bottom or specific position /// /// Current configuration /// List of server profiles /// Index of the server to move /// Direction to move the server /// Target position when using EMove.Position /// 0 if successful, -1 if failed public static async Task MoveServer(Config config, List lstProfile, int index, EMove eMove, int pos = -1) { int count = lstProfile.Count; if (index < 0 || index > lstProfile.Count - 1) { return -1; } for (int i = 0; i < lstProfile.Count; i++) { ProfileExHandler.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10); } var sort = 0; switch (eMove) { case EMove.Top: { if (index == 0) { return 0; } sort = ProfileExHandler.Instance.GetSort(lstProfile.First().IndexId) - 1; break; } case EMove.Up: { if (index == 0) { return 0; } sort = ProfileExHandler.Instance.GetSort(lstProfile[index - 1].IndexId) - 1; break; } case EMove.Down: { if (index == count - 1) { return 0; } sort = ProfileExHandler.Instance.GetSort(lstProfile[index + 1].IndexId) + 1; break; } case EMove.Bottom: { if (index == count - 1) { return 0; } sort = ProfileExHandler.Instance.GetSort(lstProfile[^1].IndexId) + 1; break; } case EMove.Position: sort = (pos * 10) + 1; break; } ProfileExHandler.Instance.SetSort(lstProfile[index].IndexId, sort); return await Task.FromResult(0); } /// /// Add a custom server configuration from a file /// Copies the configuration file to the app's config directory /// /// Current configuration /// Profile item with the file path in Address /// Whether to delete the source file after copying /// 0 if successful, -1 if failed public static async Task AddCustomServer(Config config, ProfileItem profileItem, bool blDelete) { var fileName = profileItem.Address; if (!File.Exists(fileName)) { return -1; } var ext = Path.GetExtension(fileName); string newFileName = $"{Utils.GetGuid()}{ext}"; //newFileName = Path.Combine(Utile.GetTempPath(), newFileName); try { File.Copy(fileName, Utils.GetConfigPath(newFileName)); if (blDelete) { File.Delete(fileName); } } catch (Exception ex) { Logging.SaveLog(_tag, ex); return -1; } profileItem.Address = newFileName; profileItem.ConfigType = EConfigType.Custom; if (profileItem.Remarks.IsNullOrEmpty()) { profileItem.Remarks = $"import custom@{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}"; } await AddServerCommon(config, profileItem, true); return 0; } /// /// Edit an existing custom server configuration /// Updates the server's properties without changing the file /// /// Current configuration /// Profile item with updated properties /// 0 if successful, -1 if failed public static async Task EditCustomServer(Config config, ProfileItem profileItem) { var item = await AppHandler.Instance.GetProfileItem(profileItem.IndexId); if (item is null) { item = profileItem; } else { item.Remarks = profileItem.Remarks; item.Address = profileItem.Address; item.CoreType = profileItem.CoreType; item.DisplayLog = profileItem.DisplayLog; item.PreSocksPort = profileItem.PreSocksPort; } if (await SQLiteHelper.Instance.UpdateAsync(item) > 0) { return 0; } else { return -1; } //ToJsonFile(config); } /// /// Add or edit a Shadowsocks server /// Validates and processes Shadowsocks-specific settings /// /// Current configuration /// Shadowsocks profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddShadowsocksServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.Shadowsocks; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Security = profileItem.Security.TrimEx(); if (!AppHandler.Instance.GetShadowsocksSecurities(profileItem).Contains(profileItem.Security)) { return -1; } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a SOCKS server /// Processes SOCKS-specific settings /// /// Current configuration /// SOCKS profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddSocksServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.SOCKS; profileItem.Address = profileItem.Address.TrimEx(); await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit an HTTP server /// Processes HTTP-specific settings /// /// Current configuration /// HTTP profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddHttpServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.HTTP; profileItem.Address = profileItem.Address.TrimEx(); await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a Trojan server /// Validates and processes Trojan-specific settings /// /// Current configuration /// Trojan profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddTrojanServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.Trojan; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a Hysteria2 server /// Validates and processes Hysteria2-specific settings /// Sets the core type to sing_box as required by Hysteria2 /// /// Current configuration /// Hysteria2 profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddHysteria2Server(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.Hysteria2; //profileItem.CoreType = ECoreType.sing_box; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Path = profileItem.Path.TrimEx(); profileItem.Network = string.Empty; if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a TUIC server /// Validates and processes TUIC-specific settings /// Sets the core type to sing_box as required by TUIC /// /// Current configuration /// TUIC profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddTuicServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.TUIC; //profileItem.CoreType = ECoreType.sing_box; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Security = profileItem.Security.TrimEx(); profileItem.Network = string.Empty; if (!Global.CongestionControls.Contains(profileItem.HeaderType)) { profileItem.HeaderType = Global.CongestionControls.FirstOrDefault()!; } if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } if (profileItem.Alpn.IsNullOrEmpty()) { profileItem.Alpn = "h3"; } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a WireGuard server /// Validates and processes WireGuard-specific settings /// /// Current configuration /// WireGuard profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddWireguardServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.WireGuard; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.PublicKey = profileItem.PublicKey.TrimEx(); profileItem.Path = profileItem.Path.TrimEx(); profileItem.RequestHost = profileItem.RequestHost.TrimEx(); profileItem.Network = string.Empty; if (profileItem.ShortId.IsNullOrEmpty()) { profileItem.ShortId = Global.TunMtus.First().ToString(); } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a Anytls server /// Validates and processes Anytls-specific settings /// /// Current configuration /// Anytls profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddAnytlsServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.Anytls; profileItem.CoreType = ECoreType.sing_box; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Security = profileItem.Security.TrimEx(); profileItem.Network = string.Empty; if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a Naive server /// Validates and processes Naive-specific settings /// /// Current configuration /// Naive profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddNaiveServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.NaiveProxy; profileItem.CoreType = ECoreType.naiveproxy; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Network = string.Empty; if (!Global.NaiveProxyProtocols.Contains(profileItem.HeaderType)) { profileItem.HeaderType = Global.NaiveProxyProtocols.FirstOrDefault()!; } if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a Juicity server /// Validates and processes Juicity-specific settings /// /// Current configuration /// Juicity profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddJuicityServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.Juicity; profileItem.CoreType = ECoreType.juicity; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Security = profileItem.Security.TrimEx(); profileItem.Network = string.Empty; if (!Global.CongestionControls.Contains(profileItem.HeaderType)) { profileItem.HeaderType = Global.CongestionControls.FirstOrDefault()!; } if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } if (profileItem.Alpn.IsNullOrEmpty()) { profileItem.Alpn = "h3"; } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a Brook server /// Validates and processes Brook-specific settings /// /// Current configuration /// Brook profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddBrookServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.Brook; profileItem.CoreType = ECoreType.brook; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Network = string.Empty; if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Add or edit a Shadowquic server /// Validates and processes Shadowquic-specific settings /// /// Current configuration /// Shadowquic profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddShadowquicServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.Shadowquic; profileItem.CoreType = ECoreType.shadowquic; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Security = profileItem.Security.TrimEx(); profileItem.Network = string.Empty; if (!Global.CongestionControls.Contains(profileItem.HeaderType)) { profileItem.HeaderType = Global.CongestionControls.FirstOrDefault()!; } if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } if (profileItem.Alpn.IsNullOrEmpty()) { profileItem.Alpn = "h3"; } if (profileItem.Id.IsNullOrEmpty()) { return -1; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Sort the server list by the specified column /// Updates the sort order in the profile extension data /// /// Current configuration /// Subscription ID to filter servers /// Column name to sort by /// Sort in ascending order if true, descending if false /// 0 if successful, -1 if failed public static async Task SortServers(Config config, string subId, string colName, bool asc) { var lstModel = await AppHandler.Instance.ProfileItems(subId, ""); if (lstModel.Count <= 0) { return -1; } var lstServerStat = (config.GuiItem.EnableStatistics ? StatisticsHandler.Instance.ServerStat : null) ?? []; var lstProfileExs = await ProfileExHandler.Instance.GetProfileExs(); var lstProfile = (from t in lstModel join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b from t22 in t2b.DefaultIfEmpty() join t3 in lstProfileExs on t.IndexId equals t3.IndexId into t3b from t33 in t3b.DefaultIfEmpty() select new ProfileItemModel { IndexId = t.IndexId, ConfigType = t.ConfigType, Remarks = t.Remarks, Address = t.Address, Port = t.Port, Security = t.Security, Network = t.Network, StreamSecurity = t.StreamSecurity, Delay = t33?.Delay ?? 0, Speed = t33?.Speed ?? 0, Sort = t33?.Sort ?? 0, TodayDown = (t22?.TodayDown ?? 0).ToString("D16"), TodayUp = (t22?.TodayUp ?? 0).ToString("D16"), TotalDown = (t22?.TotalDown ?? 0).ToString("D16"), TotalUp = (t22?.TotalUp ?? 0).ToString("D16"), }).ToList(); Enum.TryParse(colName, true, out EServerColName name); if (asc) { lstProfile = name switch { EServerColName.ConfigType => lstProfile.OrderBy(t => t.ConfigType).ToList(), EServerColName.Remarks => lstProfile.OrderBy(t => t.Remarks).ToList(), EServerColName.Address => lstProfile.OrderBy(t => t.Address).ToList(), EServerColName.Port => lstProfile.OrderBy(t => t.Port).ToList(), EServerColName.Network => lstProfile.OrderBy(t => t.Network).ToList(), EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(), EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(), EServerColName.SpeedVal => lstProfile.OrderBy(t => t.Speed).ToList(), EServerColName.SubRemarks => lstProfile.OrderBy(t => t.Subid).ToList(), EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(), EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(), EServerColName.TotalDown => lstProfile.OrderBy(t => t.TotalDown).ToList(), EServerColName.TotalUp => lstProfile.OrderBy(t => t.TotalUp).ToList(), _ => lstProfile }; } else { lstProfile = name switch { EServerColName.ConfigType => lstProfile.OrderByDescending(t => t.ConfigType).ToList(), EServerColName.Remarks => lstProfile.OrderByDescending(t => t.Remarks).ToList(), EServerColName.Address => lstProfile.OrderByDescending(t => t.Address).ToList(), EServerColName.Port => lstProfile.OrderByDescending(t => t.Port).ToList(), EServerColName.Network => lstProfile.OrderByDescending(t => t.Network).ToList(), EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(), EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(), EServerColName.SpeedVal => lstProfile.OrderByDescending(t => t.Speed).ToList(), EServerColName.SubRemarks => lstProfile.OrderByDescending(t => t.Subid).ToList(), EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(), EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(), EServerColName.TotalDown => lstProfile.OrderByDescending(t => t.TotalDown).ToList(), EServerColName.TotalUp => lstProfile.OrderByDescending(t => t.TotalUp).ToList(), _ => lstProfile }; } for (var i = 0; i < lstProfile.Count; i++) { ProfileExHandler.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10); } switch (name) { case EServerColName.DelayVal: { var maxSort = lstProfile.Max(t => t.Sort) + 10; foreach (var item in lstProfile.Where(item => item.Delay <= 0)) { ProfileExHandler.Instance.SetSort(item.IndexId, maxSort); } break; } case EServerColName.SpeedVal: { var maxSort = lstProfile.Max(t => t.Sort) + 10; foreach (var item in lstProfile.Where(item => item.Speed <= 0)) { ProfileExHandler.Instance.SetSort(item.IndexId, maxSort); } break; } } return 0; } /// /// Add or edit a VLESS server /// Validates and processes VLESS-specific settings /// /// Current configuration /// VLESS profile to add /// Whether to save to file /// 0 if successful, -1 if failed public static async Task AddVlessServer(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.VLESS; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Id = profileItem.Id.TrimEx(); profileItem.Security = profileItem.Security.TrimEx(); profileItem.Network = profileItem.Network.TrimEx(); profileItem.HeaderType = profileItem.HeaderType.TrimEx(); profileItem.RequestHost = profileItem.RequestHost.TrimEx(); profileItem.Path = profileItem.Path.TrimEx(); profileItem.StreamSecurity = profileItem.StreamSecurity.TrimEx(); if (!Global.Flows.Contains(profileItem.Flow)) { profileItem.Flow = Global.Flows.First(); } if (profileItem.Id.IsNullOrEmpty()) { return -1; } if (profileItem.Security.IsNotEmpty() && profileItem.Security != Global.None) { profileItem.Security = Global.None; } await AddServerCommon(config, profileItem, toFile); return 0; } /// /// Remove duplicate servers from a subscription /// Compares servers based on their properties rather than just names /// /// Current configuration /// Subscription ID to deduplicate /// Tuple with total count and remaining count after deduplication public static async Task> DedupServerList(Config config, string subId) { var lstProfile = await AppHandler.Instance.ProfileItems(subId); if (lstProfile == null) { return new Tuple(0, 0); } List lstKeep = new(); List lstRemove = new(); if (!config.GuiItem.KeepOlderDedupl) { lstProfile.Reverse(); } foreach (var item in lstProfile) { if (!lstKeep.Exists(i => CompareProfileItem(i, item, false))) { lstKeep.Add(item); } else { lstRemove.Add(item); } } await RemoveServers(config, lstRemove); return new Tuple(lstProfile.Count, lstKeep.Count); } /// /// Common server addition logic used by all server types /// Sets common properties and handles sorting and persistence /// /// Current configuration /// Profile item to add /// Whether to save to database /// 0 if successful public static async Task AddServerCommon(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigVersion = 2; if (profileItem.StreamSecurity.IsNotEmpty()) { if (profileItem.StreamSecurity != Global.StreamSecurity && profileItem.StreamSecurity != Global.StreamSecurityReality) { profileItem.StreamSecurity = string.Empty; } else { if (profileItem.AllowInsecure.IsNullOrEmpty()) { profileItem.AllowInsecure = config.CoreBasicItem.DefAllowInsecure.ToString().ToLower(); } if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality) { profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint; } } } if (profileItem.Network.IsNotEmpty() && !Global.Networks.Contains(profileItem.Network)) { profileItem.Network = Global.DefaultNetwork; } var maxSort = -1; if (profileItem.IndexId.IsNullOrEmpty()) { profileItem.IndexId = Utils.GetGuid(false); maxSort = ProfileExHandler.Instance.GetMaxSort(); } if (!toFile && maxSort < 0) { maxSort = ProfileExHandler.Instance.GetMaxSort(); } if (maxSort > 0) { ProfileExHandler.Instance.SetSort(profileItem.IndexId, maxSort + 1); } if (toFile) { await SQLiteHelper.Instance.ReplaceAsync(profileItem); } return 0; } /// /// Compare two profile items to determine if they represent the same server /// Used for deduplication and server matching /// /// First profile item /// Second profile item /// Whether to compare remarks /// True if the profiles match, false otherwise private static bool CompareProfileItem(ProfileItem? o, ProfileItem? n, bool remarks) { if (o == null || n == null) { return false; } return o.ConfigType == n.ConfigType && AreEqual(o.Address, n.Address) && o.Port == n.Port && AreEqual(o.Id, n.Id) && AreEqual(o.Security, n.Security) && AreEqual(o.Network, n.Network) && AreEqual(o.HeaderType, n.HeaderType) && AreEqual(o.RequestHost, n.RequestHost) && AreEqual(o.Path, n.Path) && (o.ConfigType == EConfigType.Trojan || o.StreamSecurity == n.StreamSecurity) && AreEqual(o.Flow, n.Flow) && AreEqual(o.Sni, n.Sni) && AreEqual(o.Alpn, n.Alpn) && AreEqual(o.Fingerprint, n.Fingerprint) && AreEqual(o.PublicKey, n.PublicKey) && AreEqual(o.ShortId, n.ShortId) && (!remarks || o.Remarks == n.Remarks); static bool AreEqual(string? a, string? b) { return string.Equals(a, b) || (string.IsNullOrEmpty(a) && string.IsNullOrEmpty(b)); } } /// /// Remove a single server profile by its index ID /// Deletes the configuration file if it's a custom config /// /// Current configuration /// Index ID of the profile to remove /// 0 if successful private static async Task RemoveProfileItem(Config config, string indexId) { try { var item = await AppHandler.Instance.GetProfileItem(indexId); if (item == null) { return 0; } if (item.ConfigType == EConfigType.Custom) { File.Delete(Utils.GetConfigPath(item.Address)); } await SQLiteHelper.Instance.DeleteAsync(item); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } return 0; } /// /// Create a custom server that combines multiple servers for load balancing /// Generates a configuration file that references multiple servers /// /// Current configuration /// Selected servers to combine /// Core type to use (Xray or sing_box) /// Load balancing algorithm /// Result object with success state and data public static async Task AddCustomServer4Multiple(Config config, List selecteds, ECoreType coreType, EMultipleLoad multipleLoad) { var indexId = Utils.GetMd5(Global.CoreMultipleLoadConfigFileName); var configPath = Utils.GetConfigPath(Global.CoreMultipleLoadConfigFileName); var result = await CoreConfigHandler.GenerateClientMultipleLoadConfig(config, configPath, selecteds, coreType, multipleLoad); if (result.Success != true) { return result; } if (!File.Exists(configPath)) { return result; } var profileItem = await AppHandler.Instance.GetProfileItem(indexId) ?? new(); profileItem.IndexId = indexId; if (coreType == ECoreType.Xray) { profileItem.Remarks = multipleLoad switch { EMultipleLoad.Random => ResUI.menuSetDefaultMultipleServerXrayRandom, EMultipleLoad.RoundRobin => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, EMultipleLoad.LeastPing => ResUI.menuSetDefaultMultipleServerXrayLeastPing, EMultipleLoad.LeastLoad => ResUI.menuSetDefaultMultipleServerXrayLeastLoad, _ => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, }; } else if (coreType == ECoreType.sing_box) { profileItem.Remarks = ResUI.menuSetDefaultMultipleServerSingBoxLeastPing; } profileItem.Address = Global.CoreMultipleLoadConfigFileName; profileItem.ConfigType = EConfigType.Custom; profileItem.CoreType = coreType; await AddServerCommon(config, profileItem, true); result.Data = indexId; return result; } /// /// Remove servers with invalid test results (timeout) /// Useful for cleaning up subscription lists /// /// Current configuration /// Subscription ID to filter servers /// Number of removed servers or -1 if failed public static async Task RemoveInvalidServerResult(Config config, string subid) { var lstModel = await AppHandler.Instance.ProfileItems(subid, ""); if (lstModel is { Count: <= 0 }) { return -1; } var lstProfileExs = await ProfileExHandler.Instance.GetProfileExs(); var lstProfile = (from t in lstModel join t2 in lstProfileExs on t.IndexId equals t2.IndexId where t2.Delay == -1 select t).ToList(); await RemoveServers(config, JsonUtils.Deserialize>(JsonUtils.Serialize(lstProfile))); return lstProfile.Count; } #endregion Server #region Batch add servers /// /// Add multiple servers from string data (common protocols) /// Parses the string data into server profiles /// /// Current configuration /// String data containing server information /// Subscription ID to associate with the servers /// Whether this is from a subscription /// Number of successfully imported servers or -1 if failed private static async Task AddBatchServersCommon(Config config, string strData, string subid, bool isSub) { if (strData.IsNullOrEmpty()) { return -1; } var subFilter = string.Empty; //remove sub items if (isSub && subid.IsNotEmpty()) { await RemoveServersViaSubid(config, subid, isSub); subFilter = (await AppHandler.Instance.GetSubItem(subid))?.Filter ?? ""; } var countServers = 0; List lstAdd = new(); var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty()); if (isSub) { arrData = arrData.Distinct(); } foreach (var str in arrData) { //maybe sub if (!isSub && (str.StartsWith(Global.HttpsProtocol) || str.StartsWith(Global.HttpProtocol))) { if (await AddSubItem(config, str) == 0) { countServers++; } continue; } var profileItem = FmtHandler.ResolveConfig(str, out string msg); if (profileItem is null) { continue; } //exist sub items //filter if (isSub && subid.IsNotEmpty() && subFilter.IsNotEmpty()) { if (!Regex.IsMatch(profileItem.Remarks, subFilter)) { continue; } } profileItem.Subid = subid; profileItem.IsSub = isSub; var addStatus = profileItem.ConfigType switch { EConfigType.VMess => await AddVMessServer(config, profileItem, false), EConfigType.Shadowsocks => await AddShadowsocksServer(config, profileItem, false), EConfigType.SOCKS => await AddSocksServer(config, profileItem, false), EConfigType.Trojan => await AddTrojanServer(config, profileItem, false), EConfigType.VLESS => await AddVlessServer(config, profileItem, false), EConfigType.Hysteria2 => await AddHysteria2Server(config, profileItem, false), EConfigType.TUIC => await AddTuicServer(config, profileItem, false), EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false), EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false), EConfigType.NaiveProxy => await AddNaiveServer(config, profileItem, false), EConfigType.Juicity => await AddJuicityServer(config, profileItem, false), EConfigType.Brook => await AddBrookServer(config, profileItem, false), EConfigType.Shadowquic => await AddShadowquicServer(config, profileItem, false), _ => -1, }; if (addStatus == 0) { countServers++; lstAdd.Add(profileItem); } } if (lstAdd.Count > 0) { await SQLiteHelper.Instance.InsertAllAsync(lstAdd); } await SaveConfig(config); return countServers; } /// /// Add servers from custom configuration formats (sing-box, v2ray, etc.) /// Handles various configuration formats and imports them as custom configs /// /// Current configuration /// String data containing server information /// Subscription ID to associate with the servers /// Whether this is from a subscription /// Number of successfully imported servers or -1 if failed private static async Task AddBatchServers4Custom(Config config, string strData, string subid, bool isSub) { if (strData.IsNullOrEmpty()) { return -1; } var subItem = await AppHandler.Instance.GetSubItem(subid); var subRemarks = subItem?.Remarks; var preSocksPort = subItem?.PreSocksPort; List? lstProfiles = null; //Is sing-box array configuration if (lstProfiles is null || lstProfiles.Count <= 0) { lstProfiles = SingboxFmt.ResolveFullArray(strData, subRemarks); } //Is v2ray array configuration if (lstProfiles is null || lstProfiles.Count <= 0) { lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks); } if (lstProfiles != null && lstProfiles.Count > 0) { if (isSub && subid.IsNotEmpty()) { await RemoveServersViaSubid(config, subid, isSub); } int count = 0; foreach (var it in lstProfiles) { it.Subid = subid; it.IsSub = isSub; it.PreSocksPort = preSocksPort; if (await AddCustomServer(config, it, true) == 0) { count++; } } if (count > 0) { return count; } } ProfileItem? profileItem = null; //Is sing-box configuration if (profileItem is null) { profileItem = SingboxFmt.ResolveFull(strData, subRemarks); } //Is v2ray configuration if (profileItem is null) { profileItem = V2rayFmt.ResolveFull(strData, subRemarks); } //Is Clash configuration if (profileItem is null) { profileItem = ClashFmt.ResolveFull(strData, subRemarks); } //Is hysteria configuration if (profileItem is null) { profileItem = Hysteria2Fmt.ResolveFull2(strData, subRemarks); } if (profileItem is null) { profileItem = Hysteria2Fmt.ResolveFull(strData, subRemarks); } //Is naiveproxy configuration if (profileItem is null) { profileItem = NaiveproxyFmt.ResolveFull(strData, subRemarks); } if (profileItem is null || profileItem.Address.IsNullOrEmpty()) { return -1; } if (isSub && subid.IsNotEmpty()) { await RemoveServersViaSubid(config, subid, isSub); } profileItem.Subid = subid; profileItem.IsSub = isSub; profileItem.PreSocksPort = preSocksPort; if (await AddCustomServer(config, profileItem, true) == 0) { return 1; } else { return -1; } } /// /// Add Shadowsocks servers from SIP008 format /// SIP008 is a JSON-based format for Shadowsocks servers /// /// Current configuration /// String data in SIP008 format /// Subscription ID to associate with the servers /// Whether this is from a subscription /// Number of successfully imported servers or -1 if failed private static async Task AddBatchServers4SsSIP008(Config config, string strData, string subid, bool isSub) { if (strData.IsNullOrEmpty()) { return -1; } if (isSub && subid.IsNotEmpty()) { await RemoveServersViaSubid(config, subid, isSub); } var lstSsServer = ShadowsocksFmt.ResolveSip008(strData); if (lstSsServer?.Count > 0) { int counter = 0; foreach (var ssItem in lstSsServer) { ssItem.Subid = subid; ssItem.IsSub = isSub; if (await AddShadowsocksServer(config, ssItem) == 0) { counter++; } } await SaveConfig(config); return counter; } return -1; } /// /// Main entry point for adding batch servers from various formats /// Tries different parsing methods to import as many servers as possible /// /// Current configuration /// String data containing server information /// Subscription ID to associate with the servers /// Whether this is from a subscription /// Number of successfully imported servers or -1 if failed public static async Task AddBatchServers(Config config, string strData, string subid, bool isSub) { if (strData.IsNullOrEmpty()) { return -1; } List? lstOriSub = null; ProfileItem? activeProfile = null; if (isSub && subid.IsNotEmpty()) { lstOriSub = await AppHandler.Instance.ProfileItems(subid); activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId); } var counter = 0; if (Utils.IsBase64String(strData)) { counter = await AddBatchServersCommon(config, Utils.Base64Decode(strData), subid, isSub); } if (counter < 1) { counter = await AddBatchServersCommon(config, strData, subid, isSub); } if (counter < 1) { counter = await AddBatchServersCommon(config, Utils.Base64Decode(strData), subid, isSub); } if (counter < 1) { counter = await AddBatchServers4SsSIP008(config, strData, subid, isSub); } //maybe other sub if (counter < 1) { counter = await AddBatchServers4Custom(config, strData, subid, isSub); } //Select active node if (activeProfile != null) { var lstSub = await AppHandler.Instance.ProfileItems(subid); var existItem = lstSub?.FirstOrDefault(t => config.UiItem.EnableUpdateSubOnlyRemarksExist ? t.Remarks == activeProfile.Remarks : CompareProfileItem(t, activeProfile, true)); if (existItem != null) { await ConfigHandler.SetDefaultServerIndex(config, existItem.IndexId); } } //Keep the last traffic statistics if (lstOriSub != null) { var lstSub = await AppHandler.Instance.ProfileItems(subid); foreach (var item in lstSub) { var existItem = lstOriSub?.FirstOrDefault(t => config.UiItem.EnableUpdateSubOnlyRemarksExist ? t.Remarks == item.Remarks : CompareProfileItem(t, item, true)); if (existItem != null) { await StatisticsHandler.Instance.CloneServerStatItem(existItem.IndexId, item.IndexId); } } } return counter; } #endregion Batch add servers #region Sub & Group /// /// Add a subscription item from URL /// Creates a new subscription with default settings /// /// Current configuration /// Subscription URL /// 0 if successful, -1 if failed public static async Task AddSubItem(Config config, string url) { //already exists var count = await SQLiteHelper.Instance.TableAsync().CountAsync(e => e.Url == url); if (count > 0) { return 0; } SubItem subItem = new() { Id = string.Empty, Url = url }; var uri = Utils.TryUri(url); if (uri == null) return -1; //Do not allow http protocol if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost)) { //TODO Temporary reminder to be removed later NoticeHandler.Instance.Enqueue(ResUI.InsecureUrlProtocol); //return -1; } var queryVars = Utils.ParseQueryString(uri.Query); subItem.Remarks = queryVars["remarks"] ?? "import_sub"; return await AddSubItem(config, subItem); } /// /// Add or update a subscription item /// /// Current configuration /// Subscription item to add or update /// 0 if successful, -1 if failed public static async Task AddSubItem(Config config, SubItem subItem) { var item = await AppHandler.Instance.GetSubItem(subItem.Id); if (item is null) { item = subItem; } else { item.Remarks = subItem.Remarks; item.Url = subItem.Url; item.MoreUrl = subItem.MoreUrl; item.Enabled = subItem.Enabled; item.AutoUpdateInterval = subItem.AutoUpdateInterval; item.UserAgent = subItem.UserAgent; item.Sort = subItem.Sort; item.Filter = subItem.Filter; item.UpdateTime = subItem.UpdateTime; item.ConvertTarget = subItem.ConvertTarget; item.PrevProfile = subItem.PrevProfile; item.NextProfile = subItem.NextProfile; item.PreSocksPort = subItem.PreSocksPort; item.Memo = subItem.Memo; } if (item.Id.IsNullOrEmpty()) { item.Id = Utils.GetGuid(false); if (item.Sort <= 0) { var maxSort = 0; if (await SQLiteHelper.Instance.TableAsync().CountAsync() > 0) { var lstSubs = (await AppHandler.Instance.SubItems()); maxSort = lstSubs.LastOrDefault()?.Sort ?? 0; } item.Sort = maxSort + 1; } } if (await SQLiteHelper.Instance.ReplaceAsync(item) > 0) { return 0; } else { return -1; } } /// /// Remove servers associated with a subscription ID /// /// Current configuration /// Subscription ID /// Whether to only remove servers marked as subscription items /// 0 if successful, -1 if failed public static async Task RemoveServersViaSubid(Config config, string subid, bool isSub) { if (subid.IsNullOrEmpty()) { return -1; } var customProfile = await SQLiteHelper.Instance.TableAsync().Where(t => t.Subid == subid && t.ConfigType == EConfigType.Custom).ToListAsync(); if (isSub) { await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileItem where isSub = 1 and subid = '{subid}'"); } else { await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileItem where subid = '{subid}'"); } foreach (var item in customProfile) { File.Delete(Utils.GetConfigPath(item.Address)); } return 0; } /// /// Delete a subscription item and all its associated servers /// /// Current configuration /// Subscription ID to delete /// 0 if successful public static async Task DeleteSubItem(Config config, string id) { var item = await AppHandler.Instance.GetSubItem(id); if (item is null) { return 0; } await SQLiteHelper.Instance.DeleteAsync(item); await RemoveServersViaSubid(config, id, false); return 0; } /// /// Move servers to a different group (subscription) /// /// Current configuration /// List of profiles to move /// Target subscription ID /// 0 if successful public static async Task MoveToGroup(Config config, List lstProfile, string subid) { foreach (var item in lstProfile) { item.Subid = subid; } await SQLiteHelper.Instance.UpdateAllAsync(lstProfile); return 0; } #endregion Sub & Group #region Routing /// /// Save a routing item to the database /// /// Current configuration /// Routing item to save /// 0 if successful, -1 if failed public static async Task SaveRoutingItem(Config config, RoutingItem item) { if (item.Id.IsNullOrEmpty()) { item.Id = Utils.GetGuid(false); } if (await SQLiteHelper.Instance.ReplaceAsync(item) > 0) { return 0; } else { return -1; } } /// /// Add multiple routing rules to a routing item /// /// Routing item to add rules to /// JSON string containing rules data /// 0 if successful, -1 if failed public static async Task AddBatchRoutingRules(RoutingItem routingItem, string strData) { if (strData.IsNullOrEmpty()) { return -1; } var lstRules = JsonUtils.Deserialize>(strData); if (lstRules == null) { return -1; } foreach (var item in lstRules) { item.Id = Utils.GetGuid(false); } routingItem.RuleNum = lstRules.Count; routingItem.RuleSet = JsonUtils.Serialize(lstRules, false); if (routingItem.Id.IsNullOrEmpty()) { routingItem.Id = Utils.GetGuid(false); } if (await SQLiteHelper.Instance.ReplaceAsync(routingItem) > 0) { return 0; } else { return -1; } } /// /// Move a routing rule within a rules list /// Supports moving to top, up, down, bottom or specific position /// /// List of routing rules /// Index of the rule to move /// Direction to move the rule /// Target position when using EMove.Position /// 0 if successful, -1 if failed public static async Task MoveRoutingRule(List rules, int index, EMove eMove, int pos = -1) { int count = rules.Count; if (index < 0 || index > rules.Count - 1) { return -1; } switch (eMove) { case EMove.Top: { if (index == 0) { return 0; } var item = JsonUtils.DeepCopy(rules[index]); rules.RemoveAt(index); rules.Insert(0, item); break; } case EMove.Up: { if (index == 0) { return 0; } var item = JsonUtils.DeepCopy(rules[index]); rules.RemoveAt(index); rules.Insert(index - 1, item); break; } case EMove.Down: { if (index == count - 1) { return 0; } var item = JsonUtils.DeepCopy(rules[index]); rules.RemoveAt(index); rules.Insert(index + 1, item); break; } case EMove.Bottom: { if (index == count - 1) { return 0; } var item = JsonUtils.DeepCopy(rules[index]); rules.RemoveAt(index); rules.Add(item); break; } case EMove.Position: { var removeItem = rules[index]; var item = JsonUtils.DeepCopy(rules[index]); rules.Insert(pos, item); rules.Remove(removeItem); break; } } return await Task.FromResult(0); } /// /// Set the default routing configuration /// /// Current configuration /// Routing item to set as default /// 0 if successful public static async Task SetDefaultRouting(Config config, RoutingItem routingItem) { var items = await AppHandler.Instance.RoutingItems(); if (items.Any(t => t.Id == routingItem.Id && t.IsActive == true)) { return -1; } foreach (var item in items) { if (item.Id == routingItem.Id) { item.IsActive = true; } else { item.IsActive = false; } } await SQLiteHelper.Instance.UpdateAllAsync(items); return 0; } /// /// Get the current default routing configuration /// If no default is set, selects the first available routing item /// /// Current configuration /// The default routing item public static async Task GetDefaultRouting(Config config) { var item = await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.IsActive == true); if (item is null) { var item2 = await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(); await SetDefaultRouting(config, item2); return item2; } return item; } /// /// Initialize routing rules from built-in or external templates /// /// Current configuration /// Whether to import advanced rules /// 0 if successful public static async Task InitRouting(Config config, bool blImportAdvancedRules = false) { if (config.ConstItem.RouteRulesTemplateSourceUrl.IsNullOrEmpty()) { await InitBuiltinRouting(config, blImportAdvancedRules); } else { await InitExternalRouting(config, blImportAdvancedRules); } return 0; } /// /// Initialize routing rules from external templates /// Downloads and processes routing templates from a URL /// /// Current configuration /// Whether to import advanced rules /// 0 if successful public static async Task InitExternalRouting(Config config, bool blImportAdvancedRules = false) { var downloadHandle = new DownloadService(); var templateContent = await downloadHandle.TryDownloadString(config.ConstItem.RouteRulesTemplateSourceUrl, true, ""); if (templateContent.IsNullOrEmpty()) return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback var template = JsonUtils.Deserialize(templateContent); if (template == null) return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback var items = await AppHandler.Instance.RoutingItems(); var maxSort = items.Count; if (!blImportAdvancedRules && items.Where(t => t.Remarks.StartsWith(template.Version)).ToList().Count > 0) { return 0; } for (var i = 0; i < template.RoutingItems.Length; i++) { var item = template.RoutingItems[i]; if (item.Url.IsNullOrEmpty() && item.RuleSet.IsNullOrEmpty()) continue; var ruleSetsString = !item.RuleSet.IsNullOrEmpty() ? item.RuleSet : await downloadHandle.TryDownloadString(item.Url, true, ""); if (ruleSetsString.IsNullOrEmpty()) continue; item.Remarks = $"{template.Version}-{item.Remarks}"; item.Enabled = true; item.Sort = ++maxSort; item.Url = string.Empty; await AddBatchRoutingRules(item, ruleSetsString); //first rule as default at first startup if (!blImportAdvancedRules && i == 0) { await SetDefaultRouting(config, item); } } return 0; } /// /// Initialize built-in routing rules /// Creates default routing configurations (whitelist, blacklist, global) /// /// Current configuration /// Whether to import advanced rules /// 0 if successful public static async Task InitBuiltinRouting(Config config, bool blImportAdvancedRules = false) { var ver = "V3-"; var items = await AppHandler.Instance.RoutingItems(); //TODO Temporary code to be removed later var lockItem = items?.FirstOrDefault(t => t.Locked == true); if (lockItem != null) { await ConfigHandler.RemoveRoutingItem(lockItem); items = await AppHandler.Instance.RoutingItems(); } if (!blImportAdvancedRules && items.Count > 0) { //migrate //TODO Temporary code to be removed later if (config.RoutingBasicItem.RoutingIndexId.IsNotEmpty()) { var item = items.FirstOrDefault(t => t.Id == config.RoutingBasicItem.RoutingIndexId); if (item != null) { await SetDefaultRouting(config, item); } config.RoutingBasicItem.RoutingIndexId = string.Empty; } return 0; } var maxSort = items.Count; //Bypass the mainland var item2 = new RoutingItem() { Remarks = $"{ver}绕过大陆(Whitelist)", Url = string.Empty, Sort = maxSort + 1, }; await AddBatchRoutingRules(item2, EmbedUtils.GetEmbedText(Global.CustomRoutingFileName + "white")); //Blacklist var item3 = new RoutingItem() { Remarks = $"{ver}黑名单(Blacklist)", Url = string.Empty, Sort = maxSort + 2, }; await AddBatchRoutingRules(item3, EmbedUtils.GetEmbedText(Global.CustomRoutingFileName + "black")); //Global var item1 = new RoutingItem() { Remarks = $"{ver}全局(Global)", Url = string.Empty, Sort = maxSort + 3, }; await AddBatchRoutingRules(item1, EmbedUtils.GetEmbedText(Global.CustomRoutingFileName + "global")); if (!blImportAdvancedRules) { await SetDefaultRouting(config, item2); } return 0; } /// /// Remove a routing item from the database /// /// Routing item to remove public static async Task RemoveRoutingItem(RoutingItem routingItem) { await SQLiteHelper.Instance.DeleteAsync(routingItem); } #endregion Routing #region DNS /// /// Initialize built-in DNS configurations /// Creates default DNS items for V2Ray and sing-box /// /// Current configuration /// 0 if successful public static async Task InitBuiltinDNS(Config config) { var items = await AppHandler.Instance.DNSItems(); if (items.Count <= 0) { var item = new DNSItem() { Remarks = "V2ray", CoreType = ECoreType.Xray, }; await SaveDNSItems(config, item); var item2 = new DNSItem() { Remarks = "sing-box", CoreType = ECoreType.sing_box, }; await SaveDNSItems(config, item2); } return 0; } /// /// Save a DNS item to the database /// /// Current configuration /// DNS item to save /// 0 if successful, -1 if failed public static async Task SaveDNSItems(Config config, DNSItem item) { if (item == null) { return -1; } if (item.Id.IsNullOrEmpty()) { item.Id = Utils.GetGuid(false); } if (await SQLiteHelper.Instance.ReplaceAsync(item) > 0) { return 0; } else { return -1; } } /// /// Get an external DNS configuration from URL /// Downloads and processes DNS templates /// /// Core type (Xray or sing-box) /// URL of the DNS template /// DNS item with configuration from the URL public static async Task GetExternalDNSItem(ECoreType type, string url) { var currentItem = await AppHandler.Instance.GetDNSItem(type); var downloadHandle = new DownloadService(); var templateContent = await downloadHandle.TryDownloadString(url, true, ""); if (templateContent.IsNullOrEmpty()) return currentItem; var template = JsonUtils.Deserialize(templateContent); if (template == null) return currentItem; if (!template.NormalDNS.IsNullOrEmpty()) template.NormalDNS = await downloadHandle.TryDownloadString(template.NormalDNS, true, ""); if (!template.TunDNS.IsNullOrEmpty()) template.TunDNS = await downloadHandle.TryDownloadString(template.TunDNS, true, ""); template.Id = currentItem.Id; template.Enabled = currentItem.Enabled; template.Remarks = currentItem.Remarks; template.CoreType = type; return template; } #endregion DNS #region Regional Presets /// /// Apply regional presets for geo-specific configurations /// Sets up geo files, routing rules, and DNS for specific regions /// /// Current configuration /// Type of preset (Default, Russia, Iran) /// True if successful public static async Task ApplyRegionalPreset(Config config, EPresetType type) { switch (type) { case EPresetType.Default: config.ConstItem.GeoSourceUrl = ""; config.ConstItem.SrsSourceUrl = ""; config.ConstItem.RouteRulesTemplateSourceUrl = ""; await SQLiteHelper.Instance.DeleteAllAsync(); await InitBuiltinDNS(config); return true; case EPresetType.Russia: config.ConstItem.GeoSourceUrl = Global.GeoFilesSources[1]; config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[1]; config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[1]; await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[1] + "v2ray.json")); await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[1] + "sing_box.json")); return true; case EPresetType.Iran: config.ConstItem.GeoSourceUrl = Global.GeoFilesSources[2]; config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[2]; config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[2]; await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[2] + "v2ray.json")); await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[2] + "sing_box.json")); return true; } return false; } #endregion Regional Presets #region UIItem public static WindowSizeItem? GetWindowSizeItem(Config config, string typeName) { var sizeItem = config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName); if (sizeItem == null || sizeItem.Width <= 0 || sizeItem.Height <= 0) { return null; } return sizeItem; } public static int SaveWindowSizeItem(Config config, string typeName, double width, double height) { var sizeItem = config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName); if (sizeItem == null) { sizeItem = new WindowSizeItem { TypeName = typeName }; config.UiItem.WindowSizeItem.Add(sizeItem); } sizeItem.Width = (int)width; sizeItem.Height = (int)height; return 0; } public static int SaveMainGirdHeight(Config config, double height1, double height2) { var uiItem = config.UiItem ?? new(); uiItem.MainGirdHeight1 = (int)(height1 + 0.1); uiItem.MainGirdHeight2 = (int)(height2 + 0.1); return 0; } #endregion UIItem }