Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ client = CapMonsterClient(options=options)
async def run_scraper():
# Solve Turnstile challenge via CapMonster Cloud
turnstile_req = TurnstileRequest(
websiteUrl="https://target-website.com/login",
websiteURL="https://target-website.com/login",
Comment thread
grk717 marked this conversation as resolved.
websiteKey="0x4AAAAAAABnPIDnK2k_e-2"
)
solution = await client.solve_captcha(turnstile_req)
Expand Down
36 changes: 21 additions & 15 deletions capmonstercloud_client/requests/AlibabaCustomTaskRequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

ALLOWED_METADATA_KEYS = {
'sceneId', 'prefix', 'userId', 'userUserId', 'verifyType',
'region', 'UserCertifyId', 'apiGetLib', 'cookieRequired',
'region', 'UserCertifyId', 'apiGetLib', 'cookieRequired', 'punishUrl',
}

class AlibabaCustomTaskRequest(CustomTaskRequestBase):
Expand All @@ -15,29 +15,35 @@ class AlibabaCustomTaskRequest(CustomTaskRequestBase):
Attributes:
captchaClass: The constant string value identifying the captcha
class as "alibaba".
metadata: A dictionary of Alibaba-specific parameters. Requires sceneId
and prefix; userId, userUserId, verifyType, region, UserCertifyId,
apiGetLib, and cookieRequired are optional, needed only for sites
that use them.
metadata: A dictionary of Alibaba-specific parameters. Standard mode
requires sceneId and prefix; userId, userUserId, verifyType, region,
UserCertifyId, apiGetLib, and cookieRequired are optional, needed
only for sites that use them. Alternatively, for websites that
trigger verification through a separate "/punish" URL, pass only
punishUrl (the full punish page URL) instead of sceneId/prefix.
"""
captchaClass: str = Field(default='alibaba', description='The constant string value identifying the captcha class as "alibaba".')
metadata: Dict[str, Union[str, bool]] = Field(..., description='A dictionary of Alibaba-specific parameters. Requires sceneId and prefix; userId, userUserId, verifyType, region, UserCertifyId, apiGetLib, and cookieRequired are optional, needed only for sites that use them.')
metadata: Dict[str, Union[str, bool]] = Field(..., description='A dictionary of Alibaba-specific parameters. Requires sceneId and prefix, unless punishUrl is provided instead. userId, userUserId, verifyType, region, UserCertifyId, apiGetLib, and cookieRequired are optional, needed only for sites that use them.')

@field_validator('metadata')
@classmethod
def validate_metadata(cls, value):
if not set(value.keys()).issubset(ALLOWED_METADATA_KEYS):
raise TypeError(f'Allowed keys for metadata are {sorted(ALLOWED_METADATA_KEYS)}')
if value.get('sceneId') is None:
raise TypeError(f'sceneId must be defined inside metadata.')
if value.get('punishUrl') is not None:
if not isinstance(value.get('punishUrl'), str):
raise TypeError(f'punishUrl must be str.')
else:
if not isinstance(value.get('sceneId'), str):
raise TypeError(f'sceneId must be str.')
if value.get('prefix') is None:
raise TypeError(f'prefix must be defined inside metadata.')
else:
if not isinstance(value.get('prefix'), str):
raise TypeError(f'prefix must be str.')
if value.get('sceneId') is None:
raise TypeError(f'sceneId must be defined inside metadata (unless punishUrl is used).')
else:
if not isinstance(value.get('sceneId'), str):
raise TypeError(f'sceneId must be str.')
if value.get('prefix') is None:
raise TypeError(f'prefix must be defined inside metadata (unless punishUrl is used).')
else:
if not isinstance(value.get('prefix'), str):
raise TypeError(f'prefix must be str.')
if value.get('cookieRequired') is not None and not isinstance(value.get('cookieRequired'), bool):
raise TypeError(f'cookieRequired must be bool.')
return value
Expand Down
22 changes: 14 additions & 8 deletions capmonstercloud_client/requests/FuncaptchaRequest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Dict, Union, Optional
from pydantic import Field
from pydantic import Field, model_validator

from .baseRequestWithProxy import BaseRequestWithProxy

Expand All @@ -22,6 +22,7 @@ class FuncaptchaRequest(BaseRequestWithProxy):
provided as a string of key-value pairs.
userAgent: Browser User-Agent to emulate. Pass only a current
Windows OS UA.
proxy: Required. Your own proxy used to solve the task.
"""
type: str = Field(default='FunCaptchaTask', description='The task type identifier, "FunCaptchaTask".')
websiteUrl: str = Field(..., description='The URL of the webpage containing the FunCaptcha challenge.')
Expand All @@ -31,18 +32,23 @@ class FuncaptchaRequest(BaseRequestWithProxy):
cookies: Optional[str] = Field(default=None, description='Cookies to be used when accessing the target webpage, provided as a string of key-value pairs.')
userAgent: Optional[str] = Field(default=None, description='Browser User-Agent to emulate. Pass only a current Windows OS UA.')

