feat: exhaustive feature expansion — cookie modes, DNS/WebRTC testers, map, Firefox fix
Cookie system: - Expand from 5 to 12 fully-specified CookiePolicy modes in browser_identity.py - Add CookiePolicy dataclass: behavior, lifetime, TCP partitioning, clearOnShutdown.* - browser_profile.py emits full cookie pref set from policy object - UI dropdown widened to 680px with live description label per mode Firefox launch fix: - Detect Firefox via Windows registry, AppData, and shutil.which - Use DETACHED_PROCESS|CREATE_NO_WINDOW|CREATE_NEW_PROCESS_GROUP flags - Wait up to 3s for firefox.exe in tasklist instead of polling parent pid - taskkill on stop() to terminate all firefox.exe processes DNS leak tester: - dns_leak.py: FullDnsLeakReport dataclass, run_dns_leak_test comparing proxy DoH resolution vs direct system DNS WebRTC tester: - webrtc_check.py: STUN UDP probe, registry policy check, user.js pref check Ban tester: - Added SITES_SHOPPING, SITES_CRYPTO, SITES_DNS categories - Parallel execution via ThreadPoolExecutor - Expanded banned-text hint keywords Fingerprint audit: - OS identity checks: hostname, MAC, GUID, OS version, timezone, screen res - Browser consistency analysis of user.js Neon world map: - world_map.png bundled; chain_map.py renders hop arcs over it with glow effect Signup prep: - Auto-save account on Open & Autofill; Copy Email / Copy Pass buttons - Auto-fill custom URL when preset site selected - PyInstaller-safe path resolution for signup_extension Spec: - Bundle signup_extension dir and world_map.png as PyInstaller data files Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
"""World-map visualization for proxy chains (Pillow + equirectangular projection)."""
|
||||
"""World-map visualization for proxy chains.
|
||||
|
||||
Renders hop arcs over the bundled neon world map image. Falls back to a
|
||||
drawn landmass map if the image file is absent.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
@@ -6,10 +10,11 @@ import logging
|
||||
import math
|
||||
import socket
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||
|
||||
try:
|
||||
_RESAMPLE = Image.Resampling.LANCZOS
|
||||
@@ -23,21 +28,42 @@ log = logging.getLogger(__name__)
|
||||
|
||||
ChainStatus = Literal["healthy", "connecting", "dead", "idle"]
|
||||
|
||||
# Theme colors (match app.py palette)
|
||||
_BG = "#070713"
|
||||
_GRID = "#141432"
|
||||
_LAND = "#12182e"
|
||||
_LAND_EDGE = "#1e2a4a"
|
||||
_LINE_HEALTHY = "#00e5ff"
|
||||
# Theme palette
|
||||
_BG = "#070713"
|
||||
_GRID = "#1a1f3a"
|
||||
_LAND = "#12182e"
|
||||
_LAND_EDGE = "#1e2a4a"
|
||||
_LINE_HEALTHY = "#00e5ff"
|
||||
_LINE_CONNECTING = "#ffd000"
|
||||
_LINE_DEAD = "#ff2a6d"
|
||||
_LINE_IDLE = "#3052ff"
|
||||
_YOU = "#00ff9c"
|
||||
_HOP = "#00e5ff"
|
||||
_EXIT = "#ffd000"
|
||||
_LABEL = "#eaf2ff"
|
||||
_LABEL_DIM = "#7a8aab"
|
||||
_UNKNOWN = "#2a2f4a"
|
||||
_LINE_DEAD = "#ff2a6d"
|
||||
_LINE_IDLE = "#3052ff"
|
||||
_YOU = "#00ff9c"
|
||||
_HOP = "#00e5ff"
|
||||
_EXIT = "#ffd000"
|
||||
_LABEL = "#eaf2ff"
|
||||
_LABEL_DIM = "#7a8aab"
|
||||
_UNKNOWN = "#2a2f4a"
|
||||
|
||||
def _map_img_path() -> Path:
|
||||
"""Locate world_map.png whether running from source or a frozen PyInstaller bundle."""
|
||||
import sys
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||
return Path(sys._MEIPASS) / "proxy_chain_manager" / "world_map.png"
|
||||
return Path(__file__).resolve().parent / "world_map.png"
|
||||
|
||||
_cached_map: Image.Image | None = None
|
||||
|
||||
|
||||
def _load_world_map(width: int, height: int) -> Image.Image | None:
|
||||
"""Return the neon world map resized to (width × height), or None if unavailable."""
|
||||
global _cached_map
|
||||
try:
|
||||
if _cached_map is None:
|
||||
_cached_map = Image.open(_map_img_path()).convert("RGB")
|
||||
return _cached_map.resize((width, height), _RESAMPLE)
|
||||
except Exception as exc:
|
||||
log.debug("world_map load failed: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -134,26 +160,23 @@ def build_chain_map_points(
|
||||
ip = resolve_host_ip(host, timeout_seconds=min(4.0, timeout_seconds))
|
||||
if not ip:
|
||||
continue
|
||||
short = redact_proxy_url(hop)
|
||||
if len(short) > 22:
|
||||
short = short[:20] + "…"
|
||||
intel = fetch_ip_geo(ip, timeout_seconds)
|
||||
pt = _intel_to_point("hop", f"H{i + 1}", intel, hop_index=i)
|
||||
if pt:
|
||||
points.append(pt)
|
||||
|
||||
exit = (exit_ip or "").strip()
|
||||
if exit and exit != "—":
|
||||
exit_str = (exit_ip or "").strip()
|
||||
if exit_str and exit_str != "—":
|
||||
if (
|
||||
exit_intel
|
||||
and exit_intel.ok
|
||||
and exit_intel.ip == exit
|
||||
and exit_intel.ip == exit_str
|
||||
and exit_intel.lat is not None
|
||||
and exit_intel.lon is not None
|
||||
):
|
||||
pt = _intel_to_point("exit", "EXIT", exit_intel)
|
||||
else:
|
||||
pt = _intel_to_point("exit", "EXIT", fetch_ip_geo(exit, timeout_seconds))
|
||||
pt = _intel_to_point("exit", "EXIT", fetch_ip_geo(exit_str, timeout_seconds))
|
||||
if pt:
|
||||
points.append(pt)
|
||||
|
||||
@@ -173,7 +196,7 @@ def _great_circle_points(
|
||||
lon1: float,
|
||||
lat2: float,
|
||||
lon2: float,
|
||||
steps: int = 24,
|
||||
steps: int = 32,
|
||||
) -> list[tuple[float, float]]:
|
||||
"""Interpolate along a great circle for curved hop arcs."""
|
||||
phi1, lam1 = math.radians(lat1), math.radians(lon1)
|
||||
@@ -194,21 +217,50 @@ def _great_circle_points(
|
||||
x = a * math.cos(phi1) * math.cos(lam1) + b * math.cos(phi2) * math.cos(lam2)
|
||||
y = a * math.cos(phi1) * math.sin(lam1) + b * math.cos(phi2) * math.sin(lam2)
|
||||
z = a * math.sin(phi1) + b * math.sin(phi2)
|
||||
out.append((math.degrees(math.atan2(z, math.sqrt(x * x + y * y))), math.degrees(math.atan2(y, x))))
|
||||
out.append((
|
||||
math.degrees(math.atan2(z, math.sqrt(x * x + y * y))),
|
||||
math.degrees(math.atan2(y, x)),
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def _line_color(status: ChainStatus) -> str:
|
||||
return {
|
||||
"healthy": _LINE_HEALTHY,
|
||||
"connecting": _LINE_CONNECTING,
|
||||
"dead": _LINE_DEAD,
|
||||
"idle": _LINE_IDLE,
|
||||
}.get(status, _LINE_IDLE)
|
||||
def _line_color(status: ChainStatus) -> tuple[int, int, int]:
|
||||
table = {
|
||||
"healthy": (0, 229, 255),
|
||||
"connecting": (255, 208, 0),
|
||||
"dead": (255, 42, 109),
|
||||
"idle": (48, 82, 255),
|
||||
}
|
||||
return table.get(status, (48, 82, 255))
|
||||
|
||||
|
||||
def _node_color(role: str) -> str:
|
||||
return {"you": _YOU, "hop": _HOP, "exit": _EXIT}.get(role, _HOP)
|
||||
def _node_color(role: str) -> tuple[int, int, int]:
|
||||
table = {"you": (0, 255, 156), "hop": (0, 229, 255), "exit": (255, 208, 0)}
|
||||
return table.get(role, (0, 229, 255))
|
||||
|
||||
|
||||
def _hex_to_rgb(h: str) -> tuple[int, int, int]:
|
||||
h = h.lstrip("#")
|
||||
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
||||
|
||||
|
||||
def _draw_glow_line(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
pts: list[tuple[float, float]],
|
||||
color: tuple[int, int, int],
|
||||
) -> None:
|
||||
"""Draw neon glow arc: fat dim outer halo → bright core line."""
|
||||
if len(pts) < 2:
|
||||
return
|
||||
r, g, b = color
|
||||
# outer halo — wider, dim
|
||||
halo = (r // 4, g // 4, b // 4)
|
||||
draw.line(pts, fill=halo, width=7)
|
||||
# mid glow
|
||||
mid = (r // 2, g // 2, b // 2)
|
||||
draw.line(pts, fill=mid, width=4)
|
||||
# bright core
|
||||
draw.line(pts, fill=color, width=2)
|
||||
|
||||
|
||||
def _load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
@@ -221,18 +273,13 @@ def _load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
|
||||
|
||||
def _draw_landmasses(draw: ImageDraw.ImageDraw, width: int, height: int, margin: int) -> None:
|
||||
"""Minimal continent silhouettes — stylized, not survey-accurate."""
|
||||
"""Fallback minimal continent silhouettes."""
|
||||
blobs: list[list[tuple[float, float]]] = [
|
||||
# North America
|
||||
[(-168, 72), (-140, 75), (-100, 72), (-80, 50), (-75, 25), (-95, 15), (-110, 20), (-125, 48), (-168, 72)],
|
||||
# South America
|
||||
[(-82, 12), (-72, -5), (-55, -35), (-65, -55), (-75, -18), (-82, 12)],
|
||||
# Europe / Africa
|
||||
[(-10, 72), (30, 70), (40, 55), (35, 30), (20, 5), (-5, 5), (-18, 28), (-10, 72)],
|
||||
[(15, 35), (35, 32), (50, 12), (42, -5), (18, -35), (15, 35)],
|
||||
# Asia
|
||||
[(40, 72), (100, 75), (140, 55), (130, 35), (110, 10), (80, 8), (60, 25), (40, 45), (40, 72)],
|
||||
# Australia
|
||||
[(115, -12), (135, -12), (150, -25), (145, -38), (115, -38), (115, -12)],
|
||||
]
|
||||
for poly in blobs:
|
||||
@@ -246,58 +293,75 @@ def render_chain_map(
|
||||
width: int = 960,
|
||||
height: int = 220,
|
||||
status: ChainStatus = "idle",
|
||||
margin: int = 18,
|
||||
margin: int = 12,
|
||||
) -> Image.Image:
|
||||
"""Render chain hops on a dark equirectangular world map."""
|
||||
img = Image.new("RGB", (max(320, width), max(120, height)), _BG)
|
||||
draw = ImageDraw.Draw(img)
|
||||
_draw_landmasses(draw, width, height, margin)
|
||||
"""Render chain hops over the neon world map (or a drawn fallback)."""
|
||||
w = max(320, width)
|
||||
h = max(120, height)
|
||||
|
||||
# Lat/lon grid
|
||||
for lat in range(-60, 91, 30):
|
||||
y = _project(lat, 0, width, height, margin)[1]
|
||||
draw.line([(margin, y), (width - margin, y)], fill=_GRID, width=1)
|
||||
for lon in range(-150, 181, 30):
|
||||
x = _project(0, lon, width, height, margin)[0]
|
||||
draw.line([(x, margin), (x, height - margin)], fill=_GRID, width=1)
|
||||
# ── background ──────────────────────────────────────────────────────────
|
||||
world = _load_world_map(w, h)
|
||||
if world is not None:
|
||||
# Slightly darken so overlaid lines pop
|
||||
bg = Image.new("RGB", (w, h), (0, 0, 0))
|
||||
img = Image.blend(world, bg, alpha=0.22)
|
||||
else:
|
||||
img = Image.new("RGB", (w, h), _BG)
|
||||
draw_bg = ImageDraw.Draw(img)
|
||||
_draw_landmasses(draw_bg, w, h, margin)
|
||||
# grid
|
||||
for lat in range(-60, 91, 30):
|
||||
y = _project(lat, 0, w, h, margin)[1]
|
||||
draw_bg.line([(margin, y), (w - margin, y)], fill=_GRID, width=1)
|
||||
for lon in range(-150, 181, 30):
|
||||
x = _project(0, lon, w, h, margin)[0]
|
||||
draw_bg.line([(x, margin), (x, h - margin)], fill=_GRID, width=1)
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
if len(points) < 2:
|
||||
font = _load_font(13)
|
||||
font = _load_font(12)
|
||||
msg = "Start the chain to plot hops on the map." if not points else "Need 2+ geo points to draw path."
|
||||
draw.text((margin, height // 2 - 8), msg, fill=_LABEL_DIM, font=font)
|
||||
# drop shadow
|
||||
draw.text((margin + 1, h // 2 - 7), msg, fill=(0, 0, 0), font=font)
|
||||
draw.text((margin, h // 2 - 8), msg, fill=_hex_to_rgb(_LABEL_DIM), font=font)
|
||||
return img
|
||||
|
||||
line_c = _line_color(status)
|
||||
projected = [_project(p.lat, p.lon, width, height, margin) for p in points]
|
||||
line_rgb = _line_color(status)
|
||||
projected = [_project(p.lat, p.lon, w, h, margin) for p in points]
|
||||
|
||||
# Glow underlay
|
||||
# ── arc lines between consecutive hops ─────────────────────────────────
|
||||
for i in range(len(projected) - 1):
|
||||
arc = _great_circle_points(points[i].lat, points[i].lon, points[i + 1].lat, points[i + 1].lon)
|
||||
arc_xy = [_project(lat, lon, width, height, margin) for lat, lon in arc]
|
||||
draw.line(arc_xy, fill=line_c, width=5)
|
||||
for i in range(len(projected) - 1):
|
||||
arc = _great_circle_points(points[i].lat, points[i].lon, points[i + 1].lat, points[i + 1].lon)
|
||||
arc_xy = [_project(lat, lon, width, height, margin) for lat, lon in arc]
|
||||
draw.line(arc_xy, fill=line_c, width=2)
|
||||
arc = _great_circle_points(
|
||||
points[i].lat, points[i].lon,
|
||||
points[i + 1].lat, points[i + 1].lon,
|
||||
)
|
||||
arc_xy = [_project(lat, lon, w, h, margin) for lat, lon in arc]
|
||||
_draw_glow_line(draw, arc_xy, line_rgb)
|
||||
|
||||
label_font = _load_font(11)
|
||||
# ── nodes ───────────────────────────────────────────────────────────────
|
||||
label_font = _load_font(11)
|
||||
detail_font = _load_font(9)
|
||||
for pt, (x, y) in zip(points, projected):
|
||||
color = _node_color(pt.role)
|
||||
nc = _node_color(pt.role)
|
||||
r = 7 if pt.role == "exit" else (6 if pt.role == "you" else 5)
|
||||
draw.ellipse((x - r - 2, y - r - 2, x + r + 2, y + r + 2), fill=color)
|
||||
draw.ellipse((x - r, y - r, x + r, y + r), fill=_BG, outline=color, width=2)
|
||||
tx, ty = x + 10, y - 14
|
||||
draw.text((tx + 1, ty + 1), pt.label, fill="#000000", font=label_font)
|
||||
draw.text((tx, ty), pt.label, fill=_LABEL, font=label_font)
|
||||
# outer glow ring
|
||||
glow = (nc[0] // 3, nc[1] // 3, nc[2] // 3)
|
||||
draw.ellipse((x - r - 4, y - r - 4, x + r + 4, y + r + 4), fill=glow)
|
||||
# filled node with dark center
|
||||
draw.ellipse((x - r, y - r, x + r, y + r), fill=nc)
|
||||
draw.ellipse((x - r + 2, y - r + 2, x + r - 2, y + r - 2), fill=(5, 5, 20))
|
||||
|
||||
tx, ty = int(x + r + 5), int(y - 8)
|
||||
# text drop-shadow
|
||||
draw.text((tx + 1, ty + 1), pt.label, fill=(0, 0, 0), font=label_font)
|
||||
draw.text((tx, ty), pt.label, fill=_hex_to_rgb(_LABEL), font=label_font)
|
||||
if pt.detail:
|
||||
draw.text((tx, ty + 13), pt.detail[:28], fill=_LABEL_DIM, font=detail_font)
|
||||
draw.text((tx, ty + 12), pt.detail[:30], fill=_hex_to_rgb(_LABEL_DIM), font=detail_font)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def resize_map_display(pil_img: Image.Image, width: int, height: int) -> Image.Image:
|
||||
"""Resize rendered map for the GUI widget."""
|
||||
w = max(320, int(width))
|
||||
h = max(120, int(height))
|
||||
return pil_img.resize((w, h), _RESAMPLE)
|
||||
return pil_img.resize((max(320, int(width)), max(120, int(height))), _RESAMPLE)
|
||||
|
||||
Reference in New Issue
Block a user