Files
STM32F4-Base/Drivers/BSP/NET/lftpd/lftpd.c
T
wandering1 5224f21622 fix(NET,lftpd): 处置覆盖率照出的三处死路径(改名 + 修 timeout=0 + 激活异常集)
三处都由本轮 P1 新增的主机单测照出,用户逐条裁定后实施。共同性质:都不是改动引入
的回归,而是**从没被执行到的分支**——生产路径上要么没有调用者、要么没走边界,所以
编译器(0 错 0 警)、cppcheck、真机日志对它都无话可说。

1) 缺陷一(选 a:改名,行为零变化)
   lftpd_string_trim 的尾部循环起点落在 strlen(p) 那一格 NUL,isspace(0) 为假 →
   循环体一次都不执行,"尾部空白从未被裁",而名字承诺两头都去。改名
   lftpd_string_ltrim + 删掉那条死循环 + 在 .h/.c 写明"只去前导"。不去尾才是对的:
   RFC 959 以命令终止符 CRLF 界定命令,参数(PASS 口令、含空格的文件名)的尾随
   空格属于参数本体,去掉等于静默改数据;CRLF 已由 lftpd_inet_read_line 截断。
   调用点 lftpd.c 同步改名。全库无遗留真引用(余 6 处旧名均为解释改名历史的散文)。

2) 缺陷二(按推荐修)
   net_select 的 timeout = {0,0} 换算出 timeout_ms = 0,而退出条件写着
   if ((timeout_ms > 0) && (已用时 >= timeout_ms)) → 超时永不生效,退化成死等;
   POSIX 语义是"只查一轮立即返回"。去掉 timeout_ms > 0 这个错误前置即可。就绪
   检查在本判断之前,故有就绪时仍立刻带计数返回;同文件 net_poll_events 的
   timeout >= 0 本来就对,现在两条对齐。风险面:拿它当"非阻塞查一遍"会让 netTask
   回不到主循环,而 netTask 在 IWDG required_mask 里(串行 socket 栈唯一执行者)。

3) 缺陷三(选 ①:按 POSIX 置位)
   exceptfds 是恒为空的死参数:循环开头 p_sock == NULL → continue 先跳过,而
   check_socket_error() 只有"表项为空 / fd 越界"才返回 1,越界的 i 又不在本循环
   范围内 → 调用方按 POSIX 直觉用它收异常,永远什么都收不到,且返回码不报错。
   把异常判定提到 continue 之前:表项不存在的 fd 计入 exceptfds 与返回计数,并更新
   函数注释。选 ① 会改变返回计数语义,故补一条"未请求异常集时不计数"的用例,
   确保只查可读的老调用方不被误伤(TC-NETSEL-012b)。

测试同步翻转:原 TC-NETSEL-008 / TC-NETSEL-012 是"现状桩、改了会响",现在改成正向
契约,并新增 TC-NETSEL-012c(异常位只落到被请求的那个集合);TC-LFTSTR-005 从
"尾部不裁"的现状登记改为正向契约(尾随空格必须保留)。net_select 22/22、
lftpd_io 33/33 全绿。

@build:0 错误 0 警告。实测体积 ER_IROM1 232,512 B(快照 232,536 → **−24 B**)、
Flash 233,704 B(−20 B)、RW_IRAM1 75,272 B(+8 B 是上一提交 net_cfg 那个字节,
本批零新增)、**RW_CCM 57,376 B 不变**;四档百分比 22.2/57.4/87.9/22.3 全不变。
Code 减少 36 B 即删掉的死循环。

覆盖率台账:门禁如实报了一次 lftpd_string.c 已覆盖行 8 → 7(**代码少了一行**,不是
覆盖退化),已 --update-baseline 重登;现 22/22 套件、line=3003/3446(87.1%)、
func=236/259(91.1%)、tested_files=26、untested_files=118。

文档:主机单测覆盖率口径 §9 新增"三处死路径的处置"小节(裁定、改法、影响、钉住
它的用例一一对应),并把两处"等裁定"的登记改为已处置。

