Files
STM32F4-Base/Drivers/BSP/NET/lftpd/lftpd.c
T
wandering1 8b0ea17b71 fix(lftpd): LIST/NLST 满块续发——大目录不再被静默截断并回 550
现象:`ftp_test.py` 的 `LIST 150->226` 断言失败——1512 B 数据全收到了,完成回包却是 **550**。

**根因**(`send_list`/`send_nlst` 各一处):定案三把"整份目录一次性批量发送"落进 static 1536 B
缓冲时,顺手留下了"装满就 `break` 并回 −1",`cmd_list` 据此回 550。于是目录一旦超过一块,客户端
拿到的是**静默截断**的清单**外加一个错误码**——而清单看起来完全像"目录里就这些"。这比单纯报错
更危险:没有人会去数条目个数。

**台架实测(同一台设备、同一个根目录,改动前后各测一次)**:
· 改前:1512 B / **24 条目** / 完成回包 **550**(停在 A0007.BIN,后面 6 个文件被吞)
· 改后:1880 B / **30 条目** / 完成回包 **226**

**修法:满块续发**,而不是把缓冲区加大。新增 `list_batch_flush()`——装满一块就整块发出、清空
继续装,收尾再发最后一块。为什么仍要"批量":定案三的本体是实测结论——同一连接上背靠背 ~60 B
小段写会毒化 CH395F 的 TX(芯片报 SEND_OK 但对端全丢,无 RST 无 DISCONNECT),而 RETR 的 1 KB
满块连发安全;所以这里只发大块,绝不退回"每行一发"。单行真装不下时**明确失败**而不是少发条目
(8.3 短名 + 定长格式远小于 1536,真发生说明格式被改坏)。两条出口的 `lftpd_io_closedir` 收敛到
循环后单点:卷锁持有至 closedir,任何提前 return 都会漏锁。

**防回退**(关键):修完之后,**目录清空的设备上这颗雷再也暴露不出来**,所以补了
`test/host/ftp_list_fill_test.py`——自造 26 个 8.3 短名文件把清单撑过一块,三条判据(STOR 全 226
/LIST 全文里每个名字都在/完成回包 226 而非 550,用 ftplib `retrlines` 让非 2xx 完成直接抛),
跑完逐个 DELE 还原。真机实测 `收到 56 行 / 3544 字节`(跨 3 块,满块续发被真正走到)→ 三条全
PASS。用法登记进 `docs/测试说明/FTP_Test_Guide.md` §4.5,机理与增补写进
`docs/问题记录/CH395F_Trap_Records.md` 定案三。

**顺带记录的已知偏差(未修)**:`cmd_list`/`cmd_nlst` 忽略命令参数、恒列会话 CWD
(`client->directory`)。主流客户端都是 `CWD` + 裸 `LIST`,实际影响小,先记在案。

体量:`Code` 213,264 → **213,436 B(+172)**、RO 不变 → `ER_IROM1` **232,088 B(22.1%)
  **、
`ROM 合计` 233,276 B;**RAM 一字未动**(`RW_IRAM1` 75,264 B 57.4% / `RW_CCM` 57,376 B
  87.9%)。
数字联动照做:`data/ram_report` 重跑、`ram_baseline --update/--write`、
  `check_mem_numbers --write`、
FLASH 配图常量重算重生成(桶合计 = 232,088 与 `Code+RO` 相符)、报告 §1.3/§5.1/§5.4 与部署 §3.3。

门禁:`@build` 0 错 0 警告;`@test` PASS(692 ms);`ftp_test.py` → **TEST
  PASSED**(含此前失败
的 `LIST 150->226`);`ftp_up_dl_test.py` → TEST PASSED;`ftp_list_fill_test.py`
  → TEST PASSED;
`doc-links --all` 146 文档 0 问题;`id-check --strict` 增量拦截通过(可判定 82.82%);
`mem-numbers` / `mem-budget` 通过;改动文件字节卫生 BOM 无 / CR 0 / U+FFFD 0 / 无行尾空格。未推送。
2026-10-22 22:41:31 +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_trim(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;
}