Files
MegghysAPI/Modules/BiliCaptchaResolver.cs

401 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
/// <summary>
/// Geetest 点选验证码解析器C# 版)。
/// - 对应 Rust 版本的 Click 结构实现:注册、拉取参数、计算 key、生成 w、验证、刷新。
/// - 依赖 CaptchaBreaker.ChineseClick0ONNX 模型)。
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<BiliCaptchaResolver> 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<string> 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) ==========
/// <summary>
/// 从业务 URL 注册,返回 (gt, challenge)。
/// </summary>
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);
}
/// <summary>
/// 获取初始 c/s 参数,对应 Rust 版本的 get_c_s 方法。
/// </summary>
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<string, string>
{
["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<int>()).ToArray() ?? [];
var s = (string?)data["s"] ?? throw new Exception("缺少 s");
return (cList, s);
}
/// <summary>
/// 获取验证类型,对应 Rust 版本的 get_type 方法。
/// </summary>
public async Task<string> GetTypeAsync(string gt, string challenge, string? proxy = null)
{
var url = "http://api.geevisit.com/gettype.php";
var callback = "geetest_1717934072177";
var query = new Dictionary<string, string>
{
["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");
}
/// <summary>
/// 获取 new c/s/args图片地址
/// </summary>
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<string, string>
{
["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<int>()).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);
}
/// <summary>
/// 提交验证,可带 w。
/// </summary>
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<string, string>
{
["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);
}
/// <summary>
/// 刷新获取新的图片地址。
/// </summary>
public async Task<string> RefreshAsync(string gt, string challenge, string? proxy = null)
{
var url = "http://api.geevisit.com/refresh.php";
var callback = "geetest_1717918222610";
var query = new Dictionary<string, string>
{
["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('/')}";
}
/// <summary>
/// 计算 key坐标串会下载图片并通过 ONNX 模型推理。
/// </summary>
public async Task<string> CalculateKeyAsync(string picUrl, string? proxy = null)
{
var http = await ProxyManager.GetHttpClientAsync(proxy);
var bytes = await http.GetByteArrayAsync(picUrl);
var raw = Image.Load<Rgba32>(bytes);
// 保障尺寸不超过 384
List<PointF> 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<string>(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);
}
/// <summary>
/// 生成 w。注意目前使用占位实现请按需替换为真实算法。
/// </summary>
public string GenerateW(string key, string gt, string challenge, ReadOnlySpan<byte> c, string s)
{
return ClickCalculate(key, gt, challenge, c, s);
}
/// <summary>
/// 完整流程测试:注册 -> get_c_s -> get_type -> 拉取 c/s/args -> 计算 key -> 生成 w -> 提交 -> 返回 validate。
/// 按照原始 Rust 版本流程顺序sleep 保证总耗时 >= 2s。
/// </summary>
public async Task<string> 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;
}
/// <summary>
/// 简单匹配(不注册):直接使用已知 gt, challenge按照原始流程顺序。
/// </summary>
public async Task<string> 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;
}
/// <summary>
/// 带自动重试的简单匹配,按照原始流程顺序。
/// </summary>
public async Task<string> 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<string, string> 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<byte> c, string s)
{
_ = c;
_ = s;
return GeetestCrypto.ClickCalculate(key, gt, challenge);
}
}
}