自查:coding-standard / test-cppcheck / id-check --strict(无台账外新账)/ doc-links
(35 处 = 存量)全过;八个改动文件与 HEAD 逐项比对 BOM/CR/U+FFFD/行尾无变化
(这几个文件本就不带 BOM,未被动过)。pre-commit --all-files 仍只有三条失败且与
本分支无关:mem-numbers / mem-budget(.map 与 docs/内存管理 受管快照漂移,增量见
上,按并行分支约定不在分支里 --write,等 Lead 合并后统一回填)与 ui-golden
(simulator/tests/ref/*.bin 在 .gitignore 内,新建 worktree 天生没有黄金参考)。
2026-10-23 23:44:48 +08:00

901 lines
31 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.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdbool.h>
#include <ctype.h>
#include "FreeRTOS.h"
#include "lftpd.h"
#include "private/lftpd_status.h"
#include "private/lftpd_inet.h"
#include "private/lftpd_log.h"
#include "private/lftpd_string.h"
#include "private/lftpd_io.h"
#include "net_socket.h"
#include "net_config.h"
#include "lftpd_auth.h"
#include "ftp_cfg.h"
#include "ff.h"
typedef struct {
const char *command;
int (*handler)(lftpd_client_t *client, const char *arg);
} command_t;
static int cmd_cwd(lftpd_client_t *client, const char *arg);
static int cmd_dele(lftpd_client_t *client, const char *arg);
static int cmd_epsv(lftpd_client_t *client, const char *arg);
static int cmd_feat(lftpd_client_t *client, const char *arg);
static int cmd_list(lftpd_client_t *client, const char *arg);
static int cmd_nlst(lftpd_client_t *client, const char *arg);
static int cmd_noop(lftpd_client_t *client, const char *arg);
static int cmd_pass(lftpd_client_t *client, const char *arg);
static int cmd_pasv(lftpd_client_t *client, const char *arg);
static int cmd_pwd(lftpd_client_t *client, const char *arg);
static int cmd_quit(lftpd_client_t *client, const char *arg);
static int cmd_retr(lftpd_client_t *client, const char *arg);
static int cmd_size(lftpd_client_t *client, const char *arg);
static int cmd_stor(lftpd_client_t *client, const char *arg);
static int cmd_syst(lftpd_client_t *client, const char *arg);
static int cmd_type(lftpd_client_t *client, const char *arg);
static int cmd_user(lftpd_client_t *client, const char *arg);
static command_t commands[] = {
{ "CWD", cmd_cwd },
{ "DELE", cmd_dele },
{ "EPSV", cmd_epsv },
{ "FEAT", cmd_feat },
{ "LIST", cmd_list },
{ "NLST", cmd_nlst },
{ "NOOP", cmd_noop },
{ "PASS", cmd_pass },
{ "PASV", cmd_pasv },
{ "PWD", cmd_pwd },
{ "QUIT", cmd_quit },
{ "RETR", cmd_retr },
{ "SIZE", cmd_size },
{ "STOR", cmd_stor },
{ "SYST", cmd_syst },
{ "TYPE", cmd_type },
{ "USER", cmd_user },
{ NULL, NULL },
};
/* N-19 FTP 鉴权:全局失败计数/锁定状态(跨会话持久——单连接服务器,全局
* 计数即可;成功清零、锁定期满自动放行,见 lftpd_auth.c) */
static ftp_auth_state_t s_auth_state;
static int send_response(int socket, int code, bool include_code,
bool multiline_start, const char *format, ...)
{
va_list args;
static char message[256]; /* static: off-stack, single-client ftpTask */
static char response[512]; /* static: off-stack, single-client ftpTask */
va_start(args, format);
vsnprintf(message, sizeof(message), format, args);
va_end(args);
if (include_code) {
if (multiline_start) {
snprintf(response, sizeof(response), "%d-%s%s", code, message, CRLF);
} else {
snprintf(response, sizeof(response), "%d %s%s", code, message, CRLF);
}
} else {
snprintf(response, sizeof(response), "%s%s", message, CRLF);
}
return lftpd_inet_write_string(socket, response);
}
#define send_simple_response(socket, code, format, ...) \
send_response(socket, code, true, false, format, ##__VA_ARGS__)
#define send_multiline_response_begin(socket, code, format, ...) \
send_response(socket, code, true, true, format, ##__VA_ARGS__)
#define send_multiline_response_line(socket, format, ...) \
send_response(socket, 0, false, false, format, ##__VA_ARGS__)
#define send_multiline_response_end(socket, code, format, ...) \
send_response(socket, code, true, false, format, ##__VA_ARGS__)
/* 一块装满就整块发出、清空继续装。
* 为什么仍要"批量":定案三(../问题记录/CH395F_Trap_Records.md,docs 里那条 LIST 吞包
* 定案)实测同一连接上**背靠背 ~60 B 小段写**会毒化 CH395F 的 TX(芯片报 SEND_OK 但对端
* 全丢,无 RST 无 DISCONNECT),而 RETR 的 1 KB 满块连发安全——所以这里只发大块,
* 绝不退回"每行一发"。
* 为什么必须"续发"而不是截断:原先装满就 break 并回 -1,cmd_list 据此回 550,客户端
* 既拿到**不完整**的目录、又收到错误码(实测 24 条目/1512 B 之后全部被吞)。目录大
* 小不该由一个缓冲区上限决定。
* 返回:0 = 已发出(或本就空),-1 = 写失败 */
static int list_batch_flush(int socket, char *batch, int *p_blen)
{
int len = *p_blen;
if (len <= 0) {
return 0;
}
lftpd_task_wd_beat(); /* 看门狗:目录列表分块发送处 */
if (lftpd_inet_write(socket, batch, len) < 0) {
return -1;
}
*p_blen = 0;
return 0;
}
static int send_list(int socket, const char *path)
{
static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n";
static const char *file_format = "-rw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n";
void *dp = lftpd_io_opendir(path);
if (dp == NULL) {
return -1;
}
static char name[256]; /* static: off-stack, single-client ftpTask */
uint32_t size;
int is_dir;
static char batch[1536]; /* static: off-stack */
int blen = 0;
int fail = 0;
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
int n;
if (is_dir) {
n = snprintf(batch + blen, sizeof(batch) - blen, directory_format,
(unsigned long)size, name);
} else {
n = snprintf(batch + blen, sizeof(batch) - blen, file_format,
(unsigned long)size, name);
}
if (n < 0) {
fail = 1;
break;
}
if ((size_t)n >= sizeof(batch) - blen) {
if (list_batch_flush(socket, batch, &blen) != 0) {
fail = 1;
break;
}
/* 单行就超过整块 → 明确失败,绝不静默少发条目
* (8.3 短名 + 定长格式远小于 1536,真发生说明格式被改坏) */
if ((size_t)n >= sizeof(batch)) {
fail = 1;
break;
}
if (is_dir) {
n = snprintf(batch, sizeof(batch), directory_format,
(unsigned long)size, name);
} else {
n = snprintf(batch, sizeof(batch), file_format,
(unsigned long)size, name);
}
if (n < 0 || (size_t)n >= sizeof(batch)) {
fail = 1;
break;
}
}
blen += n;
}
lftpd_io_closedir(dp); /* 所有出口都必须走到:卷锁持有至 closedir */
if (!fail && blen > 0) {
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
if (lftpd_inet_write(socket, batch, blen) < 0) {
fail = 1;
}
}
return fail ? -1 : 0;
}
static int send_nlst(int socket, const char *path)
{
void *dp = lftpd_io_opendir(path);
if (dp == NULL) {
return -1;
}
static char name[256]; /* static: off-stack, single-client ftpTask */
uint32_t size;
int is_dir;
static char batch[1536]; /* static: off-stack */
int blen = 0;
int fail = 0;
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
if (!is_dir) {
int n = snprintf(batch + blen, sizeof(batch) - blen, "%s\r\n", name);
if (n < 0) {
fail = 1;
break;
}
if ((size_t)n >= sizeof(batch) - blen) {
if (list_batch_flush(socket, batch, &blen) != 0) { /* 同 send_list:满块续发 */
fail = 1;
break;
}
if ((size_t)n >= sizeof(batch)) {
fail = 1;
break;
}
n = snprintf(batch, sizeof(batch), "%s\r\n", name);
if (n < 0 || (size_t)n >= sizeof(batch)) {
fail = 1;
break;
}
}
blen += n;
}
}
lftpd_io_closedir(dp); /* 所有出口都必须走到:卷锁持有至 closedir */
if (!fail && blen > 0) {
if (lftpd_inet_write(socket, batch, blen) < 0) { /* 成功返回字节数,非 0=成功 */
fail = 1;
}
}
return fail ? -1 : 0;
}
static int send_file(int socket, const char *path)
{
int fh = lftpd_io_open_read(path);
if (fh < 0) {
lftpd_log_error("failed to open file for read");
return -1;
}
static unsigned char buffer[1024]; /* static: 1KB off-stack, single-client ftpTask */
int read_len;
while ((read_len = lftpd_io_read(fh, buffer, sizeof(buffer))) > 0) {
lftpd_task_wd_beat(); /* 看门狗:每分片喂心跳(文件传输分片处,RETR) */
unsigned char *p = buffer;
while (read_len > 0) {
int write_len = lftpd_inet_write(socket, p, read_len);
if (write_len < 0) {
lftpd_log_error("write error");
lftpd_io_close(fh);
return -1;
}
p += write_len;
read_len -= write_len;
}
}
lftpd_io_close(fh);
return 0;
}
static int receive_file(int socket, const char *path)
{
int fh = lftpd_io_open_write(path);
if (fh < 0) {
lftpd_log_error("failed to open file for write");
return -1;
}
static unsigned char buffer[1024]; /* static: 1KB off-stack, single-client ftpTask */
int err;
int total = 0;
while ((err = lftpd_inet_read(socket, buffer, sizeof(buffer))) > 0) {
lftpd_task_wd_beat(); /* 看门狗:每分片喂心跳(文件传输分片处,STOR) */
total += err;
if (lftpd_io_write(fh, buffer, err) != err) {
err = -1;
break;
}
}
lftpd_io_close(fh);
if (err < 0) {
/* 异常结束(连接重置/超时/预算耗尽):删除截断文件防残留并返回错误码
* → STOR 回报 450 而非误报 226(TRAP-NET-18 待办 #3) */
lftpd_io_unlink(path);
return err;
}
/* EOF 正常结束:total==0 是合法空文件上传(如以空内容 STOR 清台账),
* 回报 226 成功(2026-10-02 台架:0 字节 STOR 曾被误判故障并删文件) */
return 0;
}
static int cmd_cwd(lftpd_client_t *client, const char *arg)
{
if (arg == NULL || strlen(arg) == 0) {
send_simple_response(client->socket, 550, STATUS_550);
return -1;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
/* 根目录始终有效,跳过 stat 检查(FatFS FF_FS_RPATH=0 下 stat 根目录可能失败) */
if (strcmp(path, "/") != 0) {
uint32_t size;
int is_dir;
if (lftpd_io_stat(path, &size, &is_dir) != 0 || !is_dir) {
send_simple_response(client->socket, 550, STATUS_550);
vPortFree(path);
return -1;
}
}
strncpy(client->directory, path, sizeof(client->directory) - 1);
client->directory[sizeof(client->directory) - 1] = '\0';
vPortFree(path);
send_simple_response(client->socket, 250, STATUS_250);
return 0;
}
static int cmd_dele(lftpd_client_t *client, const char *arg)
{
/* N-19:写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
const ftp_cfg_t *cfg = ftp_cfg_get();
if ((cfg == NULL) || (cfg->write_en == 0u)) {
send_simple_response(client->socket, 550, STATUS_550);
return 0;
}
if (arg == NULL || strlen(arg) == 0) {
send_simple_response(client->socket, 550, STATUS_550);
return -1;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
uint32_t size;
int is_dir;
if (lftpd_io_stat(path, &size, &is_dir) != 0) {
send_simple_response(client->socket, 550, STATUS_550);
vPortFree(path);
return -1;
}
if (is_dir) {
send_simple_response(client->socket, 550, STATUS_550);
vPortFree(path);
return -1;
}
lftpd_io_unlink(path);
vPortFree(path);
send_simple_response(client->socket, 250, STATUS_250);
return 0;
}
static int cmd_epsv(lftpd_client_t *client, const char *arg)
{
if (client->data_socket >= 0) {
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
}
int listener_socket = lftpd_inet_listen(0);
if (listener_socket < 0) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
int port = lftpd_inet_get_socket_port(listener_socket);
send_simple_response(client->socket, 229, STATUS_229, port);
lftpd_log_debug("EPSV listener on port %d", port);
client->data_socket = listener_socket;
return 0;
}
static int cmd_feat(lftpd_client_t *client, const char *arg)
{
send_multiline_response_begin(client->socket, 211, STATUS_211);
send_multiline_response_line(client->socket, "EPSV");
send_multiline_response_line(client->socket, "PASV");
send_multiline_response_line(client->socket, "SIZE");
send_multiline_response_line(client->socket, "NLST");
send_multiline_response_end(client->socket, 211, STATUS_211);
return 0;
}
static int cmd_list(lftpd_client_t *client, const char *arg)
{
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
__func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
return -1;
}
int err = send_list(data_sock, client->directory);
/* 传输结束必须关闭数据连接:对端以 EOF 判定"数据传完"(LIST/RETR);
* 不关会让客户端一直读到超时(曾试"优雅断开+复用监听器"方案,因该语义限制
* 与芯片行为未能稳定工作,见 链路延迟测试说明.md §8.2)。 */
lftpd_inet_close(data_sock);
client->data_socket = -1;
if (err == 0) {
send_simple_response(client->socket, 226, STATUS_226);
} else {
send_simple_response(client->socket, 550, STATUS_550);
}
return 0;
}
static int cmd_nlst(lftpd_client_t *client, const char *arg)
{
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
__func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
return -1;
}
int err = send_nlst(data_sock, client->directory);
/* 传输结束必须关闭数据连接:对端以 EOF 判定"数据传完"(LIST/RETR);
* 不关会让客户端一直读到超时(曾试"优雅断开+复用监听器"方案,因该语义限制
* 与芯片行为未能稳定工作,见 链路延迟测试说明.md §8.2)。 */
lftpd_inet_close(data_sock);
client->data_socket = -1;
if (err == 0) {
send_simple_response(client->socket, 226, STATUS_226);
} else {
send_simple_response(client->socket, 550, STATUS_550);
}
return 0;
}
static int cmd_noop(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 200, STATUS_200);
return 0;
}
static int cmd_pass(lftpd_client_t *client, const char *arg)
{
const ftp_cfg_t *cfg = ftp_cfg_get();
ftp_auth_creds_t creds;
ftp_auth_result_t res;
if (cfg == NULL) {
/* fail-closed:配置不可用即拒绝(s_cfg 静态常驻,正常不会走到) */
send_simple_response(client->socket, 530, STATUS_530);
return 0;
}
creds.p_user = cfg->user;
creds.p_pwd_hash = cfg->pwd_hash;
creds.write_en = cfg->write_en;
res = ftp_auth_check(&creds, client->auth_user_ok,
(arg != NULL) ? arg : "",
&s_auth_state, (uint32_t)xTaskGetTickCount());
if (res == FTP_AUTH_OK) {
client->auth_logged_in = 1;
send_simple_response(client->socket, 230, STATUS_230);
return 0;
}
/* 预期事件(爆破/配错口令):用 INFO 而非 ERROR——[ERR] 是 @test Forbidden 标记 */
if (res == FTP_AUTH_LOCKED) {
lftpd_log_info("auth locked after %u failures", (unsigned)FTP_AUTH_FAIL_MAX);
} else {
lftpd_log_info("auth failed for user '%s'", client->auth_user);
}
send_simple_response(client->socket, 530, STATUS_530);
return 0;
}
static int cmd_pasv(lftpd_client_t *client, const char *arg)
{
if (client->data_socket >= 0) {
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
}
/* 原 3×200ms 应用层重试已删(lftpd_inet_listen 单次化)——
* 失败直接 425,FTP 客户端自带传输重试语义;服务端不再叠阻塞重试 */
int listener_socket = lftpd_inet_listen(0);
if (listener_socket < 0) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
int port = lftpd_inet_get_socket_port(listener_socket);
{
struct net_sockaddr_in local_addr;
int addrlen = sizeof(local_addr);
int err = net_getsockname(client->socket,
(struct net_sockaddr *)&local_addr, &addrlen);
if (err != 0) {
lftpd_log_error("error getting client IP info");
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(listener_socket);
return -1;
}
uint32_t ip = net_htonl(local_addr.sin_addr.s_addr);
send_simple_response(client->socket, 227, STATUS_227,
(ip >> 24) & 0xff,
(ip >> 16) & 0xff,
(ip >> 8) & 0xff,
(ip >> 0) & 0xff,
(port >> 8) & 0xff, (port >> 0) & 0xff);
}
lftpd_log_debug("PASV listener on port %d", port);
client->data_socket = listener_socket;
return 0;
}
static int cmd_pwd(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 257, "\"%s\"", client->directory);
return 0;
}
static int cmd_quit(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 221, STATUS_221);
return -1;
}
static int cmd_retr(lftpd_client_t *client, const char *arg)
{
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
__func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
return -1;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
lftpd_log_debug("send '%s'", path);
int err = send_file(data_sock, path);
/* 传输结束必须关闭数据连接:对端以 EOF 判定"数据传完"(LIST/RETR);
* 不关会让客户端一直读到超时(曾试"优雅断开+复用监听器"方案,因该语义限制
* 与芯片行为未能稳定工作,见 链路延迟测试说明.md §8.2)。 */
lftpd_inet_close(data_sock);
client->data_socket = -1;
if (err == 0) {
lftpd_log_info("DOWNLOAD '%s' OK", path);
send_simple_response(client->socket, 226, STATUS_226);
} else {
lftpd_log_error("DOWNLOAD '%s' FAILED", path);
send_simple_response(client->socket, 450, STATUS_450);
}
vPortFree(path);
return 0;
}
static int cmd_size(lftpd_client_t *client, const char *arg)
{
if (arg == NULL) {
send_simple_response(client->socket, 550, STATUS_550);
return 0;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
lftpd_log_debug("size %s", path);
uint32_t size;
int is_dir;
if (lftpd_io_stat(path, &size, &is_dir) == 0 && !is_dir) {
send_simple_response(client->socket, 213, "%lu", (unsigned long)size);
} else {
send_simple_response(client->socket, 550, STATUS_550);
}
vPortFree(path);
return 0;
}
static int cmd_stor(lftpd_client_t *client, const char *arg)
{
/* N-19:写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
const ftp_cfg_t *cfg = ftp_cfg_get();
if ((cfg == NULL) || (cfg->write_en == 0u)) {
send_simple_response(client->socket, 550, STATUS_550);
return 0;
}
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
__func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
return -1;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
lftpd_log_debug("receive '%s'", path);
int err = receive_file(data_sock, path);
/* 传输结束必须关闭数据连接:对端以 EOF 判定"数据传完"(LIST/RETR);
* 不关会让客户端一直读到超时(曾试"优雅断开+复用监听器"方案,因该语义限制
* 与芯片行为未能稳定工作,见 链路延迟测试说明.md §8.2)。 */
lftpd_inet_close(data_sock);
client->data_socket = -1;
if (err == 0) {
uint32_t size = 0;
int is_dir = 0;
lftpd_io_stat(path, &size, &is_dir);
lftpd_log_info("UPLOAD '%s' %lu bytes OK", path, (unsigned long)size);
send_simple_response(client->socket, 226, STATUS_226);
} else {
lftpd_log_error("UPLOAD '%s' FAILED", path);
send_simple_response(client->socket, 450, STATUS_450);
}
vPortFree(path);
return 0;
}
static int cmd_syst(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 215, "UNIX Type: L8");
return 0;
}
static int cmd_type(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 200, STATUS_200);
return 0;
}
static int cmd_user(lftpd_client_t *client, const char *arg)
{
const ftp_cfg_t *cfg = ftp_cfg_get();
client->auth_logged_in = 0;
client->auth_user[0] = '\0';
client->auth_user_ok = 0;
if ((arg != NULL) && (cfg != NULL)) {
strncpy(client->auth_user, arg, sizeof(client->auth_user) - 1);
client->auth_user[sizeof(client->auth_user) - 1] = '\0';
client->auth_user_ok = (uint8_t)ftp_auth_user_match(cfg->user,
client->auth_user);
}
/* 不论用户名对错一律 331:不泄露"用户是否存在";错用户名在 cmd_pass
* 仍走一次同耗时哈希比较(时间拉平,见 lftpd_auth.h 说明 2) */
send_simple_response(client->socket, 331, STATUS_331);
return 0;
}
static int handle_control_channel(lftpd_client_t *client)
{
int err = send_simple_response(client->socket, 220, STATUS_220);
if (err != 0) {
lftpd_log_peer("peer gone before welcome acked");
goto cleanup;
}
static char read_buffer[512]; /* static: off-stack, single-client ftpTask */
while (err == 0) {
int line_len = lftpd_inet_read_line(client->socket, read_buffer,
sizeof(read_buffer));
if (line_len != 0) {
/* 绝大多数是客户端正常收尾(关窗口 / QUIT 前先断了 TCP);
* 真读错也由返回值区分得出来,留 ret=%d 便于排障 */
lftpd_log_peer("control channel closed by peer (ret=%d)", line_len);
goto cleanup;
}
int index;
char *p = strchr(read_buffer, ' ');
if (p != NULL) {
index = (int)(p - read_buffer);
} else {
index = (int)strlen(read_buffer);
}
if (index >= 5) {
err = send_simple_response(client->socket, 500, STATUS_500);
continue;
}
char command_tmp[4 + 1];
memset(command_tmp, 0, sizeof(command_tmp));
memcpy(command_tmp, read_buffer, (size_t)index);
for (int i = 0; command_tmp[i]; i++) {
command_tmp[i] = (char)toupper((int)command_tmp[i]);
}
/* N-19:未鉴权命令门——仅 QUIT/FEAT/SYST/NOOP 放行(客户端兼容需要,
* 且无信息量);其余一律 530。USER/PASS 本身也在放行名单外,
* 但它们在 gate 之前不可能通过 → 单独放行见下(USER/PASS 例外) */
if (client->auth_logged_in == 0) {
if ((strcmp(command_tmp, "USER") != 0) &&
(strcmp(command_tmp, "PASS") != 0) &&
(strcmp(command_tmp, "QUIT") != 0) &&
(strcmp(command_tmp, "FEAT") != 0) &&
(strcmp(command_tmp, "SYST") != 0) &&
(strcmp(command_tmp, "NOOP") != 0)) {
send_simple_response(client->socket, 530, STATUS_530);
continue;
}
}
bool matched = false;
for (int i = 0; commands[i].command; i++) {
if (strcmp(commands[i].command, command_tmp) == 0) {
static char arg_buf[512]; /* static: off-stack, single-client ftpTask */
const char *arg = NULL;
if (index < (int)strlen(read_buffer)) {
strncpy(arg_buf, read_buffer + index + 1, sizeof(arg_buf) - 1);
arg_buf[sizeof(arg_buf) - 1] = '\0';
arg = lftpd_string_ltrim(arg_buf);
}
lftpd_log_info("CMD %s %s", command_tmp, arg ? arg : "");
err = commands[i].handler(client, arg);
matched = true;
break;
}
}
if (!matched) {
send_simple_response(client->socket, 502, STATUS_502);
}
}
cleanup:
/* 数据监听器在复用模式下会跨命令保留,会话结束必须显式释放(否则每次会话泄漏一个 Socket) */
if (client->data_socket >= 0) {
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
}
/* 控制通道不得 close:单连接模型下它即监听器(fd 与监听一体),EOF/断连时
* net 层已 auto_relisten 回 LISTEN;此处 close 会连监听一起毁掉并触发
* close→重建竞态(2026-09-24 台架 F1:快速重连在芯片沉降窗内被拒)。
* 直接返回外层循环,在同一监听 fd 上再次 accept。 */
return 0;
}
int lftpd_start(const char *directory, int port, lftpd_t *lftpd)
{
memset(lftpd, 0, sizeof(lftpd_t));
lftpd->directory = directory;
lftpd->port = port;
lftpd->server_socket = -1; /* memset 清零后显式置"无监听器"(0 是合法 fd) */
/* N-19:出厂默认口令在用告警(台架锚点;改了 [ftp] pwd_hash 即消失) */
if (ftp_cfg_is_default_pwd() != 0) {
lftpd_log_info("using factory default password");
}
while (true) {
lftpd_task_wd_beat(); /* 看门狗:事件循环每轮喂心跳(ftpTask 主循环节拍点) */
if (lftpd->server_socket < 0) {
/* 首次启动 / 网络重建后:全量创建监听器(socket+bind+listen) */
lftpd->server_socket = lftpd_inet_listen(port);
if (lftpd->server_socket < 0) {
lftpd_log_error("error creating listener");
/* 看门狗:3s 重试等待拆短片喂心跳(单次 3s 挂起超 IWDG 最坏窗
* ~1.7s → 误复位;与 time_sync.c 监听重试同模式) */
uint32_t wd_left = 3000U;
while (wd_left >= LFTPD_WAIT_SLICE_MS) {
lftpd_task_wd_beat();
vTaskDelay(pdMS_TO_TICKS(LFTPD_WAIT_SLICE_MS));
wd_left -= LFTPD_WAIT_SLICE_MS;
}
continue;
}
}
/* 后续会话:单连接模型下控制通道 fd 即监听器 fd,会话断连后 net 层已
* auto_relisten 回 LISTEN(同 fd,实测 ~50ms,D10 机制),直接复用;
* 不得 close→重建——close 会毁掉已就绪的监听器,重建的 open 落在芯片
* 沉降窗内返回假 OK → 窗内快速重连被拒(2026-09-24 台架 F1 发现;
* 铁律:EOF 时应用层不得 net_close 监听 fd)。 */
lftpd_log_info("waiting for connection...");
int client_socket = lftpd_inet_accept(lftpd->server_socket);
if (client_socket < 0) {
lftpd_log_peer("listener rebuilt (reason logged by accept layer)");
lftpd_inet_close(lftpd->server_socket);
lftpd->server_socket = -1; /* 监听器已不可用(网络重建):下轮重建 */
continue;
}
{
struct net_sockaddr_in addr;
int addrlen = sizeof(addr);
/* E13 定案 修复:原用 getsockname 打印的是**本机** IP:21(且
* ntoa 段序反转成 "100.1.168.192")——改 getpeername 打印真实
* 对端(现场排障需要"谁连进来");未连接则退回无地址形态 */
if (net_getpeername(client_socket,
(struct net_sockaddr *)&addr, &addrlen) == 0) {
char ip_str[16];
net_inet_ntoa(addr.sin_addr.s_addr, ip_str);
/* 对端端口:直接取 getpeername 结果(原 get_socket_port
* 走 getsockname,打印的是本机监听端口) */
int remote_port = (int)net_ntohs(addr.sin_port);
lftpd_log_info("connection received from %s:%d...",
ip_str, remote_port);
} else {
lftpd_log_info("connection received...");
}
}
static lftpd_client_t client; /* static: off-stack session context (single session) */
memset(&client, 0, sizeof(client));
strncpy(client.directory, directory, sizeof(client.directory) - 1);
client.directory[sizeof(client.directory) - 1] = '\0';
client.socket = client_socket;
client.data_socket = -1;
lftpd->client = &client;
handle_control_channel(&client);
lftpd->client = NULL;
/* 控制 Socket 不关闭:断连时 net 层已 auto_relisten(同 fd),
* 外层循环直接复用同一监听 fd 再次 accept */
}
}
int lftpd_stop(lftpd_t *lftpd)
{
if (lftpd->server_socket >= 0) {
lftpd_inet_close(lftpd->server_socket);
}
if (lftpd->client) {
lftpd_inet_close(lftpd->client->socket);
}
return 0;
}