Files
NF-MSL/main.py
T
2026-05-30 10:52:45 +08:00

158 lines
5.7 KiB
Python

from __future__ import annotations
import argparse, glob as _glob, os, re, sys, time
from pathlib import Path
from typing import List
from modules.config import setup_config
from modules.logging import setup_logger
from modules.platforms.android_rsa import run_android_rsa
from modules.platforms.android import run_android
from modules.platforms.ios import run_ios
from modules.platforms.tv import run_tv
from modules.platforms.tv_otp import run_tv_otp
from modules.platforms.web import run_web
from modules.platforms.mgk import run_mgk
log = setup_logger("MSL HANDSHAKE")
class _ColoredHelpFormatter(argparse.HelpFormatter):
"""HelpFormatter with ANSI colors applied after layout so column widths are unaffected."""
_RST = "\033[0m"
_BOLD = "\033[1m"
_CYAN = "\033[36m"
_GREEN = "\033[32m"
_YELLOW = "\033[33m"
@staticmethod
def _tty() -> bool:
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
def _c(self, codes: str, text: str) -> str:
return f"{codes}{text}{self._RST}" if self._tty() else text
def _format_usage(self, usage, actions, groups, prefix):
if prefix is None:
prefix = self._c(f"{self._YELLOW}{self._BOLD}", "usage") + ": "
return super()._format_usage(usage, actions, groups, prefix)
def start_section(self, heading: str | None) -> None:
if heading:
heading = self._c(f"{self._CYAN}{self._BOLD}", heading)
super().start_section(heading)
def _format_action(self, action: argparse.Action) -> str:
text = super()._format_action(action)
if not self._tty():
return text
# Color flag/option text (e.g. "-h, --help" or "--platform {…}")
# Applied after layout so len() calculations are already done.
return re.sub(
r"^(\s+)(-\S.*?)(\s{2,}|$)",
lambda m: m.group(1) + self._c(self._GREEN, m.group(2)) + m.group(3),
text,
flags=re.MULTILINE,
)
class _ColoredArgumentParser(argparse.ArgumentParser):
"""ArgumentParser with colored --help and colored error messages."""
def error(self, message: str) -> None:
log.error(message)
self.print_usage(sys.stderr)
sys.exit(2)
_WVD_LOOP_DELAY = 10 # seconds between WVD iterations to avoid throttling
def _run_wvd_loop(run_fn, wvd_paths: List[Path], **kwargs) -> None:
passed: List[str] = []
failed: List[str] = []
for wvd_path in wvd_paths:
if passed or failed:
log.info("Waiting %ds before next WVD to avoid throttling...", _WVD_LOOP_DELAY)
time.sleep(_WVD_LOOP_DELAY)
log.info("--- [%d/%d] WVD: %s ---", len(passed) + len(failed) + 1, len(wvd_paths), wvd_path.name)
try:
run_fn(wvd_path=wvd_path, **kwargs)
passed.append(wvd_path.name)
except SystemExit:
failed.append(wvd_path.name)
log.warning("WVD failed, continuing to next...")
except Exception as exc:
failed.append(wvd_path.name)
log.warning("WVD raised %s: %s — continuing to next...", type(exc).__name__, exc)
log.info("=== Results: %d/%d passed ===", len(passed), len(wvd_paths))
for name in passed:
log.info(" PASS: %s", name)
for name in failed:
log.warning(" FAIL: %s", name)
if not passed:
sys.exit(1)
def main():
WVD_PLATFORMS = {"android", "ios", "tv", "tv_otp"}
parser = _ColoredArgumentParser(
description="Netflix MSL multi-platform login",
formatter_class=_ColoredHelpFormatter,
)
parser.add_argument("--platform", required=True, choices=["android", "android_rsa", "ios", "tv", "tv_otp", "web", "mgk"], help="Target platform")
parser.add_argument("--wvd", type=str, help="Path or glob pattern to .wvd file(s) (e.g. devices/*.wvd)")
parser.add_argument("--kpekph", type=str, default=None, help="KpeKph value (Kpe:Kph) or file path (mgk platform); auto-discovered if omitted")
parser.add_argument("--esnid", type=str, help="ESN identity string or file path (mgk platform)")
parser.add_argument("--new-msl", action="store_true", help="Force new MSL key exchange")
parser.add_argument("--no-verify", action="store_true", help="Skip TLS verification")
parser.add_argument("--proxy", type=str, default=None, help="Proxy URL (e.g. http://ip:port or http://user:pass@ip:port)")
args = parser.parse_args()
wvd_paths: List[Path] = []
if args.platform in WVD_PLATFORMS:
if not args.wvd:
parser.error(f"--wvd is required for {args.platform} platform")
wvd_paths = sorted(
Path(p) for p in _glob.glob(args.wvd) if p.endswith(".wvd")
)
if not wvd_paths:
parser.error(f"No .wvd files found matching: {args.wvd}")
log.info("Found %d .wvd file(s) to test", len(wvd_paths))
kwargs = {"new_msl": args.new_msl, "no_verify": args.no_verify, "proxy": args.proxy}
if args.platform == "android_rsa":
run_android_rsa(**kwargs)
elif args.platform == "android":
_run_wvd_loop(run_android, wvd_paths, **kwargs)
elif args.platform == "ios":
_run_wvd_loop(run_ios, wvd_paths, **kwargs)
elif args.platform == "tv":
_run_wvd_loop(run_tv, wvd_paths, **kwargs)
elif args.platform == "tv_otp":
_run_wvd_loop(run_tv_otp, wvd_paths, **kwargs)
elif args.platform == "web":
run_web(**kwargs)
elif args.platform == "mgk":
if not args.esnid:
parser.error("--esnid is required for mgk platform")
run_mgk(kpekph_path=args.kpekph, esnid=args.esnid, **kwargs)
if __name__ == "__main__":
if os.name == "nt":
os.system('cls')
else:
os.system('clear')
main()