first commit
This commit is contained in:
271
proxy_chain_manager/browser_profile.py
Normal file
271
proxy_chain_manager/browser_profile.py
Normal file
@@ -0,0 +1,271 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from .browser_identity import (
|
||||
CookiePolicy,
|
||||
Persona,
|
||||
cookie_behavior_value,
|
||||
cookie_session_only,
|
||||
get_cookie_policy,
|
||||
get_persona,
|
||||
is_blend_persona,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FirefoxHardening:
|
||||
# Proxy & network always-on
|
||||
force_proxy: bool = True
|
||||
disable_webrtc: bool = True # always strongly recommended; leaks real IP
|
||||
|
||||
# Hardened block mode — overrides persona spoofing
|
||||
resist_fingerprinting: bool = True
|
||||
disable_telemetry: bool = True
|
||||
first_party_isolation: bool = True
|
||||
strict_tracking_protection: bool = True
|
||||
clear_on_shutdown: bool = True
|
||||
timezone_utc: bool = True
|
||||
|
||||
# Persona / cookie selectors (added in "spoof when possible" mode)
|
||||
persona_key: str = "custom" # see browser_identity.PERSONAS
|
||||
cookie_mode: str = "block_third_party" # see browser_identity.COOKIE_MODES
|
||||
|
||||
# Optional Firefox-managed user-agent override even outside of a persona
|
||||
user_agent_override: str = ""
|
||||
|
||||
# Filled in by ``apply_persona`` when persona_key starts with "blend_".
|
||||
persona: Persona | None = field(default=None, repr=False, compare=False)
|
||||
|
||||
|
||||
def apply_persona(hard: FirefoxHardening) -> FirefoxHardening:
|
||||
"""Mutate hardening flags consistent with the persona choice.
|
||||
|
||||
Blend-in personas relax the "stand out" toggles (RFP, FPI, strict TP) but
|
||||
keep the IP-leakers off (WebRTC, geolocation, etc.). Cookie mode is
|
||||
honored. The result is a profile that looks like a normal browser.
|
||||
"""
|
||||
if hard.persona_key == "hardened":
|
||||
hard.resist_fingerprinting = True
|
||||
hard.first_party_isolation = True
|
||||
hard.strict_tracking_protection = True
|
||||
hard.timezone_utc = True
|
||||
hard.persona = None
|
||||
return hard
|
||||
|
||||
if is_blend_persona(hard.persona_key):
|
||||
hard.resist_fingerprinting = False # RFP itself is a fingerprint
|
||||
hard.first_party_isolation = False # breaks logins on many sites
|
||||
hard.strict_tracking_protection = False # Firefox default ETP is enough
|
||||
hard.timezone_utc = False
|
||||
hard.persona = get_persona(hard.persona_key)
|
||||
if hard.persona and not hard.user_agent_override:
|
||||
hard.user_agent_override = hard.persona.user_agent
|
||||
return hard
|
||||
|
||||
# custom — honor the individual flags as-is.
|
||||
hard.persona = None
|
||||
return hard
|
||||
|
||||
|
||||
def _bool(v: bool) -> str:
|
||||
return "true" if v else "false"
|
||||
|
||||
|
||||
def build_user_js(
|
||||
proxy_host: str,
|
||||
proxy_port: int,
|
||||
hard: FirefoxHardening,
|
||||
) -> str:
|
||||
hard = apply_persona(hard)
|
||||
lines: list[str] = [
|
||||
"// Managed by Proxy God. Changes are overwritten on next launch.",
|
||||
# Always-on baseline (no UX cost)
|
||||
'user_pref("app.normandy.enabled", false);',
|
||||
'user_pref("app.shield.optoutstudies.enabled", false);',
|
||||
'user_pref("browser.newtabpage.activity-stream.feeds.telemetry", false);',
|
||||
'user_pref("browser.newtabpage.activity-stream.telemetry", false);',
|
||||
'user_pref("browser.ping-centre.telemetry", false);',
|
||||
'user_pref("toolkit.telemetry.archive.enabled", false);',
|
||||
'user_pref("toolkit.telemetry.bhrPing.enabled", false);',
|
||||
'user_pref("toolkit.telemetry.enabled", false);',
|
||||
'user_pref("toolkit.telemetry.firstShutdownPing.enabled", false);',
|
||||
'user_pref("toolkit.telemetry.hybridContent.enabled", false);',
|
||||
'user_pref("toolkit.telemetry.newProfilePing.enabled", false);',
|
||||
'user_pref("toolkit.telemetry.shutdownPingSender.enabled", false);',
|
||||
'user_pref("toolkit.telemetry.unified", false);',
|
||||
'user_pref("datareporting.healthreport.uploadEnabled", false);',
|
||||
'user_pref("datareporting.policy.dataSubmissionEnabled", false);',
|
||||
'user_pref("network.trr.mode", 5);',
|
||||
'user_pref("network.captive-portal-service.enabled", false);',
|
||||
'user_pref("geo.enabled", false);',
|
||||
'user_pref("media.navigator.enabled", false);',
|
||||
'user_pref("dom.battery.enabled", false);',
|
||||
'user_pref("dom.gamepad.enabled", false);',
|
||||
'user_pref("dom.netinfo.enabled", false);',
|
||||
# WebGL allowed by default (blocking it makes you stand out massively)
|
||||
'user_pref("webgl.enable-debug-renderer-info", false);',
|
||||
]
|
||||
|
||||
if hard.force_proxy:
|
||||
lines.extend(
|
||||
[
|
||||
'user_pref("network.proxy.type", 1);',
|
||||
'user_pref("network.proxy.share_proxy_settings", true);',
|
||||
f'user_pref("network.proxy.http", "{proxy_host}");',
|
||||
f'user_pref("network.proxy.http_port", {int(proxy_port)});',
|
||||
f'user_pref("network.proxy.ssl", "{proxy_host}");',
|
||||
f'user_pref("network.proxy.ssl_port", {int(proxy_port)});',
|
||||
'user_pref("network.proxy.ftp", "");',
|
||||
'user_pref("network.proxy.ftp_port", 0);',
|
||||
'user_pref("network.proxy.socks", "");',
|
||||
'user_pref("network.proxy.socks_port", 0);',
|
||||
'user_pref("network.proxy.socks_version", 5);',
|
||||
'user_pref("network.proxy.socks_remote_dns", false);',
|
||||
'user_pref("network.proxy.no_proxies_on", "");',
|
||||
]
|
||||
)
|
||||
|
||||
if hard.disable_webrtc:
|
||||
# Off regardless of persona — WebRTC leaks real IP through STUN.
|
||||
lines.extend(
|
||||
[
|
||||
'user_pref("media.peerconnection.enabled", false);',
|
||||
'user_pref("media.peerconnection.ice.default_address_only", true);',
|
||||
'user_pref("media.peerconnection.ice.no_host", true);',
|
||||
]
|
||||
)
|
||||
|
||||
if hard.resist_fingerprinting:
|
||||
lines.extend(
|
||||
[
|
||||
'user_pref("privacy.resistFingerprinting", true);',
|
||||
'user_pref("privacy.resistFingerprinting.letterboxing", true);',
|
||||
'user_pref("privacy.window.maxInnerWidth", 1600);',
|
||||
'user_pref("privacy.window.maxInnerHeight", 900);',
|
||||
]
|
||||
)
|
||||
|
||||
if hard.first_party_isolation:
|
||||
lines.append('user_pref("privacy.firstparty.isolate", true);')
|
||||
|
||||
if hard.disable_telemetry:
|
||||
lines.append('user_pref("browser.send_pings", false);')
|
||||
|
||||
if hard.strict_tracking_protection:
|
||||
lines.extend(
|
||||
[
|
||||
'user_pref("privacy.trackingprotection.enabled", true);',
|
||||
'user_pref("privacy.trackingprotection.pbmode.enabled", true);',
|
||||
]
|
||||
)
|
||||
|
||||
# Persona spoofing (UA, accept-language, screen, platform)
|
||||
if hard.persona is not None:
|
||||
p = hard.persona
|
||||
lines.extend(
|
||||
[
|
||||
f'user_pref("general.useragent.override", "{p.user_agent}");',
|
||||
f'user_pref("intl.accept_languages", "{p.accept_language}");',
|
||||
f'user_pref("general.useragent.locale", "{p.locale}");',
|
||||
'user_pref("javascript.use_us_english_locale", true);',
|
||||
f'user_pref("privacy.window.maxInnerWidth", {int(p.screen_w)});',
|
||||
f'user_pref("privacy.window.maxInnerHeight", {int(p.screen_h)});',
|
||||
# Hint timezone — Firefox honors TZ env var at launch (set by
|
||||
# the launcher) but this pref nudges some site detection too.
|
||||
f'user_pref("intl.locale.requested", "{p.locale}");',
|
||||
]
|
||||
)
|
||||
elif hard.user_agent_override:
|
||||
lines.append(
|
||||
f'user_pref("general.useragent.override", "{hard.user_agent_override}");'
|
||||
)
|
||||
|
||||
# ── Cookie policy ────────────────────────────────────────────────────────
|
||||
cp: CookiePolicy = get_cookie_policy(hard.cookie_mode)
|
||||
|
||||
lines.extend([
|
||||
f'user_pref("network.cookie.cookieBehavior", {cp.behavior});',
|
||||
# pbmode = private-browsing mode uses same behavior
|
||||
f'user_pref("network.cookie.cookieBehavior.pbmode", {cp.behavior});',
|
||||
# lifetime: 0 = normal, 2 = session-only
|
||||
f'user_pref("network.cookie.lifetimePolicy", {cp.lifetime});',
|
||||
])
|
||||
|
||||
# Total Cookie Protection (TCP / Firefox partitioning)
|
||||
if cp.partition:
|
||||
lines.extend([
|
||||
'user_pref("network.cookie.cookieBehavior.optInPartitioning", true);',
|
||||
# Also isolate cache, localStorage, IndexedDB per top-level site
|
||||
'user_pref("privacy.partition.serviceWorkers", true);',
|
||||
'user_pref("privacy.partition.network_state", true);',
|
||||
'user_pref("privacy.partition.bloburl_by_default", true);',
|
||||
])
|
||||
else:
|
||||
lines.extend([
|
||||
'user_pref("network.cookie.cookieBehavior.optInPartitioning", false);',
|
||||
])
|
||||
|
||||
# SameSite=None cookies must be Secure (good hygiene regardless of mode)
|
||||
lines.append('user_pref("network.cookie.sameSite.noneRequiresSecure", true);')
|
||||
|
||||
# ── Sanitize-on-shutdown ─────────────────────────────────────────────────
|
||||
# The CookiePolicy sanitize flag takes priority; the hard.clear_on_shutdown
|
||||
# toggle acts as an additional override for cookies+cache even when the
|
||||
# cookie mode doesn't request it.
|
||||
do_sanitize = cp.sanitize or hard.clear_on_shutdown
|
||||
if do_sanitize:
|
||||
lines.append('user_pref("privacy.sanitize.sanitizeOnShutdown", true);')
|
||||
# Cookies
|
||||
lines.append(f'user_pref("privacy.clearOnShutdown.cookies", {_bool(cp.clear_cookies or hard.clear_on_shutdown)});')
|
||||
# Cache
|
||||
lines.append(f'user_pref("privacy.clearOnShutdown.cache", {_bool(cp.clear_cache or hard.clear_on_shutdown)});')
|
||||
# History
|
||||
lines.append(f'user_pref("privacy.clearOnShutdown.history", {_bool(cp.clear_history or hard.clear_on_shutdown)});')
|
||||
# Downloads list
|
||||
lines.append('user_pref("privacy.clearOnShutdown.downloads", true);')
|
||||
# Form / search bar autofill data
|
||||
lines.append(f'user_pref("privacy.clearOnShutdown.formdata", {_bool(cp.clear_formdata or hard.clear_on_shutdown)});')
|
||||
# Active sessions (HTTP auth, logged-in sites)
|
||||
lines.append('user_pref("privacy.clearOnShutdown.sessions", true);')
|
||||
# localStorage + IndexedDB
|
||||
lines.append(f'user_pref("privacy.clearOnShutdown.localStorage", {_bool(cp.clear_ls or hard.clear_on_shutdown)});')
|
||||
lines.append(f'user_pref("privacy.clearOnShutdown.indexedDB", {_bool(cp.clear_ls or hard.clear_on_shutdown)});')
|
||||
# Service worker registrations
|
||||
lines.append('user_pref("privacy.clearOnShutdown.serviceWorkers", true);')
|
||||
# Offline web app cache
|
||||
lines.append('user_pref("privacy.clearOnShutdown.offlineApps", true);')
|
||||
else:
|
||||
lines.append('user_pref("privacy.sanitize.sanitizeOnShutdown", false);')
|
||||
|
||||
if hard.timezone_utc:
|
||||
lines.append('user_pref("privacy.resistFingerprinting.reduceTimerPrecision", true);')
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def persona_env(hard: FirefoxHardening) -> dict[str, str]:
|
||||
"""Environment overrides applied when launching Firefox under a persona.
|
||||
|
||||
Only the timezone needs the OS-level env var (TZ) — Firefox reads it
|
||||
even when running under Windows.
|
||||
"""
|
||||
hard = apply_persona(hard)
|
||||
if hard.persona is None:
|
||||
return {}
|
||||
return {"TZ": hard.persona.timezone}
|
||||
|
||||
|
||||
def ensure_firefox_profile(
|
||||
profile_dir: Path,
|
||||
proxy_host: str,
|
||||
proxy_port: int,
|
||||
hard: FirefoxHardening,
|
||||
) -> None:
|
||||
profile_dir.mkdir(parents=True, exist_ok=True)
|
||||
(profile_dir / "user.js").write_text(
|
||||
build_user_js(proxy_host, proxy_port, hard),
|
||||
encoding="utf-8",
|
||||
)
|
||||
Reference in New Issue
Block a user