mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-17 21:24:37 +03:00
Add cross-platform CA auto-installer and production hardening
- Add cert_installer.py: cross-platform trusted CA installer (Windows certutil/PowerShell, macOS security, Linux update-ca-certificates, Firefox NSS via certutil/certutil) - main.py: add --install-cert and --no-cert-check CLI flags; auto-detect and auto-install MITM CA on startup when not yet trusted - mitm.py: rename CA CN/O from 'DomainFront Tunnel' to 'MasterHttpRelayVPN' - proxy_server.py: downgrade TLS handshake errors to DEBUG to reduce log noise for non-HTTPS traffic (MTProto, plain HTTP on non-443 ports) - README.md / README_FA.md: document new CLI flags, auto-install behaviour, and cert_installer.py in project files table
This commit is contained in:
@@ -152,6 +152,8 @@ Firefox uses its own certificate store, so even after OS-level install you need
|
|||||||
3. Select `ca/ca.crt` from the project folder.
|
3. Select `ca/ca.crt` from the project folder.
|
||||||
4. Check **Trust this CA to identify websites** → click **OK**.
|
4. Check **Trust this CA to identify websites** → click **OK**.
|
||||||
|
|
||||||
|
> **Auto-install on startup:** When running in `apps_script` mode the proxy will automatically detect if the CA is not yet trusted and attempt to install it for you. If it succeeds you'll see a confirmation in the log; if it fails (e.g. needs administrator rights) it will print instructions. You can also run `python main.py --install-cert` at any time to (re-)install the certificate.
|
||||||
|
|
||||||
> ⚠️ **Security note:** This certificate only works locally on your machine. Don't share the `ca/` folder with anyone. If you want to start fresh, delete the `ca/` folder and the tool will generate a new one.
|
> ⚠️ **Security note:** This certificate only works locally on your machine. Don't share the `ca/` folder with anyone. If you want to start fresh, delete the `ca/` folder and the tool will generate a new one.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -222,8 +224,12 @@ python main.py # Normal start
|
|||||||
python main.py -p 9090 # Use port 9090 instead
|
python main.py -p 9090 # Use port 9090 instead
|
||||||
python main.py --log-level DEBUG # Show detailed logs
|
python main.py --log-level DEBUG # Show detailed logs
|
||||||
python main.py -c /path/to/config.json # Use a different config file
|
python main.py -c /path/to/config.json # Use a different config file
|
||||||
|
python main.py --install-cert # Install MITM CA certificate and exit
|
||||||
|
python main.py --no-cert-check # Skip automatic CA install check on startup
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Auto-install:** On startup (MITM mode), the proxy automatically checks if the CA certificate is trusted and attempts to install it. Use `--no-cert-check` to skip this. If auto-install fails (e.g. needs elevation), run `python main.py --install-cert` manually or follow Step 6 above.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -248,6 +254,7 @@ python main.py -c /path/to/config.json # Use a different config file
|
|||||||
| `domain_fronter.py` | Disguises traffic through CDN/Google |
|
| `domain_fronter.py` | Disguises traffic through CDN/Google |
|
||||||
| `h2_transport.py` | Faster connections using HTTP/2 (optional) |
|
| `h2_transport.py` | Faster connections using HTTP/2 (optional) |
|
||||||
| `mitm.py` | Handles HTTPS certificate generation |
|
| `mitm.py` | Handles HTTPS certificate generation |
|
||||||
|
| `cert_installer.py` | Cross-platform CA certificate installer (Windows/macOS/Linux + Firefox) |
|
||||||
| `ws.py` | WebSocket support |
|
| `ws.py` | WebSocket support |
|
||||||
| `Code.gs` | The relay script you deploy to Google Apps Script |
|
| `Code.gs` | The relay script you deploy to Google Apps Script |
|
||||||
| `config.example.json` | Example config — copy to `config.json` |
|
| `config.example.json` | Example config — copy to `config.json` |
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ Firefox معمولا certificate store جداگانه دارد:
|
|||||||
4. فایل `ca/ca.crt` را انتخاب کنید.
|
4. فایل `ca/ca.crt` را انتخاب کنید.
|
||||||
5. گزینه **Trust this CA to identify websites** را فعال کنید.
|
5. گزینه **Trust this CA to identify websites** را فعال کنید.
|
||||||
|
|
||||||
|
> **نصب خودکار هنگام اجرا:** در حالت `apps_script`، برنامه به صورت خودکار وضعیت اعتماد گواهی CA را بررسی کرده و در صورت نیاز نصب میکند. در صورت موفقیت پیام تأیید در لاگ نمایش داده میشود. اگر نصب خودکار ناموفق بود، میتوانید دستور `python main.py --install-cert` را اجرا کنید.
|
||||||
|
|
||||||
نکته امنیتی: پوشه `ca/` را با کسی به اشتراک نگذارید. اگر خواستید از اول گواهی جدید بسازید، این پوشه را حذف کنید تا دوباره ساخته شود.
|
نکته امنیتی: پوشه `ca/` را با کسی به اشتراک نگذارید. اگر خواستید از اول گواهی جدید بسازید، این پوشه را حذف کنید تا دوباره ساخته شود.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -210,8 +212,12 @@ python main.py
|
|||||||
python main.py -p 9090
|
python main.py -p 9090
|
||||||
python main.py --log-level DEBUG
|
python main.py --log-level DEBUG
|
||||||
python main.py -c /path/to/config.json
|
python main.py -c /path/to/config.json
|
||||||
|
python main.py --install-cert # نصب گواهی CA و خروج
|
||||||
|
python main.py --no-cert-check # رد شدن از بررسی خودکار گواهی
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **نصب خودکار:** هنگام اجرا در حالت `apps_script`، برنامه بهطور خودکار بررسی میکند که آیا گواهی CA قابل اعتماد است یا نه و در صورت نیاز آن را نصب میکند. اگر نصب خودکار ناموفق بود (مثلاً نیاز به دسترسی مدیر دارد)، میتوانید دستور `python main.py --install-cert` را اجرا کنید یا مراحل مرحله ۶ را دنبال کنید.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## معماری
|
## معماری
|
||||||
@@ -234,6 +240,7 @@ python main.py -c /path/to/config.json
|
|||||||
| `domain_fronter.py` | انجام domain fronting |
|
| `domain_fronter.py` | انجام domain fronting |
|
||||||
| `h2_transport.py` | ارتباط سریعتر با HTTP/2 |
|
| `h2_transport.py` | ارتباط سریعتر با HTTP/2 |
|
||||||
| `mitm.py` | ساخت و مدیریت certificate |
|
| `mitm.py` | ساخت و مدیریت certificate |
|
||||||
|
| `cert_installer.py` | نصب خودکار گواهی CA در ویندوز، مک، لینوکس و Firefox |
|
||||||
| `ws.py` | پشتیبانی WebSocket |
|
| `ws.py` | پشتیبانی WebSocket |
|
||||||
| `Code.gs` | رله Apps Script |
|
| `Code.gs` | رله Apps Script |
|
||||||
| `config.example.json` | فایل نمونه تنظیمات |
|
| `config.example.json` | فایل نمونه تنظیمات |
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
"""
|
||||||
|
Cross-platform trusted CA certificate installer.
|
||||||
|
|
||||||
|
Supports: Windows, macOS, Linux (Debian/Ubuntu, RHEL/Fedora/CentOS, Arch).
|
||||||
|
Also attempts to install into Firefox's NSS certificate store when found.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from cert_installer import install_ca, is_ca_trusted
|
||||||
|
install_ca("/path/to/ca.crt", cert_name="MasterHttpRelayVPN")
|
||||||
|
"""
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
log = logging.getLogger("CertInstaller")
|
||||||
|
|
||||||
|
CERT_NAME = "MasterHttpRelayVPN"
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Helpers
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _run(cmd: list[str], *, check: bool = True, capture: bool = True) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(
|
||||||
|
cmd,
|
||||||
|
check=check,
|
||||||
|
stdout=subprocess.PIPE if capture else None,
|
||||||
|
stderr=subprocess.PIPE if capture else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_cmd(name: str) -> bool:
|
||||||
|
return shutil.which(name) is not None
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Windows
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _install_windows(cert_path: str, cert_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Install into the current user's Trusted Root store (no admin required).
|
||||||
|
Falls back to the system store if certutil fails.
|
||||||
|
"""
|
||||||
|
# Per-user store — works without elevation
|
||||||
|
try:
|
||||||
|
_run(["certutil", "-addstore", "-user", "Root", cert_path])
|
||||||
|
log.info("Certificate installed in Windows user Trusted Root store.")
|
||||||
|
return True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||||
|
log.warning("certutil user store failed: %s", exc)
|
||||||
|
|
||||||
|
# Try system store (requires admin)
|
||||||
|
try:
|
||||||
|
_run(["certutil", "-addstore", "Root", cert_path])
|
||||||
|
log.info("Certificate installed in Windows system Trusted Root store.")
|
||||||
|
return True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||||
|
log.error("certutil system store failed: %s", exc)
|
||||||
|
|
||||||
|
# Fallback: use PowerShell
|
||||||
|
try:
|
||||||
|
ps_cmd = (
|
||||||
|
f"Import-Certificate -FilePath '{cert_path}' "
|
||||||
|
f"-CertStoreLocation Cert:\\CurrentUser\\Root"
|
||||||
|
)
|
||||||
|
_run(["powershell", "-NoProfile", "-Command", ps_cmd])
|
||||||
|
log.info("Certificate installed via PowerShell.")
|
||||||
|
return True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||||
|
log.error("PowerShell install failed: %s", exc)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_trusted_windows(cert_path: str) -> bool:
|
||||||
|
# Check by thumbprint only — the cert name is also the folder name, so
|
||||||
|
# a plain string search on certutil output would produce a false positive.
|
||||||
|
thumbprint = _cert_thumbprint(cert_path)
|
||||||
|
if not thumbprint:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
result = _run(["certutil", "-user", "-store", "Root"])
|
||||||
|
output = result.stdout.decode(errors="replace").upper()
|
||||||
|
return thumbprint in output
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _cert_thumbprint(cert_path: str) -> str:
|
||||||
|
"""Return the SHA-1 thumbprint of a PEM cert (uppercase hex, no colons)."""
|
||||||
|
try:
|
||||||
|
from cryptography import x509 as _x509
|
||||||
|
from cryptography.hazmat.primitives import hashes as _hashes
|
||||||
|
with open(cert_path, "rb") as f:
|
||||||
|
cert = _x509.load_pem_x509_certificate(f.read())
|
||||||
|
return cert.fingerprint(_hashes.SHA1()).hex().upper()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# macOS
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _install_macos(cert_path: str, cert_name: str) -> bool:
|
||||||
|
"""Install into the login keychain (per-user, no sudo required)."""
|
||||||
|
login_keychain = os.path.expanduser("~/Library/Keychains/login.keychain-db")
|
||||||
|
if not os.path.exists(login_keychain):
|
||||||
|
login_keychain = os.path.expanduser("~/Library/Keychains/login.keychain")
|
||||||
|
|
||||||
|
try:
|
||||||
|
_run([
|
||||||
|
"security", "add-trusted-cert",
|
||||||
|
"-d", "-r", "trustRoot",
|
||||||
|
"-k", login_keychain,
|
||||||
|
cert_path,
|
||||||
|
])
|
||||||
|
log.info("Certificate installed in macOS login keychain.")
|
||||||
|
return True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||||
|
log.warning("login keychain install failed: %s. Trying system keychain (needs sudo)…", exc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_run([
|
||||||
|
"sudo", "security", "add-trusted-cert",
|
||||||
|
"-d", "-r", "trustRoot",
|
||||||
|
"-k", "/Library/Keychains/System.keychain",
|
||||||
|
cert_path,
|
||||||
|
])
|
||||||
|
log.info("Certificate installed in macOS system keychain.")
|
||||||
|
return True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||||
|
log.error("System keychain install failed: %s", exc)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_trusted_macos(cert_name: str) -> bool:
|
||||||
|
try:
|
||||||
|
result = _run(["security", "find-certificate", "-a", "-c", cert_name])
|
||||||
|
return bool(result.stdout.strip())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Linux
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _detect_linux_distro() -> str:
|
||||||
|
"""Return 'debian', 'rhel', 'arch', or 'unknown'."""
|
||||||
|
if os.path.exists("/etc/debian_version") or os.path.exists("/etc/ubuntu"):
|
||||||
|
return "debian"
|
||||||
|
if os.path.exists("/etc/redhat-release") or os.path.exists("/etc/fedora-release"):
|
||||||
|
return "rhel"
|
||||||
|
if os.path.exists("/etc/arch-release"):
|
||||||
|
return "arch"
|
||||||
|
# Read /etc/os-release as fallback
|
||||||
|
try:
|
||||||
|
with open("/etc/os-release") as f:
|
||||||
|
content = f.read().lower()
|
||||||
|
if "debian" in content or "ubuntu" in content or "mint" in content:
|
||||||
|
return "debian"
|
||||||
|
if "fedora" in content or "rhel" in content or "centos" in content or "rocky" in content or "alma" in content:
|
||||||
|
return "rhel"
|
||||||
|
if "arch" in content or "manjaro" in content:
|
||||||
|
return "arch"
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _install_linux(cert_path: str, cert_name: str) -> bool:
|
||||||
|
distro = _detect_linux_distro()
|
||||||
|
log.info("Detected Linux distro family: %s", distro)
|
||||||
|
|
||||||
|
installed = False
|
||||||
|
|
||||||
|
if distro == "debian":
|
||||||
|
dest_dir = "/usr/local/share/ca-certificates"
|
||||||
|
dest_file = os.path.join(dest_dir, f"{cert_name.replace(' ', '_')}.crt")
|
||||||
|
try:
|
||||||
|
os.makedirs(dest_dir, exist_ok=True)
|
||||||
|
shutil.copy2(cert_path, dest_file)
|
||||||
|
_run(["update-ca-certificates"])
|
||||||
|
log.info("Certificate installed via update-ca-certificates.")
|
||||||
|
installed = True
|
||||||
|
except (OSError, subprocess.CalledProcessError) as exc:
|
||||||
|
log.warning("Debian install failed (needs sudo?): %s", exc)
|
||||||
|
# Try with sudo
|
||||||
|
try:
|
||||||
|
_run(["sudo", "cp", cert_path, dest_file])
|
||||||
|
_run(["sudo", "update-ca-certificates"])
|
||||||
|
log.info("Certificate installed via sudo update-ca-certificates.")
|
||||||
|
installed = True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc2:
|
||||||
|
log.error("sudo Debian install failed: %s", exc2)
|
||||||
|
|
||||||
|
elif distro == "rhel":
|
||||||
|
dest_dir = "/etc/pki/ca-trust/source/anchors"
|
||||||
|
dest_file = os.path.join(dest_dir, f"{cert_name.replace(' ', '_')}.crt")
|
||||||
|
try:
|
||||||
|
os.makedirs(dest_dir, exist_ok=True)
|
||||||
|
shutil.copy2(cert_path, dest_file)
|
||||||
|
_run(["update-ca-trust", "extract"])
|
||||||
|
log.info("Certificate installed via update-ca-trust.")
|
||||||
|
installed = True
|
||||||
|
except (OSError, subprocess.CalledProcessError) as exc:
|
||||||
|
log.warning("RHEL install failed (needs sudo?): %s", exc)
|
||||||
|
try:
|
||||||
|
_run(["sudo", "cp", cert_path, dest_file])
|
||||||
|
_run(["sudo", "update-ca-trust", "extract"])
|
||||||
|
log.info("Certificate installed via sudo update-ca-trust.")
|
||||||
|
installed = True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc2:
|
||||||
|
log.error("sudo RHEL install failed: %s", exc2)
|
||||||
|
|
||||||
|
elif distro == "arch":
|
||||||
|
dest_dir = "/etc/ca-certificates/trust-source/anchors"
|
||||||
|
dest_file = os.path.join(dest_dir, f"{cert_name.replace(' ', '_')}.crt")
|
||||||
|
try:
|
||||||
|
os.makedirs(dest_dir, exist_ok=True)
|
||||||
|
shutil.copy2(cert_path, dest_file)
|
||||||
|
_run(["trust", "extract-compat"])
|
||||||
|
log.info("Certificate installed via trust extract-compat.")
|
||||||
|
installed = True
|
||||||
|
except (OSError, subprocess.CalledProcessError) as exc:
|
||||||
|
log.warning("Arch install failed (needs sudo?): %s", exc)
|
||||||
|
try:
|
||||||
|
_run(["sudo", "cp", cert_path, dest_file])
|
||||||
|
_run(["sudo", "trust", "extract-compat"])
|
||||||
|
log.info("Certificate installed via sudo trust extract-compat.")
|
||||||
|
installed = True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc2:
|
||||||
|
log.error("sudo Arch install failed: %s", exc2)
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
"Unknown Linux distro. Manually install %s as a trusted root CA.", cert_path
|
||||||
|
)
|
||||||
|
|
||||||
|
return installed
|
||||||
|
|
||||||
|
|
||||||
|
def _is_trusted_linux(cert_path: str) -> bool:
|
||||||
|
"""Check if our cert thumbprint is in the system's OpenSSL trust bundle."""
|
||||||
|
thumbprint = _cert_thumbprint(cert_path)
|
||||||
|
if not thumbprint:
|
||||||
|
return False
|
||||||
|
bundle_paths = [
|
||||||
|
"/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
|
||||||
|
"/etc/pki/tls/certs/ca-bundle.crt", # RHEL/Fedora
|
||||||
|
"/etc/ssl/ca-bundle.pem", # OpenSUSE
|
||||||
|
"/etc/ca-certificates/ca-certificates.crt",
|
||||||
|
]
|
||||||
|
# A fast heuristic: check if our CA cert file was copied to known dirs
|
||||||
|
anchor_dirs = [
|
||||||
|
"/usr/local/share/ca-certificates",
|
||||||
|
"/etc/pki/ca-trust/source/anchors",
|
||||||
|
"/etc/ca-certificates/trust-source/anchors",
|
||||||
|
]
|
||||||
|
for d in anchor_dirs:
|
||||||
|
if os.path.isdir(d):
|
||||||
|
for f in os.listdir(d):
|
||||||
|
if "DomainFront" in f or "domainfront" in f.lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Firefox NSS (cross-platform)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _install_firefox(cert_path: str, cert_name: str):
|
||||||
|
"""Install into all detected Firefox profile NSS databases."""
|
||||||
|
if not _has_cmd("certutil"):
|
||||||
|
log.debug("NSS certutil not found — skipping Firefox install.")
|
||||||
|
return
|
||||||
|
|
||||||
|
profile_dirs: list[str] = []
|
||||||
|
system = platform.system()
|
||||||
|
|
||||||
|
if system == "Windows":
|
||||||
|
appdata = os.environ.get("APPDATA", "")
|
||||||
|
profile_dirs += glob.glob(os.path.join(appdata, r"Mozilla\Firefox\Profiles\*"))
|
||||||
|
elif system == "Darwin":
|
||||||
|
profile_dirs += glob.glob(os.path.expanduser("~/Library/Application Support/Firefox/Profiles/*"))
|
||||||
|
else:
|
||||||
|
profile_dirs += glob.glob(os.path.expanduser("~/.mozilla/firefox/*.default*"))
|
||||||
|
profile_dirs += glob.glob(os.path.expanduser("~/.mozilla/firefox/*.release*"))
|
||||||
|
|
||||||
|
if not profile_dirs:
|
||||||
|
log.debug("No Firefox profiles found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for profile in profile_dirs:
|
||||||
|
db = f"sql:{profile}" if os.path.exists(os.path.join(profile, "cert9.db")) else f"dbm:{profile}"
|
||||||
|
try:
|
||||||
|
# Remove old entry first (ignore errors)
|
||||||
|
_run(["certutil", "-D", "-n", cert_name, "-d", db], check=False)
|
||||||
|
_run([
|
||||||
|
"certutil", "-A",
|
||||||
|
"-n", cert_name,
|
||||||
|
"-t", "CT,,",
|
||||||
|
"-i", cert_path,
|
||||||
|
"-d", db,
|
||||||
|
])
|
||||||
|
log.info("Installed in Firefox profile: %s", os.path.basename(profile))
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||||
|
log.warning("Firefox profile %s: %s", os.path.basename(profile), exc)
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Public API
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def is_ca_trusted(cert_path: str) -> bool:
|
||||||
|
"""Return True if the CA cert appears to be already installed."""
|
||||||
|
system = platform.system()
|
||||||
|
try:
|
||||||
|
if system == "Windows":
|
||||||
|
return _is_trusted_windows(cert_path)
|
||||||
|
if system == "Darwin":
|
||||||
|
return _is_trusted_macos(CERT_NAME)
|
||||||
|
return _is_trusted_linux(cert_path)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def install_ca(cert_path: str, cert_name: str = CERT_NAME) -> bool:
|
||||||
|
"""
|
||||||
|
Install *cert_path* as a trusted root CA on the current platform.
|
||||||
|
Also attempts Firefox NSS installation.
|
||||||
|
|
||||||
|
Returns True if the system store installation succeeded.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(cert_path):
|
||||||
|
log.error("Certificate file not found: %s", cert_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
system = platform.system()
|
||||||
|
log.info("Installing CA certificate on %s…", system)
|
||||||
|
|
||||||
|
if system == "Windows":
|
||||||
|
ok = _install_windows(cert_path, cert_name)
|
||||||
|
elif system == "Darwin":
|
||||||
|
ok = _install_macos(cert_path, cert_name)
|
||||||
|
elif system == "Linux":
|
||||||
|
ok = _install_linux(cert_path, cert_name)
|
||||||
|
else:
|
||||||
|
log.error("Unsupported platform: %s", system)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Best-effort Firefox install on all platforms
|
||||||
|
_install_firefox(cert_path, cert_name)
|
||||||
|
|
||||||
|
return ok
|
||||||
@@ -14,6 +14,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from cert_installer import install_ca, is_ca_trusted
|
||||||
|
from mitm import CA_CERT_FILE
|
||||||
from proxy_server import ProxyServer
|
from proxy_server import ProxyServer
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
__version__ = "1.0.0"
|
||||||
@@ -60,6 +62,16 @@ def parse_args():
|
|||||||
action="version",
|
action="version",
|
||||||
version=f"%(prog)s {__version__}",
|
version=f"%(prog)s {__version__}",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--install-cert",
|
||||||
|
action="store_true",
|
||||||
|
help="Install the MITM CA certificate as a trusted root and exit.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-cert-check",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip the certificate installation check on startup.",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -125,6 +137,14 @@ def main():
|
|||||||
print("Deploy the Apps Script from appsscript/Code.gs and paste the Deployment ID.")
|
print("Deploy the Apps Script from appsscript/Code.gs and paste the Deployment ID.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# ── Certificate installation ──────────────────────────────────────────
|
||||||
|
if args.install_cert:
|
||||||
|
setup_logging("INFO")
|
||||||
|
_log = logging.getLogger("Main")
|
||||||
|
_log.info("Installing CA certificate…")
|
||||||
|
ok = install_ca(CA_CERT_FILE)
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
|
|
||||||
setup_logging(config.get("log_level", "INFO"))
|
setup_logging(config.get("log_level", "INFO"))
|
||||||
log = logging.getLogger("Main")
|
log = logging.getLogger("Main")
|
||||||
|
|
||||||
@@ -147,7 +167,27 @@ def main():
|
|||||||
log.info(" [%d] %s", i + 1, sid)
|
log.info(" [%d] %s", i + 1, sid)
|
||||||
else:
|
else:
|
||||||
log.info("Script ID : %s", script_ids)
|
log.info("Script ID : %s", script_ids)
|
||||||
log.info("MITM enabled — install ca/ca.crt in your browser!")
|
|
||||||
|
# Ensure CA file exists before checking / installing it.
|
||||||
|
# MITMCertManager generates ca/ca.crt on first instantiation.
|
||||||
|
if not os.path.exists(CA_CERT_FILE):
|
||||||
|
from mitm import MITMCertManager
|
||||||
|
MITMCertManager() # side-effect: creates ca/ca.crt + ca/ca.key
|
||||||
|
|
||||||
|
# Auto-install MITM CA if not already trusted
|
||||||
|
if not args.no_cert_check:
|
||||||
|
if not is_ca_trusted(CA_CERT_FILE):
|
||||||
|
log.warning("MITM CA is not trusted — attempting automatic installation…")
|
||||||
|
ok = install_ca(CA_CERT_FILE)
|
||||||
|
if ok:
|
||||||
|
log.info("CA certificate installed. You may need to restart your browser.")
|
||||||
|
else:
|
||||||
|
log.error(
|
||||||
|
"Auto-install failed. Run with --install-cert (may need admin/sudo) "
|
||||||
|
"or manually install ca/ca.crt as a trusted root CA."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.info("MITM CA is already trusted.")
|
||||||
else:
|
else:
|
||||||
log.info("Front domain (SNI) : %s", config.get("front_domain", "?"))
|
log.info("Front domain (SNI) : %s", config.get("front_domain", "?"))
|
||||||
log.info("Worker host (Host) : %s", config.get("worker_host", "?"))
|
log.info("Worker host (Host) : %s", config.get("worker_host", "?"))
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ class MITMCertManager:
|
|||||||
public_exponent=65537, key_size=2048
|
public_exponent=65537, key_size=2048
|
||||||
)
|
)
|
||||||
subject = issuer = x509.Name([
|
subject = issuer = x509.Name([
|
||||||
x509.NameAttribute(NameOID.COMMON_NAME, "DomainFront Tunnel CA"),
|
x509.NameAttribute(NameOID.COMMON_NAME, "MasterHttpRelayVPN"),
|
||||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "DomainFront Tunnel"),
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MasterHttpRelayVPN"),
|
||||||
])
|
])
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
self._ca_cert = (
|
self._ca_cert = (
|
||||||
|
|||||||
+7
-1
@@ -279,7 +279,13 @@ class ProxyServer:
|
|||||||
transport, protocol, ssl_ctx, server_side=True,
|
transport, protocol, ssl_ctx, server_side=True,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("TLS handshake failed for %s: %s", host, e)
|
# Non-HTTPS traffic (e.g. MTProto, plain HTTP on port 80/443)
|
||||||
|
# routed through the proxy will always fail TLS — log at DEBUG
|
||||||
|
# to avoid alarming noise.
|
||||||
|
if port != 443:
|
||||||
|
log.debug("TLS handshake skipped for %s:%d (non-HTTPS): %s", host, port, e)
|
||||||
|
else:
|
||||||
|
log.debug("TLS handshake failed for %s: %s", host, e)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update writer to use the new TLS transport
|
# Update writer to use the new TLS transport
|
||||||
|
|||||||
Reference in New Issue
Block a user