mirror of
https://github.com/denuitt1/mhr-cfw.git
synced 2026-05-17 21:24:36 +03:00
add setup.py
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interactive setup wizard.
|
||||
|
||||
Writes a ready-to-use config.json by prompting only for the values
|
||||
the user really has to choose. Everything else gets a sane default.
|
||||
|
||||
Run:
|
||||
python setup.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import shutil
|
||||
import string
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
CONFIG_PATH = HERE / "config.json"
|
||||
EXAMPLE_PATH = HERE / "config.example.json"
|
||||
|
||||
|
||||
def _c(code: str, text: str) -> str:
|
||||
if os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
||||
return text
|
||||
return f"\033[{code}m{text}\033[0m"
|
||||
|
||||
|
||||
def bold(t: str) -> str: return _c("1", t)
|
||||
def cyan(t: str) -> str: return _c("36", t)
|
||||
def green(t: str) -> str: return _c("32", t)
|
||||
def yellow(t: str) -> str: return _c("33", t)
|
||||
def red(t: str) -> str: return _c("31", t)
|
||||
def dim(t: str) -> str: return _c("2", t)
|
||||
|
||||
|
||||
def prompt(question: str, default: str | None = None) -> str:
|
||||
suffix = f" [{dim(default)}]" if default else ""
|
||||
while True:
|
||||
try:
|
||||
raw = input(f"{cyan('?')} {question}{suffix}: ").strip()
|
||||
except EOFError:
|
||||
print()
|
||||
sys.exit(1)
|
||||
if not raw and default is not None:
|
||||
return default
|
||||
if raw:
|
||||
return raw
|
||||
print(red(" value required"))
|
||||
|
||||
|
||||
def prompt_yes_no(question: str, default: bool = True) -> bool:
|
||||
hint = "Y/n" if default else "y/N"
|
||||
while True:
|
||||
raw = input(f"{cyan('?')} {question} [{hint}]: ").strip().lower()
|
||||
if not raw:
|
||||
return default
|
||||
if raw in ("y", "yes"):
|
||||
return True
|
||||
if raw in ("n", "no"):
|
||||
return False
|
||||
|
||||
|
||||
def random_auth_key(length: int = 32) -> str:
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def load_base_config() -> dict:
|
||||
if EXAMPLE_PATH.exists():
|
||||
try:
|
||||
with EXAMPLE_PATH.open() as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"mode": "apps_script",
|
||||
"google_ip": "216.239.38.120",
|
||||
"front_domain": "www.google.com",
|
||||
"listen_host": "127.0.0.1",
|
||||
"listen_port": 8085,
|
||||
"socks5_enabled": True,
|
||||
"socks5_port": 1080,
|
||||
"log_level": "INFO",
|
||||
"verify_ssl": True,
|
||||
"lan_sharing": False,
|
||||
"relay_timeout": 25,
|
||||
"tls_connect_timeout": 15,
|
||||
"tcp_connect_timeout": 10,
|
||||
"max_response_body_bytes": 200 * 1024 * 1024,
|
||||
"chunked_download_min_size": 5 * 1024 * 1024,
|
||||
"chunked_download_chunk_size": 512 * 1024,
|
||||
"chunked_download_max_parallel": 8,
|
||||
"chunked_download_max_chunks": 256,
|
||||
"hosts": {},
|
||||
}
|
||||
|
||||
|
||||
def configure_apps_script(cfg: dict) -> dict:
|
||||
print()
|
||||
print(bold("Google Apps Script setup"))
|
||||
print(dim(" 1. Open https://script.google.com -> New project"))
|
||||
print(dim(" 2. Paste apps_script/Code.gs from this repo into the editor"))
|
||||
print(dim(" 3. Set AUTH_KEY in Code.gs to the password below"))
|
||||
print(dim(" 4. Deploy -> New deployment -> Web app"))
|
||||
print(dim(" Execute as: Me | Who has access: Anyone"))
|
||||
print(dim(" 5. Copy the Deployment ID and paste it here"))
|
||||
print()
|
||||
|
||||
ids_raw = prompt(
|
||||
"Deployment ID(s) - comma-separated for load balancing",
|
||||
default=None,
|
||||
)
|
||||
ids = [x.strip() for x in ids_raw.split(",") if x.strip()]
|
||||
if len(ids) == 1:
|
||||
cfg["script_id"] = ids[0]
|
||||
cfg.pop("script_ids", None)
|
||||
else:
|
||||
cfg["script_ids"] = ids
|
||||
cfg.pop("script_id", None)
|
||||
return cfg
|
||||
|
||||
|
||||
def configure_network(cfg: dict) -> dict:
|
||||
print()
|
||||
print(bold("Network settings") + dim(" (press enter to accept defaults)"))
|
||||
cfg["lan_sharing"] = prompt_yes_no(
|
||||
"Enable LAN sharing?",
|
||||
default=bool(cfg.get("lan_sharing", False)),
|
||||
)
|
||||
|
||||
default_host = str(cfg.get("listen_host", "127.0.0.1"))
|
||||
if cfg["lan_sharing"] and default_host == "127.0.0.1":
|
||||
default_host = "0.0.0.0"
|
||||
cfg["listen_host"] = prompt("Listen host", default=default_host)
|
||||
|
||||
port = prompt("HTTP proxy port", default=str(cfg.get("listen_port", 8085)))
|
||||
try:
|
||||
cfg["listen_port"] = int(port)
|
||||
except ValueError:
|
||||
cfg["listen_port"] = 8085
|
||||
|
||||
socks5 = prompt_yes_no("Enable SOCKS5 proxy?", default=bool(cfg.get("socks5_enabled", True)))
|
||||
cfg["socks5_enabled"] = socks5
|
||||
if socks5:
|
||||
sport = prompt("SOCKS5 port", default=str(cfg.get("socks5_port", 1080)))
|
||||
try:
|
||||
cfg["socks5_port"] = int(sport)
|
||||
except ValueError:
|
||||
cfg["socks5_port"] = 1080
|
||||
return cfg
|
||||
|
||||
|
||||
def write_config(cfg: dict) -> None:
|
||||
if CONFIG_PATH.exists():
|
||||
backup = CONFIG_PATH.with_suffix(".json.bak")
|
||||
shutil.copy2(CONFIG_PATH, backup)
|
||||
print(yellow(f" existing config.json backed up to {backup.name}"))
|
||||
with CONFIG_PATH.open("w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print()
|
||||
print(bold("mhr-cfw - setup wizard"))
|
||||
print(dim("Answer a few questions and we'll write config.json for you."))
|
||||
|
||||
if CONFIG_PATH.exists():
|
||||
if not prompt_yes_no("config.json already exists. Overwrite?", default=False):
|
||||
print(dim("Nothing changed."))
|
||||
return 0
|
||||
|
||||
cfg = load_base_config()
|
||||
cfg["mode"] = "apps_script"
|
||||
|
||||
suggested_key = random_auth_key()
|
||||
print()
|
||||
print(bold("Shared password (auth_key)"))
|
||||
print(dim(" Must match AUTH_KEY inside apps_script/Code.gs."))
|
||||
cfg["auth_key"] = prompt("auth_key", default=suggested_key)
|
||||
|
||||
cfg = configure_apps_script(cfg)
|
||||
cfg = configure_network(cfg)
|
||||
|
||||
write_config(cfg)
|
||||
|
||||
print()
|
||||
print(green(f"[OK] wrote {CONFIG_PATH.name}"))
|
||||
print()
|
||||
print(bold("Next step:"))
|
||||
print(f" python main.py")
|
||||
print()
|
||||
print(yellow("Reminder: the AUTH_KEY inside apps_script/Code.gs must match the auth_key"))
|
||||
print(yellow("you just entered - otherwise the relay will return 'unauthorized'."))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
print(dim("Cancelled."))
|
||||
sys.exit(130)
|
||||
Reference in New Issue
Block a user