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 Get(string token, string url) { return ExecuteAsync(new CorseuRequest { url = url + HttpContext.Request.QueryString.Value, auth_token = token }); } [HttpGet] [AllowAnonymous] [Route("/corseu")] public Task 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 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(body); if (model == null) return BadRequest("Invalid body"); return await ExecuteAsync(model); } } catch (JsonException) { return BadRequest("Invalid JSON"); } } #endregion #region Execute async Task 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(model.headers, StringComparer.OrdinalIgnoreCase) : new Dictionary(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(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 SendWithHttpClientAsync( string method, string url, string data, Dictionary 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 SendWithChromiumAsync( string method, string url, string data, Dictionary 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(headers, StringComparer.OrdinalIgnoreCase); var requestHeaders = new Dictionary(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 ParseHeaders(string headers) { try { if (!string.IsNullOrEmpty(headers)) return JsonConvert.DeserializeObject>(headers); } catch { } return new Dictionary(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 } }