# -*- coding: utf-8 -*- """2.3.0 新命令的单元测试:media_list / media_pull / kill_all。 重点验**安全边界**,不是"能跑就行": * `media_pull` 必须拒绝媒体目录之外的文件(否则就是个远程任意文件读取器) * 同前缀的兄弟目录(`音乐-evil`)不能被误判成子目录 * `kill_all` 必须保护自己、父进程链、系统关键进程 """ from __future__ import annotations import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "windows")) passed = 0 failed = 0 def check(name, ok, detail=""): global passed, failed if ok: passed += 1 print(" [PASS] " + name) else: failed += 1 print(" [FAIL] " + name + ((" —— " + str(detail)) if detail else "")) return ok def main() -> int: print("=" * 62) print(" 2.3.0 新命令测试") print("=" * 62) # ---------- 1. 静态检查:实现存在且关键点齐 ----��------ print() print("一、代码存在性") server = (ROOT / "windows" / "lanremote" / "server.py").read_text( encoding="utf-8") features = (ROOT / "windows" / "lanremote" / "features.py").read_text( encoding="utf-8") check("server.py 有 _media_list", "def _media_list(self)" in server) check("server.py 有 _media_pull", "def _media_pull(self" in server) check("路由 media_list", 'cmd == "media_list"' in server) check("路由 media_pull", 'cmd == "media_pull"' in server) check("features.py 有 _do_kill_all", "def _do_kill_all(self" in features) check("注册了 kill_all 功能", 'add("kill_all"' in features) # ---------- 2. 安全边界:只允许媒体目录 ---------- print() print("二、media_pull 的安全边界") check("用 relative_to 判断(不是字符串前缀)", "wanted.relative_to(root)" in server, "字符串前缀会被「音乐-evil」这种同前缀兄弟目录绕过") check("有「不在允许的媒体目录里」的拒绝", "不在允许的媒体目录里" in server) check("有文件大小上限(512MB)", "512 * 1024 * 1024" in server) check("拒绝空文件", "文件是空的" in server) # ---------- 3. kill_all 的保护名单 ---------- print() print("三、kill_all 的保护名单") for name in ("csrss.exe", "winlogon.exe", "lsass.exe", "svchost.exe", "explorer.exe", "python.exe", "ykxmz.exe"): check("保护 %s" % name, '"%s"' % name in features, "少了它一按就可能蓝屏/自断后路") check("保护自己的 PID", "_protected_pids" in features) check("追父进程链", "ParentProcessId" in features) # ---------- 4. 真的跑一次 _media_list(用临时目录)---------- print() print("四、_media_list 实跑") try: import os os.environ.setdefault("PYTHONIOENCODING", "utf-8") from lanremote import server as srv_mod # 造一个假的 store(不需要 Config —— 那个类不存在,之前是我写错了) tmp = Path(tempfile.mkdtemp(prefix="media_")) music = tmp / "Music" music.mkdir() (music / "a.mp3").write_bytes(b"x" * 100) (music / "b.mp4").write_bytes(b"y" * 200) (music / "c.txt").write_text("not media", encoding="utf-8") class FakeStore: def received_dir(self): return tmp # 直接借 Server 的方法:构造太麻烦,用 types 绑定 import types obj = types.SimpleNamespace() obj.store = FakeStore() obj.AUDIO_EXT = srv_mod.RemoteServer.AUDIO_EXT obj.VIDEO_EXT = srv_mod.RemoteServer.VIDEO_EXT obj.MEDIA_LIST_LIMIT = srv_mod.RemoteServer.MEDIA_LIST_LIMIT obj.MEDIA_SUBDIRS = ("Music", "Videos") obj._media_roots = types.MethodType(srv_mod.RemoteServer._media_roots, obj) # Path.home() 是真实用户目录,这里把 _media_roots 换成只返回 music。 # ⚠ 必须返回**装文件的那一层**,不是它的父目录 —— # _media_list 只扫根目录下的文件,不递归子目录。 # (第一版写成了 tmp,扫到的是 Music 这个文件夹本身,全被跳过。) obj._media_roots = lambda: [music.resolve()] result = srv_mod.RemoteServer._media_list(obj) names = [f["name"] for f in result["files"]] check("列出了 a.mp3", "a.mp3" in names) check("列出了 b.mp4", "b.mp4" in names) check("**排除了 c.txt**", "c.txt" not in names, "非媒体扩展名不该出现") kinds = {f["name"]: f["kind"] for f in result["files"]} check("a.mp3 标成 audio", kinds.get("a.mp3") == "audio") check("b.mp4 标成 video", kinds.get("b.mp4") == "video") except Exception as exc: # noqa: BLE001 check("_media_list 实跑", False, "%s: %s" % (type(exc).__name__, exc)) print() print("=" * 62) print(" 通过 %d 项,失败 %d 项" % (passed, failed)) print("=" * 62) return 0 if failed == 0 else 1 if __name__ == "__main__": sys.exit(main())