From c4fea455b463dba5ed94a2caaab17f7e0f82a734 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Thu, 12 Mar 2026 19:04:24 +0100 Subject: [PATCH 1/7] Added AltchaCustomTask --- .../IntegrationTests.cs | 46 ++++++++++++++++ .../ObjectGen.cs | 27 ++++++++++ .../Sut.cs | 3 ++ ...CapMonsterCloudClient_GetResultTimeouts.cs | 9 ++++ .../Requests/AltchaCustomTaskRequest.cs | 53 +++++++++++++++++++ .../Responses/AltchaTaskResponse.cs | 9 ++++ 6 files changed, 147 insertions(+) create mode 100644 CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs create mode 100644 CapMonsterCloud.Client/Responses/AltchaTaskResponse.cs diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index cfcf589..741a89c 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -1249,5 +1249,51 @@ 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); + } } } \ No newline at end of file diff --git a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs index 0abcf17..a44a0a9 100644 --- a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs @@ -483,6 +483,33 @@ 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 class AmazonWaf diff --git a/CapMonsterCloud.Client.IntegrationTests/Sut.cs b/CapMonsterCloud.Client.IntegrationTests/Sut.cs index 8ed962a..adb7e88 100644 --- a/CapMonsterCloud.Client.IntegrationTests/Sut.cs +++ b/CapMonsterCloud.Client.IntegrationTests/Sut.cs @@ -110,6 +110,9 @@ 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 GetBalanceAsync() { return await _cloudClient.GetBalanceAsync(); diff --git a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs index c587a40..f3d09b8 100644 --- a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs +++ b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs @@ -222,6 +222,15 @@ 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) + } + }, }; } } diff --git a/CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs new file mode 100644 index 0000000..21a7d1a --- /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/Responses/AltchaTaskResponse.cs b/CapMonsterCloud.Client/Responses/AltchaTaskResponse.cs new file mode 100644 index 0000000..4f85946 --- /dev/null +++ b/CapMonsterCloud.Client/Responses/AltchaTaskResponse.cs @@ -0,0 +1,9 @@ +namespace Zennolab.CapMonsterCloud.Responses +{ + /// + /// Altcha recognition response + /// + public sealed class AltchaTaskResponse : CaptchaResponseBase + { + } +} From fd43bc31f2efd91e6777d7aeeb637fcd95dee570 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Thu, 12 Mar 2026 19:11:33 +0100 Subject: [PATCH 2/7] Class name starts with capital letter --- CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs index 21a7d1a..b7277a3 100644 --- a/CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs +++ b/CapMonsterCloud.Client/Requests/AltchaCustomTaskRequest.cs @@ -12,7 +12,7 @@ namespace Zennolab.CapMonsterCloud.Requests public sealed class AltchaCustomTaskRequest : CustomTaskRequestBase { /// - public override string Class => "altcha"; + public override string Class => "Altcha"; /// /// For this task, sending an empty string is allowed. From 94f44742cc63042825687badbe60410820e5d50b Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Fri, 13 Mar 2026 12:04:11 +0100 Subject: [PATCH 3/7] Added CastleCustomTask --- .../IntegrationTests.cs | 47 ++++++++++++++++ .../ObjectGen.cs | 36 ++++++++++++ .../Sut.cs | 3 + ...CapMonsterCloudClient_GetResultTimeouts.cs | 9 +++ .../Requests/CastleCustomTaskRequest.cs | 55 +++++++++++++++++++ 5 files changed, 150 insertions(+) create mode 100644 CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index 741a89c..d232be8 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -1295,5 +1295,52 @@ public async Task Altcha_ShouldSolve() 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); + } } } \ No newline at end of file diff --git a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs index a44a0a9..f6dbd21 100644 --- a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs @@ -510,6 +510,42 @@ public static CaptchaResult CreateAltchaSolution() } }; } + + 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 class AmazonWaf diff --git a/CapMonsterCloud.Client.IntegrationTests/Sut.cs b/CapMonsterCloud.Client.IntegrationTests/Sut.cs index adb7e88..81f5f4b 100644 --- a/CapMonsterCloud.Client.IntegrationTests/Sut.cs +++ b/CapMonsterCloud.Client.IntegrationTests/Sut.cs @@ -113,6 +113,9 @@ public async Task> SolveAsync( public async Task> SolveAsync( AltchaCustomTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task> SolveAsync( + CastleCustomTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task GetBalanceAsync() { return await _cloudClient.GetBalanceAsync(); diff --git a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs index f3d09b8..65c29af 100644 --- a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs +++ b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs @@ -231,6 +231,15 @@ private static GetResultTimeouts GetTimeouts(Type type) Timeout = TimeSpan.FromSeconds(50) } }, + { + typeof(CastleCustomTaskRequest), + new GetResultTimeouts + { + FirstRequestDelay = TimeSpan.FromSeconds(1), + RequestsInterval = TimeSpan.FromSeconds(2), + Timeout = TimeSpan.FromSeconds(60) + } + }, }; } } diff --git a/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs new file mode 100644 index 0000000..b692edd --- /dev/null +++ b/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs @@ -0,0 +1,55 @@ +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 = null) => Metadata = count.HasValue + ? new { wUrl, swUrl, count = count.Value } + : (object)new { wUrl, swUrl }; + } +} From ded4899c24b443d9a5c251a4a322bd8d9cf6df3a Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Fri, 13 Mar 2026 12:08:41 +0100 Subject: [PATCH 4/7] Removed AltchaTaskResponse --- CapMonsterCloud.Client/Responses/AltchaTaskResponse.cs | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 CapMonsterCloud.Client/Responses/AltchaTaskResponse.cs diff --git a/CapMonsterCloud.Client/Responses/AltchaTaskResponse.cs b/CapMonsterCloud.Client/Responses/AltchaTaskResponse.cs deleted file mode 100644 index 4f85946..0000000 --- a/CapMonsterCloud.Client/Responses/AltchaTaskResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Zennolab.CapMonsterCloud.Responses -{ - /// - /// Altcha recognition response - /// - public sealed class AltchaTaskResponse : CaptchaResponseBase - { - } -} From 9a7b96bd164161b4b8f3a4a6c1c3e052071c1c64 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Fri, 13 Mar 2026 12:32:05 +0100 Subject: [PATCH 5/7] Added tspd custom task --- .../IntegrationTests.cs | 46 +++++++++++++++++++ .../ObjectGen.cs | 39 ++++++++++++++++ .../Sut.cs | 3 ++ ...CapMonsterCloudClient_GetResultTimeouts.cs | 9 ++++ .../Requests/CastleCustomTaskRequest.cs | 5 +- .../Requests/TspdCustomTaskRequest.cs | 31 +++++++++++++ 6 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 CapMonsterCloud.Client/Requests/TspdCustomTaskRequest.cs diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index d232be8..10fb027 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -1342,5 +1342,51 @@ public async Task Castle_ShouldSolve() 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); + } } } \ No newline at end of file diff --git a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs index f6dbd21..4152a0e 100644 --- a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs @@ -546,6 +546,45 @@ public static CaptchaResult CreateCastleSolution() } }; } + + 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 class AmazonWaf diff --git a/CapMonsterCloud.Client.IntegrationTests/Sut.cs b/CapMonsterCloud.Client.IntegrationTests/Sut.cs index 81f5f4b..80d42bf 100644 --- a/CapMonsterCloud.Client.IntegrationTests/Sut.cs +++ b/CapMonsterCloud.Client.IntegrationTests/Sut.cs @@ -116,6 +116,9 @@ public async Task> SolveAsync( public async Task> SolveAsync( CastleCustomTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task> SolveAsync( + TspdCustomTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task GetBalanceAsync() { return await _cloudClient.GetBalanceAsync(); diff --git a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs index 65c29af..52291ff 100644 --- a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs +++ b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs @@ -240,6 +240,15 @@ private static GetResultTimeouts GetTimeouts(Type type) Timeout = TimeSpan.FromSeconds(60) } }, + { + typeof(TspdCustomTaskRequest), + new GetResultTimeouts + { + FirstRequestDelay = TimeSpan.FromSeconds(1), + RequestsInterval = TimeSpan.FromSeconds(2), + Timeout = TimeSpan.FromSeconds(60) + } + }, }; } } diff --git a/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs index b692edd..b679d64 100644 --- a/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs +++ b/CapMonsterCloud.Client/Requests/CastleCustomTaskRequest.cs @@ -48,8 +48,7 @@ public sealed class CastleCustomTaskRequest : CustomTaskRequestBase /// 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 = null) => Metadata = count.HasValue - ? new { wUrl, swUrl, count = count.Value } - : (object)new { wUrl, swUrl }; + public CastleCustomTaskRequest(string wUrl, string swUrl, int count = 1) => + Metadata = new { wUrl, swUrl, count }; } } 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 }; + } +} From 7a4f3ee56412a74ea9e29a4b1b6e7bc6758b8908 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Fri, 13 Mar 2026 13:10:05 +0100 Subject: [PATCH 6/7] Added hunt custom task --- .../IntegrationTests.cs | 46 ++++++++++++++++++ .../ObjectGen.cs | 25 ++++++++++ .../Sut.cs | 3 ++ ...CapMonsterCloudClient_GetResultTimeouts.cs | 9 ++++ .../Requests/HuntCustomTaskRequest.cs | 48 +++++++++++++++++++ 5 files changed, 131 insertions(+) create mode 100644 CapMonsterCloud.Client/Requests/HuntCustomTaskRequest.cs diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index 10fb027..d8f8d77 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -1388,5 +1388,51 @@ public async Task Tspd_ShouldSolve() 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 4152a0e..e643206 100644 --- a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs @@ -585,6 +585,31 @@ public static CaptchaResult CreateTspdSolution() } }; } + + 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 80d42bf..d7c6dba 100644 --- a/CapMonsterCloud.Client.IntegrationTests/Sut.cs +++ b/CapMonsterCloud.Client.IntegrationTests/Sut.cs @@ -119,6 +119,9 @@ public async Task> SolveAsync( 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/CapMonsterCloudClient_GetResultTimeouts.cs b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs index 52291ff..bab1a1d 100644 --- a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs +++ b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs @@ -249,6 +249,15 @@ private static GetResultTimeouts GetTimeouts(Type type) 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/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 }; + } +} From dc1cbb51e48bbc6c35831c62abdbf8af9aa135cc Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Fri, 13 Mar 2026 15:30:40 +0100 Subject: [PATCH 7/7] Up version --- CapMonsterCloud.Client/CapMonsterCloud.Client.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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