Fix code quality: trailing whitespace, unused imports, clippy warnings

- Fix trailing whitespace in kex.rs and s3.rs
- Add missing KexProposal import in kex_complete.rs
- Auto-fix clippy warnings across all crates
- All 153 tests pass
This commit is contained in:
Warren
2026-06-19 05:21:38 +08:00
parent 4b37e524cf
commit d94cb2df4c
135 changed files with 7256 additions and 4321 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+83
View File
@@ -0,0 +1,83 @@
# Phase 16.2:性能优化分析
**测试时间**:2026-06-17 22:30
**目标**:将传输速度从780 KB/s提升到21-36 MB/s
## 性能瓶颈分析 ⭐⭐⭐⭐⭐
**当前配置**:
- Window size: 2MB (local_window = 2097152)
- poll timeout: 10ms (每iteration)
- max_poll_iterations: 5000 (50s总timeout)
- stdin timeout: 3000 iterations (30s)
**瓶颈1:poll iteration overhead ⭐⭐⭐⭐⭐**
- 每iteration: 10ms poll timeout
- 总iteration: 5000次
- 每iteration开销: log输出 + try_wait() check
- **估算开销**: 5000 iterations * 10ms = 50秒(理论最大)
- **实际开销**: 20MB传输用了24秒,说明poll overhead占用了大量时间
**瓶颈2:Window size太小 ⭐⭐⭐⭐**
- OpenSSH默认: 2MB
- 实际测试: 20MB传输用了24秒
- **问题**: Window size限制了单次传输的数据量
- **解决方案**: 增加到16MB或32MB
**瓶颈3:AES-CTR encryption overhead ⭐⭐⭐**
- AES-256-CTR加密/解密: 每packet需要计算
- MAC计算: HMAC-SHA256 (每packet)
- **估算**: 每packet约100-200us开销
- **影响**: 780 KB/s可能受encryption限制
**瓶颈4:sshbuf zero-copy性能 ⭐⭐**
- sshbuf实现: 339行
- **问题**: 未进行性能测试
- **可能**: zero-copy优化不足
## 性能优化方案 ⭐⭐⭐⭐⭐
**方案1:减少poll iteration overhead(优先 ⭐⭐⭐⭐⭐)**
- 增加poll timeout: 从10ms改到100ms
- 减少iteration次数: 从5000改到500
- 减少log频率: 从每10次改到每50次
- **预期效果**: 减少50-80% poll overhead
**方案2:增加Window size ⭐⭐⭐⭐**
- 从2MB增加到16MB或32MB
- 动态调整Window size(根据传输速度)
- **预期效果**: 提升单次传输数据量
**方案3:优化encryption ⭐⭐⭐**
- 使用AES-NI硬件加速(检查是否已启用)
- 减少MAC计算频率(批量计算)
- **预期效果**: 减少encryption overhead
**方案4:sshbuf性能测试 ⭐⭐**
- 编写benchmark测试sshbuf性能
- 对比临时buffer vs zero-copy
- **预期效果**: 验证zero-copy优势
## 实施计划 ⭐⭐⭐⭐⭐
**Phase 16.2.1:减少poll overhead(立即实施)**
- 修改poll timeout: 10ms → 100ms
- 修改iteration次数: 5000 → 500
- 修改log频率: 每10次 → 每50次
- **预期传输速度**: 从780 KB/s提升到10-20 MB/s
**Phase 16.2.2:增加Window size**
- 从2MB增加到16MB
- 测试传输速度变化
**Phase 16.2.3:encryption优化**
- 检查AES-NI是否启用
- 如果未启用,添加AES-NI支持
---
**立即实施Phase 16.2.1**(减少poll overhead)
---
**最后更新**:2026-06-17 22:30
+441
View File
@@ -0,0 +1,441 @@
# SFTP Client 测试建议与分析
**测试目标**:验证 MarkBaseSSH SFTP 实现(Phase 7)的兼容性和稳定性
**测试环境**:MarkBaseSSH server (port 2024) + macOS client
**测试用户**:demo (password: demo123)
---
## 推荐测试方案 ⭐⭐⭐⭐⭐
### 方案 1: OpenSSH sftp client(必须测试)
**推荐等级**:⭐⭐⭐⭐⭐ **最高优先级**
**理由**:
- ✅ OpenSSH 是标准实现,MarkBaseSSH 必须完全兼容
- ✅ macOS 内置,无需额外安装
- ✅ 命令行工具,适合自动化测试
- ✅ 完整的 SFTP 协议支持(SSH_FXP_* 所有 packet)
- ✅ 错误信息清晰,易于调试
**测试命令**:
```bash
# 基本连接测试
sftp -P 2024 demo@127.0.0.1
# 批量测试脚本
sftp -P 2024 -b /tmp/sftp_test_batch.txt demo@127.0.0.1
# 大文件传输测试
sftp -P 2024 demo@127.0.0.1 <<EOF
put /tmp/test_100mb.bin /data/test_100mb.bin
get /data/test_100mb.bin /tmp/test_download.bin
ls -la /data/
bye
EOF
# MD5 校验
md5 /tmp/test_100mb.bin /tmp/test_download.bin
```
**测试覆盖**:
- ✅ SSH_FXP_INIT/VERSION(握手)
- ✅ SSH_FXP_REALPATH(路径解析)
- ✅ SSH_FXP_OPENDIR/READDIR(目录浏览)
- ✅ SSH_FXP_OPEN/READ/WRITE(文件传输)
- ✅ SSH_FXP_CLOSE(句柄管理)
- ✅ SSH_FXP_STAT/LSTAT(文件属性)
- ✅ SSH_FXP_MKDIR/RMDIR(目录操作)
- ✅ SSH_FXP_REMOVE/RENAME(文件操作)
**预期结果**:
- ✅ 所有操作成功
- ✅ 文件完整性校验一致
- ✅ 错误处理正确(权限、路径不存在等)
---
### 方案 2: Cyberduck(macOS 推荐 GUI client)
**推荐等级**:⭐⭐⭐⭐⭐ **强烈推荐**
**理由**:
- ✅ macOS 原生应用,用户友好
- ✅ 广泛使用,稳定可靠
- ✅ 支持 SFTP、FTP、WebDAV 等多种协议
- ✅ 支持大文件传输(断点续传)
- ✅ 支持同步功能(同步本地和远程目录)
- ✅ 书签管理(保存连接配置)
**安装方式**:
```bash
# Homebrew 安装
brew install --cask cyberduck
# 或从 App Store 下载
# https://apps.apple.com/app/cyberduck/id409222152
```
**测试配置**:
```
协议: SFTP
服务器: 127.0.0.1
端口: 2024
用户名: demo
密码: demo123
路径: /Users/accusys/markbase/data
```
**测试覆盖**:
- ✅ GUI 连接测试(用户交互)
- ✅ 文件上传(拖拽上传)
- ✅ 文件下载(拖拽下载)
- ✅ 目录浏览(双击进入)
- ✅ 文件删除(右键菜单)
- ✅ 文件重命名(右键菜单)
- ✅ 新建目录(右键菜单)
- ✅ 大文件传输(100MB+)
- ✅ 断点续传测试(中断后重新连接)
**预期结果**:
- ✅ 连接成功,显示文件列表
- ✅ 上传/下载正常,进度显示
- ✅ 文件操作正常,错误提示清晰
---
### 方案 3: FileZilla(跨平台 GUI client)
**推荐等级**:⭐⭐⭐⭐ **推荐**
**理由**:
- ✅ 跨平台(Windows, macOS, Linux)
- ✅ 广泛使用,社区活跃
- ✅ 支持多种协议(SFTP, FTP, FTPS)
- ✅ 详细日志显示(packet 级别)
- ✅ 支持并发传输(多文件同时上传/下载)
- ✅ 站点管理器(保存连接配置)
**安装方式**:
```bash
# Homebrew 安装
brew install --cask filezilla
# 或从官网下载
# https://filezilla-project.org/download.php
```
**测试配置**:
```
协议: SFTP - SSH File Transfer Protocol
主机: 127.0.0.1
端口: 2024
用户: demo
密码: demo123
```
**测试覆盖**:
- ✅ GUI 连接测试
- ✅ 文件传输(上传/下载)
- ✅ 目录浏览
- ✅ 文件操作(删除、重命名、新建目录)
- ✅ 并发传输测试(多文件同时传输)
- ✅ 日志分析(查看 SFTP packet)
- ✅ 大文件传输(100MB+)
**预期结果**:
- ✅ 连接成功
- ✅ 日志显示 SSH_FXP_* packet(验证协议实现)
- ✅ 文件传输正常
- ✅ 并发传输正常(Window Control 验证)
---
### 方案 4: lftp(高级命令行 client)
**推荐等级**:⭐⭐⭐⭐ **推荐(高级测试)**
**理由**:
- ✅ 功能丰富(镜像、同步、断点续传)
- ✅ 支持多种协议(SFTP, FTP, HTTP, HTTPS)
- ✅ 支持并行传输(多连接并发)
- ✅ 支持脚本化(批量操作)
- ✅ 详细日志(调试信息)
**安装方式**:
```bash
# Homebrew 安装
brew install lftp
```
**测试命令**:
```bash
# 基本连接测试
lftp sftp://demo:demo123@127.0.0.1:2024
# 镜像测试(同步目录)
lftp sftp://demo:demo123@127.0.0.1:2024 <<EOF
mirror -R /tmp/test_folder /data/test_folder
mirror /data/test_folder /tmp/download_folder
bye
EOF
# 并行传输测试
lftp sftp://demo:demo123@127.0.0.1:2024 <<EOF
set sftp:parallel 4
mput /tmp/test_*.bin
bye
EOF
# 断点续传测试
lftp sftp://demo:demo123@127.0.0.1:2024 <<EOF
pget -n 4 -c /data/test_100mb.bin
bye
EOF
```
**测试覆盖**:
- ✅ 基本操作(ls, get, put, rm)
- ✅ 镜像功能(mirror,同步目录)
- ✅ 并行传输(mput, mget)
- ✅ 断点续传(pget -c)
- ✅ 高级功能验证
**预期结果**:
- ✅ 连接成功
- ✅ 镜像同步正常
- ✅ 并行传输正常(Window Control 验证)
- ✅ 断点续传正常
---
## 测试优先级排序 ⭐⭐⭐⭐⭐
| 优先级 | Client | 类型 | 安装状态 | 测试必要性 |
|--------|--------|------|---------|-----------|
| **1** | OpenSSH sftp | 命令行 | ✅ 已安装 | ⭐⭐⭐⭐⭐ **必须测试** |
| **2** | Cyberduck | GUI | ❌ 未安装 | ⭐⭐⭐⭐⭐ **强烈推荐** |
| **3** | FileZilla | GUI | ❌ 未安装 | ⭐⭐⭐⭐ **推荐** |
| **4** | lftp | 命令行 | ❌ 未安装 | ⭐⭐⭐⭐ **推荐(高级测试)** |
---
## 建议测试流程 ⭐⭐⭐⭐⭐
### Phase 1: OpenSSH sftp(必须)
**时间**:30 分钟
**步骤**:
1. 基本连接测试(pwd, ls, cd)
2. 文件上传测试(put)
3. 文件下载测试(get)
4. 文件操作测试(rm, rename, mkdir)
5. 大文件传输测试(100MB)
6. MD5 校验验证
**验证重点**:
- ✅ SSH_FXP_* packet 完整实现
- ✅ Window Control 正常工作
- ✅ 文件完整性校验
---
### Phase 2: Cyberduck(强烈推荐)
**时间**:20 分钟
**步骤**:
1. GUI 连接测试
2. 文件拖拽上传
3. 文件拖拽下载
4. 目录浏览测试
5. 文件操作测试(右键菜单)
6. 大文件传输测试(100MB)
7. 断点续传测试
**验证重点**:
- ✅ 用户交互友好性
- ✅ 大文件传输稳定性
- ✅ 错误提示清晰性
---
### Phase 3: FileZilla(推荐)
**时间**:30 分钟
**步骤**:
1. GUI 连接测试
2. 文件传输测试
3. 并发传输测试(多文件同时)
4. 日志分析(SSH_FXP_* packet)
5. 大文件传输测试
**验证重点**:
- ✅ 并发传输(Window Control)
- ✅ 协议实现验证(packet 日志)
- ✅ 错误处理
---
### Phase 4: lftp(高级测试)
**时间**:40 分钟
**步骤**:
1. 基本连接测试
2. 镜像同步测试(mirror)
3. 并行传输测试(mput)
4. 断点续传测试(pget)
5. 性能对比
**验证重点**:
- ✅ 高级功能兼容性
- ✅ 性能优化验证
- ✅ 稳定性测试
---
## 测试脚本建议 ⭐⭐⭐⭐⭐
### OpenSSH sftp 批量测试脚本
**文件**:`/tmp/sftp_test_batch.txt`
```bash
# 基本操作测试
pwd
ls -la
cd data
ls -la
# 文件上传测试
put /tmp/test_5mb.bin test_5mb.bin
put /tmp/test_10mb.bin test_10mb.bin
put /tmp/test_100mb.bin test_100mb.bin
# 文件下载测试
get test_100mb.bin /tmp/test_download.bin
# 文件操作测试
mkdir test_dir
rename test_5mb.bin test_5mb_renamed.bin
rm test_10mb.bin
rmdir test_dir
# 属性查询测试
stat test_100mb.bin
ls -la
# 退出
bye
```
**执行命令**:
```bash
# 创建测试文件
dd if=/dev/urandom of=/tmp/test_5mb.bin bs=1M count=5
dd if=/dev/urandom of=/tmp/test_10mb.bin bs=1M count=10
dd if=/dev/urandom of=/tmp/test_100mb.bin bs=1M count=100
# 执行批量测试
sftp -P 2024 -b /tmp/sftp_test_batch.txt demo@127.0.0.1
# MD5 校验
md5 /tmp/test_100mb.bin /tmp/test_download.bin
```
---
## 错误处理测试 ⭐⭐⭐⭐⭐
**测试场景**:
1. ✅ 路径不存在(SSH_FXP_NO_SUCH_FILE)
2. ✅ 权限不足(SSH_FXP_PERMISSION_DENIED)
3. ✅ 文件已存在(SSH_FXP_FILE_ALREADY_EXISTS)
4. ✅ 磁盘空间不足(SSH_FXP_FAILURE)
5. ✅ 连接中断(断点续传)
**测试命令**:
```bash
# 路径不存在测试
sftp -P 2024 demo@127.0.0.1 <<EOF
get /data/nonexistent_file.bin /tmp/test.bin
EOF
# 预期: "No such file"
# 权限不足测试
sftp -P 2024 demo@127.0.0.1 <<EOF
put /tmp/test.bin /root/test.bin
EOF
# 预期: "Permission denied"
# 文件已存在测试
sftp -P 2024 demo@127.0.0.1 <<EOF
put /tmp/test_100mb.bin /data/test_100mb.bin
EOF
# 预期: 文件已存在,询问是否覆盖
```
---
## 性能测试建议 ⭐⭐⭐⭐⭐
**测试指标**:
- 传输速率(MB/s)
- 并发传输能力(多文件同时)
- 大文件传输稳定性(100MB+)
- Window Control 效率(window adjust frequency)
**测试命令**:
```bash
# OpenSSH sftp 性能测试
time sftp -P 2024 demo@127.0.0.1 <<EOF
put /tmp/test_100mb.bin /data/test_100mb.bin
bye
EOF
# FileZilla 并发传输测试
# 同时上传 10 个 10MB 文件,测试 Window Control
# lftp 并行传输测试
lftp sftp://demo:demo123@127.0.0.1:2024 <<EOF
set sftp:parallel 4
mput /tmp/test_1*.bin
bye
EOF
```
---
## 总结与建议 ⭐⭐⭐⭐⭐
**必须测试**:
- ⭐⭐⭐⭐⭐ **OpenSSH sftp**(标准实现,兼容性验证)
**强烈推荐**:
- ⭐⭐⭐⭐⭐ **Cyberduck**(macOS 原生,用户友好)
- ⭐⭐⭐⭐ **FileZilla**(跨平台,日志详细)
**可选测试**:
- ⭐⭐⭐⭐ **lftp**(高级功能,性能优化)
**测试时间估算**:
- Phase 1(OpenSSH sftp):30 分钟
- Phase 2(Cyberduck):20 分钟
- Phase 3(FileZilla):30 分钟
- Phase 4(lftp):40 分钟
- **总计**:约 2 小时
**预期结果**:
- ✅ 所有 client 连接成功
- ✅ 所有操作正常(上传、下载、浏览、删除等)
- ✅ 文件完整性校验一致
- ✅ 错误处理正确
- ✅ Window Control 正常工作
---
**最后更新**:2026-06-17
+260
View File
@@ -0,0 +1,260 @@
# SSH/SFTP/SCP/rsync 完整整合測試計劃 ⭐⭐⭐⭐⭐
**版本**: 1.0 | **日期**: 2026-06-18 | **實施狀態**: Phase 1-16 + Window Control + SFTP batch fix ✅
---
## 環境
- **伺服器**: `markbase-core ssh-start -p 2024` (本機)
- **用戶**: `demo` / `demo123` (bcrypt)
- **日誌**: `RUST_LOG=info` 輸出至檔案
- **計時**: 每個測試 `timeout 30` (大檔案 `timeout 120`)
---
## 1. SSH 基本連線 (Phase 1-5)
```bash
# 1.1 連線 + 密碼認證
timeout 10 ssh -v -p 2024 -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null demo@127.0.0.1 'echo "SSH OK"' 2>&1
# ✅ 預期: "SSH OK" + debug1: Authentication succeeded (password)
```
---
## 2. SFTP 操作 (Phase 7 + batch fix)
### 2.1 基礎功能
```bash
timeout 30 sftp -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -P 2024 demo@127.0.0.1 << 'EOF'
pwd
ls
mkdir sftp_test_dir
cd sftp_test_dir
put /etc/hostname test_upload.txt
get test_upload.txt /tmp/test_download.txt
rm test_upload.txt
cd ..
rmdir sftp_test_dir
!md5 /tmp/test_download.txt
bye
EOF
```
### 2.2 企業級錯誤處理
```bash
timeout 10 sftp -P 2024 demo@127.0.0.1 << 'EOF'
mkdir /etc/forbidden
get /root/.ssh/id_rsa
stat /nonexistent
rm /nonexistent
bye
EOF
# ✅ 預期: Permission denied / No such file (非 generic Failure)
```
### 2.3 大檔案傳輸 (MD5驗證, 每次 2MB / 5MB / 10MB)
```bash
# 建立測試檔
dd if=/dev/urandom of=/tmp/test_2m.bin bs=1M count=2 2>/dev/null
dd if=/dev/urandom of=/tmp/test_5m.bin bs=1M count=5 2>/dev/null
dd if=/dev/urandom of=/tmp/test_10m.bin bs=1M count=10 2>/dev/null
# 測試每個檔案上傳+下載
for f in test_2m test_5m test_10m; do
echo "=== Testing $f ==="
md5sum /tmp/${f}.bin | awk '{print $1}' > /tmp/${f}.md5
timeout 120 sftp -P 2024 demo@127.0.0.1 << EOFSFTP 2>&1 | grep -v debug
put /tmp/${f}.bin ${f}.bin
get ${f}.bin /tmp/${f}_dl.bin
rm ${f}.bin
bye
EOFSFTP
md5sum -c /tmp/${f}.md5 <<< "$(md5sum /tmp/${f}_dl.bin | awk '{print $1}')" 2>/dev/null \
&& echo "✅ $f PASS" || echo "❌ $f FAIL"
done
```
### 2.4 多檔案批次傳輸
```bash
# 建立10個小檔
for i in $(seq 1 10); do
dd if=/dev/urandom of=/tmp/batch_${i}.bin bs=1K count=64 2>/dev/null
done
# 批次上傳 + 下載 + 驗證
timeout 60 sftp -P 2024 demo@127.0.0.1 << 'EOF'
lcd /tmp
cd /tmp
put batch_1.bin batch_2.bin batch_3.bin batch_4.bin batch_5.bin
put batch_6.bin batch_7.bin batch_8.bin batch_9.bin batch_10.bin
get batch_1.bin batch_2.bin batch_3.bin batch_4.bin batch_5.bin
get batch_6.bin batch_7.bin batch_8.bin batch_9.bin batch_10.bin
rm batch_1.bin batch_2.bin batch_3.bin batch_4.bin batch_5.bin
rm batch_6.bin batch_7.bin batch_8.bin batch_9.bin batch_10.bin
bye
EOF
# MD5比對
for i in $(seq 1 10); do
[ "$(md5sum /tmp/batch_${i}.bin | awk '{print $1}')" = \
"$(md5sum /tmp/batch_${i}.bin 2>/dev/null | awk '{print $1}')" ] \
&& echo "✅ batch_${i}" || echo "❌ batch_${i}"
done
```
---
## 3. SCP 測試 (Phase 8)
```bash
# 3.1 上傳
timeout 30 scp -P 2024 -o StrictHostKeyChecking=no \
/tmp/test_5m.bin demo@127.0.0.1:scp_test.bin
# 3.2 下載
timeout 30 scp -P 2024 -o StrictHostKeyChecking=no \
demo@127.0.0.1:scp_test.bin /tmp/scp_dl.bin
# 3.3 目錄傳輸
mkdir -p /tmp/scp_dir && for i in 1 2 3; do
dd if=/dev/urandom of=/tmp/scp_dir/file_${i}.bin bs=1M count=1 2>/dev/null
done
timeout 30 scp -P 2024 -r -o StrictHostKeyChecking=no \
/tmp/scp_dir demo@127.0.0.1:scp_dir_remote
# 3.4 完整驗證
md5sum /tmp/test_5m.bin /tmp/scp_dl.bin
md5sum /tmp/scp_dir/*
rm -rf /tmp/scp_dir
```
---
## 4. rsync 測試 (Phase 16 Final: subprocess)
```bash
export RSYNC_RSH="ssh -p 2024 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
# 4.1 上傳
timeout 30 rsync -avz --rsh="$RSYNC_RSH" /tmp/test_5m.bin demo@127.0.0.1:rsync_test.bin
# 4.2 下載
timeout 30 rsync -avz --rsh="$RSYNC_RSH" demo@127.0.0.1:rsync_test.bin /tmp/rsync_dl.bin
# 4.3 差異傳輸 (delta transfer)
echo "extra data" >> /tmp/test_5m.bin
timeout 30 rsync -avz --rsh="$RSYNC_RSH" /tmp/test_5m.bin demo@127.0.0.1:rsync_test.bin
# 4.4 大檔案 (50MB-100MB)
dd if=/dev/urandom of=/tmp/test_50m.bin bs=1M count=50 2>/dev/null
md5sum /tmp/test_50m.bin > /tmp/test_50m.md5
timeout 120 rsync -avz --rsh="$RSYNC_RSH" /tmp/test_50m.bin demo@127.0.0.1:rsync_large.bin
timeout 120 rsync -avz --rsh="$RSYNC_RSH" demo@127.0.0.1:rsync_large.bin /tmp/rsync_50m_dl.bin
md5sum -c /tmp/test_50m.md5 <<< "$(md5sum /tmp/rsync_50m_dl.bin | awk '{print $1}')"
# 4.5 目錄同步
mkdir -p /tmp/rsync_dir && for i in $(seq 1 5); do
dd if=/dev/urandom of=/tmp/rsync_dir/f_${i}.bin bs=1M count=2 2>/dev/null
done
timeout 60 rsync -avz --rsh="$RSYNC_RSH" /tmp/rsync_dir/ demo@127.0.0.1:rsync_dir/
timeout 60 rsync -avz --rsh="$RSYNC_RSH" demo@127.0.0.1:rsync_dir/ /tmp/rsync_dir_dl/
```
---
## 5. SSH 通道指令執行 (Phase 6)
```bash
# 5.1 基本指令
ssh -p 2024 demo@127.0.0.1 'echo "hello"; whoami; pwd; uname -a'
# 5.2 多指令管線
ssh -p 2024 demo@127.0.0.1 'ls -la /tmp | head -5; echo "---"; df -h | head -3'
# 5.3 環境變數
ssh -p 2024 demo@127.0.0.1 'export TEST_VAR=hello; echo $TEST_VAR'
```
---
## 6. 壓力測試
### 6.1 連續連線
```bash
for i in $(seq 1 20); do
echo "=== Iteration $i ==="
timeout 10 ssh -p 2024 demo@127.0.0.1 'echo OK' 2>&1 | grep "^OK$" \
&& echo "✅" || echo "❌"
done
```
### 6.2 並行連線
```bash
for i in $(seq 1 5); do
(timeout 30 sftp -P 2024 demo@127.0.0.1 << EOF &
put /tmp/test_5m.bin parallel_${i}.bin
get parallel_${i}.bin /tmp/parallel_${i}_dl.bin
rm parallel_${i}.bin
bye
EOF
) &
done
wait
echo "All parallel transfers done"
```
---
## 7. 清理
```bash
# 清理伺服器端檔案 (需在 SFTP session 中執行)
rm scp_test.bin rsync_test.bin rsync_large.bin
rm -rf rsync_dir scp_dir_remote
# 清理本機暫存
rm -f /tmp/test_upload.txt /tmp/test_download.txt
rm -f /tmp/test_2m.bin /tmp/test_5m.bin /tmp/test_10m.bin
rm -f /tmp/test_2m_dl.bin /tmp/test_5m_dl.bin /tmp/test_10m_dl.bin
rm -f /tmp/rsync_test.bin /tmp/rsync_dl.bin /tmp/rsync_large.bin
rm -f /tmp/test_50m.bin /tmp/rsync_50m_dl.bin
rm -f /tmp/scp_test.bin /tmp/scp_dl.bin
rm -rf /tmp/rsync_dir /tmp/rsync_dir_dl /tmp/scp_dir
rm -f /tmp/batch_*.bin /tmp/parallel_*.bin
```
---
## 驗證矩陣
| 編號 | 測試項目 | 預期結果 | 檢查方法 |
|------|----------|----------|----------|
| 1.1 | SSH連線+認證 | `SSH OK` 輸出 | stdout |
| 2.1 | SFTP基礎功能 | 所有操作成功 | exit code=0 |
| 2.2 | SFTP錯誤處理 | 非 generic 錯誤 | 日誌比對 |
| 2.3 | SFTP大檔案 | MD5吻合 | md5sum |
| 2.4 | SFTP批次檔案 | 所有MD5吻合 | md5sum |
| 3.1 | SCP上傳 | 檔案存在 | md5sum |
| 3.2 | SCP下載 | MD5吻合 | md5sum |
| 3.3 | SCP目錄 | 結構一致 | ls -la |
| 4.1 | rsync上傳 | MD5吻合 | md5sum |
| 4.2 | rsync下載 | MD5吻合 | md5sum |
| 4.3 | rsync增量 | 僅傳差異 | speedup > 1 |
| 4.4 | rsync 50MB | MD5吻合 | md5sum |
| 5.1 | Shell指令 | 正確輸出 | stdout |
| 6.1 | 連續連線20次 | 100%成功 | 計數 |
| 6.2 | 並行xfer | 所有MD5吻合 | md5sum |
+2 -2
View File
@@ -312,9 +312,9 @@ impl FileTreeRocksDB {
label: &str, label: &str,
file_uuid: &str, file_uuid: &str,
sha256: Option<&str>, sha256: Option<&str>,
original_name: &str, _original_name: &str,
file_size: Option<i64>, file_size: Option<i64>,
mime_type: Option<&str>, _mime_type: Option<&str>,
parent_id: Option<&str>, parent_id: Option<&str>,
) -> FileNode { ) -> FileNode {
FileNode { FileNode {
+3 -3
View File
@@ -286,9 +286,9 @@ impl FileTreeSled {
label: &str, label: &str,
file_uuid: &str, file_uuid: &str,
sha256: Option<&str>, sha256: Option<&str>,
original_name: &str, _original_name: &str,
file_size: Option<i64>, file_size: Option<i64>,
mime_type: Option<&str>, _mime_type: Option<&str>,
parent_id: Option<&str>, parent_id: Option<&str>,
) -> FileNode { ) -> FileNode {
FileNode { FileNode {
@@ -314,7 +314,7 @@ impl FileTreeSled {
pub fn build_tree(nodes: &[FileNode]) -> Vec<FileNode> { pub fn build_tree(nodes: &[FileNode]) -> Vec<FileNode> {
let mut roots = Vec::new(); let mut roots = Vec::new();
let node_map: HashMap<String, &FileNode> = let _node_map: HashMap<String, &FileNode> =
nodes.iter().map(|n| (n.node_id.clone(), n)).collect(); nodes.iter().map(|n| (n.node_id.clone(), n)).collect();
for node in nodes { for node in nodes {
+23 -23
View File
@@ -630,28 +630,28 @@ mod tests {
} }
} }
// 新增:创建虚拟树类型 // 新增:创建虚拟树类型
pub fn create_tree_type( pub fn create_tree_type(
conn: &Connection, conn: &Connection,
tree_type: &str, tree_type: &str,
tree_name: &str, tree_name: &str,
description: &str, description: &str,
is_system_defined: bool, is_system_defined: bool,
) -> Result<()> { ) -> Result<()> {
conn.execute( conn.execute(
"INSERT INTO tree_registry (tree_type, tree_name, description, is_system_defined) "INSERT INTO tree_registry (tree_type, tree_name, description, is_system_defined)
VALUES (?1, ?2, ?3, ?4)", VALUES (?1, ?2, ?3, ?4)",
rusqlite::params![tree_type, tree_name, description, is_system_defined as i64], rusqlite::params![tree_type, tree_name, description, is_system_defined as i64],
)?; )?;
Ok(()) Ok(())
} }
// 新增:获取所有虚拟树类型 // 新增:获取所有虚拟树类型
// 新增:删除虚拟树类型(仅限用户自定义) // 新增:删除虚拟树类型(仅限用户自定义)
pub fn delete_tree_type(conn: &Connection, tree_type: &str) -> Result<()> { pub fn delete_tree_type(conn: &Connection, tree_type: &str) -> Result<()> {
conn.execute( conn.execute(
"DELETE FROM tree_registry WHERE tree_type = ?1 AND is_system_defined = 0", "DELETE FROM tree_registry WHERE tree_type = ?1 AND is_system_defined = 0",
[tree_type], [tree_type],
)?; )?;
Ok(()) Ok(())
} }
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -1,5 +1,5 @@
// API Handlers Module // API Handlers Module
// //
// This module provides space for future modular API handlers. // This module provides space for future modular API handlers.
// Current handlers are implemented in server.rs for stability. // Current handlers are implemented in server.rs for stability.
// //
@@ -13,4 +13,4 @@
// - view.rs: Category/Series view handlers // - view.rs: Category/Series view handlers
// - static.rs: Static page handlers // - static.rs: Static page handlers
pub use crate::server::AppState; pub use crate::server::AppState;
+1 -1
View File
@@ -9,4 +9,4 @@ pub mod handlers;
// - Clear separation of concerns // - Clear separation of concerns
// - Easier maintenance for new features // - Easier maintenance for new features
// - Gradual migration path from server.rs // - Gradual migration path from server.rs
// - Independent testing per handler module // - Independent testing per handler module
+27 -27
View File
@@ -1,22 +1,21 @@
// Archive Configuration - User Configurable Options // Archive Configuration - User Configurable Options
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
use log::warn; use log::warn;
use serde::{Deserialize, Serialize};
/// Archive Configuration /// Archive Configuration
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveConfig { pub struct ArchiveConfig {
// Optional formats (controversial) // Optional formats (controversial)
pub enable_rar: bool, // ⚠️ Legal risk (RARLAB patent) pub enable_rar: bool, // ⚠️ Legal risk (RARLAB patent)
pub enable_xz: bool, // ⚠️ External dependency (liblzma) pub enable_xz: bool, // ⚠️ External dependency (liblzma)
pub enable_7z: bool, // ⚠️ Unstable library pub enable_7z: bool, // ⚠️ Unstable library
// Performance settings // Performance settings
pub cache_size_mb: u64, pub cache_size_mb: u64,
pub max_concurrent_extractions: usize, pub max_concurrent_extractions: usize,
// Security settings // Security settings
pub max_decompression_ratio: u64, pub max_decompression_ratio: u64,
pub max_file_size_mb: u64, pub max_file_size_mb: u64,
@@ -29,11 +28,11 @@ impl Default for ArchiveConfig {
enable_rar: false, enable_rar: false,
enable_xz: false, enable_xz: false,
enable_7z: false, enable_7z: false,
// Performance // Performance
cache_size_mb: 100, cache_size_mb: 100,
max_concurrent_extractions: 4, max_concurrent_extractions: 4,
// Security // Security
max_decompression_ratio: 1000, max_decompression_ratio: 1000,
max_file_size_mb: 1024, max_file_size_mb: 1024,
@@ -46,45 +45,46 @@ impl ArchiveConfig {
pub fn load(path: &str) -> Result<Self> { pub fn load(path: &str) -> Result<Self> {
let content = std::fs::read_to_string(path)?; let content = std::fs::read_to_string(path)?;
let config: ArchiveConfig = toml::from_str(&content)?; let config: ArchiveConfig = toml::from_str(&content)?;
// Validate configuration // Validate configuration
config.validate()?; config.validate()?;
Ok(config) Ok(config)
} }
/// Save configuration to TOML file /// Save configuration to TOML file
pub fn save(&self, path: &str) -> Result<()> { pub fn save(&self, path: &str) -> Result<()> {
let content = toml::to_string_pretty(self)?; let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?; std::fs::write(path, content)?;
Ok(()) Ok(())
} }
/// Validate configuration /// Validate configuration
pub fn validate(&self) -> Result<()> { pub fn validate(&self) -> Result<()> {
if self.cache_size_mb > 1000 { if self.cache_size_mb > 1000 {
warn!("Cache size > 1GB may cause memory pressure"); warn!("Cache size > 1GB may cause memory pressure");
} }
if self.max_concurrent_extractions > 10 { if self.max_concurrent_extractions > 10 {
warn!("Concurrent extractions > 10 may cause resource exhaustion"); warn!("Concurrent extractions > 10 may cause resource exhaustion");
} }
if self.max_decompression_ratio < 10 { if self.max_decompression_ratio < 10 {
return Err(anyhow::anyhow!("Max decompression ratio too low (min 10)")); return Err(anyhow::anyhow!("Max decompression ratio too low (min 10)"));
} }
if self.max_file_size_mb > 10_000 { // 10GB if self.max_file_size_mb > 10_000 {
// 10GB
warn!("Max file size > 10GB may cause disk space issues"); warn!("Max file size > 10GB may cause disk space issues");
} }
Ok(()) Ok(())
} }
/// Generate default config file template /// Generate default config file template
pub fn generate_template() -> String { pub fn generate_template() -> String {
let config = Self::default(); let config = Self::default();
format!( format!(
"# === Archive Configuration === "# === Archive Configuration ===
# MarkBase Universal Compression Format Support # MarkBase Universal Compression Format Support
@@ -138,33 +138,33 @@ max_file_size_mb = {} # File size limit (MB)
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_default_config() { fn test_default_config() {
let config = ArchiveConfig::default(); let config = ArchiveConfig::default();
assert_eq!(config.enable_rar, false); assert_eq!(config.enable_rar, false);
assert_eq!(config.enable_xz, false); assert_eq!(config.enable_xz, false);
assert_eq!(config.enable_7z, false); assert_eq!(config.enable_7z, false);
assert_eq!(config.cache_size_mb, 100); assert_eq!(config.cache_size_mb, 100);
assert_eq!(config.max_decompression_ratio, 1000); assert_eq!(config.max_decompression_ratio, 1000);
} }
#[test] #[test]
fn test_config_validation() { fn test_config_validation() {
let config = ArchiveConfig { let config = ArchiveConfig {
max_decompression_ratio: 5, max_decompression_ratio: 5,
..Default::default() ..Default::default()
}; };
assert!(config.validate().is_err()); assert!(config.validate().is_err());
} }
#[test] #[test]
fn test_config_template() { fn test_config_template() {
let template = ArchiveConfig::generate_template(); let template = ArchiveConfig::generate_template();
assert!(template.contains("enable_rar = false")); assert!(template.contains("enable_rar = false"));
assert!(template.contains("⚠️ RAR Format Legal Risk Warning")); assert!(template.contains("⚠️ RAR Format Legal Risk Warning"));
} }
} }
+31 -34
View File
@@ -1,9 +1,9 @@
// Format Detector - Automatic Detection Based on Magic Numbers // Format Detector - Automatic Detection Based on Magic Numbers
use anyhow::Result;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path::Path; use std::path::Path;
use anyhow::Result;
use crate::archive::processor::ArchiveFormat; use crate::archive::processor::ArchiveFormat;
@@ -18,64 +18,61 @@ impl FormatDetector {
// ZIP: 50 4B 03 04 or 50 4B 05 06 (empty) or 50 4B 07 08 (spanned) // ZIP: 50 4B 03 04 or 50 4B 05 06 (empty) or 50 4B 07 08 (spanned)
(vec![0x50, 0x4B, 0x03, 0x04], ArchiveFormat::Zip, 4), (vec![0x50, 0x4B, 0x03, 0x04], ArchiveFormat::Zip, 4),
(vec![0x50, 0x4B, 0x05, 0x06], ArchiveFormat::Zip, 4), (vec![0x50, 0x4B, 0x05, 0x06], ArchiveFormat::Zip, 4),
// GZIP: 1F 8B // GZIP: 1F 8B
(vec![0x1F, 0x8B], ArchiveFormat::Gzip, 2), (vec![0x1F, 0x8B], ArchiveFormat::Gzip, 2),
]; ];
Self { magic_table } Self { magic_table }
} }
/// Detect file format based on Magic Number /// Detect file format based on Magic Number
pub fn detect(&self, path: &Path) -> Result<ArchiveFormat> { pub fn detect(&self, path: &Path) -> Result<ArchiveFormat> {
let mut file = File::open(path)?; let mut file = File::open(path)?;
let mut buffer = vec![0u8; 512]; let mut buffer = vec![0u8; 512];
let bytes_read = file.read(&mut buffer)?; let bytes_read = file.read(&mut buffer)?;
if bytes_read < 2 { if bytes_read < 2 {
return Ok(ArchiveFormat::Unknown); return Ok(ArchiveFormat::Unknown);
} }
// Match Magic Numbers // Match Magic Numbers
for (magic, format, offset) in &self.magic_table { for (magic, format, offset) in &self.magic_table {
if buffer.len() >= *offset && buffer[0..magic.len()] == *magic { if buffer.len() >= *offset && buffer[0..magic.len()] == *magic {
return Ok(*format); return Ok(*format);
} }
} }
// Special detection: TAR format (check ustar magic at offset 257) // Special detection: TAR format (check ustar magic at offset 257)
if buffer.len() >= 262 { if buffer.len() >= 262
if &buffer[257..262] == b"ustar" { && &buffer[257..262] == b"ustar" {
return Ok(ArchiveFormat::Tar); return Ok(ArchiveFormat::Tar);
} }
}
Ok(ArchiveFormat::Unknown) Ok(ArchiveFormat::Unknown)
} }
/// Detect composite format (e.g., TAR.GZ) /// Detect composite format (e.g., TAR.GZ)
pub fn detect_composite(&self, path: &Path) -> Result<ArchiveFormat> { pub fn detect_composite(&self, path: &Path) -> Result<ArchiveFormat> {
let format = self.detect(path)?; let format = self.detect(path)?;
// If GZIP, check if it's TAR.GZ (by extension for now) // If GZIP, check if it's TAR.GZ (by extension for now)
if format == ArchiveFormat::Gzip { if format == ArchiveFormat::Gzip {
let ext = path.extension() let ext = path
.extension()
.and_then(|e| e.to_str()) .and_then(|e| e.to_str())
.unwrap_or("") .unwrap_or("")
.to_lowercase(); .to_lowercase();
if ext == "tgz" || ext == "gz" { if ext == "tgz" || ext == "gz" {
// Check if filename contains .tar // Check if filename contains .tar
let filename = path.file_name() let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
.and_then(|n| n.to_str())
.unwrap_or("");
if filename.contains(".tar") { if filename.contains(".tar") {
return Ok(ArchiveFormat::TarGzip); return Ok(ArchiveFormat::TarGzip);
} }
} }
} }
Ok(format) Ok(format)
} }
} }
@@ -89,51 +86,51 @@ impl Default for FormatDetector {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use tempfile::TempDir;
use std::io::Write; use std::io::Write;
use tempfile::TempDir;
#[test] #[test]
fn test_detect_zip() { fn test_detect_zip() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let zip_path = temp_dir.path().join("test.zip"); let zip_path = temp_dir.path().join("test.zip");
// Create minimal ZIP file header // Create minimal ZIP file header
let mut file = File::create(&zip_path).unwrap(); let mut file = File::create(&zip_path).unwrap();
file.write_all(&[0x50, 0x4B, 0x03, 0x04]).unwrap(); file.write_all(&[0x50, 0x4B, 0x03, 0x04]).unwrap();
let detector = FormatDetector::new(); let detector = FormatDetector::new();
let format = detector.detect(&zip_path).unwrap(); let format = detector.detect(&zip_path).unwrap();
assert_eq!(format, ArchiveFormat::Zip); assert_eq!(format, ArchiveFormat::Zip);
} }
#[test] #[test]
fn test_detect_gzip() { fn test_detect_gzip() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let gz_path = temp_dir.path().join("test.gz"); let gz_path = temp_dir.path().join("test.gz");
// Create minimal GZIP file header // Create minimal GZIP file header
let mut file = File::create(&gz_path).unwrap(); let mut file = File::create(&gz_path).unwrap();
file.write_all(&[0x1F, 0x8B]).unwrap(); file.write_all(&[0x1F, 0x8B]).unwrap();
let detector = FormatDetector::new(); let detector = FormatDetector::new();
let format = detector.detect(&gz_path).unwrap(); let format = detector.detect(&gz_path).unwrap();
assert_eq!(format, ArchiveFormat::Gzip); assert_eq!(format, ArchiveFormat::Gzip);
} }
#[test] #[test]
fn test_detect_unknown() { fn test_detect_unknown() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let unknown_path = temp_dir.path().join("test.bin"); let unknown_path = temp_dir.path().join("test.bin");
// Create unknown file // Create unknown file
let mut file = File::create(&unknown_path).unwrap(); let mut file = File::create(&unknown_path).unwrap();
file.write_all(b"unknown data").unwrap(); file.write_all(b"unknown data").unwrap();
let detector = FormatDetector::new(); let detector = FormatDetector::new();
let format = detector.detect(&unknown_path).unwrap(); let format = detector.detect(&unknown_path).unwrap();
assert_eq!(format, ArchiveFormat::Unknown); assert_eq!(format, ArchiveFormat::Unknown);
} }
} }
+15 -15
View File
@@ -1,8 +1,8 @@
// Metadata Module - Archive Entry Metadata Management // Metadata Module - Archive Entry Metadata Management
use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::SystemTime; use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use crate::archive::processor::ArchiveFormat; use crate::archive::processor::ArchiveFormat;
@@ -29,7 +29,7 @@ impl ArchiveMetadata {
self.total_size as f64 / self.compressed_size as f64 self.total_size as f64 / self.compressed_size as f64
} }
} }
/// Check if compression ratio exceeds limit (Zip Bomb detection) /// Check if compression ratio exceeds limit (Zip Bomb detection)
pub fn check_zip_bomb(&self, max_ratio: u64) -> bool { pub fn check_zip_bomb(&self, max_ratio: u64) -> bool {
self.actual_ratio() > max_ratio as f64 self.actual_ratio() > max_ratio as f64
@@ -65,7 +65,7 @@ impl ArchiveEntry {
checksum: None, checksum: None,
} }
} }
/// Create file entry /// Create file entry
pub fn file(path: PathBuf, size: u64, compressed_size: u64) -> Self { pub fn file(path: PathBuf, size: u64, compressed_size: u64) -> Self {
Self { Self {
@@ -104,7 +104,7 @@ impl ExtractResult {
warnings: Vec::new(), warnings: Vec::new(),
} }
} }
pub fn success_rate(&self) -> f64 { pub fn success_rate(&self) -> f64 {
if self.total_files == 0 { if self.total_files == 0 {
100.0 100.0
@@ -113,11 +113,11 @@ impl ExtractResult {
(success_count as f64 / self.total_files as f64) * 100.0 (success_count as f64 / self.total_files as f64) * 100.0
} }
} }
pub fn has_failures(&self) -> bool { pub fn has_failures(&self) -> bool {
!self.failed_files.is_empty() !self.failed_files.is_empty()
} }
pub fn has_warnings(&self) -> bool { pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty() !self.warnings.is_empty()
} }
@@ -126,7 +126,7 @@ impl ExtractResult {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_archive_metadata() { fn test_archive_metadata() {
let metadata = ArchiveMetadata { let metadata = ArchiveMetadata {
@@ -140,37 +140,37 @@ mod tests {
created_time: None, created_time: None,
modified_time: None, modified_time: None,
}; };
assert_eq!(metadata.actual_ratio(), 2.0); assert_eq!(metadata.actual_ratio(), 2.0);
assert!(!metadata.check_zip_bomb(1000)); assert!(!metadata.check_zip_bomb(1000));
assert!(metadata.check_zip_bomb(1)); // Should detect as bomb assert!(metadata.check_zip_bomb(1)); // Should detect as bomb
} }
#[test] #[test]
fn test_archive_entry() { fn test_archive_entry() {
let dir_entry = ArchiveEntry::directory(PathBuf::from("test_dir")); let dir_entry = ArchiveEntry::directory(PathBuf::from("test_dir"));
assert!(dir_entry.is_dir); assert!(dir_entry.is_dir);
assert!(!dir_entry.is_file); assert!(!dir_entry.is_file);
let file_entry = ArchiveEntry::file(PathBuf::from("test.txt"), 100, 50); let file_entry = ArchiveEntry::file(PathBuf::from("test.txt"), 100, 50);
assert!(!file_entry.is_dir); assert!(!file_entry.is_dir);
assert!(file_entry.is_file); assert!(file_entry.is_file);
assert_eq!(file_entry.size, 100); assert_eq!(file_entry.size, 100);
} }
#[test] #[test]
fn test_extract_result() { fn test_extract_result() {
let result = ExtractResult::new(); let result = ExtractResult::new();
assert_eq!(result.success_rate(), 100.0); assert_eq!(result.success_rate(), 100.0);
let result_with_failure = ExtractResult { let result_with_failure = ExtractResult {
total_files: 10, total_files: 10,
success_files: 8, success_files: 8,
failed_files: vec![PathBuf::from("failed.txt")], failed_files: vec![PathBuf::from("failed.txt")],
..Default::default() ..Default::default()
}; };
assert_eq!(result_with_failure.success_rate(), 80.0); assert_eq!(result_with_failure.success_rate(), 80.0);
assert!(result_with_failure.has_failures()); assert!(result_with_failure.has_failures());
} }
} }
+54 -36
View File
@@ -25,9 +25,9 @@ pub use metadata::{ArchiveEntry, ArchiveMetadata, ExtractResult};
pub use processor::{ArchiveFormat, ArchiveProcessor}; pub use processor::{ArchiveFormat, ArchiveProcessor};
use anyhow::Result; use anyhow::Result;
use log::info;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use log::{info, warn};
/// Processor Registry - Plugin Architecture /// Processor Registry - Plugin Architecture
pub struct ProcessorRegistry { pub struct ProcessorRegistry {
@@ -43,93 +43,108 @@ impl ProcessorRegistry {
config, config,
} }
} }
/// Initialize all processors (based on config) /// Initialize all processors (based on config)
pub fn initialize(&mut self) -> Result<()> { pub fn initialize(&mut self) -> Result<()> {
// Core formats (always registered) // Core formats (always registered)
self.register_core_processors()?; self.register_core_processors()?;
// Optional formats (based on config) // Optional formats (based on config)
self.register_optional_processors()?; self.register_optional_processors()?;
Ok(()) Ok(())
} }
/// Register core format processors (9 formats) /// Register core format processors (9 formats)
fn register_core_processors(&mut self) -> Result<()> { fn register_core_processors(&mut self) -> Result<()> {
use crate::archive::processors::core::*; use crate::archive::processors::core::*;
self.processors.insert(ArchiveFormat::Zip, Box::new(ZipProcessor::new())); self.processors
self.processors.insert(ArchiveFormat::Tar, Box::new(TarProcessor::new())); .insert(ArchiveFormat::Zip, Box::new(ZipProcessor::new()));
self.processors.insert(ArchiveFormat::Gzip, Box::new(GzipProcessor::new())); self.processors
self.processors.insert(ArchiveFormat::Zstd, Box::new(ZstdProcessor::new())); .insert(ArchiveFormat::Tar, Box::new(TarProcessor::new()));
self.processors.insert(ArchiveFormat::Bzip2, Box::new(Bzip2Processor::new())); self.processors
self.processors.insert(ArchiveFormat::Lz4, Box::new(Lz4Processor::new())); .insert(ArchiveFormat::Gzip, Box::new(GzipProcessor::new()));
self.processors.insert(ArchiveFormat::TarGzip, Box::new(TarGzipProcessor::new())); self.processors
self.processors.insert(ArchiveFormat::TarBzip2, Box::new(TarBzip2Processor::new())); .insert(ArchiveFormat::Zstd, Box::new(ZstdProcessor::new()));
self.processors.insert(ArchiveFormat::TarZstd, Box::new(TarZstdProcessor::new())); self.processors
.insert(ArchiveFormat::Bzip2, Box::new(Bzip2Processor::new()));
self.processors
.insert(ArchiveFormat::Lz4, Box::new(Lz4Processor::new()));
self.processors
.insert(ArchiveFormat::TarGzip, Box::new(TarGzipProcessor::new()));
self.processors
.insert(ArchiveFormat::TarBzip2, Box::new(TarBzip2Processor::new()));
self.processors
.insert(ArchiveFormat::TarZstd, Box::new(TarZstdProcessor::new()));
info!("✅ Core formats registered: 9 formats"); info!("✅ Core formats registered: 9 formats");
Ok(()) Ok(())
} }
/// Register optional format processors (3 formats, based on config) /// Register optional format processors (3 formats, based on config)
fn register_optional_processors(&mut self) -> Result<()> { fn register_optional_processors(&mut self) -> Result<()> {
#[cfg(feature = "optional-formats")] #[cfg(feature = "optional-formats")]
{ {
use crate::archive::processors::optional::*; use crate::archive::processors::optional::*;
// RAR format (legal risk) // RAR format (legal risk)
if self.config.enable_rar { if self.config.enable_rar {
crate::archive::warning::show_rar_legal_warning(); crate::archive::warning::show_rar_legal_warning();
self.processors.insert(ArchiveFormat::Rar, Box::new(RarProcessor::new())); self.processors
.insert(ArchiveFormat::Rar, Box::new(RarProcessor::new()));
warn!("⚠️ RAR format enabled (legal risk)"); warn!("⚠️ RAR format enabled (legal risk)");
} }
// XZ format (external dependency) // XZ format (external dependency)
if self.config.enable_xz { if self.config.enable_xz {
if check_liblzma_available() { if check_liblzma_available() {
self.processors.insert(ArchiveFormat::Xz, Box::new(XzProcessor::new())); self.processors
.insert(ArchiveFormat::Xz, Box::new(XzProcessor::new()));
info!("✅ XZ format enabled"); info!("✅ XZ format enabled");
} else { } else {
crate::archive::warning::show_xz_dependency_warning(); crate::archive::warning::show_xz_dependency_warning();
warn!("⚠️ XZ format disabled (liblzma not found)"); warn!("⚠️ XZ format disabled (liblzma not found)");
} }
} }
// 7z format (unstable library) // 7z format (unstable library)
if self.config.enable_7z { if self.config.enable_7z {
crate::archive::warning::show_7z_stability_warning(); crate::archive::warning::show_7z_stability_warning();
self.processors.insert(ArchiveFormat::SevenZ, Box::new(SevenZProcessor::new())); self.processors
.insert(ArchiveFormat::SevenZ, Box::new(SevenZProcessor::new()));
warn!("⚠️ 7z format enabled (stability warning)"); warn!("⚠️ 7z format enabled (stability warning)");
} }
} }
Ok(()) Ok(())
} }
/// Get processor for detected format (mutable version for open/extraction) /// Get processor for detected format (mutable version for open/extraction)
pub fn get_processor_mut(&mut self, path: &Path) -> Result<&mut (dyn ArchiveProcessor + '_)> { pub fn get_processor_mut(&mut self, path: &Path) -> Result<&mut (dyn ArchiveProcessor + '_)> {
let detector = FormatDetector::new(); let detector = FormatDetector::new();
let format = detector.detect(path)?; let format = detector.detect(path)?;
match self.processors.get_mut(&format) { match self.processors.get_mut(&format) {
Some(p) => Ok(p.as_mut()), Some(p) => Ok(p.as_mut()),
None => Err(anyhow::anyhow!("Format {} not supported or not enabled", format)), None => Err(anyhow::anyhow!(
"Format {} not supported or not enabled",
format
)),
} }
} }
/// Get processor for detected format (immutable version for listing) /// Get processor for detected format (immutable version for listing)
pub fn get_processor(&self, path: &Path) -> Result<&dyn ArchiveProcessor> { pub fn get_processor(&self, path: &Path) -> Result<&dyn ArchiveProcessor> {
let detector = FormatDetector::new(); let detector = FormatDetector::new();
let format = detector.detect(path)?; let format = detector.detect(path)?;
self.processors self.processors
.get(&format) .get(&format)
.map(|p| p.as_ref()) .map(|p| p.as_ref())
.ok_or_else(|| anyhow::anyhow!("Format {} not supported or not enabled", format)) .ok_or_else(|| anyhow::anyhow!("Format {} not supported or not enabled", format))
} }
/// List all enabled formats /// List all enabled formats
pub fn enabled_formats(&self) -> Vec<ArchiveFormat> { pub fn enabled_formats(&self) -> Vec<ArchiveFormat> {
self.processors.keys().cloned().collect() self.processors.keys().cloned().collect()
@@ -141,7 +156,7 @@ impl ProcessorRegistry {
fn check_liblzma_available() -> bool { fn check_liblzma_available() -> bool {
// Try to load xz2 library // Try to load xz2 library
// Simplified check: try to create XzProcessor // Simplified check: try to create XzProcessor
true // Simplified for now, actual implementation needs better detection true // Simplified for now, actual implementation needs better detection
} }
#[cfg(not(feature = "optional-formats"))] #[cfg(not(feature = "optional-formats"))]
@@ -156,13 +171,16 @@ pub fn init_archive_system(config_path: Option<&str>) -> Result<ProcessorRegistr
} else { } else {
ArchiveConfig::default() ArchiveConfig::default()
}; };
// Show startup warnings for optional formats // Show startup warnings for optional formats
crate::archive::warning::show_startup_warnings(&config); crate::archive::warning::show_startup_warnings(&config);
let mut registry = ProcessorRegistry::new(config); let mut registry = ProcessorRegistry::new(config);
registry.initialize()?; registry.initialize()?;
info!("Archive system initialized with {} formats", registry.enabled_formats().len()); info!(
"Archive system initialized with {} formats",
registry.enabled_formats().len()
);
Ok(registry) Ok(registry)
} }
+54 -39
View File
@@ -4,7 +4,7 @@ use anyhow::Result;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
// Re-export types from metadata.rs // Re-export types from metadata.rs
pub use crate::archive::metadata::{ArchiveMetadata, ArchiveEntry, ExtractResult}; pub use crate::archive::metadata::{ArchiveEntry, ArchiveMetadata, ExtractResult};
/// Archive Format Type Enumeration /// Archive Format Type Enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
@@ -19,12 +19,12 @@ pub enum ArchiveFormat {
TarGzip, TarGzip,
TarBzip2, TarBzip2,
TarZstd, TarZstd,
// Optional formats (controversial) // Optional formats (controversial)
Rar, // ⚠️ Legal risk (RARLAB patent) Rar, // ⚠️ Legal risk (RARLAB patent)
Xz, // ⚠️ External dependency (liblzma) Xz, // ⚠️ External dependency (liblzma)
SevenZ, // ⚠️ Unstable library (sevenz-rust 0.21.0) SevenZ, // ⚠️ Unstable library (sevenz-rust 0.21.0)
Unknown, Unknown,
} }
@@ -53,30 +53,34 @@ impl std::fmt::Display for ArchiveFormat {
pub trait ArchiveProcessor: Send + Sync { pub trait ArchiveProcessor: Send + Sync {
/// Format type supported by this processor /// Format type supported by this processor
fn format(&self) -> ArchiveFormat; fn format(&self) -> ArchiveFormat;
/// Open archive file and read metadata /// Open archive file and read metadata
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata>; fn open(&mut self, path: &Path) -> Result<ArchiveMetadata>;
/// List all file entries in archive /// List all file entries in archive
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>>; fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>>;
/// Extract single file (on-demand decompression) /// Extract single file (on-demand decompression)
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64>; fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64>;
/// Extract all files to directory (batch extraction) /// Extract all files to directory (batch extraction)
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult>; fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult>;
/// Check if this processor can handle the format /// Check if this processor can handle the format
fn can_process(format: ArchiveFormat) -> bool where Self: Sized; fn can_process(format: ArchiveFormat) -> bool
where
Self: Sized;
/// Create new processor instance /// Create new processor instance
fn new() -> Self where Self: Sized; fn new() -> Self
where
Self: Sized;
} }
/// Security Validation - Zip Slip Protection /// Security Validation - Zip Slip Protection
pub fn validate_extraction_path(entry_path: &Path, base_dir: &Path) -> Result<PathBuf> { pub fn validate_extraction_path(entry_path: &Path, base_dir: &Path) -> Result<PathBuf> {
use std::path::Component; use std::path::Component;
// 1. Check path components // 1. Check path components
for component in entry_path.components() { for component in entry_path.components() {
match component { match component {
@@ -92,51 +96,62 @@ pub fn validate_extraction_path(entry_path: &Path, base_dir: &Path) -> Result<Pa
Component::Normal(_) | Component::CurDir => {} Component::Normal(_) | Component::CurDir => {}
} }
} }
// 2. Build full path // 2. Build full path
let full_path = base_dir.join(entry_path); let full_path = base_dir.join(entry_path);
// 3. Canonicalize and validate (ensure within base_dir) // 3. Canonicalize and validate (ensure within base_dir)
let canonical_base = base_dir.canonicalize() let canonical_base = base_dir
.canonicalize()
.map_err(|e| anyhow::anyhow!("Cannot canonicalize base dir: {}", e))?; .map_err(|e| anyhow::anyhow!("Cannot canonicalize base dir: {}", e))?;
// Create parent directories first // Create parent directories first
if let Some(parent) = full_path.parent() { if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
// 4. Verify extraction path is within base_dir // 4. Verify extraction path is within base_dir
// Note: full_path may not exist yet, so we check parent directory // Note: full_path may not exist yet, so we check parent directory
if full_path.exists() { if full_path.exists() {
let canonical_full = full_path.canonicalize() let canonical_full = full_path
.canonicalize()
.map_err(|e| anyhow::anyhow!("Cannot canonicalize full path: {}", e))?; .map_err(|e| anyhow::anyhow!("Cannot canonicalize full path: {}", e))?;
if !canonical_full.starts_with(&canonical_base) { if !canonical_full.starts_with(&canonical_base) {
return Err(anyhow::anyhow!("Zip Slip detected: path escapes base directory")); return Err(anyhow::anyhow!(
"Zip Slip detected: path escapes base directory"
));
} }
} else { } else {
// Check parent directory instead // Check parent directory instead
if let Some(parent) = full_path.parent() { if let Some(parent) = full_path.parent() {
let canonical_parent = parent.canonicalize() let canonical_parent = parent
.canonicalize()
.map_err(|e| anyhow::anyhow!("Cannot canonicalize parent: {}", e))?; .map_err(|e| anyhow::anyhow!("Cannot canonicalize parent: {}", e))?;
if !canonical_parent.starts_with(&canonical_base) { if !canonical_parent.starts_with(&canonical_base) {
return Err(anyhow::anyhow!("Zip Slip detected: path escapes base directory")); return Err(anyhow::anyhow!(
"Zip Slip detected: path escapes base directory"
));
} }
} }
} }
Ok(full_path) Ok(full_path)
} }
/// Security Validation - Zip Bomb Protection /// Security Validation - Zip Bomb Protection
pub fn check_decompression_ratio(compressed_size: u64, decompressed_size: u64, max_ratio: u64) -> Result<()> { pub fn check_decompression_ratio(
compressed_size: u64,
decompressed_size: u64,
max_ratio: u64,
) -> Result<()> {
if compressed_size == 0 { if compressed_size == 0 {
return Ok(()); // Empty file, allow return Ok(()); // Empty file, allow
} }
let ratio = decompressed_size / compressed_size; let ratio = decompressed_size / compressed_size;
if ratio > max_ratio { if ratio > max_ratio {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Zip Bomb detected: compression ratio {} exceeds limit {}", "Zip Bomb detected: compression ratio {} exceeds limit {}",
@@ -144,7 +159,7 @@ pub fn check_decompression_ratio(compressed_size: u64, decompressed_size: u64, m
max_ratio max_ratio
)); ));
} }
Ok(()) Ok(())
} }
@@ -157,7 +172,7 @@ pub fn check_file_size_limit(file_size: u64, max_size: u64) -> Result<()> {
max_size / 1024 / 1024 max_size / 1024 / 1024
)); ));
} }
Ok(()) Ok(())
} }
@@ -165,34 +180,34 @@ pub fn check_file_size_limit(file_size: u64, max_size: u64) -> Result<()> {
mod tests { mod tests {
use super::*; use super::*;
use tempfile::TempDir; use tempfile::TempDir;
#[test] #[test]
fn test_zip_slip_protection() { fn test_zip_slip_protection() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path(); let base = temp_dir.path();
// Safe path: should pass // Safe path: should pass
let safe_path = Path::new("safe/file.txt"); let safe_path = Path::new("safe/file.txt");
assert!(validate_extraction_path(safe_path, base).is_ok()); assert!(validate_extraction_path(safe_path, base).is_ok());
// Evil path: should be rejected // Evil path: should be rejected
let evil_path = Path::new("../../etc/passwd"); let evil_path = Path::new("../../etc/passwd");
assert!(validate_extraction_path(evil_path, base).is_err()); assert!(validate_extraction_path(evil_path, base).is_err());
// Absolute path: should be rejected // Absolute path: should be rejected
let abs_path = Path::new("/etc/passwd"); let abs_path = Path::new("/etc/passwd");
assert!(validate_extraction_path(abs_path, base).is_err()); assert!(validate_extraction_path(abs_path, base).is_err());
} }
#[test] #[test]
fn test_zip_bomb_detection() { fn test_zip_bomb_detection() {
// Normal ratio: should pass // Normal ratio: should pass
assert!(check_decompression_ratio(1000, 5000, 1000).is_ok()); assert!(check_decompression_ratio(1000, 5000, 1000).is_ok());
// Zip Bomb ratio: should be rejected // Zip Bomb ratio: should be rejected
assert!(check_decompression_ratio(42_000, 5_000_000_000, 1000).is_err()); assert!(check_decompression_ratio(42_000, 5_000_000_000, 1000).is_err());
} }
#[test] #[test]
fn test_compression_ratio_calculation() { fn test_compression_ratio_calculation() {
let metadata = ArchiveMetadata { let metadata = ArchiveMetadata {
@@ -206,7 +221,7 @@ mod tests {
created_time: None, created_time: None,
modified_time: None, modified_time: None,
}; };
assert_eq!(metadata.actual_ratio(), 2.0); assert_eq!(metadata.actual_ratio(), 2.0);
} }
} }
+296 -173
View File
@@ -1,16 +1,16 @@
// Core Format Processors - ZIP, TAR, GZIP, TAR.GZ Full Implementation // Core Format Processors - ZIP, TAR, GZIP, TAR.GZ Full Implementation
use crate::archive::{
ArchiveProcessor, ArchiveFormat, ArchiveMetadata, ArchiveEntry, ExtractResult,
processor::{validate_extraction_path, check_decompression_ratio, check_file_size_limit},
};
use crate::archive::config::ArchiveConfig; use crate::archive::config::ArchiveConfig;
use anyhow::{Result, anyhow}; use crate::archive::{
processor::{check_decompression_ratio, check_file_size_limit, validate_extraction_path},
ArchiveEntry, ArchiveFormat, ArchiveMetadata, ArchiveProcessor, ExtractResult,
};
use anyhow::{anyhow, Result};
use log::{debug, info, warn};
use std::fs::{create_dir_all, File};
use std::io::{BufWriter, Read};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::{File, create_dir_all};
use std::io::{Read, Write, BufReader, BufWriter};
use std::time::SystemTime; use std::time::SystemTime;
use log::{info, warn, debug};
// ==================== ZIP Processor ==================== // ==================== ZIP Processor ====================
@@ -21,6 +21,12 @@ pub struct ZipProcessor {
config: ArchiveConfig, config: ArchiveConfig,
} }
impl Default for ZipProcessor {
fn default() -> Self {
Self::new()
}
}
impl ZipProcessor { impl ZipProcessor {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -29,7 +35,7 @@ impl ZipProcessor {
config: ArchiveConfig::default(), config: ArchiveConfig::default(),
} }
} }
pub fn with_config(config: ArchiveConfig) -> Self { pub fn with_config(config: ArchiveConfig) -> Self {
Self { Self {
archive: None, archive: None,
@@ -43,7 +49,7 @@ impl ArchiveProcessor for ZipProcessor {
fn format(&self) -> ArchiveFormat { fn format(&self) -> ArchiveFormat {
ArchiveFormat::Zip ArchiveFormat::Zip
} }
fn new() -> Self { fn new() -> Self {
Self { Self {
archive: None, archive: None,
@@ -51,64 +57,72 @@ impl ArchiveProcessor for ZipProcessor {
config: ArchiveConfig::default(), config: ArchiveConfig::default(),
} }
} }
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
info!("Opening ZIP archive: {}", path.display()); info!("Opening ZIP archive: {}", path.display());
let file = File::open(path)?; let file = File::open(path)?;
let archive = zip::ZipArchive::new(file)?; let archive = zip::ZipArchive::new(file)?;
self.archive = Some(archive); self.archive = Some(archive);
self.path = path.to_path_buf(); self.path = path.to_path_buf();
// Extract metadata (need mutable reference for by_index) // Extract metadata (need mutable reference for by_index)
let archive_ref = self.archive.as_mut().unwrap(); let archive_ref = self.archive.as_mut().unwrap();
let total_files = archive_ref.len() as u64; let total_files = archive_ref.len() as u64;
let mut total_size = 0u64; let mut total_size = 0u64;
let mut compressed_size = 0u64; let mut compressed_size = 0u64;
for i in 0..archive_ref.len() { for i in 0..archive_ref.len() {
let file = archive_ref.by_index(i)?; let file = archive_ref.by_index(i)?;
total_size += file.size(); total_size += file.size();
compressed_size += file.compressed_size(); compressed_size += file.compressed_size();
} }
let compression_ratio = if compressed_size > 0 { let compression_ratio = if compressed_size > 0 {
total_size as f64 / compressed_size as f64 total_size as f64 / compressed_size as f64
} else { } else {
0.0 0.0
}; };
// Check for Zip Bomb // Check for Zip Bomb
if compression_ratio > self.config.max_decompression_ratio as f64 { if compression_ratio > self.config.max_decompression_ratio as f64 {
warn!("Potential Zip Bomb detected: ratio {:.1}:1", compression_ratio); warn!(
return Err(anyhow!("Zip Bomb detected: compression ratio {:.1} exceeds limit {}", "Potential Zip Bomb detected: ratio {:.1}:1",
compression_ratio, self.config.max_decompression_ratio)); compression_ratio
);
return Err(anyhow!(
"Zip Bomb detected: compression ratio {:.1} exceeds limit {}",
compression_ratio,
self.config.max_decompression_ratio
));
} }
Ok(ArchiveMetadata { Ok(ArchiveMetadata {
format: ArchiveFormat::Zip, format: ArchiveFormat::Zip,
total_files, total_files,
total_size, total_size,
compressed_size, compressed_size,
compression_ratio, compression_ratio,
is_encrypted: false, // TODO: Check encryption is_encrypted: false, // TODO: Check encryption
is_multi_volume: false, is_multi_volume: false,
created_time: Some(SystemTime::now()), created_time: Some(SystemTime::now()),
modified_time: Some(SystemTime::now()), modified_time: Some(SystemTime::now()),
}) })
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
let archive = self.archive.as_mut() let archive = self
.archive
.as_mut()
.ok_or_else(|| anyhow!("Archive not opened"))?; .ok_or_else(|| anyhow!("Archive not opened"))?;
let mut entries = Vec::new(); let mut entries = Vec::new();
for i in 0..archive.len() { for i in 0..archive.len() {
let file = archive.by_index(i)?; let file = archive.by_index(i)?;
let entry = ArchiveEntry { let entry = ArchiveEntry {
path: PathBuf::from(file.name()), path: PathBuf::from(file.name()),
size: file.size(), size: file.size(),
@@ -116,61 +130,64 @@ impl ArchiveProcessor for ZipProcessor {
is_dir: file.name().ends_with('/'), is_dir: file.name().ends_with('/'),
is_file: !file.name().ends_with('/'), is_file: !file.name().ends_with('/'),
is_encrypted: false, is_encrypted: false,
modified: SystemTime::UNIX_EPOCH, // TODO: Get actual time modified: SystemTime::UNIX_EPOCH, // TODO: Get actual time
permissions: Some(0o644), permissions: Some(0o644),
checksum: None, checksum: None,
}; };
entries.push(entry); entries.push(entry);
} }
info!("Listed {} entries in ZIP archive", entries.len()); info!("Listed {} entries in ZIP archive", entries.len());
Ok(entries) Ok(entries)
} }
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> { fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
let archive = self.archive.as_mut() let archive = self
.archive
.as_mut()
.ok_or_else(|| anyhow!("Archive not opened"))?; .ok_or_else(|| anyhow!("Archive not opened"))?;
let entry_name = entry_path.to_str() let entry_name = entry_path
.to_str()
.ok_or_else(|| anyhow!("Invalid entry path"))?; .ok_or_else(|| anyhow!("Invalid entry path"))?;
let mut file = archive.by_name(entry_name)?; let mut file = archive.by_name(entry_name)?;
// Check file size limit // Check file size limit
check_file_size_limit(file.size(), self.config.max_file_size_mb * 1024 * 1024)?; check_file_size_limit(file.size(), self.config.max_file_size_mb * 1024 * 1024)?;
output.clear(); output.clear();
output.reserve(file.size() as usize); output.reserve(file.size() as usize);
file.read_to_end(output)?; file.read_to_end(output)?;
info!("Extracted file: {} ({} bytes)", entry_name, output.len()); info!("Extracted file: {} ({} bytes)", entry_name, output.len());
Ok(output.len() as u64) Ok(output.len() as u64)
} }
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> { fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> {
create_dir_all(output_dir)?; create_dir_all(output_dir)?;
let mut result = ExtractResult::new(); let mut result = ExtractResult::new();
// Open archive if not already open // Open archive if not already open
if self.archive.is_none() { if self.archive.is_none() {
let file = File::open(&self.path)?; let file = File::open(&self.path)?;
let archive = zip::ZipArchive::new(file)?; let archive = zip::ZipArchive::new(file)?;
self.archive = Some(archive); self.archive = Some(archive);
} }
let archive = self.archive.as_mut().unwrap(); let archive = self.archive.as_mut().unwrap();
result.total_files = archive.len() as u64; result.total_files = archive.len() as u64;
// Use archive iteration to extract files // Use archive iteration to extract files
for i in 0..archive.len() { for i in 0..archive.len() {
let mut file = archive.by_index(i)?; let mut file = archive.by_index(i)?;
let entry_name = file.name().to_string(); let entry_name = file.name().to_string();
let file_size = file.size(); let file_size = file.size();
let is_dir = entry_name.ends_with('/'); let is_dir = entry_name.ends_with('/');
// Zip Slip protection // Zip Slip protection
match validate_extraction_path(&PathBuf::from(&entry_name), output_dir) { match validate_extraction_path(&PathBuf::from(&entry_name), output_dir) {
Ok(safe_path) => { Ok(safe_path) => {
@@ -181,21 +198,24 @@ impl ArchiveProcessor for ZipProcessor {
result.success_files += 1; result.success_files += 1;
} else { } else {
// File // File
check_file_size_limit(file_size, self.config.max_file_size_mb * 1024 * 1024)?; check_file_size_limit(
file_size,
self.config.max_file_size_mb * 1024 * 1024,
)?;
if let Some(parent) = safe_path.parent() { if let Some(parent) = safe_path.parent() {
create_dir_all(parent)?; create_dir_all(parent)?;
} }
// Extract file content // Extract file content
let mut outfile = BufWriter::new(File::create(&safe_path)?); let mut outfile = BufWriter::new(File::create(&safe_path)?);
std::io::copy(&mut file, &mut outfile)?; std::io::copy(&mut file, &mut outfile)?;
result.success_files += 1; result.success_files += 1;
result.total_bytes += file_size; result.total_bytes += file_size;
debug!("Extracted: {} ({} bytes)", entry_name, file_size); debug!("Extracted: {} ({} bytes)", entry_name, file_size);
} }
}, }
Err(e) => { Err(e) => {
warn!("Zip Slip detected: {} - {}", entry_name, e); warn!("Zip Slip detected: {} - {}", entry_name, e);
result.failed_files.push(PathBuf::from(&entry_name)); result.failed_files.push(PathBuf::from(&entry_name));
@@ -203,13 +223,17 @@ impl ArchiveProcessor for ZipProcessor {
} }
} }
} }
info!("Extracted {} files ({} bytes) to {}", info!(
result.success_files, result.total_bytes, output_dir.display()); "Extracted {} files ({} bytes) to {}",
result.success_files,
result.total_bytes,
output_dir.display()
);
Ok(result) Ok(result)
} }
fn can_process(format: ArchiveFormat) -> bool { fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Zip format == ArchiveFormat::Zip
} }
@@ -224,6 +248,12 @@ pub struct TarProcessor {
config: ArchiveConfig, config: ArchiveConfig,
} }
impl Default for TarProcessor {
fn default() -> Self {
Self::new()
}
}
impl TarProcessor { impl TarProcessor {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -232,7 +262,7 @@ impl TarProcessor {
config: ArchiveConfig::default(), config: ArchiveConfig::default(),
} }
} }
pub fn with_config(config: ArchiveConfig) -> Self { pub fn with_config(config: ArchiveConfig) -> Self {
Self { Self {
path: PathBuf::new(), path: PathBuf::new(),
@@ -246,7 +276,7 @@ impl ArchiveProcessor for TarProcessor {
fn format(&self) -> ArchiveFormat { fn format(&self) -> ArchiveFormat {
ArchiveFormat::Tar ArchiveFormat::Tar
} }
fn new() -> Self { fn new() -> Self {
Self { Self {
path: PathBuf::new(), path: PathBuf::new(),
@@ -254,30 +284,30 @@ impl ArchiveProcessor for TarProcessor {
config: ArchiveConfig::default(), config: ArchiveConfig::default(),
} }
} }
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
info!("Opening TAR archive: {}", path.display()); info!("Opening TAR archive: {}", path.display());
self.path = path.to_path_buf(); self.path = path.to_path_buf();
self.entries.clear(); self.entries.clear();
let file = File::open(path)?; let file = File::open(path)?;
let mut archive = tar::Archive::new(file); let mut archive = tar::Archive::new(file);
let mut total_size = 0u64; let mut total_size = 0u64;
// Iterate entries to collect metadata // Iterate entries to collect metadata
for entry in archive.entries()? { for entry in archive.entries()? {
let entry = entry?; let entry = entry?;
let path = entry.path()?.to_path_buf(); let path = entry.path()?.to_path_buf();
let size = entry.size(); let size = entry.size();
total_size += size; total_size += size;
self.entries.push(ArchiveEntry { self.entries.push(ArchiveEntry {
path, path,
size, size,
compressed_size: size, // TAR has no compression compressed_size: size, // TAR has no compression
is_dir: entry.header().entry_type().is_dir(), is_dir: entry.header().entry_type().is_dir(),
is_file: entry.header().entry_type().is_file(), is_file: entry.header().entry_type().is_file(),
is_encrypted: false, is_encrypted: false,
@@ -286,78 +316,87 @@ impl ArchiveProcessor for TarProcessor {
checksum: None, checksum: None,
}); });
} }
let total_files = self.entries.len() as u64; let total_files = self.entries.len() as u64;
Ok(ArchiveMetadata { Ok(ArchiveMetadata {
format: ArchiveFormat::Tar, format: ArchiveFormat::Tar,
total_files, total_files,
total_size, total_size,
compressed_size: total_size, // TAR has no compression compressed_size: total_size, // TAR has no compression
compression_ratio: 1.0, // No compression compression_ratio: 1.0, // No compression
is_encrypted: false, is_encrypted: false,
is_multi_volume: false, is_multi_volume: false,
created_time: Some(SystemTime::now()), created_time: Some(SystemTime::now()),
modified_time: Some(SystemTime::now()), modified_time: Some(SystemTime::now()),
}) })
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
Ok(self.entries.clone()) Ok(self.entries.clone())
} }
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> { fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
// TAR doesn't support random access, need to unpack entire archive // TAR doesn't support random access, need to unpack entire archive
// This is a limitation - for single file extraction, we unpack everything // This is a limitation - for single file extraction, we unpack everything
warn!("TAR format doesn't support random access - extracting entire archive"); warn!("TAR format doesn't support random access - extracting entire archive");
let temp_dir = tempfile::tempdir()?; let temp_dir = tempfile::tempdir()?;
self.extract_all(temp_dir.path())?; self.extract_all(temp_dir.path())?;
let file_path = temp_dir.path().join(entry_path); let file_path = temp_dir.path().join(entry_path);
let mut file = File::open(&file_path)?; let mut file = File::open(&file_path)?;
output.clear(); output.clear();
file.read_to_end(output)?; file.read_to_end(output)?;
Ok(output.len() as u64) Ok(output.len() as u64)
} }
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> { fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> {
create_dir_all(output_dir)?; create_dir_all(output_dir)?;
let file = File::open(&self.path)?; let file = File::open(&self.path)?;
let mut archive = tar::Archive::new(file); let mut archive = tar::Archive::new(file);
let mut result = ExtractResult::new(); let mut result = ExtractResult::new();
result.total_files = self.entries.len() as u64; result.total_files = self.entries.len() as u64;
for entry in archive.entries()? { for entry in archive.entries()? {
let mut entry = entry?; let mut entry = entry?;
let entry_path = entry.path()?.to_path_buf(); let entry_path = entry.path()?.to_path_buf();
let entry_path_str = entry_path.display().to_string(); // Save for warning let entry_path_str = entry_path.display().to_string(); // Save for warning
// Zip Slip protection // Zip Slip protection
match validate_extraction_path(&entry_path, output_dir) { match validate_extraction_path(&entry_path, output_dir) {
Ok(safe_path) => { Ok(safe_path) => {
check_file_size_limit(entry.size(), self.config.max_file_size_mb * 1024 * 1024)?; check_file_size_limit(
entry.size(),
self.config.max_file_size_mb * 1024 * 1024,
)?;
entry.unpack(&safe_path)?; entry.unpack(&safe_path)?;
result.success_files += 1; result.success_files += 1;
result.total_bytes += entry.size(); result.total_bytes += entry.size();
}, }
Err(e) => { Err(e) => {
warn!("Zip Slip detected: {} - {}", entry_path_str, e); warn!("Zip Slip detected: {} - {}", entry_path_str, e);
result.failed_files.push(entry_path); result.failed_files.push(entry_path);
result.warnings.push(format!("Zip Slip: {}", entry_path_str)); result
.warnings
.push(format!("Zip Slip: {}", entry_path_str));
} }
} }
} }
info!("Extracted {} TAR entries to {}", result.success_files, output_dir.display()); info!(
"Extracted {} TAR entries to {}",
result.success_files,
output_dir.display()
);
Ok(result) Ok(result)
} }
fn can_process(format: ArchiveFormat) -> bool { fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Tar format == ArchiveFormat::Tar
} }
@@ -372,6 +411,12 @@ pub struct GzipProcessor {
config: ArchiveConfig, config: ArchiveConfig,
} }
impl Default for GzipProcessor {
fn default() -> Self {
Self::new()
}
}
impl GzipProcessor { impl GzipProcessor {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -380,7 +425,7 @@ impl GzipProcessor {
config: ArchiveConfig::default(), config: ArchiveConfig::default(),
} }
} }
pub fn with_config(config: ArchiveConfig) -> Self { pub fn with_config(config: ArchiveConfig) -> Self {
Self { Self {
path: PathBuf::new(), path: PathBuf::new(),
@@ -394,7 +439,7 @@ impl ArchiveProcessor for GzipProcessor {
fn format(&self) -> ArchiveFormat { fn format(&self) -> ArchiveFormat {
ArchiveFormat::Gzip ArchiveFormat::Gzip
} }
fn new() -> Self { fn new() -> Self {
Self { Self {
path: PathBuf::new(), path: PathBuf::new(),
@@ -402,27 +447,31 @@ impl ArchiveProcessor for GzipProcessor {
config: ArchiveConfig::default(), config: ArchiveConfig::default(),
} }
} }
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
info!("Opening GZIP archive: {}", path.display()); info!("Opening GZIP archive: {}", path.display());
self.path = path.to_path_buf(); self.path = path.to_path_buf();
let file = File::open(path)?; let file = File::open(path)?;
let compressed_size = file.metadata()?.len(); let compressed_size = file.metadata()?.len();
let mut decoder = flate2::read::GzDecoder::new(file); let mut decoder = flate2::read::GzDecoder::new(file);
let mut buffer = Vec::new(); let mut buffer = Vec::new();
decoder.read_to_end(&mut buffer)?; decoder.read_to_end(&mut buffer)?;
self.decompressed_size = buffer.len() as u64; self.decompressed_size = buffer.len() as u64;
// Check Zip Bomb // Check Zip Bomb
check_decompression_ratio(compressed_size, self.decompressed_size, self.config.max_decompression_ratio)?; check_decompression_ratio(
compressed_size,
self.decompressed_size,
self.config.max_decompression_ratio,
)?;
Ok(ArchiveMetadata { Ok(ArchiveMetadata {
format: ArchiveFormat::Gzip, format: ArchiveFormat::Gzip,
total_files: 1, // GZIP is single file total_files: 1, // GZIP is single file
total_size: self.decompressed_size, total_size: self.decompressed_size,
compressed_size, compressed_size,
compression_ratio: if compressed_size > 0 { compression_ratio: if compressed_size > 0 {
@@ -436,58 +485,64 @@ impl ArchiveProcessor for GzipProcessor {
modified_time: Some(SystemTime::now()), modified_time: Some(SystemTime::now()),
}) })
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
// GZIP is single file - infer name from archive name // GZIP is single file - infer name from archive name
let name = self.path.file_name() let name = self
.path
.file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("unknown") .unwrap_or("unknown")
.replace(".gz", "") .replace(".gz", "")
.replace(".gzip", ""); .replace(".gzip", "");
Ok(vec![ArchiveEntry::file( Ok(vec![ArchiveEntry::file(
PathBuf::from(name), PathBuf::from(name),
self.decompressed_size, self.decompressed_size,
0, // GZIP doesn't preserve compressed size per file 0, // GZIP doesn't preserve compressed size per file
)]) )])
} }
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> { fn extract_file(&mut self, _entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
// GZIP is single file - just decompress it // GZIP is single file - just decompress it
let file = File::open(&self.path)?; let file = File::open(&self.path)?;
let mut decoder = flate2::read::GzDecoder::new(file); let mut decoder = flate2::read::GzDecoder::new(file);
output.clear(); output.clear();
decoder.read_to_end(output)?; decoder.read_to_end(output)?;
check_file_size_limit(output.len() as u64, self.config.max_file_size_mb * 1024 * 1024)?; check_file_size_limit(
output.len() as u64,
self.config.max_file_size_mb * 1024 * 1024,
)?;
info!("Decompressed GZIP file: {} bytes", output.len()); info!("Decompressed GZIP file: {} bytes", output.len());
Ok(output.len() as u64) Ok(output.len() as u64)
} }
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> { fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> {
create_dir_all(output_dir)?; create_dir_all(output_dir)?;
let entries = self.list_entries()?; let entries = self.list_entries()?;
let entry = entries.first() let entry = entries
.first()
.ok_or_else(|| anyhow!("No entry in GZIP archive"))?; .ok_or_else(|| anyhow!("No entry in GZIP archive"))?;
let outpath = output_dir.join(&entry.path); let outpath = output_dir.join(&entry.path);
// Zip Slip protection // Zip Slip protection
validate_extraction_path(&entry.path, output_dir)?; validate_extraction_path(&entry.path, output_dir)?;
if let Some(parent) = outpath.parent() { if let Some(parent) = outpath.parent() {
create_dir_all(parent)?; create_dir_all(parent)?;
} }
let file = File::open(&self.path)?; let file = File::open(&self.path)?;
let mut decoder = flate2::read::GzDecoder::new(file); let mut decoder = flate2::read::GzDecoder::new(file);
let mut outfile = BufWriter::new(File::create(&outpath)?); let mut outfile = BufWriter::new(File::create(&outpath)?);
std::io::copy(&mut decoder, &mut outfile)?; std::io::copy(&mut decoder, &mut outfile)?;
let result = ExtractResult { let result = ExtractResult {
total_files: 1, total_files: 1,
total_bytes: self.decompressed_size, total_bytes: self.decompressed_size,
@@ -496,11 +551,11 @@ impl ArchiveProcessor for GzipProcessor {
skipped_files: Vec::new(), skipped_files: Vec::new(),
warnings: Vec::new(), warnings: Vec::new(),
}; };
info!("Decompressed GZIP to: {}", outpath.display()); info!("Decompressed GZIP to: {}", outpath.display());
Ok(result) Ok(result)
} }
fn can_process(format: ArchiveFormat) -> bool { fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Gzip format == ArchiveFormat::Gzip
} }
@@ -514,6 +569,12 @@ pub struct TarGzipProcessor {
config: ArchiveConfig, config: ArchiveConfig,
} }
impl Default for TarGzipProcessor {
fn default() -> Self {
Self::new()
}
}
impl TarGzipProcessor { impl TarGzipProcessor {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -521,7 +582,7 @@ impl TarGzipProcessor {
config: ArchiveConfig::default(), config: ArchiveConfig::default(),
} }
} }
pub fn with_config(config: ArchiveConfig) -> Self { pub fn with_config(config: ArchiveConfig) -> Self {
Self { Self {
gzip_processor: GzipProcessor::with_config(config.clone()), gzip_processor: GzipProcessor::with_config(config.clone()),
@@ -534,32 +595,33 @@ impl ArchiveProcessor for TarGzipProcessor {
fn format(&self) -> ArchiveFormat { fn format(&self) -> ArchiveFormat {
ArchiveFormat::TarGzip ArchiveFormat::TarGzip
} }
fn new() -> Self { fn new() -> Self {
Self { Self {
gzip_processor: GzipProcessor::new(), gzip_processor: GzipProcessor::new(),
config: ArchiveConfig::default(), config: ArchiveConfig::default(),
} }
} }
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
info!("Opening TAR.GZ archive: {}", path.display()); info!("Opening TAR.GZ archive: {}", path.display());
// Step 1: Decompress GZIP // Step 1: Decompress GZIP
let temp_dir = tempfile::tempdir()?; let temp_dir = tempfile::tempdir()?;
self.gzip_processor.open(path)?; self.gzip_processor.open(path)?;
self.gzip_processor.extract_all(temp_dir.path())?; self.gzip_processor.extract_all(temp_dir.path())?;
// Step 2: Open TAR // Step 2: Open TAR
let tar_entries = self.gzip_processor.list_entries()?; let tar_entries = self.gzip_processor.list_entries()?;
let tar_file = tar_entries.first() let tar_file = tar_entries
.first()
.ok_or_else(|| anyhow!("No TAR file in GZIP"))?; .ok_or_else(|| anyhow!("No TAR file in GZIP"))?;
let tar_path = temp_dir.path().join(&tar_file.path); let tar_path = temp_dir.path().join(&tar_file.path);
let mut tar_processor = TarProcessor::with_config(self.config.clone()); let mut tar_processor = TarProcessor::with_config(self.config.clone());
let tar_metadata = tar_processor.open(&tar_path)?; let tar_metadata = tar_processor.open(&tar_path)?;
Ok(ArchiveMetadata { Ok(ArchiveMetadata {
format: ArchiveFormat::TarGzip, format: ArchiveFormat::TarGzip,
total_files: tar_metadata.total_files, total_files: tar_metadata.total_files,
@@ -576,46 +638,47 @@ impl ArchiveProcessor for TarGzipProcessor {
modified_time: Some(SystemTime::now()), modified_time: Some(SystemTime::now()),
}) })
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
// Need to implement properly - this requires decompressing first // Need to implement properly - this requires decompressing first
warn!("TAR.GZ list_entries requires full decompression - consider extract_all instead"); warn!("TAR.GZ list_entries requires full decompression - consider extract_all instead");
Ok(Vec::new()) Ok(Vec::new())
} }
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> { fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
warn!("TAR.GZ extract_file requires full unpacking - inefficient for single file"); warn!("TAR.GZ extract_file requires full unpacking - inefficient for single file");
let temp_dir = tempfile::tempdir()?; let temp_dir = tempfile::tempdir()?;
self.extract_all(temp_dir.path())?; self.extract_all(temp_dir.path())?;
let file_path = temp_dir.path().join(entry_path); let file_path = temp_dir.path().join(entry_path);
let mut file = File::open(&file_path)?; let mut file = File::open(&file_path)?;
output.clear(); output.clear();
file.read_to_end(output)?; file.read_to_end(output)?;
Ok(output.len() as u64) Ok(output.len() as u64)
} }
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> { fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> {
info!("Extracting TAR.GZ to: {}", output_dir.display()); info!("Extracting TAR.GZ to: {}", output_dir.display());
// Step 1: Decompress GZIP to temp // Step 1: Decompress GZIP to temp
let temp_dir = tempfile::tempdir()?; let temp_dir = tempfile::tempdir()?;
self.gzip_processor.extract_all(temp_dir.path())?; self.gzip_processor.extract_all(temp_dir.path())?;
// Step 2: Extract TAR // Step 2: Extract TAR
let tar_entries = self.gzip_processor.list_entries()?; let tar_entries = self.gzip_processor.list_entries()?;
let tar_file = tar_entries.first() let tar_file = tar_entries
.first()
.ok_or_else(|| anyhow!("No TAR file found"))?; .ok_or_else(|| anyhow!("No TAR file found"))?;
let tar_path = temp_dir.path().join(&tar_file.path); let tar_path = temp_dir.path().join(&tar_file.path);
let mut tar_processor = TarProcessor::with_config(self.config.clone()); let mut tar_processor = TarProcessor::with_config(self.config.clone());
tar_processor.open(&tar_path)?; tar_processor.open(&tar_path)?;
tar_processor.extract_all(output_dir) tar_processor.extract_all(output_dir)
} }
fn can_process(format: ArchiveFormat) -> bool { fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::TarGzip format == ArchiveFormat::TarGzip
} }
@@ -627,73 +690,133 @@ impl ArchiveProcessor for TarGzipProcessor {
pub struct ZstdProcessor; pub struct ZstdProcessor;
impl ArchiveProcessor for ZstdProcessor { impl ArchiveProcessor for ZstdProcessor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::Zstd } fn format(&self) -> ArchiveFormat {
ArchiveFormat::Zstd
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("ZSTD processor not yet implemented")) Err(anyhow!("ZSTD processor not yet implemented"))
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) } fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) } Ok(Vec::new())
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) } }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::Zstd } fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
fn new() -> Self { Self } Ok(0)
}
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Zstd
}
fn new() -> Self {
Self
}
} }
/// BZIP2 Processor Stub (Phase 2/3) /// BZIP2 Processor Stub (Phase 2/3)
pub struct Bzip2Processor; pub struct Bzip2Processor;
impl ArchiveProcessor for Bzip2Processor { impl ArchiveProcessor for Bzip2Processor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::Bzip2 } fn format(&self) -> ArchiveFormat {
ArchiveFormat::Bzip2
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("BZIP2 processor not yet implemented")) Err(anyhow!("BZIP2 processor not yet implemented"))
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) } fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) } Ok(Vec::new())
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) } }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::Bzip2 } fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
fn new() -> Self { Self } Ok(0)
}
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Bzip2
}
fn new() -> Self {
Self
}
} }
/// LZ4 Processor Stub (Phase 2/3) /// LZ4 Processor Stub (Phase 2/3)
pub struct Lz4Processor; pub struct Lz4Processor;
impl ArchiveProcessor for Lz4Processor { impl ArchiveProcessor for Lz4Processor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::Lz4 } fn format(&self) -> ArchiveFormat {
ArchiveFormat::Lz4
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("LZ4 processor not yet implemented")) Err(anyhow!("LZ4 processor not yet implemented"))
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) } fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) } Ok(Vec::new())
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) } }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::Lz4 } fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
fn new() -> Self { Self } Ok(0)
}
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Lz4
}
fn new() -> Self {
Self
}
} }
/// TAR.BZ2 Composite Processor Stub (Phase 2/3) /// TAR.BZ2 Composite Processor Stub (Phase 2/3)
pub struct TarBzip2Processor; pub struct TarBzip2Processor;
impl ArchiveProcessor for TarBzip2Processor { impl ArchiveProcessor for TarBzip2Processor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::TarBzip2 } fn format(&self) -> ArchiveFormat {
ArchiveFormat::TarBzip2
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("TAR.BZ2 processor not yet implemented")) Err(anyhow!("TAR.BZ2 processor not yet implemented"))
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) } fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) } Ok(Vec::new())
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) } }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::TarBzip2 } fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
fn new() -> Self { Self } Ok(0)
}
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::TarBzip2
}
fn new() -> Self {
Self
}
} }
/// TAR.ZST Composite Processor Stub (Phase 2/3) /// TAR.ZST Composite Processor Stub (Phase 2/3)
pub struct TarZstdProcessor; pub struct TarZstdProcessor;
impl ArchiveProcessor for TarZstdProcessor { impl ArchiveProcessor for TarZstdProcessor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::TarZstd } fn format(&self) -> ArchiveFormat {
ArchiveFormat::TarZstd
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("TAR.ZST processor not yet implemented")) Err(anyhow!("TAR.ZST processor not yet implemented"))
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) } fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) } Ok(Vec::new())
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) } }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::TarZstd } fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
fn new() -> Self { Self } Ok(0)
} }
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::TarZstd
}
fn new() -> Self {
Self
}
}
@@ -1,13 +1,15 @@
// Optional Format Processors - RAR, XZ, 7z // Optional Format Processors - RAR, XZ, 7z
// All optional formats have warnings displayed when enabled // All optional formats have warnings displayed when enabled
use crate::archive::{ArchiveFormat, ArchiveProcessor, ArchiveMetadata, ArchiveEntry, ExtractResult}; use crate::archive::processor::{check_decompression_ratio, validate_extraction_path};
use crate::archive::warning; use crate::archive::warning;
use crate::archive::processor::{validate_extraction_path, check_decompression_ratio}; use crate::archive::{
use anyhow::{Result, anyhow}; ArchiveEntry, ArchiveFormat, ArchiveMetadata, ArchiveProcessor, ExtractResult,
use std::path::Path; };
use anyhow::{anyhow, Result};
use log::{info, warn};
use std::fs; use std::fs;
use log::{warn, info}; use std::path::Path;
/// RAR Processor - Only Decompression /// RAR Processor - Only Decompression
/// ⚠️ Legal Warning: RARLAB patent, commercial use requires license /// ⚠️ Legal Warning: RARLAB patent, commercial use requires license
@@ -28,54 +30,65 @@ impl ArchiveProcessor for RarProcessor {
fn format(&self) -> ArchiveFormat { fn format(&self) -> ArchiveFormat {
ArchiveFormat::Rar ArchiveFormat::Rar
} }
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
// Show legal warning when RAR is used // Show legal warning when RAR is used
warning::show_rar_legal_warning(); warning::show_rar_legal_warning();
self.archive_path = Some(path.to_path_buf()); self.archive_path = Some(path.to_path_buf());
// Use unrar library to open RAR // Use unrar library to open RAR
// Note: unrar only supports decompression, no compression // Note: unrar only supports decompression, no compression
use unrar::Archive; use unrar::Archive;
let archive = Archive::new(path)?; let archive = Archive::new(path)?;
let entries: Vec<_> = archive.list()?.collect(); let entries: Vec<_> = archive.list()?.collect();
let total_files = entries.len() as u64; let total_files = entries.len() as u64;
let total_size = entries.iter() let total_size = entries
.iter()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.map(|e| e.uncompressed_size) .map(|e| e.uncompressed_size)
.sum(); .sum();
let compressed_size = fs::metadata(path)?.len(); let compressed_size = fs::metadata(path)?.len();
Ok(ArchiveMetadata { Ok(ArchiveMetadata {
format: ArchiveFormat::Rar, format: ArchiveFormat::Rar,
total_files, total_files,
total_size, total_size,
compressed_size, compressed_size,
compression_ratio: if compressed_size > 0 { total_size as f64 / compressed_size as f64 } else { 0.0 }, compression_ratio: if compressed_size > 0 {
is_encrypted: entries.iter().any(|e| e.ok().map_or(false, |e| e.is_encrypted())), total_size as f64 / compressed_size as f64
is_multi_volume: false, // unrar library limitation } else {
0.0
},
is_encrypted: entries
.iter()
.any(|e| e.ok().map_or(false, |e| e.is_encrypted())),
is_multi_volume: false, // unrar library limitation
created_time: None, created_time: None,
modified_time: None, modified_time: None,
}) })
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
use unrar::Archive; use unrar::Archive;
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?; let path = self
.archive_path
.as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
let archive = Archive::new(path)?; let archive = Archive::new(path)?;
let entries: Vec<ArchiveEntry> = archive.list()? let entries: Vec<ArchiveEntry> = archive
.list()?
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.map(|e| ArchiveEntry { .map(|e| ArchiveEntry {
path: PathBuf::from(e.filename), path: PathBuf::from(e.filename),
size: e.uncompressed_size, size: e.uncompressed_size,
compressed_size: 0, // unrar doesn't provide this compressed_size: 0, // unrar doesn't provide this
is_dir: e.is_directory(), is_dir: e.is_directory(),
is_file: !e.is_directory(), is_file: !e.is_directory(),
is_encrypted: e.is_encrypted(), is_encrypted: e.is_encrypted(),
@@ -83,45 +96,49 @@ impl ArchiveProcessor for RarProcessor {
permissions: None, permissions: None,
}) })
.collect(); .collect();
Ok(entries) Ok(entries)
} }
fn extract_file(&self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> { fn extract_file(&self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
// RAR doesn't support random access efficiently // RAR doesn't support random access efficiently
// Need to extract entire archive // Need to extract entire archive
warn!("RAR extract_file requires full extraction (no random access)"); warn!("RAR extract_file requires full extraction (no random access)");
let entries = self.list_entries()?; let entries = self.list_entries()?;
let entry = entries.iter() let entry = entries
.iter()
.find(|e| e.path == entry_path) .find(|e| e.path == entry_path)
.ok_or_else(|| anyhow!("Entry not found: {}", entry_path.display()))?; .ok_or_else(|| anyhow!("Entry not found: {}", entry_path.display()))?;
// Extract to temp dir, then read // Extract to temp dir, then read
let temp_dir = tempfile::tempdir()?; let temp_dir = tempfile::tempdir()?;
self.extract_all(temp_dir.path())?; self.extract_all(temp_dir.path())?;
let extracted_file = temp_dir.path().join(entry_path); let extracted_file = temp_dir.path().join(entry_path);
let content = fs::read(&extracted_file)?; let content = fs::read(&extracted_file)?;
output.extend_from_slice(&content); output.extend_from_slice(&content);
Ok(content.len() as u64) Ok(content.len() as u64)
} }
fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult> { fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult> {
use unrar::Archive; use unrar::Archive;
use unrar::ExtractOption; use unrar::ExtractOption;
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?; let path = self
.archive_path
.as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
// Validate output_dir path // Validate output_dir path
validate_extraction_path(output_dir, output_dir)?; validate_extraction_path(output_dir, output_dir)?;
let mut result = ExtractResult::new(); let mut result = ExtractResult::new();
result.total_files = self.list_entries()?.len() as u64; result.total_files = self.list_entries()?.len() as u64;
let archive = Archive::new(path)?; let archive = Archive::new(path)?;
for entry_result in archive.extract_all(output_dir, ExtractOption::Recurse)? { for entry_result in archive.extract_all(output_dir, ExtractOption::Recurse)? {
match entry_result { match entry_result {
Ok(entry) => { Ok(entry) => {
@@ -135,10 +152,10 @@ impl ArchiveProcessor for RarProcessor {
} }
} }
} }
Ok(result) Ok(result)
} }
fn can_process(format: ArchiveFormat) -> bool { fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Rar format == ArchiveFormat::Rar
} }
@@ -163,57 +180,65 @@ impl ArchiveProcessor for XzProcessor {
fn format(&self) -> ArchiveFormat { fn format(&self) -> ArchiveFormat {
ArchiveFormat::Xz ArchiveFormat::Xz
} }
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
// Check if liblzma is available // Check if liblzma is available
if !check_liblzma_available() { if !check_liblzma_available() {
warning::show_xz_dependency_warning(); warning::show_xz_dependency_warning();
return Err(anyhow!("liblzma library not found, XZ format disabled")); return Err(anyhow!("liblzma library not found, XZ format disabled"));
} }
self.archive_path = Some(path.to_path_buf()); self.archive_path = Some(path.to_path_buf());
use xz2::read::XzDecoder;
use std::io::Read; use std::io::Read;
use xz2::read::XzDecoder;
let file = fs::File::open(path)?; let file = fs::File::open(path)?;
let mut decoder = XzDecoder::new(file); let mut decoder = XzDecoder::new(file);
// Read decompressed size (estimate) // Read decompressed size (estimate)
let mut buffer = Vec::new(); let mut buffer = Vec::new();
decoder.read_to_end(&mut buffer)?; decoder.read_to_end(&mut buffer)?;
let decompressed_size = buffer.len() as u64; let decompressed_size = buffer.len() as u64;
let compressed_size = fs::metadata(path)?.len(); let compressed_size = fs::metadata(path)?.len();
// Check decompression ratio // Check decompression ratio
check_decompression_ratio(compressed_size, decompressed_size, 1000)?; check_decompression_ratio(compressed_size, decompressed_size, 1000)?;
Ok(ArchiveMetadata { Ok(ArchiveMetadata {
format: ArchiveFormat::Xz, format: ArchiveFormat::Xz,
total_files: 1, // XZ is single-file format total_files: 1, // XZ is single-file format
total_size: decompressed_size, total_size: decompressed_size,
compressed_size, compressed_size,
compression_ratio: if compressed_size > 0 { decompressed_size as f64 / compressed_size as f64 } else { 0.0 }, compression_ratio: if compressed_size > 0 {
decompressed_size as f64 / compressed_size as f64
} else {
0.0
},
is_encrypted: false, is_encrypted: false,
is_multi_volume: false, is_multi_volume: false,
created_time: None, created_time: None,
modified_time: None, modified_time: None,
}) })
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
// XZ is single-file, infer filename from archive name // XZ is single-file, infer filename from archive name
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?; let path = self
.archive_path
let filename = path.file_name() .as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
let filename = path
.file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.map(|s| s.strip_suffix(".xz").unwrap_or(s)) .map(|s| s.strip_suffix(".xz").unwrap_or(s))
.unwrap_or("output"); .unwrap_or("output");
Ok(vec![ArchiveEntry { Ok(vec![ArchiveEntry {
path: PathBuf::from(filename), path: PathBuf::from(filename),
size: 0, // Will be determined during extraction size: 0, // Will be determined during extraction
compressed_size: 0, compressed_size: 0,
is_dir: false, is_dir: false,
is_file: true, is_file: true,
@@ -222,48 +247,54 @@ impl ArchiveProcessor for XzProcessor {
permissions: None, permissions: None,
}]) }])
} }
fn extract_file(&self, _entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> { fn extract_file(&self, _entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
use xz2::read::XzDecoder;
use std::io::Read; use std::io::Read;
use xz2::read::XzDecoder;
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?;
let path = self
.archive_path
.as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
let file = fs::File::open(path)?; let file = fs::File::open(path)?;
let mut decoder = XzDecoder::new(file); let mut decoder = XzDecoder::new(file);
decoder.read_to_end(output)?; decoder.read_to_end(output)?;
Ok(output.len() as u64) Ok(output.len() as u64)
} }
fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult> { fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult> {
use xz2::read::XzDecoder;
use std::io::Read; use std::io::Read;
use xz2::read::XzDecoder;
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?;
let path = self
.archive_path
.as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
// Infer output filename // Infer output filename
let entries = self.list_entries()?; let entries = self.list_entries()?;
let output_path = output_dir.join(&entries[0].path); let output_path = output_dir.join(&entries[0].path);
// Validate path // Validate path
validate_extraction_path(&entries[0].path, output_dir)?; validate_extraction_path(&entries[0].path, output_dir)?;
let file = fs::File::open(path)?; let file = fs::File::open(path)?;
let mut decoder = XzDecoder::new(file); let mut decoder = XzDecoder::new(file);
let mut output_file = fs::File::create(&output_path)?; let mut output_file = fs::File::create(&output_path)?;
std::io::copy(&mut decoder, &mut output_file)?; std::io::copy(&mut decoder, &mut output_file)?;
let mut result = ExtractResult::new(); let mut result = ExtractResult::new();
result.success_files = 1; result.success_files = 1;
result.total_files = 1; result.total_files = 1;
result.total_bytes = fs::metadata(&output_path)?.len(); result.total_bytes = fs::metadata(&output_path)?.len();
Ok(result) Ok(result)
} }
fn can_process(format: ArchiveFormat) -> bool { fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Xz && check_liblzma_available() format == ArchiveFormat::Xz && check_liblzma_available()
} }
@@ -286,59 +317,61 @@ impl ArchiveProcessor for SevenZProcessor {
fn format(&self) -> ArchiveFormat { fn format(&self) -> ArchiveFormat {
ArchiveFormat::SevenZ ArchiveFormat::SevenZ
} }
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> { fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
// Show stability warning // Show stability warning
warning::show_7z_stability_warning(); warning::show_7z_stability_warning();
use sevenz_rust::SevenZReader; use sevenz_rust::SevenZReader;
let reader = SevenZReader::new(path)?; let reader = SevenZReader::new(path)?;
let entries = reader.entries()?; let entries = reader.entries()?;
let total_files = entries.len() as u64; let total_files = entries.len() as u64;
let total_size = entries.iter() let total_size = entries.iter().map(|e| e.uncompressed_size as u64).sum();
.map(|e| e.uncompressed_size as u64)
.sum();
let compressed_size = fs::metadata(path)?.len(); let compressed_size = fs::metadata(path)?.len();
Ok(ArchiveMetadata { Ok(ArchiveMetadata {
format: ArchiveFormat::SevenZ, format: ArchiveFormat::SevenZ,
total_files, total_files,
total_size, total_size,
compressed_size, compressed_size,
compression_ratio: if compressed_size > 0 { total_size as f64 / compressed_size as f64 } else { 0.0 }, compression_ratio: if compressed_size > 0 {
total_size as f64 / compressed_size as f64
} else {
0.0
},
is_encrypted: entries.iter().any(|e| e.is_encrypted), is_encrypted: entries.iter().any(|e| e.is_encrypted),
is_multi_volume: false, is_multi_volume: false,
created_time: None, created_time: None,
modified_time: None, modified_time: None,
}) })
} }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
// Note: sevenz-rust doesn't have full entry listing yet // Note: sevenz-rust doesn't have full entry listing yet
// This is a stub returning empty list // This is a stub returning empty list
warn!("7z list_entries not fully implemented (library limitation)"); warn!("7z list_entries not fully implemented (library limitation)");
Ok(Vec::new()) Ok(Vec::new())
} }
fn extract_file(&self, _entry_path: &Path, _output: &mut Vec<u8>) -> Result<u64> { fn extract_file(&self, _entry_path: &Path, _output: &mut Vec<u8>) -> Result<u64> {
warn!("7z extract_file not implemented (library limitation)"); warn!("7z extract_file not implemented (library limitation)");
Err(anyhow!("7z library doesn't support random access")) Err(anyhow!("7z library doesn't support random access"))
} }
fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult> { fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult> {
use sevenz_rust::SevenZReader; use sevenz_rust::SevenZReader;
// Note: sevenz-rust doesn't have full extraction yet // Note: sevenz-rust doesn't have full extraction yet
// This is a stub // This is a stub
warn!("7z extract_all limited (library under development)"); warn!("7z extract_all limited (library under development)");
Ok(ExtractResult::new()) Ok(ExtractResult::new())
} }
fn can_process(format: ArchiveFormat) -> bool { fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::SevenZ format == ArchiveFormat::SevenZ
} }
@@ -369,15 +402,21 @@ pub struct SevenZProcessor;
#[cfg(not(feature = "optional-formats"))] #[cfg(not(feature = "optional-formats"))]
impl RarProcessor { impl RarProcessor {
pub fn new() -> Self { Self } pub fn new() -> Self {
Self
}
} }
#[cfg(not(feature = "optional-formats"))] #[cfg(not(feature = "optional-formats"))]
impl XzProcessor { impl XzProcessor {
pub fn new() -> Self { Self } pub fn new() -> Self {
Self
}
} }
#[cfg(not(feature = "optional-formats"))] #[cfg(not(feature = "optional-formats"))]
impl SevenZProcessor { impl SevenZProcessor {
pub fn new() -> Self { Self } pub fn new() -> Self {
} Self
}
}
@@ -1,31 +1,31 @@
use crate::archive::{ use crate::archive::{
ArchiveProcessor, ArchiveFormat, ArchiveMetadata, ArchiveEntry, ExtractResult,
processors::core::{ZipProcessor, TarProcessor, GzipProcessor, TarGzipProcessor},
processor::{validate_extraction_path, check_decompression_ratio},
config::ArchiveConfig, config::ArchiveConfig,
processor::{check_decompression_ratio, validate_extraction_path},
processors::core::{GzipProcessor, TarGzipProcessor, TarProcessor, ZipProcessor},
ArchiveEntry, ArchiveFormat, ArchiveMetadata, ArchiveProcessor, ExtractResult,
}; };
use tempfile::TempDir; use anyhow::Result;
use std::fs::{File, create_dir_all}; use std::fs::{create_dir_all, File};
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::Result; use tempfile::TempDir;
#[cfg(test)] #[cfg(test)]
mod helpers { mod helpers {
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
pub fn create_test_zip(path: &PathBuf, files: Vec<(&str, &[u8])>) { pub fn create_test_zip(path: &PathBuf, files: Vec<(&str, &[u8])>) {
use std::io::Cursor; use std::io::Cursor;
let mut buffer = Cursor::new(Vec::new()); let mut buffer = Cursor::new(Vec::new());
{ {
let mut zip = zip::ZipWriter::new(&mut buffer); let mut zip = zip::ZipWriter::new(&mut buffer);
let options = zip::write::FileOptions::default() let options = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Stored); .compression_method(zip::CompressionMethod::Stored);
for (name, content) in files { for (name, content) in files {
if name.ends_with('/') { if name.ends_with('/') {
zip.add_directory(name, options).unwrap(); zip.add_directory(name, options).unwrap();
@@ -34,31 +34,31 @@ mod helpers {
zip.write_all(content).unwrap(); zip.write_all(content).unwrap();
} }
} }
zip.finish().unwrap(); zip.finish().unwrap();
} }
let zip_data = buffer.into_inner(); let zip_data = buffer.into_inner();
File::create(path).unwrap().write_all(&zip_data).unwrap(); File::create(path).unwrap().write_all(&zip_data).unwrap();
} }
pub fn create_test_tar(path: &PathBuf, files: Vec<(&str, &[u8])>) { pub fn create_test_tar(path: &PathBuf, files: Vec<(&str, &[u8])>) {
let file = File::create(path).unwrap(); let file = File::create(path).unwrap();
let mut builder = tar::Builder::new(file); let mut builder = tar::Builder::new(file);
for (name, content) in files { for (name, content) in files {
let mut header = tar::Header::new_gnu(); let mut header = tar::Header::new_gnu();
header.set_size(content.len() as u64); header.set_size(content.len() as u64);
header.set_path(name); header.set_path(name);
header.set_mode(0o644); header.set_mode(0o644);
header.set_cksum(); header.set_cksum();
builder.append_data(&mut header, name, content).unwrap(); builder.append_data(&mut header, name, content).unwrap();
} }
builder.finish().unwrap(); builder.finish().unwrap();
} }
pub fn create_test_gzip(path: &PathBuf, content: &[u8]) { pub fn create_test_gzip(path: &PathBuf, content: &[u8]) {
let file = File::create(path).unwrap(); let file = File::create(path).unwrap();
let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default()); let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
@@ -69,74 +69,74 @@ mod helpers {
#[cfg(test)] #[cfg(test)]
mod core_format_tests { mod core_format_tests {
use super::*;
use super::helpers::*; use super::helpers::*;
use super::*;
#[test] #[test]
fn test_zip_processor_basic() { fn test_zip_processor_basic() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let zip_path = temp_dir.path().join("test.zip"); let zip_path = temp_dir.path().join("test.zip");
create_test_zip(&zip_path, vec![("file1.txt", b"hello")]); create_test_zip(&zip_path, vec![("file1.txt", b"hello")]);
let mut processor = ZipProcessor::new(); let mut processor = ZipProcessor::new();
let metadata = processor.open(&zip_path).unwrap(); let metadata = processor.open(&zip_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Zip); assert_eq!(metadata.format, ArchiveFormat::Zip);
assert_eq!(metadata.total_files, 1); assert_eq!(metadata.total_files, 1);
} }
#[test] #[test]
fn test_tar_processor_basic() { fn test_tar_processor_basic() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let tar_path = temp_dir.path().join("test.tar"); let tar_path = temp_dir.path().join("test.tar");
create_test_tar(&tar_path, vec![("file1.txt", b"tar content")]); create_test_tar(&tar_path, vec![("file1.txt", b"tar content")]);
let mut processor = TarProcessor::new(); let mut processor = TarProcessor::new();
let metadata = processor.open(&tar_path).unwrap(); let metadata = processor.open(&tar_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Tar); assert_eq!(metadata.format, ArchiveFormat::Tar);
} }
#[test] #[test]
fn test_gzip_processor_basic() { fn test_gzip_processor_basic() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let gz_path = temp_dir.path().join("test.gz"); let gz_path = temp_dir.path().join("test.gz");
create_test_gzip(&gz_path, b"gzip content here"); create_test_gzip(&gz_path, b"gzip content here");
let mut processor = GzipProcessor::new(); let mut processor = GzipProcessor::new();
let metadata = processor.open(&gz_path).unwrap(); let metadata = processor.open(&gz_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Gzip); assert_eq!(metadata.format, ArchiveFormat::Gzip);
assert_eq!(metadata.total_files, 1); assert_eq!(metadata.total_files, 1);
} }
#[test] #[test]
fn test_validate_extraction_path_safe() { fn test_validate_extraction_path_safe() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path(); let base = temp_dir.path();
let safe_path = PathBuf::from("safe/file.txt"); let safe_path = PathBuf::from("safe/file.txt");
let result = validate_extraction_path(&safe_path, base); let result = validate_extraction_path(&safe_path, base);
assert!(result.is_ok()); assert!(result.is_ok());
let resolved = result.unwrap(); let resolved = result.unwrap();
assert!(resolved.starts_with(base)); assert!(resolved.starts_with(base));
} }
#[test] #[test]
fn test_validate_extraction_path_zip_slip() { fn test_validate_extraction_path_zip_slip() {
let base = PathBuf::from("/tmp/extract"); let base = PathBuf::from("/tmp/extract");
let evil_path = PathBuf::from("../../etc/passwd"); let evil_path = PathBuf::from("../../etc/passwd");
let result = validate_extraction_path(&evil_path, &base); let result = validate_extraction_path(&evil_path, &base);
assert!(result.is_err()); assert!(result.is_err());
} }
#[test] #[test]
fn test_check_decompression_ratio_ok() { fn test_check_decompression_ratio_ok() {
assert!(check_decompression_ratio(1000, 5000, 1000).is_ok()); assert!(check_decompression_ratio(1000, 5000, 1000).is_ok());
} }
#[test] #[test]
fn test_check_decompression_ratio_zip_bomb() { fn test_check_decompression_ratio_zip_bomb() {
assert!(check_decompression_ratio(42_000, 5_000_000_000, 1000).is_err()); assert!(check_decompression_ratio(42_000, 5_000_000_000, 1000).is_err());
@@ -145,39 +145,39 @@ mod core_format_tests {
#[cfg(test)] #[cfg(test)]
mod integration_tests { mod integration_tests {
use super::*;
use super::helpers::*; use super::helpers::*;
use super::*;
use crate::archive::detector::FormatDetector; use crate::archive::detector::FormatDetector;
use crate::archive::ProcessorRegistry; use crate::archive::ProcessorRegistry;
#[test] #[test]
fn test_format_detection_automation() { fn test_format_detection_automation() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let detector = FormatDetector::new(); let detector = FormatDetector::new();
let zip_path = temp_dir.path().join("test.zip"); let zip_path = temp_dir.path().join("test.zip");
create_test_zip(&zip_path, vec![("f.txt", b"z")]); create_test_zip(&zip_path, vec![("f.txt", b"z")]);
assert_eq!(detector.detect(&zip_path).unwrap(), ArchiveFormat::Zip); assert_eq!(detector.detect(&zip_path).unwrap(), ArchiveFormat::Zip);
let tar_path = temp_dir.path().join("test.tar"); let tar_path = temp_dir.path().join("test.tar");
create_test_tar(&tar_path, vec![("f.txt", b"t")]); create_test_tar(&tar_path, vec![("f.txt", b"t")]);
assert_eq!(detector.detect(&tar_path).unwrap(), ArchiveFormat::Tar); assert_eq!(detector.detect(&tar_path).unwrap(), ArchiveFormat::Tar);
let gz_path = temp_dir.path().join("test.gz"); let gz_path = temp_dir.path().join("test.gz");
create_test_gzip(&gz_path, b"g"); create_test_gzip(&gz_path, b"g");
assert_eq!(detector.detect(&gz_path).unwrap(), ArchiveFormat::Gzip); assert_eq!(detector.detect(&gz_path).unwrap(), ArchiveFormat::Gzip);
} }
#[test] #[test]
fn test_processor_registry_integration() { fn test_processor_registry_integration() {
let config = ArchiveConfig::default(); let config = ArchiveConfig::default();
let mut registry = ProcessorRegistry::new(config); let mut registry = ProcessorRegistry::new(config);
registry.initialize().unwrap(); registry.initialize().unwrap();
let formats = registry.enabled_formats(); let formats = registry.enabled_formats();
assert!(formats.contains(&ArchiveFormat::Zip)); assert!(formats.contains(&ArchiveFormat::Zip));
assert!(formats.contains(&ArchiveFormat::Tar)); assert!(formats.contains(&ArchiveFormat::Tar));
assert!(formats.contains(&ArchiveFormat::Gzip)); assert!(formats.contains(&ArchiveFormat::Gzip));
assert!(formats.contains(&ArchiveFormat::TarGzip)); assert!(formats.contains(&ArchiveFormat::TarGzip));
} }
} }
@@ -4,48 +4,46 @@ use std::fs;
use std::io::Read; use std::io::Read;
use tempfile::TempDir; use tempfile::TempDir;
use crate::archive::*;
use crate::archive::processor::check_decompression_ratio; use crate::archive::processor::check_decompression_ratio;
use crate::archive::tests::test_helpers::*; use crate::archive::tests::test_helpers::*;
use crate::archive::*;
#[test] #[test]
fn test_zip_processor_full_workflow() { fn test_zip_processor_full_workflow() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let zip_path = create_test_zip(&temp_dir); let zip_path = create_test_zip(&temp_dir);
// Initialize processor // Initialize processor
let mut processor = processors::core::ZipProcessor::new(); let mut processor = processors::core::ZipProcessor::new();
// Test open // Test open
let metadata = processor.open(&zip_path).unwrap(); let metadata = processor.open(&zip_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Zip); assert_eq!(metadata.format, ArchiveFormat::Zip);
assert_eq!(metadata.total_files, 3); assert_eq!(metadata.total_files, 3);
// Test list_entries // Test list_entries
let entries = processor.list_entries().unwrap(); let entries = processor.list_entries().unwrap();
assert_eq!(entries.len(), 3); assert_eq!(entries.len(), 3);
// Verify entry names // Verify entry names
let names: Vec<&str> = entries.iter() let names: Vec<&str> = entries.iter().map(|e| e.path.to_str().unwrap()).collect();
.map(|e| e.path.to_str().unwrap())
.collect();
assert!(names.contains(&"file1.txt")); assert!(names.contains(&"file1.txt"));
assert!(names.contains(&"file2.txt")); assert!(names.contains(&"file2.txt"));
assert!(names.contains(&"subdir/file3.txt")); assert!(names.contains(&"subdir/file3.txt"));
// Test extract_all // Test extract_all
let extract_dir = temp_dir.path().join("extracted"); let extract_dir = temp_dir.path().join("extracted");
fs::create_dir_all(&extract_dir).unwrap(); fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir).unwrap(); let result = processor.extract_all(&extract_dir).unwrap();
assert_eq!(result.success_files, 3); assert_eq!(result.success_files, 3);
assert_eq!(result.failed_files.len(), 0); assert_eq!(result.failed_files.len(), 0);
// Verify extracted files // Verify extracted files
assert!(extract_dir.join("file1.txt").exists()); assert!(extract_dir.join("file1.txt").exists());
assert!(extract_dir.join("file2.txt").exists()); assert!(extract_dir.join("file2.txt").exists());
assert!(extract_dir.join("subdir/file3.txt").exists()); assert!(extract_dir.join("subdir/file3.txt").exists());
// Verify content // Verify content
let content1 = fs::read_to_string(extract_dir.join("file1.txt")).unwrap(); let content1 = fs::read_to_string(extract_dir.join("file1.txt")).unwrap();
assert_eq!(content1, "content of file 1"); assert_eq!(content1, "content of file 1");
@@ -55,24 +53,24 @@ fn test_zip_processor_full_workflow() {
fn test_tar_processor_full_workflow() { fn test_tar_processor_full_workflow() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let tar_path = create_test_tar(&temp_dir); let tar_path = create_test_tar(&temp_dir);
let mut processor = processors::core::TarProcessor::new(); let mut processor = processors::core::TarProcessor::new();
// Test open // Test open
let metadata = processor.open(&tar_path).unwrap(); let metadata = processor.open(&tar_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Tar); assert_eq!(metadata.format, ArchiveFormat::Tar);
// Test list_entries // Test list_entries
let entries = processor.list_entries().unwrap(); let entries = processor.list_entries().unwrap();
assert!(entries.len() >= 3); // TAR may include directory entries assert!(entries.len() >= 3); // TAR may include directory entries
// Test extract_all // Test extract_all
let extract_dir = temp_dir.path().join("extracted_tar"); let extract_dir = temp_dir.path().join("extracted_tar");
fs::create_dir_all(&extract_dir).unwrap(); fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir).unwrap(); let result = processor.extract_all(&extract_dir).unwrap();
assert!(result.success_files >= 3); assert!(result.success_files >= 3);
// Verify extracted files exist // Verify extracted files exist
assert!(extract_dir.join("file1.txt").exists()); assert!(extract_dir.join("file1.txt").exists());
assert!(extract_dir.join("file2.txt").exists()); assert!(extract_dir.join("file2.txt").exists());
@@ -82,25 +80,25 @@ fn test_tar_processor_full_workflow() {
fn test_gzip_processor_full_workflow() { fn test_gzip_processor_full_workflow() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let gz_path = create_test_gzip(&temp_dir); let gz_path = create_test_gzip(&temp_dir);
let mut processor = processors::core::GzipProcessor::new(); let mut processor = processors::core::GzipProcessor::new();
// Test open // Test open
let metadata = processor.open(&gz_path).unwrap(); let metadata = processor.open(&gz_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Gzip); assert_eq!(metadata.format, ArchiveFormat::Gzip);
assert_eq!(metadata.total_files, 1); // GZIP is single file assert_eq!(metadata.total_files, 1); // GZIP is single file
// Test extract_all // Test extract_all
let extract_dir = temp_dir.path().join("extracted_gz"); let extract_dir = temp_dir.path().join("extracted_gz");
fs::create_dir_all(&extract_dir).unwrap(); fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir).unwrap(); let result = processor.extract_all(&extract_dir).unwrap();
assert_eq!(result.success_files, 1); assert_eq!(result.success_files, 1);
// Verify extracted file (should strip .gz extension) // Verify extracted file (should strip .gz extension)
let extracted_file = extract_dir.join("test.txt"); let extracted_file = extract_dir.join("test.txt");
assert!(extracted_file.exists()); assert!(extracted_file.exists());
// Verify content // Verify content
let content = fs::read_to_string(&extracted_file).unwrap(); let content = fs::read_to_string(&extracted_file).unwrap();
assert_eq!(content, "test gzip content for validation"); assert_eq!(content, "test gzip content for validation");
@@ -110,20 +108,20 @@ fn test_gzip_processor_full_workflow() {
fn test_tar_gz_processor_workflow() { fn test_tar_gz_processor_workflow() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let tar_gz_path = create_test_tar_gz(&temp_dir); let tar_gz_path = create_test_tar_gz(&temp_dir);
let mut processor = processors::core::TarGzipProcessor::new(); let mut processor = processors::core::TarGzipProcessor::new();
// Test open // Test open
let metadata = processor.open(&tar_gz_path).unwrap(); let metadata = processor.open(&tar_gz_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::TarGzip); assert_eq!(metadata.format, ArchiveFormat::TarGzip);
// Test extract_all // Test extract_all
let extract_dir = temp_dir.path().join("extracted_tar_gz"); let extract_dir = temp_dir.path().join("extracted_tar_gz");
fs::create_dir_all(&extract_dir).unwrap(); fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir).unwrap(); let result = processor.extract_all(&extract_dir).unwrap();
assert!(result.success_files >= 2); assert!(result.success_files >= 2);
// Verify extracted TAR files // Verify extracted TAR files
assert!(extract_dir.join("file1.txt").exists()); assert!(extract_dir.join("file1.txt").exists());
assert!(extract_dir.join("file2.txt").exists()); assert!(extract_dir.join("file2.txt").exists());
@@ -132,18 +130,18 @@ fn test_tar_gz_processor_workflow() {
#[test] #[test]
fn test_format_detection_auto() { fn test_format_detection_auto() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
// Test ZIP detection // Test ZIP detection
let zip_path = create_test_zip(&temp_dir); let zip_path = create_test_zip(&temp_dir);
let detector = FormatDetector::new(); let detector = FormatDetector::new();
let format = detector.detect(&zip_path).unwrap(); let format = detector.detect(&zip_path).unwrap();
assert_eq!(format, ArchiveFormat::Zip); assert_eq!(format, ArchiveFormat::Zip);
// Test TAR detection // Test TAR detection
let tar_path = create_test_tar(&temp_dir); let tar_path = create_test_tar(&temp_dir);
let format = detector.detect(&tar_path).unwrap(); let format = detector.detect(&tar_path).unwrap();
assert_eq!(format, ArchiveFormat::Tar); assert_eq!(format, ArchiveFormat::Tar);
// Test GZIP detection // Test GZIP detection
let gz_path = create_test_gzip(&temp_dir); let gz_path = create_test_gzip(&temp_dir);
let format = detector.detect(&gz_path).unwrap(); let format = detector.detect(&gz_path).unwrap();
@@ -155,12 +153,12 @@ fn test_processor_registry_core_formats() {
let config = ArchiveConfig::default(); let config = ArchiveConfig::default();
let mut registry = ProcessorRegistry::new(config); let mut registry = ProcessorRegistry::new(config);
registry.initialize().unwrap(); registry.initialize().unwrap();
let formats = registry.enabled_formats(); let formats = registry.enabled_formats();
// Should have 9 core formats // Should have 9 core formats
assert!(formats.len() >= 4); // At least the ones we implemented assert!(formats.len() >= 4); // At least the ones we implemented
// Verify format support // Verify format support
assert!(formats.contains(&ArchiveFormat::Zip)); assert!(formats.contains(&ArchiveFormat::Zip));
assert!(formats.contains(&ArchiveFormat::Tar)); assert!(formats.contains(&ArchiveFormat::Tar));
@@ -172,20 +170,20 @@ fn test_processor_registry_core_formats() {
fn test_zip_slip_protection() { fn test_zip_slip_protection() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let zip_bomb_data = create_zip_slip_test(); let zip_bomb_data = create_zip_slip_test();
// Write malicious ZIP to file // Write malicious ZIP to file
let evil_zip_path = temp_dir.path().join("evil.zip"); let evil_zip_path = temp_dir.path().join("evil.zip");
fs::write(&evil_zip_path, &zip_bomb_data).unwrap(); fs::write(&evil_zip_path, &zip_bomb_data).unwrap();
let mut processor = processors::core::ZipProcessor::new(); let mut processor = processors::core::ZipProcessor::new();
processor.open(&evil_zip_path).unwrap(); processor.open(&evil_zip_path).unwrap();
// Attempt extraction should fail due to Zip Slip protection // Attempt extraction should fail due to Zip Slip protection
let extract_dir = temp_dir.path().join("should_fail"); let extract_dir = temp_dir.path().join("should_fail");
fs::create_dir_all(&extract_dir).unwrap(); fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir); let result = processor.extract_all(&extract_dir);
// Should either fail or have empty extracted files // Should either fail or have empty extracted files
// (validate_extraction_path prevents malicious paths) // (validate_extraction_path prevents malicious paths)
if result.is_ok() { if result.is_ok() {
@@ -199,11 +197,11 @@ fn test_zip_slip_protection() {
fn test_zip_bomb_detection() { fn test_zip_bomb_detection() {
// Test decompression ratio check // Test decompression ratio check
let result = check_decompression_ratio(42_000, 5_000_000_000, 1000); let result = check_decompression_ratio(42_000, 5_000_000_000, 1000);
assert!(result.is_err()); // Should detect as Zip Bomb assert!(result.is_err()); // Should detect as Zip Bomb
// Test normal ratio // Test normal ratio
let result = check_decompression_ratio(1000, 5000, 1000); let result = check_decompression_ratio(1000, 5000, 1000);
assert!(result.is_ok()); // Normal ratio should pass assert!(result.is_ok()); // Normal ratio should pass
} }
#[test] #[test]
@@ -219,21 +217,21 @@ fn test_metadata_compression_ratio() {
created_time: None, created_time: None,
modified_time: None, modified_time: None,
}; };
assert_eq!(metadata.actual_ratio(), 5.0); // 5000/1000 = 5.0 assert_eq!(metadata.actual_ratio(), 5.0); // 5000/1000 = 5.0
assert!(!metadata.check_zip_bomb(10)); // ratio 5.0 < 10, not a bomb assert!(!metadata.check_zip_bomb(10)); // ratio 5.0 < 10, not a bomb
assert!(metadata.check_zip_bomb(4)); // ratio 5.0 > 4, detected as bomb assert!(metadata.check_zip_bomb(4)); // ratio 5.0 > 4, detected as bomb
} }
#[test] #[test]
fn test_config_validation() { fn test_config_validation() {
let config = ArchiveConfig { let config = ArchiveConfig {
max_decompression_ratio: 5, // Too low max_decompression_ratio: 5, // Too low
..Default::default() ..Default::default()
}; };
assert!(config.validate().is_err()); assert!(config.validate().is_err());
let valid_config = ArchiveConfig::default(); let valid_config = ArchiveConfig::default();
assert!(valid_config.validate().is_ok()); assert!(valid_config.validate().is_ok());
} }
+2 -2
View File
@@ -7,10 +7,10 @@ pub mod test_helpers;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_module_structure() { fn test_module_structure() {
// Test that all test modules exist // Test that all test modules exist
assert!(true); assert!(true);
} }
} }
+49 -35
View File
@@ -1,28 +1,27 @@
use flate2::write::GzEncoder;
use flate2::Compression;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use tempfile::TempDir;
use zip::{ZipWriter, write::FileOptions, CompressionMethod};
use flate2::write::GzEncoder;
use flate2::Compression;
use tar::Builder; use tar::Builder;
use tempfile::TempDir;
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
pub fn create_test_zip(temp_dir: &TempDir) -> PathBuf { pub fn create_test_zip(temp_dir: &TempDir) -> PathBuf {
let zip_path = temp_dir.path().join("test.zip"); let zip_path = temp_dir.path().join("test.zip");
let file = File::create(&zip_path).unwrap(); let file = File::create(&zip_path).unwrap();
let mut zip = ZipWriter::new(file); let mut zip = ZipWriter::new(file);
let options = FileOptions::default() let options = FileOptions::default().compression_method(CompressionMethod::Stored);
.compression_method(CompressionMethod::Stored);
zip.start_file("file1.txt", options).unwrap(); zip.start_file("file1.txt", options).unwrap();
zip.write_all(b"content of file 1").unwrap(); zip.write_all(b"content of file 1").unwrap();
zip.start_file("file2.txt", options).unwrap(); zip.start_file("file2.txt", options).unwrap();
zip.write_all(b"content of file 2").unwrap(); zip.write_all(b"content of file 2").unwrap();
zip.start_file("subdir/file3.txt", options).unwrap(); zip.start_file("subdir/file3.txt", options).unwrap();
zip.write_all(b"content of file 3 in subdir").unwrap(); zip.write_all(b"content of file 3 in subdir").unwrap();
zip.finish().unwrap(); zip.finish().unwrap();
zip_path zip_path
} }
@@ -31,28 +30,38 @@ pub fn create_test_tar(temp_dir: &TempDir) -> PathBuf {
let tar_path = temp_dir.path().join("test.tar"); let tar_path = temp_dir.path().join("test.tar");
let file = File::create(&tar_path).unwrap(); let file = File::create(&tar_path).unwrap();
let mut builder = Builder::new(file); let mut builder = Builder::new(file);
let mut header1 = tar::Header::new_gnu(); let mut header1 = tar::Header::new_gnu();
header1.set_path("file1.txt").unwrap(); header1.set_path("file1.txt").unwrap();
header1.set_size(17); header1.set_size(17);
header1.set_mode(0o644); header1.set_mode(0o644);
header1.set_cksum(); header1.set_cksum();
builder.append_data(&mut header1, "file1.txt", &b"content of file 1"[..]).unwrap(); builder
.append_data(&mut header1, "file1.txt", &b"content of file 1"[..])
.unwrap();
let mut header2 = tar::Header::new_gnu(); let mut header2 = tar::Header::new_gnu();
header2.set_path("file2.txt").unwrap(); header2.set_path("file2.txt").unwrap();
header2.set_size(17); header2.set_size(17);
header2.set_mode(0o644); header2.set_mode(0o644);
header2.set_cksum(); header2.set_cksum();
builder.append_data(&mut header2, "file2.txt", &b"content of file 2"[..]).unwrap(); builder
.append_data(&mut header2, "file2.txt", &b"content of file 2"[..])
.unwrap();
let mut header3 = tar::Header::new_gnu(); let mut header3 = tar::Header::new_gnu();
header3.set_path("subdir/file3.txt").unwrap(); header3.set_path("subdir/file3.txt").unwrap();
header3.set_size(27); header3.set_size(27);
header3.set_mode(0o644); header3.set_mode(0o644);
header3.set_cksum(); header3.set_cksum();
builder.append_data(&mut header3, "subdir/file3.txt", &b"content of file 3 in subdir"[..]).unwrap(); builder
.append_data(
&mut header3,
"subdir/file3.txt",
&b"content of file 3 in subdir"[..],
)
.unwrap();
builder.finish().unwrap(); builder.finish().unwrap();
tar_path tar_path
} }
@@ -61,7 +70,9 @@ pub fn create_test_gzip(temp_dir: &TempDir) -> PathBuf {
let gz_path = temp_dir.path().join("test.txt.gz"); let gz_path = temp_dir.path().join("test.txt.gz");
let file = File::create(&gz_path).unwrap(); let file = File::create(&gz_path).unwrap();
let mut encoder = GzEncoder::new(file, Compression::default()); let mut encoder = GzEncoder::new(file, Compression::default());
encoder.write_all(b"test gzip content for validation").unwrap(); encoder
.write_all(b"test gzip content for validation")
.unwrap();
encoder.finish().unwrap(); encoder.finish().unwrap();
gz_path gz_path
} }
@@ -70,33 +81,37 @@ pub fn create_test_tar_gz(temp_dir: &TempDir) -> PathBuf {
let tar_path = temp_dir.path().join("test.tar"); let tar_path = temp_dir.path().join("test.tar");
let tar_file = File::create(&tar_path).unwrap(); let tar_file = File::create(&tar_path).unwrap();
let mut builder = Builder::new(tar_file); let mut builder = Builder::new(tar_file);
let mut header1 = tar::Header::new_gnu(); let mut header1 = tar::Header::new_gnu();
header1.set_path("file1.txt").unwrap(); header1.set_path("file1.txt").unwrap();
header1.set_size(10); header1.set_size(10);
header1.set_mode(0o644); header1.set_mode(0o644);
header1.set_cksum(); header1.set_cksum();
builder.append_data(&mut header1, "file1.txt", &b"file1 data"[..]).unwrap(); builder
.append_data(&mut header1, "file1.txt", &b"file1 data"[..])
.unwrap();
let mut header2 = tar::Header::new_gnu(); let mut header2 = tar::Header::new_gnu();
header2.set_path("file2.txt").unwrap(); header2.set_path("file2.txt").unwrap();
header2.set_size(10); header2.set_size(10);
header2.set_mode(0o644); header2.set_mode(0o644);
header2.set_cksum(); header2.set_cksum();
builder.append_data(&mut header2, "file2.txt", &b"file2 data"[..]).unwrap(); builder
.append_data(&mut header2, "file2.txt", &b"file2 data"[..])
.unwrap();
builder.finish().unwrap(); builder.finish().unwrap();
let tar_gz_path = temp_dir.path().join("test.tar.gz"); let tar_gz_path = temp_dir.path().join("test.tar.gz");
let gz_file = File::create(&tar_gz_path).unwrap(); let gz_file = File::create(&tar_gz_path).unwrap();
let mut encoder = GzEncoder::new(gz_file, Compression::default()); let mut encoder = GzEncoder::new(gz_file, Compression::default());
let tar_content = std::fs::read(&tar_path).unwrap(); let tar_content = std::fs::read(&tar_path).unwrap();
encoder.write_all(&tar_content).unwrap(); encoder.write_all(&tar_content).unwrap();
encoder.finish().unwrap(); encoder.finish().unwrap();
std::fs::remove_file(&tar_path).unwrap(); std::fs::remove_file(&tar_path).unwrap();
tar_gz_path tar_gz_path
} }
@@ -105,13 +120,12 @@ pub fn create_zip_bomb_test() -> Vec<u8> {
{ {
let writer = std::io::Cursor::new(&mut buffer); let writer = std::io::Cursor::new(&mut buffer);
let mut zip = ZipWriter::new(writer); let mut zip = ZipWriter::new(writer);
let options = FileOptions::default() let options = FileOptions::default().compression_method(CompressionMethod::Stored);
.compression_method(CompressionMethod::Stored);
zip.start_file("bomb.txt", options).unwrap(); zip.start_file("bomb.txt", options).unwrap();
zip.write_all(&[0u8; 100]).unwrap(); zip.write_all(&[0u8; 100]).unwrap();
zip.finish().unwrap(); zip.finish().unwrap();
} }
buffer buffer
@@ -123,11 +137,11 @@ pub fn create_zip_slip_test() -> Vec<u8> {
let writer = std::io::Cursor::new(&mut buffer); let writer = std::io::Cursor::new(&mut buffer);
let mut zip = ZipWriter::new(writer); let mut zip = ZipWriter::new(writer);
let options = FileOptions::default(); let options = FileOptions::default();
zip.start_file("../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../etc/passwd", options).unwrap(); zip.start_file("../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../etc/passwd", options).unwrap();
zip.write_all(b"malicious content").unwrap(); zip.write_all(b"malicious content").unwrap();
zip.finish().unwrap(); zip.finish().unwrap();
} }
buffer buffer
} }
+16 -16
View File
@@ -1,6 +1,6 @@
// Warning System - Legal and Technical Warnings for Optional Formats // Warning System - Legal and Technical Warnings for Optional Formats
use log::{warn, info}; use log::{info, warn};
use crate::archive::config::ArchiveConfig; use crate::archive::config::ArchiveConfig;
@@ -63,25 +63,27 @@ pub fn show_startup_warnings(config: &ArchiveConfig) {
if config.enable_rar { if config.enable_rar {
show_rar_legal_warning(); show_rar_legal_warning();
} }
if config.enable_xz { if config.enable_xz {
// Dependency check happens in ProcessorRegistry // Dependency check happens in ProcessorRegistry
} }
if config.enable_7z { if config.enable_7z {
show_7z_stability_warning(); show_7z_stability_warning();
} }
// Show summary of enabled formats // Show summary of enabled formats
let enabled_optional = [ let enabled_optional = [config.enable_rar, config.enable_xz, config.enable_7z]
config.enable_rar, .iter()
config.enable_xz, .filter(|&x| *x)
config.enable_7z, .count();
].iter().filter(|&x| *x).count();
if enabled_optional > 0 { if enabled_optional > 0 {
info!(""); info!("");
info!("⚠️ {} optional format(s) enabled with warnings shown above", enabled_optional); info!(
"⚠️ {} optional format(s) enabled with warnings shown above",
enabled_optional
);
info!("Core formats (9): ZIP, TAR, GZIP, ZSTD, BZIP2, LZ4, TAR.GZ, TAR.BZ2, TAR.ZST"); info!("Core formats (9): ZIP, TAR, GZIP, ZSTD, BZIP2, LZ4, TAR.GZ, TAR.BZ2, TAR.ZST");
info!(""); info!("");
} }
@@ -89,8 +91,7 @@ pub fn show_startup_warnings(config: &ArchiveConfig) {
/// Generate user-facing legal disclaimer text /// Generate user-facing legal disclaimer text
pub fn generate_rar_legal_disclaimer() -> String { pub fn generate_rar_legal_disclaimer() -> String {
format!( "RAR FORMAT LEGAL DISCLAIMER
"RAR FORMAT LEGAL DISCLAIMER
IMPORTANT WARNING: IMPORTANT WARNING:
@@ -136,6 +137,5 @@ CONTACT:
Last Updated: 2026-06-10 Last Updated: 2026-06-10
Version: 1.0 Version: 1.0
Legal Consultation: [Please consult professional lawyer for commercial use] Legal Consultation: [Please consult professional lawyer for commercial use]
" ".to_string()
) }
}
+4 -4
View File
@@ -52,7 +52,7 @@ impl AuditLogger {
}; };
self.write_entry(&entry)?; self.write_entry(&entry)?;
log::info!( log::info!(
"Audit: {} config {} changed from '{}' to '{}' by {}", "Audit: {} config {} changed from '{}' to '{}' by {}",
config_type, config_type,
@@ -61,7 +61,7 @@ impl AuditLogger {
new_value, new_value,
user user
); );
Ok(()) Ok(())
} }
@@ -126,7 +126,7 @@ impl AuditLogger {
} else { } else {
0 0
}; };
Ok(entries[start..].to_vec()) Ok(entries[start..].to_vec())
} }
} }
+13 -2
View File
@@ -5,7 +5,7 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use uuid::Uuid; use uuid::Uuid;
use crate::provider::{DataProvider, ProviderError}; use crate::provider::DataProvider;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User { pub struct User {
@@ -71,6 +71,12 @@ pub struct AuthState {
pub provider: Option<Arc<dyn DataProvider>>, pub provider: Option<Arc<dyn DataProvider>>,
} }
impl Default for AuthState {
fn default() -> Self {
Self::new()
}
}
impl AuthState { impl AuthState {
pub fn new() -> Self { pub fn new() -> Self {
let mut users = HashMap::new(); let mut users = HashMap::new();
@@ -284,7 +290,12 @@ impl AuthState {
} }
} }
fn login_with_provider(&self, provider: &dyn DataProvider, username: &str, password: &str) -> Option<LoginResponse> { fn login_with_provider(
&self,
provider: &dyn DataProvider,
username: &str,
password: &str,
) -> Option<LoginResponse> {
match provider.get_user(username) { match provider.get_user(username) {
Ok(Some(user)) => { Ok(Some(user)) => {
if user.status != 1 { if user.status != 1 {
+130 -63
View File
@@ -118,14 +118,20 @@ fn get_series_display_name(name: &str) -> String {
pub fn get_all_categories() -> Result<CategoriesResponse> { pub fn get_all_categories() -> Result<CategoriesResponse> {
let conn = FileTree::open_user_db("accusys")?; let conn = FileTree::open_user_db("accusys")?;
let tree = FileTree::load(&conn, "accusys", "categories")?; let tree = FileTree::load(&conn, "accusys", "categories")?;
let categories: Vec<Category> = tree.nodes.iter() let categories: Vec<Category> = tree
.nodes
.iter()
.filter(|n| n.parent_id.is_none() && n.node_type.as_str() == "folder") .filter(|n| n.parent_id.is_none() && n.node_type.as_str() == "folder")
.map(|n| { .map(|n| {
let file_count = tree.nodes.iter() let file_count = tree
.filter(|f| f.parent_id == Some(n.node_id.clone()) && f.node_type.as_str() == "file") .nodes
.iter()
.filter(|f| {
f.parent_id == Some(n.node_id.clone()) && f.node_type.as_str() == "file"
})
.count(); .count();
Category { Category {
name: n.label.clone(), name: n.label.clone(),
display_name: get_category_display_name(&n.label), display_name: get_category_display_name(&n.label),
@@ -135,11 +141,13 @@ pub fn get_all_categories() -> Result<CategoriesResponse> {
} }
}) })
.collect(); .collect();
let total_files = tree.nodes.iter() let total_files = tree
.nodes
.iter()
.filter(|n| n.node_type.as_str() == "file") .filter(|n| n.node_type.as_str() == "file")
.count(); .count();
Ok(CategoriesResponse { Ok(CategoriesResponse {
total_categories: categories.len(), total_categories: categories.len(),
total_files, total_files,
@@ -150,42 +158,65 @@ pub fn get_all_categories() -> Result<CategoriesResponse> {
pub fn get_category_detail(category_name: &str) -> Result<CategoryDetail> { pub fn get_category_detail(category_name: &str) -> Result<CategoryDetail> {
let conn = FileTree::open_user_db("accusys")?; let conn = FileTree::open_user_db("accusys")?;
let tree = FileTree::load(&conn, "accusys", "categories")?; let tree = FileTree::load(&conn, "accusys", "categories")?;
let category_node = tree.nodes.iter() let category_node = tree
.find(|n| n.label == category_name && n.parent_id.is_none() && n.node_type.as_str() == "folder") .nodes
.iter()
.find(|n| {
n.label == category_name && n.parent_id.is_none() && n.node_type.as_str() == "folder"
})
.ok_or_else(|| anyhow::anyhow!("Category not found: {}", category_name))?; .ok_or_else(|| anyhow::anyhow!("Category not found: {}", category_name))?;
let series_groups: Vec<SeriesGroup> = tree.nodes.iter() let series_groups: Vec<SeriesGroup> = tree
.filter(|n| n.parent_id == Some(category_node.node_id.clone()) && n.node_type.as_str() == "folder") .nodes
.iter()
.filter(|n| {
n.parent_id == Some(category_node.node_id.clone()) && n.node_type.as_str() == "folder"
})
.map(|series_node| { .map(|series_node| {
let files: Vec<CategoryFile> = tree.nodes.iter() let files: Vec<CategoryFile> = tree
.filter(|f| f.parent_id == Some(series_node.node_id.clone()) && f.node_type.as_str() == "file") .nodes
.map(|file_node| { .iter()
CategoryFile { .filter(|f| {
filename: file_node.label.clone(), f.parent_id == Some(series_node.node_id.clone())
size: file_node.aliases.get("file_size_display").cloned().unwrap_or_default(), && f.node_type.as_str() == "file"
download_url: file_node.aliases.get("download_url").cloned().unwrap_or_default(), })
sha256: file_node.sha256.clone(), .map(|file_node| CategoryFile {
} filename: file_node.label.clone(),
size: file_node
.aliases
.get("file_size_display")
.cloned()
.unwrap_or_default(),
download_url: file_node
.aliases
.get("download_url")
.cloned()
.unwrap_or_default(),
sha256: file_node.sha256.clone(),
}) })
.collect(); .collect();
SeriesGroup { SeriesGroup {
series_name: series_node.label.clone(), series_name: series_node.label.clone(),
files, files,
} }
}) })
.collect(); .collect();
let file_count = series_groups.iter().map(|g| g.files.len()).sum(); let file_count = series_groups.iter().map(|g| g.files.len()).sum();
Ok(CategoryDetail { Ok(CategoryDetail {
category: Category { category: Category {
name: category_name.to_string(), name: category_name.to_string(),
display_name: get_category_display_name(category_name), display_name: get_category_display_name(category_name),
file_count, file_count,
last_updated: category_node.updated_at.clone(), last_updated: category_node.updated_at.clone(),
description: category_node.aliases.get("description").cloned().unwrap_or_default(), description: category_node
.aliases
.get("description")
.cloned()
.unwrap_or_default(),
}, },
series_groups, series_groups,
}) })
@@ -194,25 +225,31 @@ pub fn get_category_detail(category_name: &str) -> Result<CategoryDetail> {
pub fn get_all_series() -> Result<SeriesResponse> { pub fn get_all_series() -> Result<SeriesResponse> {
let conn = FileTree::open_user_db("accusys")?; let conn = FileTree::open_user_db("accusys")?;
let tree = FileTree::load(&conn, "accusys", "series")?; let tree = FileTree::load(&conn, "accusys", "series")?;
let series: Vec<Series> = tree.nodes.iter() let series: Vec<Series> = tree
.nodes
.iter()
.filter(|n| n.parent_id.is_none() && n.node_type.as_str() == "folder") .filter(|n| n.parent_id.is_none() && n.node_type.as_str() == "folder")
.map(|n| { .map(|n| {
let file_count = tree.nodes.iter() let file_count = tree
.nodes
.iter()
.filter(|f| { .filter(|f| {
let mut current = f.parent_id.clone(); let mut current = f.parent_id.clone();
while let Some(pid) = current { while let Some(pid) = current {
if pid == n.node_id { if pid == n.node_id {
return f.node_type.as_str() == "file"; return f.node_type.as_str() == "file";
} }
current = tree.nodes.iter() current = tree
.nodes
.iter()
.find(|p| p.node_id == pid) .find(|p| p.node_id == pid)
.map(|p| p.parent_id.clone()).flatten(); .and_then(|p| p.parent_id.clone());
} }
false false
}) })
.count(); .count();
Series { Series {
name: n.label.clone(), name: n.label.clone(),
display_name: get_series_display_name(&n.label), display_name: get_series_display_name(&n.label),
@@ -223,11 +260,13 @@ pub fn get_all_series() -> Result<SeriesResponse> {
} }
}) })
.collect(); .collect();
let total_files = tree.nodes.iter() let total_files = tree
.nodes
.iter()
.filter(|n| n.node_type.as_str() == "file") .filter(|n| n.node_type.as_str() == "file")
.count(); .count();
Ok(SeriesResponse { Ok(SeriesResponse {
total_series: series.len(), total_series: series.len(),
total_files, total_files,
@@ -238,45 +277,63 @@ pub fn get_all_series() -> Result<SeriesResponse> {
pub fn get_series_detail(series_name: &str) -> Result<SeriesDetail> { pub fn get_series_detail(series_name: &str) -> Result<SeriesDetail> {
let conn = FileTree::open_user_db("accusys")?; let conn = FileTree::open_user_db("accusys")?;
let tree = FileTree::load(&conn, "accusys", "series")?; let tree = FileTree::load(&conn, "accusys", "series")?;
let series_node = tree.nodes.iter() let series_node = tree
.find(|n| n.label == series_name && n.parent_id.is_none() && n.node_type.as_str() == "folder") .nodes
.iter()
.find(|n| {
n.label == series_name && n.parent_id.is_none() && n.node_type.as_str() == "folder"
})
.ok_or_else(|| anyhow::anyhow!("Series not found: {}", series_name))?; .ok_or_else(|| anyhow::anyhow!("Series not found: {}", series_name))?;
let categories: Vec<SeriesCategory> = tree.nodes.iter() let categories: Vec<SeriesCategory> = tree
.filter(|n| n.parent_id == Some(series_node.node_id.clone()) && n.node_type.as_str() == "folder") .nodes
.iter()
.filter(|n| {
n.parent_id == Some(series_node.node_id.clone()) && n.node_type.as_str() == "folder"
})
.map(|category_node| { .map(|category_node| {
let files: Vec<SeriesFile> = tree.nodes.iter() let files: Vec<SeriesFile> = tree
.nodes
.iter()
.filter(|f| { .filter(|f| {
let mut current = f.parent_id.clone(); let mut current = f.parent_id.clone();
while let Some(pid) = current { while let Some(pid) = current {
if pid == category_node.node_id && f.node_type.as_str() == "file" { if pid == category_node.node_id && f.node_type.as_str() == "file" {
return true; return true;
} }
current = tree.nodes.iter() current = tree
.nodes
.iter()
.find(|p| p.node_id == pid) .find(|p| p.node_id == pid)
.map(|p| p.parent_id.clone()).flatten(); .and_then(|p| p.parent_id.clone());
} }
false false
}) })
.map(|file_node| { .map(|file_node| SeriesFile {
SeriesFile { filename: file_node.label.clone(),
filename: file_node.label.clone(), size: file_node
size: file_node.aliases.get("file_size_display").unwrap_or(&"N/A".to_string()).clone(), .aliases
download_url: file_node.aliases.get("download_url").unwrap_or(&"".to_string()).clone(), .get("file_size_display")
} .unwrap_or(&"N/A".to_string())
.clone(),
download_url: file_node
.aliases
.get("download_url")
.unwrap_or(&"".to_string())
.clone(),
}) })
.collect(); .collect();
SeriesCategory { SeriesCategory {
category_name: category_node.label.clone(), category_name: category_node.label.clone(),
files, files,
} }
}) })
.collect(); .collect();
let file_count = categories.iter().map(|c| c.files.len()).sum(); let file_count = categories.iter().map(|c| c.files.len()).sum();
Ok(SeriesDetail { Ok(SeriesDetail {
series: Series { series: Series {
name: series_name.to_string(), name: series_name.to_string(),
@@ -296,29 +353,39 @@ pub fn search_files(query: &str, view: &str) -> Result<SearchResponse> {
"series" => "series", "series" => "series",
_ => "untitled folder", _ => "untitled folder",
}; };
let conn = FileTree::open_user_db("accusys")?; let conn = FileTree::open_user_db("accusys")?;
let tree = FileTree::load(&conn, "accusys", tree_type)?; let tree = FileTree::load(&conn, "accusys", tree_type)?;
let results: Vec<SearchResult> = tree.nodes.iter() let results: Vec<SearchResult> = tree
.filter(|n| n.node_type.as_str() == "file" && n.label.to_lowercase().contains(&query.to_lowercase())) .nodes
.iter()
.filter(|n| {
n.node_type.as_str() == "file" && n.label.to_lowercase().contains(&query.to_lowercase())
})
.map(|file_node| { .map(|file_node| {
let parent_node = tree.nodes.iter() let parent_node = tree
.nodes
.iter()
.find(|n| n.node_id == file_node.parent_id.clone().unwrap_or_default()); .find(|n| n.node_id == file_node.parent_id.clone().unwrap_or_default());
SearchResult { SearchResult {
category: parent_node.map(|n| n.label.clone()), category: parent_node.map(|n| n.label.clone()),
series: parent_node.map(|n| n.label.clone()), series: parent_node.map(|n| n.label.clone()),
filename: file_node.label.clone(), filename: file_node.label.clone(),
download_url: file_node.aliases.get("download_url").unwrap_or(&"".to_string()).clone(), download_url: file_node
.aliases
.get("download_url")
.unwrap_or(&"".to_string())
.clone(),
} }
}) })
.collect(); .collect();
Ok(SearchResponse { Ok(SearchResponse {
query: query.to_string(), query: query.to_string(),
view: view.to_string(), view: view.to_string(),
total_results: results.len(), total_results: results.len(),
results, results,
}) })
} }
+5 -4
View File
@@ -25,7 +25,7 @@ pub async fn handle_iscsi_command(cmd: IscsiCommand) -> anyhow::Result<()> {
let binary = find_binary("markbase-iscsi"); let binary = find_binary("markbase-iscsi");
let mut cmd_process = std::process::Command::new(&binary); let mut cmd_process = std::process::Command::new(&binary);
cmd_process.arg("iscsi"); cmd_process.arg("iscsi");
match cmd { match cmd {
IscsiCommand::Start { IscsiCommand::Start {
user, user,
@@ -34,7 +34,8 @@ pub async fn handle_iscsi_command(cmd: IscsiCommand) -> anyhow::Result<()> {
force, force,
device, device,
} => { } => {
cmd_process.arg("start") cmd_process
.arg("start")
.args(["--user", &user]) .args(["--user", &user])
.args(["--port", &port.to_string()]) .args(["--port", &port.to_string()])
.args(["--lun-size", &lun_size]); .args(["--lun-size", &lun_size]);
@@ -52,7 +53,7 @@ pub async fn handle_iscsi_command(cmd: IscsiCommand) -> anyhow::Result<()> {
cmd_process.arg("status"); cmd_process.arg("status");
} }
} }
let status = cmd_process.status()?; let status = cmd_process.status()?;
std::process::exit(status.code().unwrap_or(1)); std::process::exit(status.code().unwrap_or(1));
} }
@@ -61,4 +62,4 @@ fn find_binary(name: &str) -> std::path::PathBuf {
let exe = std::env::current_exe().unwrap(); let exe = std::env::current_exe().unwrap();
let dir = exe.parent().unwrap(); let dir = exe.parent().unwrap();
dir.join(name) dir.join(name)
} }
+4 -4
View File
@@ -1,8 +1,8 @@
pub mod web;
pub mod ssh;
pub mod webdav;
pub mod iscsi; pub mod iscsi;
pub mod ssh;
pub mod tree; pub mod tree;
pub mod web;
pub mod webdav;
use clap::Subcommand; use clap::Subcommand;
@@ -29,4 +29,4 @@ pub async fn handle_interface_command(cmd: InterfaceCommands) -> anyhow::Result<
InterfaceCommands::Tree(c) => tree::handle_tree_command(c).await?, InterfaceCommands::Tree(c) => tree::handle_tree_command(c).await?,
} }
Ok(()) Ok(())
} }
+1 -1
View File
@@ -32,4 +32,4 @@ pub async fn handle_ssh_command(cmd: SshCommand) -> anyhow::Result<()> {
} }
} }
Ok(()) Ok(())
} }
+194 -124
View File
@@ -1,6 +1,6 @@
use anyhow::Context;
use clap::Subcommand; use clap::Subcommand;
use rusqlite::Connection; use rusqlite::Connection;
use anyhow::Context;
use uuid::Uuid; use uuid::Uuid;
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -33,12 +33,12 @@ pub enum TreeCommand {
#[arg(short, long)] #[arg(short, long)]
name: String, name: String,
}, },
Folder { Folder {
#[command(subcommand)] #[command(subcommand)]
action: FolderCommand, action: FolderCommand,
}, },
Ls { Ls {
#[arg(short, long)] #[arg(short, long)]
user: String, user: String,
@@ -47,7 +47,7 @@ pub enum TreeCommand {
#[arg(short, long)] #[arg(short, long)]
tree_type: String, tree_type: String,
}, },
Cp { Cp {
#[arg(short, long)] #[arg(short, long)]
user: String, user: String,
@@ -58,7 +58,7 @@ pub enum TreeCommand {
#[arg(short, long)] #[arg(short, long)]
tree_type: String, tree_type: String,
}, },
Mv { Mv {
#[arg(short, long)] #[arg(short, long)]
user: String, user: String,
@@ -113,44 +113,54 @@ pub enum FolderCommand {
pub async fn handle_tree_command(cmd: TreeCommand) -> anyhow::Result<()> { pub async fn handle_tree_command(cmd: TreeCommand) -> anyhow::Result<()> {
match cmd { match cmd {
TreeCommand::Create { name, user, tree_type } => { TreeCommand::Create {
name,
user,
tree_type,
} => {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
let node_id = Uuid::new_v4().to_string(); let node_id = Uuid::new_v4().to_string();
let created_at = chrono::Utc::now().to_rfc3339(); let created_at = chrono::Utc::now().to_rfc3339();
conn.execute( conn.execute(
"INSERT INTO file_nodes (node_id, label, node_type, tree_type, created_at, updated_at) "INSERT INTO file_nodes (node_id, label, node_type, tree_type, created_at, updated_at)
VALUES (?1, ?2, 'folder', ?3, ?4, ?4)", VALUES (?1, ?2, 'folder', ?3, ?4, ?4)",
rusqlite::params![node_id, name, tree_type, created_at] rusqlite::params![node_id, name, tree_type, created_at]
).context("Failed to create tree")?; ).context("Failed to create tree")?;
println!("✓ Tree created: {} (type: {}) for user: {}", name, tree_type, user); println!(
"✓ Tree created: {} (type: {}) for user: {}",
name, tree_type, user
);
println!("✓ Node ID: {}", node_id); println!("✓ Node ID: {}", node_id);
} }
TreeCommand::List { user } => { TreeCommand::List { user } => {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
let mut stmt = conn.prepare( let mut stmt = conn
"SELECT DISTINCT tree_type FROM file_nodes ORDER BY tree_type" .prepare("SELECT DISTINCT tree_type FROM file_nodes ORDER BY tree_type")
).context("Failed to prepare query")?; .context("Failed to prepare query")?;
let tree_types = stmt.query_map([], |row| row.get::<_, String>(0)) let tree_types = stmt
.query_map([], |row| row.get::<_, String>(0))
.context("Failed to query tree types")?; .context("Failed to query tree types")?;
println!("=== Trees for user: {} ===", user); println!("=== Trees for user: {} ===", user);
for tree_type in tree_types { for tree_type in tree_types {
let tt = tree_type?; let tt = tree_type?;
let count: i64 = conn.query_row( let count: i64 = conn
"SELECT COUNT(*) FROM file_nodes WHERE tree_type = ?1", .query_row(
[&tt], "SELECT COUNT(*) FROM file_nodes WHERE tree_type = ?1",
|row| row.get(0) [&tt],
).unwrap_or(0); |row| row.get(0),
)
.unwrap_or(0);
println!(" {} ({} nodes)", tt, count); println!(" {} ({} nodes)", tt, count);
} }
} }
@@ -158,9 +168,9 @@ pub async fn handle_tree_command(cmd: TreeCommand) -> anyhow::Result<()> {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
println!("Importing Markdown files to {} virtual tree...", tree_type); println!("Importing Markdown files to {} virtual tree...", tree_type);
if tree_type == "categories" { if tree_type == "categories" {
crate::import_markdown::import_categories_to_db(&conn, &user, &tree_type)?; crate::import_markdown::import_categories_to_db(&conn, &user, &tree_type)?;
println!("✓ Categories imported successfully!"); println!("✓ Categories imported successfully!");
@@ -168,53 +178,66 @@ pub async fn handle_tree_command(cmd: TreeCommand) -> anyhow::Result<()> {
crate::import_markdown::import_series_to_db(&conn, &user, &tree_type)?; crate::import_markdown::import_series_to_db(&conn, &user, &tree_type)?;
println!("✓ Series imported successfully!"); println!("✓ Series imported successfully!");
} else { } else {
eprintln!("Invalid tree_type: {}. Use 'categories' or 'series'", tree_type); eprintln!(
"Invalid tree_type: {}. Use 'categories' or 'series'",
tree_type
);
} }
} }
TreeCommand::Delete { user, name } => { TreeCommand::Delete { user, name } => {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
conn.execute( conn.execute(
"DELETE FROM file_nodes WHERE label = ?1 AND node_type = 'folder'", "DELETE FROM file_nodes WHERE label = ?1 AND node_type = 'folder'",
[&name] [&name],
).context("Failed to delete tree")?; )
.context("Failed to delete tree")?;
println!("✓ Tree deleted: {} for user: {}", name, user); println!("✓ Tree deleted: {} for user: {}", name, user);
} }
TreeCommand::Folder { action } => { TreeCommand::Folder { action } => {
handle_folder_command(action)?; handle_folder_command(action)?;
} }
TreeCommand::Ls { user, path, tree_type } => { TreeCommand::Ls {
user,
path,
tree_type,
} => {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
let parent_id = find_node_id(&conn, &path, &tree_type)?; let parent_id = find_node_id(&conn, &path, &tree_type)?;
let mut stmt = conn.prepare( let mut stmt = conn
"SELECT label, node_type, file_size FROM file_nodes .prepare(
"SELECT label, node_type, file_size FROM file_nodes
WHERE parent_id = ?1 AND tree_type = ?2 WHERE parent_id = ?1 AND tree_type = ?2
ORDER BY node_type DESC, label ASC" ORDER BY node_type DESC, label ASC",
).context("Failed to prepare ls query")?; )
.context("Failed to prepare ls query")?;
let entries = stmt.query_map(
rusqlite::params![parent_id, tree_type], let entries = stmt
|row| Ok(( .query_map(rusqlite::params![parent_id, tree_type], |row| {
row.get::<_, String>(0)?, Ok((
row.get::<_, String>(1)?, row.get::<_, String>(0)?,
row.get::<_, Option<i64>>(2)? row.get::<_, String>(1)?,
)) row.get::<_, Option<i64>>(2)?,
).context("Failed to query entries")?; ))
})
.context("Failed to query entries")?;
println!("=== Contents of {} (tree_type: {}) ===", path, tree_type); println!("=== Contents of {} (tree_type: {}) ===", path, tree_type);
for entry in entries { for entry in entries {
let (name, node_type, size) = entry?; let (name, node_type, size) = entry?;
let size_str = size.map(|s| format!("{} bytes", s)).unwrap_or_else(|| "-".to_string()); let size_str = size
.map(|s| format!("{} bytes", s))
.unwrap_or_else(|| "-".to_string());
if node_type == "folder" { if node_type == "folder" {
println!(" 📁 {} ({})", name, size_str); println!(" 📁 {} ({})", name, size_str);
} else { } else {
@@ -222,57 +245,72 @@ pub async fn handle_tree_command(cmd: TreeCommand) -> anyhow::Result<()> {
} }
} }
} }
TreeCommand::Cp { user, source, target, tree_type } => { TreeCommand::Cp {
user,
source,
target,
tree_type,
} => {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
let source_id = find_node_id(&conn, &source, &tree_type)?; let source_id = find_node_id(&conn, &source, &tree_type)?;
let target_parent_id = find_node_id(&conn, &target, &tree_type)?; let target_parent_id = find_node_id(&conn, &target, &tree_type)?;
let (label, node_type, aliases_json, file_uuid, sha256, file_size) = conn.query_row( let (label, node_type, aliases_json, file_uuid, sha256, file_size) = conn
"SELECT label, node_type, aliases_json, file_uuid, sha256, file_size .query_row(
"SELECT label, node_type, aliases_json, file_uuid, sha256, file_size
FROM file_nodes WHERE node_id = ?1", FROM file_nodes WHERE node_id = ?1",
[&source_id], [&source_id],
|row| Ok(( |row| {
row.get::<_, String>(0)?, Ok((
row.get::<_, String>(1)?, row.get::<_, String>(0)?,
row.get::<_, String>(2)?, row.get::<_, String>(1)?,
row.get::<_, Option<String>>(3)?, row.get::<_, String>(2)?,
row.get::<_, Option<String>>(4)?, row.get::<_, Option<String>>(3)?,
row.get::<_, Option<i64>>(5)? row.get::<_, Option<String>>(4)?,
)) row.get::<_, Option<i64>>(5)?,
).context("Failed to get source node")?; ))
},
)
.context("Failed to get source node")?;
let new_id = Uuid::new_v4().to_string(); let new_id = Uuid::new_v4().to_string();
let created_at = chrono::Utc::now().to_rfc3339(); let created_at = chrono::Utc::now().to_rfc3339();
conn.execute( conn.execute(
"INSERT INTO file_nodes "INSERT INTO file_nodes
(node_id, label, aliases_json, file_uuid, sha256, parent_id, node_type, file_size, tree_type, created_at, updated_at) (node_id, label, aliases_json, file_uuid, sha256, parent_id, node_type, file_size, tree_type, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10)",
rusqlite::params![new_id, label, aliases_json, file_uuid, sha256, target_parent_id, node_type, file_size, tree_type, created_at] rusqlite::params![new_id, label, aliases_json, file_uuid, sha256, target_parent_id, node_type, file_size, tree_type, created_at]
).context("Failed to copy node")?; ).context("Failed to copy node")?;
println!("✓ Copied {} to {} (new ID: {})", source, target, new_id); println!("✓ Copied {} to {} (new ID: {})", source, target, new_id);
} }
TreeCommand::Mv { user, source, target, tree_type } => { TreeCommand::Mv {
user,
source,
target,
tree_type,
} => {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
let source_id = find_node_id(&conn, &source, &tree_type)?; let source_id = find_node_id(&conn, &source, &tree_type)?;
let target_parent_id = find_node_id(&conn, &target, &tree_type)?; let target_parent_id = find_node_id(&conn, &target, &tree_type)?;
let updated_at = chrono::Utc::now().to_rfc3339(); let updated_at = chrono::Utc::now().to_rfc3339();
conn.execute( conn.execute(
"UPDATE file_nodes SET parent_id = ?1, updated_at = ?2 WHERE node_id = ?3", "UPDATE file_nodes SET parent_id = ?1, updated_at = ?2 WHERE node_id = ?3",
rusqlite::params![target_parent_id, updated_at, source_id] rusqlite::params![target_parent_id, updated_at, source_id],
).context("Failed to move node")?; )
.context("Failed to move node")?;
println!("✓ Moved {} to {}", source, target); println!("✓ Moved {} to {}", source, target);
} }
} }
@@ -281,104 +319,136 @@ pub async fn handle_tree_command(cmd: TreeCommand) -> anyhow::Result<()> {
fn handle_folder_command(cmd: FolderCommand) -> anyhow::Result<()> { fn handle_folder_command(cmd: FolderCommand) -> anyhow::Result<()> {
match cmd { match cmd {
FolderCommand::Create { user, path, name, tree_type } => { FolderCommand::Create {
user,
path,
name,
tree_type,
} => {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
let parent_id = if path == "/" || path == "" { let parent_id = if path == "/" || path.is_empty() {
None None
} else { } else {
Some(find_node_id(&conn, &path, &tree_type)?) Some(find_node_id(&conn, &path, &tree_type)?)
}; };
let node_id = Uuid::new_v4().to_string(); let node_id = Uuid::new_v4().to_string();
let created_at = chrono::Utc::now().to_rfc3339(); let created_at = chrono::Utc::now().to_rfc3339();
conn.execute( conn.execute(
"INSERT INTO file_nodes "INSERT INTO file_nodes
(node_id, label, parent_id, node_type, tree_type, created_at, updated_at) (node_id, label, parent_id, node_type, tree_type, created_at, updated_at)
VALUES (?1, ?2, ?3, 'folder', ?4, ?5, ?5)", VALUES (?1, ?2, ?3, 'folder', ?4, ?5, ?5)",
rusqlite::params![node_id, name, parent_id, tree_type, created_at] rusqlite::params![node_id, name, parent_id, tree_type, created_at],
).context("Failed to create folder")?; )
.context("Failed to create folder")?;
println!("✓ Folder created: {} in {} (tree_type: {})", name, path, tree_type);
println!(
"✓ Folder created: {} in {} (tree_type: {})",
name, path, tree_type
);
println!("✓ Node ID: {}", node_id); println!("✓ Node ID: {}", node_id);
} }
FolderCommand::Delete { user, path, name, tree_type } => { FolderCommand::Delete {
user,
path,
name,
tree_type,
} => {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
let folder_path = if path == "/" || path == "" { let folder_path = if path == "/" || path.is_empty() {
name.clone() name.clone()
} else { } else {
format!("{}/{}", path, name) format!("{}/{}", path, name)
}; };
let folder_id = find_node_id(&conn, &folder_path, &tree_type)?; let folder_id = find_node_id(&conn, &folder_path, &tree_type)?;
conn.execute( conn.execute(
"DELETE FROM file_nodes WHERE node_id = ?1 OR parent_id = ?1", "DELETE FROM file_nodes WHERE node_id = ?1 OR parent_id = ?1",
[&folder_id] [&folder_id],
).context("Failed to delete folder and children")?; )
.context("Failed to delete folder and children")?;
println!("✓ Folder deleted: {} in {} (tree_type: {})", name, path, tree_type);
println!(
"✓ Folder deleted: {} in {} (tree_type: {})",
name, path, tree_type
);
} }
FolderCommand::Rename { user, path, old_name, new_name, tree_type } => { FolderCommand::Rename {
user,
path,
old_name,
new_name,
tree_type,
} => {
let db_path = format!("data/users/{}.sqlite", user); let db_path = format!("data/users/{}.sqlite", user);
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?; .with_context(|| format!("Failed to open database: {}", db_path))?;
let folder_path = if path == "/" || path == "" { let folder_path = if path == "/" || path.is_empty() {
old_name.clone() old_name.clone()
} else { } else {
format!("{}/{}", path, old_name) format!("{}/{}", path, old_name)
}; };
let folder_id = find_node_id(&conn, &folder_path, &tree_type)?; let folder_id = find_node_id(&conn, &folder_path, &tree_type)?;
let updated_at = chrono::Utc::now().to_rfc3339(); let updated_at = chrono::Utc::now().to_rfc3339();
conn.execute( conn.execute(
"UPDATE file_nodes SET label = ?1, updated_at = ?2 WHERE node_id = ?3", "UPDATE file_nodes SET label = ?1, updated_at = ?2 WHERE node_id = ?3",
rusqlite::params![new_name, updated_at, folder_id] rusqlite::params![new_name, updated_at, folder_id],
).context("Failed to rename folder")?; )
.context("Failed to rename folder")?;
println!("✓ Folder renamed: {} → {} in {} (tree_type: {})", old_name, new_name, path, tree_type);
println!(
"✓ Folder renamed: {} → {} in {} (tree_type: {})",
old_name, new_name, path, tree_type
);
} }
} }
Ok(()) Ok(())
} }
fn find_node_id(conn: &Connection, path: &str, tree_type: &str) -> anyhow::Result<String> { fn find_node_id(conn: &Connection, path: &str, tree_type: &str) -> anyhow::Result<String> {
if path == "/" || path == "" { if path == "/" || path.is_empty() {
let node_id: String = conn.query_row( let node_id: String = conn
"SELECT node_id FROM file_nodes .query_row(
"SELECT node_id FROM file_nodes
WHERE parent_id IS NULL AND node_type = 'folder' AND tree_type = ?1 WHERE parent_id IS NULL AND node_type = 'folder' AND tree_type = ?1
LIMIT 1", LIMIT 1",
[tree_type], [tree_type],
|row| row.get(0) |row| row.get(0),
).context("Failed to find root folder")?; )
.context("Failed to find root folder")?;
return Ok(node_id); return Ok(node_id);
} }
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut current_parent: Option<String> = None; let mut current_parent: Option<String> = None;
for part in parts { for part in parts {
let node_id: String = conn.query_row( let node_id: String = conn
"SELECT node_id FROM file_nodes .query_row(
"SELECT node_id FROM file_nodes
WHERE label = ?1 AND tree_type = ?2 AND WHERE label = ?1 AND tree_type = ?2 AND
(parent_id = ?3 OR (?3 IS NULL AND parent_id IS NULL))", (parent_id = ?3 OR (?3 IS NULL AND parent_id IS NULL))",
rusqlite::params![part, tree_type, current_parent], rusqlite::params![part, tree_type, current_parent],
|row| row.get(0) |row| row.get(0),
).context(format!("Failed to find node: {}", part))?; )
.context(format!("Failed to find node: {}", part))?;
current_parent = Some(node_id); current_parent = Some(node_id);
} }
current_parent.context("Failed to find node ID for path") current_parent.context("Failed to find node ID for path")
} }
+1 -1
View File
@@ -18,4 +18,4 @@ pub async fn handle_web_command(cmd: WebCommand) -> anyhow::Result<()> {
} }
} }
Ok(()) Ok(())
} }
+5 -5
View File
@@ -1,5 +1,5 @@
use clap::Subcommand;
use axum::{extract::Request, response::IntoResponse, Extension}; use axum::{extract::Request, response::IntoResponse, Extension};
use clap::Subcommand;
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum WebdavCommand { pub enum WebdavCommand {
@@ -28,7 +28,7 @@ pub async fn handle_webdav_command(cmd: WebdavCommand) -> anyhow::Result<()> {
println!("User: {}", user); println!("User: {}", user);
println!("Port: {}", port); println!("Port: {}", port);
println!("Database: {}", db_path.display()); println!("Database: {}", db_path.display());
println!(""); println!();
run_webdav_server(port, user, db_path).await?; run_webdav_server(port, user, db_path).await?;
} }
@@ -41,7 +41,7 @@ async fn run_webdav_server(
user: String, user: String,
db_path: std::path::PathBuf, db_path: std::path::PathBuf,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
use axum::{extract::Request, response::IntoResponse, routing::any, Extension, Router}; use axum::{routing::any, Extension, Router};
use tokio::net::TcpListener; use tokio::net::TcpListener;
let webdav = markbase_webdav::webdav::MarkBaseWebDAV::new(user, db_path); let webdav = markbase_webdav::webdav::MarkBaseWebDAV::new(user, db_path);
@@ -58,7 +58,7 @@ async fn run_webdav_server(
println!("WebDAV server listening on http://{}", addr); println!("WebDAV server listening on http://{}", addr);
println!("Mount point: /webdav"); println!("Mount point: /webdav");
println!(""); println!();
println!("Press Ctrl+C to stop"); println!("Press Ctrl+C to stop");
axum::serve(listener, app).await?; axum::serve(listener, app).await?;
@@ -71,4 +71,4 @@ async fn handle_dav(
req: Request, req: Request,
) -> impl IntoResponse { ) -> impl IntoResponse {
dav.handle(req).await dav.handle(req).await
} }
+30 -28
View File
@@ -1,6 +1,6 @@
use anyhow::Context;
use clap::Subcommand; use clap::Subcommand;
use rusqlite::Connection; use rusqlite::Connection;
use anyhow::Context;
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum AuthCommand { pub enum AuthCommand {
@@ -24,29 +24,30 @@ pub fn handle_auth_command(cmd: AuthCommand) -> anyhow::Result<()> {
match cmd { match cmd {
AuthCommand::Login { user, password } => { AuthCommand::Login { user, password } => {
let db_path = "data/auth.sqlite"; let db_path = "data/auth.sqlite";
if !std::path::Path::new(db_path).exists() { if !std::path::Path::new(db_path).exists() {
return Err(anyhow::anyhow!("Auth database not found: {}", db_path)); return Err(anyhow::anyhow!("Auth database not found: {}", db_path));
} }
let conn = Connection::open(db_path) let conn = Connection::open(db_path).context("Failed to open auth database")?;
.context("Failed to open auth database")?;
let password_hash: String = conn
let password_hash: String = conn.query_row( .query_row(
"SELECT password_hash FROM sftpgo_users WHERE username = ?", "SELECT password_hash FROM sftpgo_users WHERE username = ?",
[&user], [&user],
|row| row.get(0) |row| row.get(0),
).context("Failed to query password hash")?; )
.context("Failed to query password hash")?;
let valid = bcrypt::verify(&password, &password_hash)
.context("Failed to verify password")?; let valid =
bcrypt::verify(&password, &password_hash).context("Failed to verify password")?;
if !valid { if !valid {
return Err(anyhow::anyhow!("Invalid password for user: {}", user)); return Err(anyhow::anyhow!("Invalid password for user: {}", user));
} }
let token = generate_simple_token(&user); let token = generate_simple_token(&user);
println!("✓ Login successful for user: {}", user); println!("✓ Login successful for user: {}", user);
println!("✓ Token: {}", token); println!("✓ Token: {}", token);
println!("Note: This is a simple token for demonstration. Use JWT in production."); println!("Note: This is a simple token for demonstration. Use JWT in production.");
@@ -57,7 +58,7 @@ pub fn handle_auth_command(cmd: AuthCommand) -> anyhow::Result<()> {
} }
AuthCommand::Verify { token } => { AuthCommand::Verify { token } => {
let user = verify_simple_token(&token)?; let user = verify_simple_token(&token)?;
println!("✓ Token valid for user: {}", user); println!("✓ Token valid for user: {}", user);
println!("Note: This is simple token verification. Use JWT in production."); println!("Note: This is simple token verification. Use JWT in production.");
} }
@@ -67,37 +68,38 @@ pub fn handle_auth_command(cmd: AuthCommand) -> anyhow::Result<()> {
fn generate_simple_token(user: &str) -> String { fn generate_simple_token(user: &str) -> String {
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now() let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs(); .as_secs();
format!("{}_{}", user, timestamp) format!("{}_{}", user, timestamp)
} }
fn verify_simple_token(token: &str) -> anyhow::Result<String> { fn verify_simple_token(token: &str) -> anyhow::Result<String> {
let parts: Vec<&str> = token.split('_').collect(); let parts: Vec<&str> = token.split('_').collect();
if parts.len() < 2 { if parts.len() < 2 {
return Err(anyhow::anyhow!("Invalid token format")); return Err(anyhow::anyhow!("Invalid token format"));
} }
let user = parts[0]; let user = parts[0];
let timestamp_str = parts[1]; let timestamp_str = parts[1];
let timestamp: u64 = timestamp_str.parse() let timestamp: u64 = timestamp_str
.parse()
.context("Failed to parse token timestamp")?; .context("Failed to parse token timestamp")?;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now() let now = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs(); .as_secs();
if now - timestamp > 86400 { if now - timestamp > 86400 {
return Err(anyhow::anyhow!("Token expired (valid for 24 hours)")); return Err(anyhow::anyhow!("Token expired (valid for 24 hours)"));
} }
Ok(user.to_string()) Ok(user.to_string())
} }
+10 -4
View File
@@ -45,7 +45,9 @@ pub fn handle_config_command(cmd: ConfigCommand) -> anyhow::Result<()> {
let config_path = Path::new("config/markbase.toml"); let config_path = Path::new("config/markbase.toml");
if !config_path.exists() { if !config_path.exists() {
println!("Configuration file not found. Run 'markbase metadata config init' first."); println!(
"Configuration file not found. Run 'markbase metadata config init' first."
);
return Ok(()); return Ok(());
} }
@@ -61,7 +63,9 @@ pub fn handle_config_command(cmd: ConfigCommand) -> anyhow::Result<()> {
let config_path = Path::new("config/markbase.toml"); let config_path = Path::new("config/markbase.toml");
if !config_path.exists() { if !config_path.exists() {
println!("Configuration file not found. Run 'markbase metadata config init' first."); println!(
"Configuration file not found. Run 'markbase metadata config init' first."
);
return Ok(()); return Ok(());
} }
@@ -86,7 +90,9 @@ pub fn handle_config_command(cmd: ConfigCommand) -> anyhow::Result<()> {
let config_path = Path::new("config/markbase.toml"); let config_path = Path::new("config/markbase.toml");
if !config_path.exists() { if !config_path.exists() {
println!("Configuration file not found. Run 'markbase metadata config init' first."); println!(
"Configuration file not found. Run 'markbase metadata config init' first."
);
return Ok(()); return Ok(());
} }
@@ -115,4 +121,4 @@ fn show_section(config: &crate::config::MarkBaseConfig, section: &str) {
"logging" => println!("{}", toml::to_string_pretty(&config.logging).unwrap()), "logging" => println!("{}", toml::to_string_pretty(&config.logging).unwrap()),
_ => println!("Invalid section: {}. Valid sections: server, postgresql, authentication, test, logging", section), _ => println!("Invalid section: {}. Valid sections: server, postgresql, authentication, test, logging", section),
} }
} }
+46 -46
View File
@@ -1,6 +1,6 @@
use anyhow::Context;
use clap::Subcommand; use clap::Subcommand;
use rusqlite::Connection; use rusqlite::Connection;
use anyhow::Context;
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum DbCommand { pub enum DbCommand {
@@ -34,54 +34,52 @@ pub fn handle_db_command(cmd: DbCommand) -> anyhow::Result<()> {
match cmd { match cmd {
DbCommand::Create { user } => { DbCommand::Create { user } => {
let db_path = filetree::FileTree::user_db_path(&user); let db_path = filetree::FileTree::user_db_path(&user);
if std::path::Path::new(&db_path).exists() { if std::path::Path::new(&db_path).exists() {
println!("Database already exists: {}", db_path); println!("Database already exists: {}", db_path);
println!("Use 'db status' to check database info"); println!("Use 'db status' to check database info");
return Ok(()); return Ok(());
} }
println!("Creating database for user: {}", user); println!("Creating database for user: {}", user);
let conn = filetree::FileTree::init_user_db(&user) let conn =
.context("Failed to initialize database")?; filetree::FileTree::init_user_db(&user).context("Failed to initialize database")?;
println!("✓ Database created: {}", db_path); println!("✓ Database created: {}", db_path);
println!("✓ Tables initialized: file_nodes, file_registry, file_locations, tree_registry"); println!(
"✓ Tables initialized: file_nodes, file_registry, file_locations, tree_registry"
conn.close().map_err(|e| anyhow::anyhow!("Failed to close database: {:?}", e))?; );
conn.close()
.map_err(|e| anyhow::anyhow!("Failed to close database: {:?}", e))?;
} }
DbCommand::Status { user } => { DbCommand::Status { user } => {
let db_path = filetree::FileTree::user_db_path(&user); let db_path = filetree::FileTree::user_db_path(&user);
if !std::path::Path::new(&db_path).exists() { if !std::path::Path::new(&db_path).exists() {
return Err(anyhow::anyhow!("Database not found: {}", db_path)); return Err(anyhow::anyhow!("Database not found: {}", db_path));
} }
let conn = Connection::open(&db_path) let conn = Connection::open(&db_path).context("Failed to open database")?;
.context("Failed to open database")?;
let file_size = std::fs::metadata(&db_path)?.len(); let file_size = std::fs::metadata(&db_path)?.len();
let file_size_mb = file_size as f64 / 1024.0 / 1024.0; let file_size_mb = file_size as f64 / 1024.0 / 1024.0;
let node_count: i64 = conn.query_row( let node_count: i64 = conn
"SELECT COUNT(*) FROM file_nodes", .query_row("SELECT COUNT(*) FROM file_nodes", [], |row| row.get(0))
[], .context("Failed to count nodes")?;
|row| row.get(0)
).context("Failed to count nodes")?; let file_count: i64 = conn
.query_row("SELECT COUNT(*) FROM file_registry", [], |row| row.get(0))
let file_count: i64 = conn.query_row( .context("Failed to count files")?;
"SELECT COUNT(*) FROM file_registry",
[],
|row| row.get(0)
).context("Failed to count files")?;
let tree_types: Vec<String> = { let tree_types: Vec<String> = {
let mut stmt = conn.prepare("SELECT tree_type FROM tree_registry")?; let mut stmt = conn.prepare("SELECT tree_type FROM tree_registry")?;
let rows = stmt.query_map([], |row| row.get(0))?; let rows = stmt.query_map([], |row| row.get(0))?;
rows.collect::<Result<Vec<_>, _>>()? rows.collect::<Result<Vec<_>, _>>()?
}; };
println!("=== Database Status ==="); println!("=== Database Status ===");
println!("User: {}", user); println!("User: {}", user);
println!("Path: {}", db_path); println!("Path: {}", db_path);
@@ -89,21 +87,21 @@ pub fn handle_db_command(cmd: DbCommand) -> anyhow::Result<()> {
println!("Nodes: {}", node_count); println!("Nodes: {}", node_count);
println!("Files: {}", file_count); println!("Files: {}", file_count);
println!("Tree Types: {:?}", tree_types); println!("Tree Types: {:?}", tree_types);
conn.close().map_err(|e| anyhow::anyhow!("Failed to close database: {:?}", e))?; conn.close()
.map_err(|e| anyhow::anyhow!("Failed to close database: {:?}", e))?;
} }
DbCommand::Backup { user, output } => { DbCommand::Backup { user, output } => {
let db_path = filetree::FileTree::user_db_path(&user); let db_path = filetree::FileTree::user_db_path(&user);
if !std::path::Path::new(&db_path).exists() { if !std::path::Path::new(&db_path).exists() {
return Err(anyhow::anyhow!("Database not found: {}", db_path)); return Err(anyhow::anyhow!("Database not found: {}", db_path));
} }
println!("Backing up database for user: {} to {}", user, output); println!("Backing up database for user: {} to {}", user, output);
std::fs::copy(&db_path, &output) std::fs::copy(&db_path, &output).context("Failed to backup database")?;
.context("Failed to backup database")?;
println!("✓ Database backed up to: {}", output); println!("✓ Database backed up to: {}", output);
println!("✓ Backup size: {} bytes", std::fs::metadata(&output)?.len()); println!("✓ Backup size: {} bytes", std::fs::metadata(&output)?.len());
} }
@@ -111,24 +109,26 @@ pub fn handle_db_command(cmd: DbCommand) -> anyhow::Result<()> {
if !std::path::Path::new(&input).exists() { if !std::path::Path::new(&input).exists() {
return Err(anyhow::anyhow!("Backup file not found: {}", input)); return Err(anyhow::anyhow!("Backup file not found: {}", input));
} }
let db_path = filetree::FileTree::user_db_path(&user); let db_path = filetree::FileTree::user_db_path(&user);
if std::path::Path::new(&db_path).exists() { if std::path::Path::new(&db_path).exists() {
let backup_path = format!("{}.bak", db_path); let backup_path = format!("{}.bak", db_path);
println!("Warning: Database exists, creating backup: {}", backup_path); println!("Warning: Database exists, creating backup: {}", backup_path);
std::fs::copy(&db_path, &backup_path) std::fs::copy(&db_path, &backup_path)
.context("Failed to create backup before restore")?; .context("Failed to create backup before restore")?;
} }
println!("Restoring database for user: {} from {}", user, input); println!("Restoring database for user: {} from {}", user, input);
std::fs::copy(&input, &db_path) std::fs::copy(&input, &db_path).context("Failed to restore database")?;
.context("Failed to restore database")?;
println!("✓ Database restored from: {}", input); println!("✓ Database restored from: {}", input);
println!("✓ Database size: {} bytes", std::fs::metadata(&db_path)?.len()); println!(
"✓ Database size: {} bytes",
std::fs::metadata(&db_path)?.len()
);
} }
} }
Ok(()) Ok(())
} }
+4 -4
View File
@@ -1,7 +1,7 @@
pub mod config;
pub mod user;
pub mod db;
pub mod auth; pub mod auth;
pub mod config;
pub mod db;
pub mod user;
use clap::Subcommand; use clap::Subcommand;
@@ -25,4 +25,4 @@ pub async fn handle_metadata_command(cmd: MetadataCommands) -> anyhow::Result<()
MetadataCommands::Auth(c) => auth::handle_auth_command(c)?, MetadataCommands::Auth(c) => auth::handle_auth_command(c)?,
} }
Ok(()) Ok(())
} }
+73 -69
View File
@@ -1,6 +1,6 @@
use anyhow::Context;
use clap::Subcommand; use clap::Subcommand;
use rusqlite::Connection; use rusqlite::Connection;
use anyhow::Context;
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum UserCommand { pub enum UserCommand {
@@ -18,7 +18,7 @@ pub enum UserCommand {
#[arg(short, long)] #[arg(short, long)]
name: String, name: String,
}, },
#[command(name = "user-delete")] #[command(name = "user-delete")]
Delete { Delete {
#[arg(short, long)] #[arg(short, long)]
name: String, name: String,
@@ -29,58 +29,60 @@ pub fn handle_user_command(cmd: UserCommand) -> anyhow::Result<()> {
match cmd { match cmd {
UserCommand::Create { name, password } => { UserCommand::Create { name, password } => {
let db_path = "data/auth.sqlite"; let db_path = "data/auth.sqlite";
if !std::path::Path::new(db_path).exists() { if !std::path::Path::new(db_path).exists() {
return Err(anyhow::anyhow!("Auth database not found: {}", db_path)); return Err(anyhow::anyhow!("Auth database not found: {}", db_path));
} }
let conn = Connection::open(db_path) let conn = Connection::open(db_path).context("Failed to open auth database")?;
.context("Failed to open auth database")?;
let exists: i64 = conn
let exists: i64 = conn.query_row( .query_row(
"SELECT COUNT(*) FROM sftpgo_users WHERE username = ?", "SELECT COUNT(*) FROM sftpgo_users WHERE username = ?",
[&name], [&name],
|row| row.get(0) |row| row.get(0),
).context("Failed to check user existence")?; )
.context("Failed to check user existence")?;
if exists > 0 { if exists > 0 {
return Err(anyhow::anyhow!("User already exists: {}", name)); return Err(anyhow::anyhow!("User already exists: {}", name));
} }
let password_hash = bcrypt::hash(&password, bcrypt::DEFAULT_COST) let password_hash =
.context("Failed to hash password")?; bcrypt::hash(&password, bcrypt::DEFAULT_COST).context("Failed to hash password")?;
conn.execute( conn.execute(
"INSERT INTO sftpgo_users (username, password_hash, role, created_at) VALUES (?, ?, 'user', datetime('now'))", "INSERT INTO sftpgo_users (username, password_hash, role, created_at) VALUES (?, ?, 'user', datetime('now'))",
rusqlite::params![name, password_hash] rusqlite::params![name, password_hash]
).context("Failed to create user")?; ).context("Failed to create user")?;
println!("✓ User created: {}", name); println!("✓ User created: {}", name);
println!("✓ Role: user"); println!("✓ Role: user");
println!("✓ Password hashed with bcrypt"); println!("✓ Password hashed with bcrypt");
} }
UserCommand::List => { UserCommand::List => {
let db_path = "data/auth.sqlite"; let db_path = "data/auth.sqlite";
if !std::path::Path::new(db_path).exists() { if !std::path::Path::new(db_path).exists() {
return Err(anyhow::anyhow!("Auth database not found: {}", db_path)); return Err(anyhow::anyhow!("Auth database not found: {}", db_path));
} }
let conn = Connection::open(db_path) let conn = Connection::open(db_path).context("Failed to open auth database")?;
.context("Failed to open auth database")?;
let mut stmt = conn
let mut stmt = conn.prepare( .prepare("SELECT username, role, created_at FROM sftpgo_users ORDER BY username")
"SELECT username, role, created_at FROM sftpgo_users ORDER BY username" .context("Failed to prepare query")?;
).context("Failed to prepare query")?;
let users = stmt
let users = stmt.query_map([], |row| { .query_map([], |row| {
Ok(( Ok((
row.get::<_, String>(0)?, row.get::<_, String>(0)?,
row.get::<_, String>(1)?, row.get::<_, String>(1)?,
row.get::<_, String>(2)?, row.get::<_, String>(2)?,
)) ))
}).context("Failed to query users")?; })
.context("Failed to query users")?;
println!("=== Users List ==="); println!("=== Users List ===");
let mut count = 0; let mut count = 0;
for user in users { for user in users {
@@ -88,7 +90,7 @@ pub fn handle_user_command(cmd: UserCommand) -> anyhow::Result<()> {
println!(" {} (role: {}, created: {})", name, role, created_at); println!(" {} (role: {}, created: {})", name, role, created_at);
count += 1; count += 1;
} }
if count == 0 { if count == 0 {
println!("No users found"); println!("No users found");
} else { } else {
@@ -97,24 +99,27 @@ pub fn handle_user_command(cmd: UserCommand) -> anyhow::Result<()> {
} }
UserCommand::Show { name } => { UserCommand::Show { name } => {
let db_path = "data/auth.sqlite"; let db_path = "data/auth.sqlite";
if !std::path::Path::new(db_path).exists() { if !std::path::Path::new(db_path).exists() {
return Err(anyhow::anyhow!("Auth database not found: {}", db_path)); return Err(anyhow::anyhow!("Auth database not found: {}", db_path));
} }
let conn = Connection::open(db_path) let conn = Connection::open(db_path).context("Failed to open auth database")?;
.context("Failed to open auth database")?;
let user = conn
let user = conn.query_row( .query_row(
"SELECT username, role, created_at FROM sftpgo_users WHERE username = ?", "SELECT username, role, created_at FROM sftpgo_users WHERE username = ?",
[&name], [&name],
|row| Ok(( |row| {
row.get::<_, String>(0)?, Ok((
row.get::<_, String>(1)?, row.get::<_, String>(0)?,
row.get::<_, String>(2)?, row.get::<_, String>(1)?,
)) row.get::<_, String>(2)?,
).context("Failed to query user")?; ))
},
)
.context("Failed to query user")?;
let (username, role, created_at) = user; let (username, role, created_at) = user;
println!("=== User Details ==="); println!("=== User Details ===");
println!("Username: {}", username); println!("Username: {}", username);
@@ -123,31 +128,30 @@ pub fn handle_user_command(cmd: UserCommand) -> anyhow::Result<()> {
} }
UserCommand::Delete { name } => { UserCommand::Delete { name } => {
let db_path = "data/auth.sqlite"; let db_path = "data/auth.sqlite";
if !std::path::Path::new(db_path).exists() { if !std::path::Path::new(db_path).exists() {
return Err(anyhow::anyhow!("Auth database not found: {}", db_path)); return Err(anyhow::anyhow!("Auth database not found: {}", db_path));
} }
let conn = Connection::open(db_path) let conn = Connection::open(db_path).context("Failed to open auth database")?;
.context("Failed to open auth database")?;
let exists: i64 = conn
let exists: i64 = conn.query_row( .query_row(
"SELECT COUNT(*) FROM sftpgo_users WHERE username = ?", "SELECT COUNT(*) FROM sftpgo_users WHERE username = ?",
[&name], [&name],
|row| row.get(0) |row| row.get(0),
).context("Failed to check user existence")?; )
.context("Failed to check user existence")?;
if exists == 0 { if exists == 0 {
return Err(anyhow::anyhow!("User not found: {}", name)); return Err(anyhow::anyhow!("User not found: {}", name));
} }
conn.execute( conn.execute("DELETE FROM sftpgo_users WHERE username = ?", [&name])
"DELETE FROM sftpgo_users WHERE username = ?", .context("Failed to delete user")?;
[&name]
).context("Failed to delete user")?;
println!("✓ User deleted: {}", name); println!("✓ User deleted: {}", name);
} }
} }
Ok(()) Ok(())
} }
+1 -1
View File
@@ -22,4 +22,4 @@ pub enum Commands {
Storage(storage::StorageCommands), Storage(storage::StorageCommands),
#[command(flatten)] #[command(flatten)]
Tools(tools::ToolsCommands), Tools(tools::ToolsCommands),
} }
+14 -14
View File
@@ -21,56 +21,56 @@ pub fn handle_archive_command(cmd: ArchiveCommand) -> anyhow::Result<()> {
match cmd { match cmd {
ArchiveCommand::Decompress { file, output } => { ArchiveCommand::Decompress { file, output } => {
use crate::archive::{ArchiveConfig, ProcessorRegistry}; use crate::archive::{ArchiveConfig, ProcessorRegistry};
println!("Decompressing {} to {}", file, output); println!("Decompressing {} to {}", file, output);
let archive_path = Path::new(&file); let archive_path = Path::new(&file);
if !archive_path.exists() { if !archive_path.exists() {
return Err(anyhow::anyhow!("Archive file not found: {}", file)); return Err(anyhow::anyhow!("Archive file not found: {}", file));
} }
let config = ArchiveConfig::default(); let config = ArchiveConfig::default();
let mut registry = ProcessorRegistry::new(config); let mut registry = ProcessorRegistry::new(config);
registry.initialize()?; registry.initialize()?;
let output_path = Path::new(&output); let output_path = Path::new(&output);
std::fs::create_dir_all(output_path)?; std::fs::create_dir_all(output_path)?;
let processor = registry.get_processor_mut(archive_path)?; let processor = registry.get_processor_mut(archive_path)?;
let result = processor.extract_all(output_path)?; let result = processor.extract_all(output_path)?;
println!("✓ Archive decompressed to: {}", output); println!("✓ Archive decompressed to: {}", output);
println!("✓ Files extracted: {}", result.success_files); println!("✓ Files extracted: {}", result.success_files);
println!("✓ Total size: {} bytes", result.total_bytes); println!("✓ Total size: {} bytes", result.total_bytes);
} }
ArchiveCommand::List { file } => { ArchiveCommand::List { file } => {
use crate::archive::{ArchiveConfig, ProcessorRegistry}; use crate::archive::{ArchiveConfig, ProcessorRegistry};
println!("Listing contents of {}", file); println!("Listing contents of {}", file);
let archive_path = Path::new(&file); let archive_path = Path::new(&file);
if !archive_path.exists() { if !archive_path.exists() {
return Err(anyhow::anyhow!("Archive file not found: {}", file)); return Err(anyhow::anyhow!("Archive file not found: {}", file));
} }
let config = ArchiveConfig::default(); let config = ArchiveConfig::default();
let mut registry = ProcessorRegistry::new(config); let mut registry = ProcessorRegistry::new(config);
registry.initialize()?; registry.initialize()?;
let processor = registry.get_processor_mut(archive_path)?; let processor = registry.get_processor_mut(archive_path)?;
let metadata = processor.open(archive_path)?; let metadata = processor.open(archive_path)?;
let entries = processor.list_entries()?; let entries = processor.list_entries()?;
println!("=== Archive Contents ==="); println!("=== Archive Contents ===");
println!("Format: {}", metadata.format); println!("Format: {}", metadata.format);
println!("Total files: {}", metadata.total_files); println!("Total files: {}", metadata.total_files);
println!("Total size: {} bytes", metadata.total_size); println!("Total size: {} bytes", metadata.total_size);
println!(""); println!();
for entry in entries { for entry in entries {
println!(" {} ({} bytes)", entry.path.display(), entry.size); println!(" {} ({} bytes)", entry.path.display(), entry.size);
} }
} }
} }
Ok(()) Ok(())
} }
+1 -1
View File
@@ -17,4 +17,4 @@ pub fn handle_hash_command(cmd: HashCommand) -> anyhow::Result<()> {
} }
} }
Ok(()) Ok(())
} }
+4 -4
View File
@@ -1,8 +1,8 @@
pub mod scan;
pub mod hash;
pub mod archive; pub mod archive;
pub mod sync; pub mod hash;
pub mod mount; pub mod mount;
pub mod scan;
pub mod sync;
use clap::Subcommand; use clap::Subcommand;
@@ -29,4 +29,4 @@ pub async fn handle_storage_command(cmd: StorageCommands) -> anyhow::Result<()>
StorageCommands::Mount(c) => mount::handle_mount_command(c)?, StorageCommands::Mount(c) => mount::handle_mount_command(c)?,
} }
Ok(()) Ok(())
} }
+27 -23
View File
@@ -22,21 +22,25 @@ pub enum MountCommand {
pub fn handle_mount_command(cmd: MountCommand) -> anyhow::Result<()> { pub fn handle_mount_command(cmd: MountCommand) -> anyhow::Result<()> {
match cmd { match cmd {
MountCommand::Attach { type_, server, path } => { MountCommand::Attach {
type_,
server,
path,
} => {
use std::process::Command; use std::process::Command;
println!("Mounting {} from {} to {}", type_, server, path); println!("Mounting {} from {} to {}", type_, server, path);
if type_ == "nfs" { if type_ == "nfs" {
let mount_point = std::path::Path::new(&path); let mount_point = std::path::Path::new(&path);
std::fs::create_dir_all(mount_point)?; std::fs::create_dir_all(mount_point)?;
let nfs_path = format!("{}:{}", server, path); let nfs_path = format!("{}:{}", server, path);
let status = Command::new("mount") let status = Command::new("mount")
.args(["-t", "nfs", &nfs_path, &path]) .args(["-t", "nfs", &nfs_path, &path])
.status()?; .status()?;
if status.success() { if status.success() {
println!("✓ NFS mounted: {} to {}", nfs_path, path); println!("✓ NFS mounted: {} to {}", nfs_path, path);
} else { } else {
@@ -45,31 +49,32 @@ pub fn handle_mount_command(cmd: MountCommand) -> anyhow::Result<()> {
} else if type_ == "smb" { } else if type_ == "smb" {
let mount_point = std::path::Path::new(&path); let mount_point = std::path::Path::new(&path);
std::fs::create_dir_all(mount_point)?; std::fs::create_dir_all(mount_point)?;
let smb_path = format!("//{}", server); let smb_path = format!("//{}", server);
let status = Command::new("mount") let status = Command::new("mount")
.args(["-t", "smbfs", &smb_path, &path]) .args(["-t", "smbfs", &smb_path, &path])
.status()?; .status()?;
if status.success() { if status.success() {
println!("✓ SMB mounted: {} to {}", smb_path, path); println!("✓ SMB mounted: {} to {}", smb_path, path);
} else { } else {
return Err(anyhow::anyhow!("SMB mount failed")); return Err(anyhow::anyhow!("SMB mount failed"));
} }
} else { } else {
return Err(anyhow::anyhow!("Unknown mount type: {}. Use 'nfs' or 'smb'", type_)); return Err(anyhow::anyhow!(
"Unknown mount type: {}. Use 'nfs' or 'smb'",
type_
));
} }
} }
MountCommand::Detach { path } => { MountCommand::Detach { path } => {
use std::process::Command; use std::process::Command;
println!("Unmounting {}", path); println!("Unmounting {}", path);
let status = Command::new("umount") let status = Command::new("umount").arg(&path).status()?;
.arg(&path)
.status()?;
if status.success() { if status.success() {
println!("✓ Unmounted: {}", path); println!("✓ Unmounted: {}", path);
} else { } else {
@@ -78,14 +83,13 @@ pub fn handle_mount_command(cmd: MountCommand) -> anyhow::Result<()> {
} }
MountCommand::List => { MountCommand::List => {
use std::process::Command; use std::process::Command;
println!("Listing mounted storage"); println!("Listing mounted storage");
let output = Command::new("mount") let output = Command::new("mount").output()?;
.output()?;
let mounts = String::from_utf8_lossy(&output.stdout); let mounts = String::from_utf8_lossy(&output.stdout);
println!("=== Mounted Filesystems ==="); println!("=== Mounted Filesystems ===");
for line in mounts.lines() { for line in mounts.lines() {
if line.contains("nfs") || line.contains("smbfs") || line.contains("fuse") { if line.contains("nfs") || line.contains("smbfs") || line.contains("fuse") {
@@ -95,4 +99,4 @@ pub fn handle_mount_command(cmd: MountCommand) -> anyhow::Result<()> {
} }
} }
Ok(()) Ok(())
} }
+1 -1
View File
@@ -31,4 +31,4 @@ pub fn handle_scan_command(cmd: ScanCommand) -> anyhow::Result<()> {
} }
} }
Ok(()) Ok(())
} }
+13 -9
View File
@@ -17,27 +17,31 @@ pub enum SyncCommand {
pub fn handle_sync_command(cmd: SyncCommand) -> anyhow::Result<()> { pub fn handle_sync_command(cmd: SyncCommand) -> anyhow::Result<()> {
match cmd { match cmd {
SyncCommand::Start { source, target, mode } => { SyncCommand::Start {
source,
target,
mode,
} => {
use std::path::Path; use std::path::Path;
println!("Syncing {} to {} (mode: {})", source, target, mode); println!("Syncing {} to {} (mode: {})", source, target, mode);
let source_path = Path::new(&source); let source_path = Path::new(&source);
let target_path = Path::new(&target); let target_path = Path::new(&target);
if !source_path.exists() { if !source_path.exists() {
return Err(anyhow::anyhow!("Source path not found: {}", source)); return Err(anyhow::anyhow!("Source path not found: {}", source));
} }
if mode == "mirror" { if mode == "mirror" {
std::fs::create_dir_all(target_path)?; std::fs::create_dir_all(target_path)?;
let entries = std::fs::read_dir(source_path)?; let entries = std::fs::read_dir(source_path)?;
for entry in entries { for entry in entries {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
let target_file = target_path.join(entry.file_name()); let target_file = target_path.join(entry.file_name());
if path.is_file() { if path.is_file() {
std::fs::copy(&path, &target_file)?; std::fs::copy(&path, &target_file)?;
println!(" Copied: {:?}", entry.file_name()); println!(" Copied: {:?}", entry.file_name());
@@ -46,7 +50,7 @@ pub fn handle_sync_command(cmd: SyncCommand) -> anyhow::Result<()> {
println!(" Created directory: {:?}", entry.file_name()); println!(" Created directory: {:?}", entry.file_name());
} }
} }
println!("✓ Sync completed (mirror mode)"); println!("✓ Sync completed (mirror mode)");
} else { } else {
return Err(anyhow::anyhow!("Unknown sync mode: {}. Use 'mirror'", mode)); return Err(anyhow::anyhow!("Unknown sync mode: {}. Use 'mirror'", mode));
@@ -59,4 +63,4 @@ pub fn handle_sync_command(cmd: SyncCommand) -> anyhow::Result<()> {
} }
} }
Ok(()) Ok(())
} }
+1 -1
View File
@@ -17,4 +17,4 @@ pub async fn handle_tools_command(cmd: ToolsCommands) -> anyhow::Result<()> {
ToolsCommands::Test(c) => test::handle_test_command(c)?, ToolsCommands::Test(c) => test::handle_test_command(c)?,
} }
Ok(()) Ok(())
} }
+1 -1
View File
@@ -23,4 +23,4 @@ pub fn handle_render_command(cmd: RenderCommand) -> anyhow::Result<()> {
} }
} }
Ok(()) Ok(())
} }
+10 -7
View File
@@ -16,36 +16,39 @@ pub enum TestCommand {
pub fn handle_test_command(cmd: TestCommand) -> anyhow::Result<()> { pub fn handle_test_command(cmd: TestCommand) -> anyhow::Result<()> {
match cmd { match cmd {
TestCommand::Bcrypt { password, verify_hash } => { TestCommand::Bcrypt {
password,
verify_hash,
} => {
use bcrypt::{hash, verify, DEFAULT_COST}; use bcrypt::{hash, verify, DEFAULT_COST};
println!("=== bcrypt Hash Test ==="); println!("=== bcrypt Hash Test ===");
println!("Password: {}", password); println!("Password: {}", password);
println!(""); println!();
let new_hash = hash(&password, DEFAULT_COST)?; let new_hash = hash(&password, DEFAULT_COST)?;
println!("Generated hash:"); println!("Generated hash:");
println!("{}", new_hash); println!("{}", new_hash);
println!(""); println!();
if let Some(hash_to_verify) = verify_hash { if let Some(hash_to_verify) = verify_hash {
println!("Verifying hash: {}", hash_to_verify); println!("Verifying hash: {}", hash_to_verify);
let valid = verify(&password, &hash_to_verify)?; let valid = verify(&password, &hash_to_verify)?;
println!("Valid: {}", valid); println!("Valid: {}", valid);
println!(""); println!();
} }
let db_hash = "$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6"; let db_hash = "$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6";
println!("Database hash: {}", db_hash); println!("Database hash: {}", db_hash);
let valid = verify(&password, db_hash)?; let valid = verify(&password, db_hash)?;
println!("Database hash valid for '{}': {}", password, valid); println!("Database hash valid for '{}': {}", password, valid);
println!(""); println!();
if !valid { if !valid {
println!("❌ Database hash is incorrect!"); println!("❌ Database hash is incorrect!");
println!("Update SQL:"); println!("Update SQL:");
println!("UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');", new_hash); println!("UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');", new_hash);
println!(""); println!();
println!("Execute:"); println!("Execute:");
println!("sqlite3 data/auth.sqlite \"UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');\"", new_hash); println!("sqlite3 data/auth.sqlite \"UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');\"", new_hash);
} else { } else {
@@ -58,4 +61,4 @@ pub fn handle_test_command(cmd: TestCommand) -> anyhow::Result<()> {
} }
} }
Ok(()) Ok(())
} }
+10 -13
View File
@@ -8,6 +8,7 @@ pub use web::*;
/// Unified application configuration /// Unified application configuration
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct AppConfig { pub struct AppConfig {
#[serde(default)] #[serde(default)]
pub web: WebSection, pub web: WebSection,
@@ -154,13 +155,19 @@ impl AppConfig {
self.web.host = v; self.web.host = v;
} }
if let Ok(v) = std::env::var("MB_WEB_PORT") { if let Ok(v) = std::env::var("MB_WEB_PORT") {
if let Ok(p) = v.parse() { self.web.port = p; } if let Ok(p) = v.parse() {
self.web.port = p;
}
} }
if let Ok(v) = std::env::var("MB_SSH_PORT") { if let Ok(v) = std::env::var("MB_SSH_PORT") {
if let Ok(p) = v.parse() { self.ssh.port = p; } if let Ok(p) = v.parse() {
self.ssh.port = p;
}
} }
if let Ok(v) = std::env::var("MB_SFTP_PORT") { if let Ok(v) = std::env::var("MB_SFTP_PORT") {
if let Ok(p) = v.parse() { self.sftp.port = p; } if let Ok(p) = v.parse() {
self.sftp.port = p;
}
} }
if let Ok(v) = std::env::var("MB_S3_ENABLED") { if let Ok(v) = std::env::var("MB_S3_ENABLED") {
self.s3.enabled = v == "true" || v == "1"; self.s3.enabled = v == "true" || v == "1";
@@ -172,16 +179,6 @@ impl AppConfig {
} }
} }
impl Default for AppConfig {
fn default() -> Self {
Self {
web: WebSection::default(),
s3: S3Section::default(),
sftp: SftpSection::default(),
ssh: SshSection::default(),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
+6 -2
View File
@@ -323,11 +323,15 @@ impl MarkBaseConfig {
} }
if self.authentication.default_user.is_empty() { if self.authentication.default_user.is_empty() {
return Err(anyhow::anyhow!("authentication.default_user cannot be empty")); return Err(anyhow::anyhow!(
"authentication.default_user cannot be empty"
));
} }
if self.authentication.default_password.is_empty() { if self.authentication.default_password.is_empty() {
return Err(anyhow::anyhow!("authentication.default_password cannot be empty")); return Err(anyhow::anyhow!(
"authentication.default_password cannot be empty"
));
} }
if self.authentication.max_sessions_per_user == 0 { if self.authentication.max_sessions_per_user == 0 {
+117 -98
View File
@@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use rusqlite::{Connection, params}; use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
@@ -46,10 +46,10 @@ impl DownloadDb {
Self::init_tables(&conn)?; Self::init_tables(&conn)?;
conn conn
}; };
Ok(DownloadDb { conn }) Ok(DownloadDb { conn })
} }
fn init_tables(conn: &Connection) -> Result<()> { fn init_tables(conn: &Connection) -> Result<()> {
conn.execute_batch( conn.execute_batch(
"CREATE TABLE IF NOT EXISTS products ( "CREATE TABLE IF NOT EXISTS products (
@@ -74,63 +74,70 @@ impl DownloadDb {
CREATE INDEX IF NOT EXISTS idx_product_files_product_id ON product_files(product_id); CREATE INDEX IF NOT EXISTS idx_product_files_product_id ON product_files(product_id);
CREATE INDEX IF NOT EXISTS idx_products_series ON products(series); CREATE INDEX IF NOT EXISTS idx_products_series ON products(series);
" ",
)?; )?;
Ok(()) Ok(())
} }
pub fn create_product(&mut self, product_name: &str, series: &str, description: Option<&str>) -> Result<i64> { pub fn create_product(
&mut self,
product_name: &str,
series: &str,
description: Option<&str>,
) -> Result<i64> {
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
self.conn.execute( self.conn.execute(
"INSERT INTO products (product_name, series, description, created_at) "INSERT INTO products (product_name, series, description, created_at)
VALUES (?1, ?2, ?3, ?4)", VALUES (?1, ?2, ?3, ?4)",
params![product_name, series, description, now], params![product_name, series, description, now],
)?; )?;
Ok(self.conn.last_insert_rowid()) Ok(self.conn.last_insert_rowid())
} }
pub fn get_all_products(&self) -> Result<Vec<Product>> { pub fn get_all_products(&self) -> Result<Vec<Product>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT id, product_name, series, description, created_at FROM products ORDER BY series, product_name" "SELECT id, product_name, series, description, created_at FROM products ORDER BY series, product_name"
)?; )?;
let products = stmt.query_map([], |row| { let products = stmt
Ok(Product { .query_map([], |row| {
id: row.get(0)?, Ok(Product {
product_name: row.get(1)?, id: row.get(0)?,
series: row.get(2)?, product_name: row.get(1)?,
description: row.get(3)?, series: row.get(2)?,
created_at: row.get(4)?, description: row.get(3)?,
}) created_at: row.get(4)?,
})? })
.collect::<Result<Vec<_>, _>>()?; })?
.collect::<Result<Vec<_>, _>>()?;
Ok(products) Ok(products)
} }
pub fn get_products_by_series(&self, series: &str) -> Result<Vec<Product>> { pub fn get_products_by_series(&self, series: &str) -> Result<Vec<Product>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT id, product_name, series, description, created_at FROM products "SELECT id, product_name, series, description, created_at FROM products
WHERE series = ?1 ORDER BY product_name" WHERE series = ?1 ORDER BY product_name",
)?; )?;
let products = stmt.query_map([series], |row| { let products = stmt
Ok(Product { .query_map([series], |row| {
id: row.get(0)?, Ok(Product {
product_name: row.get(1)?, id: row.get(0)?,
series: row.get(2)?, product_name: row.get(1)?,
description: row.get(3)?, series: row.get(2)?,
created_at: row.get(4)?, description: row.get(3)?,
}) created_at: row.get(4)?,
})? })
.collect::<Result<Vec<_>, _>>()?; })?
.collect::<Result<Vec<_>, _>>()?;
Ok(products) Ok(products)
} }
pub fn get_series_stats(&self) -> Result<Vec<SeriesStats>> { pub fn get_series_stats(&self) -> Result<Vec<SeriesStats>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT "SELECT
@@ -141,106 +148,118 @@ impl DownloadDb {
FROM products p FROM products p
LEFT JOIN product_files pf ON p.id = pf.product_id LEFT JOIN product_files pf ON p.id = pf.product_id
GROUP BY p.series GROUP BY p.series
ORDER BY p.series" ORDER BY p.series",
)?; )?;
let stats = stmt.query_map([], |row| { let stats = stmt
Ok(SeriesStats { .query_map([], |row| {
series: row.get(0)?, Ok(SeriesStats {
product_count: row.get(1)?, series: row.get(0)?,
file_count: row.get(2)?, product_count: row.get(1)?,
total_size: row.get::<_, i64>(3)? as u64, file_count: row.get(2)?,
}) total_size: row.get::<_, i64>(3)? as u64,
})? })
.collect::<Result<Vec<_>, _>>()?; })?
.collect::<Result<Vec<_>, _>>()?;
Ok(stats) Ok(stats)
} }
pub fn add_file_to_product(&mut self, product_id: i64, file_path: &str, file_name: &str, file_size: u64, file_hash: Option<&str>) -> Result<i64> { pub fn add_file_to_product(
&mut self,
product_id: i64,
file_path: &str,
file_name: &str,
file_size: u64,
file_hash: Option<&str>,
) -> Result<i64> {
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
self.conn.execute( self.conn.execute(
"INSERT INTO product_files (product_id, file_path, file_name, file_size, file_hash, uploaded_at) "INSERT INTO product_files (product_id, file_path, file_name, file_size, file_hash, uploaded_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)", VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![product_id, file_path, file_name, file_size as i64, file_hash, now], params![product_id, file_path, file_name, file_size as i64, file_hash, now],
)?; )?;
Ok(self.conn.last_insert_rowid()) Ok(self.conn.last_insert_rowid())
} }
pub fn get_files_by_product(&self, product_id: i64) -> Result<Vec<ProductFile>> { pub fn get_files_by_product(&self, product_id: i64) -> Result<Vec<ProductFile>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT id, product_id, file_path, file_name, file_size, file_hash, download_count, uploaded_at "SELECT id, product_id, file_path, file_name, file_size, file_hash, download_count, uploaded_at
FROM product_files WHERE product_id = ?1 ORDER BY file_name" FROM product_files WHERE product_id = ?1 ORDER BY file_name"
)?; )?;
let files = stmt.query_map([product_id], |row| { let files = stmt
Ok(ProductFile { .query_map([product_id], |row| {
id: row.get(0)?, Ok(ProductFile {
product_id: row.get(1)?, id: row.get(0)?,
file_path: row.get(2)?, product_id: row.get(1)?,
file_name: row.get(3)?, file_path: row.get(2)?,
file_size: row.get::<_, i64>(4)? as u64, file_name: row.get(3)?,
file_hash: row.get(5)?, file_size: row.get::<_, i64>(4)? as u64,
download_count: row.get(6)?, file_hash: row.get(5)?,
uploaded_at: row.get(7)?, download_count: row.get(6)?,
}) uploaded_at: row.get(7)?,
})? })
.collect::<Result<Vec<_>, _>>()?; })?
.collect::<Result<Vec<_>, _>>()?;
Ok(files) Ok(files)
} }
pub fn increment_download_count(&mut self, file_id: i64) -> Result<()> { pub fn increment_download_count(&mut self, file_id: i64) -> Result<()> {
self.conn.execute( self.conn.execute(
"UPDATE product_files SET download_count = download_count + 1 WHERE id = ?1", "UPDATE product_files SET download_count = download_count + 1 WHERE id = ?1",
params![file_id], params![file_id],
)?; )?;
Ok(()) Ok(())
} }
pub fn get_all_files(&self) -> Result<Vec<ProductFile>> { pub fn get_all_files(&self) -> Result<Vec<ProductFile>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT id, product_id, file_path, file_name, file_size, file_hash, download_count, uploaded_at "SELECT id, product_id, file_path, file_name, file_size, file_hash, download_count, uploaded_at
FROM product_files ORDER BY uploaded_at DESC" FROM product_files ORDER BY uploaded_at DESC"
)?; )?;
let files = stmt.query_map([], |row| { let files = stmt
Ok(ProductFile { .query_map([], |row| {
id: row.get(0)?, Ok(ProductFile {
product_id: row.get(1)?, id: row.get(0)?,
file_path: row.get(2)?, product_id: row.get(1)?,
file_name: row.get(3)?, file_path: row.get(2)?,
file_size: row.get::<_, i64>(4)? as u64, file_name: row.get(3)?,
file_hash: row.get(5)?, file_size: row.get::<_, i64>(4)? as u64,
download_count: row.get(6)?, file_hash: row.get(5)?,
uploaded_at: row.get(7)?, download_count: row.get(6)?,
}) uploaded_at: row.get(7)?,
})? })
.collect::<Result<Vec<_>, _>>()?; })?
.collect::<Result<Vec<_>, _>>()?;
Ok(files) Ok(files)
} }
pub fn delete_product_with_files(&mut self, product_id: i64) -> Result<(i64, i64)> { pub fn delete_product_with_files(&mut self, product_id: i64) -> Result<(i64, i64)> {
// 先删除关联的文件映射 // 先删除关联的文件映射
self.conn.execute( self.conn.execute(
"DELETE FROM product_files WHERE product_id = ?1", "DELETE FROM product_files WHERE product_id = ?1",
params![product_id], params![product_id],
)?; )?;
let deleted_files = self.conn.last_insert_rowid(); let deleted_files = self.conn.last_insert_rowid();
// 再删除产品记录 // 再删除产品记录
self.conn.execute( self.conn
"DELETE FROM products WHERE id = ?1", .execute("DELETE FROM products WHERE id = ?1", params![product_id])?;
params![product_id],
)?; let deleted_product = if self.conn.last_insert_rowid() > 0 {
1
let deleted_product = if self.conn.last_insert_rowid() > 0 { 1 } else { 0 }; } else {
0
};
Ok((deleted_files, deleted_product)) Ok((deleted_files, deleted_product))
} }
} }
+132 -90
View File
@@ -7,15 +7,19 @@ use axum::{
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use crate::server::AppState;
use crate::download::db::DownloadDb; use crate::download::db::DownloadDb;
use crate::server::AppState;
pub async fn download_file( pub async fn download_file(
Path(file_id): Path<i64>, Path(file_id): Path<i64>,
State(state): State<AppState>, State(state): State<AppState>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite"); let db_path = format!(
"{}{}",
state.db_dir.replace("users", "downloads"),
"/products.sqlite"
);
match DownloadDb::new(&db_path) { match DownloadDb::new(&db_path) {
Ok(mut db) => { Ok(mut db) => {
// 获取文件信息 // 获取文件信息
@@ -24,48 +28,65 @@ pub async fn download_file(
if files.is_empty() { if files.is_empty() {
return (StatusCode::NOT_FOUND, "File not found").into_response(); return (StatusCode::NOT_FOUND, "File not found").into_response();
} }
let file_info = &files[0]; let file_info = &files[0];
// 更新下载统计 // 更新下载统计
db.increment_download_count(file_info.id).ok(); db.increment_download_count(file_info.id).ok();
// 构建文件路径(使用配置的db_dir) // 构建文件路径(使用配置的db_dir)
let base_path = state.db_dir.replace("users", "Downloads"); let base_path = state.db_dir.replace("users", "Downloads");
let file_path = std::path::Path::new(&base_path).join(&file_info.file_path); let file_path = std::path::Path::new(&base_path).join(&file_info.file_path);
if !file_path.exists() { if !file_path.exists() {
return (StatusCode::NOT_FOUND, "File not found on disk").into_response(); return (StatusCode::NOT_FOUND, "File not found on disk").into_response();
} }
// 读取文件内容 // 读取文件内容
match File::open(&file_path) { match File::open(&file_path) {
Ok(mut file) => { Ok(mut file) => {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
match file.read_to_end(&mut buffer) { match file.read_to_end(&mut buffer) {
Ok(_) => { Ok(_) => Response::builder()
Response::builder() .status(StatusCode::OK)
.status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/octet-stream")
.header(header::CONTENT_TYPE, "application/octet-stream") .header(
.header( header::CONTENT_DISPOSITION,
header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", file_info.file_name),
format!("attachment; filename=\"{}\"", file_info.file_name) )
) .header(
.header("X-File-Hash", file_info.file_hash.clone().unwrap_or_default()) "X-File-Hash",
.header("X-File-Size", file_info.file_size) file_info.file_hash.clone().unwrap_or_default(),
.body(buffer.into()) )
.unwrap() .header("X-File-Size", file_info.file_size)
} .body(buffer.into())
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error reading file: {}", e)).into_response() .unwrap(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error reading file: {}", e),
)
.into_response(),
} }
} }
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error opening file: {}", e)).into_response() Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error opening file: {}", e),
)
.into_response(),
} }
} }
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response() Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Database error: {}", e),
)
.into_response(),
} }
} }
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response() Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Database error: {}", e),
)
.into_response(),
} }
} }
@@ -80,69 +101,84 @@ pub async fn download_file_by_path(
// User files are in Downloads/user_id/ // User files are in Downloads/user_id/
format!("/Users/accusys/Downloads/{}", user_id) format!("/Users/accusys/Downloads/{}", user_id)
}; };
let full_path = std::path::Path::new(&base_path).join(&file_path); let full_path = std::path::Path::new(&base_path).join(&file_path);
if !full_path.exists() { if !full_path.exists() {
return (StatusCode::NOT_FOUND, "File not found").into_response(); return (StatusCode::NOT_FOUND, "File not found").into_response();
} }
let filename = file_path.split('/').last().unwrap_or("unknown"); let filename = file_path.split('/').next_back().unwrap_or("unknown");
match File::open(&full_path) { match File::open(&full_path) {
Ok(mut file) => { Ok(mut file) => {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
match file.read_to_end(&mut buffer) { match file.read_to_end(&mut buffer) {
Ok(_) => { Ok(_) => Response::builder()
Response::builder() .status(StatusCode::OK)
.status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/octet-stream")
.header(header::CONTENT_TYPE, "application/octet-stream") .header(
.header( header::CONTENT_DISPOSITION,
header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", filename),
format!("attachment; filename=\"{}\"", filename) )
) .body(buffer.into())
.body(buffer.into()) .unwrap(),
.unwrap() Err(e) => (
} StatusCode::INTERNAL_SERVER_ERROR,
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error reading file: {}", e)).into_response() format!("Error reading file: {}", e),
)
.into_response(),
} }
} }
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error opening file: {}", e)).into_response() Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error opening file: {}", e),
)
.into_response(),
} }
} }
pub async fn get_download_stats( pub async fn get_download_stats(State(state): State<AppState>) -> impl IntoResponse {
State(state): State<AppState>, let db_path = format!(
) -> impl IntoResponse { "{}{}",
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite"); state.db_dir.replace("users", "downloads"),
"/products.sqlite"
);
match DownloadDb::new(&db_path) { match DownloadDb::new(&db_path) {
Ok(db) => { Ok(db) => match db.get_all_files() {
match db.get_all_files() { Ok(files) => {
Ok(files) => { let total_downloads: i64 = files.iter().map(|f| f.download_count).sum();
let total_downloads: i64 = files.iter().map(|f| f.download_count).sum(); let top_files: Vec<_> = files
let top_files: Vec<_> = files.iter() .iter()
.filter(|f| f.download_count > 0) .filter(|f| f.download_count > 0)
.take(10) .take(10)
.map(|f| serde_json::json!({ .map(|f| {
serde_json::json!({
"file_name": f.file_name, "file_name": f.file_name,
"download_count": f.download_count "download_count": f.download_count
})) })
.collect(); })
.collect();
(
StatusCode::OK, (
Json(serde_json::json!({ StatusCode::OK,
"total_files": files.len(), Json(serde_json::json!({
"total_downloads": total_downloads, "total_files": files.len(),
"top_files": top_files "total_downloads": total_downloads,
})) "top_files": top_files
) })),
} )
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})))
} }
} Err(e) => (
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))) StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
),
},
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
),
} }
} }
@@ -151,35 +187,41 @@ pub async fn download_product_file(
) -> impl IntoResponse { ) -> impl IntoResponse {
let base_path = format!("/Users/accusys/markbase/data/downloads/{}/", product_series); let base_path = format!("/Users/accusys/markbase/data/downloads/{}/", product_series);
let full_path = std::path::Path::new(&base_path).join(&file_path); let full_path = std::path::Path::new(&base_path).join(&file_path);
if !full_path.exists() { if !full_path.exists() {
return (StatusCode::NOT_FOUND, "File not found").into_response(); return (StatusCode::NOT_FOUND, "File not found").into_response();
} }
if full_path.is_dir() { if full_path.is_dir() {
return (StatusCode::BAD_REQUEST, "Path is a directory, not a file").into_response(); return (StatusCode::BAD_REQUEST, "Path is a directory, not a file").into_response();
} }
let filename = file_path.split('/').last().unwrap_or("unknown"); let filename = file_path.split('/').next_back().unwrap_or("unknown");
match File::open(&full_path) { match File::open(&full_path) {
Ok(mut file) => { Ok(mut file) => {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
match file.read_to_end(&mut buffer) { match file.read_to_end(&mut buffer) {
Ok(_) => { Ok(_) => Response::builder()
Response::builder() .status(StatusCode::OK)
.status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/octet-stream")
.header(header::CONTENT_TYPE, "application/octet-stream") .header(
.header( header::CONTENT_DISPOSITION,
header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", filename),
format!("attachment; filename=\"{}\"", filename) )
) .body(buffer.into())
.body(buffer.into()) .unwrap(),
.unwrap() Err(e) => (
} StatusCode::INTERNAL_SERVER_ERROR,
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error reading file: {}", e)).into_response() format!("Error reading file: {}", e),
)
.into_response(),
} }
} }
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error opening file: {}", e)).into_response() Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error opening file: {}", e),
)
.into_response(),
} }
} }
+9 -14
View File
@@ -1,27 +1,22 @@
use axum::{ use axum::{
extract::{Path, State}, extract::Path,
http::{HeaderMap, StatusCode}, http::StatusCode,
response::{Html, IntoResponse, Json}, response::{IntoResponse, Json},
}; };
use serde_json::json; use serde_json::json;
use std::path::PathBuf; use std::path::PathBuf;
use crate::server::AppState;
use crate::download::storage; use crate::download::storage;
pub async fn list_uploaded_files( pub async fn list_uploaded_files(Path(user_id): Path<String>) -> impl IntoResponse {
Path(user_id): Path<String>,
) -> impl IntoResponse {
let file_list = storage::scan_uploaded_files(&user_id); let file_list = storage::scan_uploaded_files(&user_id);
(StatusCode::OK, Json(file_list)) (StatusCode::OK, Json(file_list))
} }
pub async fn get_file_info( pub async fn get_file_info(Path((user_id, filename)): Path<(String, String)>) -> impl IntoResponse {
Path((user_id, filename)): Path<(String, String)>,
) -> impl IntoResponse {
let base_path = format!("/Users/accusys/Downloads/{}", user_id); let base_path = format!("/Users/accusys/Downloads/{}", user_id);
let file_path = PathBuf::from(&base_path).join(&filename); let file_path = PathBuf::from(&base_path).join(&filename);
if !file_path.exists() { if !file_path.exists() {
return ( return (
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
@@ -29,7 +24,7 @@ pub async fn get_file_info(
) )
.into_response(); .into_response();
} }
let metadata = std::fs::metadata(&file_path).unwrap(); let metadata = std::fs::metadata(&file_path).unwrap();
let file_size = metadata.len(); let file_size = metadata.len();
let file_hash = if file_size > 0 { let file_hash = if file_size > 0 {
@@ -37,7 +32,7 @@ pub async fn get_file_info(
} else { } else {
Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string()) Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string())
}; };
( (
StatusCode::OK, StatusCode::OK,
Json(json!({ Json(json!({
@@ -49,4 +44,4 @@ pub async fn get_file_info(
})), })),
) )
.into_response() .into_response()
} }
+6 -6
View File
@@ -1,12 +1,12 @@
pub mod models;
pub mod db; pub mod db;
pub mod handlers;
pub mod storage;
pub mod product_handlers;
pub mod download_handler; pub mod download_handler;
pub mod handlers;
pub mod models;
pub mod product_handlers;
pub mod storage;
pub use models::*;
pub use db::{DownloadDb, Product, ProductFile, SeriesStats}; pub use db::{DownloadDb, Product, ProductFile, SeriesStats};
pub use download_handler::*;
pub use handlers::*; pub use handlers::*;
pub use models::*;
pub use product_handlers::*; pub use product_handlers::*;
pub use download_handler::*;
+1 -1
View File
@@ -39,4 +39,4 @@ pub struct DownloadStats {
pub total_files: i64, pub total_files: i64,
pub total_downloads: i64, pub total_downloads: i64,
pub series_stats: Vec<ProductSeries>, pub series_stats: Vec<ProductSeries>,
} }
+185 -109
View File
@@ -1,33 +1,42 @@
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
http::StatusCode, http::StatusCode,
response::{Json, IntoResponse}, response::{IntoResponse, Json},
}; };
use serde_json::json; use serde_json::json;
use crate::download::db::DownloadDb;
use crate::server::AppState; use crate::server::AppState;
use crate::download::db::{DownloadDb, Product, ProductFile, SeriesStats};
pub async fn list_all_products( pub async fn list_all_products(State(state): State<AppState>) -> impl IntoResponse {
State(state): State<AppState>, let db_path = format!(
) -> impl IntoResponse { "{}{}",
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite"); state.db_dir.replace("users", "downloads"),
"/products.sqlite"
);
match DownloadDb::new(&db_path) { match DownloadDb::new(&db_path) {
Ok(db) => { Ok(db) => match db.get_all_products() {
match db.get_all_products() { Ok(products) => (
Ok(products) => (StatusCode::OK, Json(json!({ StatusCode::OK,
Json(json!({
"products": products, "products": products,
"total": products.len() "total": products.len()
}))), })),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ ),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": e.to_string() "error": e.to_string()
}))), })),
} ),
} },
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ Err(e) => (
"error": e.to_string() StatusCode::INTERNAL_SERVER_ERROR,
}))), Json(json!({
"error": e.to_string()
})),
),
} }
} }
@@ -35,47 +44,67 @@ pub async fn list_products_by_series(
Path(series): Path<String>, Path(series): Path<String>,
State(state): State<AppState>, State(state): State<AppState>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite"); let db_path = format!(
"{}{}",
state.db_dir.replace("users", "downloads"),
"/products.sqlite"
);
match DownloadDb::new(&db_path) { match DownloadDb::new(&db_path) {
Ok(db) => { Ok(db) => match db.get_products_by_series(&series) {
match db.get_products_by_series(&series) { Ok(products) => (
Ok(products) => (StatusCode::OK, Json(json!({ StatusCode::OK,
Json(json!({
"series": series, "series": series,
"products": products, "products": products,
"total": products.len() "total": products.len()
}))), })),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ ),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": e.to_string() "error": e.to_string()
}))), })),
} ),
} },
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ Err(e) => (
"error": e.to_string() StatusCode::INTERNAL_SERVER_ERROR,
}))), Json(json!({
"error": e.to_string()
})),
),
} }
} }
pub async fn get_series_stats( pub async fn get_series_stats(State(state): State<AppState>) -> impl IntoResponse {
State(state): State<AppState>, let db_path = format!(
) -> impl IntoResponse { "{}{}",
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite"); state.db_dir.replace("users", "downloads"),
"/products.sqlite"
);
match DownloadDb::new(&db_path) { match DownloadDb::new(&db_path) {
Ok(db) => { Ok(db) => match db.get_series_stats() {
match db.get_series_stats() { Ok(stats) => (
Ok(stats) => (StatusCode::OK, Json(json!({ StatusCode::OK,
Json(json!({
"series_stats": stats, "series_stats": stats,
"total_series": stats.len() "total_series": stats.len()
}))), })),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ ),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": e.to_string() "error": e.to_string()
}))), })),
} ),
} },
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ Err(e) => (
"error": e.to_string() StatusCode::INTERNAL_SERVER_ERROR,
}))), Json(json!({
"error": e.to_string()
})),
),
} }
} }
@@ -83,25 +112,36 @@ pub async fn get_product_files(
Path(product_id): Path<i64>, Path(product_id): Path<i64>,
State(state): State<AppState>, State(state): State<AppState>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite"); let db_path = format!(
"{}{}",
state.db_dir.replace("users", "downloads"),
"/products.sqlite"
);
match DownloadDb::new(&db_path) { match DownloadDb::new(&db_path) {
Ok(db) => { Ok(db) => match db.get_files_by_product(product_id) {
match db.get_files_by_product(product_id) { Ok(files) => (
Ok(files) => (StatusCode::OK, Json(json!({ StatusCode::OK,
Json(json!({
"product_id": product_id, "product_id": product_id,
"files": files, "files": files,
"total_files": files.len(), "total_files": files.len(),
"total_size": files.iter().map(|f| f.file_size).sum::<u64>() "total_size": files.iter().map(|f| f.file_size).sum::<u64>()
}))), })),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ ),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": e.to_string() "error": e.to_string()
}))), })),
} ),
} },
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ Err(e) => (
"error": e.to_string() StatusCode::INTERNAL_SERVER_ERROR,
}))), Json(json!({
"error": e.to_string()
})),
),
} }
} }
@@ -109,29 +149,40 @@ pub async fn create_product_handler(
State(state): State<AppState>, State(state): State<AppState>,
Json(payload): Json<serde_json::Value>, Json(payload): Json<serde_json::Value>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite"); let db_path = format!(
"{}{}",
state.db_dir.replace("users", "downloads"),
"/products.sqlite"
);
let product_name = payload["product_name"].as_str().unwrap_or(""); let product_name = payload["product_name"].as_str().unwrap_or("");
let series = payload["series"].as_str().unwrap_or(""); let series = payload["series"].as_str().unwrap_or("");
let description = payload["description"].as_str(); let description = payload["description"].as_str();
match DownloadDb::new(&db_path) { match DownloadDb::new(&db_path) {
Ok(mut db) => { Ok(mut db) => match db.create_product(product_name, series, description) {
match db.create_product(product_name, series, description) { Ok(product_id) => (
Ok(product_id) => (StatusCode::OK, Json(json!({ StatusCode::OK,
Json(json!({
"ok": true, "ok": true,
"product_id": product_id, "product_id": product_id,
"product_name": product_name, "product_name": product_name,
"series": series "series": series
}))), })),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ ),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": e.to_string() "error": e.to_string()
}))), })),
} ),
} },
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ Err(e) => (
"error": e.to_string() StatusCode::INTERNAL_SERVER_ERROR,
}))), Json(json!({
"error": e.to_string()
})),
),
} }
} }
@@ -140,48 +191,62 @@ pub async fn assign_files_to_product(
State(state): State<AppState>, State(state): State<AppState>,
Json(payload): Json<serde_json::Value>, Json(payload): Json<serde_json::Value>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite"); let db_path = format!(
"{}{}",
state.db_dir.replace("users", "downloads"),
"/products.sqlite"
);
let files_vec = payload["files"].as_array().cloned().unwrap_or_default(); let files_vec = payload["files"].as_array().cloned().unwrap_or_default();
let files = files_vec.as_slice(); let files = files_vec.as_slice();
match DownloadDb::new(&db_path) { match DownloadDb::new(&db_path) {
Ok(mut db) => { Ok(mut db) => {
let mut assigned_count = 0; let mut assigned_count = 0;
let mut errors = vec![]; let mut errors = vec![];
for file in files { for file in files {
let file_path = file["file_path"].as_str().unwrap_or(""); let file_path = file["file_path"].as_str().unwrap_or("");
let file_name = file["file_name"].as_str().unwrap_or(""); let file_name = file["file_name"].as_str().unwrap_or("");
let file_size = file["file_size"].as_u64().unwrap_or(0); let file_size = file["file_size"].as_u64().unwrap_or(0);
let file_hash = file["file_hash"].as_str(); let file_hash = file["file_hash"].as_str();
match db.add_file_to_product(product_id, file_path, file_name, file_size, file_hash) { match db.add_file_to_product(product_id, file_path, file_name, file_size, file_hash)
{
Ok(_) => assigned_count += 1, Ok(_) => assigned_count += 1,
Err(e) => { Err(e) => {
errors.push(format!("Failed to assign {}: {}", file_path, e)); errors.push(format!("Failed to assign {}: {}", file_path, e));
} }
} }
} }
if errors.is_empty() { if errors.is_empty() {
(StatusCode::OK, Json(json!({ (
"ok": true, StatusCode::OK,
"product_id": product_id, Json(json!({
"assigned_count": assigned_count "ok": true,
}))) "product_id": product_id,
"assigned_count": assigned_count
})),
)
} else { } else {
(StatusCode::PARTIAL_CONTENT, Json(json!({ (
"ok": true, StatusCode::PARTIAL_CONTENT,
"product_id": product_id, Json(json!({
"assigned_count": assigned_count, "ok": true,
"errors": errors "product_id": product_id,
}))) "assigned_count": assigned_count,
"errors": errors
})),
)
} }
} }
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ Err(e) => (
"error": e.to_string() StatusCode::INTERNAL_SERVER_ERROR,
}))), Json(json!({
"error": e.to_string()
})),
),
} }
} }
@@ -189,24 +254,35 @@ pub async fn delete_product(
Path(product_id): Path<i64>, Path(product_id): Path<i64>,
State(state): State<AppState>, State(state): State<AppState>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let db_path = format!("{}{}", state.db_dir.replace("users", "downloads"), "/products.sqlite"); let db_path = format!(
"{}{}",
state.db_dir.replace("users", "downloads"),
"/products.sqlite"
);
match DownloadDb::new(&db_path) { match DownloadDb::new(&db_path) {
Ok(mut db) => { Ok(mut db) => match db.delete_product_with_files(product_id) {
match db.delete_product_with_files(product_id) { Ok((deleted_files, deleted_product)) => (
Ok((deleted_files, deleted_product)) => (StatusCode::OK, Json(json!({ StatusCode::OK,
Json(json!({
"ok": true, "ok": true,
"product_id": product_id, "product_id": product_id,
"deleted_files": deleted_files, "deleted_files": deleted_files,
"deleted_product": deleted_product "deleted_product": deleted_product
}))), })),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ ),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": e.to_string() "error": e.to_string()
}))), })),
} ),
} },
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ Err(e) => (
"error": e.to_string() StatusCode::INTERNAL_SERVER_ERROR,
}))), Json(json!({
"error": e.to_string()
})),
),
} }
} }
+27 -23
View File
@@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf}; use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInfo { pub struct FileInfo {
@@ -23,17 +23,17 @@ pub struct FileListResponse {
pub fn scan_uploaded_files(user_id: &str) -> FileListResponse { pub fn scan_uploaded_files(user_id: &str) -> FileListResponse {
let base_path = format!("/Users/accusys/Downloads/{}", user_id); let base_path = format!("/Users/accusys/Downloads/{}", user_id);
let path = Path::new(&base_path); let path = Path::new(&base_path);
let mut files = Vec::new(); let mut files = Vec::new();
let mut total_size = 0u64; let mut total_size = 0u64;
if path.exists() { if path.exists() {
scan_directory_recursive(path, path, &mut files, &mut total_size); scan_directory_recursive(path, path, &mut files, &mut total_size);
} }
FileListResponse { FileListResponse {
user_id: user_id.to_string(), user_id: user_id.to_string(),
base_path: base_path, base_path,
total_files: files.len(), total_files: files.len(),
total_size, total_size,
files, files,
@@ -49,24 +49,25 @@ fn scan_directory_recursive(
if let Ok(entries) = std::fs::read_dir(current) { if let Ok(entries) = std::fs::read_dir(current) {
for entry in entries.flatten() { for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
if path.is_file() { if path.is_file() {
let filename = path.file_name() let filename = path
.file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("unknown") .unwrap_or("unknown")
.to_string(); .to_string();
let file_size = entry.metadata() let file_size = entry.metadata().map(|m| m.len()).unwrap_or(0);
.map(|m| m.len())
.unwrap_or(0); let relative_path = path
.strip_prefix(base)
let relative_path = path.strip_prefix(base)
.ok() .ok()
.and_then(|p| p.to_str()) .and_then(|p| p.to_str())
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or_else(|| filename.clone()); .unwrap_or_else(|| filename.clone());
let upload_time = entry.metadata() let upload_time = entry
.metadata()
.ok() .ok()
.and_then(|m| m.modified().ok()) .and_then(|m| m.modified().ok())
.and_then(|t| { .and_then(|t| {
@@ -75,13 +76,16 @@ fn scan_directory_recursive(
.map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()) .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
}) })
.unwrap_or_else(|| chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()); .unwrap_or_else(|| chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string());
let file_hash = if file_size > 0 { let file_hash = if file_size > 0 {
compute_file_hash(&path).ok() compute_file_hash(&path).ok()
} else { } else {
Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string()) Some(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
.to_string(),
)
}; };
files.push(FileInfo { files.push(FileInfo {
filename, filename,
file_size, file_size,
@@ -90,7 +94,7 @@ fn scan_directory_recursive(
relative_path, relative_path,
upload_time, upload_time,
}); });
*total_size += file_size; *total_size += file_size;
} else if path.is_dir() { } else if path.is_dir() {
scan_directory_recursive(base, &path, files, total_size); scan_directory_recursive(base, &path, files, total_size);
@@ -102,11 +106,11 @@ fn scan_directory_recursive(
pub fn compute_file_hash(path: &Path) -> Result<String, std::io::Error> { pub fn compute_file_hash(path: &Path) -> Result<String, std::io::Error> {
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::io::Read; use std::io::Read;
let mut file = std::fs::File::open(path)?; let mut file = std::fs::File::open(path)?;
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192]; let mut buffer = [0u8; 8192];
loop { loop {
let bytes_read = file.read(&mut buffer)?; let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 { if bytes_read == 0 {
@@ -114,6 +118,6 @@ pub fn compute_file_hash(path: &Path) -> Result<String, std::io::Error> {
} }
hasher.update(&buffer[..bytes_read]); hasher.update(&buffer[..bytes_read]);
} }
Ok(format!("{:x}", hasher.finalize())) Ok(format!("{:x}", hasher.finalize()))
} }
+126 -75
View File
@@ -1,9 +1,7 @@
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use serde::{Deserialize, Serialize};
use pulldown_cmark::{Parser, Event, Tag, HeadingLevel, TagEnd};
use regex::Regex;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarkdownFile { pub struct MarkdownFile {
@@ -39,17 +37,21 @@ pub struct SeriesMarkdown {
pub fn parse_category_markdown(content: &str) -> Result<CategoryMarkdown> { pub fn parse_category_markdown(content: &str) -> Result<CategoryMarkdown> {
let mut category = String::new(); let mut category = String::new();
let mut sections: Vec<CategorySection> = Vec::new(); let mut sections: Vec<CategorySection> = Vec::new();
let lines: Vec<&str> = content.lines().collect(); let lines: Vec<&str> = content.lines().collect();
let mut current_product = String::new(); let mut current_product = String::new();
let mut current_files: Vec<MarkdownFile> = Vec::new(); let mut current_files: Vec<MarkdownFile> = Vec::new();
let mut pending_file: Option<(String, String)> = None; let mut pending_file: Option<(String, String)> = None;
for i in 0..lines.len() { for i in 0..lines.len() {
let line = lines[i].trim(); let line = lines[i].trim();
if line.contains("**Category**:") { if line.contains("**Category**:") {
category = line.replace("**Category**:", "").replace("**", "").trim().to_string(); category = line
.replace("**Category**:", "")
.replace("**", "")
.trim()
.to_string();
} else if line.starts_with("## ") { } else if line.starts_with("## ") {
if !current_product.is_empty() && !current_files.is_empty() { if !current_product.is_empty() && !current_files.is_empty() {
sections.push(CategorySection { sections.push(CategorySection {
@@ -72,13 +74,17 @@ pub fn parse_category_markdown(content: &str) -> Result<CategoryMarkdown> {
current_files.push(MarkdownFile { current_files.push(MarkdownFile {
filename, filename,
size: Some(size), size: Some(size),
download_url: line.trim_start_matches('`').trim_end_matches('`').trim().to_string(), download_url: line
.trim_start_matches('`')
.trim_end_matches('`')
.trim()
.to_string(),
}); });
pending_file = None; pending_file = None;
} }
} }
} }
if !current_product.is_empty() && !current_files.is_empty() { if !current_product.is_empty() && !current_files.is_empty() {
sections.push(CategorySection { sections.push(CategorySection {
product: current_product.clone(), product: current_product.clone(),
@@ -92,17 +98,21 @@ pub fn parse_category_markdown(content: &str) -> Result<CategoryMarkdown> {
pub fn parse_series_markdown(content: &str) -> Result<SeriesMarkdown> { pub fn parse_series_markdown(content: &str) -> Result<SeriesMarkdown> {
let mut series = String::new(); let mut series = String::new();
let mut sections: Vec<SeriesSection> = Vec::new(); let mut sections: Vec<SeriesSection> = Vec::new();
let lines: Vec<&str> = content.lines().collect(); let lines: Vec<&str> = content.lines().collect();
let mut current_category = String::new(); let mut current_category = String::new();
let mut current_files: Vec<MarkdownFile> = Vec::new(); let mut current_files: Vec<MarkdownFile> = Vec::new();
let mut pending_file: Option<(String, String)> = None; let mut pending_file: Option<(String, String)> = None;
for i in 0..lines.len() { for i in 0..lines.len() {
let line = lines[i].trim(); let line = lines[i].trim();
if line.starts_with("# ") && line.contains("Download Links") { if line.starts_with("# ") && line.contains("Download Links") {
series = line.replace("# ", "").replace(" Download Links", "").trim().to_string(); series = line
.replace("# ", "")
.replace(" Download Links", "")
.trim()
.to_string();
} else if line.starts_with("## ") { } else if line.starts_with("## ") {
if !current_category.is_empty() && !current_files.is_empty() { if !current_category.is_empty() && !current_files.is_empty() {
sections.push(SeriesSection { sections.push(SeriesSection {
@@ -125,13 +135,17 @@ pub fn parse_series_markdown(content: &str) -> Result<SeriesMarkdown> {
current_files.push(MarkdownFile { current_files.push(MarkdownFile {
filename, filename,
size: Some(size), size: Some(size),
download_url: line.trim_start_matches('`').trim_end_matches('`').trim().to_string(), download_url: line
.trim_start_matches('`')
.trim_end_matches('`')
.trim()
.to_string(),
}); });
pending_file = None; pending_file = None;
} }
} }
} }
if !current_category.is_empty() && !current_files.is_empty() { if !current_category.is_empty() && !current_files.is_empty() {
sections.push(SeriesSection { sections.push(SeriesSection {
category: current_category.clone(), category: current_category.clone(),
@@ -144,65 +158,77 @@ pub fn parse_series_markdown(content: &str) -> Result<SeriesMarkdown> {
pub fn read_category_files(dir: &Path) -> Result<Vec<(String, String)>> { pub fn read_category_files(dir: &Path) -> Result<Vec<(String, String)>> {
let mut files = Vec::new(); let mut files = Vec::new();
for entry in fs::read_dir(dir)? { for entry in fs::read_dir(dir)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
if path.extension().map_or(false, |ext| ext == "md") && path.file_name() != Some(std::ffi::OsStr::new("README.md")) { if path.extension().is_some_and(|ext| ext == "md")
&& path.file_name() != Some(std::ffi::OsStr::new("README.md"))
{
let filename = path.file_name().unwrap().to_string_lossy().to_string(); let filename = path.file_name().unwrap().to_string_lossy().to_string();
let content = fs::read_to_string(&path)?; let content = fs::read_to_string(&path)?;
files.push((filename, content)); files.push((filename, content));
} }
} }
Ok(files) Ok(files)
} }
pub fn read_series_files(dir: &Path) -> Result<Vec<(String, String)>> { pub fn read_series_files(dir: &Path) -> Result<Vec<(String, String)>> {
let mut files = Vec::new(); let mut files = Vec::new();
for entry in fs::read_dir(dir)? { for entry in fs::read_dir(dir)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
if path.extension().map_or(false, |ext| ext == "md") && path.file_name() != Some(std::ffi::OsStr::new("README.md")) { if path.extension().is_some_and(|ext| ext == "md")
&& path.file_name() != Some(std::ffi::OsStr::new("README.md"))
{
let filename = path.file_name().unwrap().to_string_lossy().to_string(); let filename = path.file_name().unwrap().to_string_lossy().to_string();
let content = fs::read_to_string(&path)?; let content = fs::read_to_string(&path)?;
files.push((filename, content)); files.push((filename, content));
} }
} }
Ok(files) Ok(files)
} }
pub fn import_categories_to_db(conn: &rusqlite::Connection, user_id: &str, tree_type: &str) -> Result<()> { pub fn import_categories_to_db(
conn: &rusqlite::Connection,
user_id: &str,
tree_type: &str,
) -> Result<()> {
use crate::FileTree; use crate::FileTree;
use filetree::node::{FileNode, Aliases, NodeType}; use filetree::node::{Aliases, FileNode, NodeType};
use uuid::Uuid;
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid;
let category_dir = Path::new("/Users/accusys/markbase/data/downloads/by_category"); let category_dir = Path::new("/Users/accusys/markbase/data/downloads/by_category");
let files = read_category_files(category_dir)?; let files = read_category_files(category_dir)?;
println!("Found {} Markdown files", files.len()); println!("Found {} Markdown files", files.len());
let mut tree = FileTree::load(conn, user_id, tree_type)?; let mut tree = FileTree::load(conn, user_id, tree_type)?;
for (_filename, content) in files { for (_filename, content) in files {
let parsed = parse_category_markdown(&content)?; let parsed = parse_category_markdown(&content)?;
println!("Parsed category: '{}', sections: {}", parsed.category, parsed.sections.len()); println!(
"Parsed category: '{}', sections: {}",
parsed.category,
parsed.sections.len()
);
if parsed.category.is_empty() { if parsed.category.is_empty() {
println!("Warning: category is empty, skipping"); println!("Warning: category is empty, skipping");
continue; continue;
} }
let category_node_id = Uuid::new_v4().to_string(); let category_node_id = Uuid::new_v4().to_string();
let mut aliases_map = HashMap::new(); let mut aliases_map = HashMap::new();
aliases_map.insert("category_type".to_string(), "category".to_string()); aliases_map.insert("category_type".to_string(), "category".to_string());
let category_node = FileNode { let category_node = FileNode {
node_id: category_node_id.clone(), node_id: category_node_id.clone(),
label: parsed.category.clone(), label: parsed.category.clone(),
@@ -221,20 +247,27 @@ pub fn import_categories_to_db(conn: &rusqlite::Connection, user_id: &str, tree_
updated_at: chrono::Utc::now().to_rfc3339(), updated_at: chrono::Utc::now().to_rfc3339(),
sort_order: 0, sort_order: 0,
}; };
println!("Inserting category node: {} (id: {})", category_node.label, category_node_id); println!(
"Inserting category node: {} (id: {})",
category_node.label, category_node_id
);
tree.insert_node(conn, &category_node)?; tree.insert_node(conn, &category_node)?;
println!("Category node inserted successfully"); println!("Category node inserted successfully");
for section in parsed.sections { for section in parsed.sections {
println!("Processing section: {} with {} files", section.product, section.files.len()); println!(
"Processing section: {} with {} files",
section.product,
section.files.len()
);
let product_node_id = Uuid::new_v4().to_string(); let product_node_id = Uuid::new_v4().to_string();
let mut aliases_map = HashMap::new(); let mut aliases_map = HashMap::new();
aliases_map.insert("product".to_string(), section.product.clone()); aliases_map.insert("product".to_string(), section.product.clone());
let product_node = FileNode { let product_node = FileNode {
node_id: product_node_id.clone(), node_id: product_node_id.clone(),
label: section.product.clone(), label: section.product.clone(),
@@ -253,15 +286,18 @@ pub fn import_categories_to_db(conn: &rusqlite::Connection, user_id: &str, tree_
updated_at: chrono::Utc::now().to_rfc3339(), updated_at: chrono::Utc::now().to_rfc3339(),
sort_order: 0, sort_order: 0,
}; };
tree.insert_node(conn, &product_node)?; tree.insert_node(conn, &product_node)?;
for file in section.files { for file in section.files {
let file_node_id = Uuid::new_v4().to_string(); let file_node_id = Uuid::new_v4().to_string();
let mut aliases_map = HashMap::new(); let mut aliases_map = HashMap::new();
aliases_map.insert("download_url".to_string(), file.download_url.clone()); aliases_map.insert("download_url".to_string(), file.download_url.clone());
aliases_map.insert("file_size_display".to_string(), file.size.clone().unwrap_or_else(|| "Unknown".to_string())); aliases_map.insert(
"file_size_display".to_string(),
file.size.clone().unwrap_or_else(|| "Unknown".to_string()),
);
let file_node = FileNode { let file_node = FileNode {
node_id: file_node_id.clone(), node_id: file_node_id.clone(),
label: file.filename.clone(), label: file.filename.clone(),
@@ -280,42 +316,50 @@ pub fn import_categories_to_db(conn: &rusqlite::Connection, user_id: &str, tree_
updated_at: chrono::Utc::now().to_rfc3339(), updated_at: chrono::Utc::now().to_rfc3339(),
sort_order: 0, sort_order: 0,
}; };
tree.insert_node(conn, &file_node)?; tree.insert_node(conn, &file_node)?;
} }
} }
} }
Ok(()) Ok(())
} }
pub fn import_series_to_db(conn: &rusqlite::Connection, user_id: &str, tree_type: &str) -> Result<()> { pub fn import_series_to_db(
conn: &rusqlite::Connection,
user_id: &str,
tree_type: &str,
) -> Result<()> {
use crate::FileTree; use crate::FileTree;
use filetree::node::{FileNode, Aliases, NodeType}; use filetree::node::{Aliases, FileNode, NodeType};
use uuid::Uuid;
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid;
let series_dir = Path::new("/Users/accusys/markbase/data/downloads/by_series"); let series_dir = Path::new("/Users/accusys/markbase/data/downloads/by_series");
let files = read_series_files(series_dir)?; let files = read_series_files(series_dir)?;
println!("Found {} Markdown files for series", files.len()); println!("Found {} Markdown files for series", files.len());
let mut tree = FileTree::load(conn, user_id, tree_type)?; let mut tree = FileTree::load(conn, user_id, tree_type)?;
for (_filename, content) in files { for (_filename, content) in files {
let parsed = parse_series_markdown(&content)?; let parsed = parse_series_markdown(&content)?;
println!("Parsed series: '{}', sections: {}", parsed.series, parsed.sections.len()); println!(
"Parsed series: '{}', sections: {}",
parsed.series,
parsed.sections.len()
);
if parsed.series.is_empty() { if parsed.series.is_empty() {
println!("Warning: series is empty, skipping"); println!("Warning: series is empty, skipping");
continue; continue;
} }
let series_node_id = Uuid::new_v4().to_string(); let series_node_id = Uuid::new_v4().to_string();
let mut aliases_map = HashMap::new(); let mut aliases_map = HashMap::new();
aliases_map.insert("series_type".to_string(), "series".to_string()); aliases_map.insert("series_type".to_string(), "series".to_string());
let series_node = FileNode { let series_node = FileNode {
node_id: series_node_id.clone(), node_id: series_node_id.clone(),
label: parsed.series.clone(), label: parsed.series.clone(),
@@ -334,18 +378,22 @@ pub fn import_series_to_db(conn: &rusqlite::Connection, user_id: &str, tree_type
updated_at: chrono::Utc::now().to_rfc3339(), updated_at: chrono::Utc::now().to_rfc3339(),
sort_order: 0, sort_order: 0,
}; };
tree.insert_node(conn, &series_node)?; tree.insert_node(conn, &series_node)?;
println!("Series node inserted successfully"); println!("Series node inserted successfully");
for section in parsed.sections { for section in parsed.sections {
println!("Processing section: {} with {} files", section.category, section.files.len()); println!(
"Processing section: {} with {} files",
section.category,
section.files.len()
);
let category_node_id = Uuid::new_v4().to_string(); let category_node_id = Uuid::new_v4().to_string();
let mut aliases_map = HashMap::new(); let mut aliases_map = HashMap::new();
aliases_map.insert("category".to_string(), section.category.clone()); aliases_map.insert("category".to_string(), section.category.clone());
let category_node = FileNode { let category_node = FileNode {
node_id: category_node_id.clone(), node_id: category_node_id.clone(),
label: section.category.clone(), label: section.category.clone(),
@@ -364,15 +412,18 @@ pub fn import_series_to_db(conn: &rusqlite::Connection, user_id: &str, tree_type
updated_at: chrono::Utc::now().to_rfc3339(), updated_at: chrono::Utc::now().to_rfc3339(),
sort_order: 0, sort_order: 0,
}; };
tree.insert_node(conn, &category_node)?; tree.insert_node(conn, &category_node)?;
for file in section.files { for file in section.files {
let file_node_id = Uuid::new_v4().to_string(); let file_node_id = Uuid::new_v4().to_string();
let mut aliases_map = HashMap::new(); let mut aliases_map = HashMap::new();
aliases_map.insert("download_url".to_string(), file.download_url.clone()); aliases_map.insert("download_url".to_string(), file.download_url.clone());
aliases_map.insert("file_size_display".to_string(), file.size.clone().unwrap_or_else(|| "Unknown".to_string())); aliases_map.insert(
"file_size_display".to_string(),
file.size.clone().unwrap_or_else(|| "Unknown".to_string()),
);
let file_node = FileNode { let file_node = FileNode {
node_id: file_node_id.clone(), node_id: file_node_id.clone(),
label: file.filename.clone(), label: file.filename.clone(),
@@ -391,12 +442,12 @@ pub fn import_series_to_db(conn: &rusqlite::Connection, user_id: &str, tree_type
updated_at: chrono::Utc::now().to_rfc3339(), updated_at: chrono::Utc::now().to_rfc3339(),
sort_order: 0, sort_order: 0,
}; };
tree.insert_node(conn, &file_node)?; tree.insert_node(conn, &file_node)?;
} }
} }
} }
Ok(()) Ok(())
} }
@@ -418,8 +469,8 @@ mod tests {
```https://download.accusys.ddns.net/api/v2/download/products/ExaSAN-DAS/C1M_C2M/User%20Guide/C2M-QIG20170906.zip ```https://download.accusys.ddns.net/api/v2/download/products/ExaSAN-DAS/C1M_C2M/User%20Guide/C2M-QIG20170906.zip
``` ```
"#; "#;
let result = parse_category_markdown(content).unwrap(); let result = parse_category_markdown(content).unwrap();
assert_eq!(result.category, "GUI"); assert_eq!(result.category, "GUI");
} }
} }
+15 -15
View File
@@ -1,11 +1,14 @@
pub mod audio;
pub mod auth;
pub mod audit;
pub mod cli;
pub mod api; pub mod api;
pub mod archive; // Archive Module - Universal Compression Format Support (Phase 1-3完成)
pub mod audio;
pub mod audit;
pub mod auth;
pub mod category_view;
pub mod cli;
pub mod command; pub mod command;
pub mod config; pub mod config;
pub mod download; pub mod download;
pub mod import_markdown;
pub mod pg_client; pub mod pg_client;
pub mod render; pub mod render;
pub mod rsync; pub mod rsync;
@@ -14,20 +17,17 @@ pub mod s3_auth;
pub mod s3_config; pub mod s3_config;
pub mod s3_xml; pub mod s3_xml;
pub mod scan; pub mod scan;
pub mod server; pub mod server; // Category View Module - 双视图管理(Phase 1)
pub mod archive; // Archive Module - Universal Compression Format Support (Phase 1-3完成) // pub mod sftp; // ⚠️ russh版本(已禁用)
pub mod category_view; // pub mod ssh2_server; // ssh2服务器(已禁用)
pub mod import_markdown; // Category View Module - 双视图管理(Phase 1) // pub mod ssh2_mod; // ssh2辅助模块(已禁用)
// pub mod sftp; // ⚠️ russh版本(已禁用) pub mod provider; // DataProvider抽象层(Phase 5)
// pub mod ssh2_server; // ssh2服务器(已禁用) pub mod ssh_server; // SSH服务器(Phase 1-9完成,正在修复编译错误)⭐⭐⭐⭐⭐
// pub mod ssh2_mod; // ssh2辅助模块(已禁用)
pub mod ssh_server; // SSH服务器(Phase 1-9完成,正在修复编译错误)⭐⭐⭐⭐⭐
pub mod sync; pub mod sync;
pub mod provider; // DataProvider抽象层(Phase 5) pub mod vfs; // VFS抽象层(Phase 1-6重构计划)
pub mod vfs; // VFS抽象层(Phase 1-6重构计划)
#[cfg(test)] #[cfg(test)]
mod security_audit; // Security Audit Module - Phase 9 mod security_audit; // Security Audit Module - Phase 9
// Re-export from external filetree crate // Re-export from external filetree crate
pub use filetree::node::FileNode; pub use filetree::node::FileNode;
+3 -3
View File
@@ -1,12 +1,12 @@
use markbase_core::cli::Cli;
use clap::Parser; use clap::Parser;
use markbase_core::cli::Cli;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
env_logger::Builder::from_default_env() env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Info) .filter_level(log::LevelFilter::Info)
.init(); .init();
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { match cli.command {
@@ -25,4 +25,4 @@ async fn main() -> anyhow::Result<()> {
} }
Ok(()) Ok(())
} }
+6
View File
@@ -10,6 +10,12 @@ pub struct PgClient {
database: String, database: String,
} }
impl Default for PgClient {
fn default() -> Self {
Self::new()
}
}
impl PgClient { impl PgClient {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
+6 -3
View File
@@ -1,8 +1,8 @@
pub mod sqlite;
pub mod pg; pub mod pg;
pub mod sqlite;
pub use sqlite::SqliteProvider;
pub use pg::PgProvider; pub use pg::PgProvider;
pub use sqlite::SqliteProvider;
use std::path::PathBuf; use std::path::PathBuf;
@@ -57,7 +57,10 @@ pub trait DataProvider: Send + Sync {
/// 检查用户是否存在且启用 /// 检查用户是否存在且启用
fn user_exists(&self, username: &str) -> Result<bool, ProviderError> { fn user_exists(&self, username: &str) -> Result<bool, ProviderError> {
Ok(self.get_user(username)?.map(|u| u.status == 1).unwrap_or(false)) Ok(self
.get_user(username)?
.map(|u| u.status == 1)
.unwrap_or(false))
} }
/// 获取用户的公开密钥列表(OpenSSH authorized_keys格式) /// 获取用户的公开密钥列表(OpenSSH authorized_keys格式)
+52 -32
View File
@@ -1,7 +1,7 @@
use std::path::PathBuf;
use postgres::{Client, NoTls};
use bcrypt::verify;
use super::{DataProvider, ProviderError, User}; use super::{DataProvider, ProviderError, User};
use bcrypt::verify;
use postgres::{Client, NoTls};
use std::path::PathBuf;
/// PostgreSQL 数据提供者(兼容 SFTPGo 的 users 表) /// PostgreSQL 数据提供者(兼容 SFTPGo 的 users 表)
pub struct PgProvider { pub struct PgProvider {
@@ -13,7 +13,9 @@ impl PgProvider {
/// ///
/// 连接字符串格式:host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026 /// 连接字符串格式:host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026
pub fn new(conn_str: &str) -> Result<Self, ProviderError> { pub fn new(conn_str: &str) -> Result<Self, ProviderError> {
Ok(Self { conn_str: conn_str.to_string() }) Ok(Self {
conn_str: conn_str.to_string(),
})
} }
pub fn from_params( pub fn from_params(
@@ -40,18 +42,22 @@ impl DataProvider for PgProvider {
fn get_user(&self, username: &str) -> Result<Option<User>, ProviderError> { fn get_user(&self, username: &str) -> Result<Option<User>, ProviderError> {
let mut conn = self.open_conn()?; let mut conn = self.open_conn()?;
let result = conn.query_opt( let result = conn
"SELECT username, password, home_dir, permissions, uid, gid, status .query_opt(
"SELECT username, password, home_dir, permissions, uid, gid, status
FROM users WHERE username = $1 AND status = 1", FROM users WHERE username = $1 AND status = 1",
&[&username], &[&username],
).map_err(|e| ProviderError::Internal(format!("Query error: {}", e)))?; )
.map_err(|e| ProviderError::Internal(format!("Query error: {}", e)))?;
match result { match result {
Some(row) => Ok(Some(User { Some(row) => Ok(Some(User {
username: row.get(0), username: row.get(0),
password_hash: row.get::<_, Option<String>>(1).unwrap_or_default(), password_hash: row.get::<_, Option<String>>(1).unwrap_or_default(),
home_dir: PathBuf::from(row.get::<_, String>(2)), home_dir: PathBuf::from(row.get::<_, String>(2)),
permissions: row.get::<_, Option<String>>(3).unwrap_or_else(|| "*".to_string()), permissions: row
.get::<_, Option<String>>(3)
.unwrap_or_else(|| "*".to_string()),
uid: row.get::<_, i64>(4) as u32, uid: row.get::<_, i64>(4) as u32,
gid: row.get::<_, i64>(5) as u32, gid: row.get::<_, i64>(5) as u32,
status: row.get(6), status: row.get(6),
@@ -75,24 +81,31 @@ impl DataProvider for PgProvider {
} }
fn get_home_dir(&self, username: &str) -> Result<Option<String>, ProviderError> { fn get_home_dir(&self, username: &str) -> Result<Option<String>, ProviderError> {
Ok(self.get_user(username)?.map(|u| u.home_dir.to_string_lossy().to_string())) Ok(self
.get_user(username)?
.map(|u| u.home_dir.to_string_lossy().to_string()))
} }
fn get_public_keys(&self, username: &str) -> Result<Vec<String>, ProviderError> { fn get_public_keys(&self, username: &str) -> Result<Vec<String>, ProviderError> {
let mut conn = self.open_conn()?; let mut conn = self.open_conn()?;
let result = conn.query_opt( let result = conn
"SELECT public_keys FROM users WHERE username = $1 AND status = 1", .query_opt(
&[&username], "SELECT public_keys FROM users WHERE username = $1 AND status = 1",
).map_err(|e| ProviderError::Internal(format!("Query error: {}", e)))?; &[&username],
)
.map_err(|e| ProviderError::Internal(format!("Query error: {}", e)))?;
match result { match result {
Some(row) => { Some(row) => {
let json_str: Option<String> = row.get(0); let json_str: Option<String> = row.get(0);
match json_str { match json_str {
Some(s) if !s.is_empty() => { Some(s) if !s.is_empty() => {
let keys: Vec<serde_json::Value> = serde_json::from_str(&s) let keys: Vec<serde_json::Value> =
.map_err(|e| ProviderError::Internal(format!("JSON parse error: {}", e)))?; serde_json::from_str(&s).map_err(|e| {
Ok(keys.iter() ProviderError::Internal(format!("JSON parse error: {}", e))
})?;
Ok(keys
.iter()
.filter_map(|v| v.get("public_key")?.as_str().map(|s| s.to_string())) .filter_map(|v| v.get("public_key")?.as_str().map(|s| s.to_string()))
.collect()) .collect())
} }
@@ -112,7 +125,7 @@ mod tests {
fn test_pg_provider_connection() { fn test_pg_provider_connection() {
// 仅当 SFTPGo PostgreSQL 可用时运行 // 仅当 SFTPGo PostgreSQL 可用时运行
let provider = PgProvider::new( let provider = PgProvider::new(
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026" "host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026",
); );
assert!(provider.is_ok(), "Should connect to SFTPGo PostgreSQL"); assert!(provider.is_ok(), "Should connect to SFTPGo PostgreSQL");
} }
@@ -120,8 +133,9 @@ mod tests {
#[test] #[test]
fn test_pg_get_user_demo() { fn test_pg_get_user_demo() {
let provider = PgProvider::new( let provider = PgProvider::new(
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026" "host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026",
).unwrap(); )
.unwrap();
let user = provider.get_user("demo").unwrap(); let user = provider.get_user("demo").unwrap();
assert!(user.is_some(), "Demo user should exist"); assert!(user.is_some(), "Demo user should exist");
assert_eq!(user.unwrap().username, "demo"); assert_eq!(user.unwrap().username, "demo");
@@ -130,8 +144,9 @@ mod tests {
#[test] #[test]
fn test_pg_get_user_momentry() { fn test_pg_get_user_momentry() {
let provider = PgProvider::new( let provider = PgProvider::new(
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026" "host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026",
).unwrap(); )
.unwrap();
let user = provider.get_user("momentry").unwrap(); let user = provider.get_user("momentry").unwrap();
assert!(user.is_some(), "Momentry user should exist"); assert!(user.is_some(), "Momentry user should exist");
} }
@@ -139,8 +154,9 @@ mod tests {
#[test] #[test]
fn test_pg_get_user_warren() { fn test_pg_get_user_warren() {
let provider = PgProvider::new( let provider = PgProvider::new(
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026" "host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026",
).unwrap(); )
.unwrap();
let user = provider.get_user("warren").unwrap(); let user = provider.get_user("warren").unwrap();
assert!(user.is_some(), "Warren user should exist"); assert!(user.is_some(), "Warren user should exist");
} }
@@ -148,8 +164,9 @@ mod tests {
#[test] #[test]
fn test_pg_check_password_demo() { fn test_pg_check_password_demo() {
let provider = PgProvider::new( let provider = PgProvider::new(
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026" "host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026",
).unwrap(); )
.unwrap();
let valid = provider.check_password("demo", "demo123").unwrap(); let valid = provider.check_password("demo", "demo123").unwrap();
assert!(valid, "Password should be valid"); assert!(valid, "Password should be valid");
} }
@@ -157,8 +174,9 @@ mod tests {
#[test] #[test]
fn test_pg_check_password_invalid() { fn test_pg_check_password_invalid() {
let provider = PgProvider::new( let provider = PgProvider::new(
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026" "host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026",
).unwrap(); )
.unwrap();
let valid = provider.check_password("demo", "wrong").unwrap(); let valid = provider.check_password("demo", "wrong").unwrap();
assert!(!valid, "Wrong password should fail"); assert!(!valid, "Wrong password should fail");
} }
@@ -166,8 +184,9 @@ mod tests {
#[test] #[test]
fn test_pg_get_home_dir() { fn test_pg_get_home_dir() {
let provider = PgProvider::new( let provider = PgProvider::new(
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026" "host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026",
).unwrap(); )
.unwrap();
let dir = provider.get_home_dir("demo").unwrap(); let dir = provider.get_home_dir("demo").unwrap();
assert!(dir.is_some()); assert!(dir.is_some());
assert!(dir.unwrap().contains("momentry")); assert!(dir.unwrap().contains("momentry"));
@@ -176,8 +195,9 @@ mod tests {
#[test] #[test]
fn test_pg_nonexistent_user() { fn test_pg_nonexistent_user() {
let provider = PgProvider::new( let provider = PgProvider::new(
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026" "host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026",
).unwrap(); )
.unwrap();
let user = provider.get_user("__nonexistent__").unwrap(); let user = provider.get_user("__nonexistent__").unwrap();
assert!(user.is_none()); assert!(user.is_none());
} }
+14 -7
View File
@@ -1,7 +1,7 @@
use std::path::PathBuf;
use rusqlite::{Connection, params};
use bcrypt::verify;
use super::{DataProvider, ProviderError, User}; use super::{DataProvider, ProviderError, User};
use bcrypt::verify;
use rusqlite::{params, Connection};
use std::path::PathBuf;
/// SQLite 数据提供者 /// SQLite 数据提供者
pub struct SqliteProvider { pub struct SqliteProvider {
@@ -13,7 +13,8 @@ impl SqliteProvider {
let path = PathBuf::from(db_path); let path = PathBuf::from(db_path);
if !path.exists() { if !path.exists() {
return Err(ProviderError::NotFound(format!( return Err(ProviderError::NotFound(format!(
"Database not found: {}", db_path "Database not found: {}",
db_path
))); )));
} }
Ok(Self { db_path: path }) Ok(Self { db_path: path })
@@ -50,7 +51,8 @@ impl DataProvider for SqliteProvider {
Ok(user) => Ok(Some(user)), Ok(user) => Ok(Some(user)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(ProviderError::Internal(format!( Err(e) => Err(ProviderError::Internal(format!(
"Database query error: {}", e "Database query error: {}",
e
))), ))),
} }
} }
@@ -66,7 +68,9 @@ impl DataProvider for SqliteProvider {
} }
fn get_home_dir(&self, username: &str) -> Result<Option<String>, ProviderError> { fn get_home_dir(&self, username: &str) -> Result<Option<String>, ProviderError> {
Ok(self.get_user(username)?.map(|u| u.home_dir.to_string_lossy().to_string())) Ok(self
.get_user(username)?
.map(|u| u.home_dir.to_string_lossy().to_string()))
} }
fn get_public_keys(&self, username: &str) -> Result<Vec<String>, ProviderError> { fn get_public_keys(&self, username: &str) -> Result<Vec<String>, ProviderError> {
@@ -98,7 +102,10 @@ mod tests {
} }
fn get_test_db_path() -> String { fn get_test_db_path() -> String {
format!("{}/../data/auth.sqlite", std::env::var("CARGO_MANIFEST_DIR").unwrap()) format!(
"{}/../data/auth.sqlite",
std::env::var("CARGO_MANIFEST_DIR").unwrap()
)
} }
#[test] #[test]
-1
View File
@@ -1,4 +1,3 @@
use anyhow::Result;
use md5::compute; use md5::compute;
pub struct RollingChecksum { pub struct RollingChecksum {
+6
View File
@@ -50,6 +50,12 @@ pub struct DecompressionStream {
decompressor: Decompress, decompressor: Decompress,
} }
impl Default for DecompressionStream {
fn default() -> Self {
Self::new()
}
}
impl DecompressionStream { impl DecompressionStream {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
+1 -3
View File
@@ -1,7 +1,5 @@
use crate::rsync::checksum::{compute_block_checksums, BlockChecksum};
use crate::rsync::compress::{CompressionStream, DecompressionStream};
use crate::rsync::delta::{DeltaAlgorithm, DeltaInstruction}; use crate::rsync::delta::{DeltaAlgorithm, DeltaInstruction};
use crate::rsync::protocol::{RsyncCommand, RsyncProtocol}; use crate::rsync::protocol::RsyncCommand;
use crate::rsync::RsyncConfig; use crate::rsync::RsyncConfig;
use anyhow::Result; use anyhow::Result;
use std::sync::Arc; use std::sync::Arc;
+6
View File
@@ -162,6 +162,12 @@ pub struct RsyncHandshake {
negotiated_version: u32, negotiated_version: u32,
} }
impl Default for RsyncHandshake {
fn default() -> Self {
Self::new()
}
}
impl RsyncHandshake { impl RsyncHandshake {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
+17 -19
View File
@@ -1,15 +1,17 @@
use filetree::{FileTree, node::{FileNode, Aliases}};
use axum::{ use axum::{
body::Body, body::Body,
extract::{Path, State}, extract::{Path, State},
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
response::{IntoResponse, Json}, response::{IntoResponse, Json},
}; };
use filetree::{
node::FileNode,
FileTree,
};
use futures_util::StreamExt; use futures_util::StreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
@@ -41,10 +43,10 @@ pub async fn list_buckets(State(state): State<crate::server::AppState>) -> impl
pub async fn list_objects( pub async fn list_objects(
Path(bucket): Path<String>, Path(bucket): Path<String>,
State(state): State<crate::server::AppState>, State(_state): State<crate::server::AppState>,
) -> impl IntoResponse { ) -> impl IntoResponse {
println!("S3 List Objects: bucket={}", bucket); println!("S3 List Objects: bucket={}", bucket);
let conn = match FileTree::open_user_db(&bucket) { let conn = match FileTree::open_user_db(&bucket) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
@@ -70,20 +72,20 @@ pub async fn list_objects(
"Key": build_s3_key(&tree, n), "Key": build_s3_key(&tree, n),
"LastModified": n.registered_at.clone().unwrap_or_default(), "LastModified": n.registered_at.clone().unwrap_or_default(),
"ETag": n.sha256.clone().unwrap_or_default(), "ETag": n.sha256.clone().unwrap_or_default(),
"Size": n.file_size.clone().unwrap_or(0), "Size": n.file_size.unwrap_or(0),
}) })
}) })
.collect(); .collect();
println!("Listed {} objects for bucket {}", objects.len(), bucket); println!("Listed {} objects for bucket {}", objects.len(), bucket);
let (headers, xml_body) = crate::s3_xml::list_objects_xml(&bucket, &objects); let (headers, xml_body) = crate::s3_xml::list_objects_xml(&bucket, &objects);
(StatusCode::OK, headers, xml_body).into_response() (StatusCode::OK, headers, xml_body).into_response()
} }
pub async fn get_object( pub async fn get_object(
Path((bucket, key)): Path<(String, String)>, Path((bucket, key)): Path<(String, String)>,
State(state): State<crate::server::AppState>, State(_state): State<crate::server::AppState>,
headers: HeaderMap, headers: HeaderMap,
) -> impl IntoResponse { ) -> impl IntoResponse {
println!("S3 GET Object: bucket={}, key={}", bucket, key); println!("S3 GET Object: bucket={}, key={}", bucket, key);
@@ -119,7 +121,7 @@ pub async fn get_object(
); );
let file_uuid = node.file_uuid.clone().unwrap_or_default(); let file_uuid = node.file_uuid.clone().unwrap_or_default();
let file_size = node.file_size.clone().unwrap_or(0); let file_size = node.file_size.unwrap_or(0);
let sha256 = node.sha256.clone().unwrap_or_default(); let sha256 = node.sha256.clone().unwrap_or_default();
let real_path = get_real_file_path(&conn, &file_uuid); let real_path = get_real_file_path(&conn, &file_uuid);
@@ -233,7 +235,7 @@ pub async fn put_object(
let sha256_hash_clone = sha256_hash.clone(); let sha256_hash_clone = sha256_hash.clone();
let file_path_clone = file_path.clone(); let file_path_clone = file_path.clone();
let label = key.split('/').last().unwrap_or(&key).to_string(); let label = key.split('/').next_back().unwrap_or(&key).to_string();
let result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { let result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let conn = match FileTree::open_user_db(&bucket) { let conn = match FileTree::open_user_db(&bucket) {
@@ -246,7 +248,7 @@ pub async fn put_object(
|row| row.get::<_, i32>(0), |row| row.get::<_, i32>(0),
) )
.unwrap_or(0) > 0; .unwrap_or(0) > 0;
if !has_tables { if !has_tables {
// Initialize tables if not exist // Initialize tables if not exist
c.execute_batch(filetree::CREATE_TABLES)?; c.execute_batch(filetree::CREATE_TABLES)?;
@@ -298,7 +300,7 @@ pub async fn put_object(
pub async fn head_object( pub async fn head_object(
Path((bucket, key)): Path<(String, String)>, Path((bucket, key)): Path<(String, String)>,
State(state): State<crate::server::AppState>, State(_state): State<crate::server::AppState>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let conn = match FileTree::open_user_db(&bucket) { let conn = match FileTree::open_user_db(&bucket) {
Ok(c) => c, Ok(c) => c,
@@ -323,7 +325,7 @@ pub async fn head_object(
"ETag", "ETag",
node.sha256.clone().unwrap_or_default().parse().unwrap(), node.sha256.clone().unwrap_or_default().parse().unwrap(),
); );
headers.insert("Content-Length", node.file_size.clone().unwrap_or(0).into()); headers.insert("Content-Length", node.file_size.unwrap_or(0).into());
(StatusCode::OK, headers) (StatusCode::OK, headers)
} }
@@ -438,7 +440,7 @@ fn find_node_by_s3_key(tree: &FileTree, key: &str) -> Option<FileNode> {
} }
// 方法2:通过filename直接匹配(fallback) // 方法2:通过filename直接匹配(fallback)
let filename = key.split('/').last().unwrap_or(key); let filename = key.split('/').next_back().unwrap_or(key);
tree.nodes tree.nodes
.iter() .iter()
.filter(|n| n.node_type == filetree::node::NodeType::File) .filter(|n| n.node_type == filetree::node::NodeType::File)
@@ -501,7 +503,7 @@ async fn handle_range_request(
} }
// 使用take限制读取长度 // 使用take限制读取长度
let limited_file = file.take(content_length as u64); let limited_file = file.take(content_length);
let stream = ReaderStream::new(limited_file); let stream = ReaderStream::new(limited_file);
let body = Body::from_stream(stream); let body = Body::from_stream(stream);
@@ -535,11 +537,7 @@ fn parse_range_header(range: &str, file_size: i64) -> Option<(u64, u64)> {
let (start, end) = if parts[0].is_empty() { let (start, end) = if parts[0].is_empty() {
// "bytes=-N"格式:最后N字节 // "bytes=-N"格式:最后N字节
let suffix_length = parts[1].parse::<u64>().ok()?; let suffix_length = parts[1].parse::<u64>().ok()?;
let start = if suffix_length > file_size as u64 { let start = (file_size as u64).saturating_sub(suffix_length);
0
} else {
file_size as u64 - suffix_length
};
(start, file_size as u64 - 1) (start, file_size as u64 - 1)
} else if parts[1].is_empty() { } else if parts[1].is_empty() {
// "bytes=N-"格式:从N到结尾 // "bytes=N-"格式:从N到结尾
+5 -5
View File
@@ -8,11 +8,11 @@ type HmacSha256 = Hmac<Sha256>;
pub fn verify_signature(headers: HeaderMap, method: &str, path: &str) -> bool { pub fn verify_signature(headers: HeaderMap, method: &str, path: &str) -> bool {
// Load S3 config and check require_auth flag // Load S3 config and check require_auth flag
let config = crate::s3_config::S3Config::load_default().unwrap_or_default(); let config = crate::s3_config::S3Config::load_default().unwrap_or_default();
// Merge environment variables (allows override via MB_S3_REQUIRE_AUTH) // Merge environment variables (allows override via MB_S3_REQUIRE_AUTH)
let mut config = config; let mut config = config;
config.merge_env(); config.merge_env();
if !config.s3.require_auth { if !config.s3.require_auth {
// Development mode: allow access without authentication // Development mode: allow access without authentication
return true; return true;
@@ -127,7 +127,7 @@ fn calculate_signature(
headers: HeaderMap, headers: HeaderMap,
method: &str, method: &str,
path: &str, path: &str,
access_key: &str, _access_key: &str,
secret_key: &str, secret_key: &str,
region: &str, region: &str,
service: &str, service: &str,
@@ -143,9 +143,9 @@ fn calculate_signature(
let signing_key = calculate_signing_key(secret_key, date, region, service); let signing_key = calculate_signing_key(secret_key, date, region, service);
// 4. Calculate Signature // 4. Calculate Signature
let signature = hmac_sha256_hex(&signing_key, &string_to_sign);
signature hmac_sha256_hex(&signing_key, &string_to_sign)
} }
fn create_canonical_request(headers: HeaderMap, method: &str, path: &str) -> String { fn create_canonical_request(headers: HeaderMap, method: &str, path: &str) -> String {
+44 -50
View File
@@ -4,6 +4,7 @@ use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct S3Config { pub struct S3Config {
#[serde(default)] #[serde(default)]
pub s3: S3Section, pub s3: S3Section,
@@ -40,6 +41,7 @@ pub struct KeysSection {
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct BucketsSection { pub struct BucketsSection {
#[serde(default)] #[serde(default)]
pub mappings: std::collections::HashMap<String, String>, pub mappings: std::collections::HashMap<String, String>,
@@ -96,16 +98,6 @@ fn admin_permissions() -> Vec<String> {
] ]
} }
impl Default for S3Config {
fn default() -> Self {
Self {
s3: S3Section::default(),
keys: KeysSection::default(),
buckets: BucketsSection::default(),
permissions: PermissionsSection::default(),
}
}
}
impl Default for S3Section { impl Default for S3Section {
fn default() -> Self { fn default() -> Self {
@@ -129,13 +121,6 @@ impl Default for KeysSection {
} }
} }
impl Default for BucketsSection {
fn default() -> Self {
Self {
mappings: std::collections::HashMap::new(),
}
}
}
impl Default for PermissionsSection { impl Default for PermissionsSection {
fn default() -> Self { fn default() -> Self {
@@ -169,9 +154,9 @@ impl S3Config {
Self::load("config/s3.toml") Self::load("config/s3.toml")
} }
pub fn save(&self, path: &str) -> Result<()> { pub fn save(&self, path: &str) -> Result<()> {
let config_path = PathBuf::from(path); let config_path = PathBuf::from(path);
// Create backup before saving // Create backup before saving
if config_path.exists() { if config_path.exists() {
let backup_path = config_path.with_extension("toml.bak"); let backup_path = config_path.with_extension("toml.bak");
@@ -179,13 +164,13 @@ pub fn save(&self, path: &str) -> Result<()> {
.with_context(|| format!("Failed to create backup: {}", backup_path.display()))?; .with_context(|| format!("Failed to create backup: {}", backup_path.display()))?;
log::info!("S3 config backup created: {}", backup_path.display()); log::info!("S3 config backup created: {}", backup_path.display());
} }
let content = toml::to_string_pretty(self) let content =
.with_context(|| "Failed to serialize S3 config")?; toml::to_string_pretty(self).with_context(|| "Failed to serialize S3 config")?;
std::fs::write(&config_path, content) std::fs::write(&config_path, content)
.with_context(|| format!("Failed to write S3 config: {}", path))?; .with_context(|| format!("Failed to write S3 config: {}", path))?;
log::info!("S3 config saved to: {}", path); log::info!("S3 config saved to: {}", path);
Ok(()) Ok(())
} }
@@ -255,10 +240,16 @@ pub fn save(&self, path: &str) -> Result<()> {
// Validate permission format // Validate permission format
let valid_permissions = [ let valid_permissions = [
"GetObject", "PutObject", "DeleteObject", "ListBucket", "GetObject",
"HeadObject", "ListAllMyBuckets", "CreateBucket", "DeleteBucket" "PutObject",
"DeleteObject",
"ListBucket",
"HeadObject",
"ListAllMyBuckets",
"CreateBucket",
"DeleteBucket",
]; ];
for perm in &self.permissions.default_permissions { for perm in &self.permissions.default_permissions {
if !valid_permissions.contains(&perm.as_str()) { if !valid_permissions.contains(&perm.as_str()) {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
@@ -289,18 +280,18 @@ pub fn save(&self, path: &str) -> Result<()> {
"s3.region" => Some(self.s3.region.clone()), "s3.region" => Some(self.s3.region.clone()),
"s3.service" => Some(self.s3.service.clone()), "s3.service" => Some(self.s3.service.clone()),
"s3.require_auth" => Some(self.s3.require_auth.to_string()), "s3.require_auth" => Some(self.s3.require_auth.to_string()),
"keys.default_access_key" => Some(self.keys.default_access_key.clone()), "keys.default_access_key" => Some(self.keys.default_access_key.clone()),
"keys.default_secret_key" => Some(self.keys.default_secret_key.clone()), "keys.default_secret_key" => Some(self.keys.default_secret_key.clone()),
"keys.keys_db_path" => Some(self.keys.keys_db_path.clone()), "keys.keys_db_path" => Some(self.keys.keys_db_path.clone()),
"permissions.default_permissions" => { "permissions.default_permissions" => Some(
Some(serde_json::to_string(&self.permissions.default_permissions).unwrap_or_default()) serde_json::to_string(&self.permissions.default_permissions).unwrap_or_default(),
} ),
"permissions.admin_permissions" => { "permissions.admin_permissions" => {
Some(serde_json::to_string(&self.permissions.admin_permissions).unwrap_or_default()) Some(serde_json::to_string(&self.permissions.admin_permissions).unwrap_or_default())
} }
_ => None, _ => None,
} }
} }
@@ -312,11 +303,11 @@ pub fn save(&self, path: &str) -> Result<()> {
"s3.region" => self.s3.region = value.to_string(), "s3.region" => self.s3.region = value.to_string(),
"s3.service" => self.s3.service = value.to_string(), "s3.service" => self.s3.service = value.to_string(),
"s3.require_auth" => self.s3.require_auth = value.parse()?, "s3.require_auth" => self.s3.require_auth = value.parse()?,
"keys.default_access_key" => self.keys.default_access_key = value.to_string(), "keys.default_access_key" => self.keys.default_access_key = value.to_string(),
"keys.default_secret_key" => self.keys.default_secret_key = value.to_string(), "keys.default_secret_key" => self.keys.default_secret_key = value.to_string(),
"keys.keys_db_path" => self.keys.keys_db_path = value.to_string(), "keys.keys_db_path" => self.keys.keys_db_path = value.to_string(),
"permissions.default_permissions" => { "permissions.default_permissions" => {
self.permissions.default_permissions = serde_json::from_str(value) self.permissions.default_permissions = serde_json::from_str(value)
.with_context(|| "Failed to parse permissions array")?; .with_context(|| "Failed to parse permissions array")?;
@@ -325,7 +316,7 @@ pub fn save(&self, path: &str) -> Result<()> {
self.permissions.admin_permissions = serde_json::from_str(value) self.permissions.admin_permissions = serde_json::from_str(value)
.with_context(|| "Failed to parse admin permissions array")?; .with_context(|| "Failed to parse admin permissions array")?;
} }
_ => return Err(anyhow::anyhow!("Invalid S3 config key: {}", key)), _ => return Err(anyhow::anyhow!("Invalid S3 config key: {}", key)),
} }
Ok(()) Ok(())
@@ -340,15 +331,15 @@ mod tests {
#[test] #[test]
fn test_default_config() { fn test_default_config() {
let config = S3Config::default(); let config = S3Config::default();
assert_eq!(config.s3.enabled, true); assert_eq!(config.s3.enabled, true);
assert_eq!(config.s3.require_auth, false); assert_eq!(config.s3.require_auth, false);
assert_eq!(config.s3.endpoint, "http://localhost:11438/s3"); assert_eq!(config.s3.endpoint, "http://localhost:11438/s3");
assert_eq!(config.s3.region, "us-east-1"); assert_eq!(config.s3.region, "us-east-1");
assert_eq!(config.keys.default_access_key, "markbase_access_key_001"); assert_eq!(config.keys.default_access_key, "markbase_access_key_001");
assert_eq!(config.keys.default_secret_key, "markbase_secret_key_xyz123"); assert_eq!(config.keys.default_secret_key, "markbase_secret_key_xyz123");
assert_eq!(config.permissions.default_permissions.len(), 3); assert_eq!(config.permissions.default_permissions.len(), 3);
assert_eq!(config.permissions.admin_permissions.len(), 5); assert_eq!(config.permissions.admin_permissions.len(), 5);
} }
@@ -357,9 +348,9 @@ mod tests {
fn test_load_missing_config() { fn test_load_missing_config() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("missing.toml"); let config_path = temp_dir.path().join("missing.toml");
let config = S3Config::load(&config_path.to_string_lossy()).unwrap(); let config = S3Config::load(&config_path.to_string_lossy()).unwrap();
assert_eq!(config.s3.enabled, true); assert_eq!(config.s3.enabled, true);
assert_eq!(config.s3.require_auth, false); assert_eq!(config.s3.require_auth, false);
} }
@@ -368,13 +359,13 @@ mod tests {
fn test_merge_env() { fn test_merge_env() {
std::env::set_var("MB_S3_REQUIRE_AUTH", "true"); std::env::set_var("MB_S3_REQUIRE_AUTH", "true");
std::env::set_var("MB_S3_ENDPOINT", "http://custom.endpoint"); std::env::set_var("MB_S3_ENDPOINT", "http://custom.endpoint");
let mut config = S3Config::default(); let mut config = S3Config::default();
config.merge_env(); config.merge_env();
assert_eq!(config.s3.require_auth, true); assert_eq!(config.s3.require_auth, true);
assert_eq!(config.s3.endpoint, "http://custom.endpoint"); assert_eq!(config.s3.endpoint, "http://custom.endpoint");
std::env::remove_var("MB_S3_REQUIRE_AUTH"); std::env::remove_var("MB_S3_REQUIRE_AUTH");
std::env::remove_var("MB_S3_ENDPOINT"); std::env::remove_var("MB_S3_ENDPOINT");
} }
@@ -383,7 +374,7 @@ mod tests {
fn test_validate() { fn test_validate() {
let config = S3Config::default(); let config = S3Config::default();
assert!(config.validate().is_ok()); assert!(config.validate().is_ok());
let mut invalid_config = S3Config::default(); let mut invalid_config = S3Config::default();
invalid_config.s3.endpoint = "".to_string(); invalid_config.s3.endpoint = "".to_string();
assert!(invalid_config.validate().is_err()); assert!(invalid_config.validate().is_err());
@@ -392,14 +383,17 @@ mod tests {
#[test] #[test]
fn test_get_set() { fn test_get_set() {
let mut config = S3Config::default(); let mut config = S3Config::default();
assert_eq!(config.get("s3.enabled"), Some("true".to_string())); assert_eq!(config.get("s3.enabled"), Some("true".to_string()));
assert_eq!(config.get("s3.endpoint"), Some("http://localhost:11438/s3".to_string())); assert_eq!(
config.get("s3.endpoint"),
Some("http://localhost:11438/s3".to_string())
);
config.set("s3.require_auth", "true").unwrap(); config.set("s3.require_auth", "true").unwrap();
assert_eq!(config.s3.require_auth, true); assert_eq!(config.s3.require_auth, true);
config.set("s3.endpoint", "http://new.endpoint").unwrap(); config.set("s3.endpoint", "http://new.endpoint").unwrap();
assert_eq!(config.s3.endpoint, "http://new.endpoint"); assert_eq!(config.s3.endpoint, "http://new.endpoint");
} }
} }
+17 -12
View File
@@ -4,16 +4,18 @@ use serde_json::Value;
pub fn list_buckets_xml(buckets: &[String]) -> (HeaderMap, String) { pub fn list_buckets_xml(buckets: &[String]) -> (HeaderMap, String) {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/xml".parse().unwrap()); headers.insert("Content-Type", "application/xml".parse().unwrap());
let bucket_entries = buckets let bucket_entries = buckets
.iter() .iter()
.map(|b| format!( .map(|b| {
"<Bucket><Name>{}</Name><CreationDate>2026-05-27T00:00:00Z</CreationDate></Bucket>", format!(
b "<Bucket><Name>{}</Name><CreationDate>2026-05-27T00:00:00Z</CreationDate></Bucket>",
)) b
)
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n "); .join("\n ");
let xml = format!( let xml = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"> <ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">
@@ -27,22 +29,25 @@ pub fn list_buckets_xml(buckets: &[String]) -> (HeaderMap, String) {
</ListAllMyBucketsResult>", </ListAllMyBucketsResult>",
bucket_entries bucket_entries
); );
(headers, xml) (headers, xml)
} }
pub fn list_objects_xml(bucket_name: &str, objects: &[Value]) -> (HeaderMap, String) { pub fn list_objects_xml(bucket_name: &str, objects: &[Value]) -> (HeaderMap, String) {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/xml".parse().unwrap()); headers.insert("Content-Type", "application/xml".parse().unwrap());
let object_entries = objects let object_entries = objects
.iter() .iter()
.map(|obj| { .map(|obj| {
let key = obj.get("Key").and_then(|k| k.as_str()).unwrap_or(""); let key = obj.get("Key").and_then(|k| k.as_str()).unwrap_or("");
let last_modified = obj.get("LastModified").and_then(|l| l.as_str()).unwrap_or(""); let last_modified = obj
.get("LastModified")
.and_then(|l| l.as_str())
.unwrap_or("");
let etag = obj.get("ETag").and_then(|e| e.as_str()).unwrap_or(""); let etag = obj.get("ETag").and_then(|e| e.as_str()).unwrap_or("");
let size = obj.get("Size").and_then(|s| s.as_i64()).unwrap_or(0); let size = obj.get("Size").and_then(|s| s.as_i64()).unwrap_or(0);
format!( format!(
"<Contents> "<Contents>
<Key>{}</Key> <Key>{}</Key>
@@ -55,7 +60,7 @@ pub fn list_objects_xml(bucket_name: &str, objects: &[Value]) -> (HeaderMap, Str
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n "); .join("\n ");
let xml = format!( let xml = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"> <ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">
@@ -68,6 +73,6 @@ pub fn list_objects_xml(bucket_name: &str, objects: &[Value]) -> (HeaderMap, Str
</ListBucketResult>", </ListBucketResult>",
bucket_name, object_entries bucket_name, object_entries
); );
(headers, xml) (headers, xml)
} }
+1 -1
View File
@@ -439,7 +439,7 @@ fn compute_hashes_parallel(
let mut p = processed.lock().unwrap(); let mut p = processed.lock().unwrap();
*p += 1; *p += 1;
if *p % 100 == 0 { if (*p).is_multiple_of(100) {
print!("\r Hashed {}/{} files...", *p, total); print!("\r Hashed {}/{} files...", *p, total);
use std::io::Write; use std::io::Write;
std::io::stdout().flush().ok(); std::io::stdout().flush().ok();
@@ -12,20 +12,22 @@ fn get_test_provider() -> Arc<dyn DataProvider> {
#[test] #[test]
fn test_password_authentication_brute_force_prevention() { fn test_password_authentication_brute_force_prevention() {
let provider = get_test_provider(); let provider = get_test_provider();
assert!(provider.check_password("demo", "demo123").unwrap()); assert!(provider.check_password("demo", "demo123").unwrap());
assert!(!provider.check_password("demo", "wrongpassword").unwrap()); assert!(!provider.check_password("demo", "wrongpassword").unwrap());
assert!(!provider.check_password("demo", "").unwrap()); assert!(!provider.check_password("demo", "").unwrap());
assert!(!provider.check_password("__nonexistent__", "anypassword").unwrap()); assert!(!provider
.check_password("__nonexistent__", "anypassword")
.unwrap());
} }
#[test] #[test]
fn test_publickey_authentication_security() { fn test_publickey_authentication_security() {
let provider = get_test_provider(); let provider = get_test_provider();
let keys = provider.get_public_keys("demo").unwrap(); let keys = provider.get_public_keys("demo").unwrap();
assert!(keys.is_empty() || keys.len() >= 0); assert!(keys.is_empty() || keys.len() >= 0);
let keys = provider.get_public_keys("__nonexistent__").unwrap(); let keys = provider.get_public_keys("__nonexistent__").unwrap();
assert!(keys.is_empty()); assert!(keys.is_empty());
} }
@@ -33,10 +35,10 @@ fn test_publickey_authentication_security() {
#[test] #[test]
fn test_user_status_check() { fn test_user_status_check() {
let provider = get_test_provider(); let provider = get_test_provider();
let user = provider.get_user("demo").unwrap(); let user = provider.get_user("demo").unwrap();
assert!(user.is_some()); assert!(user.is_some());
let user = provider.get_user("demo").unwrap(); let user = provider.get_user("demo").unwrap();
if let Some(u) = user { if let Some(u) = user {
assert_eq!(u.status, 1); assert_eq!(u.status, 1);
@@ -46,16 +48,16 @@ fn test_user_status_check() {
#[test] #[test]
fn test_home_dir_security() { fn test_home_dir_security() {
let provider = get_test_provider(); let provider = get_test_provider();
let home = provider.get_home_dir("demo").unwrap(); let home = provider.get_home_dir("demo").unwrap();
assert!(home.is_some()); assert!(home.is_some());
let home = provider.get_home_dir("__nonexistent__").unwrap(); let home = provider.get_home_dir("__nonexistent__").unwrap();
assert!(home.is_none()); assert!(home.is_none());
if let Some(home_path) = provider.get_home_dir("demo").unwrap() { if let Some(home_path) = provider.get_home_dir("demo").unwrap() {
assert!(!home_path.contains("..")); assert!(!home_path.contains(".."));
assert!(!home_path.starts_with("/etc")); assert!(!home_path.starts_with("/etc"));
assert!(!home_path.starts_with("/root")); assert!(!home_path.starts_with("/root"));
} }
} }
@@ -19,7 +19,7 @@ fn test_channel_window_size_limits() {
#[test] #[test]
fn test_channel_request_validation() { fn test_channel_request_validation() {
let valid_requests = ["exec", "shell", "subsystem", "env"]; let valid_requests = ["exec", "shell", "subsystem", "env"];
for request in valid_requests { for request in valid_requests {
assert!(!request.is_empty()); assert!(!request.is_empty());
} }
@@ -30,6 +30,6 @@ fn test_channel_data_integrity() {
// Data should not exceed window size // Data should not exceed window size
let window_size = 32768u32; let window_size = 32768u32;
let max_data = window_size; let max_data = window_size;
assert!(max_data <= window_size); assert!(max_data <= window_size);
} }
@@ -1,11 +1,11 @@
use crate::ssh_server::cipher::EncryptionContext; use crate::ssh_server::cipher::EncryptionContext;
use crate::ssh_server::crypto::{SessionKeys, Curve25519Kex, Ed25519HostKey}; use crate::ssh_server::crypto::{Curve25519Kex, Ed25519HostKey, SessionKeys};
#[test] #[test]
fn test_aes_ctr_encryption_decryption_consistency() { fn test_aes_ctr_encryption_decryption_consistency() {
let key = vec![0u8; 16]; let key = vec![0u8; 16];
let iv = vec![0u8; 16]; let iv = vec![0u8; 16];
let mut ctx = EncryptionContext::from_session_keys(&SessionKeys { let mut ctx = EncryptionContext::from_session_keys(&SessionKeys {
session_id: vec![0u8; 32], session_id: vec![0u8; 32],
encryption_key_ctos: key.clone(), encryption_key_ctos: key.clone(),
@@ -15,10 +15,10 @@ fn test_aes_ctr_encryption_decryption_consistency() {
iv_ctos: iv.clone(), iv_ctos: iv.clone(),
iv_stoc: iv.clone(), iv_stoc: iv.clone(),
}); });
let plaintext = b"Test message for encryption"; let plaintext = b"Test message for encryption";
let ciphertext = ctx.encrypt_packet(plaintext, &key, &iv).unwrap(); let ciphertext = ctx.encrypt_packet(plaintext, &key, &iv).unwrap();
let decrypted = ctx.decrypt_packet(&ciphertext, &key, &iv).unwrap(); let decrypted = ctx.decrypt_packet(&ciphertext, &key, &iv).unwrap();
assert_eq!(plaintext.to_vec(), decrypted); assert_eq!(plaintext.to_vec(), decrypted);
} }
@@ -27,7 +27,7 @@ fn test_aes_ctr_encryption_decryption_consistency() {
fn test_hmac_sha256_authentication() { fn test_hmac_sha256_authentication() {
let key = vec![0u8; 32]; let key = vec![0u8; 32];
let data = b"Test data for HMAC"; let data = b"Test data for HMAC";
let ctx = EncryptionContext::from_session_keys(&SessionKeys { let ctx = EncryptionContext::from_session_keys(&SessionKeys {
session_id: vec![0u8; 32], session_id: vec![0u8; 32],
encryption_key_ctos: vec![0u8; 16], encryption_key_ctos: vec![0u8; 16],
@@ -37,12 +37,12 @@ fn test_hmac_sha256_authentication() {
iv_ctos: vec![0u8; 16], iv_ctos: vec![0u8; 16],
iv_stoc: vec![0u8; 16], iv_stoc: vec![0u8; 16],
}); });
let mac = ctx.compute_mac(1, data, &key).unwrap(); let mac = ctx.compute_mac(1, data, &key).unwrap();
assert_eq!(mac.len(), 32); assert_eq!(mac.len(), 32);
assert!(ctx.verify_mac(1, data, &mac, &key).unwrap()); assert!(ctx.verify_mac(1, data, &mac, &key).unwrap());
let wrong_mac = vec![0u8; 32]; let wrong_mac = vec![0u8; 32];
assert!(!ctx.verify_mac(1, data, &wrong_mac, &key).unwrap()); assert!(!ctx.verify_mac(1, data, &wrong_mac, &key).unwrap());
} }
@@ -52,19 +52,19 @@ fn test_curve25519_key_exchange_security() {
// Create client and server instances // Create client and server instances
let mut client_kex = Curve25519Kex::new(); let mut client_kex = Curve25519Kex::new();
let mut server_kex = Curve25519Kex::new(); let mut server_kex = Curve25519Kex::new();
// Get public keys first (before computing shared secrets) // Get public keys first (before computing shared secrets)
let client_pub = client_kex.public_key().to_vec(); let client_pub = client_kex.public_key().to_vec();
let server_pub = server_kex.public_key().to_vec(); let server_pub = server_kex.public_key().to_vec();
assert_eq!(client_pub.len(), 32); assert_eq!(client_pub.len(), 32);
assert_eq!(server_pub.len(), 32); assert_eq!(server_pub.len(), 32);
// Compute shared secrets using the SAME instances // Compute shared secrets using the SAME instances
// (this consumes the secret, so can only be done once) // (this consumes the secret, so can only be done once)
let client_secret = client_kex.compute_shared_secret(&server_pub).unwrap(); let client_secret = client_kex.compute_shared_secret(&server_pub).unwrap();
let server_secret = server_kex.compute_shared_secret(&client_pub).unwrap(); let server_secret = server_kex.compute_shared_secret(&client_pub).unwrap();
// Shared secrets should match (Diffie-Hellman property) // Shared secrets should match (Diffie-Hellman property)
assert_eq!(client_secret, server_secret); assert_eq!(client_secret, server_secret);
assert_eq!(client_secret.len(), 32); assert_eq!(client_secret.len(), 32);
@@ -73,12 +73,12 @@ fn test_curve25519_key_exchange_security() {
#[test] #[test]
fn test_ed25519_signature_verification() { fn test_ed25519_signature_verification() {
let host_key = Ed25519HostKey::load_or_generate("test_security_key").unwrap(); let host_key = Ed25519HostKey::load_or_generate("test_security_key").unwrap();
let message = b"Test message for signature"; let message = b"Test message for signature";
let signature = host_key.sign(message).unwrap(); let signature = host_key.sign(message).unwrap();
assert_eq!(signature.len(), 64); assert_eq!(signature.len(), 64);
// Ed25519HostKey has sign() but verify might need external library // Ed25519HostKey has sign() but verify might need external library
// For security test, we verify signature length and structure // For security test, we verify signature length and structure
assert!(!signature.is_empty()); assert!(!signature.is_empty());
@@ -89,9 +89,9 @@ fn test_encryption_key_derivation_uniqueness() {
let key1 = vec![1u8; 16]; let key1 = vec![1u8; 16];
let key2 = vec![2u8; 16]; let key2 = vec![2u8; 16];
let iv = vec![0u8; 16]; let iv = vec![0u8; 16];
let plaintext = b"Same plaintext"; let plaintext = b"Same plaintext";
let mut ctx1 = EncryptionContext::from_session_keys(&SessionKeys { let mut ctx1 = EncryptionContext::from_session_keys(&SessionKeys {
session_id: vec![0u8; 32], session_id: vec![0u8; 32],
encryption_key_ctos: key1.clone(), encryption_key_ctos: key1.clone(),
@@ -101,7 +101,7 @@ fn test_encryption_key_derivation_uniqueness() {
iv_ctos: iv.clone(), iv_ctos: iv.clone(),
iv_stoc: iv.clone(), iv_stoc: iv.clone(),
}); });
let mut ctx2 = EncryptionContext::from_session_keys(&SessionKeys { let mut ctx2 = EncryptionContext::from_session_keys(&SessionKeys {
session_id: vec![0u8; 32], session_id: vec![0u8; 32],
encryption_key_ctos: key2.clone(), encryption_key_ctos: key2.clone(),
@@ -111,9 +111,9 @@ fn test_encryption_key_derivation_uniqueness() {
iv_ctos: iv.clone(), iv_ctos: iv.clone(),
iv_stoc: iv.clone(), iv_stoc: iv.clone(),
}); });
let ciphertext1 = ctx1.encrypt_packet(plaintext, &key1, &iv).unwrap(); let ciphertext1 = ctx1.encrypt_packet(plaintext, &key1, &iv).unwrap();
let ciphertext2 = ctx2.encrypt_packet(plaintext, &key2, &iv).unwrap(); let ciphertext2 = ctx2.encrypt_packet(plaintext, &key2, &iv).unwrap();
assert_ne!(ciphertext1, ciphertext2); assert_ne!(ciphertext1, ciphertext2);
} }
@@ -3,17 +3,17 @@ use std::path::PathBuf;
#[test] #[test]
fn test_path_traversal_prevention() { fn test_path_traversal_prevention() {
let root = PathBuf::from("/tmp/test_root"); let root = PathBuf::from("/tmp/test_root");
// Test 1: Normal path should be within root // Test 1: Normal path should be within root
let safe_path = PathBuf::from("safe/file.txt"); let safe_path = PathBuf::from("safe/file.txt");
let full_path = root.join(&safe_path); let full_path = root.join(&safe_path);
assert!(full_path.starts_with(&root)); assert!(full_path.starts_with(&root));
// Test 2: Path traversal attempt should still resolve within root // Test 2: Path traversal attempt should still resolve within root
// (after normalization, ../../etc/passwd from /tmp/test_root becomes /tmp/etc/passwd or /etc/passwd) // (after normalization, ../../etc/passwd from /tmp/test_root becomes /tmp/etc/passwd or /etc/passwd)
let evil_path = PathBuf::from("../../etc/passwd"); let evil_path = PathBuf::from("../../etc/passwd");
let full_path = root.join(&evil_path); let full_path = root.join(&evil_path);
// The key security check: the resolved path should NOT be /etc/passwd // The key security check: the resolved path should NOT be /etc/passwd
// If Path::join normalizes it to /etc/passwd, that's a path traversal vulnerability // If Path::join normalizes it to /etc/passwd, that's a path traversal vulnerability
// We check that the joined path either: // We check that the joined path either:
@@ -34,7 +34,7 @@ fn test_path_traversal_prevention() {
#[test] #[test]
fn test_absolute_path_prevention() { fn test_absolute_path_prevention() {
let root = PathBuf::from("/tmp/test_root"); let root = PathBuf::from("/tmp/test_root");
let abs_path = PathBuf::from("/etc/passwd"); let abs_path = PathBuf::from("/etc/passwd");
assert!(!abs_path.starts_with(&root)); assert!(!abs_path.starts_with(&root));
} }
@@ -42,10 +42,10 @@ fn test_absolute_path_prevention() {
#[test] #[test]
fn test_directory_escape_prevention() { fn test_directory_escape_prevention() {
let root = PathBuf::from("/tmp/test_root"); let root = PathBuf::from("/tmp/test_root");
let parent_path = PathBuf::from("subdir/../.."); let parent_path = PathBuf::from("subdir/../..");
let full_path = root.join(&parent_path); let full_path = root.join(&parent_path);
// Path should not escape root // Path should not escape root
if full_path.canonicalize().is_ok() { if full_path.canonicalize().is_ok() {
let canonical = full_path.canonicalize().unwrap(); let canonical = full_path.canonicalize().unwrap();
@@ -56,11 +56,11 @@ fn test_directory_escape_prevention() {
#[test] #[test]
fn test_file_write_boundary_check() { fn test_file_write_boundary_check() {
let root = PathBuf::from("/tmp/test_root"); let root = PathBuf::from("/tmp/test_root");
let safe_file = PathBuf::from("safe.txt"); let safe_file = PathBuf::from("safe.txt");
let full_path = root.join(&safe_file); let full_path = root.join(&safe_file);
assert!(full_path.starts_with(&root)); assert!(full_path.starts_with(&root));
let outside_file = PathBuf::from("/tmp/outside.txt"); let outside_file = PathBuf::from("/tmp/outside.txt");
assert!(!outside_file.starts_with(&root)); assert!(!outside_file.starts_with(&root));
} }
@@ -68,8 +68,8 @@ fn test_file_write_boundary_check() {
#[test] #[test]
fn test_hidden_file_access() { fn test_hidden_file_access() {
let root = PathBuf::from("/tmp/test_root"); let root = PathBuf::from("/tmp/test_root");
let hidden_path = PathBuf::from(".hidden"); let hidden_path = PathBuf::from(".hidden");
let full_path = root.join(&hidden_path); let full_path = root.join(&hidden_path);
assert!(full_path.starts_with(&root)); assert!(full_path.starts_with(&root));
} }
+2 -2
View File
@@ -1,9 +1,9 @@
mod auth_security; mod auth_security;
mod channel_security;
mod crypto_security; mod crypto_security;
mod file_access_security; mod file_access_security;
mod channel_security;
pub use auth_security::*; pub use auth_security::*;
pub use channel_security::*;
pub use crypto_security::*; pub use crypto_security::*;
pub use file_access_security::*; pub use file_access_security::*;
pub use channel_security::*;
+98 -92
View File
@@ -1,22 +1,23 @@
use anyhow::Context; use anyhow::Context;
use axum::{ use axum::{
extract::DefaultBodyLimit,
extract::{Path, Query, State}, extract::{Path, Query, State},
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
response::{Html, IntoResponse, Json}, response::{Html, IntoResponse, Json},
routing::{delete, get, patch, post, put}, routing::{delete, get, patch, post, put},
Router, Router,
extract::DefaultBodyLimit,
}; };
use serde::Deserialize; use serde::Deserialize;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::archive::{
ArchiveConfig, ArchiveFormat, ArchiveProcessor, FormatDetector, ProcessorRegistry,
};
use crate::audio; use crate::audio;
use crate::auth::{AuthState, LoginRequest}; use crate::auth::{AuthState, LoginRequest};
use crate::provider::sqlite::SqliteProvider; use crate::provider::sqlite::SqliteProvider;
use crate::render; use crate::render;
use crate::download;
use crate::archive::{self, ArchiveFormat, ArchiveProcessor, FormatDetector, ArchiveConfig, ProcessorRegistry};
use filetree::{self, FileTree}; use filetree::{self, FileTree};
#[derive(Clone)] #[derive(Clone)]
@@ -60,7 +61,7 @@ pub async fn run(port: u16, file: Option<String>) -> anyhow::Result<()> {
db_dir: "data/users".to_string(), db_dir: "data/users".to_string(),
auth: AuthState::with_provider(Box::new( auth: AuthState::with_provider(Box::new(
SqliteProvider::new("data/auth.sqlite") SqliteProvider::new("data/auth.sqlite")
.map_err(|e| anyhow::anyhow!("Failed to init SqliteProvider: {}", e))? .map_err(|e| anyhow::anyhow!("Failed to init SqliteProvider: {}", e))?,
)), )),
auth_db_path: "data/auth.sqlite".to_string(), auth_db_path: "data/auth.sqlite".to_string(),
s3_keys: Arc::new(Mutex::new(load_s3_keys())), s3_keys: Arc::new(Mutex::new(load_s3_keys())),
@@ -578,7 +579,7 @@ async fn search_tree(
ORDER BY sort_order ASC, created_at ASC", ORDER BY sort_order ASC, created_at ASC",
)?; )?;
let nodes: Vec<filetree::node::FileNode> = stmt let _nodes: Vec<filetree::node::FileNode> = stmt
.query_map([&search_pattern], |row| { .query_map([&search_pattern], |row| {
let children_json: String = row.get(6)?; let children_json: String = row.get(6)?;
let children: Vec<String> = let children: Vec<String> =
@@ -607,7 +608,7 @@ async fn search_tree(
.filter_map(|r| r.ok()) .filter_map(|r| r.ok())
.collect(); .collect();
let tree = filetree::FileTree { let tree = filetree::FileTree {
user_id: user_id.clone(), user_id: user_id.clone(),
tree_type: "untitled folder".to_string(), tree_type: "untitled folder".to_string(),
nodes: vec![], nodes: vec![],
@@ -914,69 +915,78 @@ fn extract_and_register_archive(
user_id: &str, user_id: &str,
original_filename: &str, original_filename: &str,
) -> anyhow::Result<(u64, u64, String)> { ) -> anyhow::Result<(u64, u64, String)> {
use std::path::PathBuf; use sha2::{Digest, Sha256};
use sha2::{Sha256, Digest};
// Initialize archive system // Initialize archive system
let config = ArchiveConfig::default(); let config = ArchiveConfig::default();
let mut registry = ProcessorRegistry::new(config); let mut registry = ProcessorRegistry::new(config);
registry.initialize()?; registry.initialize()?;
// Detect format // Detect format
let detector = FormatDetector::new(); let detector = FormatDetector::new();
let format = detector.detect(archive_path)?; let format = detector.detect(archive_path)?;
eprintln!("[archive] Detected format: {} for file: {}", format, archive_path.display()); eprintln!(
"[archive] Detected format: {} for file: {}",
format,
archive_path.display()
);
// Get processor // Get processor
let processor = registry.get_processor_mut(archive_path)?; let processor = registry.get_processor_mut(archive_path)?;
// Create extraction directory // Create extraction directory
let base_name = original_filename let base_name = original_filename
.rsplit_once('.') .rsplit_once('.')
.map(|(name, _)| name) .map(|(name, _)| name)
.unwrap_or(original_filename); .unwrap_or(original_filename);
let extraction_dir = archive_path.parent() let extraction_dir = archive_path
.parent()
.unwrap_or(std::path::Path::new(".")) .unwrap_or(std::path::Path::new("."))
.join(format!("{}_extracted", base_name)); .join(format!("{}_extracted", base_name));
std::fs::create_dir_all(&extraction_dir)?; std::fs::create_dir_all(&extraction_dir)?;
// Open and extract // Open and extract
let metadata = processor.open(archive_path)?; let metadata = processor.open(archive_path)?;
eprintln!("[archive] Archive metadata: {} files, {} bytes", eprintln!(
metadata.total_files, metadata.total_size); "[archive] Archive metadata: {} files, {} bytes",
metadata.total_files, metadata.total_size
);
let result = processor.extract_all(&extraction_dir)?; let result = processor.extract_all(&extraction_dir)?;
eprintln!("[archive] Extracted {} files ({} bytes)", eprintln!(
result.success_files, result.total_bytes); "[archive] Extracted {} files ({} bytes)",
result.success_files, result.total_bytes
);
// Register extracted files to database // Register extracted files to database
let conn = FileTree::init_user_db(user_id)?; let conn = FileTree::init_user_db(user_id)?;
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs() as i64; .as_secs() as i64;
// Get MAC address for UUID generation // Get MAC address for UUID generation
let mac_output = std::process::Command::new("ifconfig") let mac_output = std::process::Command::new("ifconfig")
.arg("en0") .arg("en0")
.output() .output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string()) .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default(); .unwrap_or_default();
let mac = mac_output let mac = mac_output
.lines() .lines()
.find(|l| l.contains("ether")) .find(|l| l.contains("ether"))
.and_then(|l| l.split_whitespace().nth(1)) .and_then(|l| l.split_whitespace().nth(1))
.unwrap_or("00:00:00:00:00:00"); .unwrap_or("00:00:00:00:00:00");
let mut registered_count = 0u64; let mut registered_count = 0u64;
// Recursively scan extracted directory // Recursively scan extracted directory
fn scan_directory( fn scan_directory(
dir: &std::path::Path, dir: &std::path::Path,
@@ -986,11 +996,11 @@ fn extract_and_register_archive(
now: i64, now: i64,
) -> anyhow::Result<u64> { ) -> anyhow::Result<u64> {
let mut count = 0u64; let mut count = 0u64;
for entry in std::fs::read_dir(dir)? { for entry in std::fs::read_dir(dir)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
count += scan_directory(&path, conn, user_id, mac, now)?; count += scan_directory(&path, conn, user_id, mac, now)?;
} else if path.is_file() { } else if path.is_file() {
@@ -998,16 +1008,15 @@ fn extract_and_register_archive(
let file_data = std::fs::read(&path)?; let file_data = std::fs::read(&path)?;
let file_hash = format!("{:x}", Sha256::digest(&file_data)); let file_hash = format!("{:x}", Sha256::digest(&file_data));
let file_size = file_data.len() as i64; let file_size = file_data.len() as i64;
let filename = path.file_name() let filename = path
.file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("unknown") .unwrap_or("unknown")
.to_string(); .to_string();
let file_path_str = path.to_str() let file_path_str = path.to_str().unwrap_or("unknown").to_string();
.unwrap_or("unknown")
.to_string();
// Generate file UUID // Generate file UUID
let mtime = std::fs::metadata(&path) let mtime = std::fs::metadata(&path)
.ok() .ok()
@@ -1015,48 +1024,55 @@ fn extract_and_register_archive(
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_millis() as u64) .map(|d| d.as_millis() as u64)
.unwrap_or(0); .unwrap_or(0);
let input = format!("{}|{}|{}|{}", file_path_str, filename, mac, mtime); let input = format!("{}|{}|{}|{}", file_path_str, filename, mac, mtime);
let hash = Sha256::digest(input.as_bytes()); let hash = Sha256::digest(input.as_bytes());
let hex = format!("{:x}", hash); let hex = format!("{:x}", hash);
let file_uuid = hex[0..32].to_string(); let file_uuid = hex[0..32].to_string();
// Register file (no sha256 in file_registry) // Register file (no sha256 in file_registry)
conn.execute( conn.execute(
"INSERT INTO file_registry (file_uuid, original_name, file_size, file_type, registered_at) "INSERT INTO file_registry (file_uuid, original_name, file_size, file_type, registered_at)
VALUES (?1, ?2, ?3, ?4, ?5)", VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![&file_uuid, &filename, file_size, "", now], rusqlite::params![&file_uuid, &filename, file_size, "", now],
)?; )?;
// Add file location // Add file location
conn.execute( conn.execute(
"INSERT OR IGNORE INTO file_locations (file_uuid, location, added_at) "INSERT OR IGNORE INTO file_locations (file_uuid, location, added_at)
VALUES (?1, ?2, ?3)", VALUES (?1, ?2, ?3)",
rusqlite::params![&file_uuid, &file_path_str, now], rusqlite::params![&file_uuid, &file_path_str, now],
)?; )?;
// Add file node // Add file node
let uuid_str = uuid::Uuid::new_v4().to_string().replace('-', ""); let uuid_str = uuid::Uuid::new_v4().to_string().replace('-', "");
let node_id = format!("node-{}", &uuid_str[0..8]); let node_id = format!("node-{}", &uuid_str[0..8]);
conn.execute( conn.execute(
"INSERT INTO file_nodes (node_id, label, file_uuid, sha256, node_type, file_size, created_at, updated_at) "INSERT INTO file_nodes (node_id, label, file_uuid, sha256, node_type, file_size, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, 'file', ?5, ?6, ?7)", VALUES (?1, ?2, ?3, ?4, 'file', ?5, ?6, ?7)",
rusqlite::params![&node_id, &filename, &file_uuid, &file_hash, file_size, now, now], rusqlite::params![&node_id, &filename, &file_uuid, &file_hash, file_size, now, now],
)?; )?;
count += 1; count += 1;
} }
} }
Ok(count) Ok(count)
} }
registered_count = scan_directory(&extraction_dir, &conn, user_id, mac, now)?; registered_count = scan_directory(&extraction_dir, &conn, user_id, mac, now)?;
eprintln!("[archive] Registered {} extracted files to database", registered_count); eprintln!(
"[archive] Registered {} extracted files to database",
Ok((result.success_files, result.total_bytes, extraction_dir.to_str().unwrap_or("unknown").to_string())) registered_count
);
Ok((
result.success_files,
result.total_bytes,
extraction_dir.to_str().unwrap_or("unknown").to_string(),
))
} }
async fn upload_file( async fn upload_file(
@@ -1147,23 +1163,23 @@ async fn upload_file(
// Auto-extract archive files // Auto-extract archive files
let file_path_buf = std::path::PathBuf::from(&file_path); let file_path_buf = std::path::PathBuf::from(&file_path);
let detector = FormatDetector::new(); let detector = FormatDetector::new();
if let Ok(format) = detector.detect(&file_path_buf) { if let Ok(format) = detector.detect(&file_path_buf) {
if format != ArchiveFormat::Unknown { if format != ArchiveFormat::Unknown {
eprintln!("[upload] Detected archive format: {}, extracting...", format); eprintln!(
"[upload] Detected archive format: {}, extracting...",
format
);
let user_id_clone = user_id.clone(); let user_id_clone = user_id.clone();
let filename_clone = filename.clone(); let filename_clone = filename.clone();
// Extract in blocking thread // Extract in blocking thread
let extraction_result = tokio::task::spawn_blocking(move || { let extraction_result = tokio::task::spawn_blocking(move || {
extract_and_register_archive( extract_and_register_archive(&file_path_buf, &user_id_clone, &filename_clone)
&file_path_buf, })
&user_id_clone, .await;
&filename_clone,
)
}).await;
match extraction_result { match extraction_result {
Ok(Ok((count, bytes, extract_dir))) => { Ok(Ok((count, bytes, extract_dir))) => {
extracted_info = Some((count, bytes, extract_dir)); extracted_info = Some((count, bytes, extract_dir));
@@ -1208,13 +1224,13 @@ async fn upload_file(
let hex = format!("{:x}", hash); let hex = format!("{:x}", hash);
let file_uuid = hex[0..32].to_string(); let file_uuid = hex[0..32].to_string();
// Save to database (user-specific SQLite) // Save to database (user-specific SQLite)
let file_uuid_clone = file_uuid.clone(); let file_uuid_clone = file_uuid.clone();
let file_hash_clone = file_hash.clone(); let file_hash_clone = file_hash.clone();
let filename_clone = filename.clone(); let filename_clone = filename.clone();
let file_path_clone = file_path.clone(); let file_path_clone = file_path.clone();
let user_id_clone = user_id.clone(); let user_id_clone = user_id.clone();
let db_result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { let db_result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let conn = filetree::FileTree::init_user_db(&user_id_clone)?; let conn = filetree::FileTree::init_user_db(&user_id_clone)?;
@@ -1281,7 +1297,7 @@ async fn upload_file(
"sha256": file_hash, "sha256": file_hash,
"size": file_size, "size": file_size,
}); });
if let Some((count, bytes, extract_dir)) = extracted_info { if let Some((count, bytes, extract_dir)) = extracted_info {
response["extracted"] = serde_json::json!({ response["extracted"] = serde_json::json!({
"count": count, "count": count,
@@ -1289,12 +1305,8 @@ async fn upload_file(
"directory": extract_dir, "directory": extract_dir,
}); });
} }
( (StatusCode::CREATED, Json(response)).into_response()
StatusCode::CREATED,
Json(response),
)
.into_response()
} }
async fn upload_unlimited( async fn upload_unlimited(
@@ -1798,7 +1810,7 @@ async fn logout_handler(State(state): State<AppState>, headers: HeaderMap) -> im
let auth_header = headers let auth_header = headers
.get("Authorization") .get("Authorization")
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.and_then(|h| crate::auth::parse_auth_header(h)); .and_then(crate::auth::parse_auth_header);
match auth_header { match auth_header {
Some(token) => { Some(token) => {
@@ -1824,7 +1836,7 @@ async fn verify_handler(State(state): State<AppState>, headers: HeaderMap) -> im
let auth_header = headers let auth_header = headers
.get("Authorization") .get("Authorization")
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.and_then(|h| crate::auth::parse_auth_header(h)); .and_then(crate::auth::parse_auth_header);
match auth_header { match auth_header {
Some(token) => match state.auth.verify_token(&token) { Some(token) => match state.auth.verify_token(&token) {
@@ -1857,7 +1869,7 @@ fn verify_auth(state: &AppState, headers: &HeaderMap) -> Result<String, StatusCo
let auth_header = headers let auth_header = headers
.get("Authorization") .get("Authorization")
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.and_then(|h| crate::auth::parse_auth_header(h)); .and_then(crate::auth::parse_auth_header);
match auth_header { match auth_header {
Some(token) => match state.auth.verify_token(&token) { Some(token) => match state.auth.verify_token(&token) {
@@ -2039,7 +2051,7 @@ async fn edit_config_handler(Query(params): Query<EditConfigQuery>) -> impl Into
match crate::config::MarkBaseConfig::load(config_path) { match crate::config::MarkBaseConfig::load(config_path) {
Ok(mut config) => { Ok(mut config) => {
let old_value = config.get(&params.key).unwrap_or_default(); let old_value = config.get(&params.key).unwrap_or_default();
match config.set(&params.key, &params.value) { match config.set(&params.key, &params.value) {
Ok(_) => match config.validate() { Ok(_) => match config.validate() {
Ok(_) => match config.save(config_path) { Ok(_) => match config.save(config_path) {
@@ -2056,7 +2068,7 @@ async fn edit_config_handler(Query(params): Query<EditConfigQuery>) -> impl Into
) { ) {
log::warn!("Failed to write audit log: {}", e); log::warn!("Failed to write audit log: {}", e);
} }
(StatusCode::OK, Json(serde_json::json!({"ok": true}))).into_response() (StatusCode::OK, Json(serde_json::json!({"ok": true}))).into_response()
} }
Err(e) => ( Err(e) => (
@@ -2133,7 +2145,7 @@ async fn edit_s3_config_handler(Query(params): Query<EditConfigQuery>) -> impl I
match crate::s3_config::S3Config::load_default() { match crate::s3_config::S3Config::load_default() {
Ok(mut config) => { Ok(mut config) => {
let old_value = config.get(&params.key).unwrap_or_default(); let old_value = config.get(&params.key).unwrap_or_default();
match config.set(&params.key, &params.value) { match config.set(&params.key, &params.value) {
Ok(_) => match config.validate() { Ok(_) => match config.validate() {
Ok(_) => match config.save("config/s3.toml") { Ok(_) => match config.save("config/s3.toml") {
@@ -2150,7 +2162,7 @@ async fn edit_s3_config_handler(Query(params): Query<EditConfigQuery>) -> impl I
) { ) {
log::warn!("Failed to write audit log: {}", e); log::warn!("Failed to write audit log: {}", e);
} }
(StatusCode::OK, Json(serde_json::json!({"ok": true}))).into_response() (StatusCode::OK, Json(serde_json::json!({"ok": true}))).into_response()
} }
Err(e) => ( Err(e) => (
@@ -2343,7 +2355,7 @@ async fn audit_handler() -> Json<serde_json::Value> {
// Category View API handlers (Phase 1: 双视图管理) // Category View API handlers (Phase 1: 双视图管理)
async fn get_all_categories_handler() -> impl IntoResponse { async fn get_all_categories_handler() -> impl IntoResponse {
let base_path = std::path::Path::new("/Users/accusys/markbase"); let _base_path = std::path::Path::new("/Users/accusys/markbase");
match crate::category_view::get_all_categories() { match crate::category_view::get_all_categories() {
Ok(response) => (StatusCode::OK, Json(response)).into_response(), Ok(response) => (StatusCode::OK, Json(response)).into_response(),
Err(e) => ( Err(e) => (
@@ -2354,10 +2366,8 @@ async fn get_all_categories_handler() -> impl IntoResponse {
} }
} }
async fn get_category_detail_handler( async fn get_category_detail_handler(Path(category_name): Path<String>) -> impl IntoResponse {
Path(category_name): Path<String>, let _base_path = std::path::Path::new("/Users/accusys/markbase");
) -> impl IntoResponse {
let base_path = std::path::Path::new("/Users/accusys/markbase");
match crate::category_view::get_category_detail(&category_name) { match crate::category_view::get_category_detail(&category_name) {
Ok(response) => (StatusCode::OK, Json(response)).into_response(), Ok(response) => (StatusCode::OK, Json(response)).into_response(),
Err(e) => ( Err(e) => (
@@ -2369,7 +2379,7 @@ async fn get_category_detail_handler(
} }
async fn get_all_series_handler() -> impl IntoResponse { async fn get_all_series_handler() -> impl IntoResponse {
let base_path = std::path::Path::new("/Users/accusys/markbase"); let _base_path = std::path::Path::new("/Users/accusys/markbase");
match crate::category_view::get_all_series() { match crate::category_view::get_all_series() {
Ok(response) => (StatusCode::OK, Json(response)).into_response(), Ok(response) => (StatusCode::OK, Json(response)).into_response(),
Err(e) => ( Err(e) => (
@@ -2380,10 +2390,8 @@ async fn get_all_series_handler() -> impl IntoResponse {
} }
} }
async fn get_series_detail_handler( async fn get_series_detail_handler(Path(series_name): Path<String>) -> impl IntoResponse {
Path(series_name): Path<String>, let _base_path = std::path::Path::new("/Users/accusys/markbase");
) -> impl IntoResponse {
let base_path = std::path::Path::new("/Users/accusys/markbase");
match crate::category_view::get_series_detail(&series_name) { match crate::category_view::get_series_detail(&series_name) {
Ok(response) => (StatusCode::OK, Json(response)).into_response(), Ok(response) => (StatusCode::OK, Json(response)).into_response(),
Err(e) => ( Err(e) => (
@@ -2400,10 +2408,8 @@ struct SearchQuery {
view: String, view: String,
} }
async fn search_files_handler( async fn search_files_handler(Query(query): Query<SearchQuery>) -> impl IntoResponse {
Query(query): Query<SearchQuery>, let _base_path = std::path::Path::new("/Users/accusys/markbase");
) -> impl IntoResponse {
let base_path = std::path::Path::new("/Users/accusys/markbase");
match crate::category_view::search_files(&query.q, &query.view) { match crate::category_view::search_files(&query.q, &query.view) {
Ok(response) => (StatusCode::OK, Json(response)).into_response(), Ok(response) => (StatusCode::OK, Json(response)).into_response(),
Err(e) => ( Err(e) => (
+77 -29
View File
@@ -1,11 +1,11 @@
use crate::ssh_server::packet::{SshPacket, PacketType}; use crate::ssh_server::packet::{PacketType, SshPacket};
use std::io::Write; use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow}; use base64::{engine::general_purpose, Engine as _};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use log::{info, warn, debug}; use log::{debug, info, warn};
use base64::{Engine as _, engine::general_purpose}; use std::io::Write;
use ed25519_dalek::{VerifyingKey, Signature}; use ed25519_dalek::{Signature, VerifyingKey};
use crate::provider::{DataProvider, ProviderError}; use crate::provider::{DataProvider, ProviderError};
@@ -27,7 +27,11 @@ impl AuthHandler {
} }
/// 处理SSH_MSG_USERAUTH_REQUEST(参考OpenSSH auth2.c: userauth_request()) /// 处理SSH_MSG_USERAUTH_REQUEST(参考OpenSSH auth2.c: userauth_request())
pub fn handle_userauth_request(&mut self, packet: &SshPacket, session_id: &[u8]) -> Result<AuthResult> { pub fn handle_userauth_request(
&mut self,
packet: &SshPacket,
session_id: &[u8],
) -> Result<AuthResult> {
info!("Processing SSH_MSG_USERAUTH_REQUEST"); info!("Processing SSH_MSG_USERAUTH_REQUEST");
let mut cursor = std::io::Cursor::new(packet.payload.as_slice()); let mut cursor = std::io::Cursor::new(packet.payload.as_slice());
@@ -41,7 +45,10 @@ impl AuthHandler {
let service = read_ssh_string(&mut cursor)?; let service = read_ssh_string(&mut cursor)?;
let method = read_ssh_string(&mut cursor)?; let method = read_ssh_string(&mut cursor)?;
info!("Auth request: user={}, service={}, method={}", user, service, method); info!(
"Auth request: user={}, service={}, method={}",
user, service, method
);
if service != "ssh-connection" { if service != "ssh-connection" {
warn!("Unsupported service: {}", service); warn!("Unsupported service: {}", service);
@@ -62,18 +69,28 @@ impl AuthHandler {
} }
/// 处理password认证(参考OpenSSH auth-passwd.c) /// 处理password认证(参考OpenSSH auth-passwd.c)
fn handle_password_auth(&mut self, cursor: &mut std::io::Cursor<&[u8]>, user: &str) -> Result<AuthResult> { fn handle_password_auth(
&mut self,
cursor: &mut std::io::Cursor<&[u8]>,
user: &str,
) -> Result<AuthResult> {
info!("Handling password auth for user: {}", user); info!("Handling password auth for user: {}", user);
let change_password = cursor.read_u8()? != 0; let change_password = cursor.read_u8()? != 0;
if change_password { if change_password {
warn!("Password change not supported"); warn!("Password change not supported");
return Ok(AuthResult::Failure("Password change not supported".to_string())); return Ok(AuthResult::Failure(
"Password change not supported".to_string(),
));
} }
let password = read_ssh_string(cursor)?; let password = read_ssh_string(cursor)?;
debug!("Password auth attempt: user={}, password length={}", user, password.len()); debug!(
"Password auth attempt: user={}, password length={}",
user,
password.len()
);
match self.provider.check_password(user, &password) { match self.provider.check_password(user, &password) {
Ok(true) => { Ok(true) => {
@@ -88,9 +105,7 @@ impl AuthHandler {
warn!("User not found: {}", msg); warn!("User not found: {}", msg);
Ok(AuthResult::Failure("password,publickey".to_string())) Ok(AuthResult::Failure("password,publickey".to_string()))
} }
Err(e) => { Err(e) => Err(anyhow!("Password auth error: {}", e)),
Err(anyhow!("Password auth error: {}", e))
}
} }
} }
@@ -145,7 +160,12 @@ impl AuthHandler {
let algorithm = read_ssh_string(cursor)?; let algorithm = read_ssh_string(cursor)?;
let public_key_blob = read_ssh_string_bytes(cursor)?; let public_key_blob = read_ssh_string_bytes(cursor)?;
info!("Publickey auth: algorithm={}, blob_len={}, is_signed={}", algorithm, public_key_blob.len(), is_signed); info!(
"Publickey auth: algorithm={}, blob_len={}, is_signed={}",
algorithm,
public_key_blob.len(),
is_signed
);
if !self.is_key_authorized(user, &algorithm, &public_key_blob)? { if !self.is_key_authorized(user, &algorithm, &public_key_blob)? {
warn!("Public key not authorized for user: {}", user); warn!("Public key not authorized for user: {}", user);
@@ -160,14 +180,26 @@ impl AuthHandler {
let signature_blob = read_ssh_string_bytes(cursor)?; let signature_blob = read_ssh_string_bytes(cursor)?;
self.verify_signature(&algorithm, &public_key_blob, &signature_blob, user, service, session_id)?; self.verify_signature(
&algorithm,
&public_key_blob,
&signature_blob,
user,
service,
session_id,
)?;
info!("Publickey auth successful for user: {}", user); info!("Publickey auth successful for user: {}", user);
Ok(AuthResult::Success) Ok(AuthResult::Success)
} }
/// 检查public key是否在授权列表中(数据库优先,fallback到filesystem) /// 检查public key是否在授权列表中(数据库优先,fallback到filesystem)
fn is_key_authorized(&self, user: &str, algorithm: &str, public_key_blob: &[u8]) -> Result<bool> { fn is_key_authorized(
&self,
user: &str,
algorithm: &str,
public_key_blob: &[u8],
) -> Result<bool> {
// 1. 先检查数据库 // 1. 先检查数据库
match self.provider.get_public_keys(user) { match self.provider.get_public_keys(user) {
Ok(keys) => { Ok(keys) => {
@@ -187,10 +219,12 @@ impl AuthHandler {
Err(_) => match std::fs::read_to_string("data/authorized_keys") { Err(_) => match std::fs::read_to_string("data/authorized_keys") {
Ok(c) => c, Ok(c) => c,
Err(_) => return Ok(false), Err(_) => return Ok(false),
} },
}; };
Ok(content.lines().any(|line| public_key_matches_line(line, algorithm, public_key_blob))) Ok(content
.lines()
.any(|line| public_key_matches_line(line, algorithm, public_key_blob)))
} }
/// 验证Ed25519签名(RFC 4252 §7) /// 验证Ed25519签名(RFC 4252 §7)
@@ -246,7 +280,8 @@ impl AuthHandler {
signed_data.write_all(public_key_blob)?; signed_data.write_all(public_key_blob)?;
// 验证签名 // 验证签名
verifying_key.verify_strict(&signed_data, &signature) verifying_key
.verify_strict(&signed_data, &signature)
.map_err(|e| anyhow!("Ed25519 signature verification failed: {}", e)) .map_err(|e| anyhow!("Ed25519 signature verification failed: {}", e))
} }
} }
@@ -270,10 +305,10 @@ fn parse_ed25519_verifying_key(public_key_blob: &[u8]) -> Result<VerifyingKey> {
if key_bytes.len() != 32 { if key_bytes.len() != 32 {
return Err(anyhow!("Invalid Ed25519 key length: {}", key_bytes.len())); return Err(anyhow!("Invalid Ed25519 key length: {}", key_bytes.len()));
} }
let key_array: [u8; 32] = key_bytes.try_into() let key_array: [u8; 32] = key_bytes
.try_into()
.map_err(|_| anyhow!("Invalid Ed25519 key data"))?; .map_err(|_| anyhow!("Invalid Ed25519 key data"))?;
VerifyingKey::from_bytes(&key_array) VerifyingKey::from_bytes(&key_array).map_err(|e| anyhow!("Invalid Ed25519 key: {}", e))
.map_err(|e| anyhow!("Invalid Ed25519 key: {}", e))
} }
/// 解析Ed25519签名blob(SSH格式 -> Signature) /// 解析Ed25519签名blob(SSH格式 -> Signature)
@@ -285,9 +320,13 @@ fn parse_ed25519_signature(signature_blob: &[u8]) -> Result<Signature> {
} }
let sig_bytes = read_ssh_string_bytes(&mut cursor)?; let sig_bytes = read_ssh_string_bytes(&mut cursor)?;
if sig_bytes.len() != 64 { if sig_bytes.len() != 64 {
return Err(anyhow!("Invalid Ed25519 signature length: {}", sig_bytes.len())); return Err(anyhow!(
"Invalid Ed25519 signature length: {}",
sig_bytes.len()
));
} }
let sig_array: [u8; 64] = sig_bytes.try_into() let sig_array: [u8; 64] = sig_bytes
.try_into()
.map_err(|_| anyhow!("Invalid Ed25519 signature data"))?; .map_err(|_| anyhow!("Invalid Ed25519 signature data"))?;
Ok(Signature::from_bytes(&sig_array)) Ok(Signature::from_bytes(&sig_array))
} }
@@ -305,7 +344,9 @@ fn public_key_matches_line(line: &str, algorithm: &str, public_key_blob: &[u8])
if parts[0] != algorithm { if parts[0] != algorithm {
return false; return false;
} }
base64_decode(parts[1]).map(|decoded| decoded == public_key_blob).unwrap_or(false) base64_decode(parts[1])
.map(|decoded| decoded == public_key_blob)
.unwrap_or(false)
} }
fn read_ssh_string<R: std::io::Read>(reader: &mut R) -> Result<String> { fn read_ssh_string<R: std::io::Read>(reader: &mut R) -> Result<String> {
@@ -323,7 +364,8 @@ fn read_ssh_string_bytes<R: std::io::Read>(reader: &mut R) -> Result<Vec<u8>> {
} }
fn base64_decode(input: &str) -> Result<Vec<u8>> { fn base64_decode(input: &str) -> Result<Vec<u8>> {
general_purpose::STANDARD.decode(input) general_purpose::STANDARD
.decode(input)
.map_err(|e| anyhow!("Base64 decode error: {}", e)) .map_err(|e| anyhow!("Base64 decode error: {}", e))
} }
@@ -335,7 +377,10 @@ mod tests {
#[test] #[test]
fn test_userauth_success_packet() { fn test_userauth_success_packet() {
let packet = AuthHandler::build_userauth_success().unwrap(); let packet = AuthHandler::build_userauth_success().unwrap();
assert_eq!(packet.payload[0], PacketType::SSH_MSG_USERAUTH_SUCCESS as u8); assert_eq!(
packet.payload[0],
PacketType::SSH_MSG_USERAUTH_SUCCESS as u8
);
} }
#[test] #[test]
@@ -343,6 +388,9 @@ mod tests {
let methods = vec!["password".to_string(), "publickey".to_string()]; let methods = vec!["password".to_string(), "publickey".to_string()];
let packet = AuthHandler::build_userauth_failure(&methods, false).unwrap(); let packet = AuthHandler::build_userauth_failure(&methods, false).unwrap();
assert_eq!(packet.payload[0], PacketType::SSH_MSG_USERAUTH_FAILURE as u8); assert_eq!(
packet.payload[0],
PacketType::SSH_MSG_USERAUTH_FAILURE as u8
);
} }
} }
File diff suppressed because it is too large Load Diff
+162 -131
View File
@@ -1,33 +1,33 @@
// SSH加密通道实现(Phase 4) // SSH加密通道实现(Phase 4)
// 参考OpenSSH cipher.c, mac.c // 参考OpenSSH cipher.c, mac.c
use aes::Aes128; // 改为AES-128(协商算法是aes128-ctr) use super::crypto::SessionKeys;
use aes::Aes128; // 改为AES-128(协商算法是aes128-ctr)
use anyhow::{anyhow, Result};
use byteorder::{BigEndian, WriteBytesExt};
use cipher::{KeyIvInit, StreamCipher};
use ctr::Ctr128BE; use ctr::Ctr128BE;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use log::info;
use sha2::Sha256; use sha2::Sha256;
use cipher::{KeyIvInit, StreamCipher};
use std::io::Write; use std::io::Write;
use anyhow::{Result, anyhow};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use log::{info, debug, warn};
use super::crypto::SessionKeys;
type Aes128Ctr = Ctr128BE<Aes128>; // AES-128-CTR(16字节密钥) type Aes128Ctr = Ctr128BE<Aes128>; // AES-128-CTR(16字节密钥)
type HmacSha256 = Hmac<Sha256>; type HmacSha256 = Hmac<Sha256>;
/// SSH加密通道管理器(参考OpenSSH struct sshcipher_ctx) /// SSH加密通道管理器(参考OpenSSH struct sshcipher_ctx)
pub struct EncryptionContext { pub struct EncryptionContext {
pub session_id: Vec<u8>, // session identifier (exchange hash) pub session_id: Vec<u8>, // session identifier (exchange hash)
pub encryption_key_ctos: Vec<u8>, // 客户端→服务器加密密钥 pub encryption_key_ctos: Vec<u8>, // 客户端→服务器加密密钥
pub encryption_key_stoc: Vec<u8>, // 服务器→客户端加密密钥 pub encryption_key_stoc: Vec<u8>, // 服务器→客户端加密密钥
pub mac_key_ctos: Vec<u8>, // 客户端→服务器MAC密钥 pub mac_key_ctos: Vec<u8>, // 客户端→服务器MAC密钥
pub mac_key_stoc: Vec<u8>, // 服务器→客户端MAC密钥 pub mac_key_stoc: Vec<u8>, // 服务器→客户端MAC密钥
pub iv_ctos: Vec<u8>, // 客户端→服务器IV pub iv_ctos: Vec<u8>, // 客户端→服务器IV
pub iv_stoc: Vec<u8>, // 服务器→客户端IV pub iv_stoc: Vec<u8>, // 服务器→客户端IV
pub sequence_number_ctos: u32, // 客户端→服务器序列号 pub sequence_number_ctos: u32, // 客户端→服务器序列号
pub sequence_number_stoc: u32, // 服务器→客户端序列号 pub sequence_number_stoc: u32, // 服务器→客户端序列号
pub cipher_ctos: Option<Aes128Ctr>, // 客户端→服务器cipher实例(持久化) pub cipher_ctos: Option<Aes128Ctr>, // 客户端→服务器cipher实例(持久化)
pub cipher_stoc: Option<Aes128Ctr>, // 服务器→客户端cipher实例(持久化) pub cipher_stoc: Option<Aes128Ctr>, // 服务器→客户端cipher实例(持久化)
} }
impl Default for EncryptionContext { impl Default for EncryptionContext {
@@ -53,27 +53,33 @@ impl EncryptionContext {
/// OpenSSH cipher.c: cipher初始化后状态持久化,counter跨packet递增 /// OpenSSH cipher.c: cipher初始化后状态持久化,counter跨packet递增
pub fn from_session_keys(keys: &SessionKeys) -> Self { pub fn from_session_keys(keys: &SessionKeys) -> Self {
info!("Initializing ciphers with session keys:"); info!("Initializing ciphers with session keys:");
info!(" encryption_key_ctos (16 bytes): {:?}", &keys.encryption_key_ctos[..16]); info!(
" encryption_key_ctos (16 bytes): {:?}",
&keys.encryption_key_ctos[..16]
);
info!(" iv_ctos (16 bytes): {:?}", &keys.iv_ctos[..16]); info!(" iv_ctos (16 bytes): {:?}", &keys.iv_ctos[..16]);
info!(" encryption_key_stoc (16 bytes): {:?}", &keys.encryption_key_stoc[..16]); info!(
" encryption_key_stoc (16 bytes): {:?}",
&keys.encryption_key_stoc[..16]
);
info!(" iv_stoc (16 bytes): {:?}", &keys.iv_stoc[..16]); info!(" iv_stoc (16 bytes): {:?}", &keys.iv_stoc[..16]);
// 初始化客户端→服务器cipher(用于解密client packets) // 初始化客户端→服务器cipher(用于解密client packets)
let key_ctos_array = <[u8; 16]>::try_from(&keys.encryption_key_ctos[..16]) let key_ctos_array = <[u8; 16]>::try_from(&keys.encryption_key_ctos[..16])
.expect("encryption_key_ctos must be 16 bytes"); .expect("encryption_key_ctos must be 16 bytes");
let iv_ctos_array = <[u8; 16]>::try_from(&keys.iv_ctos[..16]) let iv_ctos_array =
.expect("iv_ctos must be 16 bytes"); <[u8; 16]>::try_from(&keys.iv_ctos[..16]).expect("iv_ctos must be 16 bytes");
let cipher_ctos = Aes128Ctr::new(&key_ctos_array.into(), &iv_ctos_array.into()); let cipher_ctos = Aes128Ctr::new(&key_ctos_array.into(), &iv_ctos_array.into());
// 初始化服务器→客户端cipher(用于加密server packets) // 初始化服务器→客户端cipher(用于加密server packets)
let key_stoc_array = <[u8; 16]>::try_from(&keys.encryption_key_stoc[..16]) let key_stoc_array = <[u8; 16]>::try_from(&keys.encryption_key_stoc[..16])
.expect("encryption_key_stoc must be 16 bytes"); .expect("encryption_key_stoc must be 16 bytes");
let iv_stoc_array = <[u8; 16]>::try_from(&keys.iv_stoc[..16]) let iv_stoc_array =
.expect("iv_stoc must be 16 bytes"); <[u8; 16]>::try_from(&keys.iv_stoc[..16]).expect("iv_stoc must be 16 bytes");
let cipher_stoc = Aes128Ctr::new(&key_stoc_array.into(), &iv_stoc_array.into()); let cipher_stoc = Aes128Ctr::new(&key_stoc_array.into(), &iv_stoc_array.into());
info!("Ciphers initialized successfully"); info!("Ciphers initialized successfully");
Self { Self {
session_id: keys.session_id.clone(), session_id: keys.session_id.clone(),
encryption_key_ctos: keys.encryption_key_ctos.clone(), encryption_key_ctos: keys.encryption_key_ctos.clone(),
@@ -84,26 +90,26 @@ impl EncryptionContext {
iv_stoc: keys.iv_stoc.clone(), iv_stoc: keys.iv_stoc.clone(),
sequence_number_ctos: 0, sequence_number_ctos: 0,
sequence_number_stoc: 0, sequence_number_stoc: 0,
cipher_ctos: Some(cipher_ctos), // 持久化cipher实例 cipher_ctos: Some(cipher_ctos), // 持久化cipher实例
cipher_stoc: Some(cipher_stoc), // 持久化cipher实例 cipher_stoc: Some(cipher_stoc), // 持久化cipher实例
} }
} }
/// RFC 4344: Compute AES-CTR IV for a specific packet /// RFC 4344: Compute AES-CTR IV for a specific packet
/// IV = nonce(8 bytes from derived IV) + sequence_number(8 bytes) /// IV = nonce(8 bytes from derived IV) + sequence_number(8 bytes)
fn compute_ctr_iv(nonce: &[u8], sequence_number: u32) -> Vec<u8> { fn compute_ctr_iv(nonce: &[u8], sequence_number: u32) -> Vec<u8> {
let mut iv = Vec::with_capacity(16); let mut iv = Vec::with_capacity(16);
// Nonce: first 8 bytes of derived IV (constant) // Nonce: first 8 bytes of derived IV (constant)
iv.extend_from_slice(&nonce[..8]); iv.extend_from_slice(&nonce[..8]);
// Counter: sequence number as 8-byte big-endian // Counter: sequence number as 8-byte big-endian
iv.extend_from_slice(&sequence_number.to_be_bytes()); iv.extend_from_slice(&sequence_number.to_be_bytes());
iv.extend_from_slice(&[0u8; 4]); // Upper 4 bytes = 0 iv.extend_from_slice(&[0u8; 4]); // Upper 4 bytes = 0
iv iv
} }
/// 加密packet(参考OpenSSH cipher.c: cipher_encrypt()) /// 加密packet(参考OpenSSH cipher.c: cipher_encrypt())
pub fn encrypt_packet( pub fn encrypt_packet(
&mut self, &mut self,
@@ -113,17 +119,17 @@ impl EncryptionContext {
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let key_array = <[u8; 16]>::try_from(encryption_key)?; let key_array = <[u8; 16]>::try_from(encryption_key)?;
let iv_array = <[u8; 16]>::try_from(iv)?; let iv_array = <[u8; 16]>::try_from(iv)?;
let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into()); let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into());
let mut ciphertext = plaintext.to_vec(); let mut ciphertext = plaintext.to_vec();
cipher.apply_keystream(&mut ciphertext); cipher.apply_keystream(&mut ciphertext);
self.sequence_number_stoc += 1; self.sequence_number_stoc += 1;
Ok(ciphertext) Ok(ciphertext)
} }
/// 解密packet(参考OpenSSH cipher.c: cipher_decrypt()) /// 解密packet(参考OpenSSH cipher.c: cipher_decrypt())
pub fn decrypt_packet( pub fn decrypt_packet(
&mut self, &mut self,
@@ -133,17 +139,17 @@ impl EncryptionContext {
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let key_array = <[u8; 16]>::try_from(encryption_key)?; let key_array = <[u8; 16]>::try_from(encryption_key)?;
let iv_array = <[u8; 16]>::try_from(iv)?; let iv_array = <[u8; 16]>::try_from(iv)?;
let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into()); let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into());
let mut plaintext = ciphertext.to_vec(); let mut plaintext = ciphertext.to_vec();
cipher.apply_keystream(&mut plaintext); cipher.apply_keystream(&mut plaintext);
self.sequence_number_ctos += 1; self.sequence_number_ctos += 1;
Ok(plaintext) Ok(plaintext)
} }
/// 计算MAC(参考OpenSSH mac.c: mac_compute()) /// 计算MAC(参考OpenSSH mac.c: mac_compute())
pub fn compute_mac( pub fn compute_mac(
&self, &self,
@@ -152,17 +158,17 @@ impl EncryptionContext {
mac_key: &[u8], mac_key: &[u8],
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
// HMAC-SHA256 MAC计算(参考OpenSSH mac.c) // HMAC-SHA256 MAC计算(参考OpenSSH mac.c)
let mut mac = HmacSha256::new_from_slice(mac_key)?; let mut mac = HmacSha256::new_from_slice(mac_key)?;
// OpenSSH MAC格式:sequence_number + data // OpenSSH MAC格式:sequence_number + data
mac.update(&sequence_number.to_be_bytes()); mac.update(&sequence_number.to_be_bytes());
mac.update(data); mac.update(data);
let result = mac.finalize(); let result = mac.finalize();
Ok(result.into_bytes().to_vec()) Ok(result.into_bytes().to_vec())
} }
/// 验证MAC(参考OpenSSH mac.c: mac_check()) /// 验证MAC(参考OpenSSH mac.c: mac_check())
pub fn verify_mac( pub fn verify_mac(
&self, &self,
@@ -172,14 +178,14 @@ impl EncryptionContext {
mac_key: &[u8], mac_key: &[u8],
) -> Result<bool> { ) -> Result<bool> {
// HMAC验证(参考OpenSSH mac.c) // HMAC验证(参考OpenSSH mac.c)
let computed_mac = self.compute_mac(sequence_number, data, mac_key)?; let computed_mac = self.compute_mac(sequence_number, data, mac_key)?;
// 防止时间攻击(使用常量时间比较) // 防止时间攻击(使用常量时间比较)
if computed_mac.len() != expected_mac.len() { if computed_mac.len() != expected_mac.len() {
return Ok(false); return Ok(false);
} }
// 简化实现:直接比较(实际应使用常量时间比较) // 简化实现:直接比较(实际应使用常量时间比较)
Ok(computed_mac == expected_mac) Ok(computed_mac == expected_mac)
} }
@@ -187,11 +193,11 @@ impl EncryptionContext {
/// SSH加密packet封装(参考OpenSSH packet.c: ssh_packet_write_poll()) /// SSH加密packet封装(参考OpenSSH packet.c: ssh_packet_write_poll())
pub struct EncryptedPacket { pub struct EncryptedPacket {
pub packet_length: u32, // 加密后packet长度 pub packet_length: u32, // 加密后packet长度
pub padding_length: u8, // padding长度(加密后) pub padding_length: u8, // padding长度(加密后)
pub payload: Vec<u8>, // payload(加密后) pub payload: Vec<u8>, // payload(加密后)
pub padding: Vec<u8>, // padding(加密后) pub padding: Vec<u8>, // padding(加密后)
pub mac: Vec<u8>, // MAC(32字节,HMAC-SHA256) pub mac: Vec<u8>, // MAC(32字节,HMAC-SHA256)
} }
impl EncryptedPacket { impl EncryptedPacket {
@@ -204,82 +210,88 @@ impl EncryptedPacket {
) -> Result<Self> { ) -> Result<Self> {
let block_size = 16; let block_size = 16;
let min_padding = 4; let min_padding = 4;
let payload_length = plaintext_payload.len(); let payload_length = plaintext_payload.len();
// RFC 4253: entire plaintext packet (including 4-byte packet_length field) must be multiple of block_size // RFC 4253: entire plaintext packet (including 4-byte packet_length field) must be multiple of block_size
// plaintext_packet = packet_length_field(4) + padding_length(1) + payload + padding // plaintext_packet = packet_length_field(4) + padding_length(1) + payload + padding
// So: (4 + 1 + payload_length + padding_length) % 16 == 0 // So: (4 + 1 + payload_length + padding_length) % 16 == 0
let base_size = 4 + 1 + payload_length; // without padding let base_size = 4 + 1 + payload_length; // without padding
let padding_needed = (block_size - (base_size % block_size)) % block_size; let padding_needed = (block_size - (base_size % block_size)) % block_size;
// Ensure padding >= min_padding (RFC 4253 requirement) // Ensure padding >= min_padding (RFC 4253 requirement)
let padding_length: u8 = if padding_needed < min_padding { let padding_length: u8 = if padding_needed < min_padding {
(padding_needed + block_size) as u8 // Add one more block to meet minimum (padding_needed + block_size) as u8 // Add one more block to meet minimum
} else { } else {
padding_needed as u8 padding_needed as u8
}; };
// packet_length = padding_length(1) + payload + padding // packet_length = padding_length(1) + payload + padding
let packet_length = 1 + payload_length + padding_length as usize; let packet_length = 1 + payload_length + padding_length as usize;
info!("Creating AES-CTR encrypted packet: payload_len={}, padding_len={}, packet_len={}", info!(
payload_length, padding_length, packet_length); "Creating AES-CTR encrypted packet: payload_len={}, padding_len={}, packet_len={}",
payload_length, padding_length, packet_length
);
// 构建plaintext packet(packet_length + padding_length + payload + padding) // 构建plaintext packet(packet_length + padding_length + payload + padding)
let mut plaintext_packet = Vec::new(); let mut plaintext_packet = Vec::new();
plaintext_packet.write_u32::<BigEndian>(packet_length as u32)?; // plaintext packet_length plaintext_packet.write_u32::<BigEndian>(packet_length as u32)?; // plaintext packet_length
plaintext_packet.write_u8(padding_length)?; // plaintext padding_length plaintext_packet.write_u8(padding_length)?; // plaintext padding_length
plaintext_packet.write_all(plaintext_payload)?; // plaintext payload plaintext_packet.write_all(plaintext_payload)?; // plaintext payload
let mut random_padding = vec![0u8; padding_length as usize]; let mut random_padding = vec![0u8; padding_length as usize];
use rand::RngCore; use rand::RngCore;
rand::thread_rng().fill_bytes(&mut random_padding); rand::thread_rng().fill_bytes(&mut random_padding);
plaintext_packet.write_all(&random_padding)?; // plaintext padding plaintext_packet.write_all(&random_padding)?; // plaintext padding
info!("Plaintext packet size: {} bytes", plaintext_packet.len()); info!("Plaintext packet size: {} bytes", plaintext_packet.len());
// MtE模式:先計算MAC over plaintext,再加密 // MtE模式:先計算MAC over plaintext,再加密
let sequence_number = if is_server_to_client { let sequence_number = if is_server_to_client {
encryption_ctx.sequence_number_stoc encryption_ctx.sequence_number_stoc
} else { } else {
encryption_ctx.sequence_number_ctos encryption_ctx.sequence_number_ctos
}; };
let mac_key = if is_server_to_client { let mac_key = if is_server_to_client {
&encryption_ctx.mac_key_stoc &encryption_ctx.mac_key_stoc
} else { } else {
&encryption_ctx.mac_key_ctos &encryption_ctx.mac_key_ctos
}; };
info!("MAC calculation (MtE mode) over plaintext packet:"); info!("MAC calculation (MtE mode) over plaintext packet:");
info!(" sequence_number: {}", sequence_number); info!(" sequence_number: {}", sequence_number);
info!(" mac_key length: {}", mac_key.len()); info!(" mac_key length: {}", mac_key.len());
info!(" plaintext_packet length: {}", plaintext_packet.len()); info!(" plaintext_packet length: {}", plaintext_packet.len());
// MAC計算:HMAC(sequence_number || plaintext_packet) // MAC計算:HMAC(sequence_number || plaintext_packet)
let mac = encryption_ctx.compute_mac(sequence_number, &plaintext_packet, mac_key)?; let mac = encryption_ctx.compute_mac(sequence_number, &plaintext_packet, mac_key)?;
// 然後加密plaintext packet(AES-CTR加密整個packet) // 然後加密plaintext packet(AES-CTR加密整個packet)
let cipher = if is_server_to_client { let cipher = if is_server_to_client {
encryption_ctx.cipher_stoc.as_mut() encryption_ctx
.cipher_stoc
.as_mut()
.ok_or_else(|| anyhow!("cipher_stoc not initialized"))? .ok_or_else(|| anyhow!("cipher_stoc not initialized"))?
} else { } else {
encryption_ctx.cipher_ctos.as_mut() encryption_ctx
.cipher_ctos
.as_mut()
.ok_or_else(|| anyhow!("cipher_ctos not initialized"))? .ok_or_else(|| anyhow!("cipher_ctos not initialized"))?
}; };
let mut encrypted_packet = plaintext_packet; let mut encrypted_packet = plaintext_packet;
cipher.apply_keystream(&mut encrypted_packet); cipher.apply_keystream(&mut encrypted_packet);
// 更新sequence number // 更新sequence number
if is_server_to_client { if is_server_to_client {
encryption_ctx.sequence_number_stoc += 1; encryption_ctx.sequence_number_stoc += 1;
} else { } else {
encryption_ctx.sequence_number_ctos += 1; encryption_ctx.sequence_number_ctos += 1;
} }
Ok(Self { Ok(Self {
packet_length: packet_length as u32, packet_length: packet_length as u32,
padding_length, padding_length,
@@ -288,24 +300,27 @@ impl EncryptedPacket {
mac, mac,
}) })
} }
/// 写入加密packet(参考OpenSSH cipher.c) /// 写入加密packet(参考OpenSSH cipher.c)
/// AES-CTR模式:写入完整加密packet + MAC /// AES-CTR模式:写入完整加密packet + MAC
pub fn write<W: std::io::Write>(&self, stream: &mut W) -> Result<()> { pub fn write<W: std::io::Write>(&self, stream: &mut W) -> Result<()> {
info!("Writing AES-CTR encrypted packet: total_encrypted_len={}, mac_len={}", info!(
self.payload.len(), self.mac.len()); "Writing AES-CTR encrypted packet: total_encrypted_len={}, mac_len={}",
self.payload.len(),
self.mac.len()
);
// AES-CTR: 整个packet已加密(包括packet_length),直接写入 // AES-CTR: 整个packet已加密(包括packet_length),直接写入
stream.write_all(&self.payload)?; stream.write_all(&self.payload)?;
info!("Wrote encrypted packet ({} bytes)", self.payload.len()); info!("Wrote encrypted packet ({} bytes)", self.payload.len());
// 写入MAC // 写入MAC
stream.write_all(&self.mac)?; stream.write_all(&self.mac)?;
info!("Wrote MAC ({} bytes)", self.mac.len()); info!("Wrote MAC ({} bytes)", self.mac.len());
Ok(()) Ok(())
} }
/// 读取加密packet(参考OpenSSH packet.c ssh_packet_read_poll2) /// 读取加密packet(参考OpenSSH packet.c ssh_packet_read_poll2)
/// OpenSSH packet.c: AES-CTR先解密第一个块,再提取packet_length /// OpenSSH packet.c: AES-CTR先解密第一个块,再提取packet_length
/// aadlen = 0 (没有EtM或authenticated encryption), packet_length被加密 /// aadlen = 0 (没有EtM或authenticated encryption), packet_length被加密
@@ -315,32 +330,42 @@ impl EncryptedPacket {
is_client_to_server: bool, is_client_to_server: bool,
) -> Result<Self> { ) -> Result<Self> {
use std::io::Read; use std::io::Read;
info!("Reading AES-CTR encrypted packet (packet_length encrypted)"); info!("Reading AES-CTR encrypted packet (packet_length encrypted)");
// 1. 读取第一个加密块(16字节,包含加密的packet_length) // 1. 读取第一个加密块(16字节,包含加密的packet_length)
let mut first_block_encrypted = [0u8; 16]; let mut first_block_encrypted = [0u8; 16];
stream.read_exact(&mut first_block_encrypted)?; stream.read_exact(&mut first_block_encrypted)?;
info!("Read first encrypted block (16 bytes): {:?}", &first_block_encrypted); info!(
"Read first encrypted block (16 bytes): {:?}",
&first_block_encrypted
);
// 2. 获取持久化cipher实例(counter已递增) // 2. 获取持久化cipher实例(counter已递增)
let cipher = if is_client_to_server { let cipher = if is_client_to_server {
encryption_ctx.cipher_ctos.as_mut() encryption_ctx
.cipher_ctos
.as_mut()
.ok_or_else(|| anyhow!("cipher_ctos not initialized"))? .ok_or_else(|| anyhow!("cipher_ctos not initialized"))?
} else { } else {
encryption_ctx.cipher_stoc.as_mut() encryption_ctx
.cipher_stoc
.as_mut()
.ok_or_else(|| anyhow!("cipher_stoc not initialized"))? .ok_or_else(|| anyhow!("cipher_stoc not initialized"))?
}; };
info!("Using cipher for decryption (is_client_to_server={})", is_client_to_server); info!(
"Using cipher for decryption (is_client_to_server={})",
is_client_to_server
);
// 3. 解密第一个块(counter自动递增) // 3. 解密第一个块(counter自动递增)
let mut first_block_decrypted = first_block_encrypted; let mut first_block_decrypted = first_block_encrypted;
cipher.apply_keystream(&mut first_block_decrypted); cipher.apply_keystream(&mut first_block_decrypted);
info!("Decrypted first block: {:?}", &first_block_decrypted); info!("Decrypted first block: {:?}", &first_block_decrypted);
// 3. 从解密后的数据中提取packet_length(前4字节)和padding_length(第5字节) // 3. 从解密后的数据中提取packet_length(前4字节)和padding_length(第5字节)
let packet_length = u32::from_be_bytes([ let packet_length = u32::from_be_bytes([
first_block_decrypted[0], first_block_decrypted[0],
@@ -349,67 +374,73 @@ impl EncryptedPacket {
first_block_decrypted[3], first_block_decrypted[3],
]); ]);
let padding_length = first_block_decrypted[4]; let padding_length = first_block_decrypted[4];
info!("Decrypted packet_length={}, padding_length={}", packet_length, padding_length); info!(
"Decrypted packet_length={}, padding_length={}",
packet_length, padding_length
);
// 4. 合理性检查 // 4. 合理性检查
if packet_length > 35000 { if packet_length > 35000 {
info!("packet_length raw bytes: {:?}", &first_block_decrypted[..4]); info!("packet_length raw bytes: {:?}", &first_block_decrypted[..4]);
return Err(anyhow!("Invalid packet_length: {}", packet_length)); return Err(anyhow!("Invalid packet_length: {}", packet_length));
} }
// 3. 计算剩余加密数据长度 // 3. 计算剩余加密数据长度
// packet_length = padding_length(1) + payload + padding // packet_length = padding_length(1) + payload + padding
// 总加密数据 = packet_length(4) + packet_length = packet_length + 4 // 总加密数据 = packet_length(4) + packet_length = packet_length + 4
// 已读取16字节,剩余 = packet_length + 4 - 16 // 已读取16字节,剩余 = packet_length + 4 - 16
let total_encrypted_size = packet_length as usize + 4; // packet_length field + content let total_encrypted_size = packet_length as usize + 4; // packet_length field + content
let remaining_encrypted_size = total_encrypted_size - 16; let remaining_encrypted_size = total_encrypted_size - 16;
info!("Total encrypted size: {}, remaining: {}", total_encrypted_size, remaining_encrypted_size); info!(
"Total encrypted size: {}, remaining: {}",
total_encrypted_size, remaining_encrypted_size
);
// 4. 读取剩余加密数据 // 4. 读取剩余加密数据
let mut remaining_encrypted = vec![0u8; remaining_encrypted_size]; let mut remaining_encrypted = vec![0u8; remaining_encrypted_size];
stream.read_exact(&mut remaining_encrypted)?; stream.read_exact(&mut remaining_encrypted)?;
// 5. 继续解密(使用同一个cipher) // 5. 继续解密(使用同一个cipher)
cipher.apply_keystream(&mut remaining_encrypted); cipher.apply_keystream(&mut remaining_encrypted);
info!("Remaining decrypted data: {:?}", &remaining_encrypted); info!("Remaining decrypted data: {:?}", &remaining_encrypted);
// 6. 提取payload和padding // 6. 提取payload和padding
// payload长度 = packet_length - padding_length - 1 // payload长度 = packet_length - padding_length - 1
let payload_length = packet_length as usize - padding_length as usize - 1; let payload_length = packet_length as usize - padding_length as usize - 1;
info!("Calculated payload_length: {}", payload_length); info!("Calculated payload_length: {}", payload_length);
// 从第一块提取payload_part1(5-16字节,11字节) // 从第一块提取payload_part1(5-16字节,11字节)
let payload_part1_len = std::cmp::min(payload_length, 11); let payload_part1_len = std::cmp::min(payload_length, 11);
let payload_part1 = &first_block_decrypted[5..5 + payload_part1_len]; let payload_part1 = &first_block_decrypted[5..5 + payload_part1_len];
// 从剩余数据提取payload_part2 // 从剩余数据提取payload_part2
let payload_part2_len = payload_length - payload_part1_len; let payload_part2_len = payload_length - payload_part1_len;
let payload_part2 = &remaining_encrypted[..payload_part2_len]; let payload_part2 = &remaining_encrypted[..payload_part2_len];
// 合并payload // 合并payload
let mut payload = Vec::new(); let mut payload = Vec::new();
payload.extend_from_slice(payload_part1); payload.extend_from_slice(payload_part1);
payload.extend_from_slice(payload_part2); payload.extend_from_slice(payload_part2);
// 提取padding(从remaining_encrypted的末尾) // 提取padding(从remaining_encrypted的末尾)
let padding = remaining_encrypted[payload_part2_len..].to_vec(); let padding = remaining_encrypted[payload_part2_len..].to_vec();
// 9. 读取MAC // 9. 读取MAC
info!("Reading MAC (32 bytes)..."); info!("Reading MAC (32 bytes)...");
let mut mac = vec![0u8; 32]; let mut mac = vec![0u8; 32];
stream.read_exact(&mut mac)?; stream.read_exact(&mut mac)?;
info!("MAC read successfully"); info!("MAC read successfully");
// 10. 更新sequence number // 10. 更新sequence number
if is_client_to_server { if is_client_to_server {
encryption_ctx.sequence_number_ctos += 1; encryption_ctx.sequence_number_ctos += 1;
} else { } else {
encryption_ctx.sequence_number_stoc += 1; encryption_ctx.sequence_number_stoc += 1;
} }
Ok(Self { Ok(Self {
packet_length, packet_length,
padding_length, padding_length,
@@ -418,7 +449,7 @@ impl EncryptedPacket {
mac, mac,
}) })
} }
/// 获取payload内容 /// 获取payload内容
pub fn payload(&self) -> &[u8] { pub fn payload(&self) -> &[u8] {
&self.payload &self.payload
@@ -428,13 +459,13 @@ impl EncryptedPacket {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_aes256_ctr_encryption() { fn test_aes256_ctr_encryption() {
let key = vec![0u8; 16]; // AES-128 key (16 bytes) let key = vec![0u8; 16]; // AES-128 key (16 bytes)
let iv = vec![0u8; 16]; let iv = vec![0u8; 16];
let plaintext = b"Hello World"; let plaintext = b"Hello World";
let mut ctx = EncryptionContext::from_session_keys(&SessionKeys { let mut ctx = EncryptionContext::from_session_keys(&SessionKeys {
session_id: vec![0u8; 32], session_id: vec![0u8; 32],
encryption_key_ctos: key.clone(), encryption_key_ctos: key.clone(),
@@ -444,18 +475,18 @@ mod tests {
iv_ctos: iv.clone(), iv_ctos: iv.clone(),
iv_stoc: iv.clone(), iv_stoc: iv.clone(),
}); });
let ciphertext = ctx.encrypt_packet(plaintext, &key, &iv).unwrap(); let ciphertext = ctx.encrypt_packet(plaintext, &key, &iv).unwrap();
let decrypted = ctx.decrypt_packet(&ciphertext, &key, &iv).unwrap(); let decrypted = ctx.decrypt_packet(&ciphertext, &key, &iv).unwrap();
assert_eq!(plaintext.to_vec(), decrypted); assert_eq!(plaintext.to_vec(), decrypted);
} }
#[test] #[test]
fn test_hmac_sha256() { fn test_hmac_sha256() {
let key = vec![0u8; 32]; let key = vec![0u8; 32];
let data = b"test data"; let data = b"test data";
let ctx = EncryptionContext::from_session_keys(&SessionKeys { let ctx = EncryptionContext::from_session_keys(&SessionKeys {
session_id: vec![0u8; 32], session_id: vec![0u8; 32],
encryption_key_ctos: vec![0u8; 32], encryption_key_ctos: vec![0u8; 32],
@@ -465,10 +496,10 @@ mod tests {
iv_ctos: vec![0u8; 16], iv_ctos: vec![0u8; 16],
iv_stoc: vec![0u8; 16], iv_stoc: vec![0u8; 16],
}); });
let mac = ctx.compute_mac(1, data, &key).unwrap(); let mac = ctx.compute_mac(1, data, &key).unwrap();
assert_eq!(mac.len(), 32); // HMAC-SHA256 = 32字节 assert_eq!(mac.len(), 32); // HMAC-SHA256 = 32字节
// 验证MAC // 验证MAC
assert!(ctx.verify_mac(1, data, &mac, &key).unwrap()); assert!(ctx.verify_mac(1, data, &mac, &key).unwrap());
} }
+135 -82
View File
@@ -1,16 +1,16 @@
// SSH加密模块(Phase 3:密钥交换) // SSH加密模块(Phase 3:密钥交换)
// 参考OpenSSH curve25519.c, kex.c // 参考OpenSSH curve25519.c, kex.c
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret}; use ed25519_dalek::{Signer, SigningKey};
use ed25519_dalek::{SigningKey, VerifyingKey, Signature, Signer}; use log::info;
use sha2::{Sha256, Digest};
use log::{info, debug};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use sha2::{Digest, Sha256};
use x25519_dalek::{EphemeralSecret, PublicKey};
/// Curve25519密钥交换处理器(参考OpenSSH curve25519.c) /// Curve25519密钥交换处理器(参考OpenSSH curve25519.c)
pub struct Curve25519Kex { pub struct Curve25519Kex {
secret: Option<EphemeralSecret>, // 使用Option包装(一次性使用类型) secret: Option<EphemeralSecret>, // 使用Option包装(一次性使用类型)
public: PublicKey, public: PublicKey,
} }
@@ -21,34 +21,37 @@ impl Curve25519Kex {
// x25519-dalek 2.0标准API:使用random_from_rng // x25519-dalek 2.0标准API:使用random_from_rng
let secret = EphemeralSecret::random_from_rng(OsRng); let secret = EphemeralSecret::random_from_rng(OsRng);
let public = PublicKey::from(&secret); let public = PublicKey::from(&secret);
Self { secret: Some(secret), public } // Some包装 Self {
secret: Some(secret),
public,
} // Some包装
} }
/// 获取公钥(用于SSH_MSG_KEX_ECDH_INIT) /// 获取公钥(用于SSH_MSG_KEX_ECDH_INIT)
pub fn public_key(&self) -> &[u8] { pub fn public_key(&self) -> &[u8] {
self.public.as_bytes() self.public.as_bytes()
} }
/// 计算共享密钥(参考OpenSSH curve25519_shared_secret()) /// 计算共享密钥(参考OpenSSH curve25519_shared_secret())
/// 使用&mut self(消耗模式,符合OpenSSH设计) /// 使用&mut self(消耗模式,符合OpenSSH设计)
pub fn compute_shared_secret(&mut self, client_public: &[u8]) -> Result<[u8; 32]> { pub fn compute_shared_secret(&mut self, client_public: &[u8]) -> Result<[u8; 32]> {
if client_public.len() != 32 { if client_public.len() != 32 {
return Err(anyhow!("Invalid client public key length")); return Err(anyhow!("Invalid client public key length"));
} }
info!("=== X25519 Shared Secret Calculation ==="); info!("=== X25519 Shared Secret Calculation ===");
info!("Client public key input: {:?}", client_public); info!("Client public key input: {:?}", client_public);
info!("Server public key: {:?}", self.public.as_bytes()); info!("Server public key: {:?}", self.public.as_bytes());
// 参考OpenSSH:curve25519共享密钥计算 // 参考OpenSSH:curve25519共享密钥计算
let client_public_key = PublicKey::from(<[u8; 32]>::try_from(client_public)?); let client_public_key = PublicKey::from(<[u8; 32]>::try_from(client_public)?);
// 使用take()取出secret(Rust标准模式) // 使用take()取出secret(Rust标准模式)
if let Some(secret) = self.secret.take() { if let Some(secret) = self.secret.take() {
let shared_secret = secret.diffie_hellman(&client_public_key); let shared_secret = secret.diffie_hellman(&client_public_key);
info!("Computed shared secret: {:?}", shared_secret.as_bytes()); info!("Computed shared secret: {:?}", shared_secret.as_bytes());
Ok(shared_secret.as_bytes().clone()) Ok(*shared_secret.as_bytes())
} else { } else {
Err(anyhow!("Secret already used")) Err(anyhow!("Secret already used"))
} }
@@ -71,47 +74,85 @@ impl SessionKeys {
/// RFC 4253 Section 7.2: Key = HASH(K || H || X || session_id) /// RFC 4253 Section 7.2: Key = HASH(K || H || X || session_id)
pub fn derive( pub fn derive(
shared_secret: &[u8], shared_secret: &[u8],
exchange_hash: &[u8], // H参数(exchange hash) exchange_hash: &[u8], // H参数(exchange hash)
server_public_key: &[u8], _server_public_key: &[u8],
client_public_key: &[u8], _client_public_key: &[u8],
server_host_key: &[u8], _server_host_key: &[u8],
) -> Result<Self> { ) -> Result<Self> {
// RFC 4253: session_id = H (第一次exchange hash) // RFC 4253: session_id = H (第一次exchange hash)
let session_id = exchange_hash.to_vec(); let session_id = exchange_hash.to_vec();
info!("SessionKeys::derive() starting"); info!("SessionKeys::derive() starting");
info!(" shared_secret full (32 bytes): {:?}", shared_secret); info!(" shared_secret full (32 bytes): {:?}", shared_secret);
// RFC 8731 Section 3.1: X25519 output is little-endian // RFC 8731 Section 3.1: X25519 output is little-endian
// OpenSSH sshbuf_put_bignum2_bytes() uses bytes DIRECTLY (no reversal) // OpenSSH sshbuf_put_bignum2_bytes() uses bytes DIRECTLY (no reversal)
// Treats little-endian bytes as big-endian mpint (logical reinterpret) // Treats little-endian bytes as big-endian mpint (logical reinterpret)
info!(" Using shared_secret directly (little-endian bytes as big-endian mpint)"); info!(" Using shared_secret directly (little-endian bytes as big-endian mpint)");
info!(" shared_secret[0] = {} (>=0x80? {})", shared_secret[0], shared_secret[0] >= 0x80); info!(
" shared_secret[0] = {} (>=0x80? {})",
shared_secret[0],
shared_secret[0] >= 0x80
);
info!(" exchange_hash full (32 bytes): {:?}", exchange_hash); info!(" exchange_hash full (32 bytes): {:?}", exchange_hash);
info!(" session_id full (32 bytes): {:?}", session_id); info!(" session_id full (32 bytes): {:?}", session_id);
// RFC 4253密钥派生公式:HASH(K || H || X || session_id) // RFC 4253密钥派生公式:HASH(K || H || X || session_id)
// K is shared_secret encoded as mpint (using little-endian bytes directly) // K is shared_secret encoded as mpint (using little-endian bytes directly)
let shared_secret_mpint = Self::encode_mpint(shared_secret); let shared_secret_mpint = Self::encode_mpint(shared_secret);
info!(" shared_secret_mpint ({} bytes): {:?}", shared_secret_mpint.len(), &shared_secret_mpint[..std::cmp::min(12, shared_secret_mpint.len())]); info!(
" shared_secret_mpint ({} bytes): {:?}",
let encryption_key_ctos = Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'C', &session_id)?; shared_secret_mpint.len(),
let encryption_key_stoc = Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'D', &session_id)?; &shared_secret_mpint[..std::cmp::min(12, shared_secret_mpint.len())]
let mac_key_ctos = Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'E', &session_id)?; );
let mac_key_stoc = Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'F', &session_id)?;
let encryption_key_ctos =
let iv_ctos = Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'A', &session_id)?; Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'C', &session_id)?;
let iv_stoc = Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'B', &session_id)?; let encryption_key_stoc =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'D', &session_id)?;
let mac_key_ctos =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'E', &session_id)?;
let mac_key_stoc =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'F', &session_id)?;
let iv_ctos =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'A', &session_id)?;
let iv_stoc =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'B', &session_id)?;
info!("Derived keys summary:"); info!("Derived keys summary:");
info!(" encryption_key_ctos ({} bytes): {:?}", encryption_key_ctos.len(), &encryption_key_ctos[..std::cmp::min(16, encryption_key_ctos.len())]); info!(
info!(" encryption_key_stoc ({} bytes): {:?}", encryption_key_stoc.len(), &encryption_key_stoc[..std::cmp::min(16, encryption_key_stoc.len())]); " encryption_key_ctos ({} bytes): {:?}",
info!(" iv_ctos ({} bytes): {:?}", iv_ctos.len(), &iv_ctos[..std::cmp::min(16, iv_ctos.len())]); encryption_key_ctos.len(),
info!(" iv_stoc ({} bytes): {:?}", iv_stoc.len(), &iv_stoc[..std::cmp::min(16, iv_stoc.len())]); &encryption_key_ctos[..std::cmp::min(16, encryption_key_ctos.len())]
info!(" mac_key_ctos ({} bytes): {:?}", mac_key_ctos.len(), &mac_key_ctos[..std::cmp::min(16, mac_key_ctos.len())]); );
info!(" mac_key_stoc ({} bytes): {:?}", mac_key_stoc.len(), &mac_key_stoc[..std::cmp::min(16, mac_key_stoc.len())]); info!(
" encryption_key_stoc ({} bytes): {:?}",
encryption_key_stoc.len(),
&encryption_key_stoc[..std::cmp::min(16, encryption_key_stoc.len())]
);
info!(
" iv_ctos ({} bytes): {:?}",
iv_ctos.len(),
&iv_ctos[..std::cmp::min(16, iv_ctos.len())]
);
info!(
" iv_stoc ({} bytes): {:?}",
iv_stoc.len(),
&iv_stoc[..std::cmp::min(16, iv_stoc.len())]
);
info!(
" mac_key_ctos ({} bytes): {:?}",
mac_key_ctos.len(),
&mac_key_ctos[..std::cmp::min(16, mac_key_ctos.len())]
);
info!(
" mac_key_stoc ({} bytes): {:?}",
mac_key_stoc.len(),
&mac_key_stoc[..std::cmp::min(16, mac_key_stoc.len())]
);
Ok(Self { Ok(Self {
session_id, session_id,
encryption_key_ctos, encryption_key_ctos,
@@ -122,65 +163,73 @@ impl SessionKeys {
iv_stoc, iv_stoc,
}) })
} }
/// RFC 4253密钥派生函数 /// RFC 4253密钥派生函数
/// 公式:Key = HASH(K || H || X || session_id) /// 公式:Key = HASH(K || H || X || session_id)
fn derive_key_rfc4253(K_mpint: &[u8], H: &[u8], X: char, session_id: &[u8]) -> Result<Vec<u8>> { fn derive_key_rfc4253(K_mpint: &[u8], H: &[u8], X: char, session_id: &[u8]) -> Result<Vec<u8>> {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
info!("Deriving key for X='{}'", X); info!("Deriving key for X='{}'", X);
info!(" K_mpint ({} bytes): {:?}", K_mpint.len(), &K_mpint[..std::cmp::min(8, K_mpint.len())]); info!(
" K_mpint ({} bytes): {:?}",
K_mpint.len(),
&K_mpint[..std::cmp::min(8, K_mpint.len())]
);
info!(" H ({} bytes): {:?}", H.len(), &H[..8]); info!(" H ({} bytes): {:?}", H.len(), &H[..8]);
info!(" session_id ({} bytes): {:?}", session_id.len(), &session_id[..8]); info!(
" session_id ({} bytes): {:?}",
session_id.len(),
&session_id[..8]
);
// RFC 4253: HASH(K || H || X || session_id) // RFC 4253: HASH(K || H || X || session_id)
hasher.update(K_mpint); // K (shared secret in mpint format) hasher.update(K_mpint); // K (shared secret in mpint format)
hasher.update(H); // H (exchange hash) hasher.update(H); // H (exchange hash)
hasher.update(&[X as u8]); // X (single character) hasher.update([X as u8]); // X (single character)
hasher.update(session_id); // session_id hasher.update(session_id); // session_id
let full_hash = hasher.finalize(); let full_hash = hasher.finalize();
info!(" Derived key (first 8 bytes): {:?}", &full_hash[..8]); info!(" Derived key (first 8 bytes): {:?}", &full_hash[..8]);
// 根據key類型返回不同長度: // 根據key類型返回不同長度:
// AES-128-CTR key/IV: 16 bytes // AES-128-CTR key/IV: 16 bytes
// HMAC-SHA256 key: 32 bytes // HMAC-SHA256 key: 32 bytes
match X { match X {
'A' | 'B' | 'C' | 'D' => Ok(full_hash[..16].to_vec()), // IV or encryption key 'A' | 'B' | 'C' | 'D' => Ok(full_hash[..16].to_vec()), // IV or encryption key
'E' | 'F' => Ok(full_hash.to_vec()), // MAC key (full 32 bytes) 'E' | 'F' => Ok(full_hash.to_vec()), // MAC key (full 32 bytes)
_ => Ok(full_hash[..16].to_vec()), // default _ => Ok(full_hash[..16].to_vec()), // default
} }
} }
/// SSH mpint编码(参考RFC 4253 Section 5) /// SSH mpint编码(参考RFC 4253 Section 5)
/// Curve25519 shared secret特殊处理 /// Curve25519 shared secret特殊处理
fn encode_mpint(bytes: &[u8]) -> Vec<u8> { fn encode_mpint(bytes: &[u8]) -> Vec<u8> {
// RFC 4253: mpint = uint32(length) + data // RFC 4253: mpint = uint32(length) + data
// 去掉前导零,如果最高位>=0x80前面加0 // 去掉前导零,如果最高位>=0x80前面加0
// 去掉前导零字节(但不去掉最后一个字节即使它是0) // 去掉前导零字节(但不去掉最后一个字节即使它是0)
let mut start = 0; let mut start = 0;
while start < bytes.len() - 1 && bytes[start] == 0 { while start < bytes.len() - 1 && bytes[start] == 0 {
start += 1; start += 1;
} }
let data_without_leading_zeros = &bytes[start..]; let data_without_leading_zeros = &bytes[start..];
// 构建mpint数据 // 构建mpint数据
let mut mpint_data = Vec::new(); let mut mpint_data = Vec::new();
// 如果最高位>=0x80,前面加0字节(避免负数) // 如果最高位>=0x80,前面加0字节(避免负数)
if data_without_leading_zeros[0] >= 0x80 { if data_without_leading_zeros[0] >= 0x80 {
mpint_data.push(0); mpint_data.push(0);
} }
mpint_data.extend_from_slice(data_without_leading_zeros); mpint_data.extend_from_slice(data_without_leading_zeros);
// 最终格式:uint32长度 + mpint数据 // 最终格式:uint32长度 + mpint数据
let mut result = Vec::new(); let mut result = Vec::new();
result.extend_from_slice(&(mpint_data.len() as u32).to_be_bytes()); result.extend_from_slice(&(mpint_data.len() as u32).to_be_bytes());
result.extend_from_slice(&mpint_data); result.extend_from_slice(&mpint_data);
result result
} }
} }
@@ -192,45 +241,45 @@ pub struct Ed25519HostKey {
impl Ed25519HostKey { impl Ed25519HostKey {
/// 加载或生成主机密钥(参考OpenSSH hostfile.c) /// 加载或生成主机密钥(参考OpenSSH hostfile.c)
pub fn load_or_generate(key_path: &str) -> Result<Self> { pub fn load_or_generate(_key_path: &str) -> Result<Self> {
// 简化实现:生成临时密钥(实际应从文件加载) // 简化实现:生成临时密钥(实际应从文件加载)
// 参考OpenSSH ssh-keygen // 参考OpenSSH ssh-keygen
let signing_key = SigningKey::generate(&mut OsRng); let signing_key = SigningKey::generate(&mut OsRng);
Ok(Self { signing_key }) Ok(Self { signing_key })
} }
/// 获取公钥(用于SSH_MSG_KEX_ECDH_REPLY) /// 获取公钥(用于SSH_MSG_KEX_ECDH_REPLY)
pub fn public_key_bytes(&self) -> Vec<u8> { pub fn public_key_bytes(&self) -> Vec<u8> {
// SSH Ed25519公钥格式(参考OpenSSH sshkey.c) // SSH Ed25519公钥格式(参考OpenSSH sshkey.c)
let verifying_key = self.signing_key.verifying_key(); let verifying_key = self.signing_key.verifying_key();
// SSH格式:ssh-ed25519 + 公钥bytes // SSH格式:ssh-ed25519 + 公钥bytes
// 简化:仅返回公钥bytes(32字节) // 简化:仅返回公钥bytes(32字节)
verifying_key.as_bytes().to_vec() verifying_key.as_bytes().to_vec()
} }
/// 签名(参考OpenSSH sshkey.c: sshkey_sign()) /// 签名(参考OpenSSH sshkey.c: sshkey_sign())
pub fn sign(&self, data: &[u8]) -> Result<Vec<u8>> { pub fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
// OpenSSH Ed25519签名 // OpenSSH Ed25519签名
let signature = self.signing_key.sign(data); let signature = self.signing_key.sign(data);
// SSH签名格式(参考OpenSSH ssh-sign.c) // SSH签名格式(参考OpenSSH ssh-sign.c)
// 简化:仅返回签名bytes(64字节) // 简化:仅返回签名bytes(64字节)
Ok(signature.to_bytes().to_vec()) Ok(signature.to_bytes().to_vec())
} }
/// 获取完整SSH公钥格式(参考OpenSSH sshkey.c) /// 获取完整SSH公钥格式(参考OpenSSH sshkey.c)
pub fn ssh_public_key(&self) -> String { pub fn ssh_public_key(&self) -> String {
let public_bytes = self.public_key_bytes(); let public_bytes = self.public_key_bytes();
// SSH公钥格式:ssh-ed25519 <base64-encoded-public-key> // SSH公钥格式:ssh-ed25519 <base64-encoded-public-key>
// 参考OpenSSH ssh-keygen -y // 参考OpenSSH ssh-keygen -y
use base64::{Engine as _, engine::general_purpose}; use base64::{engine::general_purpose, Engine as _};
let encoded = general_purpose::STANDARD.encode(&public_bytes); let encoded = general_purpose::STANDARD.encode(&public_bytes);
format!("ssh-ed25519 {}", encoded) format!("ssh-ed25519 {}", encoded)
} }
} }
@@ -238,40 +287,44 @@ impl Ed25519HostKey {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_curve25519_key_generation() { fn test_curve25519_key_generation() {
let kex = Curve25519Kex::new(); let kex = Curve25519Kex::new();
assert_eq!(kex.public_key().len(), 32); assert_eq!(kex.public_key().len(), 32);
} }
#[test] #[test]
fn test_curve25519_shared_secret() { fn test_curve25519_shared_secret() {
let mut client_kex = Curve25519Kex::new(); let mut client_kex = Curve25519Kex::new();
let mut server_kex = Curve25519Kex::new(); let mut server_kex = Curve25519Kex::new();
// 客户端计算共享密钥 // 客户端计算共享密钥
let client_secret = client_kex.compute_shared_secret(server_kex.public_key()).unwrap(); let client_secret = client_kex
.compute_shared_secret(server_kex.public_key())
.unwrap();
// 服务器计算共享密钥 // 服务器计算共享密钥
let server_secret = server_kex.compute_shared_secret(client_kex.public_key()).unwrap(); let server_secret = server_kex
.compute_shared_secret(client_kex.public_key())
.unwrap();
// 应该相同(Curve25519特性) // 应该相同(Curve25519特性)
assert_eq!(client_secret, server_secret); assert_eq!(client_secret, server_secret);
} }
#[test] #[test]
fn test_ed25519_host_key() { fn test_ed25519_host_key() {
let host_key = Ed25519HostKey::load_or_generate("test_key").unwrap(); let host_key = Ed25519HostKey::load_or_generate("test_key").unwrap();
assert_eq!(host_key.public_key_bytes().len(), 32); assert_eq!(host_key.public_key_bytes().len(), 32);
} }
#[test] #[test]
fn test_ed25519_signature() { fn test_ed25519_signature() {
let host_key = Ed25519HostKey::load_or_generate("test_key").unwrap(); let host_key = Ed25519HostKey::load_or_generate("test_key").unwrap();
let data = b"test data"; let data = b"test data";
let signature = host_key.sign(data).unwrap(); let signature = host_key.sign(data).unwrap();
assert_eq!(signature.len(), 64); // Ed25519签名64字节 assert_eq!(signature.len(), 64); // Ed25519签名64字节
} }
} }
+114 -73
View File
@@ -1,13 +1,13 @@
// SSH端口转发数据传输(Phase 13.5) // SSH端口转发数据传输(Phase 13.5)
// 参考OpenSSH channels.c: channel_handle_data() // 参考OpenSSH channels.c: channel_handle_data()
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use log::{info, warn, debug};
use std::net::{TcpStream};
use std::io::{Read, Write};
use std::thread;
use std::sync::{Arc, Mutex};
use byteorder::{BigEndian, WriteBytesExt}; use byteorder::{BigEndian, WriteBytesExt};
use log::{debug, info, warn};
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::{Arc, Mutex};
use std::thread;
/// 数据转发器(Phase 13.5:双向数据传输) /// 数据转发器(Phase 13.5:双向数据传输)
pub struct DataForwarder { pub struct DataForwarder {
@@ -25,29 +25,40 @@ impl DataForwarder {
max_packet_size, max_packet_size,
} }
} }
/// 启动双向数据转发(Phase 13.5:SSH channel ↔ TCP socket) /// 启动双向数据转发(Phase 13.5:SSH channel ↔ TCP socket)
pub fn start_bidirectional_forwarding( pub fn start_bidirectional_forwarding(
&mut self, &mut self,
ssh_stream: TcpStream, // SSH client连接(加密通道) ssh_stream: TcpStream, // SSH client连接(加密通道)
target_stream: TcpStream, // 目标服务连接(TCP socket) target_stream: TcpStream, // 目标服务连接(TCP socket)
) -> Result<()> { ) -> Result<()> {
info!("Starting bidirectional data forwarding for channel {}", self.channel_id); info!(
"Starting bidirectional data forwarding for channel {}",
self.channel_id
);
// Phase 13.5: SSH channel → Target socket(SSH client数据 → 本地服务) // Phase 13.5: SSH channel → Target socket(SSH client数据 → 本地服务)
let ssh_to_target = self.start_ssh_to_target_forwarding(ssh_stream.try_clone()?, target_stream.try_clone()?); let ssh_to_target = self
.start_ssh_to_target_forwarding(ssh_stream.try_clone()?, target_stream.try_clone()?);
// Phase 13.5: Target socket → SSH channel(本地服务数据 → SSH client) // Phase 13.5: Target socket → SSH channel(本地服务数据 → SSH client)
let target_to_ssh = self.start_target_to_ssh_forwarding(target_stream, ssh_stream); let target_to_ssh = self.start_target_to_ssh_forwarding(target_stream, ssh_stream);
// Phase 13.5: 等待两个转发线程完成 // Phase 13.5: 等待两个转发线程完成
ssh_to_target.join().map_err(|e| anyhow!("SSH to target thread error: {:?}", e))?; ssh_to_target
target_to_ssh.join().map_err(|e| anyhow!("Target to SSH thread error: {:?}", e))?; .join()
.map_err(|e| anyhow!("SSH to target thread error: {:?}", e))?;
info!("Bidirectional data forwarding completed for channel {}", self.channel_id); target_to_ssh
.join()
.map_err(|e| anyhow!("Target to SSH thread error: {:?}", e))?;
info!(
"Bidirectional data forwarding completed for channel {}",
self.channel_id
);
Ok(()) Ok(())
} }
/// SSH channel → Target socket转发(Phase 13.5) /// SSH channel → Target socket转发(Phase 13.5)
fn start_ssh_to_target_forwarding( fn start_ssh_to_target_forwarding(
&self, &self,
@@ -57,18 +68,21 @@ impl DataForwarder {
let channel_id = self.channel_id; let channel_id = self.channel_id;
let window_size = self.window_size.clone(); let window_size = self.window_size.clone();
let max_packet_size = self.max_packet_size; let max_packet_size = self.max_packet_size;
thread::spawn(move || { thread::spawn(move || {
info!("SSH to target forwarding thread started for channel {}", channel_id); info!(
"SSH to target forwarding thread started for channel {}",
channel_id
);
let mut buffer = vec![0u8; max_packet_size as usize]; let mut buffer = vec![0u8; max_packet_size as usize];
loop { loop {
// Phase 13.5: 从SSH channel读取数据 // Phase 13.5: 从SSH channel读取数据
let n = match ssh_stream.read(&mut buffer) { let n = match ssh_stream.read(&mut buffer) {
Ok(0) => { Ok(0) => {
info!("SSH channel EOF for channel {}", channel_id); info!("SSH channel EOF for channel {}", channel_id);
break; // EOF break; // EOF
} }
Ok(n) => n, Ok(n) => n,
Err(e) => { Err(e) => {
@@ -76,45 +90,61 @@ impl DataForwarder {
break; break;
} }
}; };
// Phase 13.5: 检查window size // Phase 13.5: 检查window size
{ {
let window = window_size.lock().unwrap(); let window = window_size.lock().unwrap();
if *window < n as u32 { if *window < n as u32 {
warn!("Window size insufficient for channel {}: need {}, have {}", warn!(
channel_id, n, *window); "Window size insufficient for channel {}: need {}, have {}",
channel_id, n, *window
);
// Phase 13.5: 理论上应该等待SSH_MSG_CHANNEL_WINDOW_ADJUST // Phase 13.5: 理论上应该等待SSH_MSG_CHANNEL_WINDOW_ADJUST
// 简化实现:继续发送(可能会违反RFC 4254) // 简化实现:继续发送(可能会违反RFC 4254)
} }
} }
// Phase 13.5: 写入目标socket // Phase 13.5: 写入目标socket
if let Err(e) = target_stream.write_all(&buffer[..n]) { if let Err(e) = target_stream.write_all(&buffer[..n]) {
warn!("Target socket write error for channel {}: {}", channel_id, e); warn!(
"Target socket write error for channel {}: {}",
channel_id, e
);
break; break;
} }
// Phase 13.5: Flush确保数据发送 // Phase 13.5: Flush确保数据发送
if let Err(e) = target_stream.flush() { if let Err(e) = target_stream.flush() {
warn!("Target socket flush error for channel {}: {}", channel_id, e); warn!(
"Target socket flush error for channel {}: {}",
channel_id, e
);
break; break;
} }
// Phase 13.5: 消耗window size // Phase 13.5: 消耗window size
{ {
let mut window = window_size.lock().unwrap(); let mut window = window_size.lock().unwrap();
*window -= n as u32; *window -= n as u32;
debug!("Window size consumed for channel {}: {} bytes, remaining {}", debug!(
channel_id, n, *window); "Window size consumed for channel {}: {} bytes, remaining {}",
channel_id, n, *window
);
} }
info!("Forwarded {} bytes from SSH to target for channel {}", n, channel_id); info!(
"Forwarded {} bytes from SSH to target for channel {}",
n, channel_id
);
} }
info!("SSH to target forwarding thread stopped for channel {}", channel_id); info!(
"SSH to target forwarding thread stopped for channel {}",
channel_id
);
}) })
} }
/// Target socket → SSH channel转发(Phase 13.5) /// Target socket → SSH channel转发(Phase 13.5)
fn start_target_to_ssh_forwarding( fn start_target_to_ssh_forwarding(
&self, &self,
@@ -122,18 +152,21 @@ impl DataForwarder {
mut ssh_stream: TcpStream, mut ssh_stream: TcpStream,
) -> thread::JoinHandle<()> { ) -> thread::JoinHandle<()> {
let channel_id = self.channel_id; let channel_id = self.channel_id;
thread::spawn(move || { thread::spawn(move || {
info!("Target to SSH forwarding thread started for channel {}", channel_id); info!(
"Target to SSH forwarding thread started for channel {}",
let mut buffer = vec![0u8; 8192]; // 8KB buffer channel_id
);
let mut buffer = vec![0u8; 8192]; // 8KB buffer
loop { loop {
// Phase 13.5: 从目标socket读取数据 // Phase 13.5: 从目标socket读取数据
let n = match target_stream.read(&mut buffer) { let n = match target_stream.read(&mut buffer) {
Ok(0) => { Ok(0) => {
info!("Target socket EOF for channel {}", channel_id); info!("Target socket EOF for channel {}", channel_id);
break; // EOF break; // EOF
} }
Ok(n) => n, Ok(n) => n,
Err(e) => { Err(e) => {
@@ -141,43 +174,51 @@ impl DataForwarder {
break; break;
} }
}; };
// Phase 13.5: 构建SSH_MSG_CHANNEL_DATA packet // Phase 13.5: 构建SSH_MSG_CHANNEL_DATA packet
// 注意:实际实现需要通过EncryptedPacket加密 // 注意:实际实现需要通过EncryptedPacket加密
// 这里简化实现,直接写入SSH stream(测试用) // 这里简化实现,直接写入SSH stream(测试用)
// Phase 13.5: 写入SSH channel // Phase 13.5: 写入SSH channel
if let Err(e) = ssh_stream.write_all(&buffer[..n]) { if let Err(e) = ssh_stream.write_all(&buffer[..n]) {
warn!("SSH channel write error for channel {}: {}", channel_id, e); warn!("SSH channel write error for channel {}: {}", channel_id, e);
break; break;
} }
// Phase 13.5: Flush确保数据发送 // Phase 13.5: Flush确保数据发送
if let Err(e) = ssh_stream.flush() { if let Err(e) = ssh_stream.flush() {
warn!("SSH channel flush error for channel {}: {}", channel_id, e); warn!("SSH channel flush error for channel {}: {}", channel_id, e);
break; break;
} }
info!("Forwarded {} bytes from target to SSH for channel {}", n, channel_id); info!(
"Forwarded {} bytes from target to SSH for channel {}",
n, channel_id
);
} }
info!("Target to SSH forwarding thread stopped for channel {}", channel_id); info!(
"Target to SSH forwarding thread stopped for channel {}",
channel_id
);
}) })
} }
/// 获取当前window size(Phase 13.5) /// 获取当前window size(Phase 13.5)
pub fn get_window_size(&self) -> u32 { pub fn get_window_size(&self) -> u32 {
*self.window_size.lock().unwrap() *self.window_size.lock().unwrap()
} }
/// 增加window size(Phase 13.5:SSH_MSG_CHANNEL_WINDOW_ADJUST) /// 增加window size(Phase 13.5:SSH_MSG_CHANNEL_WINDOW_ADJUST)
pub fn adjust_window_size(&self, bytes_to_add: u32) { pub fn adjust_window_size(&self, bytes_to_add: u32) {
let mut window = self.window_size.lock().unwrap(); let mut window = self.window_size.lock().unwrap();
*window += bytes_to_add; *window += bytes_to_add;
info!("Window size adjusted for channel {}: added {} bytes, total {}", info!(
self.channel_id, bytes_to_add, *window); "Window size adjusted for channel {}: added {} bytes, total {}",
self.channel_id, bytes_to_add, *window
);
} }
/// 检查window size是否足够(Phase 13.5) /// 检查window size是否足够(Phase 13.5)
pub fn check_window_available(&self, data_size: u32) -> bool { pub fn check_window_available(&self, data_size: u32) -> bool {
let window = self.window_size.lock().unwrap(); let window = self.window_size.lock().unwrap();
@@ -188,64 +229,64 @@ impl DataForwarder {
/// SSH_MSG_CHANNEL_DATA构建(Phase 13.5) /// SSH_MSG_CHANNEL_DATA构建(Phase 13.5)
pub fn build_channel_data_packet(channel_id: u32, data: &[u8]) -> Result<Vec<u8>> { pub fn build_channel_data_packet(channel_id: u32, data: &[u8]) -> Result<Vec<u8>> {
let mut packet = Vec::new(); let mut packet = Vec::new();
// Packet type: SSH_MSG_CHANNEL_DATA (type 94) // Packet type: SSH_MSG_CHANNEL_DATA (type 94)
packet.write_u8(94)?; packet.write_u8(94)?;
// Recipient channel ID // Recipient channel ID
packet.write_u32::<BigEndian>(channel_id)?; packet.write_u32::<BigEndian>(channel_id)?;
// Data length (SSH string) // Data length (SSH string)
packet.write_u32::<BigEndian>(data.len() as u32)?; packet.write_u32::<BigEndian>(data.len() as u32)?;
// Data content // Data content
packet.write_all(data)?; packet.write_all(data)?;
Ok(packet) Ok(packet)
} }
/// SSH_MSG_CHANNEL_WINDOW_ADJUST构建(Phase 13.5) /// SSH_MSG_CHANNEL_WINDOW_ADJUST构建(Phase 13.5)
pub fn build_window_adjust_packet(channel_id: u32, bytes_to_add: u32) -> Result<Vec<u8>> { pub fn build_window_adjust_packet(channel_id: u32, bytes_to_add: u32) -> Result<Vec<u8>> {
let mut packet = Vec::new(); let mut packet = Vec::new();
// Packet type: SSH_MSG_CHANNEL_WINDOW_ADJUST (type 93) // Packet type: SSH_MSG_CHANNEL_WINDOW_ADJUST (type 93)
packet.write_u8(93)?; packet.write_u8(93)?;
// Recipient channel ID // Recipient channel ID
packet.write_u32::<BigEndian>(channel_id)?; packet.write_u32::<BigEndian>(channel_id)?;
// Bytes to add // Bytes to add
packet.write_u32::<BigEndian>(bytes_to_add)?; packet.write_u32::<BigEndian>(bytes_to_add)?;
Ok(packet) Ok(packet)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_data_forwarder_creation() { fn test_data_forwarder_creation() {
let forwarder = DataForwarder::new(1, 2097152, 32768); let forwarder = DataForwarder::new(1, 2097152, 32768);
assert_eq!(forwarder.channel_id, 1); assert_eq!(forwarder.channel_id, 1);
assert_eq!(forwarder.get_window_size(), 2097152); assert_eq!(forwarder.get_window_size(), 2097152);
} }
#[test] #[test]
fn test_window_size_adjustment() { fn test_window_size_adjustment() {
let forwarder = DataForwarder::new(1, 2097152, 32768); let forwarder = DataForwarder::new(1, 2097152, 32768);
// 消耗window size // 消耗window size
forwarder.adjust_window_size(1000); forwarder.adjust_window_size(1000);
assert_eq!(forwarder.get_window_size(), 2097152 + 1000); assert_eq!(forwarder.get_window_size(), 2097152 + 1000);
} }
#[test] #[test]
fn test_build_channel_data_packet() { fn test_build_channel_data_packet() {
let data = b"Hello, SSH!"; let data = b"Hello, SSH!";
let packet = build_channel_data_packet(1, data).unwrap(); let packet = build_channel_data_packet(1, data).unwrap();
assert_eq!(packet[0], 94); // SSH_MSG_CHANNEL_DATA assert_eq!(packet[0], 94); // SSH_MSG_CHANNEL_DATA
// 验证packet结构 // 验证packet结构
} }
} }
+96 -77
View File
@@ -1,42 +1,42 @@
// SSH密钥交换算法协商实现(Phase 2) // SSH密钥交换算法协商实现(Phase 2)
// 参考OpenSSH kex.c: kex_send_kexinit(), kex_choose_conf() // 参考OpenSSH kex.c: kex_send_kexinit(), kex_choose_conf()
use crate::ssh_server::packet::{SshPacket, PacketType}; use crate::ssh_server::packet::{PacketType, SshPacket};
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use log::{info, debug}; use log::{debug, info};
use std::io::{Read, Write}; use std::io::{Read, Write};
/// SSH算法类型(参考OpenSSH PROTOCOL定义) /// SSH算法类型(参考OpenSSH PROTOCOL定义)
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum AlgorithmType { pub enum AlgorithmType {
KEX_ALGS = 0, // 密钥交换算法 KEX_ALGS = 0, // 密钥交换算法
SERVER_HOST_KEY_ALGS = 1, // 服务器主机密钥算法 SERVER_HOST_KEY_ALGS = 1, // 服务器主机密钥算法
ENC_ALGS_CTOS = 2, // 客户端到服务器加密算法 ENC_ALGS_CTOS = 2, // 客户端到服务器加密算法
ENC_ALGS_STOC = 3, // 服务器到客户端加密算法 ENC_ALGS_STOC = 3, // 服务器到客户端加密算法
MAC_ALGS_CTOS = 4, // 客户端到服务器MAC算法 MAC_ALGS_CTOS = 4, // 客户端到服务器MAC算法
MAC_ALGS_STOC = 5, // 服务器到客户端MAC算法 MAC_ALGS_STOC = 5, // 服务器到客户端MAC算法
COMP_ALGS_CTOS = 6, // 客户端到服务器压缩算法 COMP_ALGS_CTOS = 6, // 客户端到服务器压缩算法
COMP_ALGS_STOC = 7, // 服务器到客户端压缩算法 COMP_ALGS_STOC = 7, // 服务器到客户端压缩算法
LANGS_CTOS = 8, // 客户端到服务器语言 LANGS_CTOS = 8, // 客户端到服务器语言
LANGS_STOC = 9, // 服务器到客户端语言 LANGS_STOC = 9, // 服务器到客户端语言
} }
/// SSH算法提议(参考OpenSSH kex.h: struct kex) /// SSH算法提议(参考OpenSSH kex.h: struct kex)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct KexProposal { pub struct KexProposal {
pub kex_algorithms: String, // 密钥交换算法列表 pub kex_algorithms: String, // 密钥交换算法列表
pub server_host_key_algorithms: String, // 主机密钥算法列表 pub server_host_key_algorithms: String, // 主机密钥算法列表
pub encryption_algorithms_ctos: String, // 加密算法(客户端→服务器) pub encryption_algorithms_ctos: String, // 加密算法(客户端→服务器)
pub encryption_algorithms_stoc: String, // 加密算法(服务器→客户端) pub encryption_algorithms_stoc: String, // 加密算法(服务器→客户端)
pub mac_algorithms_ctos: String, // MAC算法(客户端→服务器) pub mac_algorithms_ctos: String, // MAC算法(客户端→服务器)
pub mac_algorithms_stoc: String, // MAC算法(服务器→客户端) pub mac_algorithms_stoc: String, // MAC算法(服务器→客户端)
pub compression_algorithms_ctos: String, // 压缩算法(客户端→服务器) pub compression_algorithms_ctos: String, // 压缩算法(客户端→服务器)
pub compression_algorithms_stoc: String, // 压缩算法(服务器→客户端) pub compression_algorithms_stoc: String, // 压缩算法(服务器→客户端)
pub languages_ctos: String, // 语言(客户端→服务器) pub languages_ctos: String, // 语言(客户端→服务器)
pub languages_stoc: String, // 语言(服务器→客户端) pub languages_stoc: String, // 语言(服务器→客户端)
pub first_kex_packet_follows: bool, // 是否立即发送第一个KEX packet pub first_kex_packet_follows: bool, // 是否立即发送第一个KEX packet
pub reserved: u32, // 保留字段(0) pub reserved: u32, // 保留字段(0)
} }
impl KexProposal { impl KexProposal {
@@ -46,31 +46,31 @@ impl KexProposal {
Self { Self {
// 密钥交换算法:优先Curve25519(推荐) + strict KEX extension // 密钥交换算法:优先Curve25519(推荐) + strict KEX extension
kex_algorithms: "curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group14-sha256,ext-info-s,kex-strict-s-v00@openssh.com".to_string(), kex_algorithms: "curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group14-sha256,ext-info-s,kex-strict-s-v00@openssh.com".to_string(),
// 主机密钥算法:优先Ed25519 // 主机密钥算法:优先Ed25519
server_host_key_algorithms: "ssh-ed25519,rsa-sha2-256,rsa-sha2-512".to_string(), server_host_key_algorithms: "ssh-ed25519,rsa-sha2-256,rsa-sha2-512".to_string(),
// 加密算法:AES-256-CTR(推荐) // 加密算法:AES-256-CTR(推荐)
encryption_algorithms_ctos: "aes256-ctr,aes128-ctr".to_string(), encryption_algorithms_ctos: "aes256-ctr,aes128-ctr".to_string(),
encryption_algorithms_stoc: "aes256-ctr,aes128-ctr".to_string(), encryption_algorithms_stoc: "aes256-ctr,aes128-ctr".to_string(),
// MAC算法:HMAC-SHA256 // MAC算法:HMAC-SHA256
mac_algorithms_ctos: "hmac-sha2-256,hmac-sha2-512".to_string(), mac_algorithms_ctos: "hmac-sha2-256,hmac-sha2-512".to_string(),
mac_algorithms_stoc: "hmac-sha2-256,hmac-sha2-512".to_string(), mac_algorithms_stoc: "hmac-sha2-256,hmac-sha2-512".to_string(),
// 压缩算法:none优先 // 压缩算法:none优先
compression_algorithms_ctos: "none,zlib".to_string(), compression_algorithms_ctos: "none,zlib".to_string(),
compression_algorithms_stoc: "none,zlib".to_string(), compression_algorithms_stoc: "none,zlib".to_string(),
// 语言:空 // 语言:空
languages_ctos: "".to_string(), languages_ctos: "".to_string(),
languages_stoc: "".to_string(), languages_stoc: "".to_string(),
first_kex_packet_follows: false, first_kex_packet_follows: false,
reserved: 0, reserved: 0,
} }
} }
/// 创建客户端默认提议(用于测试) /// 创建客户端默认提议(用于测试)
pub fn client_default() -> Self { pub fn client_default() -> Self {
Self { Self {
@@ -88,20 +88,20 @@ impl KexProposal {
reserved: 0, reserved: 0,
} }
} }
/// 序列化到SSH_MSG_KEXINIT packet(参考OpenSSH kex_send_kexinit()) /// 序列化到SSH_MSG_KEXINIT packet(参考OpenSSH kex_send_kexinit())
pub fn to_kexinit_packet(&self) -> Result<SshPacket> { pub fn to_kexinit_packet(&self) -> Result<SshPacket> {
let mut payload = Vec::new(); let mut payload = Vec::new();
// Packet type // Packet type
payload.write_u8(PacketType::SSH_MSG_KEXINIT as u8)?; payload.write_u8(PacketType::SSH_MSG_KEXINIT as u8)?;
// Cookie(16字节随机数,OpenSSH要求) // Cookie(16字节随机数,OpenSSH要求)
let mut cookie = [0u8; 16]; let mut cookie = [0u8; 16];
use rand::Rng; use rand::Rng;
rand::thread_rng().fill(&mut cookie); rand::thread_rng().fill(&mut cookie);
payload.write_all(&cookie)?; payload.write_all(&cookie)?;
// 10个算法列表(SSH string格式:length + data) // 10个算法列表(SSH string格式:length + data)
write_ssh_string(&mut payload, &self.kex_algorithms)?; write_ssh_string(&mut payload, &self.kex_algorithms)?;
write_ssh_string(&mut payload, &self.server_host_key_algorithms)?; write_ssh_string(&mut payload, &self.server_host_key_algorithms)?;
@@ -113,29 +113,29 @@ impl KexProposal {
write_ssh_string(&mut payload, &self.compression_algorithms_stoc)?; write_ssh_string(&mut payload, &self.compression_algorithms_stoc)?;
write_ssh_string(&mut payload, &self.languages_ctos)?; write_ssh_string(&mut payload, &self.languages_ctos)?;
write_ssh_string(&mut payload, &self.languages_stoc)?; write_ssh_string(&mut payload, &self.languages_stoc)?;
// first_kex_packet_follows(boolean) // first_kex_packet_follows(boolean)
payload.write_u8(if self.first_kex_packet_follows { 1 } else { 0 })?; payload.write_u8(if self.first_kex_packet_follows { 1 } else { 0 })?;
// reserved(u32) // reserved(u32)
payload.write_u32::<BigEndian>(self.reserved)?; payload.write_u32::<BigEndian>(self.reserved)?;
Ok(SshPacket::new(payload)) Ok(SshPacket::new(payload))
} }
/// 从SSH_MSG_KEXINIT packet解析(参考OpenSSH kex_input_kexinit()) /// 从SSH_MSG_KEXINIT packet解析(参考OpenSSH kex_input_kexinit())
pub fn from_kexinit_packet(packet: &SshPacket) -> Result<Self> { pub fn from_kexinit_packet(packet: &SshPacket) -> Result<Self> {
let mut cursor = std::io::Cursor::new(packet.payload.as_slice()); // 使用as_slice()(Rust标准) let mut cursor = std::io::Cursor::new(packet.payload.as_slice()); // 使用as_slice()(Rust标准)
// Packet type // Packet type
let packet_type = cursor.read_u8()?; let packet_type = cursor.read_u8()?;
if packet_type != PacketType::SSH_MSG_KEXINIT as u8 { if packet_type != PacketType::SSH_MSG_KEXINIT as u8 {
return Err(anyhow!("Invalid packet type for KEXINIT")); return Err(anyhow!("Invalid packet type for KEXINIT"));
} }
// Cookie(16字节,忽略) // Cookie(16字节,忽略)
cursor.read_exact(&mut [0u8; 16])?; cursor.read_exact(&mut [0u8; 16])?;
// 10个算法列表 // 10个算法列表
let kex_algorithms = read_ssh_string(&mut cursor)?; let kex_algorithms = read_ssh_string(&mut cursor)?;
let server_host_key_algorithms = read_ssh_string(&mut cursor)?; let server_host_key_algorithms = read_ssh_string(&mut cursor)?;
@@ -147,13 +147,13 @@ impl KexProposal {
let compression_algorithms_stoc = read_ssh_string(&mut cursor)?; let compression_algorithms_stoc = read_ssh_string(&mut cursor)?;
let languages_ctos = read_ssh_string(&mut cursor)?; let languages_ctos = read_ssh_string(&mut cursor)?;
let languages_stoc = read_ssh_string(&mut cursor)?; let languages_stoc = read_ssh_string(&mut cursor)?;
// first_kex_packet_follows // first_kex_packet_follows
let first_kex_packet_follows = cursor.read_u8()? != 0; let first_kex_packet_follows = cursor.read_u8()? != 0;
// reserved // reserved
let reserved = cursor.read_u32::<BigEndian>()?; let reserved = cursor.read_u32::<BigEndian>()?;
Ok(Self { Ok(Self {
kex_algorithms, kex_algorithms,
server_host_key_algorithms, server_host_key_algorithms,
@@ -174,14 +174,14 @@ impl KexProposal {
/// SSH算法协商结果(参考OpenSSH struct kex) /// SSH算法协商结果(参考OpenSSH struct kex)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct KexResult { pub struct KexResult {
pub kex_algorithm: String, // 选定的密钥交换算法 pub kex_algorithm: String, // 选定的密钥交换算法
pub host_key_algorithm: String, // 选定的主机密钥算法 pub host_key_algorithm: String, // 选定的主机密钥算法
pub encryption_ctos: String, // 选定的加密算法(客户端→服务器) pub encryption_ctos: String, // 选定的加密算法(客户端→服务器)
pub encryption_stoc: String, // 选定的加密算法(服务器→客户端) pub encryption_stoc: String, // 选定的加密算法(服务器→客户端)
pub mac_ctos: String, // 选定的MAC算法(客户端→服务器) pub mac_ctos: String, // 选定的MAC算法(客户端→服务器)
pub mac_stoc: String, // 选定的MAC算法(服务器→客户端) pub mac_stoc: String, // 选定的MAC算法(服务器→客户端)
pub compression_ctos: String, // 选定的压缩算法(客户端→服务器) pub compression_ctos: String, // 选定的压缩算法(客户端→服务器)
pub compression_stoc: String, // 选定的压缩算法(服务器→客户端) pub compression_stoc: String, // 选定的压缩算法(服务器→客户端)
} }
/// 算法匹配逻辑(参考OpenSSH kex_choose_conf()) /// 算法匹配逻辑(参考OpenSSH kex_choose_conf())
@@ -189,28 +189,43 @@ impl KexResult {
/// 从服务器和客户端提议中选择算法(参考OpenSSH kex_choose_conf()) /// 从服务器和客户端提议中选择算法(参考OpenSSH kex_choose_conf())
pub fn choose_algorithms(server: &KexProposal, client: &KexProposal) -> Result<Self> { pub fn choose_algorithms(server: &KexProposal, client: &KexProposal) -> Result<Self> {
info!("Starting algorithm negotiation"); info!("Starting algorithm negotiation");
// 算法匹配:优先客户端偏好(OpenSSH逻辑) // 算法匹配:优先客户端偏好(OpenSSH逻辑)
// 参考OpenSSH:客户端列出的算法顺序为偏好顺序 // 参考OpenSSH:客户端列出的算法顺序为偏好顺序
// 密钥交换算法匹配 // 密钥交换算法匹配
let kex_algorithm = match_algorithm(&client.kex_algorithms, &server.kex_algorithms)?; let kex_algorithm = match_algorithm(&client.kex_algorithms, &server.kex_algorithms)?;
// 主机密钥算法匹配 // 主机密钥算法匹配
let host_key_algorithm = match_algorithm(&client.server_host_key_algorithms, &server.server_host_key_algorithms)?; let host_key_algorithm = match_algorithm(
&client.server_host_key_algorithms,
&server.server_host_key_algorithms,
)?;
// 加密算法匹配 // 加密算法匹配
let encryption_ctos = match_algorithm(&client.encryption_algorithms_ctos, &server.encryption_algorithms_ctos)?; let encryption_ctos = match_algorithm(
let encryption_stoc = match_algorithm(&client.encryption_algorithms_stoc, &server.encryption_algorithms_stoc)?; &client.encryption_algorithms_ctos,
&server.encryption_algorithms_ctos,
)?;
let encryption_stoc = match_algorithm(
&client.encryption_algorithms_stoc,
&server.encryption_algorithms_stoc,
)?;
// MAC算法匹配 // MAC算法匹配
let mac_ctos = match_algorithm(&client.mac_algorithms_ctos, &server.mac_algorithms_ctos)?; let mac_ctos = match_algorithm(&client.mac_algorithms_ctos, &server.mac_algorithms_ctos)?;
let mac_stoc = match_algorithm(&client.mac_algorithms_stoc, &server.mac_algorithms_stoc)?; let mac_stoc = match_algorithm(&client.mac_algorithms_stoc, &server.mac_algorithms_stoc)?;
// 压缩算法匹配 // 压缩算法匹配
let compression_ctos = match_algorithm(&client.compression_algorithms_ctos, &server.compression_algorithms_ctos)?; let compression_ctos = match_algorithm(
let compression_stoc = match_algorithm(&client.compression_algorithms_stoc, &server.compression_algorithms_stoc)?; &client.compression_algorithms_ctos,
&server.compression_algorithms_ctos,
)?;
let compression_stoc = match_algorithm(
&client.compression_algorithms_stoc,
&server.compression_algorithms_stoc,
)?;
info!("Algorithm negotiation completed:"); info!("Algorithm negotiation completed:");
debug!(" KEX: {}", kex_algorithm); debug!(" KEX: {}", kex_algorithm);
debug!(" Host key: {}", host_key_algorithm); debug!(" Host key: {}", host_key_algorithm);
@@ -218,7 +233,7 @@ impl KexResult {
debug!(" Encryption (S->C): {}", encryption_stoc); debug!(" Encryption (S->C): {}", encryption_stoc);
debug!(" MAC (C->S): {}", mac_ctos); debug!(" MAC (C->S): {}", mac_ctos);
debug!(" MAC (S->C): {}", mac_stoc); debug!(" MAC (S->C): {}", mac_stoc);
Ok(Self { Ok(Self {
kex_algorithm, kex_algorithm,
host_key_algorithm, host_key_algorithm,
@@ -237,15 +252,19 @@ fn match_algorithm(client_algs: &str, server_algs: &str) -> Result<String> {
// 算法列表格式:name1,name2,name3,... // 算法列表格式:name1,name2,name3,...
let client_list: Vec<&str> = client_algs.split(',').collect(); let client_list: Vec<&str> = client_algs.split(',').collect();
let server_list: Vec<&str> = server_algs.split(',').collect(); let server_list: Vec<&str> = server_algs.split(',').collect();
// OpenSSH逻辑:按客户端偏好顺序匹配 // OpenSSH逻辑:按客户端偏好顺序匹配
for client_alg in &client_list { for client_alg in &client_list {
if server_list.contains(client_alg) { if server_list.contains(client_alg) {
return Ok(client_alg.to_string()); return Ok(client_alg.to_string());
} }
} }
Err(anyhow!("No matching algorithm found: client={}, server={}", client_algs, server_algs)) Err(anyhow!(
"No matching algorithm found: client={}, server={}",
client_algs,
server_algs
))
} }
/// SSH string写入辅助函数(length + data) /// SSH string写入辅助函数(length + data)
@@ -266,36 +285,36 @@ fn read_ssh_string<R: Read>(reader: &mut R) -> Result<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_kex_proposal_creation() { fn test_kex_proposal_creation() {
let proposal = KexProposal::server_default(); let proposal = KexProposal::server_default();
assert!(proposal.kex_algorithms.contains("curve25519-sha256")); assert!(proposal.kex_algorithms.contains("curve25519-sha256"));
} }
#[test] #[test]
fn test_kex_proposal_serialization() { fn test_kex_proposal_serialization() {
let proposal = KexProposal::server_default(); let proposal = KexProposal::server_default();
let packet = proposal.to_kexinit_packet().unwrap(); let packet = proposal.to_kexinit_packet().unwrap();
assert!(packet.payload.len() > 0); assert!(packet.payload.len() > 0);
} }
#[test] #[test]
fn test_algorithm_matching() { fn test_algorithm_matching() {
let client = "curve25519-sha256,aes256-ctr"; let client = "curve25519-sha256,aes256-ctr";
let server = "aes256-ctr,diffie-hellman-group14-sha256"; let server = "aes256-ctr,diffie-hellman-group14-sha256";
let matched = match_algorithm(client, server).unwrap(); let matched = match_algorithm(client, server).unwrap();
assert_eq!(matched, "aes256-ctr"); // 按客户端顺序匹配 assert_eq!(matched, "aes256-ctr"); // 按客户端顺序匹配
} }
#[test] #[test]
fn test_kex_negotiation() { fn test_kex_negotiation() {
let server = KexProposal::server_default(); let server = KexProposal::server_default();
let client = KexProposal::client_default(); let client = KexProposal::client_default();
let result = KexResult::choose_algorithms(&server, &client).unwrap(); let result = KexResult::choose_algorithms(&server, &client).unwrap();
assert_eq!(result.kex_algorithm, "curve25519-sha256"); // 优先Curve25519 assert_eq!(result.kex_algorithm, "curve25519-sha256"); // 优先Curve25519
assert_eq!(result.encryption_ctos, "aes256-ctr"); // AES-256-CTR assert_eq!(result.encryption_ctos, "aes256-ctr"); // AES-256-CTR
} }
} }
+78 -67
View File
@@ -1,14 +1,13 @@
// SSH密钥交换完整流程(Phase 3剩余) // SSH密钥交换完整流程(Phase 3剩余)
// 参考OpenSSH kex.c: complete implementation // 参考OpenSSH kex.c: complete implementation
use crate::ssh_server::packet::{SshPacket, PacketType}; use crate::ssh_server::crypto::SessionKeys;
use crate::ssh_server::kex::{KexProposal, KexResult}; use crate::ssh_server::kex::{KexProposal, KexResult};
use crate::ssh_server::crypto::{SessionKeys};
use crate::ssh_server::kex_exchange::KexExchangeHandler; use crate::ssh_server::kex_exchange::KexExchangeHandler;
use anyhow::{Result, anyhow}; use crate::ssh_server::packet::{PacketType, SshPacket};
use sha2::{Sha256, Digest}; use anyhow::{anyhow, Result};
use byteorder::{BigEndian, WriteBytesExt}; use log::info;
use log::{info, debug}; use sha2::{Digest, Sha256};
/// SSH密钥交换完整状态管理(参考OpenSSH struct kex) /// SSH密钥交换完整状态管理(参考OpenSSH struct kex)
pub struct KexState { pub struct KexState {
@@ -30,7 +29,7 @@ impl KexState {
kex_result: KexResult, kex_result: KexResult,
) -> Result<Self> { ) -> Result<Self> {
let exchange_handler = KexExchangeHandler::new(kex_result)?; let exchange_handler = KexExchangeHandler::new(kex_result)?;
Ok(Self { Ok(Self {
client_version, client_version,
server_version, server_version,
@@ -42,18 +41,18 @@ impl KexState {
newkeys_sent: false, newkeys_sent: false,
}) })
} }
/// 保存KEXINIT payloads(用于Exchange Hash计算) /// 保存KEXINIT payloads(用于Exchange Hash计算)
/// ///
/// 分析OpenSSH源码后的结论: /// 分析OpenSSH源码后的结论:
/// - kex->peer存储的是:incoming_packet剩余内容(payload fields + padding) /// - kex->peer存储的是:incoming_packet剩余内容(payload fields + padding)
/// - kex->my存储的是:prop2buf()结果(payload fields,不包括padding) /// - kex->my存储的是:prop2buf()结果(payload fields,不包括padding)
/// ///
/// **但exchange hash必须使用相同的I_C/I_S!** /// **但exchange hash必须使用相同的I_C/I_S!**
/// ///
/// 疑问:OpenSSH如何确保client和server使用相同的padding? /// 疑问:OpenSSH如何确保client和server使用相同的padding?
/// 可能答案:OpenSSH在计算exchange hash时,不包括padding? /// 可能答案:OpenSSH在计算exchange hash时,不包括padding?
/// ///
/// 暂时保持不包括padding(因为签名验证之前成功) /// 暂时保持不包括padding(因为签名验证之前成功)
pub fn save_kexinit_payloads( pub fn save_kexinit_payloads(
&mut self, &mut self,
@@ -63,12 +62,18 @@ impl KexState {
// Only save payload (without padding) for now // Only save payload (without padding) for now
self.client_kexinit_payload = client_kexinit.payload.clone(); self.client_kexinit_payload = client_kexinit.payload.clone();
self.server_kexinit_payload = server_kexinit.payload.clone(); self.server_kexinit_payload = server_kexinit.payload.clone();
info!("Saved KEXINIT payloads (payload only, no padding)"); info!("Saved KEXINIT payloads (payload only, no padding)");
info!(" client payload: {} bytes", self.client_kexinit_payload.len()); info!(
info!(" server payload: {} bytes", self.server_kexinit_payload.len()); " client payload: {} bytes",
self.client_kexinit_payload.len()
);
info!(
" server payload: {} bytes",
self.server_kexinit_payload.len()
);
} }
/// 计算Exchange Hash(参考OpenSSH kex.c: kex_hash()) /// 计算Exchange Hash(参考OpenSSH kex.c: kex_hash())
/// H = SHA256(V_C || V_S || I_C || I_S || K_S || K_C || K_S || shared_secret) /// H = SHA256(V_C || V_S || I_C || I_S || K_S || K_C || K_S || shared_secret)
pub fn compute_exchange_hash( pub fn compute_exchange_hash(
@@ -80,74 +85,74 @@ impl KexState {
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
// 参考OpenSSH kex.c: kex_hash() // 参考OpenSSH kex.c: kex_hash()
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
// V_C: 客户端版本字符串(SSH string格式) // V_C: 客户端版本字符串(SSH string格式)
write_ssh_string_to_hash(&mut hasher, &self.client_version)?; write_ssh_string_to_hash(&mut hasher, &self.client_version)?;
// V_S: 服务器版本字符串(SSH string格式) // V_S: 服务器版本字符串(SSH string格式)
write_ssh_string_to_hash(&mut hasher, &self.server_version)?; write_ssh_string_to_hash(&mut hasher, &self.server_version)?;
// OpenSSH kexgex.c: "kexinit messages: fake header: len+SSH2_MSG_KEXINIT" // OpenSSH kexgex.c: "kexinit messages: fake header: len+SSH2_MSG_KEXINIT"
// Remove SSH_MSG_KEXINIT type byte from payloads and prepend it in exchange hash // Remove SSH_MSG_KEXINIT type byte from payloads and prepend it in exchange hash
let client_kexinit_without_type = &self.client_kexinit_payload[1..]; let client_kexinit_without_type = &self.client_kexinit_payload[1..];
let server_kexinit_without_type = &self.server_kexinit_payload[1..]; let server_kexinit_without_type = &self.server_kexinit_payload[1..];
hasher.update(&((client_kexinit_without_type.len() + 1) as u32).to_be_bytes()); hasher.update(((client_kexinit_without_type.len() + 1) as u32).to_be_bytes());
hasher.update(&[20]); // SSH_MSG_KEXINIT type byte hasher.update([20]); // SSH_MSG_KEXINIT type byte
hasher.update(client_kexinit_without_type); hasher.update(client_kexinit_without_type);
hasher.update(&((server_kexinit_without_type.len() + 1) as u32).to_be_bytes()); hasher.update(((server_kexinit_without_type.len() + 1) as u32).to_be_bytes());
hasher.update(&[20]); // SSH_MSG_KEXINIT type byte hasher.update([20]); // SSH_MSG_KEXINIT type byte
hasher.update(server_kexinit_without_type); hasher.update(server_kexinit_without_type);
// K_S: 服务器主机密钥blob(SSH string格式) // K_S: 服务器主机密钥blob(SSH string格式)
hasher.update(server_host_key_blob); hasher.update(server_host_key_blob);
// K_C: 客户端Curve25519公钥(SSH string格式) // K_C: 客户端Curve25519公钥(SSH string格式)
write_ssh_bytes_to_hash(&mut hasher, client_public_key)?; write_ssh_bytes_to_hash(&mut hasher, client_public_key)?;
// K_S: 服务器Curve25519公钥(SSH string格式) // K_S: 服务器Curve25519公钥(SSH string格式)
write_ssh_bytes_to_hash(&mut hasher, server_public_key)?; write_ssh_bytes_to_hash(&mut hasher, server_public_key)?;
// K: 共享密钥(SSH mpint格式) // K: 共享密钥(SSH mpint格式)
// OpenSSH要求:去掉前导零 // OpenSSH要求:去掉前导零
write_ssh_mpint_to_hash(&mut hasher, shared_secret)?; write_ssh_mpint_to_hash(&mut hasher, shared_secret)?;
Ok(hasher.finalize().to_vec()) Ok(hasher.finalize().to_vec())
} }
/// 处理SSH_MSG_NEWKEYS(参考OpenSSH kex.c: kex_input_newkeys()) /// 处理SSH_MSG_NEWKEYS(参考OpenSSH kex.c: kex_input_newkeys())
pub fn handle_newkeys(&mut self, packet: &SshPacket) -> Result<()> { pub fn handle_newkeys(&mut self, packet: &SshPacket) -> Result<()> {
info!("Processing SSH_MSG_NEWKEYS"); info!("Processing SSH_MSG_NEWKEYS");
// 验证packet类型 // 验证packet类型
if packet.payload.len() < 1 { if packet.payload.is_empty() {
return Err(anyhow!("Invalid NEWKEYS packet")); return Err(anyhow!("Invalid NEWKEYS packet"));
} }
let packet_type = packet.payload[0]; let packet_type = packet.payload[0];
if packet_type != PacketType::SSH_MSG_NEWKEYS as u8 { if packet_type != PacketType::SSH_MSG_NEWKEYS as u8 {
return Err(anyhow!("Invalid packet type for NEWKEYS")); return Err(anyhow!("Invalid packet type for NEWKEYS"));
} }
// 标记NEWKEYS接收完成(参考OpenSSH) // 标记NEWKEYS接收完成(参考OpenSSH)
self.newkeys_received = true; self.newkeys_received = true;
info!("SSH_MSG_NEWKEYS received, encryption channel ready"); info!("SSH_MSG_NEWKEYS received, encryption channel ready");
Ok(()) Ok(())
} }
/// 发送SSH_MSG_NEWKEYS(参考OpenSSH kex.c: kex_send_newkeys()) /// 发送SSH_MSG_NEWKEYS(参考OpenSSH kex.c: kex_send_newkeys())
pub fn send_newkeys() -> Result<SshPacket> { pub fn send_newkeys() -> Result<SshPacket> {
info!("Sending SSH_MSG_NEWKEYS"); info!("Sending SSH_MSG_NEWKEYS");
let payload = vec![PacketType::SSH_MSG_NEWKEYS as u8]; let payload = vec![PacketType::SSH_MSG_NEWKEYS as u8];
Ok(SshPacket::new(payload)) Ok(SshPacket::new(payload))
} }
/// 检查NEWKEYS完成状态(加密通道建立) /// 检查NEWKEYS完成状态(加密通道建立)
pub fn is_encryption_ready(&self) -> bool { pub fn is_encryption_ready(&self) -> bool {
self.newkeys_received && self.newkeys_sent self.newkeys_received && self.newkeys_sent
@@ -156,14 +161,14 @@ impl KexState {
/// SSH string写入到hash(辅助函数) /// SSH string写入到hash(辅助函数)
fn write_ssh_string_to_hash(hasher: &mut Sha256, s: &str) -> Result<()> { fn write_ssh_string_to_hash(hasher: &mut Sha256, s: &str) -> Result<()> {
hasher.update(&(s.len() as u32).to_be_bytes()); hasher.update((s.len() as u32).to_be_bytes());
hasher.update(s.as_bytes()); hasher.update(s.as_bytes());
Ok(()) Ok(())
} }
/// SSH bytes写入到hash(辅助函数) /// SSH bytes写入到hash(辅助函数)
fn write_ssh_bytes_to_hash(hasher: &mut Sha256, bytes: &[u8]) -> Result<()> { fn write_ssh_bytes_to_hash(hasher: &mut Sha256, bytes: &[u8]) -> Result<()> {
hasher.update(&(bytes.len() as u32).to_be_bytes()); hasher.update((bytes.len() as u32).to_be_bytes());
hasher.update(bytes); hasher.update(bytes);
Ok(()) Ok(())
} }
@@ -171,7 +176,7 @@ fn write_ssh_bytes_to_hash(hasher: &mut Sha256, bytes: &[u8]) -> Result<()> {
/// SSH mpint写入到hash(参考OpenSSH sshbuf_put_mpint()) /// SSH mpint写入到hash(参考OpenSSH sshbuf_put_mpint())
fn write_ssh_mpint_to_hash(hasher: &mut Sha256, bytes: &[u8]) -> Result<()> { fn write_ssh_mpint_to_hash(hasher: &mut Sha256, bytes: &[u8]) -> Result<()> {
// OpenSSH要求:去掉前导零(如果最高位为1) // OpenSSH要求:去掉前导零(如果最高位为1)
let mpint_bytes = if bytes.len() > 0 && bytes[0] >= 0x80 { let mpint_bytes = if !bytes.is_empty() && bytes[0] >= 0x80 {
// 需要添加前导零(避免负数) // 需要添加前导零(避免负数)
let mut mpint = vec![0u8]; let mut mpint = vec![0u8];
mpint.extend_from_slice(bytes); mpint.extend_from_slice(bytes);
@@ -179,61 +184,67 @@ fn write_ssh_mpint_to_hash(hasher: &mut Sha256, bytes: &[u8]) -> Result<()> {
} else { } else {
bytes.to_vec() bytes.to_vec()
}; };
hasher.update(&(mpint_bytes.len() as u32).to_be_bytes()); hasher.update((mpint_bytes.len() as u32).to_be_bytes());
hasher.update(&mpint_bytes); hasher.update(&mpint_bytes);
Ok(()) Ok(())
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_exchange_hash_computation() { fn test_exchange_hash_computation() {
let kex_result = KexResult::choose_algorithms( let kex_result = KexResult::choose_algorithms(
&KexProposal::server_default(), &KexProposal::server_default(),
&KexProposal::client_default(), &KexProposal::client_default(),
).unwrap(); )
.unwrap();
let mut state = KexState::new( let mut state = KexState::new(
"SSH-2.0-OpenSSH_10.2".to_string(), "SSH-2.0-OpenSSH_10.2".to_string(),
"SSH-2.0-MarkBaseSSH_1.0".to_string(), "SSH-2.0-MarkBaseSSH_1.0".to_string(),
kex_result, kex_result,
).unwrap(); )
.unwrap();
// Set minimal KEXINIT payloads (need at least 1 byte for packet type) // Set minimal KEXINIT payloads (need at least 1 byte for packet type)
state.client_kexinit_payload = vec![20u8]; // SSH_MSG_KEXINIT type byte state.client_kexinit_payload = vec![20u8]; // SSH_MSG_KEXINIT type byte
state.server_kexinit_payload = vec![20u8]; // SSH_MSG_KEXINIT type byte state.server_kexinit_payload = vec![20u8]; // SSH_MSG_KEXINIT type byte
let shared_secret = vec![0u8; 32]; let shared_secret = vec![0u8; 32];
let host_key = vec![0u8; 32]; let host_key = vec![0u8; 32];
let client_pub = vec![0u8; 32]; let client_pub = vec![0u8; 32];
let server_pub = vec![0u8; 32]; let server_pub = vec![0u8; 32];
let hash = state.compute_exchange_hash(&shared_secret, &host_key, &client_pub, &server_pub).unwrap(); let hash = state
.compute_exchange_hash(&shared_secret, &host_key, &client_pub, &server_pub)
assert_eq!(hash.len(), 32); // SHA256输出32字节 .unwrap();
assert_eq!(hash.len(), 32); // SHA256输出32字节
} }
#[test] #[test]
fn test_newkeys_handling() { fn test_newkeys_handling() {
let kex_result = KexResult::choose_algorithms( let kex_result = KexResult::choose_algorithms(
&KexProposal::server_default(), &KexProposal::server_default(),
&KexProposal::client_default(), &KexProposal::client_default(),
).unwrap(); )
.unwrap();
let mut state = KexState::new( let mut state = KexState::new(
"SSH-2.0-OpenSSH_10.2".to_string(), "SSH-2.0-OpenSSH_10.2".to_string(),
"SSH-2.0-MarkBaseSSH_1.0".to_string(), "SSH-2.0-MarkBaseSSH_1.0".to_string(),
kex_result, kex_result,
).unwrap(); )
.unwrap();
let newkeys_packet = SshPacket::new(vec![PacketType::SSH_MSG_NEWKEYS as u8]); let newkeys_packet = SshPacket::new(vec![PacketType::SSH_MSG_NEWKEYS as u8]);
state.handle_newkeys(&newkeys_packet).unwrap(); state.handle_newkeys(&newkeys_packet).unwrap();
assert!(state.newkeys_received); assert!(state.newkeys_received);
} }
} }
+177 -112
View File
@@ -1,14 +1,14 @@
// SSH密钥交换流程实现(Phase 3) // SSH密钥交换流程实现(Phase 3)
// 参考OpenSSH kex.c: kex_input_kex_init(), kex_send_kex_reply() // 参考OpenSSH kex.c: kex_input_kex_init(), kex_send_kex_reply()
use crate::ssh_server::packet::{SshPacket, PacketType}; use crate::ssh_server::crypto::{Curve25519Kex, Ed25519HostKey, SessionKeys};
use crate::ssh_server::kex::{KexResult}; use crate::ssh_server::kex::KexResult;
use crate::ssh_server::crypto::{Curve25519Kex, SessionKeys, Ed25519HostKey}; use crate::ssh_server::packet::{PacketType, SshPacket};
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use log::{info, debug}; use log::info;
use sha2::Digest;
use std::io::{Read, Write}; use std::io::{Read, Write};
use sha2::{Sha256, Digest};
/// SSH密钥交换流程处理器(参考OpenSSH kex.c) /// SSH密钥交换流程处理器(参考OpenSSH kex.c)
pub struct KexExchangeHandler { pub struct KexExchangeHandler {
@@ -18,7 +18,7 @@ pub struct KexExchangeHandler {
shared_secret: Option<Vec<u8>>, shared_secret: Option<Vec<u8>>,
client_public_key: Option<Vec<u8>>, client_public_key: Option<Vec<u8>>,
server_public_key: Option<Vec<u8>>, server_public_key: Option<Vec<u8>>,
exchange_hash: Option<Vec<u8>>, // 保存exchange hash(H参数) exchange_hash: Option<Vec<u8>>, // 保存exchange hash(H参数)
client_version: Option<String>, client_version: Option<String>,
server_version: Option<String>, server_version: Option<String>,
client_kexinit_payload: Option<Vec<u8>>, client_kexinit_payload: Option<Vec<u8>>,
@@ -30,7 +30,7 @@ impl KexExchangeHandler {
pub fn new(kex_result: KexResult) -> Result<Self> { pub fn new(kex_result: KexResult) -> Result<Self> {
// 加载或生成服务器主机密钥 // 加载或生成服务器主机密钥
let host_key = Ed25519HostKey::load_or_generate("config/ssh_host_ed25519_key")?; let host_key = Ed25519HostKey::load_or_generate("config/ssh_host_ed25519_key")?;
Ok(Self { Ok(Self {
kex_algorithm: kex_result.kex_algorithm, kex_algorithm: kex_result.kex_algorithm,
server_kex: None, server_kex: None,
@@ -45,10 +45,10 @@ impl KexExchangeHandler {
server_kexinit_payload: None, server_kexinit_payload: None,
}) })
} }
/// 处理SSH_MSG_KEXDH_INIT(Curve25519密钥交换)(参考OpenSSH kex.c: kex_input_kex_init()) /// 处理SSH_MSG_KEXDH_INIT(Curve25519密钥交换)(参考OpenSSH kex.c: kex_input_kex_init())
pub fn handle_kexdh_init( pub fn handle_kexdh_init(
&mut self, &mut self,
packet: &SshPacket, packet: &SshPacket,
client_version: &str, client_version: &str,
server_version: &str, server_version: &str,
@@ -56,41 +56,44 @@ impl KexExchangeHandler {
server_kexinit_payload: &[u8], server_kexinit_payload: &[u8],
) -> Result<SshPacket> { ) -> Result<SshPacket> {
info!("Processing SSH_MSG_KEXDH_INIT (Curve25519)"); info!("Processing SSH_MSG_KEXDH_INIT (Curve25519)");
let mut cursor = std::io::Cursor::new(packet.payload.as_slice()); let mut cursor = std::io::Cursor::new(packet.payload.as_slice());
let packet_type = cursor.read_u8()?; let packet_type = cursor.read_u8()?;
if packet_type != PacketType::SSH_MSG_KEXDH_INIT as u8 { if packet_type != PacketType::SSH_MSG_KEXDH_INIT as u8 {
return Err(anyhow!("Invalid packet type for KEXDH_INIT")); return Err(anyhow!("Invalid packet type for KEXDH_INIT"));
} }
let key_length = cursor.read_u32::<BigEndian>()?; let key_length = cursor.read_u32::<BigEndian>()?;
if key_length != 32 { if key_length != 32 {
return Err(anyhow!("Invalid Curve25519 public key length: {}", key_length)); return Err(anyhow!(
"Invalid Curve25519 public key length: {}",
key_length
));
} }
let mut client_public_key = vec![0u8; 32]; let mut client_public_key = vec![0u8; 32];
cursor.read_exact(&mut client_public_key)?; cursor.read_exact(&mut client_public_key)?;
self.server_kex = Some(Curve25519Kex::new()); self.server_kex = Some(Curve25519Kex::new());
let server_kex = self.server_kex.as_mut().unwrap(); let server_kex = self.server_kex.as_mut().unwrap();
let shared_secret = server_kex.compute_shared_secret(&client_public_key)?; let shared_secret = server_kex.compute_shared_secret(&client_public_key)?;
let server_public_key = server_kex.public_key().to_vec(); let server_public_key = server_kex.public_key().to_vec();
// Save for later session key computation // Save for later session key computation
self.shared_secret = Some(shared_secret.to_vec()); self.shared_secret = Some(shared_secret.to_vec());
self.client_public_key = Some(client_public_key.clone()); self.client_public_key = Some(client_public_key.clone());
self.server_public_key = Some(server_public_key.clone()); self.server_public_key = Some(server_public_key.clone());
// Save client_version, server_version, kexinit payloads for exchange hash // Save client_version, server_version, kexinit payloads for exchange hash
self.client_version = Some(client_version.to_string()); self.client_version = Some(client_version.to_string());
self.server_version = Some(server_version.to_string()); self.server_version = Some(server_version.to_string());
self.client_kexinit_payload = Some(client_kexinit_payload.to_vec()); self.client_kexinit_payload = Some(client_kexinit_payload.to_vec());
self.server_kexinit_payload = Some(server_kexinit_payload.to_vec()); self.server_kexinit_payload = Some(server_kexinit_payload.to_vec());
info!("Curve25519 shared secret computed and saved"); info!("Curve25519 shared secret computed and saved");
// Compute exchange hash ONCE and reuse it // Compute exchange hash ONCE and reuse it
let host_key_blob = self.build_ssh_host_key()?; let host_key_blob = self.build_ssh_host_key()?;
let exchange_hash = self.compute_exchange_hash( let exchange_hash = self.compute_exchange_hash(
@@ -103,69 +106,69 @@ impl KexExchangeHandler {
client_kexinit_payload, client_kexinit_payload,
server_kexinit_payload, server_kexinit_payload,
)?; )?;
info!("Exchange hash computed:"); info!("Exchange hash computed:");
info!(" shared_secret[0] = {} (>=0x80? {})", shared_secret[0], shared_secret[0] >= 0x80); info!(
" shared_secret[0] = {} (>=0x80? {})",
shared_secret[0],
shared_secret[0] >= 0x80
);
info!(" exchange_hash full (32 bytes): {:?}", exchange_hash); info!(" exchange_hash full (32 bytes): {:?}", exchange_hash);
self.exchange_hash = Some(exchange_hash.clone()); self.exchange_hash = Some(exchange_hash.clone());
info!("Exchange hash saved for key derivation"); info!("Exchange hash saved for key derivation");
self.build_kexdh_reply( self.build_kexdh_reply(&exchange_hash, &host_key_blob, &server_public_key)
&exchange_hash,
&host_key_blob,
&server_public_key,
)
} }
/// 构建SSH_MSG_KEXDH_REPLY packet(参考OpenSSH kex.c) /// 构建SSH_MSG_KEXDH_REPLY packet(参考OpenSSH kex.c)
fn build_kexdh_reply( fn build_kexdh_reply(
&self, &self,
exchange_hash: &[u8], exchange_hash: &[u8],
host_key_blob: &[u8], host_key_blob: &[u8],
server_public_key: &[u8], server_public_key: &[u8],
) -> Result<SshPacket> { ) -> Result<SshPacket> {
info!("=== Building SSH_MSG_KEXDH_REPLY ==="); info!("=== Building SSH_MSG_KEXDH_REPLY ===");
info!("Input server_public_key: {:?}", server_public_key); info!("Input server_public_key: {:?}", server_public_key);
let mut payload = Vec::new(); let mut payload = Vec::new();
payload.write_u8(PacketType::SSH_MSG_KEXDH_REPLY as u8)?; payload.write_u8(PacketType::SSH_MSG_KEXDH_REPLY as u8)?;
payload.write_u32::<BigEndian>(host_key_blob.len() as u32)?; payload.write_u32::<BigEndian>(host_key_blob.len() as u32)?;
payload.write_all(host_key_blob)?; payload.write_all(host_key_blob)?;
info!("Writing server_public_key to payload (32 bytes)"); info!("Writing server_public_key to payload (32 bytes)");
payload.write_u32::<BigEndian>(32)?; payload.write_u32::<BigEndian>(32)?;
payload.write_all(server_public_key)?; payload.write_all(server_public_key)?;
let signature = self.build_exchange_signature(exchange_hash)?; let signature = self.build_exchange_signature(exchange_hash)?;
payload.write_u32::<BigEndian>(signature.len() as u32)?; payload.write_u32::<BigEndian>(signature.len() as u32)?;
payload.write_all(&signature)?; payload.write_all(&signature)?;
info!("SSH_MSG_KEXDH_REPLY payload built successfully"); info!("SSH_MSG_KEXDH_REPLY payload built successfully");
Ok(SshPacket::new(payload)) Ok(SshPacket::new(payload))
} }
/// 构建SSH主机密钥blob(参考OpenSSH sshkey.c: sshkey_to_blob()) /// 构建SSH主机密钥blob(参考OpenSSH sshkey.c: sshkey_to_blob())
fn build_ssh_host_key(&self) -> Result<Vec<u8>> { fn build_ssh_host_key(&self) -> Result<Vec<u8>> {
let mut blob = Vec::new(); let mut blob = Vec::new();
// SSH key format: key-type + public-key // SSH key format: key-type + public-key
// 参考OpenSSH sshkey.c // 参考OpenSSH sshkey.c
// Key type: ssh-ed25519 // Key type: ssh-ed25519
blob.write_u32::<BigEndian>(11)?; // "ssh-ed25519".len() blob.write_u32::<BigEndian>(11)?; // "ssh-ed25519".len()
blob.write_all("ssh-ed25519".as_bytes())?; blob.write_all("ssh-ed25519".as_bytes())?;
// Ed25519公钥(32字节) // Ed25519公钥(32字节)
let public_key = self.host_key.public_key_bytes(); let public_key = self.host_key.public_key_bytes();
blob.write_u32::<BigEndian>(32)?; blob.write_u32::<BigEndian>(32)?;
blob.write_all(&public_key)?; blob.write_all(&public_key)?;
Ok(blob) Ok(blob)
} }
/// 计算Exchange Hash(参考OpenSSH kex.c: kex_hash() RFC 4253 Section 7.2) /// 计算Exchange Hash(参考OpenSSH kex.c: kex_hash() RFC 4253 Section 7.2)
fn compute_exchange_hash( fn compute_exchange_hash(
&self, &self,
@@ -178,94 +181,147 @@ impl KexExchangeHandler {
client_kexinit_payload: &[u8], client_kexinit_payload: &[u8],
server_kexinit_payload: &[u8], server_kexinit_payload: &[u8],
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
use sha2::{Sha256, Digest}; use sha2::{Digest, Sha256};
info!("=== EXCHANGE HASH COMPUTATION ==="); info!("=== EXCHANGE HASH COMPUTATION ===");
info!("V_C (client version): {:?}", client_version.as_bytes()); info!("V_C (client version): {:?}", client_version.as_bytes());
info!("V_C length: {}", client_version.len()); info!("V_C length: {}", client_version.len());
info!("V_S (server version): {:?}", server_version.as_bytes()); info!("V_S (server version): {:?}", server_version.as_bytes());
info!("V_S length: {}", server_version.len()); info!("V_S length: {}", server_version.len());
info!("I_C (client KEXINIT payload): {:?}", &client_kexinit_payload[..std::cmp::min(50, client_kexinit_payload.len())]); info!(
"I_C (client KEXINIT payload): {:?}",
&client_kexinit_payload[..std::cmp::min(50, client_kexinit_payload.len())]
);
info!("I_C length: {}", client_kexinit_payload.len()); info!("I_C length: {}", client_kexinit_payload.len());
info!("I_C[0] (packet type): {} (should be SSH_MSG_KEXINIT=20)", client_kexinit_payload[0]); info!(
"I_C[0] (packet type): {} (should be SSH_MSG_KEXINIT=20)",
info!("I_S (server KEXINIT payload): {:?}", &server_kexinit_payload[..std::cmp::min(50, server_kexinit_payload.len())]); client_kexinit_payload[0]
);
info!(
"I_S (server KEXINIT payload): {:?}",
&server_kexinit_payload[..std::cmp::min(50, server_kexinit_payload.len())]
);
info!("I_S length: {}", server_kexinit_payload.len()); info!("I_S length: {}", server_kexinit_payload.len());
info!("I_S[0] (packet type): {} (should be SSH_MSG_KEXINIT=20)", server_kexinit_payload[0]); info!(
"I_S[0] (packet type): {} (should be SSH_MSG_KEXINIT=20)",
info!("K_S (host key blob): {:?}", &host_key_blob[..std::cmp::min(30, host_key_blob.len())]); server_kexinit_payload[0]
);
info!(
"K_S (host key blob): {:?}",
&host_key_blob[..std::cmp::min(30, host_key_blob.len())]
);
info!("K_S length: {}", host_key_blob.len()); info!("K_S length: {}", host_key_blob.len());
info!("Q_C (client ECDH public key): {:?}", &client_public_key[..std::cmp::min(16, client_public_key.len())]); info!(
"Q_C (client ECDH public key): {:?}",
&client_public_key[..std::cmp::min(16, client_public_key.len())]
);
info!("Q_C full (32 bytes): {:?}", client_public_key); info!("Q_C full (32 bytes): {:?}", client_public_key);
info!("Q_C length: {}", client_public_key.len()); info!("Q_C length: {}", client_public_key.len());
info!("Q_S (server ECDH public key): {:?}", &server_public_key[..std::cmp::min(16, server_public_key.len())]); info!(
"Q_S (server ECDH public key): {:?}",
&server_public_key[..std::cmp::min(16, server_public_key.len())]
);
info!("Q_S full (32 bytes): {:?}", server_public_key); info!("Q_S full (32 bytes): {:?}", server_public_key);
info!("Q_S length: {}", server_public_key.len()); info!("Q_S length: {}", server_public_key.len());
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
// RFC 4253 Section 7: V_C and V_S are version strings (without \r\n based on testing) // RFC 4253 Section 7: V_C and V_S are version strings (without \r\n based on testing)
let vc_ssh_string = &(client_version.len() as u32).to_be_bytes(); let vc_ssh_string = &(client_version.len() as u32).to_be_bytes();
hasher.update(vc_ssh_string); hasher.update(vc_ssh_string);
hasher.update(client_version.as_bytes()); hasher.update(client_version.as_bytes());
info!(" Exchange hash component V_C: len={} bytes=[{:?}] data=[{:?}]", 4+client_version.len(), vc_ssh_string, client_version.as_bytes()); info!(
" Exchange hash component V_C: len={} bytes=[{:?}] data=[{:?}]",
4 + client_version.len(),
vc_ssh_string,
client_version.as_bytes()
);
let vs_ssh_string = &(server_version.len() as u32).to_be_bytes(); let vs_ssh_string = &(server_version.len() as u32).to_be_bytes();
hasher.update(vs_ssh_string); hasher.update(vs_ssh_string);
hasher.update(server_version.as_bytes()); hasher.update(server_version.as_bytes());
info!(" Exchange hash component V_S: len={} bytes=[{:?}] data=[{:?}]", 4+server_version.len(), vs_ssh_string, server_version.as_bytes()); info!(
" Exchange hash component V_S: len={} bytes=[{:?}] data=[{:?}]",
4 + server_version.len(),
vs_ssh_string,
server_version.as_bytes()
);
// OpenSSH kexgex.c: "kexinit messages: fake header: len+SSH2_MSG_KEXINIT" // OpenSSH kexgex.c: "kexinit messages: fake header: len+SSH2_MSG_KEXINIT"
// KEXINIT payload should NOT include SSH_MSG_KEXINIT type byte // KEXINIT payload should NOT include SSH_MSG_KEXINIT type byte
// OpenSSH stores payload starting from cookie, prepends SSH_MSG_KEXINIT in exchange hash // OpenSSH stores payload starting from cookie, prepends SSH_MSG_KEXINIT in exchange hash
// Remove SSH_MSG_KEXINIT type byte from payloads (our payload includes it) // Remove SSH_MSG_KEXINIT type byte from payloads (our payload includes it)
let client_kexinit_without_type = &client_kexinit_payload[1..]; let client_kexinit_without_type = &client_kexinit_payload[1..];
let server_kexinit_without_type = &server_kexinit_payload[1..]; let server_kexinit_without_type = &server_kexinit_payload[1..];
info!("I_C (client KEXINIT without type byte): {} bytes (first byte should be cookie)", client_kexinit_without_type.len()); info!(
info!("I_S (server KEXINIT without type byte): {} bytes", server_kexinit_without_type.len()); "I_C (client KEXINIT without type byte): {} bytes (first byte should be cookie)",
client_kexinit_without_type.len()
);
info!(
"I_S (server KEXINIT without type byte): {} bytes",
server_kexinit_without_type.len()
);
// Exchange hash: uint32(len+1) + uint8(SSH_MSG_KEXINIT) + payload_without_type // Exchange hash: uint32(len+1) + uint8(SSH_MSG_KEXINIT) + payload_without_type
let ic_len_bytes = &((client_kexinit_without_type.len() + 1) as u32).to_be_bytes(); let ic_len_bytes = &((client_kexinit_without_type.len() + 1) as u32).to_be_bytes();
hasher.update(ic_len_bytes); hasher.update(ic_len_bytes);
hasher.update(&[20]); // SSH_MSG_KEXINIT type byte hasher.update([20]); // SSH_MSG_KEXINIT type byte
hasher.update(client_kexinit_without_type); hasher.update(client_kexinit_without_type);
info!(" Exchange hash component I_C: len={} bytes=[{:?}] type=[20] payload_len={} (first 8 bytes=[{:?}])", 4+1+client_kexinit_without_type.len(), ic_len_bytes, client_kexinit_without_type.len(), &client_kexinit_without_type[..std::cmp::min(8, client_kexinit_without_type.len())]); info!(" Exchange hash component I_C: len={} bytes=[{:?}] type=[20] payload_len={} (first 8 bytes=[{:?}])", 4+1+client_kexinit_without_type.len(), ic_len_bytes, client_kexinit_without_type.len(), &client_kexinit_without_type[..std::cmp::min(8, client_kexinit_without_type.len())]);
let is_len_bytes = &((server_kexinit_without_type.len() + 1) as u32).to_be_bytes(); let is_len_bytes = &((server_kexinit_without_type.len() + 1) as u32).to_be_bytes();
hasher.update(is_len_bytes); hasher.update(is_len_bytes);
hasher.update(&[20]); // SSH_MSG_KEXINIT type byte hasher.update([20]); // SSH_MSG_KEXINIT type byte
hasher.update(server_kexinit_without_type); hasher.update(server_kexinit_without_type);
info!(" Exchange hash component I_S: len={} bytes=[{:?}] type=[20] payload_len={} (first 8 bytes=[{:?}])", 4+1+server_kexinit_without_type.len(), is_len_bytes, server_kexinit_without_type.len(), &server_kexinit_without_type[..std::cmp::min(8, server_kexinit_without_type.len())]); info!(" Exchange hash component I_S: len={} bytes=[{:?}] type=[20] payload_len={} (first 8 bytes=[{:?}])", 4+1+server_kexinit_without_type.len(), is_len_bytes, server_kexinit_without_type.len(), &server_kexinit_without_type[..std::cmp::min(8, server_kexinit_without_type.len())]);
let ks_len_bytes = &(host_key_blob.len() as u32).to_be_bytes(); let ks_len_bytes = &(host_key_blob.len() as u32).to_be_bytes();
hasher.update(ks_len_bytes); hasher.update(ks_len_bytes);
hasher.update(host_key_blob); hasher.update(host_key_blob);
info!(" Exchange hash component K_S: len={} bytes=[{:?}] blob_len={} (full=[{:?}])", 4+host_key_blob.len(), ks_len_bytes, host_key_blob.len(), host_key_blob); info!(
" Exchange hash component K_S: len={} bytes=[{:?}] blob_len={} (full=[{:?}])",
4 + host_key_blob.len(),
ks_len_bytes,
host_key_blob.len(),
host_key_blob
);
let qc_len_bytes = &(client_public_key.len() as u32).to_be_bytes(); let qc_len_bytes = &(client_public_key.len() as u32).to_be_bytes();
hasher.update(qc_len_bytes); hasher.update(qc_len_bytes);
hasher.update(client_public_key); hasher.update(client_public_key);
info!(" Exchange hash component Q_C: len={} bytes=[{:?}] key=[{:?}]", 4+client_public_key.len(), qc_len_bytes, client_public_key); info!(
" Exchange hash component Q_C: len={} bytes=[{:?}] key=[{:?}]",
4 + client_public_key.len(),
qc_len_bytes,
client_public_key
);
let qs_len_bytes = &(server_public_key.len() as u32).to_be_bytes(); let qs_len_bytes = &(server_public_key.len() as u32).to_be_bytes();
hasher.update(qs_len_bytes); hasher.update(qs_len_bytes);
hasher.update(server_public_key); hasher.update(server_public_key);
info!(" Exchange hash component Q_S: len={} bytes=[{:?}] key=[{:?}]", 4+server_public_key.len(), qs_len_bytes, server_public_key); info!(
" Exchange hash component Q_S: len={} bytes=[{:?}] key=[{:?}]",
4 + server_public_key.len(),
qs_len_bytes,
server_public_key
);
info!("Exchange hash components:"); info!("Exchange hash components:");
info!(" shared_secret raw full (32 bytes): {:?}", shared_secret); info!(" shared_secret raw full (32 bytes): {:?}", shared_secret);
// RFC 8731 Section 3.1: X25519 output is little-endian // RFC 8731 Section 3.1: X25519 output is little-endian
// OpenSSH sshbuf_put_bignum2_bytes() uses bytes DIRECTLY (no reversal) // OpenSSH sshbuf_put_bignum2_bytes() uses bytes DIRECTLY (no reversal)
// Treats little-endian bytes as big-endian mpint (logical reinterpret) // Treats little-endian bytes as big-endian mpint (logical reinterpret)
info!(" Using shared_secret directly (little-endian bytes as big-endian mpint)"); info!(" Using shared_secret directly (little-endian bytes as big-endian mpint)");
// RFC 4253: mpint格式 = 去掉前导零 + 最高位>=0x80时前面加0 // RFC 4253: mpint格式 = 去掉前导零 + 最高位>=0x80时前面加0
// 参考OpenSSH sshbuf_put_bignum2_bytes() // 参考OpenSSH sshbuf_put_bignum2_bytes()
let mut start = 0; let mut start = 0;
@@ -273,64 +329,73 @@ impl KexExchangeHandler {
start += 1; start += 1;
} }
let trimmed_shared_secret = &shared_secret[start..]; let trimmed_shared_secret = &shared_secret[start..];
info!(" shared_secret after removing leading zeros ({} bytes): {:?}", trimmed_shared_secret.len(), trimmed_shared_secret); info!(
" shared_secret after removing leading zeros ({} bytes): {:?}",
let mpint_shared_secret_data = if trimmed_shared_secret.len() > 0 && trimmed_shared_secret[0] >= 0x80 { trimmed_shared_secret.len(),
let mut mpint = vec![0u8]; trimmed_shared_secret
mpint.extend_from_slice(trimmed_shared_secret); );
info!(" trimmed_shared_secret[0] >= 0x80, prepending 0 byte");
mpint let mpint_shared_secret_data =
} else { if !trimmed_shared_secret.is_empty() && trimmed_shared_secret[0] >= 0x80 {
trimmed_shared_secret.to_vec() let mut mpint = vec![0u8];
}; mpint.extend_from_slice(trimmed_shared_secret);
info!(" trimmed_shared_secret[0] >= 0x80, prepending 0 byte");
info!(" mpint_shared_secret_data ({} bytes): {:?}", mpint_shared_secret_data.len(), &mpint_shared_secret_data[..std::cmp::min(8, mpint_shared_secret_data.len())]); mpint
} else {
trimmed_shared_secret.to_vec()
};
info!(
" mpint_shared_secret_data ({} bytes): {:?}",
mpint_shared_secret_data.len(),
&mpint_shared_secret_data[..std::cmp::min(8, mpint_shared_secret_data.len())]
);
// mpint格式 = uint32(length) + mpint_data // mpint格式 = uint32(length) + mpint_data
let mpint_len_bytes = &(mpint_shared_secret_data.len() as u32).to_be_bytes(); let mpint_len_bytes = &(mpint_shared_secret_data.len() as u32).to_be_bytes();
hasher.update(mpint_len_bytes); hasher.update(mpint_len_bytes);
hasher.update(&mpint_shared_secret_data); hasher.update(&mpint_shared_secret_data);
info!(" Exchange hash component K (shared secret mpint): len={} bytes=[{:?}] data_len={} (first 8 bytes=[{:?}])", 4+mpint_shared_secret_data.len(), mpint_len_bytes, mpint_shared_secret_data.len(), &mpint_shared_secret_data[..std::cmp::min(8, mpint_shared_secret_data.len())]); info!(" Exchange hash component K (shared secret mpint): len={} bytes=[{:?}] data_len={} (first 8 bytes=[{:?}])", 4+mpint_shared_secret_data.len(), mpint_len_bytes, mpint_shared_secret_data.len(), &mpint_shared_secret_data[..std::cmp::min(8, mpint_shared_secret_data.len())]);
Ok(hasher.finalize().to_vec()) Ok(hasher.finalize().to_vec())
} }
/// 构建交换签名(参考OpenSSH ssh-sign.c) /// 构建交换签名(参考OpenSSH ssh-sign.c)
fn build_exchange_signature(&self, exchange_hash: &[u8]) -> Result<Vec<u8>> { fn build_exchange_signature(&self, exchange_hash: &[u8]) -> Result<Vec<u8>> {
let signature_bytes = self.host_key.sign(exchange_hash)?; let signature_bytes = self.host_key.sign(exchange_hash)?;
let mut ssh_signature = Vec::new(); let mut ssh_signature = Vec::new();
ssh_signature.write_u32::<BigEndian>(11)?; ssh_signature.write_u32::<BigEndian>(11)?;
ssh_signature.write_all("ssh-ed25519".as_bytes())?; ssh_signature.write_all("ssh-ed25519".as_bytes())?;
ssh_signature.write_u32::<BigEndian>(64)?; ssh_signature.write_u32::<BigEndian>(64)?;
ssh_signature.write_all(&signature_bytes)?; ssh_signature.write_all(&signature_bytes)?;
Ok(ssh_signature) Ok(ssh_signature)
} }
/// 计算会话密钥(参考OpenSSH kex.c: derive_keys()) /// 计算会话密钥(参考OpenSSH kex.c: derive_keys())
/// 使用保存的exchange_hash(H参数) /// 使用保存的exchange_hash(H参数)
pub fn compute_session_keys(&self) -> Result<SessionKeys> { pub fn compute_session_keys(&self) -> Result<SessionKeys> {
if self.shared_secret.is_none() { if self.shared_secret.is_none() {
return Err(anyhow!("No shared secret available")); return Err(anyhow!("No shared secret available"));
} }
if self.exchange_hash.is_none() { if self.exchange_hash.is_none() {
return Err(anyhow!("No exchange hash available")); return Err(anyhow!("No exchange hash available"));
} }
let shared_secret = self.shared_secret.as_ref().unwrap(); let shared_secret = self.shared_secret.as_ref().unwrap();
let exchange_hash = self.exchange_hash.as_ref().unwrap(); let exchange_hash = self.exchange_hash.as_ref().unwrap();
let server_public_key = self.server_public_key.as_ref().unwrap(); let server_public_key = self.server_public_key.as_ref().unwrap();
let client_public_key = self.client_public_key.as_ref().unwrap(); let client_public_key = self.client_public_key.as_ref().unwrap();
let host_key_blob = self.build_ssh_host_key()?; let host_key_blob = self.build_ssh_host_key()?;
SessionKeys::derive( SessionKeys::derive(
shared_secret, shared_secret,
exchange_hash, // 使用保存的exchange hash(H参数) exchange_hash, // 使用保存的exchange hash(H参数)
server_public_key, server_public_key,
client_public_key, client_public_key,
&host_key_blob, &host_key_blob,
@@ -342,13 +407,13 @@ impl KexExchangeHandler {
mod tests { mod tests {
use super::*; use super::*;
use crate::ssh_server::kex::KexProposal; use crate::ssh_server::kex::KexProposal;
#[test] #[test]
fn test_kex_exchange_handler_creation() { fn test_kex_exchange_handler_creation() {
let server_proposal = KexProposal::server_default(); let server_proposal = KexProposal::server_default();
let client_proposal = KexProposal::client_default(); let client_proposal = KexProposal::client_default();
let kex_result = KexResult::choose_algorithms(&server_proposal, &client_proposal).unwrap(); let kex_result = KexResult::choose_algorithms(&server_proposal, &client_proposal).unwrap();
let handler = KexExchangeHandler::new(kex_result).unwrap(); let handler = KexExchangeHandler::new(kex_result).unwrap();
assert!(handler.host_key.public_key_bytes().len() == 32); assert!(handler.host_key.public_key_bytes().len() == 32);
} }
+20 -20
View File
@@ -1,28 +1,28 @@
// SSH服务器模块(手动实现SSH协议) // SSH服务器模块(手动实现SSH协议)
// 参考OpenSSH源码实现完整的SSH/SFTP/SCP/rsync协议 // 参考OpenSSH源码实现完整的SSH/SFTP/SCP/rsync协议
pub mod server;
pub mod packet;
pub mod version;
pub mod crypto;
pub mod kex;
pub mod kex_exchange;
pub mod kex_complete;
pub mod cipher;
pub mod auth; pub mod auth;
pub mod channel; pub mod channel;
pub mod sftp_handler; pub mod cipher;
pub mod scp_handler; pub mod crypto;
pub mod data_forwarder; // Phase 13.5: 数据传输模块
pub mod kex;
pub mod kex_complete;
pub mod kex_exchange;
pub mod packet;
pub mod port_forward; // Phase 13: 端口转发模块
pub mod port_forward_listener; // Phase 13.4: 监听线程模块
pub mod rsync_handler; pub mod rsync_handler;
pub mod sshbuf; // Phase 15: SSH Buffer 零拷贝管理(参考OpenSSH sshbuf.c) pub mod scp_handler;
pub mod port_forward; // Phase 13: 端口转发模块 pub mod server;
pub mod ssh_security_config; // Phase 13.1: 企业级安全配置 pub mod sftp_handler;
pub mod port_forward_listener; // Phase 13.4: 监听线程模块 pub mod ssh_security_config; // Phase 13.1: 企业级安全配置
pub mod data_forwarder; // Phase 13.5: 数据传输模块 pub mod sshbuf; // Phase 15: SSH Buffer 零拷贝管理(参考OpenSSH sshbuf.c)
pub mod window_manager; // Phase 13.6-13.7: Window size + Channel生命周期 pub mod version;
pub mod window_manager; // Phase 13.6-13.7: Window size + Channel生命周期
pub use packet::{PacketType, SshPacket};
pub use server::SshServer; pub use server::SshServer;
pub use packet::{SshPacket, PacketType}; pub use ssh_security_config::SshSecurityConfig; // Phase 13.1: 导出安全配置
pub use version::VersionExchange; pub use sshbuf::SshBuf;
pub use ssh_security_config::SshSecurityConfig; // Phase 13.1: 导出安全配置 pub use version::VersionExchange; // Phase 15: 导出 SSH Buffer
pub use sshbuf::SshBuf; // Phase 15: 导出 SSH Buffer
+37 -37
View File
@@ -1,7 +1,7 @@
// SSH Packet基础结构定义 // SSH Packet基础结构定义
// 参考OpenSSH packet.c: ssh_packet_read(), ssh_packet_write() // 参考OpenSSH packet.c: ssh_packet_read(), ssh_packet_write()
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::{Read, Write}; use std::io::{Read, Write};
@@ -18,21 +18,21 @@ pub enum PacketType {
SSH_MSG_EXT_INFO = 7, SSH_MSG_EXT_INFO = 7,
SSH_MSG_KEXINIT = 20, SSH_MSG_KEXINIT = 20,
SSH_MSG_NEWKEYS = 21, SSH_MSG_NEWKEYS = 21,
// 密钥交换相关 // 密钥交换相关
SSH_MSG_KEXDH_INIT = 30, SSH_MSG_KEXDH_INIT = 30,
SSH_MSG_KEXDH_REPLY = 31, SSH_MSG_KEXDH_REPLY = 31,
// 注意:Curve25519和DH使用相同的消息类型(30/31) // 注意:Curve25519和DH使用相同的消息类型(30/31)
// SSH_MSG_KEX_ECDH_INIT和SSH_MSG_KEX_ECDH_REPLY已在代码中注释 // SSH_MSG_KEX_ECDH_INIT和SSH_MSG_KEX_ECDH_REPLY已在代码中注释
// 使用SSH_MSG_KEXDH_INIT和SSH_MSG_KEXDH_REPLY代替 // 使用SSH_MSG_KEXDH_INIT和SSH_MSG_KEXDH_REPLY代替
// 认证相关 // 认证相关
SSH_MSG_USERAUTH_REQUEST = 50, SSH_MSG_USERAUTH_REQUEST = 50,
SSH_MSG_USERAUTH_FAILURE = 51, SSH_MSG_USERAUTH_FAILURE = 51,
SSH_MSG_USERAUTH_SUCCESS = 52, SSH_MSG_USERAUTH_SUCCESS = 52,
SSH_MSG_USERAUTH_BANNER = 53, SSH_MSG_USERAUTH_BANNER = 53,
SSH_MSG_USERAUTH_PK_OK = 60, SSH_MSG_USERAUTH_PK_OK = 60,
// Channel相关 // Channel相关
SSH_MSG_GLOBAL_REQUEST = 80, SSH_MSG_GLOBAL_REQUEST = 80,
SSH_MSG_REQUEST_SUCCESS = 81, SSH_MSG_REQUEST_SUCCESS = 81,
@@ -70,38 +70,38 @@ impl SshPacket {
pub fn new(payload: Vec<u8>) -> Self { pub fn new(payload: Vec<u8>) -> Self {
// 计算padding(SSH协议RFC 4253规范) // 计算padding(SSH协议RFC 4253规范)
// 参考OpenSSH packet.c: construct_packet() // 参考OpenSSH packet.c: construct_packet()
let block_size = 8; // 未加密阶段block_size=8 let block_size = 8; // 未加密阶段block_size=8
let payload_length = payload.len(); let payload_length = payload.len();
let min_padding = 4; // OpenSSH要求最少4字节padding let min_padding = 4; // OpenSSH要求最少4字节padding
// SSH协议约束: // SSH协议约束:
// packet_length = padding_length + payload_length + 1 // packet_length = padding_length + payload_length + 1
// (packet_length + 4) 必须是block_size的倍数 // (packet_length + 4) 必须是block_size的倍数
// //
// 计算: // 计算:
// (1 + payload_length + padding_length + 4) % 8 == 0 // (1 + payload_length + padding_length + 4) % 8 == 0
// => (5 + payload_length + padding_length) % 8 == 0 // => (5 + payload_length + padding_length) % 8 == 0
// 先尝试padding=4(最小) // 先尝试padding=4(最小)
let mut padding_length = min_padding as u8; let mut padding_length = min_padding as u8;
// 计算packet总长度(包括4字节的packet_length字段) // 计算packet总长度(包括4字节的packet_length字段)
let packet_length = 1 + payload_length + padding_length as usize; let packet_length = 1 + payload_length + padding_length as usize;
let total_length = packet_length + 4; // 加上packet_length字段本身的4字节 let total_length = packet_length + 4; // 加上packet_length字段本身的4字节
// 如果总长度不是block_size的倍数,增加padding // 如果总长度不是block_size的倍数,增加padding
if total_length % block_size != 0 { if !total_length.is_multiple_of(block_size) {
let remainder = total_length % block_size; let remainder = total_length % block_size;
padding_length += (block_size - remainder) as u8; padding_length += (block_size - remainder) as u8;
} }
// 重新计算packet_length // 重新计算packet_length
let packet_length = (1 + payload_length + padding_length as usize) as u32; let packet_length = (1 + payload_length + padding_length as usize) as u32;
// 生成随机padding(简化:使用0) // 生成随机padding(简化:使用0)
let padding = vec![0u8; padding_length as usize]; let padding = vec![0u8; padding_length as usize];
Self { Self {
packet_length, packet_length,
padding_length, padding_length,
@@ -109,49 +109,49 @@ impl SshPacket {
padding, padding,
} }
} }
/// 写入packet到stream(未加密阶段) /// 写入packet到stream(未加密阶段)
/// 参考OpenSSH packet_write_poll() /// 参考OpenSSH packet_write_poll()
pub fn write<T: Write>(&self, stream: &mut T) -> Result<()> { pub fn write<T: Write>(&self, stream: &mut T) -> Result<()> {
// 写入packet_length(BigEndian) // 写入packet_length(BigEndian)
stream.write_u32::<BigEndian>(self.packet_length)?; stream.write_u32::<BigEndian>(self.packet_length)?;
// 写入padding_length // 写入padding_length
stream.write_u8(self.padding_length)?; stream.write_u8(self.padding_length)?;
// 写入payload // 写入payload
stream.write_all(&self.payload)?; stream.write_all(&self.payload)?;
// 写入padding // 写入padding
stream.write_all(&self.padding)?; stream.write_all(&self.padding)?;
stream.flush()?; stream.flush()?;
Ok(()) Ok(())
} }
/// 从stream读取packet(未加密阶段) /// 从stream读取packet(未加密阶段)
/// 参考OpenSSH packet_read_poll() /// 参考OpenSSH packet_read_poll()
pub fn read<T: Read>(stream: &mut T) -> Result<Self> { pub fn read<T: Read>(stream: &mut T) -> Result<Self> {
// 读取packet_length(BigEndian) // 读取packet_length(BigEndian)
let packet_length = stream.read_u32::<BigEndian>()?; let packet_length = stream.read_u32::<BigEndian>()?;
// 检查packet长度限制(OpenSSH限制:256KB) // 检查packet长度限制(OpenSSH限制:256KB)
if packet_length > 256 * 1024 { if packet_length > 256 * 1024 {
return Err(anyhow!("Packet too large: {}", packet_length)); return Err(anyhow!("Packet too large: {}", packet_length));
} }
// 读取padding_length // 读取padding_length
let padding_length = stream.read_u8()?; let padding_length = stream.read_u8()?;
// 读取payload(packet_length - padding_length - 1) // 读取payload(packet_length - padding_length - 1)
let payload_length = packet_length - padding_length as u32 - 1; let payload_length = packet_length - padding_length as u32 - 1;
let mut payload = vec![0u8; payload_length as usize]; let mut payload = vec![0u8; payload_length as usize];
stream.read_exact(&mut payload)?; stream.read_exact(&mut payload)?;
// 读取padding // 读取padding
let mut padding = vec![0u8; padding_length as usize]; let mut padding = vec![0u8; padding_length as usize];
stream.read_exact(&mut padding)?; stream.read_exact(&mut padding)?;
Ok(Self { Ok(Self {
packet_length, packet_length,
padding_length, padding_length,
@@ -159,15 +159,15 @@ impl SshPacket {
padding, padding,
}) })
} }
/// 获取payload中的packet type /// 获取payload中的packet type
pub fn get_type(&self) -> Result<PacketType> { pub fn get_type(&self) -> Result<PacketType> {
if self.payload.is_empty() { if self.payload.is_empty() {
return Err(anyhow!("Empty payload")); return Err(anyhow!("Empty payload"));
} }
let type_byte = self.payload[0]; let type_byte = self.payload[0];
// 转换为PacketType enum // 转换为PacketType enum
match type_byte { match type_byte {
1 => Ok(PacketType::SSH_MSG_DISCONNECT), 1 => Ok(PacketType::SSH_MSG_DISCONNECT),
@@ -208,27 +208,27 @@ impl SshPacket {
mod tests { mod tests {
use super::*; use super::*;
use std::io::Cursor; use std::io::Cursor;
#[test] #[test]
fn test_packet_creation() { fn test_packet_creation() {
let payload = vec![PacketType::SSH_MSG_KEXINIT as u8]; let payload = vec![PacketType::SSH_MSG_KEXINIT as u8];
let packet = SshPacket::new(payload); let packet = SshPacket::new(payload);
assert!(packet.packet_length > 0); assert!(packet.packet_length > 0);
assert!(packet.padding_length >= 4); assert!(packet.padding_length >= 4);
} }
#[test] #[test]
fn test_packet_write_read() { fn test_packet_write_read() {
let payload = vec![PacketType::SSH_MSG_KEXINIT as u8]; let payload = vec![PacketType::SSH_MSG_KEXINIT as u8];
let packet = SshPacket::new(payload); let packet = SshPacket::new(payload);
let mut buffer = Vec::new(); let mut buffer = Vec::new();
packet.write(&mut buffer).unwrap(); packet.write(&mut buffer).unwrap();
let mut cursor = Cursor::new(buffer); let mut cursor = Cursor::new(buffer);
let read_packet = SshPacket::read(&mut cursor).unwrap(); let read_packet = SshPacket::read(&mut cursor).unwrap();
assert_eq!(packet.packet_length, read_packet.packet_length); assert_eq!(packet.packet_length, read_packet.packet_length);
assert_eq!(packet.payload, read_packet.payload); assert_eq!(packet.payload, read_packet.payload);
} }
+103 -69
View File
@@ -1,21 +1,21 @@
// SSH端口转发协议实现(Phase 13) // SSH端口转发协议实现(Phase 13)
// 参考OpenSSH channels.c和RFC 4254 // 参考OpenSSH channels.c和RFC 4254
use anyhow::{Result, anyhow}; use crate::ssh_server::ssh_security_config::SshSecurityConfig;
use log::{info, warn, debug}; use anyhow::Result;
use std::net::{TcpListener, TcpStream, SocketAddr};
use std::io::{Read, Write};
use std::sync::{Arc, Mutex};
use std::thread;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use crate::ssh_server::ssh_security_config::SshSecurityConfig; // Phase 13.2: 安全配置 use log::{info, warn};
use std::io::Read;
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
// Phase 13.2: 安全配置
/// 端口转发类型(参考RFC 4254) /// 端口转发类型(参考RFC 4254)
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum PortForwardType { pub enum PortForwardType {
Local, // Local port forwarding (-L) Local, // Local port forwarding (-L)
Remote, // Remote port forwarding (-R) Remote, // Remote port forwarding (-R)
Dynamic, // Dynamic port forwarding (-D, SOCKS) Dynamic, // Dynamic port forwarding (-D, SOCKS)
} }
/// 端口转发请求(参考RFC 4254 Section 7) /// 端口转发请求(参考RFC 4254 Section 7)
@@ -36,6 +36,12 @@ pub struct PortForwardManager {
active_forwards: Arc<Mutex<Vec<(u32, PortForwardType)>>>, active_forwards: Arc<Mutex<Vec<(u32, PortForwardType)>>>,
} }
impl Default for PortForwardManager {
fn default() -> Self {
Self::new()
}
}
impl PortForwardManager { impl PortForwardManager {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -46,24 +52,29 @@ impl PortForwardManager {
/// 处理SSH_MSG_GLOBAL_REQUEST(端口转发请求) /// 处理SSH_MSG_GLOBAL_REQUEST(端口转发请求)
/// 参考RFC 4254 Section 4 /// 参考RFC 4254 Section 4
/// Phase 13.2: 添加安全配置验证 /// Phase 13.2: 添加安全配置验证
pub fn handle_global_request(&mut self, data: &[u8], security_config: &SshSecurityConfig) -> Result<(bool, Option<Vec<u8>>)> { pub fn handle_global_request(
&mut self,
data: &[u8],
security_config: &SshSecurityConfig,
) -> Result<(bool, Option<Vec<u8>>)> {
info!("Processing SSH_MSG_GLOBAL_REQUEST for port forwarding"); info!("Processing SSH_MSG_GLOBAL_REQUEST for port forwarding");
let mut cursor = std::io::Cursor::new(data); let mut cursor = std::io::Cursor::new(data);
cursor.set_position(1); // Skip packet type cursor.set_position(1); // Skip packet type
// 读取请求名称(SSH string) // 读取请求名称(SSH string)
let request_name = read_ssh_string(&mut cursor)?; let request_name = read_ssh_string(&mut cursor)?;
info!("Global request: {}", request_name); info!("Global request: {}", request_name);
// 读取want-reply标志 // 读取want-reply标志
let want_reply = cursor.read_u8()? != 0; let want_reply = cursor.read_u8()? != 0;
match request_name.as_str() { match request_name.as_str() {
"tcpip-forward" => { "tcpip-forward" => {
// Local port forwarding (-L) // Local port forwarding (-L)
self.handle_tcpip_forward(&mut cursor, want_reply, security_config) // Phase 13.2 self.handle_tcpip_forward(&mut cursor, want_reply, security_config)
// Phase 13.2
} }
"cancel-tcpip-forward" => { "cancel-tcpip-forward" => {
// Cancel port forwarding // Cancel port forwarding
@@ -84,29 +95,37 @@ impl PortForwardManager {
/// 处理tcpip-forward请求(Local port forwarding) /// 处理tcpip-forward请求(Local port forwarding)
/// 参考RFC 4254 Section 7.1 /// 参考RFC 4254 Section 7.1
/// Phase 13.2: 添加安全配置验证 /// Phase 13.2: 添加安全配置验证
fn handle_tcpip_forward(&mut self, cursor: &mut std::io::Cursor<&[u8]>, want_reply: bool, security_config: &SshSecurityConfig) -> Result<(bool, Option<Vec<u8>>)> { fn handle_tcpip_forward(
&mut self,
cursor: &mut std::io::Cursor<&[u8]>,
want_reply: bool,
security_config: &SshSecurityConfig,
) -> Result<(bool, Option<Vec<u8>>)> {
// 读取bind address(SSH string) // 读取bind address(SSH string)
let bind_address = read_ssh_string(cursor)?; let bind_address = read_ssh_string(cursor)?;
// 读取bind port // 读取bind port
let bind_port = cursor.read_u32::<BigEndian>()?; let bind_port = cursor.read_u32::<BigEndian>()?;
info!("tcpip-forward request: bind_address={}, bind_port={}", bind_address, bind_port); info!(
"tcpip-forward request: bind_address={}, bind_port={}",
bind_address, bind_port
);
// Phase 13.2: 安全配置验证 // Phase 13.2: 安全配置验证
if let Err(e) = security_config.validate_tcpip_forward_request(&bind_address, bind_port) { if let Err(e) = security_config.validate_tcpip_forward_request(&bind_address, bind_port) {
warn!("tcpip-forward security validation failed: {}", e); warn!("tcpip-forward security validation failed: {}", e);
return Ok((false, None)); // 拒绝请求 return Ok((false, None)); // 拒绝请求
} }
info!("tcpip-forward security validation passed"); info!("tcpip-forward security validation passed");
// 添加到active forwards // 添加到active forwards
let mut forwards = self.active_forwards.lock().unwrap(); let mut forwards = self.active_forwards.lock().unwrap();
forwards.push((bind_port, PortForwardType::Local)); forwards.push((bind_port, PortForwardType::Local));
info!("tcpip-forward registered: bind_port={}", bind_port); info!("tcpip-forward registered: bind_port={}", bind_port);
// 返回成功响应(包含bind_port) // 返回成功响应(包含bind_port)
if want_reply { if want_reply {
let response = self.build_global_request_response(true, Some(bind_port))?; let response = self.build_global_request_response(true, Some(bind_port))?;
@@ -117,16 +136,23 @@ impl PortForwardManager {
} }
/// 处理cancel-tcpip-forward请求 /// 处理cancel-tcpip-forward请求
fn handle_cancel_tcpip_forward(&mut self, cursor: &mut std::io::Cursor<&[u8]>, want_reply: bool) -> Result<(bool, Option<Vec<u8>>)> { fn handle_cancel_tcpip_forward(
&mut self,
cursor: &mut std::io::Cursor<&[u8]>,
want_reply: bool,
) -> Result<(bool, Option<Vec<u8>>)> {
let bind_address = read_ssh_string(cursor)?; let bind_address = read_ssh_string(cursor)?;
let bind_port = cursor.read_u32::<BigEndian>()?; let bind_port = cursor.read_u32::<BigEndian>()?;
info!("cancel-tcpip-forward: bind_address={}, bind_port={}", bind_address, bind_port); info!(
"cancel-tcpip-forward: bind_address={}, bind_port={}",
bind_address, bind_port
);
// 移除active forward // 移除active forward
let mut forwards = self.active_forwards.lock().unwrap(); let mut forwards = self.active_forwards.lock().unwrap();
forwards.retain(|(port, _)| *port != bind_port); forwards.retain(|(port, _)| *port != bind_port);
if want_reply { if want_reply {
let response = self.build_global_request_response(true, None)?; let response = self.build_global_request_response(true, None)?;
Ok((true, Some(response))) Ok((true, Some(response)))
@@ -136,14 +162,18 @@ impl PortForwardManager {
} }
/// 构建SSH_MSG_REQUEST_SUCCESS/FAILURE响应 /// 构建SSH_MSG_REQUEST_SUCCESS/FAILURE响应
fn build_global_request_response(&self, success: bool, bound_port: Option<u32>) -> Result<Vec<u8>> { fn build_global_request_response(
&self,
success: bool,
bound_port: Option<u32>,
) -> Result<Vec<u8>> {
use crate::ssh_server::packet::PacketType; use crate::ssh_server::packet::PacketType;
let mut response = Vec::new(); let mut response = Vec::new();
if success { if success {
response.write_u8(PacketType::SSH_MSG_REQUEST_SUCCESS as u8)?; response.write_u8(PacketType::SSH_MSG_REQUEST_SUCCESS as u8)?;
// 如果有bound_port,写入(用于tcpip-forward响应) // 如果有bound_port,写入(用于tcpip-forward响应)
if let Some(port) = bound_port { if let Some(port) = bound_port {
response.write_u32::<BigEndian>(port)?; response.write_u32::<BigEndian>(port)?;
@@ -151,7 +181,7 @@ impl PortForwardManager {
} else { } else {
response.write_u8(PacketType::SSH_MSG_REQUEST_FAILURE as u8)?; response.write_u8(PacketType::SSH_MSG_REQUEST_FAILURE as u8)?;
} }
Ok(response) Ok(response)
} }
@@ -159,37 +189,39 @@ impl PortForwardManager {
/// 参考RFC 4254 Section 7.2 /// 参考RFC 4254 Section 7.2
pub fn handle_direct_tcpip_channel(&mut self, data: &[u8]) -> Result<DirectTcpipChannel> { pub fn handle_direct_tcpip_channel(&mut self, data: &[u8]) -> Result<DirectTcpipChannel> {
info!("Processing direct-tcpip channel open"); info!("Processing direct-tcpip channel open");
let mut cursor = std::io::Cursor::new(data); let mut cursor = std::io::Cursor::new(data);
cursor.set_position(1); // Skip packet type cursor.set_position(1); // Skip packet type
// 读取channel type(已知道是"direct-tcpip",跳过) // 读取channel type(已知道是"direct-tcpip",跳过)
let _channel_type = read_ssh_string(&mut cursor)?; let _channel_type = read_ssh_string(&mut cursor)?;
// 读取sender_channel // 读取sender_channel
let sender_channel = cursor.read_u32::<BigEndian>()?; let sender_channel = cursor.read_u32::<BigEndian>()?;
// 读取initial window size // 读取initial window size
let initial_window_size = cursor.read_u32::<BigEndian>()?; let initial_window_size = cursor.read_u32::<BigEndian>()?;
// 读取maximum packet size // 读取maximum packet size
let max_packet_size = cursor.read_u32::<BigEndian>()?; let max_packet_size = cursor.read_u32::<BigEndian>()?;
// 读取host to connect(SSH string) // 读取host to connect(SSH string)
let host_to_connect = read_ssh_string(&mut cursor)?; let host_to_connect = read_ssh_string(&mut cursor)?;
// 读取port to connect // 读取port to connect
let port_to_connect = cursor.read_u32::<BigEndian>()?; let port_to_connect = cursor.read_u32::<BigEndian>()?;
// 读取originator address(SSH string) // 读取originator address(SSH string)
let originator_address = read_ssh_string(&mut cursor)?; let originator_address = read_ssh_string(&mut cursor)?;
// 读取originator port // 读取originator port
let originator_port = cursor.read_u32::<BigEndian>()?; let originator_port = cursor.read_u32::<BigEndian>()?;
info!("direct-tcpip: host={}, port={}, originator={}:{}", info!(
host_to_connect, port_to_connect, originator_address, originator_port); "direct-tcpip: host={}, port={}, originator={}:{}",
host_to_connect, port_to_connect, originator_address, originator_port
);
Ok(DirectTcpipChannel { Ok(DirectTcpipChannel {
sender_channel, sender_channel,
initial_window_size, initial_window_size,
@@ -205,30 +237,32 @@ impl PortForwardManager {
/// 参考RFC 4254 Section 7.1 /// 参考RFC 4254 Section 7.1
pub fn handle_forwarded_tcpip_channel(&mut self, data: &[u8]) -> Result<ForwardedTcpipChannel> { pub fn handle_forwarded_tcpip_channel(&mut self, data: &[u8]) -> Result<ForwardedTcpipChannel> {
info!("Processing forwarded-tcpip channel open"); info!("Processing forwarded-tcpip channel open");
let mut cursor = std::io::Cursor::new(data); let mut cursor = std::io::Cursor::new(data);
cursor.set_position(1); cursor.set_position(1);
let _channel_type = read_ssh_string(&mut cursor)?; let _channel_type = read_ssh_string(&mut cursor)?;
let sender_channel = cursor.read_u32::<BigEndian>()?; let sender_channel = cursor.read_u32::<BigEndian>()?;
let initial_window_size = cursor.read_u32::<BigEndian>()?; let initial_window_size = cursor.read_u32::<BigEndian>()?;
let max_packet_size = cursor.read_u32::<BigEndian>()?; let max_packet_size = cursor.read_u32::<BigEndian>()?;
// 读取bind address(SSH string) // 读取bind address(SSH string)
let bind_address = read_ssh_string(&mut cursor)?; let bind_address = read_ssh_string(&mut cursor)?;
// 读取bind port // 读取bind port
let bind_port = cursor.read_u32::<BigEndian>()?; let bind_port = cursor.read_u32::<BigEndian>()?;
// 读取originator address(SSH string) // 读取originator address(SSH string)
let originator_address = read_ssh_string(&mut cursor)?; let originator_address = read_ssh_string(&mut cursor)?;
// 读取originator port // 读取originator port
let originator_port = cursor.read_u32::<BigEndian>()?; let originator_port = cursor.read_u32::<BigEndian>()?;
info!("forwarded-tcpip: bind={}:{}, originator={}:{}", info!(
bind_address, bind_port, originator_address, originator_port); "forwarded-tcpip: bind={}:{}, originator={}:{}",
bind_address, bind_port, originator_address, originator_port
);
Ok(ForwardedTcpipChannel { Ok(ForwardedTcpipChannel {
sender_channel, sender_channel,
initial_window_size, initial_window_size,
@@ -244,10 +278,10 @@ impl PortForwardManager {
pub fn connect_to_target(host: &str, port: u32) -> Result<TcpStream> { pub fn connect_to_target(host: &str, port: u32) -> Result<TcpStream> {
let addr = format!("{}:{}", host, port); let addr = format!("{}:{}", host, port);
info!("Connecting to target: {}", addr); info!("Connecting to target: {}", addr);
let stream = TcpStream::connect(&addr)?; let stream = TcpStream::connect(&addr)?;
info!("Connected to target successfully"); info!("Connected to target successfully");
Ok(stream) Ok(stream)
} }
@@ -258,12 +292,12 @@ impl PortForwardManager {
} else { } else {
format!("{}:{}", bind_address, bind_port) format!("{}:{}", bind_address, bind_port)
}; };
info!("Creating listener on {}", addr); info!("Creating listener on {}", addr);
let listener = TcpListener::bind(&addr)?; let listener = TcpListener::bind(&addr)?;
info!("Listener created successfully"); info!("Listener created successfully");
Ok(listener) Ok(listener)
} }
} }
@@ -303,10 +337,10 @@ fn read_ssh_string<R: std::io::Read>(reader: &mut R) -> Result<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_port_forward_manager_creation() { fn test_port_forward_manager_creation() {
let manager = PortForwardManager::new(); let manager = PortForwardManager::new();
assert_eq!(manager.active_forwards.lock().unwrap().len(), 0); assert_eq!(manager.active_forwards.lock().unwrap().len(), 0);
} }
} }
@@ -1,15 +1,12 @@
// SSH端口转发监听线程(Phase 13.4) // SSH端口转发监听线程(Phase 13.4)
// 参考OpenSSH channels.c: channel_forward_listener // 参考OpenSSH channels.c: channel_forward_listener
use anyhow::{Result, anyhow};
use log::{info, warn, debug, error};
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::sync::{Arc, Mutex, mpsc};
use std::io::{Read, Write};
use byteorder::{BigEndian, WriteBytesExt};
use crate::ssh_server::packet::PacketType;
use crate::ssh_server::ssh_security_config::SshSecurityConfig; use crate::ssh_server::ssh_security_config::SshSecurityConfig;
use anyhow::Result;
use log::{error, info, warn};
use std::net::{TcpListener, TcpStream};
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
/// 监听器状态(Phase 13.4) /// 监听器状态(Phase 13.4)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -30,28 +27,18 @@ pub enum ListenerRequest {
stream: TcpStream, stream: TcpStream,
}, },
/// 停止监听 /// 停止监听
StopListener { StopListener { bind_port: u32 },
bind_port: u32,
},
} }
/// 监听器响应(Phase 13.4:线程通信) /// 监听器响应(Phase 13.4:线程通信)
#[derive(Debug)] #[derive(Debug)]
pub enum ListenerResponse { pub enum ListenerResponse {
/// Channel创建成功 /// Channel创建成功
ChannelCreated { ChannelCreated { bind_port: u32, channel_id: u32 },
bind_port: u32,
channel_id: u32,
},
/// 监听器停止 /// 监听器停止
ListenerStopped { ListenerStopped { bind_port: u32 },
bind_port: u32,
},
/// 错误 /// 错误
Error { Error { bind_port: u32, message: String },
bind_port: u32,
message: String,
},
} }
/// 端口转发监听器(Phase 13.4) /// 端口转发监听器(Phase 13.4)
@@ -73,26 +60,29 @@ impl PortForwardListener {
security_config: SshSecurityConfig, security_config: SshSecurityConfig,
) -> Result<Self> { ) -> Result<Self> {
info!("Creating port forward listener on port {}", bind_port); info!("Creating port forward listener on port {}", bind_port);
// Phase 13.4: 根据GatewayPorts决定绑定地址 // Phase 13.4: 根据GatewayPorts决定绑定地址
let bind_addr = if security_config.gateway_ports { let bind_addr = if security_config.gateway_ports {
format!("0.0.0.0:{}", bind_port) // 允许外部访问 format!("0.0.0.0:{}", bind_port) // 允许外部访问
} else { } else {
format!("127.0.0.1:{}", bind_port) // 只允许本地访问 format!("127.0.0.1:{}", bind_port) // 只允许本地访问
}; };
info!("Binding to address: {} (GatewayPorts={})", bind_addr, security_config.gateway_ports); info!(
"Binding to address: {} (GatewayPorts={})",
bind_addr, security_config.gateway_ports
);
let listener = TcpListener::bind(&bind_addr)?; let listener = TcpListener::bind(&bind_addr)?;
info!("Listener created successfully on {}", bind_addr); info!("Listener created successfully on {}", bind_addr);
// Phase 13.4: 创建线程通信channel // Phase 13.4: 创建线程通信channel
let (request_tx, request_rx) = mpsc::channel(); let (request_tx, _request_rx) = mpsc::channel();
let (response_tx, response_rx) = mpsc::channel(); let (_response_tx, response_rx) = mpsc::channel();
// Phase 13.4: 活动状态标记 // Phase 13.4: 活动状态标记
let active = Arc::new(Mutex::new(true)); let active = Arc::new(Mutex::new(true));
Ok(Self { Ok(Self {
bind_port, bind_port,
bind_address, bind_address,
@@ -103,38 +93,38 @@ impl PortForwardListener {
active, active,
}) })
} }
/// 启动监听线程(Phase 13.4) /// 启动监听线程(Phase 13.4)
pub fn start_listener_thread(&mut self) -> Result<()> { pub fn start_listener_thread(&mut self) -> Result<()> {
info!("Starting listener thread for port {}", self.bind_port); info!("Starting listener thread for port {}", self.bind_port);
let listener = self.listener.try_clone()?; let listener = self.listener.try_clone()?;
let bind_port = self.bind_port; let bind_port = self.bind_port;
let request_sender = self.request_sender.clone(); let request_sender = self.request_sender.clone();
let active = self.active.clone(); let active = self.active.clone();
// Phase 13.4: 创建独立监听线程 // Phase 13.4: 创建独立监听线程
thread::spawn(move || { thread::spawn(move || {
info!("Listener thread started for port {}", bind_port); info!("Listener thread started for port {}", bind_port);
while *active.lock().unwrap() { while *active.lock().unwrap() {
match listener.accept() { match listener.accept() {
Ok((stream, addr)) => { Ok((stream, addr)) => {
info!("New connection on port {}: {}", bind_port, addr); info!("New connection on port {}: {}", bind_port, addr);
// Phase 13.4: 发送新连接请求给主线程 // Phase 13.4: 发送新连接请求给主线程
let request = ListenerRequest::NewConnection { let request = ListenerRequest::NewConnection {
bind_port, bind_port,
originator_address: addr.ip().to_string(), originator_address: addr.ip().to_string(),
originator_port: addr.port() as u32, // Phase 13.4: u16转u32 originator_port: addr.port() as u32, // Phase 13.4: u16转u32
stream, stream,
}; };
if let Err(e) = request_sender.send(request) { if let Err(e) = request_sender.send(request) {
error!("Failed to send listener request: {}", e); error!("Failed to send listener request: {}", e);
break; break;
} }
info!("Listener request sent to main thread"); info!("Listener request sent to main thread");
} }
Err(e) => { Err(e) => {
@@ -145,32 +135,32 @@ impl PortForwardListener {
} }
} }
} }
info!("Listener thread stopped for port {}", bind_port); info!("Listener thread stopped for port {}", bind_port);
}); });
info!("Listener thread started successfully"); info!("Listener thread started successfully");
Ok(()) Ok(())
} }
/// 停止监听器(Phase 13.4) /// 停止监听器(Phase 13.4)
pub fn stop_listener(&mut self) -> Result<()> { pub fn stop_listener(&mut self) -> Result<()> {
info!("Stopping listener for port {}", self.bind_port); info!("Stopping listener for port {}", self.bind_port);
// Phase 13.4: 设置active=false,线程会自动退出 // Phase 13.4: 设置active=false,线程会自动退出
*self.active.lock().unwrap() = false; *self.active.lock().unwrap() = false;
info!("Listener stopped for port {}", self.bind_port); info!("Listener stopped for port {}", self.bind_port);
Ok(()) Ok(())
} }
/// 获取请求接收器(Phase 13.4) /// 获取请求接收器(Phase 13.4)
pub fn get_request_receiver(&self) -> mpsc::Receiver<ListenerRequest> { pub fn get_request_receiver(&self) -> mpsc::Receiver<ListenerRequest> {
// 注意:这里需要返回一个新的receiver,因为mpsc::Sender可以clone,但Receiver不能 // 注意:这里需要返回一个新的receiver,因为mpsc::Sender可以clone,但Receiver不能
// 实际应用中应该使用更复杂的channel设计 // 实际应用中应该使用更复杂的channel设计
unimplemented!("Use Arc<Mutex<mpsc::Receiver>> instead") unimplemented!("Use Arc<Mutex<mpsc::Receiver>> instead")
} }
/// 获取活动状态(Phase 13.4) /// 获取活动状态(Phase 13.4)
pub fn is_active(&self) -> bool { pub fn is_active(&self) -> bool {
*self.active.lock().unwrap() *self.active.lock().unwrap()
@@ -182,13 +172,19 @@ pub struct ListenerManager {
listeners: HashMap<u32, Arc<Mutex<PortForwardListener>>>, listeners: HashMap<u32, Arc<Mutex<PortForwardListener>>>,
} }
impl Default for ListenerManager {
fn default() -> Self {
Self::new()
}
}
impl ListenerManager { impl ListenerManager {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
listeners: HashMap::new(), listeners: HashMap::new(),
} }
} }
/// 创建并启动监听器(Phase 13.4) /// 创建并启动监听器(Phase 13.4)
pub fn create_listener( pub fn create_listener(
&mut self, &mut self,
@@ -197,21 +193,21 @@ impl ListenerManager {
security_config: SshSecurityConfig, security_config: SshSecurityConfig,
) -> Result<()> { ) -> Result<()> {
info!("Creating listener for port {}", bind_port); info!("Creating listener for port {}", bind_port);
let mut listener = PortForwardListener::new(bind_port, bind_address, security_config)?; let mut listener = PortForwardListener::new(bind_port, bind_address, security_config)?;
listener.start_listener_thread()?; listener.start_listener_thread()?;
let listener_arc = Arc::new(Mutex::new(listener)); let listener_arc = Arc::new(Mutex::new(listener));
self.listeners.insert(bind_port, listener_arc); self.listeners.insert(bind_port, listener_arc);
info!("Listener created and started for port {}", bind_port); info!("Listener created and started for port {}", bind_port);
Ok(()) Ok(())
} }
/// 停止监听器(Phase 13.4) /// 停止监听器(Phase 13.4)
pub fn stop_listener(&mut self, bind_port: u32) -> Result<()> { pub fn stop_listener(&mut self, bind_port: u32) -> Result<()> {
info!("Stopping listener for port {}", bind_port); info!("Stopping listener for port {}", bind_port);
if let Some(listener_arc) = self.listeners.remove(&bind_port) { if let Some(listener_arc) = self.listeners.remove(&bind_port) {
let mut listener = listener_arc.lock().unwrap(); let mut listener = listener_arc.lock().unwrap();
listener.stop_listener()?; listener.stop_listener()?;
@@ -219,28 +215,31 @@ impl ListenerManager {
} else { } else {
warn!("No listener found for port {}", bind_port); warn!("No listener found for port {}", bind_port);
} }
Ok(()) Ok(())
} }
/// 获取活动监听器数量(Phase 13.4) /// 获取活动监听器数量(Phase 13.4)
pub fn active_count(&self) -> usize { pub fn active_count(&self) -> usize {
self.listeners.values().filter(|l| l.lock().unwrap().is_active()).count() self.listeners
.values()
.filter(|l| l.lock().unwrap().is_active())
.count()
} }
} }
use std::collections::HashMap; // Phase 13.4: HashMap for listener management use std::collections::HashMap; // Phase 13.4: HashMap for listener management
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_listener_creation() { fn test_listener_creation() {
let security_config = SshSecurityConfig::enterprise_default(); let security_config = SshSecurityConfig::enterprise_default();
let listener = PortForwardListener::new(8080, "127.0.0.1".to_string(), security_config); let listener = PortForwardListener::new(8080, "127.0.0.1".to_string(), security_config);
// 注意:实际测试需要处理端口占用问题 // 注意:实际测试需要处理端口占用问题
assert!(listener.is_ok() || true); // 暂时跳过测试 assert!(listener.is_ok() || true); // 暂时跳过测试
} }
} }
+160 -60
View File
@@ -1,8 +1,8 @@
use std::path::PathBuf;
use anyhow::{Result, anyhow};
use log::{info, debug, warn};
use crate::vfs::{VfsBackend, VfsFile, VfsError};
use crate::vfs::open_flags::OpenFlags; use crate::vfs::open_flags::OpenFlags;
use crate::vfs::{VfsBackend, VfsFile};
use anyhow::{anyhow, Result};
use log::{debug, info, warn};
use std::path::PathBuf;
/// MPLEX_BASE from rsync io.h /// MPLEX_BASE from rsync io.h
const MPLEX_BASE: u32 = 7; const MPLEX_BASE: u32 = 7;
@@ -18,7 +18,9 @@ pub(crate) enum RsyncState {
WaitVersion, WaitVersion,
ReadFileList, ReadFileList,
/// Sum head (4 × write_int = 16 bytes) + checksum seed (4 bytes) = 20 bytes /// Sum head (4 × write_int = 16 bytes) + checksum seed (4 bytes) = 20 bytes
ReadSumHead { need: usize }, ReadSumHead {
need: usize,
},
SendSumCount, SendSumCount,
/// Raw file data from MSG_DATA packets /// Raw file data from MSG_DATA packets
ReadFileData, ReadFileData,
@@ -51,9 +53,16 @@ impl RsyncHandler {
let mut dest = String::new(); let mut dest = String::new();
for p in &parts[1..] { for p in &parts[1..] {
if *p == "--server" { is_server = true; continue; } if *p == "--server" {
if *p == "--sender" || p.starts_with('-') { continue; } is_server = true;
if *p == "." { continue; } continue;
}
if *p == "--sender" || p.starts_with('-') {
continue;
}
if *p == "." {
continue;
}
dest = p.to_string(); dest = p.to_string();
} }
@@ -107,8 +116,10 @@ impl RsyncHandler {
break; break;
} }
let header = u32::from_le_bytes([ let header = u32::from_le_bytes([
self.raw_input[0], self.raw_input[1], self.raw_input[0],
self.raw_input[2], self.raw_input[3], self.raw_input[1],
self.raw_input[2],
self.raw_input[3],
]); ]);
let raw_tag = ((header >> 24) & 0xFF) as u8; let raw_tag = ((header >> 24) & 0xFF) as u8;
let tag = raw_tag.wrapping_sub(MPLEX_BASE as u8); let tag = raw_tag.wrapping_sub(MPLEX_BASE as u8);
@@ -182,12 +193,17 @@ impl RsyncHandler {
RsyncState::WaitVersion => { RsyncState::WaitVersion => {
if self.rsync_input.len() >= 4 { if self.rsync_input.len() >= 4 {
let version = u32::from_le_bytes([ let version = u32::from_le_bytes([
self.rsync_input[0], self.rsync_input[1], self.rsync_input[0],
self.rsync_input[2], self.rsync_input[3], self.rsync_input[1],
self.rsync_input[2],
self.rsync_input[3],
]); ]);
self.rsync_input.drain(..4); self.rsync_input.drain(..4);
self.protocol_version = std::cmp::min(self.protocol_version, version); self.protocol_version = std::cmp::min(self.protocol_version, version);
info!("rsync: negotiated protocol version {}", self.protocol_version); info!(
"rsync: negotiated protocol version {}",
self.protocol_version
);
self.multiplex = self.protocol_version >= 30; self.multiplex = self.protocol_version >= 30;
self.transition(RsyncState::ReadFileList); self.transition(RsyncState::ReadFileList);
} else { } else {
@@ -197,7 +213,9 @@ impl RsyncHandler {
RsyncState::ReadFileList => { RsyncState::ReadFileList => {
loop { loop {
if self.rsync_input.is_empty() { break; } if self.rsync_input.is_empty() {
break;
}
let flags = self.rsync_input[0]; let flags = self.rsync_input[0];
if flags == 0 { if flags == 0 {
@@ -215,17 +233,25 @@ impl RsyncHandler {
let mut pos = 1; let mut pos = 1;
let _more_flags = if flags & 0x80 != 0 { let _more_flags = if flags & 0x80 != 0 {
if self.rsync_input.len() <= pos { break; } if self.rsync_input.len() <= pos {
break;
}
let ef = self.rsync_input[pos]; let ef = self.rsync_input[pos];
pos += 1; pos += 1;
ef ef
} else { 0 }; } else {
0
};
let has_name = !(flags & 0x02 != 0 && self.current_file > 0); let has_name = !(flags & 0x02 != 0 && self.current_file > 0);
if has_name { if has_name {
if let Some(nul_pos) = self.rsync_input[pos..].iter().position(|&b| b == 0) { if let Some(nul_pos) =
let name = String::from_utf8_lossy(&self.rsync_input[pos..pos + nul_pos]).to_string(); self.rsync_input[pos..].iter().position(|&b| b == 0)
{
let name =
String::from_utf8_lossy(&self.rsync_input[pos..pos + nul_pos])
.to_string();
pos += nul_pos + 1; pos += nul_pos + 1;
self.file_entries.push(name.clone()); self.file_entries.push(name.clone());
debug!("rsync: file entry: {}", name); debug!("rsync: file entry: {}", name);
@@ -269,24 +295,34 @@ impl RsyncHandler {
RsyncState::ReadSumHead { need } => { RsyncState::ReadSumHead { need } => {
if self.rsync_input.len() >= need { if self.rsync_input.len() >= need {
let sum_count = i32::from_le_bytes([ let sum_count = i32::from_le_bytes([
self.rsync_input[0], self.rsync_input[1], self.rsync_input[0],
self.rsync_input[2], self.rsync_input[3], self.rsync_input[1],
self.rsync_input[2],
self.rsync_input[3],
]); ]);
let _sum_blength = i32::from_le_bytes([ let _sum_blength = i32::from_le_bytes([
self.rsync_input[4], self.rsync_input[5], self.rsync_input[4],
self.rsync_input[6], self.rsync_input[7], self.rsync_input[5],
self.rsync_input[6],
self.rsync_input[7],
]); ]);
let _sum_s2length = i32::from_le_bytes([ let _sum_s2length = i32::from_le_bytes([
self.rsync_input[8], self.rsync_input[9], self.rsync_input[8],
self.rsync_input[10], self.rsync_input[11], self.rsync_input[9],
self.rsync_input[10],
self.rsync_input[11],
]); ]);
let _sum_remainder = i32::from_le_bytes([ let _sum_remainder = i32::from_le_bytes([
self.rsync_input[12], self.rsync_input[13], self.rsync_input[12],
self.rsync_input[14], self.rsync_input[15], self.rsync_input[13],
self.rsync_input[14],
self.rsync_input[15],
]); ]);
let checksum_seed = i32::from_le_bytes([ let checksum_seed = i32::from_le_bytes([
self.rsync_input[16], self.rsync_input[17], self.rsync_input[16],
self.rsync_input[18], self.rsync_input[19], self.rsync_input[17],
self.rsync_input[18],
self.rsync_input[19],
]); ]);
self.rsync_input.drain(..20); self.rsync_input.drain(..20);
@@ -308,7 +344,9 @@ impl RsyncHandler {
RsyncState::ReadFileData => { RsyncState::ReadFileData => {
let done_marker = b"RSYNCDONE"; let done_marker = b"RSYNCDONE";
if let Some(pos) = self.rsync_input.windows(done_marker.len()) if let Some(pos) = self
.rsync_input
.windows(done_marker.len())
.position(|w| w == done_marker) .position(|w| w == done_marker)
{ {
if pos > 0 { if pos > 0 {
@@ -323,8 +361,11 @@ impl RsyncHandler {
warn!("rsync flush error: {}", e); warn!("rsync flush error: {}", e);
} }
} }
info!("rsync: file {} complete ({} bytes written to {})", info!(
self.file_entries.get(self.current_file).unwrap_or(&"?".to_string()), "rsync: file {} complete ({} bytes written to {})",
self.file_entries
.get(self.current_file)
.unwrap_or(&"?".to_string()),
self.total_written, self.total_written,
self.dest_path.display(), self.dest_path.display(),
); );
@@ -332,8 +373,11 @@ impl RsyncHandler {
self.current_file += 1; self.current_file += 1;
if self.current_file >= self.file_entries.len() { if self.current_file >= self.file_entries.len() {
self.transition(RsyncState::Done); self.transition(RsyncState::Done);
info!("rsync ALL DONE: {} bytes written to {}", info!(
self.total_written, self.dest_path.display()); "rsync ALL DONE: {} bytes written to {}",
self.total_written,
self.dest_path.display()
);
} else { } else {
self.transition(RsyncState::ReadSumHead { need: 20 }); self.transition(RsyncState::ReadSumHead { need: 20 });
} }
@@ -360,7 +404,9 @@ impl RsyncHandler {
self.vfs.create_dir_all(parent, 0o755).ok(); self.vfs.create_dir_all(parent, 0o755).ok();
} }
let flags = OpenFlags::new().write().create().truncate(); let flags = OpenFlags::new().write().create().truncate();
let file = self.vfs.open_file(&self.dest_path, &flags) let file = self
.vfs
.open_file(&self.dest_path, &flags)
.map_err(|e| anyhow!("open error: {}", e))?; .map_err(|e| anyhow!("open error: {}", e))?;
self.output_file = Some(file); self.output_file = Some(file);
info!("rsync: opened {} for writing", self.dest_path.display()); info!("rsync: opened {} for writing", self.dest_path.display());
@@ -379,31 +425,43 @@ impl RsyncHandler {
/// Read rsync varint (LSB-first 7-bit groups, 0xFF prefix for negative) /// Read rsync varint (LSB-first 7-bit groups, 0xFF prefix for negative)
fn read_varint(buf: &[u8]) -> Option<(i32, usize)> { fn read_varint(buf: &[u8]) -> Option<(i32, usize)> {
if buf.is_empty() { return None; } if buf.is_empty() {
return None;
}
let mut pos = 0; let mut pos = 0;
let mut b = buf[pos]; let mut b = buf[pos];
pos += 1; pos += 1;
let neg = if b == 0xFF { let neg = if b == 0xFF {
if pos >= buf.len() { return None; } if pos >= buf.len() {
return None;
}
b = buf[pos]; b = buf[pos];
pos += 1; pos += 1;
true true
} else { false }; } else {
false
};
let mut x = (b & 0x7F) as i32; let mut x = (b & 0x7F) as i32;
let mut shift = 7; let mut shift = 7;
while b & 0x80 != 0 { while b & 0x80 != 0 {
if pos >= buf.len() { return None; } if pos >= buf.len() {
return None;
}
b = buf[pos]; b = buf[pos];
pos += 1; pos += 1;
x |= ((b & 0x7F) as i32) << shift; x |= ((b & 0x7F) as i32) << shift;
shift += 7; shift += 7;
} }
if neg { Some((-x, pos)) } else { Some((x, pos)) } if neg {
Some((-x, pos))
} else {
Some((x, pos))
}
} }
#[cfg(test)] #[cfg(test)]
@@ -419,8 +477,9 @@ mod tests {
fn test_parse_command() { fn test_parse_command() {
let h = RsyncHandler::parse_rsync_command( let h = RsyncHandler::parse_rsync_command(
"rsync --server -g -l -o -p -D -r -t -v --dirs . /tmp/upload.bin", "rsync --server -g -l -o -p -D -r -t -v --dirs . /tmp/upload.bin",
make_vfs() make_vfs(),
).unwrap(); )
.unwrap();
assert_eq!(h.dest_path, PathBuf::from("/tmp/upload.bin")); assert_eq!(h.dest_path, PathBuf::from("/tmp/upload.bin"));
} }
@@ -428,14 +487,16 @@ mod tests {
fn test_parse_command_sender() { fn test_parse_command_sender() {
let h = RsyncHandler::parse_rsync_command( let h = RsyncHandler::parse_rsync_command(
"rsync --server --sender -vlogDtprz . /home/user/file.txt", "rsync --server --sender -vlogDtprz . /home/user/file.txt",
make_vfs() make_vfs(),
).unwrap(); )
.unwrap();
assert_eq!(h.dest_path, PathBuf::from("/home/user/file.txt")); assert_eq!(h.dest_path, PathBuf::from("/home/user/file.txt"));
} }
#[test] #[test]
fn test_version_exchange() { fn test_version_exchange() {
let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/test.bin", make_vfs()).unwrap(); let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/test.bin", make_vfs())
.unwrap();
let output = h.drain_output(); let output = h.drain_output();
assert_eq!(output, b"\x1e\x00\x00\x00"); assert_eq!(output, b"\x1e\x00\x00\x00");
assert_eq!(h.state, RsyncState::WaitVersion); assert_eq!(h.state, RsyncState::WaitVersion);
@@ -447,7 +508,8 @@ mod tests {
#[test] #[test]
fn test_version_negotiate_down() { fn test_version_negotiate_down() {
let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/test.bin", make_vfs()).unwrap(); let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/test.bin", make_vfs())
.unwrap();
let _ = h.drain_output(); let _ = h.drain_output();
h.feed(b"\x1d\x00\x00\x00").unwrap(); h.feed(b"\x1d\x00\x00\x00").unwrap();
assert_eq!(h.protocol_version, 29); assert_eq!(h.protocol_version, 29);
@@ -464,26 +526,33 @@ mod tests {
#[test] #[test]
fn test_file_list_multiplex() { fn test_file_list_multiplex() {
let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/rsync_test.bin", make_vfs()).unwrap(); let mut h =
RsyncHandler::parse_rsync_command("rsync --server . /tmp/rsync_test.bin", make_vfs())
.unwrap();
let _ = h.drain_output(); let _ = h.drain_output();
h.feed(b"\x1e\x00\x00\x00").unwrap(); h.feed(b"\x1e\x00\x00\x00").unwrap();
assert!(h.multiplex); assert!(h.multiplex);
let mut flist = Vec::new(); let mut flist = Vec::new();
// File list: flags=1 (has name), then name with NUL terminator // File list: flags=1 (has name), then name with NUL terminator
flist.push(1); // flags: has name flist.push(1); // flags: has name
flist.extend_from_slice(b"test.txt"); flist.extend_from_slice(b"test.txt");
flist.push(0); // name terminator flist.push(0); // name terminator
fn write_varint(buf: &mut Vec<u8>, val: i32) { fn write_varint(buf: &mut Vec<u8>, val: i32) {
if val == 0 { buf.push(0); return; } if val == 0 {
buf.push(0);
return;
}
if val < 0 { if val < 0 {
buf.push(0xFF); buf.push(0xFF);
let mut v = (-val) as u32; let mut v = (-val) as u32;
while v > 0 { while v > 0 {
let mut byte = (v & 0x7F) as u8; let mut byte = (v & 0x7F) as u8;
v >>= 7; v >>= 7;
if v > 0 { byte |= 0x80; } if v > 0 {
byte |= 0x80;
}
buf.push(byte); buf.push(byte);
} }
} else { } else {
@@ -491,7 +560,9 @@ mod tests {
while v > 0 { while v > 0 {
let mut byte = (v & 0x7F) as u8; let mut byte = (v & 0x7F) as u8;
v >>= 7; v >>= 7;
if v > 0 { byte |= 0x80; } if v > 0 {
byte |= 0x80;
}
buf.push(byte); buf.push(byte);
} }
} }
@@ -502,7 +573,7 @@ mod tests {
write_varint(&mut flist, 1700000000); write_varint(&mut flist, 1700000000);
write_varint(&mut flist, 100); write_varint(&mut flist, 100);
write_varint(&mut flist, 0); write_varint(&mut flist, 0);
flist.push(0); // file list end marker flist.push(0); // file list end marker
let mut sum_head = Vec::new(); let mut sum_head = Vec::new();
sum_head.extend_from_slice(&0i32.to_le_bytes()); sum_head.extend_from_slice(&0i32.to_le_bytes());
@@ -527,22 +598,51 @@ mod tests {
#[test] #[test]
fn test_file_data_multiplex() { fn test_file_data_multiplex() {
let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/rsync_test.bin", make_vfs()).unwrap(); let mut h =
RsyncHandler::parse_rsync_command("rsync --server . /tmp/rsync_test.bin", make_vfs())
.unwrap();
let _ = h.drain_output(); let _ = h.drain_output();
h.feed(b"\x1e\x00\x00\x00").unwrap(); h.feed(b"\x1e\x00\x00\x00").unwrap();
let mut flist = Vec::new(); let mut flist = Vec::new();
flist.push(1); // flags: has name flist.push(1); // flags: has name
flist.extend_from_slice(b"test.bin"); flist.extend_from_slice(b"test.bin");
flist.push(0); flist.push(0);
fn wv(buf: &mut Vec<u8>, val: i32) { fn wv(buf: &mut Vec<u8>, val: i32) {
if val == 0 { buf.push(0); return; } if val == 0 {
if val < 0 { buf.push(0xFF); let mut v = (-val) as u32; while v > 0 { let mut byte = (v & 0x7F) as u8; v >>= 7; if v > 0 { byte |= 0x80; } buf.push(byte); } } buf.push(0);
else { let mut v = val as u32; while v > 0 { let mut byte = (v & 0x7F) as u8; v >>= 7; if v > 0 { byte |= 0x80; } buf.push(byte); } } return;
}
if val < 0 {
buf.push(0xFF);
let mut v = (-val) as u32;
while v > 0 {
let mut byte = (v & 0x7F) as u8;
v >>= 7;
if v > 0 {
byte |= 0x80;
}
buf.push(byte);
}
} else {
let mut v = val as u32;
while v > 0 {
let mut byte = (v & 0x7F) as u8;
v >>= 7;
if v > 0 {
byte |= 0x80;
}
buf.push(byte);
}
}
} }
wv(&mut flist, 33188); wv(&mut flist, 501); wv(&mut flist, 20); wv(&mut flist, 33188);
wv(&mut flist, 1700000000); wv(&mut flist, 100); wv(&mut flist, 0); wv(&mut flist, 501);
flist.push(0); // file list end wv(&mut flist, 20);
wv(&mut flist, 1700000000);
wv(&mut flist, 100);
wv(&mut flist, 0);
flist.push(0); // file list end
h.feed(&build_multiplex(&flist)).unwrap(); h.feed(&build_multiplex(&flist)).unwrap();
let mut sh = Vec::new(); let mut sh = Vec::new();
+58 -30
View File
@@ -1,13 +1,12 @@
// SCP协议实现(Phase 8) // SCP协议实现(Phase 8)
// 参考OpenSSH scp.c源码 // 参考OpenSSH scp.c源码
use crate::vfs::{VfsBackend, VfsFile, VfsError, VfsStat};
use crate::vfs::open_flags::OpenFlags; use crate::vfs::open_flags::OpenFlags;
use anyhow::{Result, anyhow}; use crate::vfs::{VfsBackend, VfsFile, VfsStat};
use log::{info, warn, debug}; use anyhow::{anyhow, Result};
use log::{debug, info, warn};
use std::io::{BufRead, Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::io::{Read, Write, BufRead};
use std::time::SystemTime;
/// SCP Handler(参考OpenSSH scp.c) /// SCP Handler(参考OpenSSH scp.c)
pub struct ScpHandler { pub struct ScpHandler {
@@ -38,13 +37,13 @@ impl ScpHandler {
/// 解析SCP命令(参考OpenSSH scp.c: parse_command()) /// 解析SCP命令(参考OpenSSH scp.c: parse_command())
pub fn parse_scp_command(command: &str, vfs: Box<dyn VfsBackend>) -> Result<Self> { pub fn parse_scp_command(command: &str, vfs: Box<dyn VfsBackend>) -> Result<Self> {
let parts: Vec<&str> = command.split_whitespace().collect(); let parts: Vec<&str> = command.split_whitespace().collect();
if parts.len() < 2 || parts[0] != "scp" { if parts.len() < 2 || parts[0] != "scp" {
return Err(anyhow!("Invalid SCP command: {}", command)); return Err(anyhow!("Invalid SCP command: {}", command));
} }
let mut handler = ScpHandler::new(PathBuf::from("/tmp"), vfs); let mut handler = ScpHandler::new(PathBuf::from("/tmp"), vfs);
for part in &parts[1..] { for part in &parts[1..] {
match part { match part {
&"-f" => handler.mode = ScpMode::Source, &"-f" => handler.mode = ScpMode::Source,
@@ -71,10 +70,15 @@ impl ScpHandler {
/// SCP Source Mode(scp -f,发送文件) /// SCP Source Mode(scp -f,发送文件)
fn handle_source_mode(&self, channel: &mut dyn ReadWrite) -> Result<()> { fn handle_source_mode(&self, channel: &mut dyn ReadWrite) -> Result<()> {
info!("SCP source mode: sending files from {}", self.root_dir.display()); info!(
"SCP source mode: sending files from {}",
self.root_dir.display()
);
let full_path = self.resolve_path(&self.root_dir.to_string_lossy())?; let full_path = self.resolve_path(&self.root_dir.to_string_lossy())?;
let stat = self.vfs.stat(&full_path) let stat = self
.vfs
.stat(&full_path)
.map_err(|e| anyhow!("stat error: {}", e))?; .map_err(|e| anyhow!("stat error: {}", e))?;
if stat.is_dir { if stat.is_dir {
@@ -91,16 +95,19 @@ impl ScpHandler {
/// SCP Destination Mode(scp -t,接收文件) /// SCP Destination Mode(scp -t,接收文件)
fn handle_destination_mode(&mut self, channel: &mut dyn ReadWrite) -> Result<()> { fn handle_destination_mode(&mut self, channel: &mut dyn ReadWrite) -> Result<()> {
info!("SCP destination mode: receiving files to {}", self.root_dir.display()); info!(
"SCP destination mode: receiving files to {}",
self.root_dir.display()
);
channel.write_all(&[0])?; channel.write_all(&[0])?;
channel.flush()?; channel.flush()?;
let mut buffer = String::new(); let mut buffer = String::new();
loop { loop {
buffer.clear(); buffer.clear();
let mut reader = std::io::BufReader::new(&mut *channel); let mut reader = std::io::BufReader::new(&mut *channel);
match reader.read_line(&mut buffer)? { match reader.read_line(&mut buffer)? {
0 => break, 0 => break,
@@ -130,7 +137,9 @@ impl ScpHandler {
/// 发送文件(参考OpenSSH scp.c: source()) /// 发送文件(参考OpenSSH scp.c: source())
fn send_file(&self, channel: &mut dyn ReadWrite, path: &Path) -> Result<()> { fn send_file(&self, channel: &mut dyn ReadWrite, path: &Path) -> Result<()> {
let stat = self.vfs.stat(path) let stat = self
.vfs
.stat(path)
.map_err(|e| anyhow!("stat error: {}", e))?; .map_err(|e| anyhow!("stat error: {}", e))?;
let size = stat.size; let size = stat.size;
let filename = path.file_name().unwrap().to_string_lossy(); let filename = path.file_name().unwrap().to_string_lossy();
@@ -146,13 +155,16 @@ impl ScpHandler {
} }
let flags = OpenFlags::new().read(); let flags = OpenFlags::new().read();
let mut file = self.vfs.open_file(path, &flags) let mut file = self
.vfs
.open_file(path, &flags)
.map_err(|e| anyhow!("open error: {}", e))?; .map_err(|e| anyhow!("open error: {}", e))?;
let mut buffer = vec![0u8; 8192]; let mut buffer = vec![0u8; 8192];
loop { loop {
let n = file.read(&mut buffer) let n = file
.read(&mut buffer)
.map_err(|e| anyhow!("read error: {}", e))?; .map_err(|e| anyhow!("read error: {}", e))?;
if n == 0 { if n == 0 {
break; break;
@@ -188,7 +200,9 @@ impl ScpHandler {
return Err(anyhow!("SCP directory command rejected")); return Err(anyhow!("SCP directory command rejected"));
} }
let entries = self.vfs.read_dir(path) let entries = self
.vfs
.read_dir(path)
.map_err(|e| anyhow!("read_dir error: {}", e))?; .map_err(|e| anyhow!("read_dir error: {}", e))?;
for entry in &entries { for entry in &entries {
@@ -218,7 +232,7 @@ impl ScpHandler {
/// 处理文件命令(C0644 size filename) /// 处理文件命令(C0644 size filename)
fn handle_file_command(&self, channel: &mut dyn ReadWrite, command: &str) -> Result<()> { fn handle_file_command(&self, channel: &mut dyn ReadWrite, command: &str) -> Result<()> {
let parts: Vec<&str> = command.split_whitespace().collect(); let parts: Vec<&str> = command.split_whitespace().collect();
if parts.len() != 3 { if parts.len() != 3 {
return self.send_error(channel, "Invalid file command format"); return self.send_error(channel, "Invalid file command format");
} }
@@ -227,7 +241,10 @@ impl ScpHandler {
let size: u64 = parts[1].parse()?; let size: u64 = parts[1].parse()?;
let filename = parts[2]; let filename = parts[2];
debug!("SCP receive file: mode={}, size={}, name={}", mode_str, size, filename); debug!(
"SCP receive file: mode={}, size={}, name={}",
mode_str, size, filename
);
if size > 1024 * 1024 * 1024 { if size > 1024 * 1024 * 1024 {
return self.send_error(channel, "File too large (max 1GB)"); return self.send_error(channel, "File too large (max 1GB)");
@@ -236,7 +253,9 @@ impl ScpHandler {
let full_path = self.resolve_path(filename)?; let full_path = self.resolve_path(filename)?;
let flags = OpenFlags::new().write().create().truncate(); let flags = OpenFlags::new().write().create().truncate();
let mut file = self.vfs.open_file(&full_path, &flags) let mut file = self
.vfs
.open_file(&full_path, &flags)
.map_err(|e| anyhow!("open error: {}", e))?; .map_err(|e| anyhow!("open error: {}", e))?;
channel.write_all(&[0])?; channel.write_all(&[0])?;
@@ -263,7 +282,8 @@ impl ScpHandler {
if mode_int != 0 { if mode_int != 0 {
let mut set_stat = VfsStat::new(); let mut set_stat = VfsStat::new();
set_stat.mode = mode_int; set_stat.mode = mode_int;
self.vfs.set_stat(&full_path, &set_stat) self.vfs
.set_stat(&full_path, &set_stat)
.map_err(|e| anyhow!("set_stat error: {}", e))?; .map_err(|e| anyhow!("set_stat error: {}", e))?;
} }
@@ -280,7 +300,7 @@ impl ScpHandler {
/// 处理目录命令(D0755 0 dirname) /// 处理目录命令(D0755 0 dirname)
fn handle_directory_command(&self, channel: &mut dyn ReadWrite, command: &str) -> Result<()> { fn handle_directory_command(&self, channel: &mut dyn ReadWrite, command: &str) -> Result<()> {
let parts: Vec<&str> = command.split_whitespace().collect(); let parts: Vec<&str> = command.split_whitespace().collect();
if parts.len() != 3 { if parts.len() != 3 {
return self.send_error(channel, "Invalid directory command format"); return self.send_error(channel, "Invalid directory command format");
} }
@@ -297,7 +317,8 @@ impl ScpHandler {
let full_path = self.resolve_path(dirname)?; let full_path = self.resolve_path(dirname)?;
let mode_int: u32 = mode_str.parse()?; let mode_int: u32 = mode_str.parse()?;
self.vfs.create_dir_all(&full_path, mode_int) self.vfs
.create_dir_all(&full_path, mode_int)
.map_err(|e| anyhow!("create_dir_all error: {}", e))?; .map_err(|e| anyhow!("create_dir_all error: {}", e))?;
channel.write_all(&[0])?; channel.write_all(&[0])?;
@@ -326,7 +347,7 @@ impl ScpHandler {
} }
let parts: Vec<&str> = command.split_whitespace().collect(); let parts: Vec<&str> = command.split_whitespace().collect();
if parts.len() != 3 { if parts.len() != 3 {
return self.send_error(channel, "Invalid time command format"); return self.send_error(channel, "Invalid time command format");
} }
@@ -353,11 +374,15 @@ impl ScpHandler {
/// 路径解析(安全性检查) /// 路径解析(安全性检查)
fn resolve_path(&self, path: &str) -> Result<PathBuf> { fn resolve_path(&self, path: &str) -> Result<PathBuf> {
let full_path = self.root_dir.join(path); let full_path = self.root_dir.join(path);
let canonical_path = self.vfs.real_path(&full_path) let canonical_path = self
.vfs
.real_path(&full_path)
.map_err(|e| anyhow!("Path resolution error: {}", e))?; .map_err(|e| anyhow!("Path resolution error: {}", e))?;
let root_canonical = self.vfs.real_path(&self.root_dir) let root_canonical = self
.vfs
.real_path(&self.root_dir)
.map_err(|e| anyhow!("Root path resolution error: {}", e))?; .map_err(|e| anyhow!("Root path resolution error: {}", e))?;
if !canonical_path.starts_with(&root_canonical) { if !canonical_path.starts_with(&root_canonical) {
@@ -383,20 +408,23 @@ mod tests {
#[test] #[test]
fn test_scp_command_parse() { fn test_scp_command_parse() {
let handler = ScpHandler::parse_scp_command("scp -t /tmp", Box::new(LocalFs::new())).unwrap(); let handler =
ScpHandler::parse_scp_command("scp -t /tmp", Box::new(LocalFs::new())).unwrap();
assert_eq!(handler.mode, ScpMode::Destination); assert_eq!(handler.mode, ScpMode::Destination);
assert_eq!(handler.root_dir, PathBuf::from("/tmp")); assert_eq!(handler.root_dir, PathBuf::from("/tmp"));
} }
#[test] #[test]
fn test_scp_recursive_parse() { fn test_scp_recursive_parse() {
let handler = ScpHandler::parse_scp_command("scp -r -t /tmp", Box::new(LocalFs::new())).unwrap(); let handler =
ScpHandler::parse_scp_command("scp -r -t /tmp", Box::new(LocalFs::new())).unwrap();
assert!(handler.recursive); assert!(handler.recursive);
} }
#[test] #[test]
fn test_scp_source_parse() { fn test_scp_source_parse() {
let handler = ScpHandler::parse_scp_command("scp -f /tmp", Box::new(LocalFs::new())).unwrap(); let handler =
ScpHandler::parse_scp_command("scp -f /tmp", Box::new(LocalFs::new())).unwrap();
assert_eq!(handler.mode, ScpMode::Source); assert_eq!(handler.mode, ScpMode::Source);
} }
} }
+240 -172
View File
@@ -1,32 +1,32 @@
// SSH服务器完整实现(Phase 1-7集成版 + Phase 13端口转发) // SSH服务器完整实现(Phase 1-7集成版 + Phase 13端口转发)
// 参考OpenSSH sshd.c: complete SSH/SFTP flow + port forwarding // 参考OpenSSH sshd.c: complete SSH/SFTP flow + port forwarding
use crate::ssh_server::version::VersionExchange;
use crate::ssh_server::packet::{SshPacket, PacketType};
use crate::ssh_server::kex::{KexResult, KexProposal};
use crate::ssh_server::kex_complete::{KexState};
use crate::ssh_server::auth::{AuthHandler, AuthResult};
use crate::provider::sqlite::SqliteProvider;
use crate::provider::pg::PgProvider; use crate::provider::pg::PgProvider;
use crate::provider::sqlite::SqliteProvider;
use crate::provider::DataProvider; use crate::provider::DataProvider;
use crate::ssh_server::channel::{ChannelManager}; use crate::ssh_server::auth::{AuthHandler, AuthResult};
use crate::ssh_server::cipher::{EncryptionContext, EncryptedPacket}; use crate::ssh_server::channel::ChannelManager;
use crate::ssh_server::ssh_security_config::SshSecurityConfig; // Phase 13.1 use crate::ssh_server::cipher::{EncryptedPacket, EncryptionContext};
use crate::ssh_server::port_forward::PortForwardManager; // Phase 13 use crate::ssh_server::kex::{KexProposal, KexResult};
use anyhow::{Result, anyhow}; use crate::ssh_server::kex_complete::KexState;
use log::{info, warn, error, debug}; use crate::ssh_server::packet::{PacketType, SshPacket};
use crate::ssh_server::port_forward::PortForwardManager; // Phase 13
use crate::ssh_server::ssh_security_config::SshSecurityConfig; // Phase 13.1
use crate::ssh_server::version::VersionExchange;
use anyhow::{anyhow, Result};
use log::{error, info, warn};
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream}; use std::net::{TcpListener, TcpStream};
use std::path::PathBuf; use std::path::PathBuf;
use std::thread; use std::sync::{Arc, Mutex};
use std::io::{Read, Write}; use std::thread; // Phase 13: 端口转发线程同步
use std::sync::{Arc, Mutex}; // Phase 13: 端口转发线程同步
/// SSH服务器配置(Phase 13.1企业级安全配置) /// SSH服务器配置(Phase 13.1企业级安全配置)
pub struct SshServerConfig { pub struct SshServerConfig {
pub port: u16, pub port: u16,
pub bind_address: String, pub bind_address: String,
pub security_config: SshSecurityConfig, // Phase 13.1: 企业级安全配置 pub security_config: SshSecurityConfig, // Phase 13.1: 企业级安全配置
pub pg_conn: Option<String>, // PostgreSQL连接字符串(SFTPGo兼容认证) pub pg_conn: Option<String>, // PostgreSQL连接字符串(SFTPGo兼容认证)
} }
impl Default for SshServerConfig { impl Default for SshServerConfig {
@@ -34,7 +34,7 @@ impl Default for SshServerConfig {
Self { Self {
port: 2024, port: 2024,
bind_address: "127.0.0.1".to_string(), bind_address: "127.0.0.1".to_string(),
security_config: SshSecurityConfig::enterprise_default(), // Phase 13.1 security_config: SshSecurityConfig::enterprise_default(), // Phase 13.1
pg_conn: None, pg_conn: None,
} }
} }
@@ -56,43 +56,48 @@ impl SshServerConfig {
/// SSH服务器主结构(Phase 1-13完整版) /// SSH服务器主结构(Phase 1-13完整版)
pub struct SshServer { pub struct SshServer {
config: SshServerConfig, config: SshServerConfig,
security_config: Arc<Mutex<SshSecurityConfig>>, // Phase 13.1: 共享安全配置 security_config: Arc<Mutex<SshSecurityConfig>>, // Phase 13.1: 共享安全配置
} }
impl SshServer { impl SshServer {
pub fn new(config: SshServerConfig) -> Self { pub fn new(config: SshServerConfig) -> Self {
let security_config = Arc::new(Mutex::new(config.security_config.clone())); // Phase 13.1: 先clone let security_config = Arc::new(Mutex::new(config.security_config.clone())); // Phase 13.1: 先clone
Self { Self {
config, config,
security_config, // Phase 13.1 security_config, // Phase 13.1
} }
} }
pub fn run(&self) -> Result<()> { pub fn run(&self) -> Result<()> {
let bind_addr = format!("{}:{}", self.config.bind_address, self.config.port); let bind_addr = format!("{}:{}", self.config.bind_address, self.config.port);
let listener = TcpListener::bind(&bind_addr)?; let listener = TcpListener::bind(&bind_addr)?;
info!("MarkBaseSSH server listening on {}", bind_addr); info!("MarkBaseSSH server listening on {}", bind_addr);
info!("Implementation: Complete SSH/SFTP + Port Forwarding (Phase 1-13)"); info!("Implementation: Complete SSH/SFTP + Port Forwarding (Phase 1-13)");
info!("Security config: GatewayPorts={}, PermitOpen={:?}, MaxSessions={}", info!(
"Security config: GatewayPorts={}, PermitOpen={:?}, MaxSessions={}",
self.config.security_config.gateway_ports, self.config.security_config.gateway_ports,
self.config.security_config.permit_open, self.config.security_config.permit_open,
self.config.security_config.max_sessions); self.config.security_config.max_sessions
);
let security_config = self.security_config.clone(); // Phase 13.1: 共享安全配置
let security_config = self.security_config.clone(); // Phase 13.1: 共享安全配置
let pg_conn = self.config.pg_conn.clone(); let pg_conn = self.config.pg_conn.clone();
for stream in listener.incoming() { for stream in listener.incoming() {
match stream { match stream {
Ok(stream) => { Ok(stream) => {
let client_addr = stream.peer_addr()?; let client_addr = stream.peer_addr()?;
info!("New SSH connection from {}", client_addr); info!("New SSH connection from {}", client_addr);
let security_config_clone = security_config.clone(); // Phase 13.1 let security_config_clone = security_config.clone(); // Phase 13.1
let pg_conn_clone = pg_conn.clone(); let pg_conn_clone = pg_conn.clone();
thread::spawn(move || { thread::spawn(move || {
if let Err(e) = handle_connection_complete(stream, security_config_clone, pg_conn_clone) { // Phase 13.1 if let Err(e) =
handle_connection_complete(stream, security_config_clone, pg_conn_clone)
{
// Phase 13.1
error!("Connection error: {}", e); error!("Connection error: {}", e);
} }
}); });
@@ -102,90 +107,127 @@ impl SshServer {
} }
} }
} }
Ok(()) Ok(())
} }
} }
/// 处理完整SSH连接(Phase 1-13完整流程) /// 处理完整SSH连接(Phase 1-13完整流程)
fn handle_connection_complete(stream: TcpStream, security_config: Arc<Mutex<SshSecurityConfig>>, pg_conn: Option<String>) -> Result<()> { fn handle_connection_complete(
stream: TcpStream,
security_config: Arc<Mutex<SshSecurityConfig>>,
pg_conn: Option<String>,
) -> Result<()> {
info!("Handling client connection (Phase 1-13 complete flow with port forwarding)"); info!("Handling client connection (Phase 1-13 complete flow with port forwarding)");
// Phase 13.1: 增加活动会话数 // Phase 13.1: 增加活动会话数
{ {
let mut security = security_config.lock().unwrap(); let mut security = security_config.lock().unwrap();
security.increment_sessions()?; security.increment_sessions()?;
} }
let mut stream = stream; let mut stream = stream;
// Phase 1: 版本交换 // Phase 1: 版本交换
let client_version = VersionExchange::exchange(&mut stream)?; let client_version = VersionExchange::exchange(&mut stream)?;
info!("Version exchange: client={}, server=SSH-2.0-MarkBaseSSH_1.0", client_version); info!(
"Version exchange: client={}, server=SSH-2.0-MarkBaseSSH_1.0",
client_version
);
// Phase 2: 箋法协商 // Phase 2: 箋法协商
let (kex_result, server_kexinit, client_kexinit) = perform_kex_negotiation_complete(&mut stream)?; let (kex_result, server_kexinit, client_kexinit) =
info!("KEX negotiation: KEX={}, Cipher={}", kex_result.kex_algorithm, kex_result.encryption_ctos); perform_kex_negotiation_complete(&mut stream)?;
info!(
"KEX negotiation: KEX={}, Cipher={}",
kex_result.kex_algorithm, kex_result.encryption_ctos
);
// Phase 3: 密钥交换完整流程 // Phase 3: 密钥交换完整流程
let mut encryption_ctx = perform_complete_kex_exchange(&mut stream, client_version.clone(), kex_result, server_kexinit, client_kexinit)?; let mut encryption_ctx = perform_complete_kex_exchange(
&mut stream,
client_version.clone(),
kex_result,
server_kexinit,
client_kexinit,
)?;
info!("Key exchange completed, encryption channel ready"); info!("Key exchange completed, encryption channel ready");
// Phase 5: SSH认证(SFTPGo兼容 — PostgreSQL或SQLite) // Phase 5: SSH认证(SFTPGo兼容 — PostgreSQL或SQLite)
let provider: Box<dyn DataProvider> = if let Some(ref conn_str) = pg_conn { let provider: Box<dyn DataProvider> = if let Some(ref conn_str) = pg_conn {
info!("Using PostgreSQL auth provider (SFTPGo-compatible): {}", conn_str); info!(
Box::new(PgProvider::new(conn_str) "Using PostgreSQL auth provider (SFTPGo-compatible): {}",
.map_err(|e| anyhow!("Failed to init PgProvider: {}", e))?) conn_str
);
Box::new(
PgProvider::new(conn_str).map_err(|e| anyhow!("Failed to init PgProvider: {}", e))?,
)
} else { } else {
info!("Using SQLite auth provider"); info!("Using SQLite auth provider");
Box::new(SqliteProvider::new("data/auth.sqlite") Box::new(
.map_err(|e| anyhow!("Failed to init SqliteProvider: {}", e))?) SqliteProvider::new("data/auth.sqlite")
.map_err(|e| anyhow!("Failed to init SqliteProvider: {}", e))?,
)
}; };
let mut auth_handler = AuthHandler::new(provider); let mut auth_handler = AuthHandler::new(provider);
let auth_user = perform_ssh_auth(&mut stream, &mut auth_handler, &mut encryption_ctx)?; let auth_user = perform_ssh_auth(&mut stream, &mut auth_handler, &mut encryption_ctx)?;
info!("SSH authentication succeeded: user={}", auth_user.username); info!("SSH authentication succeeded: user={}", auth_user.username);
// Phase 6: SSH Channel管理(参考OpenSSH channel.c) // Phase 6: SSH Channel管理(参考OpenSSH channel.c)
let mut channel_manager = ChannelManager::new(auth_user.home_dir.clone()); let mut channel_manager = ChannelManager::new(auth_user.home_dir.clone());
// Phase 13: PortForwardManager初始化 // Phase 13: PortForwardManager初始化
let mut port_forward_manager = PortForwardManager::new(); let mut port_forward_manager = PortForwardManager::new();
// Phase 6-13: SSH服务循环(处理channel请求 + 端口转发) // Phase 6-13: SSH服务循环(处理channel请求 + 端口转发)
let security_config_clone = security_config.clone(); // Phase 13.1: clone for service loop let security_config_clone = security_config.clone(); // Phase 13.1: clone for service loop
handle_ssh_service_loop(&mut stream, &mut channel_manager, &mut encryption_ctx, &mut port_forward_manager, security_config_clone)?; handle_ssh_service_loop(
&mut stream,
&mut channel_manager,
&mut encryption_ctx,
&mut port_forward_manager,
security_config_clone,
)?;
info!("SSH session completed successfully"); info!("SSH session completed successfully");
// Phase 13.1: 减少活动会话数 // Phase 13.1: 减少活动会话数
{ {
let mut security = security_config.lock().unwrap(); let mut security = security_config.lock().unwrap();
security.decrement_sessions(); security.decrement_sessions();
} }
Ok(()) Ok(())
} }
/// 完整算法协商(返回KEXINIT payloads) /// 完整算法协商(返回KEXINIT payloads)
fn perform_kex_negotiation_complete(stream: &mut TcpStream) -> Result<(KexResult, SshPacket, SshPacket)> { fn perform_kex_negotiation_complete(
stream: &mut TcpStream,
) -> Result<(KexResult, SshPacket, SshPacket)> {
info!("Starting complete KEX negotiation"); info!("Starting complete KEX negotiation");
// 1. 发送服务器KEXINIT // 1. 发送服务器KEXINIT
let server_proposal = KexProposal::server_default(); let server_proposal = KexProposal::server_default();
let server_kexinit = server_proposal.to_kexinit_packet()?; let server_kexinit = server_proposal.to_kexinit_packet()?;
server_kexinit.write(stream)?; server_kexinit.write(stream)?;
info!("Sent server KEXINIT (payload size: {} bytes)", server_kexinit.payload.len()); info!(
"Sent server KEXINIT (payload size: {} bytes)",
server_kexinit.payload.len()
);
// 2. 接收客户端KEXINIT // 2. 接收客户端KEXINIT
let client_kexinit = SshPacket::read(stream)?; let client_kexinit = SshPacket::read(stream)?;
let client_proposal = KexProposal::from_kexinit_packet(&client_kexinit)?; let client_proposal = KexProposal::from_kexinit_packet(&client_kexinit)?;
info!("Received client KEXINIT (payload size: {} bytes)", client_kexinit.payload.len()); info!(
"Received client KEXINIT (payload size: {} bytes)",
client_kexinit.payload.len()
);
// 3. 算法匹配 // 3. 算法匹配
let kex_result = KexResult::choose_algorithms(&server_proposal, &client_proposal)?; let kex_result = KexResult::choose_algorithms(&server_proposal, &client_proposal)?;
Ok((kex_result, server_kexinit, client_kexinit)) Ok((kex_result, server_kexinit, client_kexinit))
} }
@@ -198,18 +240,18 @@ fn perform_complete_kex_exchange(
client_kexinit: SshPacket, client_kexinit: SshPacket,
) -> Result<EncryptionContext> { ) -> Result<EncryptionContext> {
info!("Starting complete key exchange flow"); info!("Starting complete key exchange flow");
let mut kex_state = KexState::new( let mut kex_state = KexState::new(
client_version, client_version,
"SSH-2.0-MarkBaseSSH_1.0".to_string(), "SSH-2.0-MarkBaseSSH_1.0".to_string(),
kex_result, kex_result,
)?; )?;
kex_state.save_kexinit_payloads(&client_kexinit, &server_kexinit); kex_state.save_kexinit_payloads(&client_kexinit, &server_kexinit);
let kexdh_init = SshPacket::read(stream)?; let kexdh_init = SshPacket::read(stream)?;
info!("Received SSH_MSG_KEX_ECDH_INIT"); info!("Received SSH_MSG_KEX_ECDH_INIT");
let kexdh_reply = kex_state.exchange_handler.handle_kexdh_init( let kexdh_reply = kex_state.exchange_handler.handle_kexdh_init(
&kexdh_init, &kexdh_init,
&kex_state.client_version, &kex_state.client_version,
@@ -219,27 +261,27 @@ fn perform_complete_kex_exchange(
)?; )?;
kexdh_reply.write(stream)?; kexdh_reply.write(stream)?;
info!("Sent SSH_MSG_KEX_ECDH_REPLY"); info!("Sent SSH_MSG_KEX_ECDH_REPLY");
// Strict KEX: Wait for client NEWKEYS first (OpenSSH 10.2 requirement) // Strict KEX: Wait for client NEWKEYS first (OpenSSH 10.2 requirement)
let client_newkeys = SshPacket::read(stream)?; let client_newkeys = SshPacket::read(stream)?;
kex_state.handle_newkeys(&client_newkeys)?; kex_state.handle_newkeys(&client_newkeys)?;
info!("Received SSH_MSG_NEWKEYS from client"); info!("Received SSH_MSG_NEWKEYS from client");
// Now send server NEWKEYS // Now send server NEWKEYS
let newkeys_packet = KexState::send_newkeys()?; let newkeys_packet = KexState::send_newkeys()?;
newkeys_packet.write(stream)?; newkeys_packet.write(stream)?;
kex_state.newkeys_sent = true; kex_state.newkeys_sent = true;
info!("Sent SSH_MSG_NEWKEYS from server"); info!("Sent SSH_MSG_NEWKEYS from server");
if kex_state.is_encryption_ready() { if kex_state.is_encryption_ready() {
info!("Encryption channel established successfully"); info!("Encryption channel established successfully");
} else { } else {
return Err(anyhow::anyhow!("Encryption channel not ready")); return Err(anyhow::anyhow!("Encryption channel not ready"));
} }
let session_keys = kex_state.exchange_handler.compute_session_keys()?; let session_keys = kex_state.exchange_handler.compute_session_keys()?;
let encryption_ctx = EncryptionContext::from_session_keys(&session_keys); let encryption_ctx = EncryptionContext::from_session_keys(&session_keys);
Ok(encryption_ctx) Ok(encryption_ctx)
} }
@@ -250,102 +292,100 @@ pub struct AuthUser {
} }
fn perform_ssh_auth( fn perform_ssh_auth(
stream: &mut TcpStream, stream: &mut TcpStream,
auth_handler: &mut AuthHandler, auth_handler: &mut AuthHandler,
encryption_ctx: &mut EncryptionContext, encryption_ctx: &mut EncryptionContext,
) -> Result<AuthUser> { ) -> Result<AuthUser> {
info!("Starting SSH authentication"); info!("Starting SSH authentication");
info!("Encryption context: key_ctos_len={}, key_stoc_len={}, iv_ctos_len={}, iv_stoc_len={}", info!(
"Encryption context: key_ctos_len={}, key_stoc_len={}, iv_ctos_len={}, iv_stoc_len={}",
encryption_ctx.encryption_key_ctos.len(), encryption_ctx.encryption_key_ctos.len(),
encryption_ctx.encryption_key_stoc.len(), encryption_ctx.encryption_key_stoc.len(),
encryption_ctx.iv_ctos.len(), encryption_ctx.iv_ctos.len(),
encryption_ctx.iv_stoc.len() encryption_ctx.iv_stoc.len()
); );
// OpenSSH strict KEX: SSH_MSG_EXT_INFO may be sent before SSH_MSG_SERVICE_REQUEST // OpenSSH strict KEX: SSH_MSG_EXT_INFO may be sent before SSH_MSG_SERVICE_REQUEST
let mut encrypted_request = EncryptedPacket::read(stream, encryption_ctx, true)?; let mut encrypted_request = EncryptedPacket::read(stream, encryption_ctx, true)?;
let payload = encrypted_request.payload(); let payload = encrypted_request.payload();
if payload[0] == PacketType::SSH_MSG_EXT_INFO as u8 { if payload[0] == PacketType::SSH_MSG_EXT_INFO as u8 {
info!("Received SSH_MSG_EXT_INFO, reading next packet"); info!("Received SSH_MSG_EXT_INFO, reading next packet");
encrypted_request = EncryptedPacket::read(stream, encryption_ctx, true)?; encrypted_request = EncryptedPacket::read(stream, encryption_ctx, true)?;
} }
let payload = encrypted_request.payload(); let payload = encrypted_request.payload();
info!("Received packet type: {}", payload[0]); info!("Received packet type: {}", payload[0]);
if payload[0] != PacketType::SSH_MSG_SERVICE_REQUEST as u8 { if payload[0] != PacketType::SSH_MSG_SERVICE_REQUEST as u8 {
return Err(anyhow!("Expected SSH_MSG_SERVICE_REQUEST, got type {}", payload[0])); return Err(anyhow!(
"Expected SSH_MSG_SERVICE_REQUEST, got type {}",
payload[0]
));
} }
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
let mut cursor = std::io::Cursor::new(&payload[1..]); let mut cursor = std::io::Cursor::new(&payload[1..]);
let service_name_len = cursor.read_u32::<BigEndian>()?; let service_name_len = cursor.read_u32::<BigEndian>()?;
let mut service_name = vec![0u8; service_name_len as usize]; let mut service_name = vec![0u8; service_name_len as usize];
cursor.read_exact(&mut service_name)?; cursor.read_exact(&mut service_name)?;
let service_name_str = String::from_utf8_lossy(&service_name); let service_name_str = String::from_utf8_lossy(&service_name);
if service_name_str != "ssh-userauth" { if service_name_str != "ssh-userauth" {
return Err(anyhow!("Unsupported service: {}", service_name_str)); return Err(anyhow!("Unsupported service: {}", service_name_str));
} }
let mut service_accept_payload = Vec::new(); let mut service_accept_payload = Vec::new();
service_accept_payload.write_u8(PacketType::SSH_MSG_SERVICE_ACCEPT as u8)?; service_accept_payload.write_u8(PacketType::SSH_MSG_SERVICE_ACCEPT as u8)?;
service_accept_payload.write_u32::<BigEndian>(12)?; // "ssh-userauth" length is 12, not 14! service_accept_payload.write_u32::<BigEndian>(12)?; // "ssh-userauth" length is 12, not 14!
service_accept_payload.write_all("ssh-userauth".as_bytes())?; service_accept_payload.write_all("ssh-userauth".as_bytes())?;
let encrypted_accept = EncryptedPacket::new( let encrypted_accept = EncryptedPacket::new(&service_accept_payload, encryption_ctx, true)?;
&service_accept_payload,
encryption_ctx,
true,
)?;
encrypted_accept.write(stream)?; encrypted_accept.write(stream)?;
info!("Sent encrypted SSH_MSG_SERVICE_ACCEPT"); info!("Sent encrypted SSH_MSG_SERVICE_ACCEPT");
let session_id = encryption_ctx.session_id.clone(); let session_id = encryption_ctx.session_id.clone();
loop { loop {
let auth_packet = EncryptedPacket::read(stream, encryption_ctx, true)?; // Reading from client, use cipher_ctos let auth_packet = EncryptedPacket::read(stream, encryption_ctx, true)?; // Reading from client, use cipher_ctos
let auth_payload = auth_packet.payload(); let auth_payload = auth_packet.payload();
info!("Received encrypted SSH_MSG_USERAUTH_REQUEST"); info!("Received encrypted SSH_MSG_USERAUTH_REQUEST");
let auth_request = SshPacket::new(auth_payload.to_vec()); let auth_request = SshPacket::new(auth_payload.to_vec());
match auth_handler.handle_userauth_request(&auth_request, &session_id)? { match auth_handler.handle_userauth_request(&auth_request, &session_id)? {
AuthResult::Success => { AuthResult::Success => {
let success_payload = vec![PacketType::SSH_MSG_USERAUTH_SUCCESS as u8]; let success_payload = vec![PacketType::SSH_MSG_USERAUTH_SUCCESS as u8];
let encrypted_success = EncryptedPacket::new( let encrypted_success =
&success_payload, EncryptedPacket::new(&success_payload, encryption_ctx, true)?;
encryption_ctx,
true,
)?;
encrypted_success.write(stream)?; encrypted_success.write(stream)?;
info!("Sent encrypted SSH_MSG_USERAUTH_SUCCESS"); info!("Sent encrypted SSH_MSG_USERAUTH_SUCCESS");
// Extract username from auth request // Extract username from auth request
let user = extract_username_from_auth_request(&auth_request) let user = extract_username_from_auth_request(&auth_request)
.unwrap_or_else(|_| "unknown".to_string()); .unwrap_or_else(|_| "unknown".to_string());
let home_dir = auth_handler.get_home_dir(&user) let home_dir = auth_handler
.get_home_dir(&user)
.ok() .ok()
.flatten() .flatten()
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/Users/accusys/markbase")); .unwrap_or_else(|| PathBuf::from("/Users/accusys/markbase"));
info!("Auth success: user={}, home_dir={:?}", user, home_dir); info!("Auth success: user={}, home_dir={:?}", user, home_dir);
return Ok(AuthUser { username: user, home_dir }); return Ok(AuthUser {
username: user,
home_dir,
});
} }
AuthResult::Failure(message) => { AuthResult::Failure(message) => {
// message包含可用的认证方法列表(如"password,publickey") // message包含可用的认证方法列表(如"password,publickey")
let mut failure_payload = Vec::new(); let mut failure_payload = Vec::new();
failure_payload.write_u8(PacketType::SSH_MSG_USERAUTH_FAILURE as u8)?; failure_payload.write_u8(PacketType::SSH_MSG_USERAUTH_FAILURE as u8)?;
failure_payload.write_u32::<BigEndian>(message.len() as u32)?; failure_payload.write_u32::<BigEndian>(message.len() as u32)?;
failure_payload.write_all(message.as_bytes())?; failure_payload.write_all(message.as_bytes())?;
failure_payload.write_u8(0)?; // partial_success = false failure_payload.write_u8(0)?; // partial_success = false
let encrypted_failure = EncryptedPacket::new( let encrypted_failure =
&failure_payload, EncryptedPacket::new(&failure_payload, encryption_ctx, true)?;
encryption_ctx,
true,
)?;
encrypted_failure.write(stream)?; encrypted_failure.write(stream)?;
warn!("Sent encrypted SSH_MSG_USERAUTH_FAILURE: {}", message); warn!("Sent encrypted SSH_MSG_USERAUTH_FAILURE: {}", message);
} }
@@ -356,27 +396,23 @@ AuthResult::Failure(message) => {
AuthResult::PublicKeyOk(algorithm, public_key_blob) => { AuthResult::PublicKeyOk(algorithm, public_key_blob) => {
// SSH_MSG_USERAUTH_PK_OK:public key acceptable // SSH_MSG_USERAUTH_PK_OK:public key acceptable
info!("Public key acceptable, sending USERAUTH_PK_OK"); info!("Public key acceptable, sending USERAUTH_PK_OK");
let mut pk_ok_payload = Vec::new(); let mut pk_ok_payload = Vec::new();
pk_ok_payload.write_u8(PacketType::SSH_MSG_USERAUTH_PK_OK as u8)?; pk_ok_payload.write_u8(PacketType::SSH_MSG_USERAUTH_PK_OK as u8)?;
// algorithm (SSH string) // algorithm (SSH string)
pk_ok_payload.write_u32::<BigEndian>(algorithm.len() as u32)?; pk_ok_payload.write_u32::<BigEndian>(algorithm.len() as u32)?;
pk_ok_payload.write_all(algorithm.as_bytes())?; pk_ok_payload.write_all(algorithm.as_bytes())?;
// public key blob (SSH string) // public key blob (SSH string)
pk_ok_payload.write_u32::<BigEndian>(public_key_blob.len() as u32)?; pk_ok_payload.write_u32::<BigEndian>(public_key_blob.len() as u32)?;
pk_ok_payload.write_all(&public_key_blob)?; pk_ok_payload.write_all(&public_key_blob)?;
let encrypted_pk_ok = EncryptedPacket::new( let encrypted_pk_ok = EncryptedPacket::new(&pk_ok_payload, encryption_ctx, true)?;
&pk_ok_payload,
encryption_ctx,
true,
)?;
encrypted_pk_ok.write(stream)?; encrypted_pk_ok.write(stream)?;
info!("Sent SSH_MSG_USERAUTH_PK_OK"); info!("Sent SSH_MSG_USERAUTH_PK_OK");
continue; // Wait for signed request continue; // Wait for signed request
} }
} }
} }
@@ -389,16 +425,17 @@ fn handle_ssh_service_loop(
stream: &mut TcpStream, stream: &mut TcpStream,
channel_manager: &mut ChannelManager, channel_manager: &mut ChannelManager,
encryption_ctx: &mut EncryptionContext, encryption_ctx: &mut EncryptionContext,
port_forward_manager: &mut PortForwardManager, // Phase 13 port_forward_manager: &mut PortForwardManager, // Phase 13
security_config: Arc<Mutex<SshSecurityConfig>>, // Phase 13.1 security_config: Arc<Mutex<SshSecurityConfig>>, // Phase 13.1
) -> Result<()> { ) -> Result<()> {
info!("Starting SSH service loop (Phase 14.2: unified poll + child status)"); info!("Starting SSH service loop (Phase 14.2: unified poll + child status)");
loop { loop {
// ⭐⭐⭐⭐⭐ Phase 14.2: 统一poll + child状态检测 // ⭐⭐⭐⭐⭐ Phase 14.2: 统一poll + child状态检测
// 返回三元组:(stdout_packets, client_has_data, child_exited) // 返回三元组:(stdout_packets, client_has_data, child_exited)
let (stdout_packets, client_has_data, child_exited) = channel_manager.poll_exec_stdout_and_client(stream)?; let (stdout_packets, client_has_data, child_exited) =
channel_manager.poll_exec_stdout_and_client(stream)?;
// 1. 发送stdout/stderr数据(如果有) // 1. 发送stdout/stderr数据(如果有)
if let Some(packets) = stdout_packets { if let Some(packets) = stdout_packets {
for packet in packets { for packet in packets {
@@ -407,93 +444,100 @@ fn handle_ssh_service_loop(
info!("Sent stdout/stderr data (Phase 14.2)"); info!("Sent stdout/stderr data (Phase 14.2)");
} }
} }
// 2. 处理child exited(发送EOF + CLOSE) // 2. 处理child exited(发送EOF + CLOSE)
if child_exited { if child_exited {
info!("Child process exited, sending SSH_MSG_CHANNEL_EOF + CLOSE"); info!("Child process exited, sending SSH_MSG_CHANNEL_EOF + CLOSE");
// ⭐⭐⭐⭐⭐ Phase 14.2: 使用ChannelManager.handle_child_exited() // ⭐⭐⭐⭐⭐ Phase 14.2: 使用ChannelManager.handle_child_exited()
let exit_packets = channel_manager.handle_child_exited()?; let exit_packets = channel_manager.handle_child_exited()?;
for packet in exit_packets { for packet in exit_packets {
let encrypted_packet = EncryptedPacket::new(&packet.payload, encryption_ctx, true)?; let encrypted_packet = EncryptedPacket::new(&packet.payload, encryption_ctx, true)?;
encrypted_packet.write(stream)?; encrypted_packet.write(stream)?;
} }
// 继续处理client数据(可能还有其他请求) // 继续处理client数据(可能还有其他请求)
} }
// 3. 处理client数据(如果有) // 3. 处理client数据(如果有)
if !client_has_data { if !client_has_data {
// client没有数据,继续下一轮循环 // client没有数据,继续下一轮循环
continue; continue;
} }
// client有数据,读取并处理 // client有数据,读取并处理
let encrypted_packet = EncryptedPacket::read(stream, encryption_ctx, true)?; let encrypted_packet = EncryptedPacket::read(stream, encryption_ctx, true)?;
let packet = SshPacket::new(encrypted_packet.payload().to_vec()); let packet = SshPacket::new(encrypted_packet.payload().to_vec());
match packet.payload.first() { match packet.payload.first() {
// Phase 13: SSH_MSG_GLOBAL_REQUEST处理(端口转发) // Phase 13: SSH_MSG_GLOBAL_REQUEST处理(端口转发)
Some(&pt) if pt == PacketType::SSH_MSG_GLOBAL_REQUEST as u8 => { Some(&pt) if pt == PacketType::SSH_MSG_GLOBAL_REQUEST as u8 => {
info!("Received SSH_MSG_GLOBAL_REQUEST (port forwarding)"); info!("Received SSH_MSG_GLOBAL_REQUEST (port forwarding)");
// Phase 13.1: 安全配置验证 // Phase 13.1: 安全配置验证
let security = security_config.lock().unwrap(); let security = security_config.lock().unwrap();
if !security.allow_tcp_forwarding { if !security.allow_tcp_forwarding {
warn!("TCP forwarding disabled by security config"); warn!("TCP forwarding disabled by security config");
let failure_packet = vec![PacketType::SSH_MSG_REQUEST_FAILURE as u8]; let failure_packet = vec![PacketType::SSH_MSG_REQUEST_FAILURE as u8];
let encrypted_failure = EncryptedPacket::new(&failure_packet, encryption_ctx, true)?; let encrypted_failure =
EncryptedPacket::new(&failure_packet, encryption_ctx, true)?;
encrypted_failure.write(stream)?; encrypted_failure.write(stream)?;
info!("Sent SSH_MSG_REQUEST_FAILURE (TCP forwarding disabled)"); info!("Sent SSH_MSG_REQUEST_FAILURE (TCP forwarding disabled)");
continue; continue;
} }
// Phase 13.2: 调用PortForwardManager处理(传递security_config) // Phase 13.2: 调用PortForwardManager处理(传递security_config)
let (success, response) = port_forward_manager.handle_global_request(&packet.payload, &security)?; let (success, response) =
drop(security); // 释放锁 port_forward_manager.handle_global_request(&packet.payload, &security)?;
drop(security); // 释放锁
if success { if success {
if let Some(response_data) = response { if let Some(response_data) = response {
let encrypted_response = EncryptedPacket::new(&response_data, encryption_ctx, true)?; let encrypted_response =
EncryptedPacket::new(&response_data, encryption_ctx, true)?;
encrypted_response.write(stream)?; encrypted_response.write(stream)?;
info!("Sent SSH_MSG_REQUEST_SUCCESS (tcpip-forward accepted)"); info!("Sent SSH_MSG_REQUEST_SUCCESS (tcpip-forward accepted)");
} else { } else {
// 无响应数据时,发送简单的SUCCESS // 无响应数据时,发送简单的SUCCESS
let success_packet = vec![PacketType::SSH_MSG_REQUEST_SUCCESS as u8]; let success_packet = vec![PacketType::SSH_MSG_REQUEST_SUCCESS as u8];
let encrypted_success = EncryptedPacket::new(&success_packet, encryption_ctx, true)?; let encrypted_success =
EncryptedPacket::new(&success_packet, encryption_ctx, true)?;
encrypted_success.write(stream)?; encrypted_success.write(stream)?;
info!("Sent SSH_MSG_REQUEST_SUCCESS"); info!("Sent SSH_MSG_REQUEST_SUCCESS");
} }
} else { } else {
let failure_packet = vec![PacketType::SSH_MSG_REQUEST_FAILURE as u8]; let failure_packet = vec![PacketType::SSH_MSG_REQUEST_FAILURE as u8];
let encrypted_failure = EncryptedPacket::new(&failure_packet, encryption_ctx, true)?; let encrypted_failure =
EncryptedPacket::new(&failure_packet, encryption_ctx, true)?;
encrypted_failure.write(stream)?; encrypted_failure.write(stream)?;
info!("Sent SSH_MSG_REQUEST_FAILURE (tcpip-forward rejected)"); info!("Sent SSH_MSG_REQUEST_FAILURE (tcpip-forward rejected)");
} }
} }
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_OPEN as u8 => { Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_OPEN as u8 => {
info!("Received SSH_MSG_CHANNEL_OPEN"); info!("Received SSH_MSG_CHANNEL_OPEN");
// Phase 13.3: 获取security_config并传递给handle_channel_open // Phase 13.3: 获取security_config并传递给handle_channel_open
let security = security_config.lock().unwrap(); let security = security_config.lock().unwrap();
let response = channel_manager.handle_channel_open(&packet, Some(&security))?; let response = channel_manager.handle_channel_open(&packet, Some(&security))?;
drop(security); // 释放锁 drop(security); // 释放锁
let encrypted_response = EncryptedPacket::new(&response.payload, encryption_ctx, true)?; let encrypted_response =
EncryptedPacket::new(&response.payload, encryption_ctx, true)?;
encrypted_response.write(stream)?; encrypted_response.write(stream)?;
info!("Sent SSH_MSG_CHANNEL_OPEN_CONFIRMATION"); info!("Sent SSH_MSG_CHANNEL_OPEN_CONFIRMATION");
} }
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_REQUEST as u8 => { Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_REQUEST as u8 => {
info!("Received SSH_MSG_CHANNEL_REQUEST"); info!("Received SSH_MSG_CHANNEL_REQUEST");
if let Some(response) = channel_manager.handle_channel_request(&packet)? { if let Some(response) = channel_manager.handle_channel_request(&packet)? {
let encrypted_response = EncryptedPacket::new(&response.payload, encryption_ctx, true)?; let encrypted_response =
EncryptedPacket::new(&response.payload, encryption_ctx, true)?;
encrypted_response.write(stream)?; encrypted_response.write(stream)?;
// ⭐⭐⭐⭐⭐ Phase 14.5修复:区分普通命令和交互式进程 // ⭐⭐⭐⭐⭐ Phase 14.5修复:区分普通命令和交互式进程
// 检查是否有 exec_process(交互式进程如 rsync) // 检查是否有 exec_process(交互式进程如 rsync)
let has_exec_process = channel_manager.has_exec_process(); let has_exec_process = channel_manager.has_exec_process();
if has_exec_process { if has_exec_process {
info!("⭐⭐⭐⭐⭐ [INTERACTIVE_PROCESS] Detected exec_process (rsync/SCP), skipping immediate EOF"); info!("⭐⭐⭐⭐⭐ [INTERACTIVE_PROCESS] Detected exec_process (rsync/SCP), skipping immediate EOF");
// 对于交互式进程,只发送 SUCCESS,等待 poll 循环处理数据流 // 对于交互式进程,只发送 SUCCESS,等待 poll 循环处理数据流
@@ -503,23 +547,37 @@ fn handle_ssh_service_loop(
if let Some(channel_id) = channel_manager.get_channel_with_output() { if let Some(channel_id) = channel_manager.get_channel_with_output() {
if let Some(output) = channel_manager.get_channel_output(channel_id) { if let Some(output) = channel_manager.get_channel_output(channel_id) {
// 发送命令输出(SSH_MSG_CHANNEL_DATA) // 发送命令输出(SSH_MSG_CHANNEL_DATA)
let data_packet = channel_manager.build_channel_data(channel_id, &output)?; let data_packet =
let encrypted_data = EncryptedPacket::new(&data_packet.payload, encryption_ctx, true)?; channel_manager.build_channel_data(channel_id, &output)?;
let encrypted_data = EncryptedPacket::new(
&data_packet.payload,
encryption_ctx,
true,
)?;
encrypted_data.write(stream)?; encrypted_data.write(stream)?;
info!("Sent command output ({} bytes)", output.len()); info!("Sent command output ({} bytes)", output.len());
// 发送SSH_MSG_CHANNEL_EOF // 发送SSH_MSG_CHANNEL_EOF
let eof_packet = channel_manager.build_channel_eof(channel_id)?; let eof_packet = channel_manager.build_channel_eof(channel_id)?;
let encrypted_eof = EncryptedPacket::new(&eof_packet.payload, encryption_ctx, true)?; let encrypted_eof = EncryptedPacket::new(
&eof_packet.payload,
encryption_ctx,
true,
)?;
encrypted_eof.write(stream)?; encrypted_eof.write(stream)?;
info!("Sent SSH_MSG_CHANNEL_EOF"); info!("Sent SSH_MSG_CHANNEL_EOF");
// 发送SSH_MSG_CHANNEL_CLOSE // 发送SSH_MSG_CHANNEL_CLOSE
let close_packet = channel_manager.build_channel_close(channel_id)?; let close_packet =
let encrypted_close = EncryptedPacket::new(&close_packet.payload, encryption_ctx, true)?; channel_manager.build_channel_close(channel_id)?;
let encrypted_close = EncryptedPacket::new(
&close_packet.payload,
encryption_ctx,
true,
)?;
encrypted_close.write(stream)?; encrypted_close.write(stream)?;
info!("Sent SSH_MSG_CHANNEL_CLOSE"); info!("Sent SSH_MSG_CHANNEL_CLOSE");
// 移除channel // 移除channel
channel_manager.remove_channel(channel_id); channel_manager.remove_channel(channel_id);
} }
@@ -531,22 +589,28 @@ fn handle_ssh_service_loop(
info!("Received SSH_MSG_CHANNEL_DATA"); info!("Received SSH_MSG_CHANNEL_DATA");
if let Some(response) = channel_manager.handle_channel_data(&packet)? { if let Some(response) = channel_manager.handle_channel_data(&packet)? {
// Phase 7: SFTP响应通过CHANNEL_DATA返回 // Phase 7: SFTP响应通过CHANNEL_DATA返回
let encrypted_response = EncryptedPacket::new(&response.payload, encryption_ctx, true)?; let encrypted_response =
EncryptedPacket::new(&response.payload, encryption_ctx, true)?;
encrypted_response.write(stream)?; encrypted_response.write(stream)?;
info!("Sent SSH_MSG_CHANNEL_DATA (SFTP response)"); info!("Sent SSH_MSG_CHANNEL_DATA (SFTP response)");
} }
// ⭐⭐⭐⭐⭐ Phase 15.1: Drain pending packets (e.g. WINDOW_ADJUST + delayed SFTP response) // ⭐⭐⭐⭐⭐ Phase 15.1: Drain pending packets (e.g. WINDOW_ADJUST + delayed SFTP response)
while let Some(pending) = channel_manager.pending_packets.pop_front() { while let Some(pending) = channel_manager.pending_packets.pop_front() {
let encrypted_pending = EncryptedPacket::new(&pending.payload, encryption_ctx, true)?; let encrypted_pending =
EncryptedPacket::new(&pending.payload, encryption_ctx, true)?;
encrypted_pending.write(stream)?; encrypted_pending.write(stream)?;
info!("Sent pending packet (type {})", pending.payload.first().unwrap_or(&0)); info!(
"Sent pending packet (type {})",
pending.payload.first().unwrap_or(&0)
);
} }
} }
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_CLOSE as u8 => { Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_CLOSE as u8 => {
info!("Received SSH_MSG_CHANNEL_CLOSE"); info!("Received SSH_MSG_CHANNEL_CLOSE");
if let Some(response) = channel_manager.handle_channel_close(&packet)? { if let Some(response) = channel_manager.handle_channel_close(&packet)? {
let encrypted_response = EncryptedPacket::new(&response.payload, encryption_ctx, true)?; let encrypted_response =
EncryptedPacket::new(&response.payload, encryption_ctx, true)?;
encrypted_response.write(stream)?; encrypted_response.write(stream)?;
} }
break; break;
@@ -565,8 +629,10 @@ fn handle_ssh_service_loop(
let payload = &packet.payload; let payload = &packet.payload;
if payload.len() >= 9 { if payload.len() >= 9 {
// Format: uint32 recipient_channel || uint32 bytes_to_add // Format: uint32 recipient_channel || uint32 bytes_to_add
let recipient_channel = u32::from_be_bytes([payload[1], payload[2], payload[3], payload[4]]); let recipient_channel =
let bytes_to_add = u32::from_be_bytes([payload[5], payload[6], payload[7], payload[8]]); u32::from_be_bytes([payload[1], payload[2], payload[3], payload[4]]);
let bytes_to_add =
u32::from_be_bytes([payload[5], payload[6], payload[7], payload[8]]);
channel_manager.adjust_remote_window(recipient_channel, bytes_to_add); channel_manager.adjust_remote_window(recipient_channel, bytes_to_add);
} }
} }
@@ -575,12 +641,14 @@ fn handle_ssh_service_loop(
} }
} }
} }
Ok(()) Ok(())
} }
/// 从SSH_MSG_USERAUTH_REQUEST payload中提取用户名 /// 从SSH_MSG_USERAUTH_REQUEST payload中提取用户名
fn extract_username_from_auth_request(packet: &crate::ssh_server::packet::SshPacket) -> Result<String> { fn extract_username_from_auth_request(
packet: &crate::ssh_server::packet::SshPacket,
) -> Result<String> {
let payload = &packet.payload; let payload = &packet.payload;
if payload.len() < 5 { if payload.len() < 5 {
return Err(anyhow!("Auth request too short")); return Err(anyhow!("Auth request too short"));
@@ -598,10 +666,10 @@ pub fn run_ssh_server(port: Option<u16>, pg_conn: Option<&str>) -> Result<()> {
let config = SshServerConfig { let config = SshServerConfig {
port: port.unwrap_or(2024), port: port.unwrap_or(2024),
bind_address: "127.0.0.1".to_string(), bind_address: "127.0.0.1".to_string(),
security_config: SshSecurityConfig::enterprise_default(), // Phase 13.1: 添加安全配置 security_config: SshSecurityConfig::enterprise_default(), // Phase 13.1: 添加安全配置
pg_conn: pg_conn.map(|s| s.to_string()), pg_conn: pg_conn.map(|s| s.to_string()),
}; };
let server = SshServer::new(config); let server = SshServer::new(config);
server.run() server.run()
} }
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
// SSH企业级安全配置(Phase 13.1) // SSH企业级安全配置(Phase 13.1)
// 参考OpenSSH sshd_config安全配置 // 参考OpenSSH sshd_config安全配置
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use log::{info, warn}; use log::{info, warn};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
@@ -14,25 +14,25 @@ pub struct SshSecurityConfig {
/// false: 只绑定127.0.0.1(安全) /// false: 只绑定127.0.0.1(安全)
/// true: 允许绑定0.0.0.0(危险) /// true: 允许绑定0.0.0.0(危险)
pub gateway_ports: bool, pub gateway_ports: bool,
/// PermitOpen白名单 /// PermitOpen白名单
/// ["localhost:3000", "localhost:4000", "localhost:*"] /// ["localhost:3000", "localhost:4000", "localhost:*"]
/// 空数组表示允许所有目标(不安全) /// 空数组表示允许所有目标(不安全)
pub permit_open: Vec<String>, pub permit_open: Vec<String>,
/// AllowTcpForwarding配置 /// AllowTcpForwarding配置
/// true: 允许TCP转发 /// true: 允许TCP转发
/// false: 禁止所有TCP转发 /// false: 禁止所有TCP转发
pub allow_tcp_forwarding: bool, pub allow_tcp_forwarding: bool,
/// MaxSessions限制 /// MaxSessions限制
/// 最大会话数,防止资源耗尽 /// 最大会话数,防止资源耗尽
pub max_sessions: u32, pub max_sessions: u32,
/// ConnectTimeout超时(秒) /// ConnectTimeout超时(秒)
/// 连接超时设置,防止悬挂连接 /// 连接超时设置,防止悬挂连接
pub connect_timeout: u64, pub connect_timeout: u64,
/// 活动会话数(运行时状态) /// 活动会话数(运行时状态)
pub active_sessions: u32, pub active_sessions: u32,
} }
@@ -42,110 +42,125 @@ impl SshSecurityConfig {
/// 参考:OpenSSH企业级生产环境配置 /// 参考:OpenSSH企业级生产环境配置
pub fn enterprise_default() -> Self { pub fn enterprise_default() -> Self {
Self { Self {
gateway_ports: false, // 安全:只绑定127.0.0.1 gateway_ports: false, // 安全:只绑定127.0.0.1
permit_open: vec!["localhost:*".to_string()], // 限制转发目标(白名单) permit_open: vec!["localhost:*".to_string()], // 限制转发目标(白名单)
allow_tcp_forwarding: true, // 允许TCP转发 allow_tcp_forwarding: true, // 允许TCP转发
max_sessions: 10, // 最多10个会话 max_sessions: 10, // 最多10个会话
connect_timeout: 30, // 30秒超时 connect_timeout: 30, // 30秒超时
active_sessions: 0, // 运行时状态 active_sessions: 0, // 运行时状态
} }
} }
/// 开发环境默认配置(宽松) /// 开发环境默认配置(宽松)
pub fn development_default() -> Self { pub fn development_default() -> Self {
Self { Self {
gateway_ports: true, // 开发:允许0.0.0.0 gateway_ports: true, // 开发:允许0.0.0.0
permit_open: vec![], // 开发:允许所有目标 permit_open: vec![], // 开发:允许所有目标
allow_tcp_forwarding: true, allow_tcp_forwarding: true,
max_sessions: 20, // 开发:更多会话 max_sessions: 20, // 开发:更多会话
connect_timeout: 60, // 开发:更长超时 connect_timeout: 60, // 开发:更长超时
active_sessions: 0, active_sessions: 0,
} }
} }
/// 从JSON配置文件加载 /// 从JSON配置文件加载
pub fn load_from_file(path: &str) -> Result<Self> { pub fn load_from_file(path: &str) -> Result<Self> {
if !Path::new(path).exists() { if !Path::new(path).exists() {
info!("SSH security config file not found, using enterprise default"); info!("SSH security config file not found, using enterprise default");
return Ok(Self::enterprise_default()); return Ok(Self::enterprise_default());
} }
let config_str = fs::read_to_string(path)?; let config_str = fs::read_to_string(path)?;
let config: serde_json::Value = serde_json::from_str(&config_str)?; let config: serde_json::Value = serde_json::from_str(&config_str)?;
let security = config.get("ssh_server") let security = config
.get("ssh_server")
.and_then(|s| s.get("security")) .and_then(|s| s.get("security"))
.ok_or_else(|| anyhow!("Invalid config structure"))?; .ok_or_else(|| anyhow!("Invalid config structure"))?;
Ok(Self { Ok(Self {
gateway_ports: security.get("gateway_ports") gateway_ports: security
.get("gateway_ports")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(false), .unwrap_or(false),
permit_open: security.get("permit_open") permit_open: security
.get("permit_open")
.and_then(|v| v.as_array()) .and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()) .map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_else(|| vec!["localhost:*".to_string()]), .unwrap_or_else(|| vec!["localhost:*".to_string()]),
allow_tcp_forwarding: security.get("allow_tcp_forwarding") allow_tcp_forwarding: security
.get("allow_tcp_forwarding")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(true), .unwrap_or(true),
max_sessions: security.get("max_sessions") max_sessions: security
.get("max_sessions")
.and_then(|v| v.as_u64()) .and_then(|v| v.as_u64())
.map(|v| v as u32) .map(|v| v as u32)
.unwrap_or(10), .unwrap_or(10),
connect_timeout: security.get("connect_timeout") connect_timeout: security
.get("connect_timeout")
.and_then(|v| v.as_u64()) .and_then(|v| v.as_u64())
.unwrap_or(30), .unwrap_or(30),
active_sessions: 0, active_sessions: 0,
}) })
} }
/// 验证tcpip-forward请求(安全检查) /// 验证tcpip-forward请求(安全检查)
/// 参考OpenSSH auth2.c: ssh_forwarding_check() /// 参考OpenSSH auth2.c: ssh_forwarding_check()
pub fn validate_tcpip_forward_request( pub fn validate_tcpip_forward_request(&self, bind_address: &str, bind_port: u32) -> Result<()> {
&self, info!(
bind_address: &str, "Validating tcpip-forward request: bind_address={}, bind_port={}",
bind_port: u32, bind_address, bind_port
) -> Result<()> { );
info!("Validating tcpip-forward request: bind_address={}, bind_port={}", bind_address, bind_port);
// 1. AllowTcpForwarding检查 // 1. AllowTcpForwarding检查
if !self.allow_tcp_forwarding { if !self.allow_tcp_forwarding {
warn!("TCP forwarding disabled by security config"); warn!("TCP forwarding disabled by security config");
return Err(anyhow!("TCP forwarding disabled by AllowTcpForwarding=no")); return Err(anyhow!("TCP forwarding disabled by AllowTcpForwarding=no"));
} }
// 2. GatewayPorts检查 // 2. GatewayPorts检查
if !self.gateway_ports { if !self.gateway_ports {
// 只允许绑定到127.0.0.1或localhost // 只允许绑定到127.0.0.1或localhost
if bind_address != "127.0.0.1" && bind_address != "localhost" && bind_address != "" { if bind_address != "127.0.0.1" && bind_address != "localhost" && !bind_address.is_empty() {
warn!("GatewayPorts disabled, bind_address {} not allowed", bind_address); warn!(
"GatewayPorts disabled, bind_address {} not allowed",
bind_address
);
return Err(anyhow!("GatewayPorts=no, only 127.0.0.1 allowed")); return Err(anyhow!("GatewayPorts=no, only 127.0.0.1 allowed"));
} }
info!("GatewayPorts check passed: bind_address={}", bind_address); info!("GatewayPorts check passed: bind_address={}", bind_address);
} }
// 3. MaxSessions检查 // 3. MaxSessions检查
if self.active_sessions >= self.max_sessions { if self.active_sessions >= self.max_sessions {
warn!("Max sessions limit reached: {} >= {}", self.active_sessions, self.max_sessions); warn!(
"Max sessions limit reached: {} >= {}",
self.active_sessions, self.max_sessions
);
return Err(anyhow!("Max sessions limit reached: {}", self.max_sessions)); return Err(anyhow!("Max sessions limit reached: {}", self.max_sessions));
} }
// 4. 特权端口检查(防止<1024) // 4. 特权端口检查(防止<1024)
if bind_port < 1024 { if bind_port < 1024 {
warn!("Cannot bind to privileged port: {}", bind_port); warn!("Cannot bind to privileged port: {}", bind_port);
return Err(anyhow!("Cannot bind to privileged port < 1024")); return Err(anyhow!("Cannot bind to privileged port < 1024"));
} }
// 5. 端口范围检查(防止过大端口) // 5. 端口范围检查(防止过大端口)
if bind_port > 65535 { if bind_port > 65535 {
warn!("Invalid port number: {}", bind_port); warn!("Invalid port number: {}", bind_port);
return Err(anyhow!("Invalid port number > 65535")); return Err(anyhow!("Invalid port number > 65535"));
} }
info!("tcpip-forward request validated successfully"); info!("tcpip-forward request validated successfully");
Ok(()) Ok(())
} }
/// 验证direct-tcpip channel请求(安全检查) /// 验证direct-tcpip channel请求(安全检查)
/// 参考OpenSSH channels.c: channel_connect_direct_tcpip() /// 参考OpenSSH channels.c: channel_connect_direct_tcpip()
pub fn validate_direct_tcpip_channel( pub fn validate_direct_tcpip_channel(
@@ -153,14 +168,17 @@ impl SshSecurityConfig {
host_to_connect: &str, host_to_connect: &str,
port_to_connect: u32, port_to_connect: u32,
) -> Result<()> { ) -> Result<()> {
info!("Validating direct-tcpip channel: host={}, port={}", host_to_connect, port_to_connect); info!(
"Validating direct-tcpip channel: host={}, port={}",
host_to_connect, port_to_connect
);
// 1. AllowTcpForwarding检查 // 1. AllowTcpForwarding检查
if !self.allow_tcp_forwarding { if !self.allow_tcp_forwarding {
warn!("TCP forwarding disabled by security config"); warn!("TCP forwarding disabled by security config");
return Err(anyhow!("TCP forwarding disabled by AllowTcpForwarding=no")); return Err(anyhow!("TCP forwarding disabled by AllowTcpForwarding=no"));
} }
// 2. PermitOpen白名单检查 // 2. PermitOpen白名单检查
if !self.permit_open.is_empty() { if !self.permit_open.is_empty() {
let target = format!("{}:{}", host_to_connect, port_to_connect); let target = format!("{}:{}", host_to_connect, port_to_connect);
@@ -173,28 +191,34 @@ impl SshSecurityConfig {
target == *pattern target == *pattern
} }
}); });
if !allowed { if !allowed {
warn!("Target {}:{} not in PermitOpen whitelist", host_to_connect, port_to_connect); warn!(
return Err(anyhow!("Target {}:{} not in PermitOpen whitelist", "Target {}:{} not in PermitOpen whitelist",
host_to_connect, port_to_connect)); host_to_connect, port_to_connect
);
return Err(anyhow!(
"Target {}:{} not in PermitOpen whitelist",
host_to_connect,
port_to_connect
));
} }
info!("PermitOpen check passed: target={}", target); info!("PermitOpen check passed: target={}", target);
} else { } else {
// permit_open为空,允许所有目标(不安全,仅用于开发) // permit_open为空,允许所有目标(不安全,仅用于开发)
info!("PermitOpen whitelist empty, allowing all targets (development mode)"); info!("PermitOpen whitelist empty, allowing all targets (development mode)");
} }
// 3. 端口范围检查 // 3. 端口范围检查
if port_to_connect < 1 || port_to_connect > 65535 { if !(1..=65535).contains(&port_to_connect) {
warn!("Invalid port number: {}", port_to_connect); warn!("Invalid port number: {}", port_to_connect);
return Err(anyhow!("Invalid port number: {}", port_to_connect)); return Err(anyhow!("Invalid port number: {}", port_to_connect));
} }
info!("direct-tcpip channel validated successfully"); info!("direct-tcpip channel validated successfully");
Ok(()) Ok(())
} }
/// 增加活动会话数 /// 增加活动会话数
pub fn increment_sessions(&mut self) -> Result<()> { pub fn increment_sessions(&mut self) -> Result<()> {
if self.active_sessions >= self.max_sessions { if self.active_sessions >= self.max_sessions {
@@ -204,7 +228,7 @@ impl SshSecurityConfig {
info!("Active sessions: {}", self.active_sessions); info!("Active sessions: {}", self.active_sessions);
Ok(()) Ok(())
} }
/// 减少活动会话数 /// 减少活动会话数
pub fn decrement_sessions(&mut self) { pub fn decrement_sessions(&mut self) {
if self.active_sessions > 0 { if self.active_sessions > 0 {
@@ -217,56 +241,76 @@ impl SshSecurityConfig {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_enterprise_default_config() { fn test_enterprise_default_config() {
let config = SshSecurityConfig::enterprise_default(); let config = SshSecurityConfig::enterprise_default();
assert_eq!(config.gateway_ports, false); assert_eq!(config.gateway_ports, false);
assert_eq!(config.permit_open, vec!["localhost:*".to_string()]); assert_eq!(config.permit_open, vec!["localhost:*".to_string()]);
assert_eq!(config.allow_tcp_forwarding, true); assert_eq!(config.allow_tcp_forwarding, true);
assert_eq!(config.max_sessions, 10); assert_eq!(config.max_sessions, 10);
assert_eq!(config.connect_timeout, 30); assert_eq!(config.connect_timeout, 30);
} }
#[test] #[test]
fn test_validate_tcpip_forward_request() { fn test_validate_tcpip_forward_request() {
let config = SshSecurityConfig::enterprise_default(); let config = SshSecurityConfig::enterprise_default();
// 正常请求应该通过 // 正常请求应该通过
assert!(config.validate_tcpip_forward_request("127.0.0.1", 8080).is_ok()); assert!(config
assert!(config.validate_tcpip_forward_request("localhost", 8080).is_ok()); .validate_tcpip_forward_request("127.0.0.1", 8080)
.is_ok());
assert!(config
.validate_tcpip_forward_request("localhost", 8080)
.is_ok());
// GatewayPorts=false时,0.0.0.0应该被拒绝 // GatewayPorts=false时,0.0.0.0应该被拒绝
assert!(config.validate_tcpip_forward_request("0.0.0.0", 8080).is_err()); assert!(config
.validate_tcpip_forward_request("0.0.0.0", 8080)
.is_err());
// 特权端口应该被拒绝 // 特权端口应该被拒绝
assert!(config.validate_tcpip_forward_request("127.0.0.1", 80).is_err()); assert!(config
.validate_tcpip_forward_request("127.0.0.1", 80)
.is_err());
} }
#[test] #[test]
fn test_validate_direct_tcpip_channel() { fn test_validate_direct_tcpip_channel() {
let config = SshSecurityConfig::enterprise_default(); let config = SshSecurityConfig::enterprise_default();
// localhost:*应该通过(通配符匹配) // localhost:*应该通过(通配符匹配)
assert!(config.validate_direct_tcpip_channel("localhost", 3000).is_ok()); assert!(config
assert!(config.validate_direct_tcpip_channel("localhost", 4000).is_ok()); .validate_direct_tcpip_channel("localhost", 3000)
.is_ok());
assert!(config
.validate_direct_tcpip_channel("localhost", 4000)
.is_ok());
// 其他host应该被拒绝 // 其他host应该被拒绝
assert!(config.validate_direct_tcpip_channel("192.168.1.100", 3000).is_err()); assert!(config
assert!(config.validate_direct_tcpip_channel("example.com", 80).is_err()); .validate_direct_tcpip_channel("192.168.1.100", 3000)
.is_err());
assert!(config
.validate_direct_tcpip_channel("example.com", 80)
.is_err());
} }
#[test] #[test]
fn test_development_default_config() { fn test_development_default_config() {
let config = SshSecurityConfig::development_default(); let config = SshSecurityConfig::development_default();
assert_eq!(config.gateway_ports, true); assert_eq!(config.gateway_ports, true);
assert_eq!(config.permit_open.len(), 0); // 空数组表示允许所有 assert_eq!(config.permit_open.len(), 0); // 空数组表示允许所有
assert_eq!(config.max_sessions, 20); assert_eq!(config.max_sessions, 20);
// 开发配置应该允许所有请求 // 开发配置应该允许所有请求
assert!(config.validate_tcpip_forward_request("0.0.0.0", 8080).is_ok()); assert!(config
assert!(config.validate_direct_tcpip_channel("example.com", 80).is_ok()); .validate_tcpip_forward_request("0.0.0.0", 8080)
.is_ok());
assert!(config
.validate_direct_tcpip_channel("example.com", 80)
.is_ok());
} }
} }
+75 -67
View File
@@ -1,11 +1,11 @@
// SSH Buffer 零拷贝实现(参考 OpenSSH sshbuf.c) // SSH Buffer 零拷贝实现(参考 OpenSSH sshbuf.c)
// 提供高效的 buffer 管理,消除临时 buffer // 提供高效的 buffer 管理,消除临时 buffer
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use std::io::{Read, Write}; use std::io::{Read, Write};
/// SSH Buffer(参考 OpenSSH struct sshbuf) /// SSH Buffer(参考 OpenSSH struct sshbuf)
/// ///
/// OpenSSH 实现: /// OpenSSH 实现:
/// ```c /// ```c
/// struct sshbuf { /// struct sshbuf {
@@ -16,10 +16,10 @@ use std::io::{Read, Write};
/// }; /// };
/// ``` /// ```
pub struct SshBuf { pub struct SshBuf {
data: Vec<u8>, // Data buffer (对应 OpenSSH buf->d) data: Vec<u8>, // Data buffer (对应 OpenSSH buf->d)
off: usize, // Offset (对应 OpenSSH buf->off) off: usize, // Offset (对应 OpenSSH buf->off)
size: usize, // Size (对应 OpenSSH buf->size) size: usize, // Size (对应 OpenSSH buf->size)
max_size: usize, // Maximum size (对应 OpenSSH buf->max_size) max_size: usize, // Maximum size (对应 OpenSSH buf->max_size)
} }
impl SshBuf { impl SshBuf {
@@ -32,7 +32,7 @@ impl SshBuf {
max_size: 128 * 1024 * 1024, // 128MB (OpenSSH SSHBUF_SIZE_MAX) max_size: 128 * 1024 * 1024, // 128MB (OpenSSH SSHBUF_SIZE_MAX)
} }
} }
/// 创建指定大小的 SSH Buffer /// 创建指定大小的 SSH Buffer
pub fn with_capacity(capacity: usize) -> Self { pub fn with_capacity(capacity: usize) -> Self {
Self { Self {
@@ -42,7 +42,7 @@ impl SshBuf {
max_size: 128 * 1024 * 1024, max_size: 128 * 1024 * 1024,
} }
} }
/// 设置最大大小 /// 设置最大大小
pub fn set_max_size(&mut self, max_size: usize) -> Result<()> { pub fn set_max_size(&mut self, max_size: usize) -> Result<()> {
if max_size > 128 * 1024 * 1024 { if max_size > 128 * 1024 * 1024 {
@@ -51,47 +51,47 @@ impl SshBuf {
self.max_size = max_size; self.max_size = max_size;
Ok(()) Ok(())
} }
/// 获取 buffer 长度(对应 OpenSSH sshbuf_len) /// 获取 buffer 长度(对应 OpenSSH sshbuf_len)
/// ///
/// OpenSSH: `sshbuf_len = buf->size - buf->off` /// OpenSSH: `sshbuf_len = buf->size - buf->off`
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.size - self.off self.size - self.off
} }
/// 检查 buffer 是否为空 /// 检查 buffer 是否为空
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.len() == 0 self.len() == 0
} }
/// 获取可用空间(对应 OpenSSH sshbuf_avail) /// 获取可用空间(对应 OpenSSH sshbuf_avail)
/// ///
/// OpenSSH: `sshbuf_avail = buf->max_size - buf->size` /// OpenSSH: `sshbuf_avail = buf->max_size - buf->size`
pub fn avail(&self) -> usize { pub fn avail(&self) -> usize {
self.max_size - self.size self.max_size - self.size
} }
/// 获取可变指针(对应 OpenSSH sshbuf_mutable_ptr) /// 获取可变指针(对应 OpenSSH sshbuf_mutable_ptr)
/// ///
/// OpenSSH 实现: /// OpenSSH 实现:
/// ```c /// ```c
/// u_char *sshbuf_mutable_ptr(const struct sshbuf *buf) { /// u_char *sshbuf_mutable_ptr(const struct sshbuf *buf) {
/// return buf->d + buf->off; /// return buf->d + buf->off;
/// } /// }
/// ``` /// ```
/// ///
/// Rust 实现:返回 `&mut [u8]` slice(零拷贝) /// Rust 实现:返回 `&mut [u8]` slice(零拷贝)
pub fn mutable_ptr(&mut self) -> &mut [u8] { pub fn mutable_ptr(&mut self) -> &mut [u8] {
&mut self.data[self.off..self.size] &mut self.data[self.off..self.size]
} }
/// 获取不可变指针(对应 OpenSSH sshbuf_ptr) /// 获取不可变指针(对应 OpenSSH sshbuf_ptr)
pub fn ptr(&self) -> &[u8] { pub fn ptr(&self) -> &[u8] {
&self.data[self.off..self.size] &self.data[self.off..self.size]
} }
/// 预分配空间(对应 OpenSSH sshbuf_reserve) /// 预分配空间(对应 OpenSSH sshbuf_reserve)
/// ///
/// OpenSSH 实现: /// OpenSSH 实现:
/// ```c /// ```c
/// int sshbuf_reserve(struct sshbuf *buf, size_t len, u_char **dpp) { /// int sshbuf_reserve(struct sshbuf *buf, size_t len, u_char **dpp) {
@@ -104,31 +104,31 @@ impl SshBuf {
/// return 0; /// return 0;
/// } /// }
/// ``` /// ```
/// ///
/// Rust 实现:返回 `&mut [u8]` slice(零拷贝,可直接 write) /// Rust 实现:返回 `&mut [u8]` slice(零拷贝,可直接 write)
pub fn reserve(&mut self, len: usize) -> Result<&mut [u8]> { pub fn reserve(&mut self, len: usize) -> Result<&mut [u8]> {
if len > self.avail() { if len > self.avail() {
return Err(anyhow!("no buffer space (avail={})", self.avail())); return Err(anyhow!("no buffer space (avail={})", self.avail()));
} }
// 预分配空间 // 预分配空间
let current_size = self.size; let current_size = self.size;
let new_size = current_size + len; let new_size = current_size + len;
// 确保 Vec 有足够容量 // 确保 Vec 有足够容量
if new_size > self.data.len() { if new_size > self.data.len() {
self.data.resize(new_size, 0); self.data.resize(new_size, 0);
} }
// 更新 size // 更新 size
self.size = new_size; self.size = new_size;
// 返回新空间的 slice(零拷贝) // 返回新空间的 slice(零拷贝)
Ok(&mut self.data[current_size..new_size]) Ok(&mut self.data[current_size..new_size])
} }
/// 消费数据(对应 OpenSSH sshbuf_consume) /// 消费数据(对应 OpenSSH sshbuf_consume)
/// ///
/// OpenSSH 实现: /// OpenSSH 实现:
/// ```c /// ```c
/// int sshbuf_consume(struct sshbuf *buf, size_t len) { /// int sshbuf_consume(struct sshbuf *buf, size_t len) {
@@ -140,29 +140,33 @@ impl SshBuf {
/// return 0; /// return 0;
/// } /// }
/// ``` /// ```
/// ///
/// Rust 实现:移动偏移量(零拷贝,不实际删除数据) /// Rust 实现:移动偏移量(零拷贝,不实际删除数据)
pub fn consume(&mut self, len: usize) -> Result<()> { pub fn consume(&mut self, len: usize) -> Result<()> {
if len > self.len() { if len > self.len() {
return Err(anyhow!("message incomplete (len={}, consume={})", self.len(), len)); return Err(anyhow!(
"message incomplete (len={}, consume={})",
self.len(),
len
));
} }
self.off += len; self.off += len;
// 如果 buffer 空,重置 // 如果 buffer 空,重置
if self.off == self.size { if self.off == self.size {
self.off = 0; self.off = 0;
self.size = 0; self.size = 0;
// OpenSSH: pack buffer(移除已消费的数据) // OpenSSH: pack buffer(移除已消费的数据)
// Rust: 我们保留 Vec,但重置指针 // Rust: 我们保留 Vec,但重置指针
} }
Ok(()) Ok(())
} }
/// 从末尾消费数据(对应 OpenSSH sshbuf_consume_end) /// 从末尾消费数据(对应 OpenSSH sshbuf_consume_end)
/// ///
/// OpenSSH 实现: /// OpenSSH 实现:
/// ```c /// ```c
/// int sshbuf_consume_end(struct sshbuf *buf, size_t len) { /// int sshbuf_consume_end(struct sshbuf *buf, size_t len) {
@@ -174,13 +178,13 @@ impl SshBuf {
if len > self.len() { if len > self.len() {
return Err(anyhow!("message incomplete")); return Err(anyhow!("message incomplete"));
} }
self.size -= len; self.size -= len;
Ok(()) Ok(())
} }
/// 直接从 fd read 到 buffer(对应 OpenSSH sshbuf_read) /// 直接从 fd read 到 buffer(对应 OpenSSH sshbuf_read)
/// ///
/// OpenSSH 实现: /// OpenSSH 实现:
/// ```c /// ```c
/// int sshbuf_read(int fd, struct sshbuf *buf, size_t maxlen, size_t *rlen) { /// int sshbuf_read(int fd, struct sshbuf *buf, size_t maxlen, size_t *rlen) {
@@ -195,71 +199,75 @@ impl SshBuf {
/// return 0; /// return 0;
/// } /// }
/// ``` /// ```
/// ///
/// Rust 实现:零拷贝,直接 read 到 buffer /// Rust 实现:零拷贝,直接 read 到 buffer
pub fn read_from<R: Read>(&mut self, reader: &mut R, maxlen: usize) -> Result<usize> { pub fn read_from<R: Read>(&mut self, reader: &mut R, maxlen: usize) -> Result<usize> {
// 1. reserve 空间 // 1. reserve 空间
let space = self.reserve(maxlen)?; let space = self.reserve(maxlen)?;
// 2. 直接 read 到 buffer(零拷贝) // 2. 直接 read 到 buffer(零拷贝)
let n = reader.read(space)?; let n = reader.read(space)?;
// 3. 调整大小(移除未使用的空间) // 3. 调整大小(移除未使用的空间)
if maxlen > n { if maxlen > n {
self.consume_end(maxlen - n)?; self.consume_end(maxlen - n)?;
} }
Ok(n) Ok(n)
} }
/// 直接从 buffer write 到 fd(对应 OpenSSH channel_handle_wfd) /// 直接从 buffer write 到 fd(对应 OpenSSH channel_handle_wfd)
/// ///
/// OpenSSH 实现: /// OpenSSH 实现:
/// ```c /// ```c
/// buf = sshbuf_mutable_ptr(c->output); // 获取指针 /// buf = sshbuf_mutable_ptr(c->output); // 获取指针
/// len = write(c->wfd, buf, dlen); // 直接 write /// len = write(c->wfd, buf, dlen); // 直接 write
/// sshbuf_consume(c->output, len); // 消费已写入的数据 /// sshbuf_consume(c->output, len); // 消费已写入的数据
/// ``` /// ```
/// ///
/// Rust 实现:零拷贝,直接 write 从 buffer /// Rust 实现:零拷贝,直接 write 从 buffer
pub fn write_to<W: Write>(&mut self, writer: &mut W) -> Result<usize> { pub fn write_to<W: Write>(&mut self, writer: &mut W) -> Result<usize> {
if self.is_empty() { if self.is_empty() {
return Ok(0); return Ok(0);
} }
// 1. 获取数据指针(零拷贝) // 1. 获取数据指针(零拷贝)
let data = self.ptr(); let data = self.ptr();
// 2. 直接 write(零拷贝) // 2. 直接 write(零拷贝)
let n = writer.write(data)?; let n = writer.write(data)?;
// 3. 消费已写入的数据(零拷贝,只移动偏移) // 3. 消费已写入的数据(零拷贝,只移动偏移)
self.consume(n)?; self.consume(n)?;
Ok(n) Ok(n)
} }
/// 添加数据(对应 OpenSSH sshbuf_put) /// 添加数据(对应 OpenSSH sshbuf_put)
/// ///
/// 用于不需要零拷贝的场景 /// 用于不需要零拷贝的场景
pub fn put(&mut self, data: &[u8]) -> Result<()> { pub fn put(&mut self, data: &[u8]) -> Result<()> {
let space = self.reserve(data.len())?; let space = self.reserve(data.len())?;
space.copy_from_slice(data); space.copy_from_slice(data);
Ok(()) Ok(())
} }
/// 清空 buffer /// 清空 buffer
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.off = 0; self.off = 0;
self.size = 0; self.size = 0;
// OpenSSH: 保留 Vec,只重置指针 // OpenSSH: 保留 Vec,只重置指针
} }
/// Debug: 打印 buffer 状态 /// Debug: 打印 buffer 状态
pub fn debug_info(&self) -> String { pub fn debug_info(&self) -> String {
format!( format!(
"SshBuf: off={}, size={}, len={}, alloc={}, max_size={}", "SshBuf: off={}, size={}, len={}, alloc={}, max_size={}",
self.off, self.size, self.len(), self.data.len(), self.max_size self.off,
self.size,
self.len(),
self.data.len(),
self.max_size
) )
} }
} }
@@ -274,11 +282,11 @@ impl Default for SshBuf {
mod tests { mod tests {
use super::*; use super::*;
use std::io::Cursor; use std::io::Cursor;
#[test] #[test]
fn test_sshbuf_basic() { fn test_sshbuf_basic() {
let mut buf = SshBuf::new(); let mut buf = SshBuf::new();
// Test reserve - write into reserved space // Test reserve - write into reserved space
{ {
let space = buf.reserve(10).unwrap(); let space = buf.reserve(10).unwrap();
@@ -286,57 +294,57 @@ mod tests {
space[0] = 1; space[0] = 1;
space[1] = 2; space[1] = 2;
} // space dropped, buf accessible } // space dropped, buf accessible
// Verify buffer length after reserve // Verify buffer length after reserve
assert_eq!(buf.len(), 10); assert_eq!(buf.len(), 10);
let ptr = buf.mutable_ptr(); let ptr = buf.mutable_ptr();
assert_eq!(ptr[0], 1); assert_eq!(ptr[0], 1);
assert_eq!(ptr[1], 2); assert_eq!(ptr[1], 2);
// Test consume // Test consume
buf.consume(2).unwrap(); buf.consume(2).unwrap();
assert_eq!(buf.len(), 8); assert_eq!(buf.len(), 8);
assert_eq!(buf.off, 2); assert_eq!(buf.off, 2);
} }
#[test] #[test]
fn test_sshbuf_zero_copy_read() { fn test_sshbuf_zero_copy_read() {
let mut buf = SshBuf::with_capacity(100); let mut buf = SshBuf::with_capacity(100);
let mut reader = Cursor::new("hello world"); let mut reader = Cursor::new("hello world");
// 零拷贝 read // 零拷贝 read
let n = buf.read_from(&mut reader, 20).unwrap(); let n = buf.read_from(&mut reader, 20).unwrap();
assert_eq!(n, 11); // "hello world" length assert_eq!(n, 11); // "hello world" length
assert_eq!(buf.len(), 11); assert_eq!(buf.len(), 11);
// 检查数据 // 检查数据
let data = buf.ptr(); let data = buf.ptr();
assert_eq!(data, "hello world".as_bytes()); assert_eq!(data, "hello world".as_bytes());
} }
#[test] #[test]
fn test_sshbuf_zero_copy_write() { fn test_sshbuf_zero_copy_write() {
let mut buf = SshBuf::new(); let mut buf = SshBuf::new();
buf.put("hello world".as_bytes()).unwrap(); buf.put("hello world".as_bytes()).unwrap();
let mut writer = Vec::new(); let mut writer = Vec::new();
// 零拷贝 write // 零拷贝 write
let n = buf.write_to(&mut writer).unwrap(); let n = buf.write_to(&mut writer).unwrap();
assert_eq!(n, 11); assert_eq!(n, 11);
assert_eq!(buf.len(), 0); // 已消费 assert_eq!(buf.len(), 0); // 已消费
// 检查数据 // 检查数据
assert_eq!(writer, "hello world".as_bytes()); assert_eq!(writer, "hello world".as_bytes());
} }
#[test] #[test]
fn test_sshbuf_max_size() { fn test_sshbuf_max_size() {
let mut buf = SshBuf::new(); let mut buf = SshBuf::new();
buf.set_max_size(1000).unwrap(); buf.set_max_size(1000).unwrap();
// 尝试 reserve 超过 max_size // 尝试 reserve 超过 max_size
let result = buf.reserve(2000); let result = buf.reserve(2000);
assert!(result.is_err()); assert!(result.is_err());
} }
} }

Some files were not shown because too many files have changed in this diff Show More