import argparse import ipaddress import json from pathlib import Path import re import shutil import subprocess import sys from . import __version__, release_label from .checks import inspect from .i18n import translator from .quality import run_quality from .suite import run_suite from .bandwidth import SOURCES, CHINA_SOURCES LABELS = {"pass": "通过", "warn": "注意", "unknown": "未知", "skip": "跳过", "fail": "失败"} def redact(value, lang="zh"): """Mask IP addresses in exports; never claim arbitrary text is secret-free.""" if isinstance(value, dict): return {key: redact(item, lang) for key, item in value.items()} if isinstance(value, list): return [redact(item, lang) for item in value] if not isinstance(value, str): return value hidden = translator(lang)("[IP已隐藏]") def mask_v6(match): token = match.group(0) address = token.rstrip(".") try: ipaddress.IPv6Address(address.split("%", 1)[0]) return hidden + token[len(address):] except ValueError: return token # IPv6 first: replacing embedded IPv4 first would break mapped IPv6 parsing. value = re.sub(r"[0-9a-fA-F:.]+(?:%[a-zA-Z0-9_.-]+)?", mask_v6, value) return re.sub(r"(?= 0 else 128 - result.returncode) if not 1 <= args.port <= 65535: parser.error(t("端口必须在 1~65535 之间")) if args.public_ip: try: ipaddress.ip_address(args.public_ip) except ValueError: parser.error(t("--public-ip 必须是有效 IPv4 或 IPv6 地址")) raw_data = inspect(port=args.port, public_ip=args.public_ip, lang=lang, detect_public_ip=args.detect_public_ip) from .resources import collect as collect_resources, render as render_resources try: raw_data['resources'] = collect_resources() except Exception: raw_data['resources'] = {'status': 'unknown', 'error_code': 'RESOURCE_MODULE_ERROR'} from .security import collect as collect_security, render as render_security try: raw_data['security'] = collect_security() except Exception: raw_data['security'] = {'status':'unknown','error_code':'SECURITY_MODULE_ERROR'} from .schema import normalize_report normalize_report(raw_data) safe_data = redact(raw_data, lang) data = raw_data if args.show_ip else safe_data serialized = json.dumps(safe_data, ensure_ascii=False, indent=2) if args.output: try: with args.output.open("x", encoding="utf-8") as stream: stream.write(serialized + "\n") except OSError as exc: parser.error(t("无法保存报告:{error}", error=exc)) if args.as_json: print(json.dumps(data, ensure_ascii=False, indent=2)) else: from .console import Console, can_render_cjk console = Console(lang, getattr(args, 'color', 'auto')) theme = getattr(args, 'theme', 'default') use_theme = theme in ('iprisk', 'framed') separator = ": " if lang == "en" else ":" info_ids = {'system', 'architecture', 'kernel', 'cpu', 'memory_total', 'memory', 'disk_capacity', 'disk', 'interfaces', 'public_ip'} status_color = {'pass': 'ok', 'warn': 'bad', 'fail': 'bad', 'unknown': 'unknown', 'skip': 'unknown'} if theme == 'framed': console.frame_open(st('VPSMaster 检测报告','VPSMaster diagnostic report'), box_w=78, subtitle=release_label(lang) + ' · ' + __version__) # header key/values from datetime import datetime console.frame_keyvalue(st('扫描时间','Scan time'), datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')) console.frame_keyvalue(st('语言','Language'), st('中文','English')) console.frame_keyvalue(st('模式','Mode'), st('只读体检','Read-only')) for number, (title, is_info) in enumerate(((st('服务器信息','Server information'), True), (st('体检结果','Diagnostic results'), False)), start=1): console.frame_section(number, title, title) for item in data["checks"]: if item['id'] in ('memory_total','memory'): continue if (item['id'] in info_ids) != is_info: continue console.frame_keyvalue(item['title'], item['detail'], status_color.get(item['status'], 'unknown')) for line in item["evidence"].splitlines(): console.frame_line(st('依据','Evidence') + separator + line, 'muted') elif theme == 'iprisk': console.banner(' VPSMaster ' + ('v' + __version__)) console.line(release_label(lang), 'unknown') console.keyvalue(st('只读体检报告','Read-only diagnostic report'), st('已请求 ipify 查询出口 IP' if args.detect_public_ip else '默认不请求外部网络','ipify egress query' if args.detect_public_ip else 'no external requests by default'), 'title') console.hrule() for number, (title, is_info) in enumerate(((st('服务器信息','Server information'), True), (st('体检结果','Diagnostic results'), False)), start=1): console.theme_section(number, title, title, 'b_cyan') for item in data["checks"]: if item['id'] in ('memory_total','memory'): continue if (item['id'] in info_ids) != is_info: continue console.keyvalue(item['title'], item['detail'], status_color.get(item['status'], 'unknown')) for line in item["evidence"].splitlines(): print(' ' + console.paint(st('依据','Evidence'), 'muted') + separator + line, flush=True) else: print(release_label(lang) + f" · {__version__}\n" + t("只读体检 · 已请求 ipify 查询出口 IP" if args.detect_public_ip else "只读体检 · 默认不请求外部网络") + "\n") for heading, is_info in ((t('服务器信息'), True), (t('体检结果'), False)): print(f"{heading}:") for item in data["checks"]: if item['id'] in ('memory_total','memory'): continue if (item['id'] in info_ids) != is_info: continue print(f"[{t(LABELS[item['status']])}] {item['title']}{separator}{item['detail']}") for line in item["evidence"].splitlines(): print(" " + t("依据") + separator + line) print() counts = {t(label): sum(item["status"] == status for item in data["checks"]) for status, label in LABELS.items()} if data['resources']['status'] in ('partial','unsupported'): if console._framed: console.frame_section(3, '资源限制', 'Resource limits') render_resources(data['resources'], console) else: if console._framed: console.frame_line('RESOURCE_MODULE_ERROR', 'bad') else: print('RESOURCE_MODULE_ERROR') if data['security']['status'] in ('partial','unsupported'): if console._framed: console.frame_section(4, '安全与权限', 'Security & permissions') render_security(data['security'], console) else: if console._framed: console.frame_line('SECURITY_MODULE_ERROR', 'bad') else: print('SECURITY_MODULE_ERROR') if console._framed: summary = " / ".join(f"{key} {value}" for key, value in counts.items()) console.frame_section(5, st('汇总','Summary'), 'Summary') summary_status = 'bad' if any(row['status'] in ('warn','fail') for row in data['checks']) or data['security'].get('review_count') else 'unknown' if any(row['status']=='unknown' for row in data['checks']) else 'ok' console.frame_line(summary, summary_status) console.frame_line(st('终端完整 IP;文件脱敏。','Full terminal IP; exports redacted.') if args.show_ip else t('报告默认隐藏 IP。'), 'muted') console.frame_close() else: print("\n" + " / ".join(f"{key} {value}" for key, value in counts.items())) print(st('终端完整 IP;文件脱敏。','Full terminal IP; exports redacted.') if args.show_ip else t('报告默认隐藏 IP。')) # Stable automation contract: 1 = findings, 2 = invalid usage or export error. return_code = 1 if any(item["status"] in {"warn", "fail"} for item in data["checks"]) or data['security'].get('review_count',0) else 0 sys.exit(return_code) def main(): import os from .worker import reset,cancel_all previous=os.environ.get('PROXYDOCTOR_NETWORK_WORKERS') os.environ['PROXYDOCTOR_NETWORK_WORKERS']='1' reset() try: return _main() except KeyboardInterrupt: cancel_all() raise finally: if previous is None:os.environ.pop('PROXYDOCTOR_NETWORK_WORKERS',None) else:os.environ['PROXYDOCTOR_NETWORK_WORKERS']=previous