@model_validator(mode='before')
def validate_funcaptcha_proxy(cls, values):
if values.get('proxy') is None:
raise RuntimeError(f'You are required to use your own proxies to solve FunCaptcha.')
return values

def getTaskDict(self) -> Dict[str, Union[str, int, bool]]:
task = {}
task['type'] = self.type
task['websiteURL'] = self.websiteUrl
task['websitePublicKey'] = self.websitePublicKey
if self.proxy:
task['proxyType'] = self.proxy.proxyType
task['proxyAddress'] = self.proxy.proxyAddress
task['proxyPort'] = self.proxy.proxyPort
task['proxyLogin'] = self.proxy.proxyLogin
task['proxyPassword'] = self.proxy.proxyPassword

task['proxyType'] = self.proxy.proxyType
task['proxyAddress'] = self.proxy.proxyAddress
task['proxyPort'] = self.proxy.proxyPort
task['proxyLogin'] = self.proxy.proxyLogin
task['proxyPassword'] = self.proxy.proxyPassword

if self.funcaptchaApiJSSubdomain is not None:
task['funcaptchaApiJSSubdomain'] = self.funcaptchaApiJSSubdomain
if self.data is not None:
Expand Down
15 changes: 10 additions & 5 deletions capmonstercloud_client/requests/HuntCustomTaskRequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ class HuntCustomTaskRequest(CustomTaskRequestBase):
captcha class as "HUNT".
metadata: A dictionary of parameters required by the HUNT solver.
Always requires "apiGetLib" (the URL of the HUNT JS script on
the page). HUNT has two solving modes: fingerprint generation
(only "apiGetLib" needed) and captcha solving (also requires
"data", which must hold the "meta.token" value extracted from
the page).
the page). HUNT has three solving modes: fingerprint generation
(only "apiGetLib" needed), captcha solving via "data" (which must
hold the "meta.token" value extracted from the page), and captcha
solving via "widgetUrl" (the full Hunt CAPTCHA widget URL). "data"
and "widgetUrl" must not be used together.
"""
captchaClass: str = Field(default='HUNT', description='The constant string value identifying the underlying captcha class as "HUNT".')
metadata: Dict[str, str] = Field(..., description='Dictionary of HUNT parameters. Always requires "apiGetLib". Also requires "data" (the "meta.token" value from the page) when solving a captcha rather than just generating a fingerprint.')
metadata: Dict[str, str] = Field(..., description='Dictionary of HUNT parameters. Always requires "apiGetLib". Also requires exactly one of "data" (the "meta.token" value from the page) or "widgetUrl" (the full Hunt CAPTCHA widget URL) when solving a captcha rather than just generating a fingerprint.')

@field_validator('metadata')
@classmethod
Expand All @@ -30,6 +31,10 @@ def validate_metadata(cls, value):
raise TypeError(f'apiGetLib must be str.')
if value.get('data') is not None and not isinstance(value.get('data'), str):
raise TypeError(f'data must be str.')
if value.get('widgetUrl') is not None and not isinstance(value.get('widgetUrl'), str):
raise TypeError(f'widgetUrl must be str.')
if value.get('data') is not None and value.get('widgetUrl') is not None:
raise TypeError(f'"data" and "widgetUrl" must not be used together.')
return value

@model_validator(mode='before')
Expand Down
17 changes: 9 additions & 8 deletions capmonstercloud_client/requests/RecaptchaComplexImageTask.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,27 @@ class RecaptchaComplexImageTaskRequest(ComplexImageTaskRequestBase):
image classification challenges.

Attributes:
metadata: A dictionary describing the challenge. Must contain
"Task" (English task name, e.g. "Click on traffic lights") and
"TaskDefinition" (its technical identifier, e.g. "/m/015qff") —
both required together — along with "Grid" (e.g. "3x3", "4x4",
"1x1") specifying the image grid layout.
metadata: A dictionary describing the challenge. Must contain at
least one of "Task" (English task name, e.g. "Click on traffic
lights") or "TaskDefinition" (its technical identifier, e.g.
"/m/015qff") — each is required only if the other is not filled —
along with "Grid" (e.g. "3x3", "4x4", "1x1") specifying the image
grid layout.
captchaClass: The constant string identifying the captcha family
as "recaptcha".
imagesUrls: A collection of image URLs to be recognized. Must be
populated if imagesBase64 is not.
"""

