Files
STM32F4-Base/test/host/ftp_up_dl_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

296 lines
10 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 上传+下载测试脚本
用法:python ftp_up_dl_test.py --host 192.168.1.100 [--user admin] [--password admin]
"""
import socket
import sys
import time
import re
import argparse
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"
def recv_line(sock, timeout=TIMEOUT):
data = b""
sock.settimeout(timeout)
while True:
try:
ch = sock.recv(1)
except socket.timeout:
print(f" [TIMEOUT] partial: {data[:80]!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, expect_code=None):
print(f" > {cmd}")
sock.sendall((cmd + "\r\n").encode())
time.sleep(0.15)
response = recv_line(sock)
if response:
text = response.decode(errors='replace').rstrip()
print(f" < {text}")
if expect_code and not text.startswith(str(expect_code)):
print(f" *** Expected {expect_code}, got: {text}")
return None
return response
print(" < [NO RESPONSE]")
return None
def connect_with_retry(host, port, timeout=TIMEOUT, attempts=6, delay=3.0):
"""连接控制通道(带重试)。
CH395F 是单客户端复用:上一会话未 QUIT 就断开时,服务端仍在处理旧连接,
新连接会被 RST(WinError 10061)。实测旧会话约 30s 后被服务端丢弃并重新
listen,故这里等一会儿重试即可,不要用裸 TCP 探测端口(探测本身会占掉
唯一会话)。
"""
last_err = None
for i in range(1, attempts + 1):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
try:
s.connect((host, port))
return s, None
except Exception as e:
last_err = e
try:
s.close()
except Exception:
pass
if i < attempts:
print(f" connect failed ({e}); retry {i}/{attempts - 1} in {delay:.0f}s "
f"(single-client: stale session blocks new connections)")
time.sleep(delay)
return None, last_err
def parse_pasv(resp):
m = re.search(r"(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)", resp.decode())
if m:
a, b, c, d, e, f = [int(x) for x in m.groups()]
return f"{a}.{b}.{c}.{d}", e * 256 + f
return None, None
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=""):
checks.append((label, ok, detail))
mark = "PASS" if ok else "FAIL"
print(f" [{mark}] {label}{(' - ' + detail) if detail else ''}")
def finish(sock):
passed = all(ok for _, ok, _ in checks)
print("\n" + "=" * 40)
print("FTP Upload/Download 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 ===")
if sock:
try:
sock.close()
except Exception:
pass
return passed
# 1. Connect + Login
print("[1] Connect + Login")
sock, err = connect_with_retry(host, port)
if sock is None:
print(f" CONNECT FAILED: {err}")
check("Connect", False, str(err))
return finish(None)
recv_line(sock) # 220
# 需要口令的服务器在 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 " + 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)
# 2. TYPE I (binary mode)
print("\n[2] Set binary mode")
resp = send_cmd(sock, "TYPE I")
type_ok = bool(resp) and resp.startswith(b"200")
check("TYPE I 200", type_ok)
# 3. PASV + STOR (upload)
print("\n[3] Upload: STOR up_test.dat")
resp = send_cmd(sock, "PASV")
if not resp:
check("PASV", False, "no response")
return finish(sock)
pasv_ip, pasv_port = parse_pasv(resp)
print(f" PASV: {pasv_ip}:{pasv_port}")
data_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_sock.settimeout(10.0)
try:
data_sock.connect((pasv_ip, pasv_port))
print(f" Data connected.")
except Exception as e:
print(f" Data connect FAILED: {e}")
check("PASV data connect", False, str(e))
return finish(sock)
check("PASV data connect", True, f"{pasv_ip}:{pasv_port}")
send_cmd(sock, "STOR up_test.dat")
total = 0
while total < len(TEST_DATA):
try:
n = data_sock.send(TEST_DATA[total:])
if n <= 0:
break
total += n
except Exception as e:
print(f" Send error: {e}")
break
print(f" Uploaded {total}/{len(TEST_DATA)} bytes.")
data_sock.shutdown(socket.SHUT_WR)
data_sock.close()
resp = recv_line(sock)
stor_ok = bool(resp) and resp.startswith(b"226") and total == len(TEST_DATA)
check("STOR 226 + bytes", stor_ok,
f"uploaded={total}/{len(TEST_DATA)} resp={resp.decode().rstrip() if resp else 'none'}")
# 4. PASV + LIST (verify)
print("\n[4] Verify: LIST")
resp = send_cmd(sock, "PASV")
if not resp:
check("LIST PASV", False, "no response")
return finish(sock)
pasv_ip, pasv_port = parse_pasv(resp)
data_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_sock.settimeout(10.0)
data_sock.connect((pasv_ip, pasv_port))
send_cmd(sock, "LIST")
data = b""
while True:
try:
chunk = data_sock.recv(4096)
if not chunk:
break
data += chunk
except socket.timeout:
break
print(f" LIST: {data.decode(errors='replace').rstrip() or '(empty directory)'}")
data_sock.close()
resp = recv_line(sock)
list_ok = bool(data) and b"UP_TEST.DAT" in data.upper()
check("LIST shows UP_TEST.DAT", list_ok,
resp.decode().rstrip() if resp else "no resp")
# 5. PASV + RETR (download)
time.sleep(1.0)
print("\n[5] Download: RETR up_test.dat")
resp = send_cmd(sock, "PASV")
if not resp:
check("RETR PASV", False, "no response")
return finish(sock)
pasv_ip, pasv_port = parse_pasv(resp)
data_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_sock.settimeout(10.0)
try:
data_sock.connect((pasv_ip, pasv_port))
except Exception as e:
print(f" Data connect FAILED: {e}")
check("RETR data connect", False, str(e))
return finish(sock)
check("RETR data connect", True, f"{pasv_ip}:{pasv_port}")
send_cmd(sock, "RETR up_test.dat")
dl_data = b""
while True:
try:
chunk = data_sock.recv(4096)
if not chunk:
break
dl_data += chunk
except socket.timeout:
break
data_sock.close()
resp = recv_line(sock)
retr_ok = bool(resp) and resp.startswith(b"226")
check("RETR 226", retr_ok, resp.decode().rstrip() if resp else "no resp")
# Compare
print(f"\n[6] Compare")
print(f" Uploaded: {len(TEST_DATA)} bytes")
print(f" Downloaded: {len(dl_data)} bytes")
match = (dl_data == TEST_DATA)
if match:
print(f" *** MATCH! Upload/Download success. ***")
else:
print(f" *** MISMATCH! ***")
if dl_data:
print(f" First 80 bytes uploaded: {TEST_DATA[:80]!r}")
print(f" First 80 bytes downloaded: {dl_data[:80]!r}")
check("Upload/Download MATCH", match, f"up={len(TEST_DATA)} dl={len(dl_data)}")
# 7. DELE (delete test file)
print("\n[7] Cleanup: DELE up_test.dat")
resp = send_cmd(sock, "DELE up_test.dat")
dele_ok = bool(resp) and resp.startswith(b"250")
check("DELE 250", dele_ok, resp.decode().rstrip() if resp else "no resp")
# 8. QUIT
print("\n[8] QUIT")
resp = send_cmd(sock, "QUIT")
quit_ok = bool(resp) and resp.startswith(b"221")
check("QUIT 221", quit_ok, resp.decode().rstrip() if resp else "no resp")
return finish(sock)
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, args.user, args.password)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()