Re-license Pry from full Proprietary to a dual-license model: - Core engine, extraction, templates (80+), MCP server, x402 payment rail, CLI, SDK, browser extension, WordPress plugin, Shopify app, and llm_providers: MIT (see LICENSE) - Anti-detection / stealth subset (15 files): BSL 1.1 with Change Date 2029-01-01 (see LICENSE-BSL-STEALTH) BSL files (anti-detection moat): ultimate_scraper.py, stealth_engine.py, stealth_scripts/*.js (6), camoufox_integration.py, tls_fingerprint.py, cookie_warmer.py, behavioral_biometrics.py, adaptive.py, browser_pool.py, network.py, captcha_solver.py, shadow_dom.py, lazy_load.py, signup_automator.py, auth_connector.py This enables community contributions to the core engine (templates, integrations, MCP tools) while protecting the anti-detection techniques that constitute the actual competitive moat. BSL Additional Use Grant permits free non-production use; production deployment requires a commercial license from enterprise@rugmunch.io. Changes: - Replace proprietary LICENSE with MIT LICENSE + new LICENSE-BSL-STEALTH - Add SPDX-License-Identifier headers to 300+ source files - Add docs/adr/0002-dual-licensing.md (ADR documenting the decision) - Update README.md: new License section with BSL Additional Use Grant - Update LICENSING_PRICING_STRATEGY.md: Section 3 (PryScraper) for dual license - Update AGENTS.md: license line in header + new rule 8 (PRs touching BSL rejected) - Update pyproject.toml: license = "MIT AND BSL-1.1" + classifiers + license-files - Update DECISIONS.md index with ADR-0002 - Update STATUS.md (2026-07-03) and PLAN.md sprint goals Refs: ADR-0002
147 lines
5.6 KiB
Python
147 lines
5.6 KiB
Python
"""Pry — Camoufox (Firefox anti-detection) integration.
|
|
Camoufox patches Firefox at the source level to bypass fingerprinting
|
|
more effectively than Playwright/Puppeteer. It's a drop-in alternative
|
|
for Playwright that focuses on stealth."""
|
|
|
|
# SPDX-License-Identifier: BSL-1.1
|
|
# Copyright (c) 2026 Rug Munch Media LLC
|
|
#
|
|
# Part of Pry — Stealth / Anti-Detection Module
|
|
# Licensed under Business Source License 1.1 — see LICENSE-BSL-STEALTH.
|
|
# Change Date: 2029-01-01 (converts to MIT).
|
|
|
|
import logging
|
|
import random
|
|
import time
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Check for camoufox
|
|
try:
|
|
from camoufox import AsyncCamoufox
|
|
from camoufox.utils import DefaultAddons
|
|
_has_camoufox = True
|
|
except ImportError:
|
|
_has_camoufox = False
|
|
|
|
|
|
class CamoufoxBrowser:
|
|
"""Anti-detection Firefox browser using Camoufox."""
|
|
|
|
DEFAULT_CONFIGS = {
|
|
"chrome_windows": {
|
|
"headless": True, "os": "windows", "browser": "chrome",
|
|
"screen": (1920, 1080), "window": (1920, 1080),
|
|
},
|
|
"firefox_windows": {
|
|
"headless": True, "os": "windows", "browser": "firefox",
|
|
"screen": (1920, 1080), "window": (1920, 1080),
|
|
},
|
|
"chrome_mac": {
|
|
"headless": True, "os": "macos", "browser": "chrome",
|
|
"screen": (2560, 1600), "window": (1440, 900),
|
|
},
|
|
"firefox_linux": {
|
|
"headless": True, "os": "linux", "browser": "firefox",
|
|
"screen": (1920, 1080), "window": (1920, 1080),
|
|
},
|
|
}
|
|
|
|
def __init__(self, default_profile: str = "chrome_windows"):
|
|
self.default_profile = default_profile
|
|
if not _has_camoufox:
|
|
logger.warning("camoufox_not_available",
|
|
extra={"hint": "pip install camoufox && python -m camoufox fetch"})
|
|
|
|
async def fetch(
|
|
self,
|
|
url: str,
|
|
profile: str = "",
|
|
wait_selector: str = "",
|
|
wait_time: int = 3000,
|
|
proxy: str = "",
|
|
cookies: list[dict] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Fetch a URL with Camoufox anti-detection.
|
|
|
|
Args:
|
|
url: The URL to fetch
|
|
profile: Browser profile (chrome_windows, firefox_windows, etc.)
|
|
wait_selector: CSS selector to wait for before returning
|
|
wait_time: Time to wait in milliseconds
|
|
proxy: Proxy URL
|
|
cookies: Cookies to set before navigation
|
|
"""
|
|
if not _has_camoufox:
|
|
return {"success": False, "error": "camoufox not installed. Run: pip install camoufox && python -m camoufox fetch"}
|
|
profile_name = profile or self.default_profile
|
|
config = dict(self.DEFAULT_CONFIGS.get(profile_name, self.DEFAULT_CONFIGS["chrome_windows"]))
|
|
if proxy: config["proxy"] = proxy
|
|
try:
|
|
start = time.time()
|
|
async with AsyncCamoufox(**config) as browser:
|
|
page = await browser.new_page()
|
|
if cookies:
|
|
for cookie in cookies:
|
|
await page.context.add_cookies([cookie])
|
|
await page.goto(url, wait_until="domcontentloaded")
|
|
if wait_selector:
|
|
try:
|
|
await page.wait_for_selector(wait_selector, timeout=wait_time)
|
|
except Exception:
|
|
pass
|
|
else:
|
|
await page.wait_for_timeout(wait_time)
|
|
content = await page.content()
|
|
title = await page.title()
|
|
elapsed = time.time() - start
|
|
cookies_received = await page.context.cookies()
|
|
return {
|
|
"success": True, "url": url, "title": title,
|
|
"content": content, "raw_html": content,
|
|
"status_code": 200, "elapsed": round(elapsed, 2),
|
|
"profile": profile_name, "cookies": cookies_received,
|
|
}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)[:300], "url": url}
|
|
|
|
async def fetch_with_stealth(
|
|
self,
|
|
url: str,
|
|
human_behavior: dict | None = None,
|
|
proxy: str = "",
|
|
) -> dict[str, Any]:
|
|
"""Fetch with built-in human behavior simulation."""
|
|
result = await self.fetch(url, proxy=proxy)
|
|
if not result.get("success"):
|
|
return result
|
|
# Apply human behavior patterns
|
|
if human_behavior:
|
|
try:
|
|
from camoufox import AsyncCamoufox
|
|
config = dict(self.DEFAULT_CONFIGS.get(self.default_profile, {}))
|
|
if proxy: config["proxy"] = proxy
|
|
async with AsyncCamoufox(**config) as browser:
|
|
page = await browser.new_page()
|
|
await page.goto(url, wait_until="domcontentloaded")
|
|
# Apply human behavior
|
|
if human_behavior.get("scroll"):
|
|
for scroll_y in human_behavior["scroll"]:
|
|
await page.evaluate(f"window.scrollTo(0, {scroll_y})")
|
|
await page.wait_for_timeout(random.randint(200, 800))
|
|
if human_behavior.get("wait_time"):
|
|
await page.wait_for_timeout(human_behavior["wait_time"])
|
|
content = await page.content()
|
|
result["content"] = content
|
|
result["raw_html"] = content
|
|
except Exception:
|
|
pass
|
|
return result
|
|
|
|
def is_available(self) -> bool:
|
|
return _has_camoufox
|
|
|
|
@staticmethod
|
|
def list_profiles() -> list[str]:
|
|
return list(CamoufoxBrowser.DEFAULT_CONFIGS.keys())
|