550 lines
24 KiB
C#
550 lines
24 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Newtonsoft.Json.Linq;
|
|
using Shared;
|
|
using Shared.Engine;
|
|
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.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Lampac.Engine.Middlewares
|
|
{
|
|
public class ProxyCub
|
|
{
|
|
#region ProxyCub
|
|
static FileSystemWatcher fileWatcher;
|
|
|
|
static readonly ConcurrentDictionary<string, int> cacheFiles = new ();
|
|
|
|
public static int Stat_ContCacheFiles => cacheFiles.IsEmpty ? 0 : cacheFiles.Count;
|
|
|
|
static Timer cleanupTimer;
|
|
|
|
static ProxyCub()
|
|
{
|
|
Directory.CreateDirectory("cache/cub");
|
|
|
|
foreach (string path in Directory.EnumerateFiles("cache/cub", "*"))
|
|
{
|
|
using (var handle = File.OpenHandle(path))
|
|
cacheFiles.TryAdd(Path.GetFileName(path), (int)RandomAccess.GetLength(handle));
|
|
}
|
|
|
|
fileWatcher = new FileSystemWatcher
|
|
{
|
|
Path = "cache/cub",
|
|
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", "cub", md5fileName)))
|
|
cacheFiles.TryRemove(md5fileName, out _);
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
public ProxyCub(RequestDelegate next) { }
|
|
#endregion
|
|
|
|
async public Task InvokeAsync(HttpContext httpContext, IMemoryCache memoryCache)
|
|
{
|
|
using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContext.RequestAborted))
|
|
{
|
|
ctsHttp.CancelAfter(TimeSpan.FromSeconds(30));
|
|
|
|
var requestInfo = httpContext.Features.Get<RequestModel>();
|
|
var hybridCache = IHybridCache.Get(requestInfo);
|
|
|
|
var init = AppInit.conf.cub;
|
|
string domain = init.domain;
|
|
string path = httpContext.Request.Path.Value.Replace("/cub/", "", StringComparison.OrdinalIgnoreCase);
|
|
string query = httpContext.Request.QueryString.Value;
|
|
string uri = Regex.Match(path, "^[^/]+/(.*)", RegexOptions.IgnoreCase).Groups[1].Value + query;
|
|
|
|
if (!init.enable || domain == "ws")
|
|
{
|
|
httpContext.Response.Redirect($"https://{path}/{query}");
|
|
return;
|
|
}
|
|
|
|
if (path.Split(".")[0] is "geo" or "tmdb" or "tmapi" or "apitmdb" or "imagetmdb" or "cdn" or "ad" or "ws")
|
|
domain = $"{path.Split(".")[0]}.{domain}";
|
|
|
|
if (domain.StartsWith("geo", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string country = requestInfo.Country;
|
|
if (string.IsNullOrEmpty(country))
|
|
{
|
|
var ipify = await Http.Get<JObject>("https://api.ipify.org/?format=json");
|
|
if (ipify != null || !string.IsNullOrEmpty(ipify.Value<string>("ip")))
|
|
country = GeoIP2.Country(ipify.Value<string>("ip"));
|
|
}
|
|
|
|
await httpContext.Response.WriteAsync(country ?? "", ctsHttp.Token);
|
|
return;
|
|
}
|
|
|
|
#region checker
|
|
if (path.StartsWith("api/checker", StringComparison.OrdinalIgnoreCase) || uri.StartsWith("api/checker", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (HttpMethods.IsPost(httpContext.Request.Method))
|
|
{
|
|
if (httpContext.Request.ContentType != null &&
|
|
httpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
using (var reader = new StreamReader(httpContext.Request.Body, leaveOpen: true, bufferSize: PoolInvk.bufferSize))
|
|
{
|
|
string form = await reader.ReadToEndAsync();
|
|
|
|
var match = Regex.Match(form, @"(?:^|&)data=([^&]+)");
|
|
if (match.Success)
|
|
{
|
|
string dataValue = Uri.UnescapeDataString(match.Groups[1].Value);
|
|
await httpContext.Response.WriteAsync(dataValue, ctsHttp.Token);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
await httpContext.Response.WriteAsync("ok", ctsHttp.Token);
|
|
return;
|
|
}
|
|
#endregion
|
|
|
|
#region blacklist
|
|
if (uri.StartsWith("api/plugins/blacklist", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
httpContext.Response.ContentType = "application/json; charset=utf-8";
|
|
await httpContext.Response.WriteAsync("[]", ctsHttp.Token);
|
|
return;
|
|
}
|
|
#endregion
|
|
|
|
#region ads/log/metric
|
|
if (uri.StartsWith("api/metric/", StringComparison.OrdinalIgnoreCase) || uri.StartsWith("api/ad/stat", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
await httpContext.Response.WriteAsJsonAsync(new { secuses = true });
|
|
return;
|
|
}
|
|
|
|
if (uri.StartsWith("api/ad/vast", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
await httpContext.Response.WriteAsJsonAsync(new
|
|
{
|
|
secuses = true,
|
|
ad = new string[] { },
|
|
day_of_month = DateTime.Now.Day,
|
|
days_in_month = 31,
|
|
month = DateTime.Now.Month
|
|
});
|
|
return;
|
|
}
|
|
#endregion
|
|
|
|
var proxyManager = new ProxyManager("cub_api", init);
|
|
var proxy = proxyManager.Get();
|
|
|
|
bool isMedia = Regex.IsMatch(uri, "\\.(jpe?g|png|gif|webp|ico|svg|mp4|js|css)", RegexOptions.IgnoreCase);
|
|
|
|
if (0 >= init.cache_api || !HttpMethods.IsGet(httpContext.Request.Method) || isMedia ||
|
|
(path.Split(".")[0] is "imagetmdb" or "cdn" or "ad") ||
|
|
httpContext.Request.Headers.ContainsKey("token") || httpContext.Request.Headers.ContainsKey("profile"))
|
|
{
|
|
#region bypass or media cache
|
|
string md5key = CrypTo.md5($"{domain}:{uri}");
|
|
string outFile = Path.Combine("cache", "cub", md5key);
|
|
|
|
if (cacheFiles.ContainsKey(md5key))
|
|
{
|
|
httpContext.Response.Headers["X-Cache-Status"] = "HIT";
|
|
httpContext.Response.ContentType = getContentType(uri);
|
|
|
|
if (init.responseContentLength && cacheFiles.ContainsKey(md5key))
|
|
httpContext.Response.ContentLength = cacheFiles[md5key];
|
|
|
|
await httpContext.Response.SendFileAsync(outFile, ctsHttp.Token).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
var handler = new HttpClientHandler()
|
|
{
|
|
AutomaticDecompression = DecompressionMethods.None,
|
|
AllowAutoRedirect = true
|
|
};
|
|
|
|
handler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
|
|
|
|
if (proxy != null)
|
|
{
|
|
handler.UseProxy = true;
|
|
handler.Proxy = proxy;
|
|
}
|
|
else { handler.UseProxy = false; }
|
|
|
|
var client = FrendlyHttp.MessageClient("proxyRedirect", handler);
|
|
var request = CreateProxyHttpRequest(httpContext, new Uri($"{init.scheme}://{domain}/{uri}"), requestInfo, init.viewru && path.Split(".")[0] == "tmdb");
|
|
|
|
using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctsHttp.Token).ConfigureAwait(false))
|
|
{
|
|
if (init.cache_img > 0 && isMedia && HttpMethods.IsGet(httpContext.Request.Method) && AppInit.conf.mikrotik == false && response.StatusCode == HttpStatusCode.OK)
|
|
{
|
|
#region cache
|
|
httpContext.Response.ContentType = getContentType(uri);
|
|
httpContext.Response.Headers["X-Cache-Status"] = "MISS";
|
|
|
|
if (init.responseContentLength && response.Content?.Headers?.ContentLength > 0)
|
|
{
|
|
if (!AppInit.CompressionMimeTypes.Contains(httpContext.Response.ContentType))
|
|
httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value;
|
|
}
|
|
|
|
var semaphore = new SemaphorManager(outFile, ctsHttp.Token);
|
|
|
|
try
|
|
{
|
|
await semaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
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 httpContext.Response.Body.WriteAsync(buffer, 0, bytesRead, ctsHttp.Token).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!response.Content.Headers.ContentLength.HasValue || response.Content.Headers.ContentLength.Value == cacheLength)
|
|
{
|
|
cacheFiles[md5key] = cacheLength;
|
|
}
|
|
else
|
|
{
|
|
File.Delete(outFile);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
File.Delete(outFile);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
semaphore.Release();
|
|
}
|
|
#endregion
|
|
}
|
|
else
|
|
{
|
|
httpContext.Response.Headers["X-Cache-Status"] = "bypass";
|
|
await CopyProxyHttpResponse(httpContext, response, ctsHttp.Token).ConfigureAwait(false);
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
else
|
|
{
|
|
#region cache string
|
|
string memkey = $"cubproxy:key2:{domain}:{uri}";
|
|
(byte[] content, int statusCode, string contentType) cache = default;
|
|
|
|
var semaphore = new SemaphorManager(memkey, ctsHttp.Token);
|
|
|
|
try
|
|
{
|
|
await semaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
if (!hybridCache.TryGetValue(memkey, out cache, inmemory: false))
|
|
{
|
|
var headers = HeadersModel.Init();
|
|
|
|
if (!string.IsNullOrEmpty(requestInfo.Country))
|
|
headers.Add(new HeadersModel("cf-connecting-ip", requestInfo.IP));
|
|
|
|
if (path.Split(".")[0] == "tmdb")
|
|
{
|
|
if (init.viewru)
|
|
headers.Add(new HeadersModel("cookie", "viewru=1"));
|
|
|
|
headers.Add(new HeadersModel("user-agent", httpContext.Request.Headers.UserAgent.ToString()));
|
|
}
|
|
else
|
|
{
|
|
foreach (var header in httpContext.Request.Headers)
|
|
{
|
|
if (header.Key.ToLower() is "cookie" or "user-agent")
|
|
headers.Add(new HeadersModel(header.Key, header.Value.ToString()));
|
|
}
|
|
}
|
|
|
|
var result = await Http.BaseGet($"{init.scheme}://{domain}/{uri}", timeoutSeconds: 10, proxy: proxy, headers: headers, statusCodeOK: false, useDefaultHeaders: false).ConfigureAwait(false);
|
|
if (string.IsNullOrEmpty(result.content))
|
|
{
|
|
proxyManager.Refresh();
|
|
httpContext.Response.StatusCode = (int)result.response.StatusCode;
|
|
return;
|
|
}
|
|
|
|
cache.content = Encoding.UTF8.GetBytes(result.content);
|
|
cache.statusCode = (int)result.response.StatusCode;
|
|
cache.contentType = result.response.Content?.Headers?.ContentType?.ToString() ?? getContentType(uri);
|
|
|
|
if (domain.StartsWith("tmdb") || domain.StartsWith("tmapi") || domain.StartsWith("apitmdb"))
|
|
{
|
|
if (result.content == "{\"blocked\":true}")
|
|
{
|
|
var header = HeadersModel.Init(("localrequest", AppInit.rootPasswd));
|
|
string json = await Http.Get($"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/{uri}", timeoutSeconds: 5, headers: header).ConfigureAwait(false);
|
|
if (!string.IsNullOrEmpty(json))
|
|
{
|
|
cache.statusCode = 200;
|
|
cache.contentType = "application/json; charset=utf-8";
|
|
cache.content = Encoding.UTF8.GetBytes(json);
|
|
}
|
|
}
|
|
}
|
|
|
|
httpContext.Response.Headers["X-Cache-Status"] = "MISS";
|
|
|
|
if (cache.statusCode == 200)
|
|
{
|
|
proxyManager.Success();
|
|
hybridCache.Set(memkey, cache, DateTime.Now.AddMinutes(init.cache_api), inmemory: false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
httpContext.Response.Headers["X-Cache-Status"] = "HIT";
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
semaphore.Release();
|
|
}
|
|
|
|
if (init.responseContentLength && !AppInit.CompressionMimeTypes.Contains(cache.contentType))
|
|
httpContext.Response.ContentLength = cache.content.Length;
|
|
|
|
httpContext.Response.StatusCode = cache.statusCode;
|
|
httpContext.Response.ContentType = cache.contentType;
|
|
await httpContext.Response.Body.WriteAsync(cache.content, ctsHttp.Token).ConfigureAwait(false);
|
|
#endregion
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#region getContentType
|
|
static string getContentType(string uri)
|
|
{
|
|
return Path.GetExtension(uri).ToLowerInvariant() switch
|
|
{
|
|
".jpg" or ".jpeg" => "image/jpeg",
|
|
".png" => "image/png",
|
|
".gif" => "image/gif",
|
|
".webp" => "image/webp",
|
|
".ico" => "image/x-icon",
|
|
".svg" => "image/svg+xml",
|
|
".mp4" => "video/mp4",
|
|
".js" => "application/javascript",
|
|
".css" => "text/css",
|
|
_ => "application/octet-stream"
|
|
};
|
|
}
|
|
#endregion
|
|
|
|
#region CreateProxyHttpRequest
|
|
HttpRequestMessage CreateProxyHttpRequest(HttpContext context, Uri uri, RequestModel requestInfo, bool viewru)
|
|
{
|
|
var request = context.Request;
|
|
|
|
var requestMessage = new HttpRequestMessage();
|
|
|
|
var requestMethod = request.Method;
|
|
if (HttpMethods.IsPost(requestMethod))
|
|
{
|
|
var streamContent = new StreamContent(request.Body);
|
|
requestMessage.Content = streamContent;
|
|
}
|
|
|
|
if (viewru)
|
|
request.Headers["Cookie"] = "viewru=1";
|
|
|
|
if (!string.IsNullOrEmpty(requestInfo.Country))
|
|
request.Headers["Cf-Connecting-Ip"] = requestInfo.IP;
|
|
|
|
#region Headers
|
|
foreach (var header in request.Headers)
|
|
{
|
|
string key = header.Key;
|
|
|
|
if (key.Equals("host", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("origin", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("referer", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("content-disposition", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("accept-encoding", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
if (viewru && key.Equals("cookie", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
if (key.StartsWith("x-", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()))
|
|
{
|
|
if (requestMessage.Content?.Headers != null)
|
|
requestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
requestMessage.Headers.Host = uri.Authority;
|
|
requestMessage.RequestUri = uri;
|
|
requestMessage.Method = new HttpMethod(request.Method);
|
|
//requestMessage.Version = new Version(2, 0);
|
|
|
|
return requestMessage;
|
|
}
|
|
#endregion
|
|
|
|
#region CopyProxyHttpResponse
|
|
async Task CopyProxyHttpResponse(HttpContext context, HttpResponseMessage responseMessage, CancellationToken cancellationToken)
|
|
{
|
|
var response = context.Response;
|
|
response.StatusCode = (int)responseMessage.StatusCode;
|
|
|
|
#region responseContentLength
|
|
if (AppInit.conf.cub.responseContentLength && responseMessage.Content?.Headers?.ContentLength > 0)
|
|
{
|
|
IEnumerable<string> contentType = null;
|
|
if (responseMessage.Content?.Headers != null)
|
|
responseMessage.Content.Headers.TryGetValues("Content-Type", out contentType);
|
|
|
|
string type = contentType?.FirstOrDefault()?.ToLower();
|
|
|
|
if (string.IsNullOrEmpty(type) || !AppInit.CompressionMimeTypes.Contains(type))
|
|
response.ContentLength = responseMessage.Content.Headers.ContentLength;
|
|
}
|
|
#endregion
|
|
|
|
#region UpdateHeaders
|
|
void UpdateHeaders(HttpHeaders headers)
|
|
{
|
|
foreach (var header in headers)
|
|
{
|
|
string key = header.Key;
|
|
|
|
if (key.Equals("server", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("etag", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("connection", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("content-security-policy", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("content-disposition", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("content-length", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
if (key.StartsWith("x-", StringComparison.OrdinalIgnoreCase) ||
|
|
key.StartsWith("alt-", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
if (key.StartsWith("access-control", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
var values = header.Value;
|
|
|
|
using (var e = values.GetEnumerator())
|
|
{
|
|
if (!e.MoveNext())
|
|
continue;
|
|
|
|
var first = e.Current;
|
|
|
|
response.Headers[key] = e.MoveNext()
|
|
? string.Join("; ", values)
|
|
: first;
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
UpdateHeaders(responseMessage.Headers);
|
|
UpdateHeaders(responseMessage.Content.Headers);
|
|
|
|
using (var responseStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
|
|
{
|
|
if (response.Body == null)
|
|
throw new ArgumentNullException("destination");
|
|
|
|
if (!responseStream.CanRead && !responseStream.CanWrite)
|
|
throw new ObjectDisposedException("ObjectDisposed_StreamClosed");
|
|
|
|
if (!response.Body.CanRead && !response.Body.CanWrite)
|
|
throw new ObjectDisposedException("ObjectDisposed_StreamClosed");
|
|
|
|
if (!responseStream.CanRead)
|
|
throw new NotSupportedException("NotSupported_UnreadableStream");
|
|
|
|
if (!response.Body.CanWrite)
|
|
throw new NotSupportedException("NotSupported_UnwritableStream");
|
|
|
|
byte[] buffer = ArrayPool<byte>.Shared.Rent(PoolInvk.rentChunk);
|
|
|
|
try
|
|
{
|
|
int bytesRead;
|
|
|
|
while ((bytesRead = await responseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
|
await response.Body.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
}
|