feat(evasion): release v1.0.1 hotfix (GhostCursor Integration)
- Integrated GhostCursorEngine into CamoufoxManager.navigate() - Implemented random viewport targeting and micro-movements post-load - Added integration tests verifying call sequence - Fixed manual TLS verification script - Verified Scaling Limits (2.0 CPU)
This commit is contained in:
parent
73882fac38
commit
ad394d38e7
4 changed files with 72 additions and 4 deletions
|
|
@ -44,6 +44,7 @@ Transition the system from a functional prototype to a scalable, production-read
|
||||||
## v1.0.1 Hotfix Plan
|
## v1.0.1 Hotfix Plan
|
||||||
- **Goal**: Enhance evasion by integrating GhostCursor into standard navigation and re-verifying constraints.
|
- **Goal**: Enhance evasion by integrating GhostCursor into standard navigation and re-verifying constraints.
|
||||||
- **Changes**:
|
- **Changes**:
|
||||||
- `src/browser/manager.py`: Integrate `GhostCursorEngine` for human-like movement during navigation.
|
- `src/browser/manager.py`: Integrate `GhostCursorEngine` for human-like movement during navigation (**Completed**).
|
||||||
- **Verification**: Run unit tests (`test_ghost_cursor`) and TLS checks (`verify_tls`).
|
- **Verification**: Run unit tests (`test_ghost_cursor`) and TLS checks (`verify_tls`) (**Verified**).
|
||||||
- **Ops**: Verify `docker-compose.yml` CPU limits for Bezier overhead.
|
- **Ops**: Verify `docker-compose.yml` CPU limits for Bezier overhead (**Verified**).
|
||||||
|
- **Governance**: **APPROVED** (Director: High Efficacy; Tech Lead: Arch Aligned).
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import logging
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
from playwright.async_api import async_playwright, BrowserContext, Page, Browser
|
from playwright.async_api import async_playwright, BrowserContext, Page, Browser
|
||||||
from src.core.session import SessionState
|
from src.core.session import SessionState
|
||||||
|
from src.browser.ghost_cursor import GhostCursorEngine
|
||||||
|
import random
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
|
@ -25,6 +27,7 @@ class CamoufoxManager:
|
||||||
self.page: Optional[Page] = None
|
self.page: Optional[Page] = None
|
||||||
# Updated to Chrome 124 to align with newer Playwright builds and curl_cffi support
|
# Updated to Chrome 124 to align with newer Playwright builds and curl_cffi support
|
||||||
self._dummy_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
self._dummy_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||||
|
self.ghost_cursor = GhostCursorEngine()
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
await self.initialize()
|
await self.initialize()
|
||||||
|
|
@ -85,6 +88,15 @@ class CamoufoxManager:
|
||||||
raise RuntimeError("Browser not initialized")
|
raise RuntimeError("Browser not initialized")
|
||||||
logger.info(f"Navigating to {url}")
|
logger.info(f"Navigating to {url}")
|
||||||
await self.page.goto(url, wait_until='domcontentloaded')
|
await self.page.goto(url, wait_until='domcontentloaded')
|
||||||
|
|
||||||
|
# v1.0.1 Hotfix: Human Mimesis
|
||||||
|
# Move mouse to a random position within viewport (1920x1080)
|
||||||
|
target_x = random.randint(100, 1800)
|
||||||
|
target_y = random.randint(100, 900)
|
||||||
|
await self.ghost_cursor.move_to(self.page, target_x, target_y)
|
||||||
|
|
||||||
|
# Simulate reading/fidgeting
|
||||||
|
await self.ghost_cursor.random_micro_movement(self.page)
|
||||||
|
|
||||||
async def extract_session_state(self) -> SessionState:
|
async def extract_session_state(self) -> SessionState:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ async def verify_tls():
|
||||||
# Use the session state from browser to ensure same UA/headers context
|
# Use the session state from browser to ensure same UA/headers context
|
||||||
async with CurlClient(session_state) as client:
|
async with CurlClient(session_state) as client:
|
||||||
response = await client.fetch(TARGET_URL)
|
response = await client.fetch(TARGET_URL)
|
||||||
data = response.json()
|
data = json.loads(response)
|
||||||
|
|
||||||
client_ja3 = data.get('ja3_hash', 'UNKNOWN')
|
client_ja3 = data.get('ja3_hash', 'UNKNOWN')
|
||||||
client_ua = data.get('user_agent', 'UNKNOWN')
|
client_ua = data.get('user_agent', 'UNKNOWN')
|
||||||
|
|
|
||||||
55
tests/unit/test_manager_integration.py
Normal file
55
tests/unit/test_manager_integration.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
from src.browser.manager import CamoufoxManager
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_manager_integration_ghost_cursor():
|
||||||
|
"""
|
||||||
|
Verify that CamoufoxManager.navigate calls GhostCursor methods.
|
||||||
|
"""
|
||||||
|
# Mock GhostCursorEngine within the manager module context if possible,
|
||||||
|
# or better, mock it via patch on the class instance after init if easier,
|
||||||
|
# but since it's instantiated in __init__, we patch the class in the module.
|
||||||
|
|
||||||
|
with patch('src.browser.manager.GhostCursorEngine') as MockGhostEngine, \
|
||||||
|
patch('src.browser.manager.async_playwright') as MockPlaywright:
|
||||||
|
|
||||||
|
# Setup Mocks
|
||||||
|
mock_engine_instance = MockGhostEngine.return_value
|
||||||
|
mock_engine_instance.move_to = AsyncMock()
|
||||||
|
mock_engine_instance.random_micro_movement = AsyncMock()
|
||||||
|
|
||||||
|
mock_pw_context = AsyncMock()
|
||||||
|
MockPlaywright.return_value.start.return_value = mock_pw_context
|
||||||
|
# Mock browser, context, page chain...
|
||||||
|
# CamoufoxManager.initialize is complex to mock fully without heavy boilerplate.
|
||||||
|
# We can try to rely on the fact that navigate checks self.page.
|
||||||
|
|
||||||
|
# Let's instantiate manager
|
||||||
|
manager = CamoufoxManager()
|
||||||
|
|
||||||
|
# Manually set the mock page to bypass initialize() complexity if we just want to test navigate logic
|
||||||
|
mock_page = AsyncMock()
|
||||||
|
manager.page = mock_page
|
||||||
|
|
||||||
|
# We also need to inject our mock engine instance if the init already ran?
|
||||||
|
# Yes, line `self.ghost_cursor = GhostCursorEngine()` ran during init.
|
||||||
|
# So manager.ghost_cursor is the mock_engine_instance provided by patch.
|
||||||
|
|
||||||
|
# EXECUTE
|
||||||
|
await manager.navigate("http://example.com")
|
||||||
|
|
||||||
|
# VERIFY
|
||||||
|
# 1. Page goto called
|
||||||
|
mock_page.goto.assert_awaited_once_with("http://example.com", wait_until='domcontentloaded')
|
||||||
|
|
||||||
|
# 2. GhostCursor move_to called
|
||||||
|
assert mock_engine_instance.move_to.called
|
||||||
|
args = mock_engine_instance.move_to.call_args[0]
|
||||||
|
# args: (page, x, y)
|
||||||
|
assert args[0] == mock_page
|
||||||
|
assert 100 <= args[1] <= 1800 # x check
|
||||||
|
assert 100 <= args[2] <= 900 # y check
|
||||||
|
|
||||||
|
# 3. Random micro movement called
|
||||||
|
mock_engine_instance.random_micro_movement.assert_awaited_once_with(mock_page)
|
||||||
Loading…
Reference in a new issue