5a806d8a26
用户裁定「全部修」。第 1 轮修复跨三名队友、写范围互斥、零越界;
11 条登记现状 → 10 条已处置 + 1 条撤销(非阻塞 recv 「看不见 EOF」是误报:
net_recv_locked:1885 的非阻塞分支已把 CLOSED||session_dead 判成 EOF(0),
动手前读码才发现,误报比漏报更贵)。
存储与日志(App/log/sys_log_mcu.c、soe_store.c、cfg_file_store_mcu.c、sys_log.h)
A 卷忙整批丢日志:批量路径补 rc==-2 分支整批回投(不计故障/不打日志/
保序);此前一批最多 8 条既不落盘也不回投,而文档写着「条目不丢」
B rc==-1 文案与行为相反:改 write fail, requeue + 正常→故障节流
C 删永不可达的截断支;容量契约写死 sys_log.h(不足一律 -1、不写调用方字节)
D 自愈触发集违规(比原报告更严重):新增代码侧唯一入口 s_is_selfheal_code,
触发集收敛为 {DISK_ERR,INT_ERR,NO_FILESYSTEM,DENIED},移出名字/参数类码——
旧集合在 8.3 同名同因下每轮删一次自己的历史日志且永不自愈,正是存储架构
§10.1 第 10 条严禁的归集;soe_store 侧同源取更保守子集(打开失败绝不 unlink)
E 注释里的 FR_NO_SYSTEM 不存在,改 FR_NOT_READY / FR_NO_FILESYSTEM
F syslog_post 满态留痕节流(A 修好后「队列常驻满」成必然场景,不修等于用
新刷屏换掉静默丢);丢弃语义一字未改
NET(net_socket.c/.h、net_types.h、net_socket_sim.c、lftpd.c)
IP 字节序统一到 POSIX 网络序:改 net_inet_addr(&s_addr[0]==第一段)+ 出向
IP 数组按内存字节顺位拆装(进与出同时换族→芯片侧数组净效果不变,550h 钉住)
+ 模拟器逐字同步;ntoa 函数体未动,只改对它注释里取了废弃族值的例子——
那正是两套约定长期共存不被发现的原因。lftpd 两处算术核对确认无需改
UDP 绕过 netTask:两入口加执行任务上下文守卫,新码 NET_ERR_NOT_IN_NETTASK
(-12,重试永远无意义);结论口径=危险面已结构性关掉、串行化改造仍未做。
附带关掉第二条更隐蔽的路:非 netTask 自旋 recvfrom 期间控制块被 close 并
把同一 sockfd 分给新会话→旧自旋者消费新会话数据报
控制台(console_log.c/.h):冷却改独立 s_con_cooling 标志(旧写法在 tick==0 那
拍冷却等于没记);con_append 的判空位+推进 head+整行拷贝收进同一临界区(旧
为区外 check-then-act,环 >95% 满时可静默覆盖且既不打 [ERR] 也不计 dropped);
Instance==NULL 与僵尸通道分诊(未初始化绝不 reinit);三本账分离 + 新读口
dbg_console_tx_timeout();[ERR] 行改为发成功才销账
测试:net_socket 452、console_log 127、sys_log_mcu 104、soe_store 27、
cfg_file_store_mcu 34;回归 net_listener 34 / net_events 34 / net_pending 43 /
net_select 22 / lftpd 35 / lftpd_inet 28 / lftpd_io 33 / iec104_serv 28 /
nand_port 149 / vol_lock 33 / soe 32 / cfg_file 151 全绿。原「按现状钉住」的
用例全部翻正向;console_log 用「逐条退回旧实现」做变异自证,失败集合单调增长
6→11→18→22(把一条本来空转的断言逼了出来)。
游标抬升:TC-NETSOCK-545→560、TC-SYSLOG-100→107、TC-CONLOG-135→167。
文档:控制台设计说明 §4.1 口径与 §13-O2 销项 + v1.5;运行与自检记录 §4.4/§4.5
四条;存储架构 §10.1 第 10 条推论(指向 s_is_selfheal_code);TRAP-NET-21 后续
条目;BSD 指南新增 §9.0 s_addr 唯一约定;覆盖率口径 §9「第 1 轮修复处置」表 +
后续项 5 条 + 台架复验 7 条 + 新坑 8 条 + 变更记录。
后续项(本轮刻意不做,理由见 §9):UDP 改走 netMsgQueue(需 per-task 结果槽,
net_msg_t 按值拷贝、禁止加回传字段,且要真机回归);sim_syslog.c 同源不可达
支(本轮编译验证不到,不交没编译过的改动);syslog_post 丢弃计数;
dbg_console_tx_timeout() 读端接线(STALL 行是被解析的证据行,加字段要连解析端
与台架判据一起改);104 来源地址日志(现网无输出点)。
gate: @build 0 errors 0 warnings(Code=213720 RO=18860 RW=1204 ZI=131500)。
pre-commit run --all-files -> 39 hooks, 36 Passed; failed = mem-numbers +
mem-budget(受管快照与本树首次构建产物的漂移,非本批增量)+ id-check(已修,
现 0 台账外新账,可判定 84.63% > 基线 82.6%)。实测水位 RW_IRAM1 57.4%、
RW_CCM 87.9%,均低于 92% 红线;RAM/Flash 本批增量为数个 static 变量。
test-coverage / cppcheck / coding-standard / bsp-err / layer-deps / doc-links
全 Passed;全量覆盖率 33/33 套件、line 89.3%、func 92.3%、逐文件下降 0。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
905 lines
32 KiB
C
905 lines
32 KiB
C
#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;
|
||
}
|
||
|
||
/* 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)
|
||
{
|
||
/* 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;
|
||
}
|