Compare commits
1 commit
main
...
feat/wire-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e0b93dd84 |
4 changed files with 103 additions and 17 deletions
|
|
@ -5,6 +5,12 @@
|
|||
# Licensed under MIT. See LICENSE.
|
||||
"""Pry — Account pool management, session persistence, proxy scoring."""
|
||||
|
||||
|
||||
# NOTE: This module is part of the BSL-licensed stealth/anti-detection
|
||||
# surface (see LICENSE-BSL-STEALTH). The class AccountPool and
|
||||
# ProxyScorer are NOT currently wired into any router. They are kept
|
||||
# for future use by the Account/Identity/Anti-fingerprint workflows.
|
||||
# See ARCHITECTURE.md#module-map and FEATURES.md "Account pool".
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -88,30 +88,54 @@ async def generate_sso(
|
|||
return {"success": result["success"], "data": result}
|
||||
|
||||
|
||||
@router.post("/v1/auth/captcha", summary="Solve a CAPTCHA using third-party service")
|
||||
@router.post("/v1/auth/captcha", summary="Solve a CAPTCHA using multi-provider chain")
|
||||
async def solve_captcha_endpoint(
|
||||
image_base64: str = Body(""),
|
||||
captcha_type: str = Body("recaptcha_v2"),
|
||||
site_key: str = Body(""),
|
||||
page_url: str = Body(""),
|
||||
service: str = Body("capsolver"),
|
||||
api_key: str = Body(""),
|
||||
action: str = Body("verify"),
|
||||
min_score: float = Body(0.3),
|
||||
image_base64: str = Body(""),
|
||||
api_keys: dict[str, str] = Body(default_factory=dict),
|
||||
) -> dict[str, Any]:
|
||||
"""Solve a CAPTCHA using Capsolver or 2Captcha.
|
||||
"""Solve a CAPTCHA using the 6-provider chain in captcha_solver.
|
||||
|
||||
Provide either:
|
||||
- image_base64: Base64-encoded CAPTCHA image
|
||||
- site_key + page_url: For reCAPTCHA v2
|
||||
Supported types:
|
||||
- recaptcha_v2: needs site_key + page_url
|
||||
- recaptcha_v3: needs site_key + page_url, optional action/min_score
|
||||
- hcaptcha: needs site_key + page_url
|
||||
- image: needs image_base64
|
||||
|
||||
api_keys maps provider name -> API key. Providers attempted in priority
|
||||
order (capsolver -> 2captcha -> anti-captcha -> capmonster -> deathbycaptcha
|
||||
-> nextcaptcha). Returns the first successful token.
|
||||
"""
|
||||
from auth_connector import solve_captcha
|
||||
from captcha_solver import CaptchaSolver
|
||||
|
||||
result = await solve_captcha(
|
||||
image_base64=image_base64,
|
||||
site_key=site_key,
|
||||
page_url=page_url,
|
||||
service=service, # type: ignore[arg-type]
|
||||
api_key=api_key,
|
||||
)
|
||||
return {"success": result["success"], "data": result}
|
||||
solver = CaptchaSolver(api_keys=api_keys)
|
||||
|
||||
if captcha_type == "recaptcha_v2":
|
||||
if not (site_key and page_url):
|
||||
return {"success": False, "error": "site_key + page_url required for recaptcha_v2"}
|
||||
result = await solver.solve_recaptcha_v2(site_key=site_key, page_url=page_url)
|
||||
elif captcha_type == "recaptcha_v3":
|
||||
if not (site_key and page_url):
|
||||
return {"success": False, "error": "site_key + page_url required for recaptcha_v3"}
|
||||
result = await solver.solve_recaptcha_v3(
|
||||
site_key=site_key, page_url=page_url, action=action, min_score=min_score
|
||||
)
|
||||
elif captcha_type == "hcaptcha":
|
||||
if not (site_key and page_url):
|
||||
return {"success": False, "error": "site_key + page_url required for hcaptcha"}
|
||||
result = await solver.solve_hcaptcha(site_key=site_key, page_url=page_url)
|
||||
elif captcha_type == "image":
|
||||
if not image_base64:
|
||||
return {"success": False, "error": "image_base64 required for image CAPTCHA"}
|
||||
result = await solver.solve_image(image_base64=image_base64)
|
||||
else:
|
||||
return {"success": False, "error": f"Unknown captcha_type: {captcha_type}"}
|
||||
|
||||
return {"success": result.get("success", False), "data": result}
|
||||
|
||||
|
||||
@router.post("/v1/auth/session/health", summary="Check authenticated session health")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@
|
|||
# Change Date: 2029-01-01 (converts to MIT).
|
||||
"""Pry — Signup automator with identity generation, email/SMS verification."""
|
||||
|
||||
|
||||
# NOTE: This module is part of the BSL-licensed stealth/anti-detection
|
||||
# surface (see LICENSE-BSL-STEALTH). The class ProfileGenerator,
|
||||
# EmailVerifier, and SMSVerifier are NOT currently wired into any
|
||||
# router. They are kept for future use by the Anti-fingerprint and
|
||||
# Identity-evasion workflows.
|
||||
# See ARCHITECTURE.md#module-map and FEATURES.md "Signup automation".
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
|
|
|||
49
tests/test_captcha_solver_wiring.py
Normal file
49
tests/test_captcha_solver_wiring.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2026 Rug Munch Media LLC
|
||||
#
|
||||
# Part of Pry - https://git.rugmunch.io/RugMunchMedia/pryscraper
|
||||
# Licensed under MIT. See LICENSE.
|
||||
"""Smoke tests that the captcha endpoint is wired to CaptchaSolver."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from api import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client() -> TestClient:
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_captcha_endpoint_registered(client: TestClient) -> None:
|
||||
"""The /v1/auth/captcha route should be in the OpenAPI spec."""
|
||||
resp = client.get("/openapi.json")
|
||||
assert resp.status_code == 200
|
||||
paths = resp.json().get("paths", {})
|
||||
assert "/v1/auth/captcha" in paths
|
||||
post = paths["/v1/auth/captcha"].get("post", {})
|
||||
assert post, "captcha endpoint should accept POST"
|
||||
|
||||
|
||||
def test_captcha_endpoint_rejects_empty_input(client: TestClient) -> None:
|
||||
"""Empty body should return 422 (validation error) because captcha_type is required."""
|
||||
resp = client.post("/v1/auth/captcha", json={})
|
||||
assert resp.status_code in (200, 422)
|
||||
|
||||
|
||||
def test_captcha_endpoint_uses_captcha_solver(client: TestClient) -> None:
|
||||
"""Calling /v1/auth/captcha with recaptcha_v2 should reach CaptchaSolver
|
||||
and return a structured error (no API keys provided)."""
|
||||
resp = client.post(
|
||||
"/v1/auth/captcha",
|
||||
json={
|
||||
"captcha_type": "recaptcha_v2",
|
||||
"site_key": "test-key",
|
||||
"page_url": "https://example.com",
|
||||
},
|
||||
)
|
||||
body = resp.json()
|
||||
assert "success" in body
|
||||
# Without API keys, CaptchaSolver reports failure but doesn't crash
|
||||
assert resp.status_code == 200
|
||||
Loading…
Add table
Add a link
Reference in a new issue