mirror of
https://github.com/wechat-article/wxdown-service.git
synced 2026-08-29 00:40:40 +08:00
3bf0b1aaa2
- main: Ctrl+C 时直接 SIGKILL 所有 daemon 子进程,跳过 mitmproxy graceful shutdown
与 mitmdump 重启循环里的 time.sleep(3),实现秒退
- main: 启动失败时透传子进程真实错误行(端口/CA 缺失/文件权限等),告别"请切换端口后重试"
一刀切提示;Windows 下等用户按回车再退出,防止 PowerShell/双击启动窗口闪退
- mitm: start() 返回 (proxy_address, error_line);mitmdump 重启循环处理 KeyboardInterrupt
- watcher: start() 返回 (ws_address, queue, error_line);将 wss 证书生成失败纳入快速失败识别
- utils: check_system_proxy 修复 KeyError — Windows 下 getproxies() 可能返回仅含 'no' 等
非 http/https key 的字典,旧写法 proxy_obj.keys() < {'http', 'https'} 是真子集判断,
会漏掉这些场景继续访问 proxy_obj['http'] 引发崩溃
- ui/startup: 证书安装命令里 %userprofile% 在 PowerShell 下不会展开导致
certutil 报 ERROR_PATH_NOT_FOUND;改为 Python 侧展开为绝对路径,cmd/PowerShell/bash/zsh
均可直接粘贴执行,同时用双引号包住路径兼容带空格的用户名
188 lines
6.3 KiB
Python
188 lines
6.3 KiB
Python
import json
|
|
import multiprocessing
|
|
import queue
|
|
import re
|
|
import ssl
|
|
import sys
|
|
import threading
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
import websockets
|
|
from watchdog.events import FileSystemEventHandler
|
|
from watchdog.observers import Observer
|
|
from websockets.sync.server import serve, ServerConnection
|
|
|
|
import cert
|
|
import utils
|
|
from logger import logger
|
|
|
|
# Credentials.json 文件位置
|
|
SRC_PATH = Path.absolute(Path(__file__)).parent
|
|
CREDENTIALS_DIR = SRC_PATH / 'resources' / 'data'
|
|
CREDENTIALS_JSON_PATH = CREDENTIALS_DIR / 'credentials.json'
|
|
CREDENTIALS_JSON_FILE = str(CREDENTIALS_JSON_PATH)
|
|
WSS_CERT_PATH = CREDENTIALS_DIR / 'wss-cert.pem'
|
|
WSS_KEY_PATH = CREDENTIALS_DIR / 'wss-key.pem'
|
|
|
|
|
|
# 保存所有连接的 websocket 客户端;所有访问都要拿 ws_clients_lock
|
|
ws_clients: set[ServerConnection] = set()
|
|
ws_clients_lock = threading.Lock()
|
|
# 串行化 notify_clients,避免 notify_daemon 与 watchdog 事件并发推送
|
|
notify_lock = threading.Lock()
|
|
|
|
# 处理 websocket 连接
|
|
def connect_handler(client: ServerConnection):
|
|
with ws_clients_lock:
|
|
ws_clients.add(client)
|
|
size = len(ws_clients)
|
|
logger.debug(f"当前连接客户端数: {size}")
|
|
try:
|
|
for message in client:
|
|
client.send(message)
|
|
except Exception as e:
|
|
logger.debug(f"ws client exited: {e}")
|
|
finally:
|
|
with ws_clients_lock:
|
|
ws_clients.discard(client)
|
|
size = len(ws_clients)
|
|
logger.debug(f"当前连接客户端数: {size}")
|
|
|
|
|
|
# 每5s通知一次
|
|
def notify_daemon():
|
|
while True:
|
|
time.sleep(5)
|
|
notify_clients()
|
|
with ws_clients_lock:
|
|
size = len(ws_clients)
|
|
print(f'clients:{size}')
|
|
|
|
|
|
# 通知所有客户端最新的 Credentials 数据
|
|
def notify_clients():
|
|
with notify_lock:
|
|
try:
|
|
with open(CREDENTIALS_JSON_FILE, 'r', encoding="utf-8") as file:
|
|
data = file.read()
|
|
except Exception as e:
|
|
logger.error(f"Error reading file: {e}")
|
|
return
|
|
|
|
if len(data) == 0:
|
|
return
|
|
|
|
try:
|
|
json_data = json.loads(data)
|
|
except json.JSONDecodeError as e:
|
|
# 写入侧非原子时可能读到半截 JSON;下一次文件变更会再触发,吞掉即可
|
|
logger.debug(f"credentials.json 解析失败(可能正被写入): {e}")
|
|
return
|
|
|
|
ts = int((datetime.now() - timedelta(minutes=30)).timestamp() * 1000)
|
|
valid_data = [x for x in json_data if x['timestamp'] > ts]
|
|
result = json.dumps(valid_data, indent=4)
|
|
print(f'credentials:{len(valid_data)}')
|
|
|
|
# 先复制快照再迭代;send 失败的客户端集中收集后再统一移除
|
|
with ws_clients_lock:
|
|
snapshot = list(ws_clients)
|
|
dead = []
|
|
for ws_client in snapshot:
|
|
try:
|
|
ws_client.send(result)
|
|
except Exception:
|
|
dead.append(ws_client)
|
|
if dead:
|
|
with ws_clients_lock:
|
|
for c in dead:
|
|
ws_clients.discard(c)
|
|
|
|
|
|
class CredentialsFileHandler(FileSystemEventHandler):
|
|
def __init__(self, filename):
|
|
self.filename = filename
|
|
logger.debug(f"开始监控文件: {filename}")
|
|
|
|
def on_modified(self, event):
|
|
logger.debug(f"on_modified: {event}")
|
|
if event.src_path == self.filename:
|
|
notify_clients()
|
|
|
|
|
|
def watcher_process(port: str, output_queue: multiprocessing.Queue):
|
|
sys.stdout = sys.stderr = utils.Capture(output_queue)
|
|
|
|
Path(CREDENTIALS_JSON_FILE).parent.mkdir(parents=True, exist_ok=True)
|
|
Path(CREDENTIALS_JSON_FILE).touch()
|
|
|
|
|
|
event_handler = CredentialsFileHandler(CREDENTIALS_JSON_FILE)
|
|
observer = Observer()
|
|
observer.schedule(event_handler, str(CREDENTIALS_DIR), recursive=True)
|
|
|
|
# 用本机 mitmproxy CA 签发 wss 叶子证书;前端是 https 必须走 wss
|
|
wss_paths = cert.ensure_wss_cert(WSS_CERT_PATH, WSS_KEY_PATH)
|
|
if wss_paths is None:
|
|
# mitmproxy CA 还没生成;用户稍后看到"未检测到证书"提示后装证书时 mitmproxy 才会创建 CA
|
|
print("wss 证书启动失败: 未找到 ~/.mitmproxy/mitmproxy-ca.pem")
|
|
logger.error("wss 证书无法生成:~/.mitmproxy/mitmproxy-ca.pem 不存在")
|
|
return
|
|
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
ssl_ctx.load_cert_chain(certfile=wss_paths[0], keyfile=wss_paths[1])
|
|
|
|
try:
|
|
observer.start()
|
|
|
|
# 启动 websocket 服务
|
|
logger.info(f"开始启动 websocket 服务")
|
|
threading.Thread(target=notify_daemon, daemon=True).start()
|
|
|
|
with serve(connect_handler, "localhost", int(port), ssl=ssl_ctx) as server:
|
|
port = server.socket.getsockname()[1]
|
|
print(f"服务启动成功:{port}")
|
|
logger.info(f"websocket 端口: {port}")
|
|
logger.info(f"websocket 服务启动完毕")
|
|
server.serve_forever()
|
|
except OSError as e:
|
|
# 端口占用等系统错误:务必写到 stdout,父进程才能从 queue 拿到并快速报错
|
|
print(f"watcher启动失败: {e}")
|
|
logger.error(f"watcher启动失败: {e}")
|
|
except Exception as e:
|
|
print(f"watcher启动失败: {e}")
|
|
logger.error(f"watcher启动失败: {e}")
|
|
finally:
|
|
observer.stop()
|
|
observer.join()
|
|
logger.info(f"watcher process terminated")
|
|
|
|
|
|
def start(port: str):
|
|
watcher_output_queue = multiprocessing.Queue()
|
|
process = multiprocessing.Process(target=watcher_process, args=(port, watcher_output_queue,), daemon=True)
|
|
process.start()
|
|
|
|
start_time = time.time()
|
|
ws_address = None
|
|
error_line = None
|
|
|
|
while time.time() - start_time < 10:
|
|
try:
|
|
line = watcher_output_queue.get(timeout=0.1)
|
|
if "服务启动成功" in line:
|
|
match = re.search(r':(\d+)', line)
|
|
port = match.group(1)
|
|
ws_address = f"wss://127.0.0.1:{port}"
|
|
break
|
|
elif ("address already in use" in line.lower()
|
|
or "watcher启动失败" in line
|
|
or "wss 证书启动失败" in line):
|
|
error_line = line
|
|
break
|
|
except queue.Empty:
|
|
pass
|
|
|
|
return ws_address, watcher_output_queue, error_line
|