From 15d01d4dc18349b6ca60bcfb4ce6d186ca9fe26f Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 22 Oct 2026 21:39:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=B5=8B=E8=AF=95=E5=B7=A5=E5=85=B7):=20FT?= =?UTF-8?q?P=20=E8=84=9A=E6=9C=AC=E4=B8=8E=20@test=20=E6=8A=93=E5=8F=96?= =?UTF-8?q?=E6=94=B9=E7=94=A8=E8=AE=BE=E5=A4=87=E9=89=B4=E6=9D=83=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=EF=BC=88=E4=BF=AE=2011=20=E5=A4=A9=E5=81=87=E7=BB=BF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设备 FTP 自鉴权落地起就**没有匿名通道**(实测 `USER anonymous` → 530、`admin`+错口令 → 530、`admin`/`admin` → 230;`lftpd` 源码里也没有 anonymous 特判),但三个老主机脚本、 `@test` 的自检记录抓取、以及测试指南都还写着匿名——同目录的 `ftp_auth_bench.py` / `ftp_abort_probe.py` 反而早就记着"匿名一律 530",可见当时只改了新增件。 **改动**: · `test/host/ftp_test.py`、`ftp_up_dl_test.py`、`ftp_speed_test.py`:写死的 `USER anonymous` / `PASS test@test.com` 改为 `--user` / `--password`(默认出厂 admin/admin);**USER 的期望码从 230 修成"230 或 331"**——要口令的服务器在 USER 后 回 331,脚本原先要求 USER 直接给 230,这是第二个 bug(就算口令对也过不了);登录 失败时打印口径提示(`/config.ini` 的 `[ftp]`、无匿名通道、连续 5 次失败锁 60 秒)。 · `.dsh/skills/serial-boot-test/scripts/run_boot_test.py`:`ftp.login()`(匿名)→ `ftp.login(user, pass)`,新增 `--ftp-user/--ftp-pass` 与 config 的 `ftp_user`/ `ftp_pass`(默认 admin);函数文档与 `--device-ip` 的 help 去掉 "(anonymous)"。 · `docs/测试说明/FTP_Test_Guide.md`:§1 的"匿名登录(`USER anonymous` / `PASS` 任意)" 改为鉴权口径(`user` / `pwd_hash` = SHA-256(user‖password) / `write`,出厂 admin/admin,默认口令在用有启动告警,5 次失败锁 60 秒,**无匿名通道**),§4 参数说明 补两个新参数,§4.1 期望改为 `USER admin`→331、`PASS admin`→230,FileZilla 的用户 由 `anonymous` 改 `admin`。 **这才是真正的危害**:`@test --require-selftest-pass` 的抓取走匿名登录 → 必然 530 → 落 进 `RUN_TEST_WARN … treated as no record (S7 PASS path)` → **每次都判 PASS**。也就是 说这条"联合判定自检记录"的门禁从写下那天起就没真正看过文件(假绿)。修完凭据后第一次 真抓到 `/log/selftest.log`(1856 B),立刻报出 29 条故障记录——而它们的**时间戳全是 2026-10-10 ~ 10-14**,没有一条来自本次启动:`/log/selftest.log` 是 512 条环形累计历史, 而 S7 的判据是"有任何 64B 记录就 FAIL" → **在任何做过故障注入或长跑的设备上,这条 门禁都不可能 PASS**。判据本身要改成"只看本次启动新增",已单独提出,不在本笔。 **验证(全部真机实跑)**:`ftp_up_dl_test.py` → TEST PASSED(10 项全过,含 STOR/LIST/ RETR/逐字节比对/DELE);`ftp_speed_test.py --size 128` → TEST PASSED(上行 225.8 KB/s、 下行 250.9 KB/s,131,072 B 逐字节一致);`@test` → PASS(625 ms 到 `net init: OK`); `@test --require-selftest-pass --device-ip 192.168.1.100` → `SELFTEST_FETCH: … (1856 bytes)`,抓取路径真跑通(旧代码在此步必 530)。 `py_compile` 四个脚本全过;`check_doc_links.py --all --sections-strict` 146 文档 0 问题; `check_ids.py --strict` 增量拦截通过;5 个文件字节卫生 BOM 无 / CR 0 / U+FFFD 0 / 无行尾空格。 **顺带暴露、未在本笔处理**:① `ftp_test.py` 的 `LIST 150->226` 断言失败——lftpd 的 LIST 数据本身正确(1512 B 全收到),但传输结束后控制通道回的是 **550** 而不是 226,需单独查 收尾路径;② 设备 `/` 根目录残留约 24 个旧探针文件(AB*/E12A/SZ*/A000x/R000x/DIAG_A); ③ `MDK-ARM/` 下 4 个一次性探针脚本仍写匿名,现在必然登录不上。未推送。 --- .../serial-boot-test/scripts/run_boot_test.py | 19 ++++++++--- docs/测试说明/FTP_Test_Guide.md | 12 ++++--- test/host/ftp_speed_test.py | 25 +++++++++++---- test/host/ftp_test.py | 20 +++++++++--- test/host/ftp_up_dl_test.py | 32 ++++++++++++++----- 5 files changed, 80 insertions(+), 28 deletions(-) diff --git a/.dsh/skills/serial-boot-test/scripts/run_boot_test.py b/.dsh/skills/serial-boot-test/scripts/run_boot_test.py index 48aac463..bd85c0d6 100644 --- a/.dsh/skills/serial-boot-test/scripts/run_boot_test.py +++ b/.dsh/skills/serial-boot-test/scripts/run_boot_test.py @@ -240,8 +240,8 @@ def _prune_out_dir(out_dir, keep): return removed -def _fetch_selftest_log(device_ip, out_dir, stamp): - """从设备 FTP 抓取 /log/selftest.log(lftpd :21 匿名,根目录 = 卷根)。 +def _fetch_selftest_log(device_ip, out_dir, stamp, ftp_user="admin", ftp_pass="admin"): + """从设备 FTP 抓取 /log/selftest.log(lftpd :21 需鉴权,根目录 = 卷根)。 成功返回本地路径字符串;失败(网络不可达 / FTP 错误 / 设备侧文件缺失)返回 "" (按 S7 语义视为无故障记录;此处打 WARN,调用方不判整体 FAIL)。 @@ -256,7 +256,9 @@ def _fetch_selftest_log(device_ip, out_dir, stamp): ftp = ftplib.FTP() try: ftp.connect(device_ip, 21, timeout=10) - ftp.login() # 匿名(lftpd 接受 USER anonymous / 空 PASS) + # lftpd 按 /config.ini 的 [ftp] 校验账号(user / pwd_hash),出厂 admin/admin; + # 设备没有匿名通道——匿名登录一律 530,所以这里必须带凭据。 + ftp.login(ftp_user, ftp_pass) except (OSError, ftplib.Error) as e: print("RUN_TEST_WARN: selftest fetch failed: {} " "(按 S7 语义视为无记录;设备网络/FTP 需另行排查)".format(e)) @@ -315,7 +317,11 @@ def main(argv=None): ap.add_argument("--selftest-log", default=None, help="local copy of /log/selftest.log (skips device-side fetch when given)") ap.add_argument("--device-ip", default=None, - help="device IP for fetching /log/selftest.log via FTP (anonymous)") + help="device IP for fetching /log/selftest.log via FTP") + ap.add_argument("--ftp-user", default=None, + help="FTP user for the selftest fetch (config 'ftp_user'; default admin)") + ap.add_argument("--ftp-pass", default=None, + help="FTP password for the selftest fetch (config 'ftp_pass'; default admin)") ap.add_argument("--out-dir", default=None) ap.add_argument("--show-tail-lines", type=int, default=None) ap.add_argument("--fw-hex", default=None, @@ -388,6 +394,8 @@ def main(argv=None): "require_selftest_pass", default=False)) args.selftest_log = str(_pick(args.selftest_log, "selftest_log", default="") or "") args.device_ip = str(_pick(args.device_ip, "device_ip", default="") or "") + args.ftp_user = str(_pick(args.ftp_user, "ftp_user", default="admin") or "admin") + args.ftp_pass = str(_pick(args.ftp_pass, "ftp_pass", default="admin") or "admin") args.out_dir = str(_pick(args.out_dir, "out_dir", default=str(Path(tempfile.gettempdir()) / "serial-boot-test"))) args.show_tail_lines = int(_pick(args.show_tail_lines, "show_tail_lines", default=15)) @@ -596,7 +604,8 @@ def main(argv=None): print("RUN_TEST_WARN: require_selftest_pass but no device_ip/selftest_log " "-> treated as no record (S7 PASS path)") if args.require_selftest_pass and not selftest_local and args.device_ip: - selftest_local = _fetch_selftest_log(args.device_ip, out_dir, stamp) + selftest_local = _fetch_selftest_log(args.device_ip, out_dir, stamp, + args.ftp_user, args.ftp_pass) # 6. 标记校验(含固件身份戳、启动锚点计数、可选的自检记录联合判定) # 启动锚点(config "boot_marker",如本仓库的 'reset cause:')未配置 = diff --git a/docs/测试说明/FTP_Test_Guide.md b/docs/测试说明/FTP_Test_Guide.md index d705598d..d839b0ad 100644 --- a/docs/测试说明/FTP_Test_Guide.md +++ b/docs/测试说明/FTP_Test_Guide.md @@ -9,7 +9,10 @@ ## 1. 概述 - FTP Server(`lftpd`)在**默认固件构建中即常驻运行**:`Src/freertos.c` 的 `StartFtpTask`(默认 `__weak` 实现)直接调用 `lftpd_start("/", 21, &ftp)`,与测试套件选择无关。 -- 监听端口 **21**,根目录 `/`(即 NAND 上的 FatFS 卷),**匿名登录**(`USER anonymous` / `PASS` 任意)。 +- 监听端口 **21**,根目录 `/`(即 NAND 上的 FatFS 卷)。**需要账号鉴权,设备没有匿名通道**:账号取 + `/config.ini` 的 `[ftp]` 段(`user` / `pwd_hash` = SHA-256(user‖password) / `write`),出厂默认 + **admin/admin**(用默认口令时启动日志有告警);**连续 5 次失败锁定 60 秒**。完整口径见 + `../子系统设计/网络配置说明.md`。 - 仅支持 **1 个并发客户端**(控制通道 Socket0 + PASV 数据通道动态 Socket2,backlog=1)。 - 测试分两类: - **PC 端手测(推荐,覆盖真实 FTP 行为)**:用 `test/` 下 Python 脚本或 GUI 客户端。 @@ -48,7 +51,8 @@ ## 4. PC 端脚本手测(主要手段) -所有脚本支持 `--host`(默认 `192.168.1.100`)、`--port`(默认 21),在 `test/` 目录运行;`ftp_up_dl_test.py` / `ftp_speed_test.py` 内置**连接重试**(单客户端:旧会话未释放时自动等待重试,默认 6 次 × 3s)。 +所有脚本支持 `--host`(默认 `192.168.1.100`)、`--port`(默认 21)、`--user` / `--password` +(默认出厂账号 `admin` / `admin`;设备口令改过就要显式传,**没有匿名通道**),在 `test/` 目录运行;`ftp_up_dl_test.py` / `ftp_speed_test.py` 内置**连接重试**(单客户端:旧会话未释放时自动等待重试,默认 6 次 × 3s)。 推荐入口:`& '.venv\Scripts\python.exe' test/host/ftp_up_dl_test.py`(功能闭环:登录/TYPE I/PASV/STOR/LIST/RETR/比对/DELE/QUIT)与 `& '.venv\Scripts\python.exe' test/host/ftp_speed_test.py --size 128`(大文件 + 吞吐);后者同时是 **ftpTask 栈水位测量**的负载(见 `docs/内存管理/RAM使用分析与优化方案.md` §4.0.6)。 ### 4.1 冒烟测试:`ftp_test.py` @@ -57,7 +61,7 @@ - 命令:`python ./test/host/ftp_test.py --host 192.168.1.100` - 预期: - 连接成功并读到 `220` 欢迎; - - `USER anonymous` / `PASS` 返回 `230`; + - `USER admin` 返回 `331`(需要口令)、`PASS admin` 返回 `230`; - `PASV` 返回 `227` 并解析出数据端口(或回退 `EPSV`); - `LIST` 返回目录列表; - `QUIT` 正常关闭。 @@ -88,7 +92,7 @@ ## 5. GUI 客户端手测(可选) -- **FileZilla**:主机 `192.168.1.100`,端口 21,用户 `anonymous`,密码任意;可拖拽上传/下载、浏览目录。 +- **FileZilla**:主机 `192.168.1.100`,端口 21,用户 `admin`、密码 `admin`(**匿名会被 530 拒**);可拖拽上传/下载、浏览目录。 - 要点:使用二进制模式传输;文件名遵守 8.3;**一次只连一个客户端**。 --- diff --git a/test/host/ftp_speed_test.py b/test/host/ftp_speed_test.py index 587cfea2..2a5971e2 100644 --- a/test/host/ftp_speed_test.py +++ b/test/host/ftp_speed_test.py @@ -2,6 +2,7 @@ """ FTP 速度测试 — 上传/下载大文件,测量传输速度 用法:python ftp_speed_test.py --host 192.168.1.100 [--size 512] [--port 21] + [--user admin] [--password admin] """ import socket @@ -13,6 +14,10 @@ import os HOST = "192.168.1.100" PORT = 21 +# 设备侧 FTP 鉴权走 /config.ini 的 [ftp](user / pwd_hash),出厂默认 admin/admin; +# **没有匿名通道**(USER anonymous 一律回 530)。 +USER = "admin" +PASSWORD = "admin" FILE_SIZE_KB = 512 # 默认 512KB 测试文件 def log(msg, *args): @@ -71,7 +76,7 @@ def parse_pasv(resp): return f"{a}.{b}.{c}.{d}", e * 256 + f return None, None -def test_speed(host, port, size_kb): +def test_speed(host, port, size_kb, user, password): total_bytes = size_kb * 1024 test_data = os.urandom(total_bytes) test_name = "SPEEDTST.DAT" # 8.3 format @@ -113,15 +118,20 @@ def test_speed(host, port, size_kb): check("Connect", False, str(err)) return finish(None) recv_line(sock) # 220 - resp = send_cmd(sock, "USER anonymous") - if not (resp and resp.startswith(b"230")): - check("Login USER 230", False, + # 需要口令的服务器在 USER 之后回 331,直接放行的才回 230——两种都要接受 + resp = send_cmd(sock, "USER " + user) + if not (resp and (resp.startswith(b"230") or resp.startswith(b"331"))): + check("Login USER (230/331)", False, resp.decode(errors='replace').rstrip() if resp else "no resp") + print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;" + "设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。") return finish(sock) - resp = send_cmd(sock, "PASS test@test.com") + resp = send_cmd(sock, "PASS " + password) if not (resp and resp.startswith(b"230")): check("Login PASS 230", False, resp.decode(errors='replace').rstrip() if resp else "no resp") + print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;" + "设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。") return finish(sock) check("Login (USER/PASS 230)", True) send_cmd(sock, "TYPE I") @@ -250,8 +260,11 @@ def main(): parser.add_argument("--port", type=int, default=PORT) parser.add_argument("--size", type=int, default=FILE_SIZE_KB, help=f"File size in KB (default {FILE_SIZE_KB})") + parser.add_argument("--user", default=USER, help=f"FTP user (default: {USER})") + parser.add_argument("--password", default=PASSWORD, + help="FTP password (default: the factory password)") args = parser.parse_args() - success = test_speed(args.host, args.port, args.size) + success = test_speed(args.host, args.port, args.size, args.user, args.password) sys.exit(0 if success else 1) if __name__ == "__main__": diff --git a/test/host/ftp_test.py b/test/host/ftp_test.py index f3b6eef1..b55df586 100644 --- a/test/host/ftp_test.py +++ b/test/host/ftp_test.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ FTP Server 测试脚本 — 连接 STM32F4 上的 lftpd 服务器 -用法:python ftp_test.py [--host 192.168.1.100] [--port 21] +用法:python ftp_test.py [--host 192.168.1.100] [--port 21] [--user admin] [--password admin] """ import socket @@ -12,6 +12,10 @@ import argparse HOST = "192.168.1.100" PORT = 21 +# 设备侧 FTP 鉴权走 /config.ini 的 [ftp](user / pwd_hash),出厂默认 admin/admin; +# **没有匿名通道**(USER anonymous 一律回 530)。 +USER = "admin" +PASSWORD = "admin" TIMEOUT = 10.0 def recv_line(sock): @@ -53,7 +57,7 @@ def parse_pasv(response): return ip, port return None, None -def test_ftp(host, port): +def test_ftp(host, port, user, password): print(f"=== FTP Test: {host}:{port} ===\n") checks = [] # (label, ok, detail) @@ -90,13 +94,13 @@ def test_ftp(host, port): # 3. Login print("\n[3] Login...") - resp = send_cmd(sock, "USER anonymous") + resp = send_cmd(sock, "USER " + user) if not resp: sock.close() check("Login USER", False, "no response") return False - resp = send_cmd(sock, "PASS test@test.com") + resp = send_cmd(sock, "PASS " + password) if not resp: sock.close() check("Login PASS", False, "no response") @@ -104,6 +108,9 @@ def test_ftp(host, port): login_ok = b"230" in resp check("Login (USER/PASS 230)", login_ok, resp.decode(errors='replace').rstrip()) + if not login_ok: + print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;" + "设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。") # 4. PWD print("\n[4] PWD...") @@ -199,9 +206,12 @@ def main(): parser = argparse.ArgumentParser(description="FTP Server Test") parser.add_argument("--host", default=HOST, help=f"FTP server IP (default: {HOST})") parser.add_argument("--port", type=int, default=PORT, help=f"FTP server port (default: {PORT})") + parser.add_argument("--user", default=USER, help=f"FTP user (default: {USER})") + parser.add_argument("--password", default=PASSWORD, + help="FTP password (default: the factory password)") args = parser.parse_args() - success = test_ftp(args.host, args.port) + success = test_ftp(args.host, args.port, args.user, args.password) sys.exit(0 if success else 1) if __name__ == "__main__": diff --git a/test/host/ftp_up_dl_test.py b/test/host/ftp_up_dl_test.py index 0dd5741f..8fbc2f2f 100644 --- a/test/host/ftp_up_dl_test.py +++ b/test/host/ftp_up_dl_test.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ FTP 上传+下载测试脚本 -用法:python ftp_up_dl_test.py --host 192.168.1.100 +用法:python ftp_up_dl_test.py --host 192.168.1.100 [--user admin] [--password admin] """ import socket @@ -14,6 +14,10 @@ import os HOST = "192.168.1.100" PORT = 21 +# 设备侧 FTP 鉴权走 /config.ini 的 [ftp](user / pwd_hash = SHA-256(user‖password)), +# 出厂默认 admin/admin;**没有匿名通道**(USER anonymous 一律回 530)。 +USER = "admin" +PASSWORD = "admin" TIMEOUT = 15.0 TEST_DATA = b"Hello from Python FTP test!\r\nThis is line 2.\r\nLine 3: " + b"ABCDEFGH" * 50 + b"\r\n" @@ -83,8 +87,8 @@ def parse_pasv(resp): return f"{a}.{b}.{c}.{d}", e * 256 + f return None, None -def test_upload_download(host, port): - print(f"=== FTP Upload/Download Test: {host}:{port} ===\n") +def test_upload_download(host, port, user, password): + print(f"=== FTP Upload/Download Test: {host}:{port} user={user} ===\n") checks = [] def check(label, ok, detail=""): @@ -120,11 +124,20 @@ def test_upload_download(host, port): return finish(None) recv_line(sock) # 220 - if not send_cmd(sock, "USER anonymous", 230): - check("Login USER 230", False) + # 需要口令的服务器在 USER 之后回 331,直接放行的才回 230——两种都要接受 + resp = send_cmd(sock, "USER " + user) + if not (resp and (resp.startswith(b"230") or resp.startswith(b"331"))): + check("Login USER (230/331)", False, + resp.decode(errors="replace").rstrip() if resp else "no resp") + print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;" + "设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。") return finish(sock) - if not send_cmd(sock, "PASS test@test.com", 230): - check("Login PASS 230", False) + resp = send_cmd(sock, "PASS " + password) + if not (resp and resp.startswith(b"230")): + check("Login PASS 230", False, + resp.decode(errors="replace").rstrip() if resp else "no resp") + print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;" + "设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。") return finish(sock) check("Login (USER/PASS 230)", True) @@ -271,8 +284,11 @@ def main(): parser = argparse.ArgumentParser(description="FTP Upload/Download Test") parser.add_argument("--host", default=HOST) parser.add_argument("--port", type=int, default=PORT) + parser.add_argument("--user", default=USER, help=f"FTP user (default: {USER})") + parser.add_argument("--password", default=PASSWORD, + help="FTP password (default: the factory password)") args = parser.parse_args() - success = test_upload_download(args.host, args.port) + success = test_upload_download(args.host, args.port, args.user, args.password) sys.exit(0 if success else 1) if __name__ == "__main__":