diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index cfcf589..d8f8d77 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -1249,5 +1249,190 @@ public async Task Prosopo_ShouldSolve() sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); actual.Should().BeEquivalentTo(expectedResult); } + + [Test] + public async Task Altcha_ShouldSolve() + { + var clientKey = Gen.RandomString(); + var taskId = Gen.RandomInt(); + + var captchaRequest = ObjectGen.CustomTask.CreateAltchaTask(); + var expectedResult = ObjectGen.CustomTask.CreateAltchaSolution(); + + var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> + { + ( + Type: RequestType.CreateTask, + ExpectedRequest: JsonConvert.SerializeObject(new + { clientKey = clientKey, task = captchaRequest, softId = 53 }) + ), + ( + Type: RequestType.GetTaskResult, + ExpectedRequest: JsonConvert.SerializeObject(new { clientKey = clientKey, taskId = taskId }) + ), + }; + + var captchaResults = new List + { + new { taskId = taskId, errorId = 0, errorCode = (string)null! }, + new + { + status = "ready", + solution = new + { + data = expectedResult.Solution.Data + }, + errorId = 0, + errorCode = (string)null! + } + }; + + var sut = new Sut(clientKey); + sut.SetupHttpServer(captchaResults); + + var actual = await sut.SolveAsync(captchaRequest); + + sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); + actual.Should().BeEquivalentTo(expectedResult); + } + + [Test] + public async Task Castle_ShouldSolve() + { + var clientKey = Gen.RandomString(); + var taskId = Gen.RandomInt(); + + var captchaRequest = ObjectGen.CustomTask.CreateCastleTask(); + var expectedResult = ObjectGen.CustomTask.CreateCastleSolution(); + + var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> + { + ( + Type: RequestType.CreateTask, + ExpectedRequest: JsonConvert.SerializeObject(new + { clientKey = clientKey, task = captchaRequest, softId = 53 }) + ), + ( + Type: RequestType.GetTaskResult, + ExpectedRequest: JsonConvert.SerializeObject(new { clientKey = clientKey, taskId = taskId }) + ), + }; + + var captchaResults = new List + { + new { taskId = taskId, errorId = 0, errorCode = (string)null! }, + new + { + status = "ready", + solution = new + { + data = expectedResult.Solution.Data, + domains = expectedResult.Solution.Domains + }, + errorId = 0, + errorCode = (string)null! + } + }; + + var sut = new Sut(clientKey); + sut.SetupHttpServer(captchaResults); + + var actual = await sut.SolveAsync(captchaRequest); + + sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); + actual.Should().BeEquivalentTo(expectedResult); + } + + [Test] + public async Task Tspd_ShouldSolve() + { + var clientKey = Gen.RandomString(); + var taskId = Gen.RandomInt(); + + var captchaRequest = ObjectGen.CustomTask.CreateTspdTask(); + var expectedResult = ObjectGen.CustomTask.CreateTspdSolution(); + + var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> + { + ( + Type: RequestType.CreateTask, + ExpectedRequest: JsonConvert.SerializeObject(new + { clientKey = clientKey, task = captchaRequest, softId = 53 }) + ), + ( + Type: RequestType.GetTaskResult, + ExpectedRequest: JsonConvert.SerializeObject(new { clientKey = clientKey, taskId = taskId }) + ), + }; + + var captchaResults = new List + { + new { taskId = taskId, errorId = 0, errorCode = (string)null! }, + new + { + status = "ready", + solution = new + { + domains = expectedResult.Solution.Domains + }, + errorId = 0, + errorCode = (string)null! + } + }; + + var sut = new Sut(clientKey); + sut.SetupHttpServer(captchaResults); + + var actual = await sut.SolveAsync(captchaRequest); + + sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); + actual.Should().BeEquivalentTo(expectedResult); + } + + [Test] + public async Task Hunt_ShouldSolve() + { + var clientKey = Gen.RandomString(); + var taskId = Gen.RandomInt(); + + var captchaRequest = ObjectGen.CustomTask.CreateHuntTask(); + var expectedResult = ObjectGen.CustomTask.CreateHuntSolution(); + + var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> + { + ( + Type: RequestType.CreateTask, + ExpectedRequest: JsonConvert.SerializeObject(new + { clientKey = clientKey, task = captchaRequest, softId = 53 }) + ), + ( + Type: RequestType.GetTaskResult, + ExpectedRequest: JsonConvert.SerializeObject(new { clientKey = clientKey, taskId = taskId }) + ), + }; + + var captchaResults = new List + { + new { taskId = taskId, errorId = 0, errorCode = (string)null! }, + new + { + status = "ready", + solution = new + { + data = expectedResult.Solution.Data + }, + errorId = 0, + errorCode = (string)null! + } + }; + + var sut = new Sut(clientKey); + sut.SetupHttpServer(captchaResults); + + var actual = await sut.SolveAsync(captchaRequest); + + sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); + actual.Should().BeEquivalentTo(expectedResult); + } } } \ No newline at end of file diff --git a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs index 0abcf17..e643206 100644 --- a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs @@ -483,6 +483,133 @@ public static CaptchaResult CreateImpervaSolution() } }; } + + public static AltchaCustomTaskRequest CreateAltchaTask() + { + return new AltchaCustomTaskRequest(Gen.RandomString(), Gen.RandomString(), Gen.RandomString(), Gen.RandomString()) + { + WebsiteKey = Gen.RandomGuid(), + WebsiteUrl = Gen.RandomUri().ToString(), + UserAgent = Gen.UserAgent(), + Proxy = new ProxyContainer(Gen.RandomString(), Gen.RandomInt(0, 65535), Gen.RandomEnum(), Gen.RandomString(), Gen.RandomString()) + }; + } + + public static CaptchaResult CreateAltchaSolution() + { + return new CaptchaResult + { + Error = null, + Solution = new CustomTaskResponse + { + Data = new Dictionary + { + { "token", Gen.RandomString() } , + { "number", Gen.RandomInt().ToString() } + } + } + }; + } + + public static CastleCustomTaskRequest CreateCastleTask() + { + return new CastleCustomTaskRequest(Gen.RandomUri().ToString(), Gen.RandomUri().ToString(), Gen.RandomInt(1, 49)) + { + WebsiteKey = Gen.RandomGuid(), + WebsiteUrl = Gen.RandomUri().ToString(), + UserAgent = Gen.UserAgent(), + Proxy = new ProxyContainer(Gen.RandomString(), Gen.RandomInt(0, 65535), Gen.RandomEnum(), Gen.RandomString(), Gen.RandomString()) + }; + } + + public static CaptchaResult CreateCastleSolution() + { + return new CaptchaResult + { + Error = null, + Solution = new CustomTaskResponse + { + Domains = new Dictionary + { + { + Gen.RandomString(), + new CustomTaskResponse.DomainInfo() + { + Cookies = new Dictionary { { "__cuid", Gen.RandomGuid() } } + } + } + }, + Data = new Dictionary + { + { "tokens", Gen.RandomString() } + } + } + }; + } + + public static TspdCustomTaskRequest CreateTspdTask() + { + return new TspdCustomTaskRequest(Gen.RandomString(), Gen.RandomString()) + { + WebsiteUrl = Gen.RandomUri().ToString(), + UserAgent = Gen.UserAgent(), + Proxy = new ProxyContainer(Gen.RandomString(), Gen.RandomInt(0, 65535), Gen.RandomEnum(), Gen.RandomString(), Gen.RandomString()) + }; + } + + public static CaptchaResult CreateTspdSolution() + { + return new CaptchaResult + { + Error = null, + Solution = new CustomTaskResponse + { + Domains = new Dictionary + { + { + Gen.RandomString(), + new CustomTaskResponse.DomainInfo() + { + Cookies = new Dictionary + { + { "TS386a400d029", Gen.RandomString() }, + { "TS386a400d078", Gen.RandomString() }, + { "TSd2153684027", Gen.RandomString() }, + { "TS00000000076", Gen.RandomString() }, + { "TSPD_101_DID", Gen.RandomString() }, + { "TS386a400d075", Gen.RandomString() } + } + } + } + } + } + }; + } + + public static HuntCustomTaskRequest CreateHuntTask() + { + return new HuntCustomTaskRequest(Gen.RandomUri().ToString(), Gen.RandomString()) + { + WebsiteUrl = Gen.RandomUri().ToString(), + UserAgent = Gen.UserAgent(), + Proxy = new ProxyContainer(Gen.RandomString(), Gen.RandomInt(0, 65535), Gen.RandomEnum(), Gen.RandomString(), Gen.RandomString()) + }; + } + + public static CaptchaResult CreateHuntSolution() + { + return new CaptchaResult + { + Error = null, + Solution = new CustomTaskResponse + { + Data = new Dictionary + { + { "token", Gen.RandomString() } + } + } + }; + } } public static class AmazonWaf diff --git a/CapMonsterCloud.Client.IntegrationTests/Sut.cs b/CapMonsterCloud.Client.IntegrationTests/Sut.cs index 8ed962a..d7c6dba 100644 --- a/CapMonsterCloud.Client.IntegrationTests/Sut.cs +++ b/CapMonsterCloud.Client.IntegrationTests/Sut.cs @@ -110,6 +110,18 @@ public async Task> SolveAsync( public async Task> SolveAsync( YidunTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task> SolveAsync( + AltchaCustomTaskRequest request) => await _cloudClient.SolveAsync(request); + + public async Task> SolveAsync( + CastleCustomTaskRequest request) => await _cloudClient.SolveAsync(request); + + public async Task> SolveAsync( + TspdCustomTaskRequest request) => await _cloudClient.SolveAsync(request); + + public async Task> SolveAsync( + HuntCustomTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task GetBalanceAsync() { return await _cloudClient.GetBalanceAsync(); diff --git a/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj b/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj index dbf5e3d..bd58d8d 100644 --- a/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj +++ b/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj @@ -15,8 +15,8 @@ README.md logo.png https://github.com/ZennoLab/capmonstercloud-client-dotnet - 3.1.0 - Add Yidun, Temu, Prosopo, MTCaptcha solving support + 3.2.0 + Add Altcha, Castle, Tspd, Hunt solving support diff --git a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs index c587a40..bab1a1d 100644 --- a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs +++ b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs @@ -222,6 +222,42 @@ private static GetResultTimeouts GetTimeouts(Type type) Timeout = TimeSpan.FromSeconds(180) } }, + { + typeof(AltchaCustomTaskRequest), + new GetResultTimeouts + { + FirstRequestDelay = TimeSpan.FromSeconds(1), + RequestsInterval = TimeSpan.FromSeconds(2), + Timeout = TimeSpan.FromSeconds(50) + } + }, + { + typeof(CastleCustomTaskRequest), + new GetResultTimeouts + { + FirstRequestDelay = TimeSpan.FromSeconds(1), + RequestsInterval = TimeSpan.FromSeconds(2), + Timeout = TimeSpan.FromSeconds(60) + } + }, + { + typeof(TspdCustomTaskRequest), + new GetResultTimeouts + { + FirstRequestDelay = TimeSpan.FromSeconds(1), + RequestsInterval = TimeSpan.FromSeconds(2), + Timeout = TimeSpan.FromSeconds(60) + } + }, + { + typeof(HuntCustomTaskRequest), + new GetResultTimeouts + { + FirstRequestDelay = TimeSpan.FromSeconds(1), + RequestsInterval = TimeSpan.FromSeconds(3), + Timeout = TimeSpan.FromSeconds(180) + } + }, }; } } diff --git a/CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs new file mode 100644 index 0000000..b7277a3 --- /dev/null +++ b/CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; + +namespace Zennolab.CapMonsterCloud.Requests +{ + /// + /// Altcha CustomTask recognition request. + /// + /// + /// https://docs.capmonster.cloud/docs/captchas/altcha-task + /// + public sealed class AltchaCustomTaskRequest : CustomTaskRequestBase + { + /// + public override string Class => "Altcha"; + + /// + /// For this task, sending an empty string is allowed. + /// + [JsonProperty("websiteKey", Required = Required.Always)] + [StringLength(int.MaxValue, MinimumLength = 0)] + public string WebsiteKey { get; set; } + + /// + /// Initializes Altcha task with required metadata. + /// + /// + /// + /// "3dd28253be6cc0c54d95f7f98c517e68744597cc6e66109619d1ac975c39181c" + /// Unique task identifier obtained from the website. + /// + /// + /// + /// "5000" + /// Number of iterations or the maximum number for calculations. + /// Important: the iterations parameter corresponds to the maxnumber value! + /// + /// + /// + /// "46d5b1c8871e5152d902ee3f?edk=1493462145de1ce33a52fb569b27a364&codeChallenge=464Cjs7PbiJJhJZ_ReJ-y9UGGDndcpsnP6vS8x1nEJyTkhjQkJyL2jcnYEuMKcrG&expires=1761048664" + /// Salt obtained from the site, used for hash generation. + /// Important: Always send the full value of the salt field! If the site returns something like this: + /// "salt": "46d5b1c8871e5152d902ee3f?edk=1493462145de1ce33a52fb569b27a364&codeChallenge=464Cjs7PbiJJhJZ_ReJ-y9UGGDndcpsnP6vS8x1nEJyTkhjQkJyL2jcnYEuMKcrG&expires=1761048664" + /// Then copy and include it in your request exactly as it is, with all characters and parameters (edk, codeChallenge, etc.). + /// + /// + /// + /// "4b1cf0e0be0f4e5247e50b0f9a449830f1fbca44c32ff94bc080146815f31a18" + /// Digital signature of the request. + /// + public AltchaCustomTaskRequest(string challenge, string iterations, string salt, string signature) => Metadata = new { challenge, iterations, salt, signature }; + } +} diff --git a/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs new file mode 100644 index 0000000..b679d64 --- /dev/null +++ b/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; + +namespace Zennolab.CapMonsterCloud.Requests +{ + /// + /// Castle CustomTask recognition request. + /// + /// + /// Castle is a website protection system that blocks automated attacks and + /// suspicious activity. It monitors user behavior and requests in real time, + /// remaining almost invisible to regular visitors and only becoming noticeable + /// during unusual or high-volume traffic. + /// + /// + /// https://docs.capmonster.cloud/docs/captchas/castle-task + /// + public sealed class CastleCustomTaskRequest : CustomTaskRequestBase + { + /// + public override string Class => "Castle"; + + /// + /// Publishable Key, the Castle identifier. It can be found on the page/in the site scripts. + /// + /// pk_1Tk5Yzr1WFzxrJCh7WzMZzY1rHpaOtdK + [JsonProperty("websiteKey", Required = Required.Always)] + [StringLength(int.MaxValue, MinimumLength = 1)] + public string WebsiteKey { get; set; } + + /// + /// Initializes Castle task with required metadata. + /// + /// + /// + /// "https://s.rsg.sc/auth/js/20251234bgef/build/cw.js" + /// Link to cw.js file. You can find it in Developer Tools on the Network tab. + /// + /// + /// + /// "https://s.rsg.sc/auth/js/20251234bgef/build/csw.js" + /// Link to csw.js file. You can find it in Developer Tools on the Network tab. + /// + /// + /// + /// 1 + /// Number of tokens to generate — default is 1. + /// Generation of 1 to 49 Castle tokens bound to the same browser session (with the same __cuid identifier). + /// If a value greater than 49 is specified, the maximum allowed number of tokens — 49 — will be returned, and the cost will be calculated based on this amount. + /// + public CastleCustomTaskRequest(string wUrl, string swUrl, int count = 1) => + Metadata = new { wUrl, swUrl, count }; + } +} diff --git a/CapMonsterCloud.Client/Requests/HuntCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/HuntCustomTaskRequest.cs new file mode 100644 index 0000000..61abbf7 --- /dev/null +++ b/CapMonsterCloud.Client/Requests/HuntCustomTaskRequest.cs @@ -0,0 +1,48 @@ +namespace Zennolab.CapMonsterCloud.Requests +{ + /// + /// Hunt CustomTask recognition request. + /// Hunt CAPTCHA is an anti-bot system used on betting platforms to detect automated activity. + /// + /// + /// For this task, use your own proxies. + /// Our solving system has two operating modes: X-HD generation (fingerprint) and CAPTCHA solving. + /// If you only want to generate X-HD, do not pass the data parameter. + /// If you need to solve the CAPTCHA, pass the token that the target site provides during certain actions (for example, when requesting an SMS). + /// + /// + /// https://docs.capmonster.cloud/docs/captchas/hunt-task + /// + public sealed class HuntCustomTaskRequest : CustomTaskRequestBase + { + /// + public override string Class => "Hunt"; + + /// + /// Initializes Hunt task with required metadata for X-HD generation mode. + /// + /// + /// + /// "https://example.com/hd-api/external/apps/a2157wab1045d68672a63557e0n2a77edbfd15ea/api.js" + /// The full link to the api.js file. + /// You can find this link in DevTools (the Network or Elements tabs) on the page with the Hunt CAPTCHA. + /// + public HuntCustomTaskRequest(string apiGetLib) => Metadata = new { apiGetLib }; + + /// + /// Initializes Hunt task with required metadata for CAPTCHA solving mode. + /// + /// + /// + /// "https://example.com/hd-api/external/apps/a2157wab1045d68672a63557e0n2a77edbfd15ea/api.js" + /// The full link to the api.js file. You can find this link in DevTools (the Network or Elements tabs) on the page with the Hunt CAPTCHA. + /// Use keyword search such as: hd-api or api.js. + /// + /// + /// + /// "kufyHK/s/jTNU...AfwIW" + /// The data parameter must be specified when using CAPTCHA solving mode + /// + public HuntCustomTaskRequest(string apiGetLib, string data) => Metadata = new { apiGetLib, data }; + } +} diff --git a/CapMonsterCloud.Client/Requests/TspdCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/TspdCustomTaskRequest.cs new file mode 100644 index 0000000..ad6d879 --- /dev/null +++ b/CapMonsterCloud.Client/Requests/TspdCustomTaskRequest.cs @@ -0,0 +1,31 @@ +namespace Zennolab.CapMonsterCloud.Requests +{ + /// + /// TSPD CustomTask recognition request. + /// TSPD is a WAF (Web Application Firewall) protection designed to prevent + /// automated attacks and suspicious activity on a website. + /// + /// + /// https://docs.capmonster.cloud/docs/captchas/tspd-task + /// + public sealed class TspdCustomTaskRequest : CustomTaskRequestBase + { + /// + public override string Class => "Tspd"; + + /// + /// Initializes TSPD task with required metadata. + /// + /// + /// + /// "TS386a400d029=082670627aab2800722d179e73a60b575d00c96728a9f8dedd8be27a40f6a1aa5df467cebf7da7315a4e16675f010245; ....; ....;" + /// Cookies obtained on the TSPD challenge page. + /// + /// + /// + /// "PCFET0NUWVBFIGh0bWw+DQo8aHRtbD48aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IlByYWdtYSIgY29udGVudD0ibm8tY2FjaGUiLz4..." + /// The entire TSPD page encoded in base64. + /// + public TspdCustomTaskRequest(string tspdCookie, string htmlPageBase64) => Metadata = new { tspdCookie, htmlPageBase64 }; + } +}