metadata : Dict[str, str] = Field(..., description='Dictionary describing the challenge. Must contain "Task" and "TaskDefinition" (both required together) plus "Grid".')
metadata : Dict[str, str] = Field(..., description='Dictionary describing the challenge. Must contain at least one of "Task" or "TaskDefinition" (each required only if the other is not filled) plus "Grid".')
captchaClass: str = Field(default='recaptcha', description='Constant string identifying the captcha family as "recaptcha".')
imagesUrls: Optional[List[str]] = Field(default=None, description='Collection with image urls. Must be populated if imagesBase64 is not.')

@field_validator('metadata')
@classmethod
def validate_metadata(cls, value):
if value.get('Task') is None or value.get('TaskDefinition') is None:
raise TaskNotDefinedError(f'"Task" and "TaskDefinition" must both be filled.')
if value.get('Task') is None and value.get('TaskDefinition') is None:
raise TaskNotDefinedError(f'At least one of "Task" or "TaskDefinition" must be filled.')
elif value.get('Grid') is None:
raise TaskNotDefinedError(f'"Grid" must be filled (3x3, 4x4, 1x1).')
else:
Expand Down
2 changes: 1 addition & 1 deletion capmonstercloud_client/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.2.0
4.3.0
13 changes: 11 additions & 2 deletions examples/funCaptcha.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import time
import asyncio

from capmonstercloudclient.requests import FuncaptchaRequest
from capmonstercloudclient.requests import FuncaptchaRequest, ProxyInfo
from capmonstercloudclient import ClientOptions, CapMonsterClient

async def solve_captcha_sync(num_requests):
Expand All @@ -17,9 +17,18 @@ async def solve_captcha_async(num_requests):
key = os.getenv('API_KEY')
client_options = ClientOptions(api_key=key)
cap_monster_client = CapMonsterClient(options=client_options)
# FunCaptcha requires your own proxy.
proxy = ProxyInfo(
proxyType="http",
proxyAddress="8.8.8.8",
proxyPort=8080,
proxyLogin="proxyLoginHere",
proxyPassword="proxyPasswordHere"
)
funcaptcha_request = FuncaptchaRequest(websiteUrl='https://example.com/',
websitePublicKey='websitePublicKey',
funcaptchaApiJSSubdomain='funcaptchaApiJSSubdomain')
funcaptchaApiJSSubdomain='funcaptchaApiJSSubdomain',
proxy=proxy)

nums = 3

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
URL='https://github.com/ZennoLab/capmonstercloud-client-python'

this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()
long_description = (this_directory / "README.md").read_text(encoding="utf-8")


