diff --git a/Controllers/BiliController.cs b/Controllers/BiliController.cs new file mode 100644 index 0000000..f2c35e5 --- /dev/null +++ b/Controllers/BiliController.cs @@ -0,0 +1,136 @@ +using MegghysAPI.Modules; +using Microsoft.AspNetCore.Mvc; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace MegghysAPI.Controllers +{ + [Route("api/bili")] + [ApiController] + public class BiliController : MControllerBase + { + [HttpGet("test-cap")] + public async Task TestCap([FromQuery] string gt, [FromQuery] string challenge) + { + var solution = await CapResolver.SolveGeetestAsync("https://www.bilibili.com", gt, challenge); + return Ok(ResponseOK(solution)); + } + + [HttpGet("resolve-grisk-id")] + public async Task ResolveGriskId([FromQuery] string v_voucher, [FromQuery] string? csrf) + { + if (string.IsNullOrEmpty(v_voucher)) + { + return BadRequest(ResponseBadRequest("v_voucher 不能为空")); + } + + try + { + // 1. 调用B站注册接口获取 gt, challenge, token + var (gt, challenge, token) = await RegisterFromVVoucher(v_voucher, csrf); + + // 2. 使用 CapResolver 解决验证码, 获取 validate + var solution = await CapResolver.SolveGeetestAsync("https://live.bilibili.com", gt, challenge); + if (solution == null) + { + throw new Exception("从 CapSolver 获取 validate 失败"); + } + string validate = solution.Validate; + + // 3. 调用B站验证接口获取 grisk_id + var griskId = await ValidateToGriskId(challenge, token, validate, csrf); + + return Ok(ResponseOK(new { grisk_id = griskId })); + } + catch (Exception ex) + { + Logs.Error($"ResolveGriskId 失败: {ex.Message}"); + return StatusCode(500, ResponseInternalError(ex.Message)); + } + } + public const string FirstHost = "https://i.ukamnads.icu/api-bilibili/"; + public const string SecondHost = "https://api.bilibili.com/"; + private static readonly string[] PreferredHosts = { FirstHost, SecondHost }; + + private async Task<(string gt, string challenge, string token)> RegisterFromVVoucher(string vVoucher, string? csrf) + { + var path = "x/gaia-vgate/v1/register"; + var payload = new Dictionary { { "v_voucher", vVoucher } }; + if (!string.IsNullOrEmpty(csrf)) + { + payload["csrf"] = csrf; + } + + var root = await PostFormWithFailoverAsync(path, payload, "注册接口"); + var data = root["data"] ?? throw new Exception("注册接口返回缺少 data 字段"); + + var geetest = data["geetest"] ?? throw new Exception("注册返回缺少 geetest 字段"); + var gt = (string?)geetest["gt"] ?? throw new Exception("注册返回缺少 gt 字段"); + var challenge = (string?)geetest["challenge"] ?? throw new Exception("注册返回缺少 challenge 字段"); + var apiToken = (string?)data["token"] ?? throw new Exception("注册返回缺少 token 字段"); + + return (gt, challenge, apiToken); + } + + private async Task ValidateToGriskId(string challenge, string token, string validate, string? csrf) + { + var path = "x/gaia-vgate/v1/validate"; + var seccode = $"{validate}|jordan"; + var payload = new Dictionary + { + { "challenge", challenge }, + { "token", token }, + { "validate", validate }, + { "seccode", seccode }, + }; + if (!string.IsNullOrEmpty(csrf)) + { + payload["csrf"] = csrf; + } + + var root = await PostFormWithFailoverAsync(path, payload, "验证接口"); + var data = root["data"] ?? throw new Exception("验证接口返回缺少 data 字段"); + + var isValid = (int?)data["is_valid"]; + var griskId = (string?)data["grisk_id"]; + + if (isValid != 1 || string.IsNullOrEmpty(griskId)) + { + throw new Exception($"验证失败或缺少 grisk_id: is_valid={isValid} grisk_id={griskId}"); + } + + return griskId; + } + + private async Task PostFormWithFailoverAsync(string path, Dictionary payload, string operation) + { + Exception? lastException = null; + foreach (var host in PreferredHosts) + { + try + { + using var content = new FormUrlEncodedContent(payload); + var response = await Utils._client.PostAsync($"{host}{path}", content); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + return JsonNode.Parse(json) ?? throw new Exception($"{operation} 返回内容解析失败"); + } + catch (Exception ex) + { + lastException = ex; + if (host == FirstHost) + { + Logs.Warn($"{operation} 使用 FirstHost 调用失败: {ex.Message}"); + } + else + { + Logs.Error($"{operation} 使用 SecondHost 调用失败: {ex.Message}"); + } + } + } + + throw new Exception($"{operation} 调用失败: {lastException?.Message}", lastException); + } + } +} diff --git a/Controllers/MyController.cs b/Controllers/MyController.cs new file mode 100644 index 0000000..f83c7ed --- /dev/null +++ b/Controllers/MyController.cs @@ -0,0 +1,12 @@ +using MegghysAPI.Modules; +using Microsoft.AspNetCore.Mvc; + +namespace MegghysAPI.Controllers +{ + [Route("api/my")] + [ApiController] + public class MyController : MControllerBase + { + + } +} diff --git a/Core/FileManager.cs b/Core/FileManager.cs index e655efa..9d51e8a 100644 --- a/Core/FileManager.cs +++ b/Core/FileManager.cs @@ -109,6 +109,10 @@ namespace MegghysAPI.Core .WithIncludeUserMetadata(includeUserMetadata)); var fileList = new List(); + if(fileList.Count == 0) + { + return []; + } await foreach (var item in result) { fileList.Add(item); diff --git a/Data/siamese.onnx b/Data/siamese.onnx new file mode 100644 index 0000000..9225870 Binary files /dev/null and b/Data/siamese.onnx differ diff --git a/Data/yolov11n_captcha.onnx b/Data/yolov11n_captcha.onnx new file mode 100644 index 0000000..8070d53 Binary files /dev/null and b/Data/yolov11n_captcha.onnx differ diff --git a/MegghysAPI.csproj b/MegghysAPI.csproj index c2ddea6..ca67322 100644 --- a/MegghysAPI.csproj +++ b/MegghysAPI.csproj @@ -10,8 +10,11 @@ + + + diff --git a/Modules/BiliCaptchaResolver.cs b/Modules/BiliCaptchaResolver.cs new file mode 100644 index 0000000..20f3584 --- /dev/null +++ b/Modules/BiliCaptchaResolver.cs @@ -0,0 +1,400 @@ + +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Security.Cryptography; +using System.Linq; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using CaptchaBreaker; +using MegghysAPI; + +namespace MegghysAPI.Modules +{ + public enum VerifyType + { + Click, + Slide + } + + /// + /// Geetest 点选验证码解析器(C# 版)。 + /// - 对应 Rust 版本的 Click 结构实现:注册、拉取参数、计算 key、生成 w、验证、刷新。 + /// - 依赖 CaptchaBreaker.ChineseClick0(ONNX 模型)。 + public sealed class BiliCaptchaResolver : IDisposable + { + private readonly VerifyType _verifyType; + private readonly ChineseClick0 _cb; + + private BiliCaptchaResolver(string yoloModelPath, string siameseModelPath, VerifyType verifyType = VerifyType.Click) + { + _verifyType = verifyType; + _cb = new ChineseClick0(yoloModelPath, siameseModelPath); + } + + public static async Task CreateAsync(string yoloModelName = "yolov11n_captcha.onnx", string siameseModelName = "siamese.onnx", VerifyType verifyType = VerifyType.Click) + { + var yoloPath = await EnsureModelExistsAsync(yoloModelName); + var siamesePath = await EnsureModelExistsAsync(siameseModelName); + return new BiliCaptchaResolver(yoloPath, siamesePath, verifyType); + } + + private static async Task EnsureModelExistsAsync(string modelName) + { + var modelPath = Path.Combine(Datas.DataPath, modelName); + if (File.Exists(modelPath)) + { + return modelPath; + } + + var url = $"https://oss.suki.club/things/{modelName}"; + var client = Utils._client; + var modelBytes = await client.GetByteArrayAsync(url); + await File.WriteAllBytesAsync(modelPath, modelBytes); + return modelPath; + } + + public void Dispose() + { + _cb?.Dispose(); + } + // ========== Public APIs (Async) ========== + + /// + /// 从业务 URL 注册,返回 (gt, challenge)。 + /// + public async Task<(string gt, string challenge)> RegisterTestAsync(string url, string? proxy = null) + { + var http = await ProxyManager.GetHttpClientAsync(proxy); + var text = await http.GetStringAsync(url); + var root = JsonNode.Parse(text); + var geetest = root?["data"]?["geetest"] ?? throw new Exception("缺少 data.geetest"); + var gt = (string?)geetest["gt"] ?? throw new Exception("缺少 gt"); + var challenge = (string?)geetest["challenge"] ?? throw new Exception("缺少 challenge"); + return (gt, challenge); + } + + /// + /// 获取初始 c/s 参数,对应 Rust 版本的 get_c_s 方法。 + /// + public async Task<(byte[] c, string s)> GetCSAsync(string gt, string challenge, string? proxy = null) + { + var url = "http://api.geevisit.com/get.php"; + var callback = "geetest_1717911889779"; + var query = new Dictionary + { + ["gt"] = gt, + ["challenge"] = challenge, + ["callback"] = callback, + }; + + var http = await ProxyManager.GetHttpClientAsync(proxy); + var full = BuildUrl(url, query); + var jsonp = await http.GetStringAsync(full); + var json = UnwrapJsonp(jsonp, callback); + + var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data"); + + // c + var cList = data["c"]?.AsArray().Select(v => (byte)v!.GetValue()).ToArray() ?? []; + var s = (string?)data["s"] ?? throw new Exception("缺少 s"); + + return (cList, s); + } + + /// + /// 获取验证类型,对应 Rust 版本的 get_type 方法。 + /// + public async Task GetTypeAsync(string gt, string challenge, string? proxy = null) + { + var url = "http://api.geevisit.com/gettype.php"; + var callback = "geetest_1717934072177"; + var query = new Dictionary + { + ["gt"] = gt, + ["challenge"] = challenge, + ["callback"] = callback, + }; + + var http = await ProxyManager.GetHttpClientAsync(proxy); + var full = BuildUrl(url, query); + var jsonp = await http.GetStringAsync(full); + var json = UnwrapJsonp(jsonp, callback); + + var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data"); + return (string?)data["type"] ?? throw new Exception("缺少 type"); + } + + /// + /// 获取 new c/s/args(图片地址)。 + /// + public async Task<(byte[] c, string s, string picUrl)> GetNewCSArgsAsync(string gt, string challenge, string? proxy = null) + { + var url = "http://api.geevisit.com/get.php"; + var callback = "geetest_1717911889779"; + var query = new Dictionary + { + ["gt"] = gt, + ["challenge"] = challenge, + ["is_next"] = "true", + ["offline"] = "false", + ["isPC"] = "true", + ["callback"] = callback, + ["type"] = _verifyType == VerifyType.Click ? "click" : "slide" + }; + + var http = await ProxyManager.GetHttpClientAsync(proxy); + var full = BuildUrl(url, query); + var jsonp = await http.GetStringAsync(full); + var json = UnwrapJsonp(jsonp, callback); + + var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data"); + + // c + var cList = data["c"]?.AsArray().Select(v => (byte)v!.GetValue()).ToArray() ?? []; + + var s = (string?)data["s"] ?? throw new Exception("缺少 s"); + var staticServers = data["static_servers"]?.AsArray() ?? throw new Exception("缺少 static_servers"); + if (staticServers.Count == 0) throw new Exception("static_servers里面为空"); + var staticServer = (string?)staticServers[0] ?? throw new Exception("static_servers[0] 不是字符串"); + var pic = (string?)data["pic"] ?? throw new Exception("缺少 pic"); + var picUrl = $"https://{staticServer}{pic.TrimStart('/')}"; + + return (cList, s, picUrl); + } + + /// + /// 提交验证,可带 w。 + /// + public async Task<(string result, string validate)> VerifyAsync(string gt, string challenge, string? w = null, string? proxy = null) + { + var url = "http://api.geevisit.com/ajax.php"; + var callback = "geetest_1717918222610"; + var query = new Dictionary + { + ["gt"] = gt, + ["challenge"] = challenge, + ["callback"] = callback, + }; + if (!string.IsNullOrEmpty(w)) query["w"] = w!; + + var http = await ProxyManager.GetHttpClientAsync(proxy); + var full = BuildUrl(url, query); + var jsonp = await http.GetStringAsync(full); + var json = UnwrapJsonp(jsonp, callback); + + var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data"); + var result = (string?)data["result"] ?? throw new Exception("缺少 result"); + var validate = (string?)data["validate"] ?? throw new Exception("缺少 validate"); + return (result, validate); + } + + /// + /// 刷新获取新的图片地址。 + /// + public async Task RefreshAsync(string gt, string challenge, string? proxy = null) + { + var url = "http://api.geevisit.com/refresh.php"; + var callback = "geetest_1717918222610"; + var query = new Dictionary + { + ["gt"] = gt, + ["challenge"] = challenge, + ["callback"] = callback, + }; + + var http = await ProxyManager.GetHttpClientAsync(proxy); + var full = BuildUrl(url, query); + var jsonp = await http.GetStringAsync(full); + var json = UnwrapJsonp(jsonp, callback); + + var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data"); + var servers = data["image_servers"]?.AsArray() ?? throw new Exception("缺少 image_servers"); + if (servers.Count == 0) throw new Exception("image_servers里面为空"); + var imageServer = (string?)servers[0] ?? throw new Exception("image_servers[0] 不是字符串"); + var pic = (string?)data["pic"] ?? throw new Exception("缺少 pic"); + return $"https://{imageServer}{pic.TrimStart('/')}"; + } + + /// + /// 计算 key(坐标串),会下载图片并通过 ONNX 模型推理。 + /// + public async Task CalculateKeyAsync(string picUrl, string? proxy = null) + { + var http = await ProxyManager.GetHttpClientAsync(proxy); + var bytes = await http.GetByteArrayAsync(picUrl); + var raw = Image.Load(bytes); + + // 保障尺寸不超过 384 + List points; + if (raw.Width > 384 || raw.Height > 384) + { + var scale = Math.Min(384f / raw.Width, 384f / raw.Height); + var w = (int)MathF.Ceiling(raw.Width * scale); + var h = (int)MathF.Ceiling(raw.Height * scale); + var resized = raw.Clone(ctx => ctx.Resize(w, h)); + points = _cb.Run(resized); + } + else + { + points = _cb.Run(raw); + } + var list = new List(points.Count); + foreach (var p in points) + { + // 与 Rust 版本保持一致的缩放与取整 + var x = MathF.Round(p.X / 333.375f * 100f * 100f); + var y = MathF.Round(p.Y / 333.375f * 100f * 100f); + list.Add($"{x}_{y}"); + } + return string.Join(",", list); + } + + /// + /// 生成 w。注意:目前使用占位实现,请按需替换为真实算法。 + /// + public string GenerateW(string key, string gt, string challenge, ReadOnlySpan c, string s) + { + return ClickCalculate(key, gt, challenge, c, s); + } + + /// + /// 完整流程测试:注册 -> get_c_s -> get_type -> 拉取 c/s/args -> 计算 key -> 生成 w -> 提交 -> 返回 validate。 + /// 按照原始 Rust 版本流程顺序,sleep 保证总耗时 >= 2s。 + /// + public async Task TestAsync(string registerUrl, string? proxy = null, CancellationToken ct = default) + { + var (gt, challenge) = await RegisterTestAsync(registerUrl, proxy); + var sw = System.Diagnostics.Stopwatch.StartNew(); + + // 按照原始 Rust test 流程:先调用 get_c_s,然后 get_type + var (_c, _s) = await GetCSAsync(gt, challenge, proxy); + var _type = await GetTypeAsync(gt, challenge, proxy); + + await Task.Delay(new Random().Next(500, 1000), ct); + var v = await VerifyAsync(gt, challenge); + + + var (c, s, args) = await GetNewCSArgsAsync(gt, challenge, proxy); + var key = await CalculateKeyAsync(args, proxy); + var w = GenerateW(key, gt, challenge, c, s); + + // 至少 2 秒 + var remain = 2000 - sw.ElapsedMilliseconds; + if (remain > 0) + { + await Task.Delay((int)remain, ct); + } + + var (_, validate) = await VerifyAsync(gt, challenge, w, proxy); + return validate; + } + + /// + /// 简单匹配(不注册):直接使用已知 gt, challenge,按照原始流程顺序。 + /// + public async Task SimpleMatchAsync(string gt, string challenge, string? proxy = null, CancellationToken ct = default) + { + var sw = System.Diagnostics.Stopwatch.StartNew(); + + // 按照原始 Rust simple_match 流程:先调用 get_c_s,然后 get_type + var (_, _) = await GetCSAsync(gt, challenge, proxy); + var __ = await GetTypeAsync(gt, challenge, proxy); + + var (c, s, args) = await GetNewCSArgsAsync(gt, challenge, proxy); + var key = await CalculateKeyAsync(args, proxy); + var w = GenerateW(key, gt, challenge, c, s); + + var remain = 2000 - sw.ElapsedMilliseconds; + if (remain > 0) await Task.Delay((int)remain, ct); + + var (_, validate) = await VerifyAsync(gt, challenge, w, proxy); + return validate; + } + + /// + /// 带自动重试的简单匹配,按照原始流程顺序。 + /// + public async Task SimpleMatchRetryAsync(string gt, string challenge, string? proxy = null, CancellationToken ct = default) + { + // 按照原始 Rust simple_match_retry 流程:先调用 get_c_s,然后 get_type + var (_, _) = await GetCSAsync(gt, challenge, proxy); + var __ = await GetTypeAsync(gt, challenge, proxy); + + var (c, s, args) = await GetNewCSArgsAsync(gt, challenge, proxy); + var res = await TryOnceAsync(gt, challenge, c, s, args, proxy, ct); + if (res.success) return res.validate!; + + while (!ct.IsCancellationRequested) + { + var nextArgs = await RefreshAsync(gt, challenge, proxy); + res = await TryOnceAsync(gt, challenge, c, s, nextArgs, proxy, ct); + if (res.success) return res.validate!; + } + + throw new OperationCanceledException("已取消"); + } + + // ========== Private Helpers ========== + + private async Task<(bool success, string? validate)> TryOnceAsync( + string gt, string challenge, byte[] c, string s, string args, string? proxy, CancellationToken ct) + { + var start = System.Diagnostics.Stopwatch.StartNew(); + try + { + var key = await CalculateKeyAsync(args, proxy); + var w = GenerateW(key, gt, challenge, c, s); + var remain = 2000 - start.ElapsedMilliseconds; + if (remain > 0) await Task.Delay((int)remain, ct); + + var (_, validate) = await VerifyAsync(gt, challenge, w, proxy); + return (true, validate); + } + catch + { + return (false, null); + } + } + + + private static string BuildUrl(string baseUrl, IReadOnlyDictionary query) + { + var ub = new UriBuilder(baseUrl); + var sb = new StringBuilder(); + if (!string.IsNullOrEmpty(ub.Query)) + { + sb.Append(ub.Query.TrimStart('?')); + if (sb.Length > 0) sb.Append('&'); + } + bool first = true; + foreach (var kv in query) + { + if (!first) sb.Append('&'); + first = false; + sb.Append(Uri.EscapeDataString(kv.Key)); + sb.Append('='); + sb.Append(Uri.EscapeDataString(kv.Value)); + } + ub.Query = sb.ToString(); + return ub.Uri.ToString(); + } + + private static string UnwrapJsonp(string jsonp, string callback) + { + var prefix = callback + "("; + if (!jsonp.StartsWith(prefix)) throw new Exception("前缀错误"); + if (!jsonp.EndsWith(")")) throw new Exception("后缀错误"); + return jsonp.Substring(prefix.Length, jsonp.Length - prefix.Length - 1); + } + + private static string ClickCalculate(string key, string gt, string challenge, ReadOnlySpan c, string s) + { + _ = c; + _ = s; + return GeetestCrypto.ClickCalculate(key, gt, challenge); + } + } +} diff --git a/Modules/CapResolver.cs b/Modules/CapResolver.cs new file mode 100644 index 0000000..6690073 --- /dev/null +++ b/Modules/CapResolver.cs @@ -0,0 +1,161 @@ +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace MegghysAPI.Modules +{ + public static class CapResolver + { + private const string ApiKey = "CAP-5537BB057BE85FC11F04BB5BB6E80E73AD44279056AC22D0E70664B24964C3D9"; + private const string CreateTaskUrl = "https://api.capsolver.com/createTask"; + private const string GetTaskResultUrl = "https://api.capsolver.com/getTaskResult"; + private static readonly HttpClient HttpClient = new HttpClient(); + + private class CreateTaskRequest + { + [JsonPropertyName("clientKey")] + public string ClientKey { get; set; } + + [JsonPropertyName("task")] + public object Task { get; set; } + } + + private class GeetestTask + { + [JsonPropertyName("type")] + public string Type { get; set; } = "GeeTestTaskProxyLess"; + + [JsonPropertyName("websiteURL")] + public string WebsiteURL { get; set; } + + [JsonPropertyName("gt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Gt { get; set; } + + [JsonPropertyName("challenge")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Challenge { get; set; } + + [JsonPropertyName("captchaId")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string CaptchaId { get; set; } + + [JsonPropertyName("geetestApiServerSubdomain")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string GeetestApiServerSubdomain { get; set; } + } + + private class CreateTaskResponse + { + [JsonPropertyName("errorId")] + public int ErrorId { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("taskId")] + public string TaskId { get; set; } + } + + private class GetTaskResultRequest + { + [JsonPropertyName("clientKey")] + public string ClientKey { get; set; } + + [JsonPropertyName("taskId")] + public string TaskId { get; set; } + } + + private class GetTaskResultResponse + { + [JsonPropertyName("errorId")] + public int ErrorId { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("solution")] + public GeetestSolution Solution { get; set; } + } + public class GeetestSolution + { + [JsonPropertyName("challenge")] + public string Challenge { get; set; } + [JsonPropertyName("validate")] + public string Validate { get; set; } + } + + public static async Task SolveGeetestAsync(string websiteUrl, string gt = null, string challenge = null, string captchaId = null, string apiServerSubdomain = null) + { + var geetestTask = new GeetestTask + { + WebsiteURL = websiteUrl, + Gt = gt, + Challenge = challenge, + CaptchaId = captchaId, + GeetestApiServerSubdomain = apiServerSubdomain + }; + + var createTaskRequest = new CreateTaskRequest + { + ClientKey = ApiKey, + Task = geetestTask + }; + + var response = await HttpClient.PostAsJsonAsync(CreateTaskUrl, createTaskRequest); + if (!response.IsSuccessStatusCode) + { + // 可以添加日志记录 + return null; + } + + var createTaskResponse = await response.Content.ReadFromJsonAsync(); + if (createTaskResponse == null || createTaskResponse.ErrorId != 0 || string.IsNullOrEmpty(createTaskResponse.TaskId)) + { + // 可以添加日志记录 + return null; + } + + var getTaskResultRequest = new GetTaskResultRequest + { + ClientKey = ApiKey, + TaskId = createTaskResponse.TaskId + }; + + await Task.Delay(3000); + + while (true) + { + await Task.Delay(1000); + + var resultResponse = await HttpClient.PostAsJsonAsync(GetTaskResultUrl, getTaskResultRequest); + if (!resultResponse.IsSuccessStatusCode) + { + // 可以添加日志记录 + continue; + } + + var getTaskResultResponse = await resultResponse.Content.ReadFromJsonAsync(); + if (getTaskResultResponse == null) + { + continue; + } + + if (getTaskResultResponse.Status == "ready") + { + return getTaskResultResponse.Solution; + } + + if (getTaskResultResponse.Status == "failed" || getTaskResultResponse.ErrorId != 0) + { + // 可以添加日志记录 + return null; + } + } + } + } +} diff --git a/Modules/CaptchaClick.cs b/Modules/CaptchaClick.cs new file mode 100644 index 0000000..2374988 --- /dev/null +++ b/Modules/CaptchaClick.cs @@ -0,0 +1,453 @@ +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System.Numerics; +using System.Drawing; +using SessionOptions = Microsoft.ML.OnnxRuntime.SessionOptions; +using PointF = SixLabors.ImageSharp.PointF; + +namespace CaptchaBreaker +{ + public sealed class ChineseClick0 : IDisposable + { + private const int Canvas = 384; + private const int Patch = 96; + private const float ConfThreshold = 0.5f; + private const float SplitY = 344f; + + private readonly InferenceSession _yolo; + private readonly InferenceSession _siamese; + + public ChineseClick0(string yoloModelPath, string siameseModelPath, SessionOptions? options = null) + { + if (!File.Exists(yoloModelPath)) throw new FileNotFoundException("YOLO 模型未找到", yoloModelPath); + if (!File.Exists(siameseModelPath)) throw new FileNotFoundException("Siamese 模型未找到", siameseModelPath); + + options ??= new SessionOptions(); // 默认 CPU EP + _yolo = new InferenceSession(yoloModelPath, options); + _siamese = new InferenceSession(siameseModelPath, options); + } + + public List Run(string imagePath) + { + using var img = Image.Load(imagePath); + return Run(img); + } + + public List Run(Image raw) + { + if (raw.Width > Canvas || raw.Height > Canvas) + throw new InvalidOperationException("不能输入大于384长宽的图片!"); + + using var image = PreprocessToCanvas(raw); + + // 1. YOLO 检测 + var boxes = Detect(image); + + // 2. 分离答案框与问题框,并按 x 从左到右排序 + var (ans, ques) = SplitBoxes(boxes); + + if (ans.Count == 0 || ques.Count == 0) + return new List(); + + // 3. 裁切并缩放为 96x96,按 [ans..., ques...] 组成批次 + var (batch, dims) = CropAndResizeBatch(image, ans, ques); + + // 4. Siamese 提取特征 + var feats = ExtractFeatures(batch, dims); + + // 5. 构建成本矩阵:question × answer + var cost = BuildCostMatrix(feats, ans.Count); + + // 6. 匈牙利分配,返回 question -> answer 的映射 + var assign = Hungarian.Solve(cost); // 长度 = questionCount,值域 [0..ansCount-1] + + // 7. 生成结果:按题面顺序返回匹配到的答案框中心点 + return GenerateResults(ans, assign); + } + + private static Image PreprocessToCanvas(Image src) + { + var canvas = new Image(Canvas, Canvas, new Rgba32(0, 0, 0, 255)); + canvas.Mutate(ctx => + { + ctx.DrawImage(src, new SixLabors.ImageSharp.Point(0, 0), 1f); + }); + return canvas; + } + + private List Detect(Image img) + { + // 准备 CHW float32 [1,3,384,384], 0..1 + var input = new DenseTensor(new[] { 1, 3, Canvas, Canvas }); + img.ProcessPixelRows(accessor => + { + for (int y = 0; y < Canvas; y++) + { + var row = accessor.GetRowSpan(y); + for (int x = 0; x < Canvas; x++) + { + var p = row[x]; + int idx = y * Canvas + x; + input[0, 0, y, x] = p.R / 255f; + input[0, 1, y, x] = p.G / 255f; + input[0, 2, y, x] = p.B / 255f; + } + } + }); + + var inputs = new List + { + NamedOnnxValue.CreateFromTensor("images", input) + }; + + using var results = _yolo.Run(inputs); + + // 解析输出:假设为 [1, N, 6] -> (x_min, y_min, x_max, y_max, conf, cls) + var y = results.FirstOrDefault(r => r.Name == "output0") ?? results.First(); + var t = y.AsTensor(); + var dims = t.Dimensions; + + // 尝试兼容 [1,N,6] 或 [1,6,N] + var list = new List(); + if (dims.Length == 3 && dims[0] == 1 && dims[2] == 6) + { + int n = dims[1]; + for (int i = 0; i < n; i++) + { + float conf = t[0, i, 4]; + if (conf <= ConfThreshold) continue; + list.Add(new BBox( + t[0, i, 0], t[0, i, 1], t[0, i, 2], t[0, i, 3], + conf, t[0, i, 5])); + } + } + else if (dims.Length == 3 && dims[0] == 1 && dims[1] == 6) + { + int n = dims[2]; + for (int i = 0; i < n; i++) + { + float conf = t[0, 4, i]; + if (conf <= ConfThreshold) continue; + list.Add(new BBox( + t[0, 0, i], t[0, 1, i], t[0, 2, i], t[0, 3, i], + conf, t[0, 5, i])); + } + } + else + { + throw new NotSupportedException($"不支持的 YOLO 输出维度:[{string.Join(",", dims.ToArray())}]"); + } + + return list; + } + + private static (List ans, List ques) SplitBoxes(List boxes) + { + boxes.Sort((a, b) => a.XMin.CompareTo(b.XMin)); // 按 x 从左到右 + var ans = new List(); + var ques = new List(); + foreach (var b in boxes) + { + if (b.YMin < SplitY) ans.Add(b); + else ques.Add(b); + } + return (ans, ques); + } + + private static (DenseTensor batch, int[] dims) CropAndResizeBatch(Image img, List ans, List ques) + { + int a = ans.Count, q = ques.Count, total = a + q; + var tensor = new DenseTensor(new[] { total, 3, Patch, Patch }); + + void ProcessOne(int index, BBox b) + { + var rect = ToSafeRect(b, img.Width, img.Height); + using var cropped = img.Clone(ctx => ctx.Crop(rect).Resize(Patch, Patch, KnownResamplers.Lanczos3)); + + cropped.ProcessPixelRows(rows => + { + for (int y = 0; y < Patch; y++) + { + var row = rows.GetRowSpan(y); + for (int x = 0; x < Patch; x++) + { + var p = row[x]; + tensor[index, 0, y, x] = p.R / 255f; + tensor[index, 1, y, x] = p.G / 255f; + tensor[index, 2, y, x] = p.B / 255f; + } + } + }); + } + + for (int i = 0; i < a; i++) ProcessOne(i, ans[i]); + for (int i = 0; i < q; i++) ProcessOne(a + i, ques[i]); + + return (tensor, new[] { total, 3, Patch, Patch }); + } + + private float[,] ExtractFeatures(DenseTensor batch, int[] dims) + { + var inputs = new List + { + NamedOnnxValue.CreateFromTensor("input", batch) + }; + + using var results = _siamese.Run(inputs); + var y = results.FirstOrDefault(r => r.Name == "output") ?? results.First(); + var t = y.AsTensor(); + + // 期望 [batch, feat] + if (t.Dimensions.Length != 2 || t.Dimensions[0] != dims[0]) + { + throw new NotSupportedException($"不支持的 Siamese 输出维度:[{string.Join(",", t.Dimensions.ToArray())}]"); + } + + int rows = t.Dimensions[0]; + int cols = t.Dimensions[1]; + var feats = new float[rows, cols]; + for (int i = 0; i < rows; i++) + for (int j = 0; j < cols; j++) + feats[i, j] = t[i, j]; + return feats; + } + + private static float[,] BuildCostMatrix(float[,] feats, int ansCount) + { + int total = feats.GetLength(0); + int featDim = feats.GetLength(1); + int q = total - ansCount; + int a = ansCount; + + var cost = new float[q, a]; + for (int i = 0; i < q; i++) + { + for (int j = 0; j < a; j++) + { + float sum = 0f; + for (int k = 0; k < featDim; k++) + { + float d = feats[ansCount + i, k] - feats[j, k]; // question - answer + sum += d * d; + } + cost[i, j] = MathF.Sqrt(sum); + } + } + return cost; + } + + private static List GenerateResults(List ans, int[] assign) + { + var res = new List(assign.Length); + foreach (var aIdx in assign) + { + var b = ans[aIdx]; + res.Add(new PointF((b.XMin + b.XMax) / 2f, (b.YMin + b.YMax) / 2f)); + } + return res; + } + + private static SixLabors.ImageSharp.Rectangle ToSafeRect(BBox b, int w, int h) + { + int x = Math.Clamp((int)MathF.Floor(b.XMin), 0, w - 1); + int y = Math.Clamp((int)MathF.Floor(b.YMin), 0, h - 1); + int rw = Math.Clamp((int)MathF.Ceiling(b.XMax - b.XMin), 1, w - x); + int rh = Math.Clamp((int)MathF.Ceiling(b.YMax - b.YMin), 1, h - y); + return new SixLabors.ImageSharp.Rectangle(x, y, rw, rh); + } + + public void Dispose() + { + _yolo.Dispose(); + _siamese.Dispose(); + } + + private readonly record struct BBox(float XMin, float YMin, float XMax, float YMax, float Confidence, float Class); + } + // 最小化代价匹配(匈牙利算法),输入为 q×a 的代价矩阵,返回长度 q 的数组 rowsol:第 i 个问题匹配到的答案列索引 + public static class Hungarian + { + public static int[] Solve(float[,] cost) + { + int nRows = cost.GetLength(0); + int nCols = cost.GetLength(1); + + // 若列少于行,补齐为方阵 + int n = Math.Max(nRows, nCols); + var a = new float[n, n]; + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + a[i, j] = (i < nRows && j < nCols) ? cost[i, j] : 0f; + + // 行最小值归一 + for (int i = 0; i < n; i++) + { + float min = float.PositiveInfinity; + for (int j = 0; j < n; j++) min = Math.Min(min, a[i, j]); + for (int j = 0; j < n; j++) a[i, j] -= min; + } + + // 列最小值归一 + for (int j = 0; j < n; j++) + { + float min = float.PositiveInfinity; + for (int i = 0; i < n; i++) min = Math.Min(min, a[i, j]); + for (int i = 0; i < n; i++) a[i, j] -= min; + } + + var starred = new bool[n, n]; + var primed = new bool[n, n]; + var rowCovered = new bool[n]; + var colCovered = new bool[n]; + + // 初始:对每行选择第一个 0 且该列未被占用,打星 + for (int i = 0; i < n; i++) + { + for (int j = 0; j < n; j++) + { + if (a[i, j] == 0 && !RowHasStar(starred, i, n) && !ColHasStar(starred, j, n)) + { + starred[i, j] = true; + break; + } + } + } + + CoverStarredColumns(starred, colCovered, n); + while (CountTrue(colCovered) < n) + { + (int r, int c) = FindZero(a, rowCovered, colCovered, n); + while (r == -1) + { + AdjustMatrix(a, rowCovered, colCovered, n); + (r, c) = FindZero(a, rowCovered, colCovered, n); + } + primed[r, c] = true; + + int starCol = FindStarInRow(starred, r, n); + if (starCol != -1) + { + rowCovered[r] = true; + colCovered[starCol] = false; + } + else + { + // 交替路径:从这个打撇的 0 开始 + var path = new List<(int r, int c)> { (r, c) }; + int col = c; + int row; + + while (true) + { + row = FindStarInCol(starred, col, n); + if (row == -1) break; + path.Add((row, col)); + + col = FindPrimeInRow(primed, row, n); + path.Add((row, col)); + } + + // 交替:星改非星,撇改星 + foreach (var (rr, cc) in path) + { + if (starred[rr, cc]) starred[rr, cc] = false; + else starred[rr, cc] = true; + } + + // 清空撇与覆盖 + Array.Clear(rowCovered, 0, n); + Array.Clear(colCovered, 0, n); + Array.Clear(primed, 0, primed.Length); + + CoverStarredColumns(starred, colCovered, n); + } + } + + // 构造结果 + var rowsol = new int[nRows]; + for (int i = 0; i < nRows; i++) + { + int j = FindStarInRow(starred, i, n); + rowsol[i] = (j < nCols) ? j : Math.Min(i, nCols - 1); + } + return rowsol; + } + + private static void CoverStarredColumns(bool[,] starred, bool[] colCovered, int n) + { + Array.Clear(colCovered, 0, colCovered.Length); + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + if (starred[i, j]) colCovered[j] = true; + } + + private static (int, int) FindZero(float[,] a, bool[] rowCovered, bool[] colCovered, int n) + { + for (int i = 0; i < n; i++) + if (!rowCovered[i]) + for (int j = 0; j < n; j++) + if (!colCovered[j] && a[i, j] == 0) + return (i, j); + return (-1, -1); + } + + private static void AdjustMatrix(float[,] a, bool[] rowCovered, bool[] colCovered, int n) + { + float min = float.PositiveInfinity; + for (int i = 0; i < n; i++) + if (!rowCovered[i]) + for (int j = 0; j < n; j++) + if (!colCovered[j]) + min = Math.Min(min, a[i, j]); + + for (int i = 0; i < n; i++) + { + if (rowCovered[i]) + for (int j = 0; j < n; j++) + a[i, j] += min; + } + + for (int j = 0; j < n; j++) + { + if (!colCovered[j]) + for (int i = 0; i < n; i++) + a[i, j] -= min; + } + } + + private static bool RowHasStar(bool[,] starred, int r, int n) + { + for (int j = 0; j < n; j++) if (starred[r, j]) return true; + return false; + } + + private static bool ColHasStar(bool[,] starred, int c, int n) + { + for (int i = 0; i < n; i++) if (starred[i, c]) return true; + return false; + } + + private static int FindStarInRow(bool[,] starred, int r, int n) + { + for (int j = 0; j < n; j++) if (starred[r, j]) return j; + return -1; + } + + private static int FindStarInCol(bool[,] starred, int c, int n) + { + for (int i = 0; i < n; i++) if (starred[i, c]) return i; + return -1; + } + + private static int FindPrimeInRow(bool[,] primed, int r, int n) + { + for (int j = 0; j < n; j++) if (primed[r, j]) return j; + return -1; + } + + private static int CountTrue(bool[] a) => a.Count(x => x); + } +} \ No newline at end of file diff --git a/Modules/GeetestCrypto.cs b/Modules/GeetestCrypto.cs new file mode 100644 index 0000000..353a8b6 --- /dev/null +++ b/Modules/GeetestCrypto.cs @@ -0,0 +1,565 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace MegghysAPI.Modules +{ + internal static class GeetestCrypto + { + private const string RsaN = "00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81"; + private const string RsaE = "010001"; + private const string AesKey = "1234567890123456"; + private static readonly byte[] AesIv = + { + 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48 + }; + + private const string Base64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()"; + private const int Mask1 = 7274496; + private const int Mask2 = 9483264; + private const int Mask3 = 19220; + private const int Mask4 = 235; + + public static string ClickCalculate(string key, string gt, string challenge) + { + if (string.IsNullOrEmpty(challenge) || challenge.Length < 2) + { + throw new ArgumentException("challenge 长度必须至少为 2", nameof(challenge)); + } + + var passTime = (int)(Random.Shared.NextDouble() * 700.0 + 1300.0); + var challengePrefix = challenge[..^2]; + var md5Source = string.Concat(gt, challengePrefix, passTime.ToString()); + var rpBytes = MD5.HashData(Encoding.UTF8.GetBytes(md5Source)); + var rp = Convert.ToHexString(rpBytes).ToLowerInvariant(); + + var payload = new Dictionary + { + ["lang"] = "zh-cn", + ["passtime"] = passTime, + ["a"] = key, + ["tt"] = string.Empty, + ["ep"] = BuildEpObject(), + ["h9s9"] = "1816378497", + ["rp"] = rp, + }; + + var json = JsonSerializer.Serialize(payload); + return Encrypt(json); + } + + public static string SlideCalculate(int distance, string gt, string challenge, ReadOnlySpan c, string s) + { + if (distance < 0) + { + throw new ArgumentOutOfRangeException(nameof(distance), "distance 必须大于等于 0"); + } + if (string.IsNullOrEmpty(challenge) || challenge.Length < 2) + { + throw new ArgumentException("challenge 长度必须至少为 2", nameof(challenge)); + } + + var track = GetSlideTrack(distance); + var passTime = track[track.Count - 1][2]; + var encryptedTrack = TrackEncrypt(track); + var aa = FinalEncrypt(encryptedTrack, c, s); + var userResponse = UserResponse(distance, challenge); + + var challengePrefix = challenge[..^2]; + var md5Source = string.Concat(gt, challengePrefix, passTime.ToString()); + var rpBytes = MD5.HashData(Encoding.UTF8.GetBytes(md5Source)); + var rp = Convert.ToHexString(rpBytes).ToLowerInvariant(); + + var payload = new Dictionary + { + ["lang"] = "zh-cn", + ["userresponse"] = userResponse, + ["passtime"] = passTime, + ["imgload"] = NextIntInclusive(100, 200), + ["aa"] = aa, + ["ep"] = BuildEpObject(), + ["rp"] = rp, + }; + + var json = JsonSerializer.Serialize(payload); + return Encrypt(json); + } + + private static Dictionary BuildEpObject() + { + return new Dictionary + { + ["v"] = "9.1.8-bfget5", + ["$_E_"] = false, + ["me"] = true, + ["ven"] = "Google Inc. (Intel)", + ["ren"] = "ANGLE (Intel, Intel(R) HD Graphics 520 Direct3D11 vs_5_0 ps_5_0, D3D11)", + ["fp"] = new object[] { "move", 483, 149, 1702019849214L, "pointermove" }, + ["lp"] = new object[] { "up", 657, 100, 1702019852230L, "pointerup" }, + ["em"] = new Dictionary + { + ["ph"] = 0, + ["cp"] = 0, + ["ek"] = "11", + ["wd"] = 1, + ["nt"] = 0, + ["si"] = 0, + ["sc"] = 0, + }, + ["tm"] = new Dictionary + { + ["a"] = 1702019845759L, + ["b"] = 1702019845951L, + ["c"] = 1702019845951L, + ["d"] = 0, + ["e"] = 0, + ["f"] = 1702019845763L, + ["g"] = 1702019845785L, + ["h"] = 1702019845785L, + ["i"] = 1702019845785L, + ["j"] = 1702019845845L, + ["k"] = 1702019845812L, + ["l"] = 1702019845845L, + ["m"] = 1702019845942L, + ["n"] = 1702019845946L, + ["o"] = 1702019845954L, + ["p"] = 1702019846282L, + ["q"] = 1702019846282L, + ["r"] = 1702019846287L, + ["s"] = 1702019846288L, + ["t"] = 1702019846288L, + ["u"] = 1702019846288L, + }, + ["dnf"] = "dnf", + ["by"] = 0, + }; + } + + private static string Encrypt(string json) + { + var rsaEncryptedKey = RsaEncrypt(AesKey); + var aesEncryptedData = AesEncrypt(json); + var base64Part = CustomBase64(aesEncryptedData); + return base64Part + rsaEncryptedKey; + } + + private static byte[] AesEncrypt(string data) + { + using var aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.Key = Encoding.UTF8.GetBytes(AesKey); + aes.IV = AesIv; + + using var encryptor = aes.CreateEncryptor(); + var inputBytes = Encoding.UTF8.GetBytes(data); + return encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); + } + + private static string RsaEncrypt(string data) + { + var modulus = TrimLeadingZero(HexToBytes(RsaN)); + var exponent = TrimLeadingZero(HexToBytes(RsaE)); + + using var rsa = RSA.Create(); + rsa.ImportParameters(new RSAParameters + { + Modulus = modulus, + Exponent = exponent + }); + + var bytes = Encoding.UTF8.GetBytes(data); + var encrypted = rsa.Encrypt(bytes, RSAEncryptionPadding.Pkcs1); + return Convert.ToHexString(encrypted).ToLowerInvariant(); + } + + private static byte[] TrimLeadingZero(byte[] value) + { + if (value.Length > 0 && value[0] == 0) + { + var trimmed = new byte[value.Length - 1]; + Buffer.BlockCopy(value, 1, trimmed, 0, trimmed.Length); + return trimmed; + } + return value; + } + + private static string CustomBase64(ReadOnlySpan input) + { + var sb = new StringBuilder(); + var padding = string.Empty; + var len = input.Length; + var ptr = 0; + + while (ptr < len) + { + if (ptr + 2 < len) + { + var c = (input[ptr] << 16) + (input[ptr + 1] << 8) + input[ptr + 2]; + sb.Append(Base64Table[GetIntByMask(c, Mask1)]); + sb.Append(Base64Table[GetIntByMask(c, Mask2)]); + sb.Append(Base64Table[GetIntByMask(c, Mask3)]); + sb.Append(Base64Table[GetIntByMask(c, Mask4)]); + } + else + { + var remainder = len % 3; + if (remainder == 2) + { + var c = (input[ptr] << 16) + (input[ptr + 1] << 8); + sb.Append(Base64Table[GetIntByMask(c, Mask1)]); + sb.Append(Base64Table[GetIntByMask(c, Mask2)]); + sb.Append(Base64Table[GetIntByMask(c, Mask3)]); + padding = "."; + } + else if (remainder == 1) + { + var c = input[ptr] << 16; + sb.Append(Base64Table[GetIntByMask(c, Mask1)]); + sb.Append(Base64Table[GetIntByMask(c, Mask2)]); + padding = ".."; + } + } + + ptr += 3; + } + + sb.Append(padding); + return sb.ToString(); + } + + private static int GetIntByMask(int value, int mask) + { + var result = 0; + for (var bit = 23; bit >= 0; bit--) + { + if (ChooseBit(mask, bit) == 1) + { + result = (result << 1) | ChooseBit(value, bit); + } + } + return result; + } + + private static int ChooseBit(int value, int bit) + { + return (value >> bit) & 1; + } + + private static byte[] HexToBytes(string hex) + { + var cleaned = hex.Replace("\n", string.Empty) + .Replace("\r", string.Empty) + .Replace(" ", string.Empty); + if (cleaned.Length % 2 != 0) + { + throw new ArgumentException("十六进制字符串长度必须为偶数", nameof(hex)); + } + + var bytes = new byte[cleaned.Length / 2]; + for (var i = 0; i < bytes.Length; i++) + { + bytes[i] = Convert.ToByte(cleaned.Substring(i * 2, 2), 16); + } + return bytes; + } + + private static int NextIntInclusive(int minValue, int maxValue) + { + if (minValue > maxValue) + { + throw new ArgumentException("minValue 必须小于等于 maxValue"); + } + return Random.Shared.Next(minValue, maxValue + 1); + } + + private static List> GetSlideTrack(int distance) + { + if (distance < 0) + { + throw new ArgumentOutOfRangeException(nameof(distance)); + } + + var slideTrack = new List>(); + var x1 = NextIntInclusive(-50, -10); + var y1 = NextIntInclusive(-50, -10); + slideTrack.Add(new List { x1, y1, 0 }); + slideTrack.Add(new List { 0, 0, 0 }); + + var count = 30 + distance / 2; + var t = NextIntInclusive(50, 100); + var currentX = 0; + var currentY = 0; + + for (var i = 0; i < count; i++) + { + var sep = i / (double)count; + double x; + if (Math.Abs(sep - 1.0) < double.Epsilon) + { + x = distance; + } + else + { + x = (1.0 - Math.Pow(2.0, -10.0 * sep)) * distance; + } + + var xRounded = (int)Math.Round(x); + t += NextIntInclusive(10, 20); + + if (xRounded == currentX) + { + continue; + } + + slideTrack.Add(new List { xRounded, currentY, t }); + currentX = xRounded; + } + + var last = slideTrack[slideTrack.Count - 1]; + slideTrack.Add(new List(last)); + + return slideTrack; + } + + private static string TrackEncrypt(List> track) + { + static List> ProcessTrack(List> trackData) + { + var result = new List>(); + var o = 0; + + for (var s = 0; s < trackData.Count - 1; s++) + { + var e = trackData[s + 1][0] - trackData[s][0]; + var n = trackData[s + 1][1] - trackData[s][1]; + var r = trackData[s + 1][2] - trackData[s][2]; + + if (e == 0 && n == 0 && r == 0) + { + continue; + } + + if (e == 0 && n == 0) + { + o += r; + } + else + { + result.Add(new List { e, n, r + o }); + o = 0; + } + } + + if (o != 0 && result.Count > 0) + { + var lastItem = result[result.Count - 1]; + result.Add(new List { lastItem[0], lastItem[1], o }); + } + + return result; + } + + static string EncodeValue(int t) + { + const string chars = "()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqr"; + var n = chars.Length; + var r = new StringBuilder(); + + var i = Math.Abs(t); + var oVal = i / n; + var o = oVal >= n ? n - 1 : oVal; + if (o >= chars.Length) + { + o = chars.Length - 1; + } + + if (o > 0) + { + r.Append(chars[o]); + } + + var s = new StringBuilder(); + if (t < 0) + { + s.Append('!'); + } + if (r.Length > 0) + { + s.Append('$'); + } + + s.Append(r); + s.Append(chars[i % n]); + return s.ToString(); + } + + static char? EncodePair(IReadOnlyList values) + { + var pairs = new List + { + new[] { 1, 0 }, + new[] { 2, 0 }, + new[] { 1, -1 }, + new[] { 1, 1 }, + new[] { 0, 1 }, + new[] { 0, -1 }, + new[] { 3, 0 }, + new[] { 2, -1 }, + new[] { 2, 1 }, + }; + const string chars = "stuvwxyz~"; + + for (var idx = 0; idx < pairs.Count; idx++) + { + var pair = pairs[idx]; + if (values[0] == pair[0] && values[1] == pair[1]) + { + return chars[idx]; + } + } + + return null; + } + + static void ProcessElement(IReadOnlyList item, StringBuilder r, StringBuilder i, StringBuilder o) + { + var encodedPair = EncodePair(item); + if (encodedPair.HasValue) + { + i.Append(encodedPair.Value); + } + else + { + r.Append(EncodeValue(item[0])); + i.Append(EncodeValue(item[1])); + } + o.Append(EncodeValue(item[2])); + } + + var processed = ProcessTrack(track); + var rBuilder = new StringBuilder(); + var iBuilder = new StringBuilder(); + var oBuilder = new StringBuilder(); + + foreach (var item in processed) + { + ProcessElement(item, rBuilder, iBuilder, oBuilder); + } + + return string.Concat(rBuilder, "!!", iBuilder, "!!", oBuilder); + } + + private static string FinalEncrypt(string t, ReadOnlySpan e, string n) + { + if (e.Length < 5 || string.IsNullOrEmpty(n)) + { + return t; + } + + var s = e[0]; + var a = e[2]; + var m = e[4]; + var originalLength = t.Length; + var builder = new StringBuilder(t); + + var index = 0; + while (index <= n.Length - 2) + { + var hexPair = n.Substring(index, 2); + index += 2; + var c = Convert.ToByte(hexPair, 16); + var u = (char)c; + + var ll = ((ulong)s * c * c + (ulong)a * c + m) % (ulong)originalLength; + builder.Insert((int)ll, u); + } + + return builder.ToString(); + } + + private static string UserResponse(int key, string challenge) + { + if (challenge.Length < 2) + { + throw new ArgumentException("challenge 长度必须至少为 2", nameof(challenge)); + } + + var chars = challenge.ToCharArray(); + var lastTwo = new[] { chars[chars.Length - 2], chars[chars.Length - 1] }; + + var r = new List(); + foreach (var c in lastTwo) + { + var code = (int)c; + r.Add(code > 57 ? code - 87 : code - 48); + } + + var n = 36 * r[0] + r[1]; + var a = key + n; + + var underscores = new List> + { + new List(), + new List(), + new List(), + new List(), + new List() + }; + var charSet = new HashSet(); + var idx = 0; + + for (var i = 0; i < chars.Length - 2; i++) + { + var c = chars[i]; + if (charSet.Add(c)) + { + underscores[idx].Add(c); + idx = (idx + 1) % 5; + } + } + + var f = a; + var d = 4; + var result = new StringBuilder(); + var weights = new List { 1, 2, 5, 10, 50 }; + + while (f > 0) + { + if (d >= weights.Count) + { + throw new InvalidOperationException("权重数组越界,请检查输入参数有效性"); + } + + if (f >= weights[d]) + { + if (underscores[d].Count == 0) + { + throw new InvalidOperationException($"五元组数组 {d} 号位置无可用字符,请确保输入字符串包含足够多的唯一字符"); + } + + result.Append(underscores[d][0]); + f -= weights[d]; + } + else + { + underscores.RemoveAt(d); + weights.RemoveAt(d); + if (d > 0) + { + d -= 1; + } + else + { + throw new InvalidOperationException("权重数组耗尽,无法继续处理"); + } + } + } + + return result.ToString(); + } + } +} diff --git a/Modules/ProxyManager.cs b/Modules/ProxyManager.cs new file mode 100644 index 0000000..ea7e164 --- /dev/null +++ b/Modules/ProxyManager.cs @@ -0,0 +1,177 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Caching.Memory; + +namespace MegghysAPI.Modules +{ + /// + /// 代理IP管理器 + /// + public static class ProxyManager + { + // 用于获取代理IP的客户端,不使用代理 + private static readonly HttpClient ProxyApiHttpClient = new(); + // 缓存代理IP的响应 + private static readonly MemoryCache ProxyResponseCache = new(new MemoryCacheOptions()); + // 缓存配置了代理的HttpClient + private static readonly MemoryCache HttpClientCache = new(new MemoryCacheOptions()); + + private const string BaseUrl = "https://share.proxy.qg.net/get"; + private const string DefaultKey = "9IK2T3XL"; + private const string DefaultPassword = "04BC86C88CDB"; + + /// + /// 获取配置了代理的 HttpClient,带缓存和自动释放功能 + /// + /// 可选的代理地址。如果为空,则自动获取。 + /// 代理用户名 + /// 代理密码 + /// 配置好代理的 HttpClient + public static async Task GetHttpClientAsync(string? proxyAddress = null, string key = DefaultKey, string password = DefaultPassword) + { + // 如果未提供代理地址,则从API获取 + if (string.IsNullOrWhiteSpace(proxyAddress)) + { + var proxyResponse = await GetProxyAsync(key: key); + var proxyIp = proxyResponse?.Data?.FirstOrDefault(); + + if (proxyIp?.Server == null || proxyIp.Deadline == null) + { + // 如果无法获取有效代理,返回一个不带代理的普通HttpClient + return new HttpClient(); + } + + // 检查缓存中是否已有此代理的HttpClient + if (HttpClientCache.TryGetValue(proxyIp.Server, out HttpClient? cachedClient)) + { + return cachedClient!; + } + + // 创建新的HttpClient并配置缓存 + var client = CreateConfiguredHttpClient(proxyIp.Server, key, password); + var deadline = DateTime.Parse(proxyIp.Deadline); + + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(deadline) + .RegisterPostEvictionCallback((cacheKey, value, reason, state) => + { + (value as HttpClient)?.Dispose(); + }); + + HttpClientCache.Set(proxyIp.Server, client, cacheEntryOptions); + return client; + } + else + { + // 如果用户手动提供了代理地址,我们无法知道其过期时间,因此不进行缓存 + return CreateConfiguredHttpClient(proxyAddress, key, password); + } + } + + private static HttpClient CreateConfiguredHttpClient(string proxyAddress, string key, string password) + { + var handler = new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.All, + Proxy = new WebProxy(proxyAddress, false) + { + Credentials = new NetworkCredential(key, password) + }, + UseProxy = true + }; + var client = new HttpClient(handler, disposeHandler: true); + client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"); + + return client; + } + + /// + /// 异步获取代理IP, 带缓存功能 + /// + public static async Task GetProxyAsync(string key = DefaultKey, string? area = null, string? areaEx = null, int? isp = null, int? num = null, bool? distinct = null) + { + var queryParams = new Dictionary + { + { "key", key }, + { "area", area }, + { "area_ex", areaEx }, + { "isp", isp?.ToString() }, + { "num", num?.ToString() }, + { "distinct", distinct?.ToString().ToLower() } + }; + + var queryString = string.Join("&", queryParams + .Where(kvp => !string.IsNullOrEmpty(kvp.Value)) + .Select(kvp => $"{kvp.Key}={Uri.EscapeDataString(kvp.Value!)}")); + + var cacheKey = $"ProxyResponse_{queryString}"; + + if (ProxyResponseCache.TryGetValue(cacheKey, out ProxyResponse? cachedResponse)) + { + return cachedResponse; + } + + var requestUrl = $"{BaseUrl}?{queryString}"; + + try + { + var response = await ProxyApiHttpClient.GetAsync(requestUrl); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + var proxyResponse = JsonSerializer.Deserialize(json); + + if (proxyResponse?.Code == "SUCCESS" && proxyResponse.Data?.Any() == true) + { + // 使用代理的过期时间作为缓存的过期时间 + var deadline = DateTime.Parse(proxyResponse.Data.First().Deadline!); + ProxyResponseCache.Set(cacheKey, proxyResponse, deadline); + } + + return proxyResponse; + } + catch (HttpRequestException e) + { + Console.WriteLine($"请求代理IP时发生错误: {e.Message}"); + return null; + } + } + } + + /// + /// 代理IP接口响应 + /// + public class ProxyResponse + { + [JsonPropertyName("code")] + public string? Code { get; set; } + + [JsonPropertyName("data")] + public List? Data { get; set; } + + [JsonPropertyName("request_id")] + public string? RequestId { get; set; } + } + + /// + /// 代理IP信息 + /// + public class ProxyIp + { + [JsonPropertyName("proxy_ip")] + public string? ProxyIpAddress { get; set; } + + [JsonPropertyName("server")] + public string? Server { get; set; } + + [JsonPropertyName("area")] + public string? Area { get; set; } + + [JsonPropertyName("isp")] + public string? Isp { get; set; } + + [JsonPropertyName("deadline")] + public string? Deadline { get; set; } + } +} diff --git a/Program.cs b/Program.cs index e4aa295..09f2fee 100644 --- a/Program.cs +++ b/Program.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Reflection; using MegghysAPI.Attributes; using MegghysAPI.Components; +using MegghysAPI.Modules; using AntDesign; var builder = WebApplication.CreateBuilder(args); @@ -13,6 +14,7 @@ builder.Services.AddRazorComponents() builder.Services.AddAntDesign(); builder.Services.AddControllers(); + var app = builder.Build(); // Configure the HTTP request pipeline.