using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.RegularExpressions; using WinWARP.Core.Helper; using WinWARP.Core.Models.Entities; using WinWARP.Core.Services.Interfaces; using static System.Runtime.InteropServices.JavaScript.JSType; using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database; namespace WinWARP.Core.Services.Implementations; public partial class SubscriptionFetcher(IDialogService dialogService) : ISubscriptionFetcher { private static readonly HttpClient _httpClient = new HttpClient(); private readonly IDialogService _dialogService = dialogService; public bool IsUrlValid(string subscriptionUrl) { Uri? validatedUri; if (!Uri.TryCreate(subscriptionUrl, UriKind.Absolute, out validatedUri)) { return false; } return validatedUri.Scheme == Uri.UriSchemeHttp || validatedUri.Scheme == Uri.UriSchemeHttps; } public async Task FetchAsync(string subscriptionUrl) { _httpClient.Timeout = TimeSpan.FromSeconds(10); var subscription = new Subscription { Link = subscriptionUrl }; try { using var response = await _httpClient.GetAsync(subscriptionUrl); response.EnsureSuccessStatusCode(); response.Headers.TryGetValues("Profile-Web-Page-Url", out var profileWebPage); subscription.WebPage = profileWebPage?.FirstOrDefault() ?? string.Empty; response.Headers.TryGetValues("Profile-Title", out var profileTitle); subscription.DefaultTitle = GetSubscriptionTitle(profileTitle?.FirstOrDefault()); response.Headers.TryGetValues("Profile-Update-Interval", out var profileUpdateInterval); if (profileUpdateInterval != null && int.TryParse(profileUpdateInterval.FirstOrDefault(), out var updateInterval)) subscription.DefaultUpdateInterval = updateInterval; response.Headers.TryGetValues("Profile-Support-Url", out var profileSupportUrl); subscription.SupportURL = profileSupportUrl?.FirstOrDefault() ?? string.Empty; response.Headers.TryGetValues("Subscription-Userinfo", out var profileNetworkData); var (Upload, Download, Total, Expire) = ParseNetworkData(profileNetworkData?.FirstOrDefault() ?? string.Empty); subscription.Upload = Upload; subscription.Download = Download; subscription.Total = Total; subscription.ExpireDate = Expire; subscription.Servers = ParseServerData(await response.Content.ReadAsStringAsync()); } catch (Exception ex) when (!Debugger.IsAttached) { _dialogService.ShowErrorAsync(ex.Message); } return subscription; } #region Titles private static string GetSubscriptionTitle(string title) { if (string.IsNullOrWhiteSpace(title)) { return "New subscription"; } else if (title.StartsWith("base64:")) { var base64Data = title[7..]; try { var stringData = Encoding.UTF8.GetString(Convert.FromBase64String(base64Data)); return stringData[..(Math.Min(stringData.Length, 100))]; } catch { return "New subscription"; } } else { return title[..(Math.Min(title.Length, 100))]; } } private static string GetServerTitle(string title, out string region) { if (string.IsNullOrEmpty(title)) { region = string.Empty; return "New server"; } title = title[1..]; // skip the leading '#' character region = FlagEmojiParser.ExtractCountryCodes(title, out var countryIndicators).FirstOrDefault() ?? string.Empty; if (!string.IsNullOrEmpty(region)) { title = title.Replace(countryIndicators.FirstOrDefault() ?? string.Empty, string.Empty).Trim(); return title[..Math.Min(title.Length, 100)]; } return title[..Math.Min(title.Length, 100)]; } #endregion #region SubscriptionParsing private static (long Upload, long Download, long Total, DateTime Expire) ParseNetworkData(string data) { if (string.IsNullOrWhiteSpace(data)) throw new ArgumentException("Input string cannot be null or empty.", nameof(data)); // Dictionary to temporarily store the parsed key-value pairs var values = new Dictionary(StringComparer.OrdinalIgnoreCase); // Split the string by semicolon to get individual pairs (e.g., "upload=0") var pairs = data.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); foreach (var keyValue in from string pair in pairs let keyValue = pair.Split('=', 2) where keyValue.Length == 2 select keyValue) { values[keyValue[0].Trim()] = keyValue[1].Trim(); } // Helper to safely parse numbers, defaulting to 0 if missing or invalid long GetLongValue(string key) => values.TryGetValue(key, out var val) && long.TryParse(val, out var result) ? result : 0; var upload = GetLongValue("upload"); var download = GetLongValue("download"); var total = GetLongValue("total"); var expireUnix = GetLongValue("expire"); var expireDateTime = DateTimeOffset.FromUnixTimeSeconds(expireUnix).UtcDateTime; return (upload, download, total, expireDateTime); } [GeneratedRegex(@"^[A-Za-z0-9+/]*={0,2}$")] private static partial Regex Base64Regex(); private static List ParseServerData(string data) { if (string.IsNullOrWhiteSpace(data)) return []; data = data.Trim(); if (TryGetBase64String(data, out var decodedText)) { return [.. ParseText(decodedText)]; } else if (data.StartsWith('{') || data.StartsWith('[')) { return ParseJson(data); } else if (data.StartsWith("---") || data.Contains(':')) { return ParseYaml(data); } else { return [.. ParseText(data)]; } } #endregion #region ServersParsing private static List ParseJson(string jsonData) { throw new NotImplementedException(); } private static List ParseYaml(string yamlData) { throw new NotImplementedException(); } private static IEnumerable ParseText(string textData) { foreach (var line in textData.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) { var server = ParseServerFromLink(line); if (server != null) { yield return server; } } } private static bool TryGetBase64String(string input, out string decodedText) { decodedText = null; // Sanitize edge-case variants: URL-safe Base64 fixes ('-' -> '+', '_' -> '/') var sanitized = input.Replace('-', '+').Replace('_', '/').Replace("\r", "").Replace("\n", "").Trim(); // Fix missing padding if client/server skipped it var mod = sanitized.Length % 4; if (mod > 0) sanitized += new string('=', 4 - mod); // Regex validation for pure Base64 characters if (!Base64Regex().IsMatch(sanitized)) return false; try { var bytes = Convert.FromBase64String(sanitized); decodedText = Encoding.UTF8.GetString(bytes); return true; } catch { return false; } } private static SubscriptionServer ParseServerFromLink(string serverData) { var uri = new Uri(serverData); if (uri.Scheme == "ss" || uri.Scheme == "vmess" || uri.Scheme == "vless" || uri.Scheme == "trojan") { var title = Uri.UnescapeDataString(uri.Fragment); return new SubscriptionServer { DefaultTitle = GetServerTitle(title, out var region), Region = FlagEmojiParser.ExtractCountryCodes(title).FirstOrDefault() ?? string.Empty, ConnectionString = serverData }; } else { return null; } } #endregion #region Tags public List GetTags(string connection) { if (string.IsNullOrWhiteSpace(connection)) return null; if (connection.StartsWith('{') || connection.StartsWith('[')) return [.. GetJsonTags(connection)]; else return [.. GetTextTags(connection)]; } private static IEnumerable GetTextTags(string connection) { var input = connection.Trim(); var scheme = string.Empty; var query = string.Empty; var lowered = input.ToLowerInvariant(); if (Uri.TryCreate(input, UriKind.Absolute, out var uri)) { scheme = uri.Scheme ?? string.Empty; query = uri.Query ?? string.Empty; } else { var m = UrlCheck().Match(input); if (m.Success) { scheme = m.Groups[1].Value; var idx = input.IndexOf('?'); if (idx >= 0) query = input[idx..]; } } // 1) Protocol tag (Uri.Scheme) if (!string.IsNullOrEmpty(scheme)) yield return scheme.ToUpperInvariant(); // 2) Security tag (one of: reality, vision, tls) var security = string.Empty; if (lowered.Contains("reality") || scheme.Contains("reality", StringComparison.OrdinalIgnoreCase)) { security = "reality"; } else if (lowered.Contains("vision") || scheme.Contains("vision", StringComparison.OrdinalIgnoreCase)) { security = "vision"; } else if (scheme.EndsWith("+tls", StringComparison.OrdinalIgnoreCase) || scheme.Equals("https", StringComparison.OrdinalIgnoreCase) || scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) || lowered.Contains("tls") || lowered.Contains("secure=true") || SecurityCheck().IsMatch(query)) { security = "tls"; } if (!string.IsNullOrEmpty(security)) yield return security.ToUpperInvariant(); // 3) Transport tag (one of: tcp, ws, grpc, h2, xhttp) var transport = string.Empty; if (scheme.Equals("ws", StringComparison.OrdinalIgnoreCase) || scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) || lowered.Contains("transport=ws") || lowered.Contains("websocket")) { transport = "ws"; } else if (scheme.Contains("grpc", StringComparison.OrdinalIgnoreCase) || lowered.Contains("transport=grpc") || lowered.Contains("grpc/")) { transport = "grpc"; } else if (scheme.Equals("h2", StringComparison.OrdinalIgnoreCase) || lowered.Contains("http2") || lowered.Contains("alpn=h2") || lowered.Contains("transport=h2")) { transport = "h2"; } else if (lowered.Contains("xhttp") || lowered.Contains("x-http") || lowered.Contains("transport=xhttp")) { transport = "xhttp"; } else if (scheme.Equals("tcp", StringComparison.OrdinalIgnoreCase) || lowered.Contains("transport=tcp") || lowered.Contains("tcp/") || lowered.Contains(':')) { transport = "tcp"; } else { transport = "tcp"; } if (!string.IsNullOrEmpty(transport)) yield return transport.ToUpperInvariant(); } private IEnumerable GetJsonTags(string json) { throw new NotImplementedException(); } [GeneratedRegex(@"^([a-zA-Z][\w+\-\.]*):")] private static partial Regex UrlCheck(); [GeneratedRegex(@"(\b|&)security=(tls|true)(&|$)", RegexOptions.IgnoreCase, "en-001")] private static partial Regex SecurityCheck(); #endregion }