using Microsoft.EntityFrameworkCore; using Shared.Models; using Shared.Models.Base; using Shared.Models.Proxy; using Shared.Models.SQL; using System.Collections.Concurrent; using System.Net; using System.Text.Json; using System.Threading; namespace Shared.Engine { public class ProxyLink : IProxyLink { #region ProxyLink static readonly ConcurrentDictionary links = new(); static readonly Timer _cronTimer = new Timer(Cron, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); public static int Stat_ContLinks => links.IsEmpty ? 0 : links.Count; #endregion #region Encrypt public string Encrypt(string uri, string plugin, DateTime ex = default, bool IsProxyImg = false) => Encrypt(uri, null, verifyip: false, ex: ex, plugin: plugin, IsProxyImg: IsProxyImg); public static string Encrypt(string uri, ProxyLinkModel p, bool forceMd5 = false) => Encrypt(uri, p.reqip, p.headers, p.proxy, p.plugin, p.verifyip, forceMd5: forceMd5); public static string Encrypt(string uri, string reqip, List headers = null, WebProxy proxy = null, string plugin = null, bool verifyip = true, DateTime ex = default, bool forceMd5 = false, bool IsProxyImg = false) { if (string.IsNullOrWhiteSpace(uri)) return string.Empty; string hash; bool IsMd5 = false; string uri_clear = uri.Contains("#") ? uri.Split("#")[0].Trim() : uri.Trim(); if (plugin == "posterapi") { hash = AesTo.Encrypt(JsonSerializer.Serialize(new AesPayload() { u = uri_clear })); } else if (!forceMd5 && AppInit.conf.serverproxy.encrypt_aes && (headers == null || headers.Count == 0) && proxy == null && !uri_clear.Contains(" or ")) { if (verifyip && AppInit.conf.serverproxy.verifyip) { hash = AesTo.Encrypt(JsonSerializer.Serialize(new AesPayload() { p = plugin, u = uri_clear, i = reqip, v = true, e = DateTime.Now.AddHours(36) })); } else { hash = AesTo.Encrypt(JsonSerializer.Serialize(new AesPayload() { p = plugin, u = uri_clear })); } } else { IsMd5 = true; hash = CrypTo.md5(uri_clear + (verifyip && AppInit.conf.serverproxy.verifyip ? reqip : string.Empty)); } if (IsProxyImg) { if (uri.Contains(".png")) hash += ".png"; else if (uri.Contains(".webp")) hash += ".webp"; else hash += ".jpg"; } else { if (uri.Contains(".m3u8")) hash += ".m3u8"; else if (uri.Contains(".m3u")) hash += ".m3u"; else if (uri.Contains(".mpd")) hash += ".mpd"; else if (uri.Contains(".webm")) hash += ".webm"; else if (uri.Contains(".ts")) hash += ".ts"; else if (uri.Contains(".m4s")) hash += ".m4s"; else if (uri.Contains(".mp4")) hash += ".mp4"; else if (uri.Contains(".mov")) hash += ".mov"; else if (uri.Contains(".mkv")) hash += ".mkv"; else if (uri.Contains(".aac")) hash += ".aac"; else if (uri.Contains(".vtt")) hash += ".vtt"; else if (uri.Contains(".srt")) hash += ".srt"; else if (uri.Contains(".jpg") || uri.Contains(".jpeg")) hash += ".jpg"; else if (uri.Contains(".png")) hash += ".png"; else if (uri.Contains(".webp")) hash += ".webp"; } if (IsMd5) { var md = new ProxyLinkModel(verifyip ? reqip : null, headers, proxy, uri_clear, plugin, verifyip, ex: ex); links.AddOrUpdate(hash, md, (d, u) => md); } return hash; } #endregion #region Decrypt public static ProxyLinkModel Decrypt(string hash, string reqip) { if (string.IsNullOrEmpty(hash)) return null; if (IsAes(hash)) { ReadOnlySpan hashSpan = hash.AsSpan(); int dot = hash.LastIndexOf('.'); if (dot > 0) hashSpan = hashSpan.Slice(0, dot); string dec = AesTo.Decrypt(hashSpan); if (string.IsNullOrEmpty(dec)) return null; var root = JsonSerializer.Deserialize(dec); if (root == null) return null; if (root.v) { if (reqip != null && root.i != reqip) return null; if (DateTime.Now > root.e) return null; } List headers = null; if (root.h != null && root.h.Count > 0) headers = HeadersModel.Init(root.h); return new ProxyLinkModel(reqip, headers, null, root.u, root.p); } if (!links.TryGetValue(hash, out ProxyLinkModel val)) { try { if (IsUseSql(hash)) { using (var sqlDb = ProxyLinkContext.Factory != null ? ProxyLinkContext.Factory.CreateDbContext() : new ProxyLinkContext()) { var link = sqlDb.links.Find(hash); if (link != null && link.ex > DateTime.Now) { val = JsonSerializer.Deserialize(link.json); val.id = link.Id; val.ex = link.ex; } } } } catch { } } if (val != null) { if (val.verifyip == false || AppInit.conf.serverproxy.verifyip == false || val.reqip == string.Empty || reqip == null || reqip == val.reqip) return val; } return null; } #endregion #region IsAes public static bool IsAes(ReadOnlySpan hash) { if (hash.IsEmpty) return false; if (hash.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return false; // Ищем первый из ?, &, . int idx = hash.IndexOfAny('?', '&', '.'); ReadOnlySpan firstPart; if (idx >= 0) firstPart = hash.Slice(0, idx); else firstPart = hash; // Если длина 32 — это не AES return firstPart.Length != 32; } #endregion #region IsUseSql static bool IsUseSql(ReadOnlySpan hash) { if (AppInit.conf.mikrotik) return false; bool useSql = true; if (AppInit.conf.serverproxy.image.noSqlDb) { int dot = hash.LastIndexOf('.'); if (dot > 0) { ReadOnlySpan ext = hash.Slice(dot + 1); useSql = ext switch { var e when e.Equals("jpg", StringComparison.OrdinalIgnoreCase) => false, var e when e.Equals("jpeg", StringComparison.OrdinalIgnoreCase) => false, var e when e.Equals("png", StringComparison.OrdinalIgnoreCase) => false, var e when e.Equals("webp", StringComparison.OrdinalIgnoreCase) => false, _ => true }; } } return useSql; } #endregion #region Cron static HashSet tempLinks = new(1000), sqlLinks = new(1000), delete_ids = new(1000); static int cronRound = 0; static DateTime _nextClearDb = DateTime.Now.AddMinutes(5); static int _updatingDb = 0; async static void Cron(object state) { if (links.IsEmpty) return; if (Interlocked.Exchange(ref _updatingDb, 1) == 1) return; try { if (cronRound >= 60) { cronRound = 0; tempLinks.Clear(); } cronRound++; var now = DateTime.Now; if (now > _nextClearDb) { _nextClearDb = now.AddMinutes(5); using (var sqlDb = new ProxyLinkContext()) { await sqlDb.links .Where(i => now > i.ex) .ExecuteDeleteAsync(); } } else { sqlLinks.Clear(); delete_ids.Clear(); foreach (var link in links) { try { if (IsUseSql(link.Key) == false || link.Value.proxy != null || now.AddMinutes(5) > link.Value.ex || link.Value.uri.Contains(" or ")) { if (now > link.Value.ex) delete_ids.Add(link.Key); } else { if (tempLinks.Contains(link.Key)) delete_ids.Add(link.Key); else { sqlLinks.Add(link.Key); } } } catch { } } if (delete_ids.Count > 0) { foreach (string removeId in delete_ids) links.TryRemove(removeId, out _); } if (sqlLinks.Count > 0) { using (var sqlDb = new ProxyLinkContext()) { await sqlDb.links .Where(x => sqlLinks.Contains(x.Id)) .ExecuteDeleteAsync(); foreach (string linkId in sqlLinks) { if (links.TryRemove(linkId, out var link)) { if (link.id == null) link.id = linkId; sqlDb.links.Add(new ProxyLinkSqlModel() { Id = linkId, ex = link.ex, json = JsonSerializer.Serialize(link) }); } } await sqlDb.SaveChangesAsync(); foreach (string removeLink in sqlLinks) tempLinks.Add(removeLink); } } } } catch (Exception ex) { Console.WriteLine($"ProxyLink: {ex}"); } finally { Volatile.Write(ref _updatingDb, 0); } } #endregion sealed class AesPayload { public string p { get; set; } public string u { get; set; } public string i { get; set; } public bool v { get; set; } public DateTime e { get; set; } public Dictionary h { get; set; } } } }