lampac-talks f843f04fd4 chore: initial commit 154.3
Signed-off-by: lampac-talks <lampac-talks@users.noreply.github.com>
2026-01-30 16:23:09 +03:00

463 lines
20 KiB
C#

using DnsClient;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Engine.Utilities;
using Shared.Models;
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace Lampac.Engine.Middlewares
{
public class ProxyTmdb
{
#region ProxyTmdb
static FileSystemWatcher fileWatcher;
static readonly ConcurrentDictionary<string, int> cacheFiles = new ();
public static int Stat_ContCacheFiles => cacheFiles.IsEmpty ? 0 : cacheFiles.Count;
static Timer cleanupTimer;
static ProxyTmdb()
{
Directory.CreateDirectory("cache/tmdb");
if (AppInit.conf.multiaccess == false)
return;
foreach (string path in Directory.EnumerateFiles("cache/tmdb", "*"))
{
using (var handle = File.OpenHandle(path))
cacheFiles.TryAdd(Path.GetFileName(path), (int)RandomAccess.GetLength(handle));
}
fileWatcher = new FileSystemWatcher
{
Path = "cache/tmdb",
NotifyFilter = NotifyFilters.FileName,
EnableRaisingEvents = true
};
//fileWatcher.Created += (s, e) => { cacheFiles.TryAdd(e.Name, 0); };
fileWatcher.Deleted += (s, e) => { cacheFiles.TryRemove(e.Name, out _); };
cleanupTimer = new Timer(cleanup, null, TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(20));
}
static void cleanup(object state)
{
try
{
foreach (string md5fileName in cacheFiles.Keys)
{
if (!File.Exists(Path.Combine("cache", "tmdb", md5fileName)))
cacheFiles.TryRemove(md5fileName, out _);
}
}
catch { }
}
public ProxyTmdb(RequestDelegate next) { }
#endregion
public Task Invoke(HttpContext httpContext)
{
var requestInfo = httpContext.Features.Get<RequestModel>();
var hybridCache = IHybridCache.Get(requestInfo);
if (httpContext.Request.Path.Value.StartsWith("/tmdb/api/", StringComparison.OrdinalIgnoreCase))
return API(httpContext, hybridCache, requestInfo);
if (httpContext.Request.Path.Value.StartsWith("/tmdb/img/", StringComparison.OrdinalIgnoreCase))
return IMG(httpContext, requestInfo);
string path = Regex.Replace(httpContext.Request.Path.Value, "^/tmdb/https?://", "", RegexOptions.IgnoreCase).Replace("/tmdb/", "");
string uri = Regex.Match(path, "^[^/]+/(.*)", RegexOptions.IgnoreCase).Groups[1].Value + httpContext.Request.QueryString.Value;
if (path.Contains("api.themoviedb.org", StringComparison.OrdinalIgnoreCase))
{
httpContext.Request.Path = $"/tmdb/api/{uri}";
return API(httpContext, hybridCache, requestInfo);
}
else if (path.Contains("image.tmdb.org", StringComparison.OrdinalIgnoreCase))
{
httpContext.Request.Path = $"/tmdb/img/{uri}";
return IMG(httpContext, requestInfo);
}
httpContext.Response.StatusCode = 403;
return Task.CompletedTask;
}
#region API
async public Task API(HttpContext httpContex, IHybridCache hybridCache, RequestModel requestInfo)
{
using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContex.RequestAborted))
{
ctsHttp.CancelAfter(TimeSpan.FromSeconds(30));
httpContex.Response.ContentType = "application/json; charset=utf-8";
var init = AppInit.conf.tmdb;
if (!init.enable && !requestInfo.IsLocalRequest)
{
httpContex.Response.StatusCode = 401;
await httpContex.Response.WriteAsJsonAsync(new { error = true, msg = "disable" }, ctsHttp.Token);
return;
}
string path = httpContex.Request.Path.Value.Replace("/tmdb/api", "", StringComparison.OrdinalIgnoreCase);
path = Regex.Replace(path, "^/https?://api.themoviedb.org", "", RegexOptions.IgnoreCase);
path = Regex.Replace(path, "/$", "", RegexOptions.IgnoreCase);
string query = Regex.Replace(httpContex.Request.QueryString.Value, "(&|\\?)(account_email|email|uid|token)=[^&]+", "");
string uri = "https://api.themoviedb.org" + path + query;
string mkey = $"tmdb/api:{path}:{query}";
if (hybridCache.TryGetValue(mkey, out (string json, int statusCode) cache, inmemory: false))
{
httpContex.Response.Headers["X-Cache-Status"] = "HIT";
httpContex.Response.StatusCode = cache.statusCode;
httpContex.Response.ContentType = "application/json; charset=utf-8";
await httpContex.Response.WriteAsync(cache.json, ctsHttp.Token);
return;
}
httpContex.Response.Headers["X-Cache-Status"] = "MISS";
string tmdb_ip = init.API_IP;
#region DNS QueryType.A
if (string.IsNullOrEmpty(tmdb_ip) && string.IsNullOrEmpty(init.API_Minor) && !string.IsNullOrEmpty(init.DNS))
{
string dnskey = $"tmdb/api:dns:{init.DNS}";
var _spredns = new SemaphorManager(dnskey, ctsHttp.Token);
try
{
await _spredns.WaitAsync();
if (!Startup.memoryCache.TryGetValue(dnskey, out string dns_ip))
{
var lookup = new LookupClient(IPAddress.Parse(init.DNS));
var queryType = await lookup.QueryAsync("api.themoviedb.org", QueryType.A, cancellationToken: ctsHttp.Token);
dns_ip = queryType?.Answers?.ARecords()?.FirstOrDefault()?.Address?.ToString();
if (!string.IsNullOrEmpty(dns_ip))
Startup.memoryCache.Set(dnskey, dns_ip, DateTime.Now.AddMinutes(Math.Max(init.DNS_TTL, 5)));
else
Startup.memoryCache.Set(dnskey, string.Empty, DateTime.Now.AddMinutes(5));
}
if (!string.IsNullOrEmpty(dns_ip))
tmdb_ip = dns_ip;
}
catch { }
finally
{
_spredns.Release();
}
}
#endregion
var headers = new List<HeadersModel>();
var proxyManager = init.useproxy
? new ProxyManager("tmdb_api", init)
: null;
if (!string.IsNullOrEmpty(init.API_Minor))
{
uri = uri.Replace("api.themoviedb.org", init.API_Minor);
}
else if (!string.IsNullOrEmpty(tmdb_ip))
{
headers.Add(new HeadersModel("Host", "api.themoviedb.org"));
uri = uri.Replace("api.themoviedb.org", tmdb_ip);
}
var result = await Http.BaseGetAsync<JObject>(uri, timeoutSeconds: 20, proxy: proxyManager?.Get(), httpversion: init.httpversion, headers: headers, statusCodeOK: false);
if (result.content == null)
{
proxyManager?.Refresh();
httpContex.Response.StatusCode = 401;
await httpContex.Response.WriteAsJsonAsync(new { error = true, msg = "json null" }, ctsHttp.Token);
return;
}
cache.statusCode = (int)result.response.StatusCode;
httpContex.Response.StatusCode = cache.statusCode;
if (result.content.ContainsKey("status_message") || result.response.StatusCode != HttpStatusCode.OK)
{
proxyManager?.Refresh();
cache.json = JsonConvertPool.SerializeObject(result.content);
if (init.cache_api > 0 && !string.IsNullOrEmpty(cache.json))
hybridCache.Set(mkey, cache, DateTime.Now.AddMinutes(1), inmemory: true);
await httpContex.Response.WriteAsync(cache.json, ctsHttp.Token);
return;
}
cache.json = JsonConvertPool.SerializeObject(result.content);
if (init.cache_api > 0 && !string.IsNullOrEmpty(cache.json))
hybridCache.Set(mkey, cache, DateTime.Now.AddMinutes(init.cache_api), inmemory: false);
proxyManager?.Success();
httpContex.Response.ContentType = "application/json; charset=utf-8";
await httpContex.Response.WriteAsync(cache.json, ctsHttp.Token);
}
}
#endregion
#region IMG
async public Task IMG(HttpContext httpContex, RequestModel requestInfo)
{
using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContex.RequestAborted))
{
ctsHttp.CancelAfter(TimeSpan.FromSeconds(30));
var init = AppInit.conf.tmdb;
if (!init.enable)
{
httpContex.Response.StatusCode = 401;
await httpContex.Response.WriteAsJsonAsync(new { error = true, msg = "disable" }, ctsHttp.Token);
return;
}
string path = httpContex.Request.Path.Value.Replace("/tmdb/img", "", StringComparison.OrdinalIgnoreCase);
path = Regex.Replace(path, "^/https?://image.tmdb.org", "", RegexOptions.IgnoreCase);
string query = Regex.Replace(httpContex.Request.QueryString.Value, "(&|\\?)(account_email|email|uid|token)=[^&]+", "");
string uri = "https://image.tmdb.org" + path + query;
string md5key = CrypTo.md5($"{path}:{query}");
string outFile = Path.Combine("cache", "tmdb", md5key);
bool cacheimg = init.cache_img > 0 && AppInit.conf.mikrotik == false;
httpContex.Response.ContentType = path.Contains(".png", StringComparison.OrdinalIgnoreCase)
? "image/png"
: path.Contains(".svg", StringComparison.OrdinalIgnoreCase) ? "image/svg+xml" : "image/jpeg";
#region cacheFiles
if (cacheimg)
{
if (cacheFiles.ContainsKey(md5key) || (AppInit.conf.multiaccess == false && File.Exists(outFile)))
{
httpContex.Response.Headers["X-Cache-Status"] = "HIT";
if (init.responseContentLength && cacheFiles.ContainsKey(md5key))
httpContex.Response.ContentLength = cacheFiles[md5key];
await httpContex.Response.SendFileAsync(outFile, ctsHttp.Token).ConfigureAwait(false);
return;
}
}
#endregion
string tmdb_ip = init.IMG_IP;
#region DNS QueryType.A
if (string.IsNullOrEmpty(tmdb_ip) && string.IsNullOrEmpty(init.IMG_Minor) && !string.IsNullOrEmpty(init.DNS))
{
string dnskey = $"tmdb/img:dns:{init.DNS}";
var _spredns = new SemaphorManager(dnskey, ctsHttp.Token);
try
{
await _spredns.WaitAsync();
if (!Startup.memoryCache.TryGetValue(dnskey, out string dns_ip))
{
var lookup = new LookupClient(IPAddress.Parse(init.DNS));
var result = await lookup.QueryAsync("image.tmdb.org", QueryType.A, cancellationToken: ctsHttp.Token);
dns_ip = result?.Answers?.ARecords()?.FirstOrDefault()?.Address?.ToString();
if (!string.IsNullOrEmpty(dns_ip))
Startup.memoryCache.Set(dnskey, dns_ip, DateTime.Now.AddMinutes(Math.Max(init.DNS_TTL, 5)));
else
Startup.memoryCache.Set(dnskey, string.Empty, DateTime.Now.AddMinutes(5));
}
if (!string.IsNullOrEmpty(dns_ip))
tmdb_ip = dns_ip;
}
catch { }
finally
{
_spredns.Release();
}
}
#endregion
#region headers
var headers = HeadersModel.Init(
// используем старый ua что-бы гарантировать image/jpeg вместо image/webp
("Accept", "image/jpeg,image/png,image/*;q=0.8,*/*;q=0.5"),
("User-Agent", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2"),
("Cache-Control", "max-age=0")
);
if (!string.IsNullOrEmpty(init.IMG_Minor))
{
uri = uri.Replace("image.tmdb.org", init.IMG_Minor);
}
else if (!string.IsNullOrEmpty(tmdb_ip))
{
headers.Add(new HeadersModel("Host", "image.tmdb.org"));
uri = uri.Replace("image.tmdb.org", tmdb_ip);
}
#endregion
var proxyManager = init.useproxy
? new ProxyManager("tmdb_img", init)
: null;
var semaphore = cacheimg ? new SemaphorManager(outFile, ctsHttp.Token) : null;
try
{
if (semaphore != null)
await semaphore.WaitAsync().ConfigureAwait(false);
#region cacheFiles
if (cacheimg)
{
if (cacheFiles.ContainsKey(md5key) || (AppInit.conf.multiaccess == false && File.Exists(outFile)))
{
httpContex.Response.Headers["X-Cache-Status"] = "HIT";
if (init.responseContentLength && cacheFiles.ContainsKey(md5key))
httpContex.Response.ContentLength = cacheFiles[md5key];
semaphore?.Release();
await httpContex.Response.SendFileAsync(outFile, ctsHttp.Token).ConfigureAwait(false);
return;
}
}
#endregion
var handler = Http.Handler(uri, proxyManager?.Get());
var client = FrendlyHttp.MessageClient(init.httpversion == 2 ? "http2proxyimg" : "proxyimg", handler);
var req = new HttpRequestMessage(HttpMethod.Get, uri)
{
Version = init.httpversion == 1 ? HttpVersion.Version11 : new Version(init.httpversion, 0)
};
foreach (var h in headers)
{
if (!req.Headers.TryAddWithoutValidation(h.name, h.val))
{
if (req.Content?.Headers != null)
req.Content.Headers.TryAddWithoutValidation(h.name, h.val);
}
}
using (HttpResponseMessage response = await client.SendAsync(req, ctsHttp.Token).ConfigureAwait(false))
{
if (response.StatusCode == HttpStatusCode.OK)
proxyManager?.Success();
else
proxyManager?.Refresh();
httpContex.Response.StatusCode = (int)response.StatusCode;
if (init.responseContentLength && response.Content?.Headers?.ContentLength > 0)
httpContex.Response.ContentLength = response.Content.Headers.ContentLength.Value;
if (response.StatusCode == HttpStatusCode.OK && cacheimg)
{
#region cache
httpContex.Response.Headers["X-Cache-Status"] = "MISS";
byte[] buffer = ArrayPool<byte>.Shared.Rent(PoolInvk.rentChunk);
try
{
int cacheLength = 0;
using (var cacheStream = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None, PoolInvk.bufferSize))
{
using (var responseStream = await response.Content.ReadAsStreamAsync(ctsHttp.Token).ConfigureAwait(false))
{
int bytesRead;
while ((bytesRead = await responseStream.ReadAsync(buffer, ctsHttp.Token).ConfigureAwait(false)) > 0)
{
cacheLength += bytesRead;
await cacheStream.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
await httpContex.Response.Body.WriteAsync(buffer, 0, bytesRead, ctsHttp.Token).ConfigureAwait(false);
}
}
}
if (!response.Content.Headers.ContentLength.HasValue || response.Content.Headers.ContentLength.Value == cacheLength)
{
if (AppInit.conf.multiaccess)
cacheFiles[md5key] = cacheLength;
}
else
{
File.Delete(outFile);
}
}
catch
{
File.Delete(outFile);
throw;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
#endregion
}
else
{
semaphore?.Release();
httpContex.Response.Headers["X-Cache-Status"] = "bypass";
await response.Content.CopyToAsync(httpContex.Response.Body, ctsHttp.Token).ConfigureAwait(false);
}
}
}
catch
{
proxyManager?.Refresh();
if (!string.IsNullOrEmpty(tmdb_ip))
httpContex.Response.Redirect(uri.Replace(tmdb_ip, "image.tmdb.org"));
else
httpContex.Response.Redirect(uri);
}
finally
{
if (semaphore != null)
semaphore.Release();
}
}
}
#endregion
}
}