Files
STM32F4-Base/test/host/lftpd/lftpd_test.c
T
wandering1 a0c261b5ee test(NET/lftpd): lftpd 控制通道模块(35 条),FTP 一套四块全部进分母
新增 test/host/lftpd/,lftpd.c 覆盖 304/473 = 64.3%。做法是把生产源整个 #include 进
TU,于是命令表 commands[]、17 个 static cmd_* 与 handle_control_channel 都能直接驱动:
假 inet 层给"脚本化命令行队列 + 响应记录",假 io 层给"极简内存视图 + 可注入失败",
认证与配置当可注入黑盒(认证本身有 test/host/ftp_auth 的 10 条专测,不重复验)。

钉住的契约(都是真机上贵、却难按需复现的那几类):
- STOR 传到一半写失败必须 **unlink 半成品**:否则 Flash 上永久留个坏文件,现场表现
  是"升级包传坏了再也传不上去"。真机要卡准"传一半掐线"的时机,这里注一个短写就行。
- 数据通道是单连接 socket:LIST/RETR/STOR 用完必须关监听并把 data_socket 复位成 -1;
  accept 失败要报 425 不留残口,会话退出(QUIT 或对端断线)时 cleanup 也要关掉还没
  用的被动监听。漏一次就是下一条命令拿着上一个号继续 accept,偶发连不上。
- 命令名缓冲只有 command_tmp[4+1],前置判断 if (index >= 5) 就是防 memcpy 越界;
  恰好 5 个字符必须被拒(改一行这条就响)。
- 认证前置白名单:未登录只放行 USER/PASS/QUIT/FEAT/SYST/NOOP,其余一律 530;口令错
  必须清 auth_user_ok 强制重走 USER;被锁与口令错都只回 530,不给攻击者区分提示;
  配置读不到时 USER 无法匹配、PASS 直接 530(fail-closed)。
- 写开关([ftp] write_en=0)必须在**碰文件之前**挡住 STOR/DELE,且不去 accept 数据
  通道;DELE 不许删目录。
- 227/229 编码:IP 经 net_htonl 后按高字节到低字节排(就是点分序),端口高/低两字节;
  拿不到本机 IP 时要把刚建的监听关掉。FEAT 多行:首行 211-、四条能力无状态码、末行
  211 结束(共 6 条响应)。
- 220 欢迎没送达(对端已走)就直接收工,不空转读命令;该留痕按真实分级算 INFO 而不是
  ERROR(真实 private/lftpd_log.h 把 lftpd_log_peer 归到 INFO:任何人连 21 端口就走
  就能无限制造固件级 ERROR,还会撞掉 @test 的 Forbidden 标记 [ERR])。
- 传输循环每轮喂狗(lftpd_task_wd_beat 计数可断言)。

主机侧两个必要的兼容处理(都写在桩注释里,不是随手改):
1. lftpd.c 会 include ff.h,而这份 FatFs 在 Windows 支路上把 winsock.h 拉进来,它带
   "#define s_addr S_un.S_addr" 的 BSD 兼容宏,会把真实 net_types.h 里的字段声明
   uint32_t s_addr; 展开成 uint32_t S_un.S_addr; 直接炸(实测 C2143 在 "." 前缺 ";")。
   桩里只 #undef 这个污染宏,让固件那份结构体定义原样参与编译,被测代码读的还是真字段。
2. 引号 include 先从 lftpd.c 自己目录找 private/lftpd_log.h,桩目录赢不过它——于是
   顺着用真实那份(它只是把 lftpd_log_* 映射成 DBG_*),只桩最底下的 dbg_log.h。这样
   真实日志分级也一起被编译验证,比我自己重写一套更对。

自查(35 条里前两轮红了 4 条,全是**我的期望/前置态写错**,实现均正确):未登录时
FOO 被白名单以 530 拦掉测不到 502 分派;227 的 IP 顺序我把 htonl 前后搞反;FEAT 是
6 条响应我按 5 条索引;PASV 之后走完整会话会被 cleanup 关掉监听(那正是 006 要验的
行为),观察 PASV 留下的状态必须直调 cmd_pasv。

