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 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(); 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 _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, "([^<]+)"); 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 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(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 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.Shared.Rent(PoolInvk.Rent(buffering.rent)); try { int bytesRead = await responseStream.ReadAsync(chunkBuffer, 0, chunkBuffer.Length, context.RequestAborted); if (bytesRead == 0) { ArrayPool.Shared.Return(chunkBuffer); break; } await channel.Writer.WriteAsync((chunkBuffer, bytesRead), context.RequestAborted); } catch { ArrayPool.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.Shared.Return(chunkBuffer); } } }, context.RequestAborted, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default ).Unwrap(); await Task.WhenAll(readTask, writeTask).ConfigureAwait(false); #endregion } else { byte[] buffer = ArrayPool.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.Shared.Return(buffer); } } } } #endregion } }