849 lines
40 KiB
C#
849 lines
40 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using Shared;
|
|
using Shared.Engine;
|
|
using Shared.Models;
|
|
using Shared.Models.Proxy;
|
|
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.Channels;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Lampac.Engine.Middlewares
|
|
{
|
|
public class ProxyAPI
|
|
{
|
|
static readonly HttpClientHandler baseHandler = new HttpClientHandler()
|
|
{
|
|
AutomaticDecompression = DecompressionMethods.None,
|
|
AllowAutoRedirect = false,
|
|
UseProxy = false,
|
|
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
|
|
};
|
|
|
|
#region ProxyAPI
|
|
static readonly FileSystemWatcher fileWatcher;
|
|
|
|
static readonly ConcurrentDictionary<string, long> cacheFiles = new();
|
|
|
|
public static int Stat_ContCacheFiles => cacheFiles.IsEmpty ? 0 : cacheFiles.Count;
|
|
|
|
static ProxyAPI()
|
|
{
|
|
Directory.CreateDirectory("cache/hls");
|
|
|
|
foreach (string path in Directory.EnumerateFiles("cache/hls", "*"))
|
|
{
|
|
using (var handle = File.OpenHandle(path))
|
|
cacheFiles.TryAdd(Path.GetFileName(path), (int)RandomAccess.GetLength(handle));
|
|
}
|
|
|
|
fileWatcher = new FileSystemWatcher
|
|
{
|
|
Path = "cache/hls",
|
|
NotifyFilter = NotifyFilters.FileName,
|
|
EnableRaisingEvents = true
|
|
};
|
|
|
|
fileWatcher.Deleted += (s, e) => { cacheFiles.TryRemove(e.Name, out _); };
|
|
}
|
|
|
|
public ProxyAPI(RequestDelegate next) { }
|
|
#endregion
|
|
|
|
async public Task InvokeAsync(HttpContext httpContext)
|
|
{
|
|
var init = AppInit.conf.serverproxy;
|
|
var requestInfo = httpContext.Features.Get<RequestModel>();
|
|
|
|
string servPath = httpContext.Request.Path.Value.Replace("/proxy/", "", StringComparison.OrdinalIgnoreCase).Replace("/proxy-dash/", "", StringComparison.OrdinalIgnoreCase);
|
|
string servUri = servPath + httpContext.Request.QueryString.Value;
|
|
|
|
#region tmdb proxy
|
|
if (servUri.Contains(".themoviedb.org", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
httpContext.Response.Redirect($"/tmdb/api/{Regex.Match(servUri.Replace("://", ":/_/").Replace("//", "/").Replace(":/_/", "://"), "https?://[^/]+/(.*)").Groups[1].Value}");
|
|
return;
|
|
}
|
|
else if (servUri.Contains(".tmdb.org", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
httpContext.Response.Redirect($"/tmdb/img/{Regex.Match(servUri.Replace("://", ":/_/").Replace("//", "/").Replace(":/_/", "://"), "https?://[^/]+/(.*)").Groups[1].Value}");
|
|
return;
|
|
}
|
|
#endregion
|
|
|
|
#region decryptLink
|
|
var decryptLink = ProxyLink.Decrypt(httpContext.Request.Path.Value.StartsWith("/proxy-dash/", StringComparison.OrdinalIgnoreCase) ? servPath.Split("/")[0] : servPath, requestInfo.IP);
|
|
|
|
if (init.encrypt || decryptLink?.uri != null || httpContext.Request.Path.Value.StartsWith("/proxy-dash/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
servUri = decryptLink?.uri;
|
|
}
|
|
else
|
|
{
|
|
if (!init.enable)
|
|
{
|
|
httpContext.Response.StatusCode = 403;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(servUri) || !servUri.StartsWith("http"))
|
|
{
|
|
httpContext.Response.StatusCode = 404;
|
|
return;
|
|
}
|
|
|
|
if (decryptLink == null)
|
|
decryptLink = new ProxyLinkModel(requestInfo.IP, null, null, servUri);
|
|
#endregion
|
|
|
|
if (init.showOrigUri)
|
|
{
|
|
//Console.WriteLine("PX-Orig: " + decryptLink.uri);
|
|
httpContext.Response.Headers["PX-Orig"] = decryptLink.uri;
|
|
}
|
|
|
|
#region proxyHandler
|
|
HttpClientHandler proxyHandler = null;
|
|
|
|
if (decryptLink.proxy != null)
|
|
{
|
|
proxyHandler = new HttpClientHandler()
|
|
{
|
|
AutomaticDecompression = DecompressionMethods.None,
|
|
AllowAutoRedirect = false
|
|
};
|
|
|
|
proxyHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
|
|
proxyHandler.UseProxy = true;
|
|
proxyHandler.Proxy = decryptLink.proxy;
|
|
}
|
|
#endregion
|
|
|
|
#region cacheFiles
|
|
(string uriKey, string contentType) cacheStream = InvkEvent.IsProxyApiCacheStream()
|
|
? InvkEvent.ProxyApiCacheStream(httpContext, decryptLink)
|
|
: default;
|
|
|
|
if (cacheStream.uriKey != null && init.showOrigUri)
|
|
httpContext.Response.Headers["PX-CacheStream"] = cacheStream.uriKey;
|
|
|
|
if (cacheStream.uriKey != null)
|
|
{
|
|
string md5key = CrypTo.md5(cacheStream.uriKey);
|
|
|
|
if (cacheFiles.ContainsKey(md5key))
|
|
{
|
|
httpContext.Response.Headers["PX-Cache"] = "HIT";
|
|
httpContext.Response.Headers["accept-ranges"] = "bytes";
|
|
httpContext.Response.ContentType = cacheStream.contentType ?? "application/octet-stream";
|
|
|
|
long cacheLength = cacheFiles[md5key];
|
|
string cachePath = $"cache/hls/{md5key}";
|
|
|
|
if (RangeHeaderValue.TryParse(httpContext.Request.Headers["Range"], out var range))
|
|
{
|
|
var rangeItem = range.Ranges.FirstOrDefault();
|
|
if (rangeItem != null)
|
|
{
|
|
long start = rangeItem.From ?? 0;
|
|
long end = rangeItem.To ?? (cacheLength - 1);
|
|
|
|
if (start >= cacheLength)
|
|
{
|
|
httpContext.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
|
|
httpContext.Response.Headers["content-range"] = $"bytes */{cacheLength}";
|
|
return;
|
|
}
|
|
|
|
if (end >= cacheLength)
|
|
end = cacheLength - 1;
|
|
|
|
long length = end - start + 1;
|
|
|
|
httpContext.Response.StatusCode = StatusCodes.Status206PartialContent;
|
|
httpContext.Response.Headers["content-range"] = $"bytes {start}-{end}/{cacheLength}";
|
|
|
|
if (init.responseContentLength)
|
|
httpContext.Response.ContentLength = length;
|
|
|
|
await httpContext.Response.SendFileAsync(cachePath, start, length, httpContext.RequestAborted).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (init.responseContentLength)
|
|
httpContext.Response.ContentLength = cacheLength;
|
|
|
|
await httpContext.Response.SendFileAsync(cachePath, httpContext.RequestAborted).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
if (httpContext.Request.Path.Value.StartsWith("/proxy-dash/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
#region DASH
|
|
var uri = new Uri($"{servUri}{Regex.Replace(httpContext.Request.Path.Value, "^/[^/]+/[^/]+/", "", RegexOptions.IgnoreCase)}{httpContext.Request.QueryString.Value}");
|
|
|
|
var client = FrendlyHttp.MessageClient("proxy", proxyHandler ?? baseHandler);
|
|
|
|
using (var request = CreateProxyHttpRequest(decryptLink.plugin, httpContext, decryptLink.headers, uri, true))
|
|
{
|
|
if (InvkEvent.IsProxyApiCreateHttpRequest())
|
|
await InvkEvent.ProxyApiCreateHttpRequest(decryptLink.plugin, httpContext.Request, decryptLink.headers, uri, true, request).ConfigureAwait(false);
|
|
|
|
using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContext.RequestAborted))
|
|
{
|
|
ctsHttp.CancelAfter(TimeSpan.FromSeconds(30));
|
|
|
|
using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctsHttp.Token).ConfigureAwait(false))
|
|
{
|
|
httpContext.Response.Headers["PX-Cache"] = "BYPASS";
|
|
await CopyProxyHttpResponse(httpContext, response, cacheStream.uriKey).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
else
|
|
{
|
|
#region Video OR
|
|
if (servUri.Contains(" or "))
|
|
{
|
|
string[] links = servUri.Split(" or ");
|
|
servUri = links[0].Trim();
|
|
|
|
try
|
|
{
|
|
var hdlr = new HttpClientHandler()
|
|
{
|
|
AllowAutoRedirect = true
|
|
};
|
|
|
|
hdlr.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
|
|
|
|
if (decryptLink.proxy != null)
|
|
{
|
|
hdlr.UseProxy = true;
|
|
hdlr.Proxy = decryptLink.proxy;
|
|
}
|
|
else { hdlr.UseProxy = false; }
|
|
|
|
var clientor = FrendlyHttp.MessageClient("base", hdlr);
|
|
|
|
using (var requestor = CreateProxyHttpRequest(decryptLink.plugin, httpContext, decryptLink.headers, new Uri(servUri), true))
|
|
{
|
|
if (InvkEvent.IsProxyApiCreateHttpRequest())
|
|
await InvkEvent.ProxyApiCreateHttpRequest(decryptLink.plugin, httpContext.Request, decryptLink.headers, new Uri(servUri), true, requestor).ConfigureAwait(false);
|
|
|
|
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(7)))
|
|
{
|
|
using (var response = await clientor.SendAsync(requestor, HttpCompletionOption.ResponseHeadersRead, cts.Token).ConfigureAwait(false))
|
|
{
|
|
if ((int)response.StatusCode is 200 or 206) { }
|
|
else
|
|
servUri = links[1].Trim();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
servUri = links[1].Trim();
|
|
}
|
|
|
|
servUri = servUri.Split(" ")[0].Trim();
|
|
decryptLink.uri = servUri;
|
|
|
|
if (init.showOrigUri)
|
|
httpContext.Response.Headers["PX-Set-Orig"] = decryptLink.uri;
|
|
}
|
|
#endregion
|
|
|
|
var client = FrendlyHttp.MessageClient("proxy", proxyHandler ?? baseHandler);
|
|
|
|
bool ismedia = Regex.IsMatch(httpContext.Request.Path.Value, "\\.(m3u|ts|m4s|mp4|mkv|aacp|srt|vtt)", RegexOptions.IgnoreCase);
|
|
using (var request = CreateProxyHttpRequest(decryptLink.plugin, httpContext, decryptLink.headers, new Uri(servUri), ismedia))
|
|
{
|
|
if (InvkEvent.IsProxyApiCreateHttpRequest())
|
|
await InvkEvent.ProxyApiCreateHttpRequest(decryptLink.plugin, httpContext.Request, decryptLink.headers, new Uri(servUri), ismedia, request).ConfigureAwait(false);
|
|
|
|
using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContext.RequestAborted))
|
|
{
|
|
ctsHttp.CancelAfter(TimeSpan.FromSeconds(30));
|
|
|
|
using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctsHttp.Token).ConfigureAwait(false))
|
|
{
|
|
if ((int)response.StatusCode is 301 or 302 or 303 or 0 || response.Headers.Location != null)
|
|
{
|
|
httpContext.Response.Redirect(validArgs($"{AppInit.Host(httpContext)}/proxy/{ProxyLink.Encrypt(response.Headers.Location.AbsoluteUri, decryptLink)}", httpContext));
|
|
return;
|
|
}
|
|
|
|
IEnumerable<string> _contentType = null;
|
|
if (response.Content?.Headers != null)
|
|
response.Content.Headers.TryGetValues("Content-Type", out _contentType);
|
|
|
|
string contentType = _contentType?.FirstOrDefault()?.ToLower();
|
|
|
|
bool ists = httpContext.Request.Path.Value.EndsWith(".ts", StringComparison.OrdinalIgnoreCase) || httpContext.Request.Path.Value.EndsWith(".m4s", StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (!ists && (httpContext.Request.Path.Value.Contains(".m3u", StringComparison.OrdinalIgnoreCase) || (contentType != null && contentType is "application/x-mpegurl" or "application/vnd.apple.mpegurl" or "text/plain")))
|
|
{
|
|
#region m3u8/txt
|
|
using (HttpContent content = response.Content)
|
|
{
|
|
if (response.StatusCode == HttpStatusCode.OK ||
|
|
response.StatusCode == HttpStatusCode.PartialContent ||
|
|
response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable)
|
|
{
|
|
if (response.Content?.Headers?.ContentLength > init.maxlength_m3u)
|
|
{
|
|
httpContext.Response.StatusCode = 503;
|
|
httpContext.Response.ContentType = "text/plain";
|
|
await httpContext.Response.WriteAsync("bigfile", ctsHttp.Token).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
string m3u8 = await content.ReadAsStringAsync(ctsHttp.Token).ConfigureAwait(false);
|
|
if (m3u8 == null)
|
|
{
|
|
httpContext.Response.StatusCode = 503;
|
|
await httpContext.Response.WriteAsync("error array m3u8", ctsHttp.Token).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
byte[] hlsArray = editm3u(m3u8, httpContext, decryptLink);
|
|
|
|
httpContext.Response.ContentType = contentType ?? "application/vnd.apple.mpegurl";
|
|
httpContext.Response.StatusCode = (int)response.StatusCode;
|
|
|
|
if (response.Headers.AcceptRanges != null)
|
|
httpContext.Response.Headers["accept-ranges"] = "bytes";
|
|
|
|
if (httpContext.Response.StatusCode is 206 or 416)
|
|
{
|
|
var contentRange = response.Content?.Headers?.ContentRange;
|
|
if (contentRange != null)
|
|
{
|
|
httpContext.Response.Headers["content-range"] = contentRange.ToString();
|
|
}
|
|
else
|
|
{
|
|
if (httpContext.Response.StatusCode == 206)
|
|
httpContext.Response.Headers["content-range"] = $"bytes 0-{hlsArray.Length - 1}/{hlsArray.Length}";
|
|
|
|
if (httpContext.Response.StatusCode == 416)
|
|
httpContext.Response.Headers["content-range"] = $"bytes */{hlsArray.Length}";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (init.responseContentLength && !AppInit.CompressionMimeTypes.Contains(httpContext.Response.ContentType))
|
|
httpContext.Response.ContentLength = hlsArray.Length;
|
|
}
|
|
|
|
await httpContext.Response.Body.WriteAsync(hlsArray, ctsHttp.Token).ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
// проксируем ошибку
|
|
await CopyProxyHttpResponse(httpContext, response, null).ConfigureAwait(false);
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
else if (httpContext.Request.Path.Value.Contains(".mpd", StringComparison.OrdinalIgnoreCase) || (contentType != null && contentType == "application/dash+xml"))
|
|
{
|
|
#region dash
|
|
using (HttpContent content = response.Content)
|
|
{
|
|
if (response.StatusCode == HttpStatusCode.OK ||
|
|
response.StatusCode == HttpStatusCode.PartialContent ||
|
|
response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable)
|
|
{
|
|
if (response.Content?.Headers?.ContentLength > init.maxlength_m3u)
|
|
{
|
|
httpContext.Response.StatusCode = 503;
|
|
httpContext.Response.ContentType = "text/plain";
|
|
await httpContext.Response.WriteAsync("bigfile", ctsHttp.Token).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
string mpd = await content.ReadAsStringAsync(ctsHttp.Token).ConfigureAwait(false);
|
|
if (mpd == null)
|
|
{
|
|
httpContext.Response.StatusCode = 503;
|
|
await httpContext.Response.WriteAsync("error array mpd", ctsHttp.Token).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
var m = Regex.Match(mpd, "<BaseURL>([^<]+)</BaseURL>");
|
|
while (m.Success)
|
|
{
|
|
string baseURL = m.Groups[1].Value;
|
|
mpd = Regex.Replace(mpd, baseURL, $"{AppInit.Host(httpContext)}/proxy-dash/{ProxyLink.Encrypt(baseURL, decryptLink, forceMd5: true)}/");
|
|
m = m.NextMatch();
|
|
}
|
|
|
|
byte[] mpdArray = Encoding.UTF8.GetBytes(mpd);
|
|
|
|
httpContext.Response.ContentType = contentType ?? "application/dash+xml";
|
|
httpContext.Response.StatusCode = (int)response.StatusCode;
|
|
|
|
if (response.Headers.AcceptRanges != null)
|
|
httpContext.Response.Headers["accept-ranges"] = "bytes";
|
|
|
|
if (httpContext.Response.StatusCode is 206 or 416)
|
|
{
|
|
var contentRange = response.Content.Headers.ContentRange;
|
|
if (contentRange != null)
|
|
{
|
|
httpContext.Response.Headers["content-range"] = contentRange.ToString();
|
|
}
|
|
else
|
|
{
|
|
if (httpContext.Response.StatusCode == 206)
|
|
httpContext.Response.Headers["content-range"] = $"bytes 0-{mpdArray.Length - 1}/{mpdArray.Length}";
|
|
|
|
if (httpContext.Response.StatusCode == 416)
|
|
httpContext.Response.Headers["content-range"] = $"bytes */{mpdArray.Length}";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (init.responseContentLength && !AppInit.CompressionMimeTypes.Contains(httpContext.Response.ContentType))
|
|
httpContext.Response.ContentLength = mpdArray.Length;
|
|
}
|
|
|
|
await httpContext.Response.Body.WriteAsync(mpdArray, ctsHttp.Token).ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
// проксируем ошибку
|
|
await CopyProxyHttpResponse(httpContext, response, null).ConfigureAwait(false);
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
else
|
|
{
|
|
httpContext.Response.Headers["PX-Cache"] = cacheStream.uriKey != null ? "MISS" : "BYPASS";
|
|
await CopyProxyHttpResponse(httpContext, response, cacheStream.uriKey).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#region validArgs
|
|
static string validArgs(string uri, HttpContext httpContext)
|
|
{
|
|
if (AppInit.conf.accsdb.enable && !AppInit.conf.serverproxy.encrypt)
|
|
return AccsDbInvk.Args(uri, httpContext);
|
|
|
|
return uri;
|
|
}
|
|
#endregion
|
|
|
|
#region editm3u
|
|
static byte[] editm3u(string _m3u8, HttpContext httpContext, ProxyLinkModel decryptLink)
|
|
{
|
|
string proxyhost = $"{AppInit.Host(httpContext)}/proxy";
|
|
string m3u8 = Regex.Replace(_m3u8, "(https?://[^\n\r\"\\# ]+)", m =>
|
|
{
|
|
return validArgs($"{proxyhost}/{ProxyLink.Encrypt(m.Groups[1].Value, decryptLink)}", httpContext);
|
|
});
|
|
|
|
string hlshost = Regex.Match(decryptLink.uri, "(https?://[^/]+)/").Groups[1].Value;
|
|
string hlspatch = Regex.Match(decryptLink.uri, "(https?://[^\n\r]+/)([^/]+)$").Groups[1].Value;
|
|
if (string.IsNullOrEmpty(hlspatch) && decryptLink.uri.EndsWith("/"))
|
|
hlspatch = decryptLink.uri;
|
|
|
|
m3u8 = Regex.Replace(m3u8, "([\n\r])([^\n\r]+)", m =>
|
|
{
|
|
string uri = m.Groups[2].Value;
|
|
|
|
if (uri.Contains("#") || uri.Contains("\"") || uri.StartsWith("http"))
|
|
return m.Groups[0].Value;
|
|
|
|
if (uri.StartsWith("//"))
|
|
{
|
|
uri = "https:" + uri;
|
|
}
|
|
else if (uri.StartsWith("/"))
|
|
{
|
|
uri = hlshost + uri;
|
|
}
|
|
else if (uri.StartsWith("./"))
|
|
{
|
|
uri = hlspatch + uri.Substring(2);
|
|
}
|
|
else
|
|
{
|
|
uri = hlspatch + uri;
|
|
}
|
|
|
|
return m.Groups[1].Value + validArgs($"{proxyhost}/{ProxyLink.Encrypt(uri, decryptLink)}", httpContext);
|
|
});
|
|
|
|
m3u8 = Regex.Replace(m3u8, "(URI=\")([^\"]+)", m =>
|
|
{
|
|
string uri = m.Groups[2].Value;
|
|
|
|
if (uri.Contains("\"") || uri.StartsWith("http"))
|
|
return m.Groups[0].Value;
|
|
|
|
if (uri.StartsWith("//"))
|
|
{
|
|
uri = "https:" + uri;
|
|
}
|
|
else if (uri.StartsWith("/"))
|
|
{
|
|
uri = hlshost + uri;
|
|
}
|
|
else if (uri.StartsWith("./"))
|
|
{
|
|
uri = hlspatch + uri.Substring(2);
|
|
}
|
|
else
|
|
{
|
|
uri = hlspatch + uri;
|
|
}
|
|
|
|
return m.Groups[1].Value + validArgs($"{proxyhost}/{ProxyLink.Encrypt(uri, decryptLink)}", httpContext);
|
|
});
|
|
|
|
return Encoding.UTF8.GetBytes(m3u8);
|
|
}
|
|
#endregion
|
|
|
|
|
|
#region CreateProxyHttpRequest
|
|
static HttpRequestMessage CreateProxyHttpRequest(string plugin, HttpContext context, List<HeadersModel> headers, Uri uri, bool ismedia)
|
|
{
|
|
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;
|
|
}
|
|
|
|
#region Headers
|
|
{
|
|
var addHeaders = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["accept"] = ["*/*"],
|
|
["accept-language"] = ["ru-RU,ru;q=0.9,uk-UA;q=0.8,uk;q=0.7,en-US;q=0.6,en;q=0.5"]
|
|
};
|
|
|
|
if (headers != null && headers.Count > 0)
|
|
{
|
|
foreach (var h in headers)
|
|
addHeaders[h.name] = [h.val];
|
|
}
|
|
|
|
if (ismedia)
|
|
{
|
|
if (request.Headers.TryGetValue("range", out var range))
|
|
addHeaders["range"] = range.ToArray();
|
|
}
|
|
else
|
|
{
|
|
foreach (var header in request.Headers)
|
|
{
|
|
string key = header.Key;
|
|
|
|
if (key.Equals("host", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("origin", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("user-agent", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("referer", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("content-disposition", StringComparison.OrdinalIgnoreCase) ||
|
|
key.Equals("accept-encoding", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
if (key.StartsWith("x-"))
|
|
continue;
|
|
|
|
if (key == "range")
|
|
{
|
|
addHeaders[key] = header.Value.ToArray();
|
|
continue;
|
|
}
|
|
|
|
addHeaders.TryAdd(key, header.Value.ToArray());
|
|
}
|
|
}
|
|
|
|
foreach (var h in Http.defaultFullHeaders)
|
|
addHeaders[h.Key] = [h.Value];
|
|
|
|
foreach (var h in Http.NormalizeHeaders(addHeaders))
|
|
{
|
|
if (!requestMessage.Headers.TryAddWithoutValidation(h.Key, h.Value))
|
|
{
|
|
if (requestMessage.Content?.Headers != null)
|
|
requestMessage.Content.Headers.TryAddWithoutValidation(h.Key, h.Value);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
requestMessage.Headers.Host = uri.Authority;
|
|
requestMessage.RequestUri = uri;
|
|
requestMessage.Method = new HttpMethod(request.Method);
|
|
|
|
//requestMessage.Version = new Version(2, 0);
|
|
//Console.WriteLine(JsonConvert.SerializeObject(requestMessage.Headers, Formatting.Indented));
|
|
|
|
return requestMessage;
|
|
}
|
|
#endregion
|
|
|
|
#region CopyProxyHttpResponse
|
|
async Task CopyProxyHttpResponse(HttpContext context, HttpResponseMessage responseMessage, string uriKeyFileCache)
|
|
{
|
|
var response = context.Response;
|
|
response.StatusCode = (int)responseMessage.StatusCode;
|
|
|
|
#region responseContentLength
|
|
if (AppInit.conf.serverproxy.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()?.ToLowerInvariant();
|
|
|
|
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) ||
|
|
key.Equals("set-cookie", 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;
|
|
|
|
string 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(context.RequestAborted).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");
|
|
|
|
var buffering = AppInit.conf.serverproxy?.buffering;
|
|
|
|
if (buffering?.enable == true &&
|
|
((!string.IsNullOrEmpty(buffering.pattern) && Regex.IsMatch(context.Request.Path.Value, buffering.pattern, RegexOptions.IgnoreCase)) ||
|
|
context.Request.Path.Value.EndsWith(".mp4") || context.Request.Path.Value.EndsWith(".mkv") || responseMessage.Content?.Headers?.ContentLength > 40_000000))
|
|
{
|
|
#region buffering
|
|
var channel = Channel.CreateBounded<(byte[] Buffer, int Length)>(new BoundedChannelOptions(capacity: buffering.length)
|
|
{
|
|
FullMode = BoundedChannelFullMode.Wait,
|
|
SingleWriter = true,
|
|
SingleReader = true
|
|
});
|
|
|
|
var readTask = Task.Factory.StartNew(async () =>
|
|
{
|
|
try
|
|
{
|
|
while (!context.RequestAborted.IsCancellationRequested)
|
|
{
|
|
byte[] chunkBuffer = ArrayPool<byte>.Shared.Rent(PoolInvk.Rent(buffering.rent));
|
|
|
|
try
|
|
{
|
|
int bytesRead = await responseStream.ReadAsync(chunkBuffer, 0, chunkBuffer.Length, context.RequestAborted);
|
|
|
|
if (bytesRead == 0)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(chunkBuffer);
|
|
break;
|
|
}
|
|
|
|
await channel.Writer.WriteAsync((chunkBuffer, bytesRead), context.RequestAborted);
|
|
}
|
|
catch
|
|
{
|
|
ArrayPool<byte>.Shared.Return(chunkBuffer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
channel.Writer.Complete();
|
|
}
|
|
},
|
|
context.RequestAborted, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default
|
|
).Unwrap();
|
|
|
|
var writeTask = Task.Factory.StartNew(async () =>
|
|
{
|
|
bool reqAborted = false;
|
|
|
|
await foreach (var (chunkBuffer, length) in channel.Reader.ReadAllAsync(context.RequestAborted))
|
|
{
|
|
try
|
|
{
|
|
if (reqAborted == false)
|
|
await response.Body.WriteAsync(chunkBuffer, 0, length, context.RequestAborted);
|
|
}
|
|
catch
|
|
{
|
|
reqAborted = true;
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(chunkBuffer);
|
|
}
|
|
}
|
|
},
|
|
context.RequestAborted, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default
|
|
).Unwrap();
|
|
|
|
await Task.WhenAll(readTask, writeTask).ConfigureAwait(false);
|
|
#endregion
|
|
}
|
|
else
|
|
{
|
|
byte[] buffer = ArrayPool<byte>.Shared.Rent(PoolInvk.rentChunk);
|
|
|
|
try
|
|
{
|
|
if (uriKeyFileCache != null &&
|
|
responseMessage.Content.Headers.ContentLength.HasValue &&
|
|
AppInit.conf.serverproxy.maxlength_ts >= responseMessage.Content.Headers.ContentLength)
|
|
{
|
|
#region cache
|
|
string md5key = CrypTo.md5(uriKeyFileCache);
|
|
string targetFile = $"cache/hls/{md5key}";
|
|
var semaphore = new SemaphorManager(targetFile, context.RequestAborted);
|
|
|
|
try
|
|
{
|
|
await semaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
int cacheLength = 0;
|
|
|
|
using (var fileStream = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, PoolInvk.bufferSize))
|
|
{
|
|
int bytesRead;
|
|
while ((bytesRead = await responseStream.ReadAsync(buffer, context.RequestAborted).ConfigureAwait(false)) != 0)
|
|
{
|
|
cacheLength += bytesRead;
|
|
await fileStream.WriteAsync(buffer, 0, bytesRead, context.RequestAborted).ConfigureAwait(false);
|
|
await response.Body.WriteAsync(buffer, 0, bytesRead, context.RequestAborted).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
if (!responseMessage.Content.Headers.ContentLength.HasValue || responseMessage.Content.Headers.ContentLength.Value == cacheLength)
|
|
{
|
|
cacheFiles[md5key] = cacheLength;
|
|
}
|
|
else
|
|
{
|
|
File.Delete(targetFile);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
File.Delete(targetFile);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
semaphore.Release();
|
|
}
|
|
#endregion
|
|
}
|
|
else
|
|
{
|
|
#region bypass
|
|
int bytesRead;
|
|
|
|
while ((bytesRead = await responseStream.ReadAsync(buffer, context.RequestAborted).ConfigureAwait(false)) != 0)
|
|
await response.Body.WriteAsync(buffer, 0, bytesRead, context.RequestAborted).ConfigureAwait(false);
|
|
#endregion
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
}
|