Files
proxy-god/proxy_chain_manager/browser_launcher.py
Indiana Holmes 26863fc2a5 Add ban tester, signup prep, exit IP intel, and sticky-exit hold.
New GUI tabs probe popular sites for exit-IP bans and prep signup autofill through the chain; Live tab shows geo/ASN/datacenter flags, and sticky-exit keeps the same egress IP during signup flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 15:37:28 -07:00

149 lines
5.1 KiB
Python

from __future__ import annotations
import logging
import os
import shutil
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
from .browser_profile import FirefoxHardening, ensure_firefox_profile, persona_env
from .paths import app_data_dir
from .signup_prep import SignupDraft, install_signup_extension
log = logging.getLogger(__name__)
@dataclass
class BrowserConfig:
firefox_path: str = ""
profile_dir: str = ""
clear_on_close: bool = True
disposable_profile: bool = False
kill_on_chain_drop: bool = True
auto_relaunch: bool = False
lock_managed_profile: bool = True
force_proxy: bool = True
disable_webrtc: bool = True
resist_fingerprinting: bool = True
disable_telemetry: bool = True
first_party_isolation: bool = True
strict_tracking_protection: bool = True
timezone_utc: bool = True
persona: str = "blend_windows_chrome"
cookie_mode: str = "block_third_party"
def default_firefox_path() -> str:
candidates = [
Path(r"C:\Program Files\Mozilla Firefox\firefox.exe"),
Path(r"C:\Program Files (x86)\Mozilla Firefox\firefox.exe"),
]
for c in candidates:
if c.is_file():
return str(c)
return ""
def default_profile_dir() -> str:
return str(app_data_dir() / "browser_profiles" / "firefox_hardened")
class BrowserSession:
def __init__(self) -> None:
self._proc: subprocess.Popen[str] | None = None
self._profile_path: Path | None = None
def is_running(self) -> bool:
return self._proc is not None and self._proc.poll() is None
def pid(self) -> int | None:
if self._proc is None:
return None
return self._proc.pid
def launch(
self,
cfg: BrowserConfig,
proxy_host: str,
proxy_port: int,
start_url: str = "",
signup_draft: SignupDraft | None = None,
) -> tuple[bool, str]:
raw_exe = (cfg.firefox_path or default_firefox_path()).strip().strip('"').strip("'")
exe = Path(raw_exe)
if not exe.is_file():
return False, f"Firefox executable not found: {raw_exe or '(empty path)'}"
raw_profile = (cfg.profile_dir or default_profile_dir()).strip().strip('"').strip("'")
profile_dir = Path(raw_profile)
hard = FirefoxHardening(
force_proxy=cfg.force_proxy,
disable_webrtc=cfg.disable_webrtc,
resist_fingerprinting=cfg.resist_fingerprinting,
disable_telemetry=cfg.disable_telemetry,
first_party_isolation=cfg.first_party_isolation,
strict_tracking_protection=cfg.strict_tracking_protection,
clear_on_shutdown=cfg.clear_on_close,
timezone_utc=cfg.timezone_utc,
persona_key=cfg.persona,
cookie_mode=cfg.cookie_mode,
)
if cfg.lock_managed_profile:
ensure_firefox_profile(profile_dir, proxy_host, int(proxy_port), hard)
if signup_draft is not None:
install_signup_extension(profile_dir, signup_draft)
if self.is_running():
self.stop()
if cfg.lock_managed_profile:
args = [str(exe), "-no-remote", "-profile", str(profile_dir)]
else:
args = [str(exe)]
url = (start_url or "").strip()
if url:
args.append(url)
env = os.environ.copy()
env.update(persona_env(hard))
try:
self._proc = subprocess.Popen(
args,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
text=True,
env=env,
)
# Smoke-check: catch immediate startup crashes and report clearly.
time.sleep(0.4)
rc = self._proc.poll()
if rc is not None:
self._proc = None
return False, f"Firefox exited immediately (code {rc}). Check path/profile."
self._profile_path = profile_dir if cfg.lock_managed_profile else None
log.info("Launched hardened Firefox pid=%s profile=%s", self._proc.pid, profile_dir)
if cfg.lock_managed_profile:
return True, f"Hardened Firefox started (pid {self._proc.pid})."
return True, f"Firefox started (unlocked mode, pid {self._proc.pid})."
except Exception as e:
self._proc = None
return False, f"Launch failed: {e!s}"
def stop(self, dispose: bool = False) -> tuple[bool, str]:
if self._proc and self._proc.poll() is None:
try:
self._proc.terminate()
self._proc.wait(timeout=8)
except Exception:
try:
self._proc.kill()
except Exception:
pass
finally:
self._proc = None
if dispose and self._profile_path and self._profile_path.exists():
try:
shutil.rmtree(self._profile_path, ignore_errors=True)
except Exception:
pass
return True, "Browser stopped."