# ftp **Repository Path**: RocsSun/ftp ## Basic Information - **Project Name**: ftp - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-27 - **Last Updated**: 2026-10-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RTKO_FTP 一个可嵌入主项目的 **FTP + SFTP 上传测试模块**: - **FTP**:纯 C++17 原生实现(控制连接 + 数据连接),不依赖第三方 FTP 库 - **SFTP**:基于主项目 `core/libssh` 中的 [libssh](core/libssh) 实现 > 模块源码位于 `src/ftp/`,公共头文件位于主项目根目录的 `include/rtko_ftp/`(与 `src/` 平级)。 > 命令行参数由主项目自行解析,模块通过 `rtko::ftp::start()` 接收配置。 > 仓库根目录的 `src/main.cc` 只是一个演示宿主,展示如何解析参数并调用模块。 ## 目录结构 ```text include/ rtko_ftp/ 公共头文件(与 src 平级,主项目只需包含这里) ftp_module.h 公共 API:Config / start / stop / running transfer_event.h 传输完成回调类型 src/ main.cc 演示宿主:解析 gflags 并调用模块 app_signal.{h,cc} 演示宿主的 SIGINT/SIGTERM 处理 ftp/ 可复用的 FTP/SFTP 模块(实现) CMakeLists.txt 构建静态库 rtko_ftp(别名 rtko::ftp) ftp_module.cc ftp_server.{h,cc} sftp_server.{h,cc} app_log / fs_util / net_util / path_sandbox / transfer_notify ``` 依赖不放在模块里,而是由主项目的 `core/` 提供,例如 `core/libssh`、`core/gflags`。 ## 集成到主项目 把 `src/ftp` 放到主项目的 `src/ftp`,把 `include/rtko_ftp` 放到主项目的 `include/rtko_ftp`,然后在主项目 CMake 中: ```cmake add_subdirectory(src/ftp) target_link_libraries( PRIVATE rtko::ftp) ``` 模块默认使用主项目根目录的 `include/` 作为公共头文件目录;如果主项目的头文件目录不同: ```cmake cmake -DRTKO_FTP_INCLUDE_DIR=/path/to/main/include ... ``` 模块默认从 `${PROJECT_SOURCE_DIR}/core/libssh` 读取 libssh;如果主项目的 core 目录不同: ```cmake cmake -DRTKO_FTP_CORE_DIR=/path/to/main/core ... ``` 主项目负责参数解析,只把解析后的值传给模块。`start()` 是阻塞的,主项目通过一个线程承载它: ```cpp #include #include "rtko_ftp/ftp_module.h" rtko::ftp::Config config; config.bind_ip = "0.0.0.0"; config.ftp_port = 2121; // 0 表示禁用 FTP config.sftp_port = 2222; // 0 表示禁用 SFTP config.root_path = "/data/uploads"; config.username = "ftp"; config.password = "ftp"; config.sftp_host_key_path.clear(); // 空表示生成临时主机密钥 config.on_transfer_complete = [](const rtko::TransferEvent &event) { // 发送通知;可能被多个连接线程并发调用 }; std::string error; std::thread ftp_thread([&]() { if (!rtko::ftp::start(config, &error)) { // 启动失败,或启动期间被 stop() } }); // 主线程继续做自己的事情;需要退出时: rtko::ftp::stop(); ftp_thread.join(); ``` - `start()` 一直运行到 `stop()` 被调用后返回;重复调用会失败。 - 同时启用 FTP 和 SFTP 时,调用线程运行 FTP,SFTP 使用一个内部辅助线程。 - `stop()` 可由其他线程调用;不要在信号处理函数里直接调用它(先在信号处理函数里置标志,再从普通上下文调用)。 - 启动失败时 `start()` 会尽快返回 `false`,主线程 `join()` 后即可读取 `error`。 ## 功能 ### 通用 - 监听 IP、FTP/SFTP 端口、上传根目录、用户名、密码都由主项目解析后通过 `rtko::ftp::Config` 传入 - FTP 与 SFTP 可通过 `enable_ftp` / `enable_sftp` 单独启用或同时启用 (至少启用一个;端口设为 `0` 也可关闭对应服务) - FTP 与 SFTP 使用同一个 `root_path` 根目录和同一组账号密码 - **上传路径保留**:实际落盘路径为 `root_path / 客户端路径 / 文件名`,缺失的父目录自动创建 - 所有路径都被限制在根目录内(拒绝 `..` 逃逸和符号链接逃逸) - 仅用于上传测试:FTP 的 `RETR`、SFTP 的 `SSH_FXP_READ` 都会被拒绝 - 传输结束支持注册回调(`TransferCallback`),可用于发送通知 - 演示宿主支持 `Ctrl+C` 优雅退出;即使正在上传也能立即停止(主项目可自行控制 `stop()` 时机) - 每个连接一个线程,日志带时间戳和客户端地址 ### FTP - `PASV` / `EPSV` / `PORT` / `EPRT` - `STOR` / `APPE` / `REST`(断点续传) - `PWD/CWD/CDUP`、`MKD/RMD/DELE`、`RNFR/RNTO` - `LIST/NLST/MLSD`、`SIZE/MDTM/MLST`、`FEAT/OPTS UTF8` - 未登录时只开放少量命令 ### SFTP(libssh) - 密码认证的 SSH 会话,只接受 `sftp` 子系统(不提供 shell/exec) - 上传:`OPEN`(写/创建/截断/追加)、`WRITE`(支持 offset)、`CLOSE` - 续传:`reput` 使用的 `APPEND` 打开方式可用 - 目录与文件:`OPENDIR/READDIR`、`STAT/LSTAT/FSTAT`、`MKDIR/RMDIR/REMOVE`、 `RENAME`、`SETSTAT/FSETSTAT`(权限、时间、截断) - 兼容 `posix-rename@openssh.com` - 拒绝 `READ`(下载)、`SYMLINK`、`READLINK` ## 构建(演示宿主) 需要: - CMake ≥ 3.16、支持 C++17 的编译器(Linux 建议 GCC 9+ / Clang 10+,因为使用了 `std::filesystem`) - OpenSSL ≥ 1.1.1、zlib、pthread(libssh 的依赖) - 主项目 `core/libssh`(演示宿主还需要 `core/gflags`;系统已安装 gflags 时可不提供) > `core/` 与旧版 `lib/` 一样被 `.gitignore` 忽略,依赖源码由主项目统一维护。 ### macOS ```bash brew install cmake gflags openssl zlib cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j ``` ### Linux Debian / Ubuntu: ```bash sudo apt install build-essential cmake libssl-dev zlib1g-dev # 可选:使用系统 gflags,否则自动使用 core/gflags sudo apt install libgflags-dev # 可选:SFTP 冒烟测试依赖 sudo apt install expect curl cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j ``` Fedora / RHEL: ```bash sudo dnf install gcc-c++ cmake openssl-devel zlib-devel # 可选:sudo dnf install gflags-devel expect curl cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j ``` 构建时自动处理了: - 系统没有 gflags 时,演示宿主使用 `core/gflags`(已设置 `GFLAGS_IS_SUBPROJECT`) - GCC 8 会自动链接 `stdc++fs`,GCC 9+ 不需要 - libssh 以静态库构建(`WITH_SERVER=ON`、`WITH_SFTP=ON`),不构建示例/测试/文档 - 模块产出静态库 `librtko_ftp.a`,主项目链接目标为 `rtko::ftp` 可执行文件:`build/bin/RTKO_FTP`(仅用于演示)。 ## 演示宿主运行 ```bash # 同时启动 FTP + SFTP(默认) ./build/bin/RTKO_FTP \ --ip=0.0.0.0 \ --port=2121 \ --sftp_port=2222 \ --path=/tmp/ftp_upload \ --username=ftp \ --password=ftp # 只启动 FTP ./build/bin/RTKO_FTP --enable_sftp=false \ --ip=0.0.0.0 --port=2121 \ --path=/tmp/ftp_upload --username=ftp --password=ftp # 只启动 SFTP ./build/bin/RTKO_FTP --enable_ftp=false \ --ip=0.0.0.0 --sftp_port=2222 \ --path=/tmp/ftp_upload --username=ftp --password=ftp ``` 参数: | 参数 | 默认值 | 说明 | | --- | --- | --- | | `--ip` | `0.0.0.0` | FTP/SFTP 监听地址 | | `--enable_ftp` | `true` | 是否启用 FTP;`--enable_ftp=false` 可关闭 | | `--enable_sftp` | `true` | 是否启用 SFTP;`--enable_sftp=false` 可关闭 | | `--port` | `2121` | FTP 控制端口;设为 `0` 等同于禁用 FTP | | `--sftp_port` | `2222` | SFTP 端口;设为 `0` 等同于禁用 SFTP | | `--path` | 必填 | 上传根目录,不存在时自动创建 | | `--username` | `ftp` | 登录用户名 | | `--password` | `ftp` | 登录密码 | | `--sftp_host_key` | 空 | SFTP 主机私钥路径;为空时每次启动生成临时 RSA-3072 密钥 | gflags 参数使用 `--flag=value` 形式(注意端口参数名是下划线:`--sftp_port`)。 布尔参数可写 `--enable_ftp=false` 或 `--noenable_ftp`。 两个服务都关闭(例如 `--enable_ftp=false --enable_sftp=false`,或端口都为 `0`)时程序会报错退出。 启动后会打印实际监听地址、根目录、哪些服务被启用,以及临时主机密钥提示。 ### 上传路径规则 假设 `--path=/data`: | 客户端上传命令 | 实际落盘路径 | | --- | --- | | FTP `STOR a/b/x.txt` | `/data/a/b/x.txt` | | FTP `STOR /a/b/x.txt` | `/data/a/b/x.txt` | | SFTP `put x.txt /a/b/x.txt` | `/data/a/b/x.txt` | | SFTP `put x.txt a/b/x.txt` | `/data/a/b/x.txt` | - 客户端给出的目录部分会原样保留在 `--path` 下 - 中间目录不存在时会自动递归创建(FTP `STOR/APPE`、SFTP 写 `OPEN`) - `..` 会被归一化,最终路径不会超出 `--path` - OpenSSH `sftp` 的 `put local` 如果不指定远端路径,远端文件名默认取本地文件名(这是客户端行为) ### SFTP 主机密钥 默认每次启动生成**临时 RSA 主机密钥**,因此客户端会看到 host key 每次变化。 测试客户端应关闭 host key 校验(例如 `StrictHostKeyChecking=no` + `UserKnownHostsFile=/dev/null`)。 需要固定密钥时: ```bash ssh-keygen -t ed25519 -f /tmp/rtko_sftp_host_key -N '' ./build/bin/RTKO_FTP ... --sftp_host_key=/tmp/rtko_sftp_host_key ``` > 密码通过命令行传入会出现在进程列表(`ps`)中,仅建议在本地/隔离测试环境使用。 ### 传输完成回调 两个服务都支持在文件传输结束时触发回调,适合用来发通知(webhook、邮件、消息队列等)。 ```cpp #include "rtko_ftp/ftp_module.h" void on_transfer_complete(const rtko::TransferEvent &event) { // event.protocol : "FTP" / "SFTP" // event.operation : "STOR" / "APPE" / "WRITE" // event.path : 相对于 root_path 的虚拟路径 // event.local_path : 实际落盘路径 // event.peer : 客户端地址 // event.bytes : 本次传输字节数 // event.success : 是否成功 // event.error : 失败原因(success 为 false 时) } rtko::ftp::Config config; config.root_path = "/data/uploads"; config.on_transfer_complete = on_transfer_complete; std::string error; if (!rtko::ftp::start(config, &error)) { // 处理 error } ``` 触发时机: - FTP:`STOR` / `APPE` 结束,文件已关闭;成功和失败都会触发(失败时 `success=false`) - SFTP:写句柄 `CLOSE` 且文件已关闭后触发;客户端未发送 `CLOSE` 就断开的会话不会触发 注意事项: - 回调在连接线程中执行,可能被多个连接并发调用,实现必须线程安全 - 回调里不要长时间阻塞,否则会拖慢对应连接;耗时的通知建议转交队列/后台线程 - 回调抛出的异常会被记录并吞掉,不会影响传输会话 - 建议在 `start()` 之前设置回调 `src/main.cc` 中的 `on_transfer_complete` 是一个会打印 `[notify] ...` 日志的示例实现,可直接替换成实际的通知逻辑。 ## 客户端示例 ### FTP ```bash # 上传(默认使用 EPSV) curl -T local.bin ftp://127.0.0.1:2121/remote.bin --user ftp:ftp # 强制使用 PASV curl --disable-epsv -T local.bin ftp://127.0.0.1:2121/remote.bin --user ftp:ftp # 主动模式 curl -P - -T local.bin ftp://127.0.0.1:2121/remote.bin --user ftp:ftp # 自动创建远端目录 curl --ftp-create-dirs -T local.bin ftp://127.0.0.1:2121/a/b/c.bin --user ftp:ftp # 追加写入 curl --append -T local.bin ftp://127.0.0.1:2121/remote.bin --user ftp:ftp # 列出目录 curl ftp://127.0.0.1:2121/ --user ftp:ftp ``` ### SFTP OpenSSH 客户端: ```bash sftp -P 2222 \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ ftp@127.0.0.1 ``` 进入交互界面后: ```text sftp> put local.bin /remote.bin sftp> mkdir /uploads sftp> put local.bin /uploads/remote.bin sftp> ls -l / sftp> rename /remote.bin /renamed.bin sftp> rm /renamed.bin sftp> bye ``` `get` 会被拒绝: ```text sftp> get /remote.bin remote open "/remote.bin": Permission denied ``` FileZilla / WinSCP 选择 **SFTP** 协议,主机 `127.0.0.1`、端口 `2222`、 用户名/密码同上;首次连接需要确认或跳过 host key。 ## 冒烟测试 ### FTP ```bash sh scripts/smoke_test.sh ``` 覆盖 EPSV / PASV / 主动模式上传、自动建目录、上传路径保留与父目录自动创建(需要 `python3`)、 `APPE`、错误密码、目录列表、curl 断点续传(`SIZE + APPE`)、上传中 SIGINT 退出。 脚本使用 `--sftp_port=0`,只测试 FTP。 ### SFTP ```bash sh scripts/sftp_smoke_test.sh ``` 依赖 OpenSSH `sftp` 和 `expect`(macOS 自带;Debian/Ubuntu:`sudo apt install expect`)。覆盖: - FTP 与 SFTP 同时工作、共享根目录 - 上传、上传路径保留与父目录自动创建、建目录、`chmod`、`rename`、`rm`、`rmdir` - `reput` 续传 - 下载被拒绝 - 符号链接逃逸与 `..` 路径处理 - 错误密码被拒绝 - SFTP 传输中 SIGINT 立即退出 端口可通过 `FTP_TEST_PORT` / `SFTP_TEST_PORT` 环境变量覆盖。 ## 限制 - 明文 FTP 不加密;需要加密请使用 SFTP 端口 - 仅用于上传测试:FTP 不支持下载,SFTP 不支持 `READ`/软链接/硬链接 - FTP 的 `TYPE A` 会被接受,但不做 ASCII 换行转换,文件按字节原样写入 - SFTP 未做 chroot,依靠路径归一化 + `weakly_canonical` 做根目录沙箱 - 单进程多线程,未做并发数/带宽限制,不适合生产环境 - SFTP 默认使用临时主机密钥,重启后客户端会看到 host key 变化 - 被动 FTP 返回控制连接的本机地址;如果服务器在 NAT 后且客户端无法直连, 请使用 `EPSV` 或做端口映射