覆盖率与台账:套件 26 → 27 全绿;files_tested 30 → 31;未触及 .c 36 → 35;分母
4,072 → 4,545 行。**总体 line_rate 88.4% → 85.9% 是摊薄不是回退**(64.3% 的文件进
了分母),按 §5 口径要盯 files_tested/files_untested——它从 19 一路涨到 31。
恒等式 35 = 1(P1 剩 iec104_serv.c) + 5(P2) + 29(P3)。编号补建 TC-HOST-LFTPD
(游标 034),族数 20 → 21。

自查门禁:test-cppcheck Passed;check_ids --strict 通过;doc-links 35 处(= 存量);
9 个文件 BOM/CR/U+FFFD/行尾逐项净。零生产代码改动,未跑 @build。--all-files 仍只有
mem-numbers / mem-budget(.map 台账漂移,等 Lead 合并后统一回填)与 ui-golden
(ref/*.bin 不入库)三条失败,与本分支改动无关。
2026-10-23 23:44:52 +08:00

737 lines
25 KiB
C
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.
/*
* 模块名称:FTP 控制通道协议层(lftpd)主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/NET/lftpd/lftpd.c 的控制通道状态机与 17 条
* 命令处理器:认证前置白名单、超长命令名拒绝(不越界写命令缓冲)、未知命令
* 502、QUIT 收尾时释放数据监听、大小写不敏感、PASV/EPSV 的 227/229 响应
* 编码、数据 accept 失败时不残留半开监听、STOR 失败要**删掉半成品**、
* 写开关关闭时 DELE/STOR 一律 550、CWD 的目录判定与截断安全、FEAT 多行
* 响应的首尾行格式。
*
* 被测对象:真实源文件 ../../../../Drivers/BSP/NET/lftpd/lftpd.c(#include 纳入,
* 因此命令表 commands[]、24 个 static cmd_* 与 handle_control_channel 都
* 可在同一 TU 内直接驱动)。假的是它下面三层:inet 适配(脚本化命令行
* 队列 + 响应记录)、io 层(极简内存视图:stat/open/read/write/opendir/
* readdir/unlink + 可注入失败)、认证与配置(ftp_auth_check/ftp_cfg_get
* 可注入结果,认证本身有 test/host/ftp_auth 的 10 条专测,不重复)。
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. **半成品文件**是 FTP 最贵的坑:STOR 传到一半对端断了,如果不 unlink,Flash 上
* 就永久留一个残缺文件(现场表现为"升级包传坏了再也传不上去")。真机上要卡准
* "传一半掐线"的时机,主机侧把 io 层的写失败注进去就行。
* 2. 数据通道是**单连接 socket**:LIST/RETR/STOR 每条命令用完必须把监听关掉并复位
* data_socket=-1,否则下一条命令拿着上一个号继续 accept,表现是偶发连不上且
* 与上一会话时序相关。这类"忘了复位"只有把 accept 失败也注进来才测得干净。
* 3. 命令名缓冲是 command_tmp[4+1],而判断用 if (index >= 5) 先拦——这条边界
* (恰好 5 个字符的命令名必须被拒)就是防 memcpy 越界,改一行就没了。
* 4. 写开关([ftp] write_en=0)是现场防误删的闸门,DELE/STOR 必须在**碰文件之前**
* 就拒绝,且此时不该去 accept 数据通道。
*
* 覆盖点:TC-LFTPD-001 认证白名单;002 超长命令名;003 未知命令 502;004 QUIT 退出;
* 005 欢迎未送达就收工;006 退出时释放数据监听;007 大小写不敏感;008 无参数命令;
* 010 未开被动模式的数据命令 425;011/012/013 PASV/EPSV 响应;014 数据 accept 失败
* 不留残口;015/016 LIST 成功与目录打不开;017/018 RETR 成功与打不开;
* 019/020 STOR 删半成品与写开关;021 DELE 写开关;022/023 CWD;024 SIZE;025 PWD;
* 026 SYST;027 FEAT 多行;028/029 口令失败与锁定;030 TYPE/NOOP。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lftpd.h"
#include "lftpd_auth.h"
#include "ftp_cfg.h"
#include "ff.h"
#include "lftpd.c"
/* 命令行 trim 用**真实实现**(16 行、零依赖;lftpd_string.c 的契约由
test/host/lftpd_io 的 TC-LFTSTR-* 专测,这里只是满足链接、顺带让参数解析走真码)。 */
#include "lftpd_string.c"
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 桩:日志计数 =====================
* lftpd_log_info/error/peer/debug 是真实 private/lftpd_log.h 里的**宏**,展开成
* DBG_*;本模块只桩最底下的 dbg_log.h(计数)。这样真实分级逻辑一起被编译验证:
* peer 级归 INFO、debug 级在 MCU 上被整条摘掉。 */
int g_lg_info_count;
int g_lg_error_count;
int g_lg_debug_count;
/* ===================== 桩:假堆(返回给 vPortFree 的路径缓冲) ===================== */
static uint8_t g_pool[2048];
static size_t g_pool_used;
static int g_free_calls;
void *pvPortMalloc(size_t xWantedSize)
{
void *p;
g_pool_used = (g_pool_used + 7u) & ~(size_t)7u;
if ((g_pool_used + xWantedSize) > sizeof(g_pool)) {
return NULL;
}
p = &g_pool[g_pool_used];
g_pool_used += xWantedSize;
return p;
}
void vPortFree(void *pv)
{
if (pv != NULL) {
g_free_calls++;
}
}
/* ===================== 假 inet 层:脚本化命令行 + 响应记录 ===================== */
typedef struct {
const char *lines[16];
int line_ret[16];
int line_n;
int line_i;
char replies[32][256];
int reply_n;
int string_rc; /* write_string 返回(0=成功) */
int listen_rc;
int port;
int accept_to_rc; /* 数据通道 accept 结果 */
int data_write_rc; /* 数据写返回(<0 模拟对端断) */
int rx_left; /* 数据通道还剩多少字节可读 */
int close_calls;
int listen_calls;
int accept_calls;
int delay_calls;
} lftp_net_t;
static lftp_net_t g_n;
int lftpd_inet_read_line(int socket, char *buffer, size_t buffer_len)
{
(void)socket;
if (g_n.line_i >= g_n.line_n) {
return -1; /* 脚本喂完 = 对端关闭 */
}
(void)strncpy(buffer, g_n.lines[g_n.line_i], buffer_len - 1u);
buffer[buffer_len - 1u] = '\0';
return g_n.line_ret[g_n.line_i++];
}
int lftpd_inet_write_string(int socket, const char *message)
{
(void)socket;
if (g_n.reply_n < (int)(sizeof(g_n.replies) / sizeof(g_n.replies[0]))) {
(void)strncpy(g_n.replies[g_n.reply_n], message, sizeof(g_n.replies[0]) - 1u);
g_n.replies[g_n.reply_n][sizeof(g_n.replies[0]) - 1u] = '\0';
g_n.reply_n++;
}
return g_n.string_rc;
}
int lftpd_inet_write(int socket, const void *buf, int len)
{
(void)socket;
(void)buf;
return (g_n.data_write_rc != 0) ? g_n.data_write_rc : len;
}
int lftpd_inet_read(int socket, void *buf, int len)
{
int n;
(void)socket;
(void)buf;
if (g_n.rx_left <= 0) {
return 0; /* 对端结束数据 */
}
n = (g_n.rx_left > len) ? len : g_n.rx_left;
g_n.rx_left -= n;
return n;
}
int lftpd_inet_listen(int port)
{
(void)port;
g_n.listen_calls++;
return g_n.listen_rc;
}
int lftpd_inet_get_socket_port(int socket)
{
(void)socket;
return g_n.port;
}
int lftpd_inet_accept(int socket)
{
(void)socket;
return -1;
}
int lftpd_inet_accept_timeout(int socket, int timeout_ms)
{
(void)socket;
(void)timeout_ms;
g_n.accept_calls++;
return g_n.accept_to_rc;
}
int lftpd_inet_close(int socket)
{
(void)socket;
g_n.close_calls++;
return 0;
}
/* ===================== 假 io 层:极简内存视图 ===================== */
typedef struct {
int stat_rc;
int is_dir;
uint32_t size;
int open_read_rc;
int open_write_rc;
int write_short; /* >0:写返回这个短值 -> 触发半成品清理 */
int read_left;
int opendir_rc;
const char *names[4];
uint32_t sizes[4];
int dirs[4];
int readdir_i;
int readdir_n;
int unlink_calls;
char last_path[128];
} lftp_io_t;
static lftp_io_t g_io;
char *lftpd_io_canonicalize_path(const char *base, const char *name)
{
char *p = (char *)pvPortMalloc(128);
if (p == NULL) {
return NULL;
}
(void)snprintf(p, 128, "%s/%s", (base != NULL) ? base : "",
(name != NULL) ? name : "");
(void)strncpy(g_io.last_path, p, sizeof(g_io.last_path) - 1u);
return p;
}
int lftpd_io_stat(const char *path, uint32_t *p_size, int *p_is_dir)
{
(void)path;
if (p_size != NULL) { *p_size = g_io.size; }
if (p_is_dir != NULL) { *p_is_dir = g_io.is_dir; }
return g_io.stat_rc;
}
int lftpd_io_open_read(const char *path) { (void)path; return g_io.open_read_rc; }
int lftpd_io_open_write(const char *path) { (void)path; return g_io.open_write_rc; }
int lftpd_io_read(int handle, void *buf, int len)
{
int n;
(void)handle;
(void)buf;
if (g_io.read_left <= 0) {
return 0;
}
n = (g_io.read_left > len) ? len : g_io.read_left;
g_io.read_left -= n;
return n;
}
int lftpd_io_write(int handle, const void *buf, int len)
{
(void)handle;
(void)buf;
return (g_io.write_short > 0) ? g_io.write_short : len;
}
void lftpd_io_close(int handle) { (void)handle; }
int lftpd_io_unlink(const char *path) { (void)path; g_io.unlink_calls++; return 0; }
void *lftpd_io_opendir(const char *path)
{
(void)path;
return (g_io.opendir_rc != 0) ? NULL : (void *)&g_io;
}
int lftpd_io_readdir(void *dp, char *name, int name_max, uint32_t *p_size, int *p_is_dir)
{
int i = g_io.readdir_i;
(void)dp;
if (i >= g_io.readdir_n) {
return -1;
}
g_io.readdir_i++;
(void)strncpy(name, g_io.names[i], (size_t)name_max - 1);
name[name_max - 1] = '\0';
*p_size = g_io.sizes[i];
*p_is_dir = g_io.dirs[i];
return 0;
}
void lftpd_io_closedir(void *dp) { (void)dp; }
/* ===================== 假配置与假认证 ===================== */
static ftp_cfg_t g_cfg;
static int g_cfg_present = 1;
static int g_default_pwd;
static ftp_auth_result_t g_auth_res;
static int g_auth_calls;
const ftp_cfg_t *ftp_cfg_get(void) { return (g_cfg_present != 0) ? &g_cfg : NULL; }
int ftp_cfg_is_default_pwd(void) { return g_default_pwd; }
ftp_auth_result_t ftp_auth_check(const ftp_auth_creds_t *p_creds, uint8_t user_ok,
const char *p_pwd, ftp_auth_state_t *p_state,
uint32_t now_ms)
{
(void)p_creds;
(void)user_ok;
(void)p_pwd;
(void)p_state;
(void)now_ms;
g_auth_calls++;
return g_auth_res;
}
int ftp_auth_user_match(const char *p_cfg_user, const char *p_given)
{
if ((p_cfg_user == NULL) || (p_given == NULL)) {
return 0;
}
return (strcmp(p_cfg_user, p_given) == 0) ? 1 : 0;
}
/* ===================== 假 net 与 OS ===================== */
static uint32_t g_tick;
static int g_sockname_rc; /* net_getsockname 的返回码(PASV 分支注入失败用) */
uint32_t xTaskGetTickCount(void) { return g_tick; }
/* lftpd_task_wd_beat 的真实定义在 lftpd_inet.c 里(__weak,供 lftpd.c 覆盖),本模块
不拖那一层进来,故在此给计数假件——正好把"传输循环每轮喂狗"变成可断言的事。 */
static int g_wd_beats;
void lftpd_task_wd_beat(void) { g_wd_beats++; }
void vTaskDelay(TickType_t t)
{
(void)t;
g_n.delay_calls++;
if (g_n.delay_calls > 200) {
printf(" ABORT 循环跑飞(>200 轮)\n");
fflush(stdout);
exit(2);
}
}
int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen)
{
struct net_sockaddr_in *a = (struct net_sockaddr_in *)addr;
(void)sockfd;
(void)addrlen;
a->sin_addr.s_addr = 0x0101A8C0u; /* 192.168.1.1 的宿主序表示(假件自定) */
a->sin_port = net_htons(21u);
return g_sockname_rc;
}
int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen)
{
(void)sockfd;
(void)addr;
(void)addrlen;
return 0;
}
uint16_t net_htons(uint16_t v) { return (uint16_t)((v >> 8) | (v << 8)); }
uint16_t net_ntohs(uint16_t v) { return net_htons(v); }
uint32_t net_htonl(uint32_t v)
{
return ((v & 0xFFu) << 24) | (((v >> 8) & 0xFFu) << 16) |
(((v >> 16) & 0xFFu) << 8) | ((v >> 24) & 0xFFu);
}
char *net_inet_ntoa(uint32_t in, char *p_buf)
{
(void)snprintf(p_buf, 16, "%u.%u.%u.%u", (unsigned)(in & 0xFFu),
(unsigned)((in >> 8) & 0xFFu), (unsigned)((in >> 16) & 0xFFu),
(unsigned)((in >> 24) & 0xFFu));
return p_buf;
}
/* ===================== 复位与驱动 ===================== */
static lftpd_client_t g_cli;
static void reset_all(void)
{
(void)memset(&g_n, 0, sizeof(g_n));
(void)memset(&g_io, 0, sizeof(g_io));
g_n.string_rc = 0;
g_n.listen_rc = 20;
g_n.port = 5000;
g_n.accept_to_rc = 21; /* 默认数据通道能连上 */
g_n.data_write_rc = 0;
g_io.stat_rc = 0;
g_io.is_dir = 1;
g_io.size = 7u;
g_io.open_read_rc = 0;
g_io.open_write_rc = 0;
g_io.read_left = 0;
g_io.readdir_n = 0;
g_cfg_present = 1;
(void)memset(&g_cfg, 0, sizeof(g_cfg));
(void)strcpy(g_cfg.user, "admin");
g_cfg.write_en = 1u;
g_auth_res = FTP_AUTH_OK;
g_default_pwd = 0;
g_tick = 0u;
g_wd_beats = 0;
g_free_calls = 0;
g_pool_used = 0u;
g_lg_info_count = 0;
g_lg_error_count = 0;
g_lg_debug_count = 0;
g_sockname_rc = 0;
(void)memset(&g_cli, 0, sizeof(g_cli));
(void)strcpy(g_cli.directory, "/");
g_cli.socket = 10;
g_cli.data_socket = -1;
/* 不写死字段名:认证状态的结构由 lftpd_auth.h 定义,本模块只把它当黑盒清零
(口令/锁定逻辑有 test/host/ftp_auth 的 10 条专测,这里不重复验)。 */
(void)memset(&s_auth_state, 0, sizeof(s_auth_state));
}
static void feed(const char *l0, const char *l1, const char *l2, const char *l3)
{
int n = 0;
g_n.lines[n] = l0; g_n.line_ret[n++] = 0;
if (l1 != NULL) { g_n.lines[n] = l1; g_n.line_ret[n++] = 0; }
if (l2 != NULL) { g_n.lines[n] = l2; g_n.line_ret[n++] = 0; }
if (l3 != NULL) { g_n.lines[n] = l3; g_n.line_ret[n++] = 0; }
g_n.line_n = n;
}
static const char *rep(int k) { return (k < g_n.reply_n) ? g_n.replies[k] : ""; }
static int rep_has(const char *prefix)
{
int i;
int c = 0;
for (i = 0; i < g_n.reply_n; i++) {
if (strncmp(g_n.replies[i], prefix, strlen(prefix)) == 0) {
c++;
}
}
return c;
}
/* ===================== 控制通道闸门 ===================== */
static void tc_gates(void)
{
printf("-- TC-LFTPD-001..008 control channel gates --\n");
reset_all();
feed("RETR a.txt", "LIST", "PWD", NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("220 ") == 1) && (rep_has("530 ") == 3) &&
(g_n.accept_calls == 0),
"TC-LFTPD-001 未登录只放行 USER/PASS/QUIT/FEAT/SYST/NOOP,其它一律 530");
reset_all();
feed("ABCDEFG", "NOOP", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("500 ") == 1) && (rep_has("200 ") == 1),
"TC-LFTPD-002 命令名 >=5 字符先拒(command_tmp 只有 5 格,防 memcpy 越界)");
reset_all();
g_cli.auth_logged_in = 1; /* 否则会被认证白名单先以 530 拦掉,测不到分派 */
feed("FOO bar", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("502 ") == 1) && (rep_has("530 ") == 0),
"TC-LFTPD-003 认不出的命令回 502 而不是断开");
reset_all();
feed("QUIT", "NOOP", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("221 ") == 1) && (rep_has("200 ") == 0) && (g_n.line_i == 1),
"TC-LFTPD-004 QUIT 之后不再读下一条(会话就此结束)");
reset_all();
g_n.string_rc = -1; /* 220 都送不出去 */
feed("NOOP", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((g_n.line_i == 0) && (g_lg_info_count >= 1),
"TC-LFTPD-005 欢迎未送达(对端已走)就直接收工,不空转读命令(该留痕按真实分级算 INFO)");
reset_all();
g_cli.data_socket = 33; /* 被动模式已开但客户端直接跑了 */
feed("QUIT", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((g_n.close_calls == 1) && (g_cli.data_socket == -1),
"TC-LFTPD-006 退出时必须释放还开着的**数据监听**(单连接 socket 不能留残口)");
reset_all();
g_cli.auth_logged_in = 1;
feed("noop", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK(rep_has("200 ") == 1, "TC-LFTPD-007 命令名大小写不敏感");
reset_all();
g_cli.auth_logged_in = 1;
g_io.opendir_rc = 0;
g_io.readdir_n = 0;
g_n.listen_rc = 20;
feed("LIST", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("425 ") == 1) && (g_n.accept_calls == 0),
"TC-LFTPD-010 没先开被动模式的数据命令一律 425,且不去 accept");
}
/* ===================== 被动模式与数据通道 ===================== */
static void tc_passive(void)
{
printf("-- TC-LFTPD-011..016 passive & data --\n");
reset_all();
g_cli.auth_logged_in = 1;
/* 直调处理器:走 handle_control_channel 的话,会话退出时 cleanup 一定把被动监听
关掉(那正是 TC-LFTPD-006 要验的行为),这里要看的是 PASV 自己留下的状态。 */
(void)cmd_pasv(&g_cli, NULL);
CHECK((strcmp(rep(0), "227 Entering Passive Mode (192,168,1,1,19,136).\r\n") == 0) &&
(g_cli.data_socket == 20) && (g_n.listen_calls == 1),
"TC-LFTPD-011 227 六元组:IP 经 htonl 后按高字节到低字节(点分序)、端口高/低,并记下被动监听号");
reset_all();
g_cli.auth_logged_in = 1;
g_sockname_rc = -1; /* 问不到本机地址 */
feed("PASV", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("425 ") == 1) && (g_n.close_calls == 1) &&
(g_cli.data_socket == -1),
"TC-LFTPD-012 拿不到本机 IP 时要把刚建的监听关掉(别留半个被动模式)");
reset_all();
g_cli.auth_logged_in = 1;
feed("EPSV", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK(strcmp(rep(1), "229 Entering Extended Passive Mode (|||5000|).\r\n") == 0,
"TC-LFTPD-013 EPSV 只报端口(IPv6 客户端自己沿用控制连接地址)");
reset_all();
g_cli.auth_logged_in = 1;
g_n.listen_rc = 20;
g_n.accept_to_rc = -1; /* 对端一直不开数据通道 */
feed("PASV", "LIST", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("150 ") == 1) && (rep_has("425 ") == 1) &&
(g_cli.data_socket == -1) && (g_n.close_calls == 1),
"TC-LFTPD-014 数据 accept 失败:报 425 并**关掉监听复位**,不留给下一条命令");
reset_all();
g_cli.auth_logged_in = 1;
g_io.readdir_n = 2;
g_io.names[0] = "a.txt"; g_io.sizes[0] = 7u; g_io.dirs[0] = 0;
g_io.names[1] = "sub"; g_io.sizes[1] = 0u; g_io.dirs[1] = 1;
feed("PASV", "LIST", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("226 ") == 1) && (g_n.data_write_rc == 0),
"TC-LFTPD-015 LIST 成功:先 150、数据口写目录、再 226 收尾");
reset_all();
g_cli.auth_logged_in = 1;
g_io.opendir_rc = 1; /* 目录打不开 */
feed("PASV", "LIST", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK(rep_has("550 ") == 1, "TC-LFTPD-016 目录打不开时 LIST 回 550(不是 226)");
}
/* ===================== 传输与写开关 ===================== */
static void tc_transfer(void)
{
printf("-- TC-LFTPD-017..021 transfer & write gate --\n");
reset_all();
g_cli.auth_logged_in = 1;
g_io.read_left = 5;
feed("PASV", "RETR a.txt", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("226 ") == 1) && (g_lg_info_count >= 1) && (g_wd_beats >= 1),
"TC-LFTPD-017 RETR 成功:226 收尾、留 DOWNLOAD 记录,且传输循环有喂狗");
reset_all();
g_cli.auth_logged_in = 1;
g_io.open_read_rc = -1;
feed("PASV", "RETR gone.txt", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("450 ") == 1) && (g_lg_error_count >= 1),
"TC-LFTPD-018 文件打不开回 450(传输失败与命令不合法要分开报)");
reset_all();
g_cli.auth_logged_in = 1;
g_n.rx_left = 900;
g_io.write_short = 100; /* 写到一半失败 */
feed("PASV", "STOR up.bin", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((g_io.unlink_calls == 1) && (rep_has("450 ") == 1),
"TC-LFTPD-019 STOR 中途失败必须**删掉半成品**(否则 Flash 上永久留个坏文件)");
reset_all();
g_cli.auth_logged_in = 1;
g_cfg.write_en = 0u;
g_n.listen_rc = 20;
feed("PASV", "STOR up.bin", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("550 ") == 1) && (g_n.accept_calls == 0) &&
(g_io.open_write_rc == 0) && (g_io.unlink_calls == 0),
"TC-LFTPD-020 写开关关闭时 STOR 在碰文件之前就拒绝,且不开数据通道");
reset_all();
g_cli.auth_logged_in = 1;
g_cfg.write_en = 0u;
(void)cmd_dele(&g_cli, "a.txt");
CHECK(rep_has("550 ") == 1, "TC-LFTPD-021 写开关同样挡住 DELE");
reset_all();
g_cli.auth_logged_in = 1;
g_io.is_dir = 1;
(void)cmd_dele(&g_cli, "sub");
CHECK((rep_has("550 ") == 1) && (g_io.unlink_calls == 0),
"TC-LFTPD-022 DELE 不许删目录(本实现只支持删文件)");
reset_all();
g_cli.auth_logged_in = 1;
g_io.is_dir = 0;
(void)cmd_dele(&g_cli, "a.txt");
CHECK((g_io.unlink_calls == 1) && (rep_has("250 ") == 1),
"TC-LFTPD-022b 删普通文件走 unlink 并回 250");
}
/* ===================== 目录与元信息 ===================== */
static void tc_meta(void)
{
printf("-- TC-LFTPD-023..030 cwd / size / pwd / feat --\n");
reset_all();
(void)cmd_cwd(&g_cli, "");
CHECK((rep_has("550 ") == 1) && (strcmp(g_cli.directory, "/") == 0),
"TC-LFTPD-023 CWD 空参数拒绝,当前目录不变");
reset_all();
g_io.is_dir = 0;
(void)cmd_cwd(&g_cli, "a.txt");
CHECK(rep_has("550 ") == 1, "TC-LFTPD-023b CWD 到普通文件被拒(is_dir 必须真为目录)");
reset_all();
g_io.is_dir = 1;
(void)cmd_cwd(&g_cli, "logs");
CHECK((rep_has("250 ") == 1) && (strcmp(g_cli.directory, "//logs") == 0) &&
(g_free_calls == 1),
"TC-LFTPD-024 CWD 成功后更新当前目录并释放路径缓冲(不泄漏堆)");
reset_all();
(void)cmd_cwd(&g_cli, "/");
CHECK((rep_has("250 ") == 1) && (g_io.stat_rc == 0),
"TC-LFTPD-024b 回到根目录免 stat 直接放行(根一定存在)");
reset_all();
g_io.is_dir = 0;
g_io.size = 12345u;
(void)cmd_size(&g_cli, "up.bin");
CHECK(strcmp(rep(0), "213 12345\r\n") == 0, "TC-LFTPD-025 SIZE 报字节数");
reset_all();
g_io.stat_rc = -1;
(void)cmd_size(&g_cli, "nope");
CHECK(rep_has("550 ") == 1, "TC-LFTPD-025b 文件不存在时 SIZE 回 550");
reset_all();
(void)strcpy(g_cli.directory, "/logs");
(void)cmd_pwd(&g_cli, NULL);
CHECK(strcmp(rep(0), "257 \"/logs\"\r\n") == 0,
"TC-LFTPD-026 PWD 用双引号包住目录(RFC 959 要求)");
reset_all();
(void)cmd_syst(&g_cli, NULL);
CHECK(strcmp(rep(0), "215 UNIX Type: L8\r\n") == 0,
"TC-LFTPD-027 SYST 固定报 UNIX Type: L8(客户端据此决定模式)");
reset_all();
(void)cmd_feat(&g_cli, NULL);
CHECK((strncmp(rep(0), "211-", 4) == 0) && (strcmp(rep(1), "EPSV\r\n") == 0) &&
(strcmp(rep(2), "PASV\r\n") == 0) && (strncmp(rep(5), "211 ", 4) == 0) &&
(g_n.reply_n == 6),
"TC-LFTPD-028 FEAT 多行:首行 211-、四条能力无状态码、末行 211 结束");
reset_all();
(void)cmd_type(&g_cli, "I");
(void)cmd_noop(&g_cli, NULL);
CHECK(rep_has("200 ") == 2, "TC-LFTPD-030 TYPE 与 NOOP 都回 200");
}
/* ===================== 登录序列 ===================== */
static void tc_login(void)
{
printf("-- TC-LFTPD-031..033 login --\n");
reset_all();
feed("USER admin", "PASS good", "RETR a.txt", NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("331 ") == 1) && (rep_has("230 ") == 1) &&
(g_cli.auth_logged_in == 1u) && (strcmp(g_cli.auth_user, "admin") == 0),
"TC-LFTPD-031 USER 记下用户名与匹配位,PASS 过了就转已登录");
reset_all();
g_auth_res = FTP_AUTH_FAIL;
feed("USER admin", "PASS bad", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("530 ") == 1) && (g_cli.auth_logged_in == 0u) &&
(g_lg_info_count >= 1),
"TC-LFTPD-032 口令错:530 且**必须重新走 USER**(auth_user_ok 已清零)");
reset_all();
g_auth_res = FTP_AUTH_LOCKED;
g_cfg_present = 1;
feed("USER admin", "PASS bad", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK(rep_has("530 ") == 1,
"TC-LFTPD-033 连错被锁时同样只回 530(不给攻击者区分提示)");
reset_all();
g_cfg_present = 0; /* 配置读不到 */
feed("USER admin", "PASS x", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("331 ") == 1) && (rep_has("530 ") == 1) &&
(g_cli.auth_user_ok == 0u),
"TC-LFTPD-034 配置缺失时 USER 无法匹配、PASS 直接 530(fail-closed)");
}
int main(void)
{
printf("=== LFTPD Protocol Tests ===\n");
tc_gates();
tc_passive();
tc_transfer();
tc_meta();
tc_login();
printf("\n=== LFTPD Protocol Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}