Files
STM32F4-Base/test/host/ftp_test.py
T
wandering1 15d01d4dc1 fix(测试工具): FTP 脚本与 @test 抓取改用设备鉴权账号(修 11 天假绿)
设备 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 个一次性探针脚本仍写匿名,现在必然登录不上。未推送。
2026-10-22 21:39:25 +08:00

219 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
FTP Server 测试脚本 — 连接 STM32F4 上的 lftpd 服务器
用法:python ftp_test.py [--host 192.168.1.100] [--port 21] [--user admin] [--password admin]
"""
import socket
import sys
import time
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):
"""读取 FTP 响应行(以 \r\n 结尾)"""
data = b""
sock.settimeout(TIMEOUT)
while True:
try:
ch = sock.recv(1)
except socket.timeout:
print(f"[TIMEOUT] after receiving: {data!r}")
return None
if not ch:
return data if data else None
data += ch
if data.endswith(b"\r\n"):
return data
def send_cmd(sock, cmd):
"""发送 FTP 命令并读取响应"""
print(f">>> {cmd}")
sock.sendall((cmd + "\r\n").encode())
time.sleep(0.1)
response = recv_line(sock)
if response:
print(f"<<< {response.decode(errors='replace').rstrip()}")
else:
print(f"<<< [NO RESPONSE / CONNECTION CLOSED]")
return response
def parse_pasv(response):
"""解析 PASV 227 响应,返回 (ip, port)"""
import re
m = re.search(r"(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)", response.decode())
if m:
a, b, c, d, e, f = [int(x) for x in m.groups()]
ip = f"{a}.{b}.{c}.{d}"
port = e * 256 + f
return ip, port
return None, None
def test_ftp(host, port, user, password):
print(f"=== FTP Test: {host}:{port} ===\n")
checks = [] # (label, ok, detail)
def check(label, ok, detail=""):
checks.append((label, ok, detail))
mark = "PASS" if ok else "FAIL"
print(f" [{mark}] {label}{(' - ' + detail) if detail else ''}")
# 1. Connect
print("[1] Connecting to control channel...")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(TIMEOUT)
try:
sock.connect((host, port))
print(f" Connected to {host}:{port}")
check("Connect", True)
except Exception as e:
print(f" CONNECT FAILED: {e}")
check("Connect", False, str(e))
return False
# 2. Read welcome
print("\n[2] Reading welcome banner...")
resp = recv_line(sock)
if resp:
print(f" {resp.decode(errors='replace').rstrip()}")
check("Welcome 220", resp.startswith(b"220"),
resp.decode(errors='replace').rstrip())
else:
print(" NO WELCOME BANNER")
check("Welcome 220", False, "no banner")
sock.close()
return False
# 3. Login
print("\n[3] Login...")
resp = send_cmd(sock, "USER " + user)
if not resp:
sock.close()
check("Login USER", False, "no response")
return False
resp = send_cmd(sock, "PASS " + password)
if not resp:
sock.close()
check("Login PASS", False, "no response")
return False
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...")
resp = send_cmd(sock, "PWD")
pwd_ok = bool(resp) and resp.startswith(b"257")
check("PWD 257", pwd_ok,
resp.decode(errors='replace').rstrip() if resp else "no response")
# 5. PASV mode
print("\n[5] PASV...")
resp = send_cmd(sock, "PASV")
pasv_ip, pasv_port = (None, None)
if resp:
pasv_ip, pasv_port = parse_pasv(resp)
if pasv_port is None and resp and b"227" in resp:
# 227 响应但解析端口失败
pasv_port = None
if pasv_port is None:
# Try EPSV
print("\n[5b] Trying EPSV...")
resp = send_cmd(sock, "EPSV")
if resp and b"229" in resp:
import re
m = re.search(r"\(\|\|\|(\d+)\|\)", resp.decode())
if m:
pasv_port = int(m.group(1))
pasv_ip = host
print(f" EPSV: {pasv_ip}:{pasv_port}")
pasv_ok = pasv_port is not None
check("PASV/EPSV data port", pasv_ok,
f"{pasv_ip}:{pasv_port}" if pasv_ok else "no port")
# 6. LIST (correct PASV flow: connect data first, then send LIST)
if pasv_ok:
print(f" Data channel: {pasv_ip}:{pasv_port}")
print("\n[6] LIST...")
data_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_sock.settimeout(5.0)
try:
data_sock.connect((pasv_ip, pasv_port))
print(f" Data connected to {pasv_ip}:{pasv_port}")
resp = send_cmd(sock, "LIST")
list_open = bool(resp) and resp.startswith(b"150")
data = b""
while True:
try:
chunk = data_sock.recv(1024)
if not chunk:
break
data += chunk
except socket.timeout:
break
print(f" LIST result ({len(data)} bytes):")
print(data.decode(errors='replace'))
data_sock.close()
resp = recv_line(sock)
list_close = bool(resp) and resp.startswith(b"226")
print(f" LIST response: "
f"{resp.decode(errors='replace').rstrip() if resp else 'none'}")
list_ok = list_open and list_close
check("LIST 150->226", list_ok,
f"open={list_open} close={list_close} bytes={len(data)}")
except Exception as e:
print(f" LIST FAILED: {e}")
data_sock.close()
check("LIST 150->226", False, str(e))
else:
print(" Skipping LIST (no data port)")
# 7. QUIT
print("\n[7] QUIT...")
resp = send_cmd(sock, "QUIT")
quit_ok = bool(resp) and resp.startswith(b"221")
check("QUIT 221", quit_ok,
resp.decode(errors='replace').rstrip() if resp else "no response")
sock.close()
# Summary
passed = all(ok for _, ok, _ in checks)
print("\n" + "=" * 40)
print("FTP Test Summary:")
for label, ok, _ in checks:
print(f" [{'PASS' if ok else 'FAIL'}] {label}")
print("=" * 40)
if passed:
print(">>> TEST PASSED <<<")
else:
print(">>> TEST FAILED <<<")
print("=== Test Complete ===")
return passed
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, args.user, args.password)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()