lampac/BaseModule/Controllers/CorseuController.cs
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

453 lines
17 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Playwright;
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Base;
using Shared.PlaywrightCore;
using System;
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.Controllers
{
public class CorseuController : BaseController
{
#region Routes
[HttpGet]
[AllowAnonymous]
[Route("/corseu/{token}/{*url}")]
public Task<IActionResult> Get(string token, string url)
{
return ExecuteAsync(new CorseuRequest
{
url = url + HttpContext.Request.QueryString.Value,
auth_token = token
});
}
[HttpGet]
[AllowAnonymous]
[Route("/corseu")]
public Task<IActionResult> Get(string auth_token, string method, string url, string data, string headers, string browser, int? httpversion, int? timeout, string encoding, bool? defaultHeaders, bool? autoredirect, string proxy, string proxy_name, bool? headersOnly)
{
return ExecuteAsync(new CorseuRequest
{
url = url,
method = method,
data = data,
browser = browser,
httpversion = httpversion,
timeout = timeout,
encoding = encoding,
defaultHeaders = defaultHeaders,
autoredirect = autoredirect,
proxy = proxy,
proxy_name = proxy_name,
headersOnly = headersOnly,
auth_token = auth_token,
headers = ParseHeaders(headers)
});
}
[HttpPost]
[AllowAnonymous]
[Route("/corseu")]
async public Task<IActionResult> Post()
{
try
{
using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8, leaveOpen: true))
{
string body = await reader.ReadToEndAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(body))
return BadRequest("Empty body");
var model = JsonConvert.DeserializeObject<CorseuRequest>(body);
if (model == null)
return BadRequest("Invalid body");
return await ExecuteAsync(model);
}
}
catch (JsonException)
{
return BadRequest("Invalid JSON");
}
}
#endregion
#region Execute
async Task<IActionResult> ExecuteAsync(CorseuRequest model)
{
var init = AppInit.conf.corseu;
if (init?.tokens == null || init.tokens.Length == 0)
return StatusCode((int)HttpStatusCode.Forbidden);
if (string.IsNullOrEmpty(model?.auth_token) || !init.tokens.Contains(model.auth_token))
return StatusCode((int)HttpStatusCode.Forbidden);
if (string.IsNullOrWhiteSpace(model?.url))
return BadRequest("url is empty");
InvkEvent.CorseuRequest(model);
string method = string.IsNullOrWhiteSpace(model.method) ? "GET" : model.method.ToUpperInvariant();
string browser = string.IsNullOrWhiteSpace(model.browser) ? "http" : model.browser.ToLowerInvariant();
var headers = model.headers != null
? new Dictionary<string, string>(model.headers, StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
bool useDefaultHeaders = model.defaultHeaders ?? true;
bool autoRedirect = model.autoredirect ?? true;
bool headersOnly = model.headersOnly ?? false;
int timeout = model.timeout.HasValue && model.timeout.Value > 5 ? model.timeout.Value : 15;
int httpVersion = model.httpversion ?? 1;
#region rules
if (init?.rules != null)
{
foreach (var rule in init.rules)
{
if (rule?.headers == null || rule.headers.Count == 0)
continue;
if (string.IsNullOrEmpty(rule.method) || string.IsNullOrEmpty(rule.url))
continue;
if (!string.Equals(rule.method, method, StringComparison.OrdinalIgnoreCase))
continue;
if (!Regex.IsMatch(model.url, rule.url, RegexOptions.IgnoreCase))
continue;
var ruleHeaders = new Dictionary<string, string>(rule.headers, StringComparer.OrdinalIgnoreCase);
if (rule.replace)
{
headers = ruleHeaders;
}
else
{
foreach (var pair in ruleHeaders)
headers[pair.Key] = pair.Value;
}
}
}
#endregion
string contentType = null;
if (headers.TryGetValue("content-type", out string ct))
{
contentType = ct;
headers.Remove("content-type");
}
if (headers.ContainsKey("content-length"))
headers.Remove("content-length");
if (browser is "chromium" or "playwright")
return await SendWithChromiumAsync(method, model.url, model.data, headers, contentType, timeout, autoRedirect, headersOnly, model.proxy, model.proxy_name);
return await SendWithHttpClientAsync(method, model.url, model.data, headers, contentType, timeout, httpVersion, useDefaultHeaders, autoRedirect, headersOnly, model.proxy, model.proxy_name, model.encoding);
}
#endregion
#region HttpClient
async Task<IActionResult> SendWithHttpClientAsync(
string method, string url, string data, Dictionary<string, string> headers,
string contentType, int timeout, int httpVersion, bool useDefaultHeaders, bool autoRedirect, bool headersOnly, string encodingName,
string proxyValue, string proxyName)
{
var proxyManager = CreateProxy(url, proxyValue, proxyName);
try
{
var handler = Http.Handler(url, proxyManager.Get());
handler.AllowAutoRedirect = autoRedirect;
var client = FrendlyHttp.MessageClient(httpVersion == 2 ? "http2" : "base", handler);
using (var request = new HttpRequestMessage(new HttpMethod(method), url))
{
request.Version = httpVersion == 2 ? HttpVersion.Version20 : HttpVersion.Version11;
if (!string.IsNullOrEmpty(data))
{
var encoding = string.IsNullOrEmpty(encodingName)
? Encoding.UTF8
: Encoding.GetEncoding(encodingName);
var content = new StringContent(data, encoding);
if (!string.IsNullOrEmpty(contentType))
content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
request.Content = content;
}
var headersModel = headers.Count > 0 ? HeadersModel.Init(headers) : null;
Http.DefaultRequestHeaders(url, request, null, null, headersModel, useDefaultHeaders);
if (InvkEvent.IsCorseuHttpRequest())
InvkEvent.CorseuHttpRequest(method, url, request);
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(HttpContext.RequestAborted))
{
cts.CancelAfter(TimeSpan.FromSeconds(Math.Max(5, timeout)));
using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token).ConfigureAwait(false))
{
proxyManager.Success();
await CopyResponseAsync(response, headersOnly).ConfigureAwait(false);
return new EmptyResult();
}
}
}
}
catch (OperationCanceledException)
{
proxyManager.Refresh();
return StatusCode((int)HttpStatusCode.RequestTimeout);
}
catch (Exception ex)
{
proxyManager.Refresh();
return StatusCode((int)HttpStatusCode.BadGateway, ex.Message);
}
}
#endregion
#region Chromium
async Task<IActionResult> SendWithChromiumAsync(
string method, string url, string data, Dictionary<string, string> headers,
string contentType, int timeout, bool autoRedirect, bool headersOnly,
string proxyValue, string proxyName)
{
var proxyManager = CreateProxy(url, proxyValue, proxyName);
var proxy = proxyManager.BaseGet();
try
{
if (PlaywrightBrowser.Status == PlaywrightStatus.disabled)
return StatusCode((int)HttpStatusCode.BadGateway, "PlaywrightStatus disabled");
var contextHeaders = new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase);
var requestHeaders = new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrEmpty(contentType))
{
if (!requestHeaders.ContainsKey("content-type"))
{
requestHeaders["content-type"] = contentType;
contextHeaders["content-type"] = contentType;
}
}
var contextOptions = new APIRequestNewContextOptions
{
IgnoreHTTPSErrors = true,
ExtraHTTPHeaders = Http.NormalizeHeaders(contextHeaders),
Timeout = timeout * 1000
};
if (requestHeaders.TryGetValue("user-agent", out string _useragent))
contextOptions.UserAgent = _useragent;
var requestOptions = new APIRequestContextOptions
{
Method = method,
Headers = requestHeaders,
Timeout = timeout * 1000
};
if (!string.IsNullOrEmpty(data))
requestOptions.DataString = data;
if (!autoRedirect)
requestOptions.MaxRedirects = 0;
if (proxy.proxy != null)
{
contextOptions.Proxy = new Proxy
{
Server = proxy.data.ip,
Username = proxy.data.username,
Password = proxy.data.password
};
}
if (InvkEvent.IsCorseuPlaywrightRequest())
InvkEvent.CorseuPlaywrightRequest(method, url, contextOptions, requestOptions);
await using (var requestContext = await Chromium.playwright.APIRequest.NewContextAsync(contextOptions).ConfigureAwait(false))
{
var response = await requestContext.FetchAsync(url, requestOptions).ConfigureAwait(false);
try
{
HttpContext.Response.StatusCode = response.Status;
foreach (var header in response.HeadersArray)
{
var headerName = header.Name.ToLowerInvariant();
if (ShouldSkipHeader(headerName))
continue;
if (headerName == "content-type")
HttpContext.Response.ContentType = header.Value;
HttpContext.Response.Headers[header.Name] = header.Value;
}
if (headersOnly)
{
proxyManager.Success();
await HttpContext.Response.CompleteAsync().ConfigureAwait(false);
return new EmptyResult();
}
var body = await response.BodyAsync().ConfigureAwait(false);
if (body?.Length > 0)
await HttpContext.Response.Body.WriteAsync(body, 0, body.Length, HttpContext.RequestAborted).ConfigureAwait(false);
proxyManager.Success();
return new EmptyResult();
}
finally
{
await response.DisposeAsync().ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
proxyManager.Refresh();
return StatusCode((int)HttpStatusCode.RequestTimeout);
}
catch (Exception ex)
{
proxyManager.Refresh();
return StatusCode((int)HttpStatusCode.BadGateway, ex.Message);
}
}
#endregion
#region Helpers
Dictionary<string, string> ParseHeaders(string headers)
{
try
{
if (!string.IsNullOrEmpty(headers))
return JsonConvert.DeserializeObject<Dictionary<string, string>>(headers);
}
catch { }
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
ProxyManager CreateProxy(string url, string proxyValue, string proxyName)
{
var model = new BaseSettings()
{
plugin = $"corseu:{Regex.Match(url, "https?://([^/]+)")}"
};
if (!string.IsNullOrEmpty(proxyValue))
{
model.proxy = new ProxySettings();
model.proxy.list = [proxyValue];
}
else if (!string.IsNullOrEmpty(proxyName))
{
if (AppInit.conf.globalproxy != null)
{
var settings = AppInit.conf.globalproxy.FirstOrDefault(i => i.name == proxyName);
if (settings?.list != null && settings.list.Length > 0)
model.proxy = settings;
}
}
if (model.proxy != null)
model.useproxy = true;
return new ProxyManager("corseu", model);
}
async Task CopyResponseAsync(HttpResponseMessage response, bool headersOnly)
{
var httpResponse = HttpContext.Response;
httpResponse.StatusCode = (int)response.StatusCode;
foreach (var header in response.Headers)
{
if (ShouldSkipHeader(header.Key))
continue;
httpResponse.Headers[header.Key] = string.Join(", ", header.Value);
}
foreach (var header in response.Content.Headers)
{
if (string.Equals(header.Key, "Content-Type", StringComparison.OrdinalIgnoreCase))
{
httpResponse.ContentType = response.Content.Headers.ContentType?.ToString();
continue;
}
if (ShouldSkipHeader(header.Key))
continue;
httpResponse.Headers[header.Key] = string.Join(", ", header.Value);
}
if (headersOnly)
{
await httpResponse.CompleteAsync().ConfigureAwait(false);
return;
}
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
await responseStream.CopyToAsync(httpResponse.Body, HttpContext.RequestAborted).ConfigureAwait(false);
}
bool ShouldSkipHeader(string header)
{
string key = header.ToLowerInvariant();
return key switch
{
"content-length" => true,
"transfer-encoding" => true,
"connection" => true,
"keep-alive" => true,
"content-disposition" => true,
"content-encoding" => true,
"content-security-policy" => true,
"vary" => true,
"alt-svc" => true,
_ when key.StartsWith("access-control") => true,
_ when key.StartsWith("x-") => true,
_ => false
};
}
#endregion
}
}