Files
STM32F4-Base/Drivers/BSP/NET/lftpd/lftpd.c
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号)
一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、
采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。

门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏
undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8
reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF;
id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。

test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字,
是本批唯一改变构建产物的文件集合。

为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged
部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、
不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。

实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01%
(基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 /
告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。

基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处
引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使
识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。

⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二
根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有),
今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。

固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0;
App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889
断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿;
check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带
BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。

.cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法:
Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。

同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%,
RW_CCM 57,376 B / 87.9%,阈值 92%。
2026-10-26 12:28:14 +08:00

905 lines
32 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 },
};
/* 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)
{
/* 写命令门——[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;
}
/* s_addr 是网络序整数(本层唯一约定,见 net_inet_addr 的声明处),这里
* 先转主机序再按高字节到低字节拆 = 线序顺位,PASV 227 的 IP 文本因此正确
* (TRAP-NET-21 已定案"双重反转恰好正确,勿误判";2026-10-24 字节序统一后复核:
* 本行不属于被统一掉的那一族,语义未变,勿"顺手改对"成直接拆 s_addr) */
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)
{
/* 写命令门——[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]);
}
/* 未鉴权命令门——仅 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) */
/* 出厂默认口令在用告警(台架锚点;改了 [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;
}