Files
PROXY_GOD_MAC/proxy_chain_manager/signup_prep.py
2026-05-23 21:58:06 -07:00

445 lines
14 KiB
Python

from __future__ import annotations
import json
import secrets
import shutil
import string
import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .paths import app_data_dir
_EXT_ID = "signup-autofill@proxygod"
def _ext_src_path() -> Path:
"""Locate the signup_extension dir whether running from source or frozen."""
import sys
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
return Path(sys._MEIPASS) / "proxy_chain_manager" / "signup_extension"
return Path(__file__).resolve().parent / "signup_extension"
@dataclass(frozen=True)
class SignupSitePreset:
key: str
label: str
url: str
hosts: tuple[str, ...]
extra_selectors: dict[str, list[str]] = field(default_factory=dict)
SIGNUP_PRESETS: dict[str, SignupSitePreset] = {
"google": SignupSitePreset(
key="google",
label="Google",
url="https://accounts.google.com/signup/v2/createaccount?flowName=GlifWebSignIn&flowEntry=SignUp",
hosts=("accounts.google.com", "google.com"),
extra_selectors={
"first_name": ['input[name="firstName"]'],
"last_name": ['input[name="lastName"]'],
"email": ['input[name="Username"]', 'input[type="email"]'],
"password": ['input[name="Passwd"]'],
"password_confirm": ['input[name="PasswdAgain"]'],
},
),
"protonmail": SignupSitePreset(
key="protonmail",
label="Proton Mail",
url="https://account.proton.me/signup",
hosts=("account.proton.me", "proton.me"),
extra_selectors={
"email": ['input[id="email"]', 'input[name="email"]', 'input[type="email"]'],
"password": ['input[id="password"]', 'input[name="password"]'],
"password_confirm": ['input[id="password-confirm"]', 'input[name="passwordConfirmation"]'],
},
),
"hydraproxy": SignupSitePreset(
key="hydraproxy",
label="HydraProxy",
url="https://dashboard.hydraproxy.com/register",
hosts=("dashboard.hydraproxy.com", "hydraproxy.com"),
extra_selectors={
"email": ['input[name="email"]', 'input[type="email"]', 'input[id="email"]'],
"password": ['input[name="password"]', 'input[type="password"]'],
"password_confirm": ['input[name="password_confirmation"]', 'input[name="confirmPassword"]'],
"username": ['input[name="username"]', 'input[name="name"]'],
},
),
"outlook": SignupSitePreset(
key="outlook",
label="Outlook / Microsoft",
url="https://signup.live.com/signup",
hosts=("signup.live.com", "login.live.com", "account.microsoft.com"),
extra_selectors={
"email": ['input[name="MemberName"]', 'input[type="email"]'],
"password": ['input[name="Password"]', 'input[name="PasswordInput"]'],
"first_name": ['input[name="FirstName"]'],
"last_name": ['input[name="LastName"]'],
},
),
"github": SignupSitePreset(
key="github",
label="GitHub",
url="https://github.com/signup",
hosts=("github.com",),
extra_selectors={
"email": ['input[name="user[email]"]', 'input[autocomplete="email"]'],
"password": ['input[name="user[password]"]', 'input[autocomplete="new-password"]'],
"username": ['input[name="user[login]"]'],
},
),
"reddit": SignupSitePreset(
key="reddit",
label="Reddit",
url="https://www.reddit.com/register",
hosts=("www.reddit.com", "reddit.com"),
extra_selectors={
"email": ['input[name="email"]', 'input[type="email"]'],
"username": ['input[name="username"]'],
"password": ['input[name="password"]'],
},
),
"discord": SignupSitePreset(
key="discord",
label="Discord",
url="https://discord.com/register",
hosts=("discord.com",),
extra_selectors={
"email": ['input[name="email"]', 'input[type="email"]'],
"username": ['input[name="username"]'],
"password": ['input[name="password"]'],
},
),
"x_twitter": SignupSitePreset(
key="x_twitter",
label="X / Twitter",
url="https://x.com/i/flow/signup",
hosts=("x.com", "twitter.com"),
extra_selectors={
"email": ['input[name="email"]', 'input[type="email"]'],
"username": ['input[name="username"]'],
"password": ['input[name="password"]'],
"first_name": ['input[name="name"]'],
},
),
"telegram": SignupSitePreset(
key="telegram",
label="Telegram Web",
url="https://web.telegram.org/",
hosts=("web.telegram.org", "telegram.org"),
),
"tutanota": SignupSitePreset(
key="tutanota",
label="Tuta Mail",
url="https://app.tuta.com/signup",
hosts=("app.tuta.com", "tutanota.com", "tuta.com"),
extra_selectors={
"email": ['input[id="mailAddress"]', 'input[name="mailAddress"]'],
"password": ['input[id="password1"]', 'input[type="password"]'],
"password_confirm": ['input[id="password2"]'],
},
),
"mailcom": SignupSitePreset(
key="mailcom",
label="mail.com",
url="https://signup.mail.com/",
hosts=("signup.mail.com", "mail.com"),
extra_selectors={
"username": ['input[name="email_user"]', 'input[name="username"]'],
"password": ['input[name="password"]'],
"first_name": ['input[name="firstName"]'],
"last_name": ['input[name="lastName"]'],
},
),
}
# Public site-key list in display order. Useful for building dropdowns.
SIGNUP_PRESET_KEYS: tuple[str, ...] = (
"google", "protonmail", "outlook", "tutanota", "mailcom",
"github", "reddit", "discord", "x_twitter", "telegram",
"hydraproxy",
)
_FIRST_NAMES: tuple[str, ...] = (
"Alex", "Jordan", "Casey", "Morgan", "Taylor", "Riley", "Cameron",
"Sam", "Jamie", "Quinn", "Avery", "Drew", "Reese", "Skyler", "Logan",
"Hayden", "Parker", "Rowan", "Sage", "Emerson", "Finley", "Harper",
"Kai", "Micah", "Nico", "Phoenix", "River", "Shawn", "Sutton", "Wren",
)
_LAST_NAMES: tuple[str, ...] = (
"Reed", "Hayes", "Brooks", "Bennett", "Carter", "Foster", "Gray",
"Hughes", "Jensen", "Knight", "Lambert", "Mason", "Nash", "Owens",
"Porter", "Quincy", "Reyes", "Sutton", "Tate", "Underwood", "Vance",
"Walsh", "Yates", "Zimmerman", "Abbott", "Barker", "Cohen", "Dixon",
"Ellis", "Fitch",
)
def random_first_name() -> str:
return secrets.choice(_FIRST_NAMES)
def random_last_name() -> str:
return secrets.choice(_LAST_NAMES)
def random_full_name() -> tuple[str, str]:
return random_first_name(), random_last_name()
def random_username(first: str = "", last: str = "") -> str:
"""Generate a plausible-looking username.
If a name is provided we mix it with digits; otherwise we use two short
name fragments. Always lowercase, always 8-18 chars.
"""
f = (first or random_first_name()).lower()
l = (last or random_last_name()).lower()
seps = ("", ".", "_", "")
sep = secrets.choice(seps)
digits = "".join(secrets.choice(string.digits) for _ in range(secrets.choice([2, 3, 4])))
style = secrets.randbelow(4)
if style == 0:
base = f + sep + l + digits
elif style == 1:
base = f + digits
elif style == 2:
base = l + sep + f[0] + digits
else:
base = f[0] + sep + l + digits
base = base.strip(".-_")
return base[:18]
@dataclass
class SignupDraft:
site_key: str = "google"
custom_url: str = ""
email: str = ""
password: str = ""
username: str = ""
first_name: str = ""
last_name: str = ""
notes: str = ""
@dataclass
class AccountRecord:
id: str
site: str
url: str
email: str
password: str
username: str = ""
first_name: str = ""
last_name: str = ""
exit_ip: str = ""
exit_hop: str = ""
notes: str = ""
status: str = "created" # pending | created
created_at: str = ""
def accounts_path() -> Path:
return app_data_dir() / "signup_accounts.json"
def draft_path() -> Path:
return app_data_dir() / "signup_draft.json"
def generate_password(length: int = 18) -> str:
n = max(12, min(64, int(length)))
alphabet = string.ascii_letters + string.digits + "!@#$%^&*-_=+"
while True:
pw = "".join(secrets.choice(alphabet) for _ in range(n))
if (
any(c.islower() for c in pw)
and any(c.isupper() for c in pw)
and any(c.isdigit() for c in pw)
):
return pw
def preset_for(key: str) -> SignupSitePreset | None:
return SIGNUP_PRESETS.get(key)
def resolve_signup_url(draft: SignupDraft) -> str:
if draft.site_key == "custom":
return (draft.custom_url or "").strip()
p = preset_for(draft.site_key)
return p.url if p else ""
def load_draft() -> SignupDraft:
from . import secrets_store as _ss
p = draft_path()
if not p.is_file():
return SignupDraft()
try:
raw = json.loads(p.read_text(encoding="utf-8"))
kwargs = {k: raw.get(k, "") for k in SignupDraft.__dataclass_fields__}
if kwargs.get("password"):
kwargs["password"] = _ss.decrypt(kwargs["password"])
return SignupDraft(**kwargs)
except (OSError, json.JSONDecodeError, TypeError):
return SignupDraft()
def save_draft(draft: SignupDraft) -> None:
from . import secrets_store as _ss
d = asdict(draft)
if d.get("password"):
d["password"] = _ss.encrypt(d["password"])
draft_path().write_text(json.dumps(d, indent=2), encoding="utf-8")
def load_accounts() -> list[AccountRecord]:
from . import secrets_store as _ss
p = accounts_path()
if not p.is_file():
return []
try:
raw = json.loads(p.read_text(encoding="utf-8"))
if not isinstance(raw, list):
return []
out: list[AccountRecord] = []
for item in raw:
if not isinstance(item, dict):
continue
fields = AccountRecord.__dataclass_fields__
kwargs = {k: item.get(k, "") for k in fields}
if not kwargs.get("id"):
kwargs["id"] = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S%f")
# Transparent DPAPI decrypt; plaintext passes through.
if kwargs.get("password"):
kwargs["password"] = _ss.decrypt(kwargs["password"])
out.append(AccountRecord(**kwargs))
return out
except (OSError, json.JSONDecodeError, TypeError):
return []
def save_accounts(rows: list[AccountRecord]) -> None:
from . import secrets_store as _ss
serialized = []
for r in rows:
d = asdict(r)
# Encrypt sensitive fields at rest using Windows DPAPI when available.
# Legacy plaintext entries are migrated transparently on first save.
if d.get("password"):
d["password"] = _ss.encrypt(d["password"])
serialized.append(d)
accounts_path().write_text(
json.dumps(serialized, indent=2),
encoding="utf-8",
)
def append_account(record: AccountRecord) -> None:
rows = load_accounts()
rows.insert(0, record)
save_accounts(rows)
def build_autofill_config(draft: SignupDraft) -> dict[str, Any]:
preset = preset_for(draft.site_key)
hosts = list(preset.hosts) if preset else []
selectors: dict[str, list[str]] = {}
if preset:
selectors.update(preset.extra_selectors)
fields: dict[str, str] = {}
if draft.first_name:
fields["first_name"] = draft.first_name
if draft.last_name:
fields["last_name"] = draft.last_name
if draft.username:
fields["username"] = draft.username
if draft.email:
fields["email"] = draft.email
if draft.password:
fields["password"] = draft.password
fields["password_confirm"] = draft.password
out_fields: dict[str, Any] = {}
for key, val in fields.items():
out_fields[key] = val
sel_key = f"{key}_selectors"
if key in selectors:
out_fields[sel_key] = selectors[key]
return {
"site": draft.site_key,
"hosts": hosts,
"fields": out_fields,
"updated_at": datetime.now(timezone.utc).isoformat(),
}
def _extensions_json_entry(ext_dir_name: str) -> dict[str, Any]:
now = int(datetime.now(timezone.utc).timestamp()) * 1000
return {
"id": _EXT_ID,
"syncGUID": str(uuid.uuid4()),
"version": "1.0.0",
"type": "extension",
"location": {
"appProfile": {
"root": "app-profile",
"path": f"extensions/{ext_dir_name}",
}
},
"internalName": None,
"updateURL": None,
"webExtensionConverted": False,
"webExtensionId": _EXT_ID,
"active": True,
"userDisabled": False,
"appDisabled": False,
"installDate": now,
"scope": 1,
}
def install_signup_extension(profile_dir: Path, draft: SignupDraft) -> None:
"""Copy autofill WebExtension into the Firefox profile and write current field values."""
profile_dir.mkdir(parents=True, exist_ok=True)
ext_name = _EXT_ID
dest = profile_dir / "extensions" / ext_name
if dest.exists():
shutil.rmtree(dest, ignore_errors=True)
shutil.copytree(_ext_src_path(), dest)
cfg = build_autofill_config(draft)
(dest / "autofill_config.json").write_text(json.dumps(cfg, indent=2), encoding="utf-8")
ext_json_path = profile_dir / "extensions.json"
entry = _extensions_json_entry(ext_name)
data: dict[str, Any]
if ext_json_path.is_file():
try:
data = json.loads(ext_json_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
data = {"schemaVersion": 35, "addons": []}
else:
data = {"schemaVersion": 35, "addons": []}
addons = data.get("addons")
if not isinstance(addons, list):
addons = []
addons = [a for a in addons if isinstance(a, dict) and a.get("id") != _EXT_ID]
addons.append(entry)
data["addons"] = addons
ext_json_path.write_text(json.dumps(data, indent=2), encoding="utf-8")