altyzen-agi-browser / stealth_browser.py
Yuvan666's picture
fix: Update playwright-stealth usage to v2.0 API
72d6b08
"""
Group 1: Physical Hands - Stealth Browser Module
=================================================
This module provides a "Digital Human" browser instance with:
1. Stealth Layer (playwright-stealth)
2. Natural Mouse Movement (Bezier curves)
3. Mobile Emulation
4. Captcha Detection Hooks
"""
import asyncio
import random
from typing import Optional, Tuple
from playwright.async_api import async_playwright, Page, Browser, BrowserContext
from playwright_stealth.stealth import Stealth
# ============================================================================
# CONSTANTS
# ============================================================================
MOBILE_DEVICE = {
"viewport": {"width": 390, "height": 844},
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
"device_scale_factor": 3,
"is_mobile": True,
"has_touch": True,
}
DESKTOP_DEVICE = {
"viewport": {"width": 1920, "height": 1080},
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"device_scale_factor": 1,
"is_mobile": False,
"has_touch": False,
}
# ============================================================================
# NATURAL MOUSE MOVEMENT (Bezier Curve Simulation)
# ============================================================================
def bezier_curve(t: float, p0: tuple, p1: tuple, p2: tuple, p3: tuple) -> tuple:
"""Calculate point on a cubic Bezier curve at parameter t (0 to 1)."""
x = (1-t)**3 * p0[0] + 3*(1-t)**2 * t * p1[0] + 3*(1-t) * t**2 * p2[0] + t**3 * p3[0]
y = (1-t)**3 * p0[1] + 3*(1-t)**2 * t * p1[1] + 3*(1-t) * t**2 * p2[1] + t**3 * p3[1]
return (x, y)
def generate_human_path(start: tuple, end: tuple, steps: int = 25) -> list:
"""
Generate a human-like mouse path using Bezier curves with randomness.
Includes slight overshoot and correction.
"""
# Random control points for natural curve
dx = end[0] - start[0]
dy = end[1] - start[1]
# Control points with randomness
cp1 = (
start[0] + dx * random.uniform(0.2, 0.4) + random.randint(-50, 50),
start[1] + dy * random.uniform(0.2, 0.4) + random.randint(-50, 50)
)
cp2 = (
start[0] + dx * random.uniform(0.6, 0.8) + random.randint(-50, 50),
start[1] + dy * random.uniform(0.6, 0.8) + random.randint(-50, 50)
)
# Generate path points
path = []
for i in range(steps):
t = i / (steps - 1)
# Add micro-jitter
point = bezier_curve(t, start, cp1, cp2, end)
jitter_x = random.uniform(-2, 2)
jitter_y = random.uniform(-2, 2)
path.append((point[0] + jitter_x, point[1] + jitter_y))
# Add overshoot and correction (human behavior)
overshoot = (
end[0] + random.randint(5, 15) * (1 if random.random() > 0.5 else -1),
end[1] + random.randint(5, 15) * (1 if random.random() > 0.5 else -1)
)
path.append(overshoot)
path.append(end) # Correction back to target
return path
async def human_move_to(page: Page, x: int, y: int):
"""Move mouse to coordinates with human-like behavior."""
# Get current mouse position (approximate from viewport center if unknown)
current_x = page.viewport_size["width"] // 2
current_y = page.viewport_size["height"] // 2
path = generate_human_path((current_x, current_y), (x, y))
for point in path:
await page.mouse.move(point[0], point[1])
# Variable delay between movements (faster in middle, slower at ends)
await asyncio.sleep(random.uniform(0.01, 0.05))
async def human_click(page: Page, selector: str):
"""Click an element with human-like mouse movement and timing."""
# Get element bounding box
element = await page.query_selector(selector)
if not element:
raise Exception(f"Element not found: {selector}")
box = await element.bounding_box()
if not box:
raise Exception(f"Element has no bounding box: {selector}")
# Click at random position within element (not dead center)
target_x = box["x"] + box["width"] * random.uniform(0.3, 0.7)
target_y = box["y"] + box["height"] * random.uniform(0.3, 0.7)
# Move to target with human path
await human_move_to(page, int(target_x), int(target_y))
# Pre-click delay (human hesitation)
await asyncio.sleep(random.uniform(0.1, 0.3))
# Click
await page.mouse.click(target_x, target_y)
# Post-click delay
await asyncio.sleep(random.uniform(0.1, 0.2))
async def human_type(page: Page, selector: str, text: str):
"""Type text with human-like delays between keystrokes."""
await human_click(page, selector)
for char in text:
await page.keyboard.type(char)
# Variable typing speed
await asyncio.sleep(random.uniform(0.05, 0.15))
# ============================================================================
# STEALTH BROWSER FACTORY
# ============================================================================
class StealthBrowser:
"""Factory for creating stealth browser instances."""
def __init__(self, proxy: dict = None, mobile: bool = False, headless: bool = True):
self.proxy = proxy
self.mobile = mobile
self.headless = headless
self.playwright = None
self.browser: Browser = None
self.context: BrowserContext = None
self.page: Page = None
async def start(self) -> Page:
"""Initialize and return a stealth browser page."""
self.playwright = await async_playwright().start()
# Browser launch arguments for maximum stealth
launch_args = [
"--disable-blink-features=AutomationControlled",
"--disable-infobars",
"--disable-dev-shm-usage",
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-gpu",
"--disable-extensions",
]
# Launch browser
self.browser = await self.playwright.chromium.launch(
headless=self.headless,
args=launch_args
)
# Select device profile
device = MOBILE_DEVICE if self.mobile else DESKTOP_DEVICE
# Context options
context_options = {
"viewport": device["viewport"],
"user_agent": device["user_agent"],
"device_scale_factor": device["device_scale_factor"],
"is_mobile": device["is_mobile"],
"has_touch": device["has_touch"],
"locale": "en-US",
"timezone_id": "America/New_York",
}
# Add proxy if provided
if self.proxy:
context_options["proxy"] = self.proxy
# Create context
self.context = await self.browser.new_context(**context_options)
# Create page
self.page = await self.context.new_page()
# Apply stealth patches
stealth = Stealth()
await stealth.apply_stealth_async(self.page)
# Inject additional anti-detection scripts
await self._inject_anti_detection()
return self.page
async def _inject_anti_detection(self):
"""Inject JavaScript to further hide automation indicators."""
await self.page.add_init_script("""
// Override navigator.webdriver
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
// Override permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications' ?
Promise.resolve({ state: Notification.permission }) :
originalQuery(parameters)
);
// Override plugins
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
});
// Override languages
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en']
});
""")
async def goto_with_stealth(self, url: str, wait_until: str = "domcontentloaded"):
"""Navigate to URL with human-like behavior."""
# Random delay before navigation (human thinking time)
await asyncio.sleep(random.uniform(0.5, 1.5))
await self.page.goto(url, wait_until=wait_until)
# Random scroll after page load (human behavior)
await asyncio.sleep(random.uniform(0.5, 1.0))
await self.page.evaluate("window.scrollBy(0, Math.random() * 100)")
async def close(self):
"""Clean up browser resources."""
if self.context:
await self.context.close()
if self.browser:
await self.browser.close()
if self.playwright:
await self.playwright.stop()
# ============================================================================
# CAPTCHA DETECTION
# ============================================================================
async def detect_captcha(page: Page) -> Optional[str]:
"""
Detect if a captcha is present on the page.
Returns captcha type or None.
"""
# Check for reCAPTCHA
recaptcha = await page.query_selector('iframe[src*="recaptcha"]')
if recaptcha:
return "recaptcha"
# Check for Cloudflare Turnstile
turnstile = await page.query_selector('iframe[src*="challenges.cloudflare.com"]')
if turnstile:
return "turnstile"
# Check for hCaptcha
hcaptcha = await page.query_selector('iframe[src*="hcaptcha.com"]')
if hcaptcha:
return "hcaptcha"
# Check for Cloudflare challenge page
cf_challenge = await page.query_selector('#cf-challenge-running')
if cf_challenge:
return "cloudflare_challenge"
return None
# ============================================================================
# CONVENIENCE EXPORTS
# ============================================================================
async def create_stealth_page(proxy: dict = None, mobile: bool = False, headless: bool = True) -> Tuple[Page, 'StealthBrowser']:
"""
Convenience function to create a stealth browser page.
Returns (page, browser_instance) tuple.
"""
browser = StealthBrowser(proxy=proxy, mobile=mobile, headless=headless)
page = await browser.start()
return page, browser
# ============================================================================
# EXAMPLE USAGE (for testing)
# ============================================================================
if __name__ == "__main__":
async def test():
page, browser = await create_stealth_page(mobile=False, headless=False)
try:
await browser.goto_with_stealth("https://bot.sannysoft.com/")
await asyncio.sleep(5) # Let user see the result
# Check for captcha
captcha = await detect_captcha(page)
print(f"Captcha detected: {captcha}")
finally:
await browser.close()
asyncio.run(test())