setup(
Expand Down
21 changes: 21 additions & 0 deletions test/alibaba_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class AlibabaCustomTaskRequestTest(unittest.TestCase):
userAgentExample = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
sceneIdExample = "1ww7426c"
prefixExample = "dlw3kug"
punishUrlExample = "https://example.com:443//api/example/testlogin/_____tmd_____/punish?x5secdata=xgf6888&x5step=2&action=captchaclick&pureCaptcha="

def setUp(self):
self.proxy = ProxyInfo(
Expand Down Expand Up @@ -112,6 +113,26 @@ def test_alibaba_optional_proxy_and_useragent(self):
self.assertEqual(task_dictionary["proxyType"], self.proxy.proxyType)
self.assertEqual(task_dictionary["userAgent"], self.userAgentExample)

def test_alibaba_punish_url_mode(self):
metadata_example = {
"punishUrl": self.punishUrlExample,
}
request = AlibabaCustomTaskRequest(
websiteUrl=self.websiteUrlExample,
metadata=metadata_example,
)
task_dictionary = request.getTaskDict()
self.assertEqual(task_dictionary["metadata"]["punishUrl"], self.punishUrlExample)
self.assertNotIn("sceneId", task_dictionary["metadata"])
self.assertNotIn("prefix", task_dictionary["metadata"])

def test_alibaba_punish_url_type(self):
base_kwargs = {
"websiteUrl": self.websiteUrlExample,
"metadata": {"punishUrl": 12345},
}
self.assertRaises(ValidationError, AlibabaCustomTaskRequest, **base_kwargs)

def test_alibaba_proxy_not_required(self):
metadata_example = {
"sceneId": self.sceneIdExample,
Expand Down
28 changes: 28 additions & 0 deletions test/hunt_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class HuntCustomTaskRequestTest(unittest.TestCase):
userAgentExample = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
apiGetLibExample = "https://example.com/hd-api/external/apps/a2157wab1045d68672a63557e0n2a77edbfd15ea/api.js"
dataExample = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
widgetUrlExample = "https://captcha.example.com/widget?hash=abc123"

def setUp(self):
self.proxy = ProxyInfo(
Expand Down Expand Up @@ -83,6 +84,33 @@ def test_hunt_missing_fields(self):
base_kwargs.update({"metadata": metadata_example})
HuntCustomTaskRequest(**base_kwargs)

def test_hunt_metadata_with_widget_url(self):
metadata_example = {
"apiGetLib": self.apiGetLibExample,
"widgetUrl": self.widgetUrlExample,
}
request = HuntCustomTaskRequest(
websiteUrl=self.websiteUrlExample,
metadata=metadata_example,
proxy=self.proxy,
)
task_dictionary = request.getTaskDict()
self.assertEqual(task_dictionary["metadata"]["widgetUrl"], self.widgetUrlExample)

def test_hunt_data_and_widget_url_mutually_exclusive(self):
metadata_example = {
"apiGetLib": self.apiGetLibExample,
"data": self.dataExample,
"widgetUrl": self.widgetUrlExample,
}
self.assertRaises(
TypeError,
HuntCustomTaskRequest,
websiteUrl=self.websiteUrlExample,
metadata=metadata_example,
proxy=self.proxy,
)

def test_hunt_requires_proxy(self):
metadata_example = {
"apiGetLib": self.apiGetLibExample,
Expand Down
29 changes: 29 additions & 0 deletions test/recaptcha_image_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ def testTaskDefined(self):
userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36.",
)

def testTaskOrTaskDefinitionAlone(self):
# "Task" alone (no "TaskDefinition") must be accepted.
request = RecaptchaComplexImageTaskRequest(
websiteUrl=RecaptchaImageRequestTest.websiteUrlExample,
metadata={"Task": "Click on traffic lights", "Grid": "3x3"},
imagesUrls=RecaptchaImageRequestTest.imageUrlsExamples,
)
task_dictionary = request.getTaskDict()
self.assertIn("Task", task_dictionary["metadata"])
self.assertNotIn("TaskDefinition", task_dictionary["metadata"])

# "TaskDefinition" alone (no "Task") must be accepted.
request = RecaptchaComplexImageTaskRequest(
websiteUrl=RecaptchaImageRequestTest.websiteUrlExample,
metadata={"TaskDefinition": "/m/015qff", "Grid": "3x3"},
imagesUrls=RecaptchaImageRequestTest.imageUrlsExamples,
)
task_dictionary = request.getTaskDict()
self.assertIn("TaskDefinition", task_dictionary["metadata"])
self.assertNotIn("Task", task_dictionary["metadata"])

# Neither "Task" nor "TaskDefinition" must still fail.
with self.assertRaises(TaskNotDefinedError):
RecaptchaComplexImageTaskRequest(
websiteUrl=RecaptchaImageRequestTest.websiteUrlExample,
metadata={"Grid": "3x3"},
imagesUrls=RecaptchaImageRequestTest.imageUrlsExamples,
)

@unittest.skip("Doesnt work right now")
def testUserAgentWithUrl(self):
with self.assertRaises(UserAgentNotDefinedError):
Expand Down
18 changes: 5 additions & 13 deletions test/requests_generation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,30 +193,22 @@ def test_rcv2_enterprise(self):
)

def test_fc(self):
noproxy_type = "FunCaptchaTask"
# FunCaptcha now requires the caller's own proxy; no-proxy requests must fail.
default_keys = [
"type",
"websiteURL",
"funcaptchaApiJSSubdomain",
"websitePublicKey",
"data",
]
request = requests.FuncaptchaRequest(
self.assertRaises(
RuntimeError,
requests.FuncaptchaRequest,
websiteUrl="some_url",
websitePublicKey="some_key",
funcaptchaApiJSSubdomain="domain",
data="asdfasdf",
)
task = request.getTaskDict()
for key in default_keys:
self.assertIsNotNone(
task.get(key), msg=f"Missing {key} for FunCaptchaTask request."
)
self.assertEqual(
noproxy_type,
task.get("type"),
msg=f"Task type of FunCaptchaTask not equal to {noproxy_type}",
)

proxy_type = "FunCaptchaTask"
proxy_keys = default_keys + PROXY_LIST
Expand All @@ -243,7 +235,7 @@ def test_fc(self):
self.assertEqual(
proxy_type,
proxy_task.get("type"),
msg=f"Task type of FunCaptchaTask not equal to {noproxy_type}",
msg=f"Task type of FunCaptchaTask not equal to {proxy_type}",
)

def test_hc(self):
Expand Down
Loading