Files
fscan/plugins/local/downloader.go
T
ZacharyZcR 71b92d4408 feat: v2.1.0 核心重构与功能增强
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n

## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池

## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)

## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建

## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
  JSON 输出格式、Redis 检测、Context 超时等

## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
2026-01-11 20:16:23 +08:00

251 lines
6.8 KiB
Go

//go:build (plugin_downloader || !plugin_selective) && !no_local
package local
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins"
)
// DownloaderPlugin 文件下载插件
// 设计哲学:直接实现,删除过度设计
// - 删除复杂的继承体系
// - 直接实现文件下载功能
// - 保持原有功能逻辑
type DownloaderPlugin struct {
plugins.BasePlugin
}
// NewDownloaderPlugin 创建文件下载插件
func NewDownloaderPlugin() *DownloaderPlugin {
return &DownloaderPlugin{
BasePlugin: plugins.NewBasePlugin("downloader"),
}
}
// Scan 执行文件下载任务 - 直接实现
func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
var output strings.Builder
// 从config获取配置
downloadURL := config.LocalExploit.DownloadURL
savePath := config.LocalExploit.DownloadSavePath
downloadTimeout := 30 * time.Second
maxFileSize := int64(100 * 1024 * 1024) // 100MB
output.WriteString("=== 文件下载 ===\n")
// 验证参数
if err := p.validateParameters(downloadURL, &savePath); err != nil {
output.WriteString(fmt.Sprintf("参数验证失败: %v\n", err))
return &plugins.Result{
Success: false,
Output: output.String(),
Error: err,
}
}
output.WriteString(fmt.Sprintf("下载URL: %s\n", downloadURL))
output.WriteString(fmt.Sprintf("保存路径: %s\n", savePath))
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
// 检查保存路径权限
if err := p.checkSavePathPermissions(&savePath); err != nil {
output.WriteString(fmt.Sprintf("保存路径检查失败: %v\n", err))
return &plugins.Result{
Success: false,
Output: output.String(),
Error: err,
}
}
// 执行下载
downloadInfo, err := p.downloadFile(ctx, downloadURL, savePath, downloadTimeout, maxFileSize)
if err != nil {
output.WriteString(fmt.Sprintf("下载失败: %v\n", err))
return &plugins.Result{
Success: false,
Output: output.String(),
Error: err,
}
}
// 输出下载结果
output.WriteString("✓ 文件下载成功!\n")
output.WriteString(fmt.Sprintf("文件大小: %v bytes\n", downloadInfo["file_size"]))
if contentType, ok := downloadInfo["content_type"]; ok && contentType != "" {
output.WriteString(fmt.Sprintf("文件类型: %v\n", contentType))
}
output.WriteString(fmt.Sprintf("下载用时: %v\n", downloadInfo["download_time"]))
common.LogSuccess(i18n.Tr("downloader_success",
downloadURL, savePath, downloadInfo["file_size"]))
return &plugins.Result{
Success: true,
Type: plugins.ResultTypeService,
Output: output.String(),
Error: nil,
}
}
// validateParameters 验证输入参数
func (p *DownloaderPlugin) validateParameters(downloadURL string, savePath *string) error {
if downloadURL == "" {
return fmt.Errorf("下载URL不能为空,请使用 -download-url 参数指定")
}
// 验证URL格式
if !strings.HasPrefix(strings.ToLower(downloadURL), "http://") &&
!strings.HasPrefix(strings.ToLower(downloadURL), "https://") {
return fmt.Errorf("无效的URL格式,必须以 http:// 或 https:// 开头")
}
// 如果没有指定保存路径,使用URL中的文件名
if *savePath == "" {
filename := p.extractFilenameFromURL(downloadURL)
if filename == "" {
filename = "downloaded_file"
}
*savePath = filename
}
return nil
}
// extractFilenameFromURL 从URL中提取文件名
func (p *DownloaderPlugin) extractFilenameFromURL(url string) string {
// 移除查询参数
if idx := strings.Index(url, "?"); idx != -1 {
url = url[:idx]
}
// 获取路径的最后一部分
parts := strings.Split(url, "/")
if len(parts) > 0 {
filename := parts[len(parts)-1]
if filename != "" && !strings.Contains(filename, "=") {
return filename
}
}
return ""
}
// checkSavePathPermissions 检查保存路径权限
func (p *DownloaderPlugin) checkSavePathPermissions(savePath *string) error {
// 获取保存目录
saveDir := filepath.Dir(*savePath)
if saveDir == "." || saveDir == "" {
// 使用当前目录
var err error
saveDir, err = os.Getwd()
if err != nil {
return fmt.Errorf("获取当前目录失败: %w", err)
}
*savePath = filepath.Join(saveDir, filepath.Base(*savePath))
}
// 确保目录存在
if err := os.MkdirAll(saveDir, 0755); err != nil {
return fmt.Errorf("创建保存目录失败: %w", err)
}
// 检查写入权限
testFile := filepath.Join(saveDir, ".fscan_write_test")
file, err := os.Create(testFile)
if err != nil {
return fmt.Errorf("保存目录无写入权限: %w", err)
}
_ = file.Close() // 测试文件,Close错误可忽略
_ = os.Remove(testFile)
return nil
}
// downloadFile 执行文件下载
func (p *DownloaderPlugin) downloadFile(ctx context.Context, downloadURL, savePath string, downloadTimeout time.Duration, maxFileSize int64) (map[string]interface{}, error) {
startTime := time.Now()
// 创建带超时的HTTP客户端
client := &http.Client{
Timeout: downloadTimeout,
}
// 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 设置User-Agent
req.Header.Set("User-Agent", "fscan-downloader/1.0")
// 发送请求
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("HTTP请求失败: %w", err)
}
defer func() { _ = resp.Body.Close() }() // HTTP响应体,Close错误可安全忽略
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP请求失败,状态码: %d %s", resp.StatusCode, resp.Status)
}
// 检查文件大小
contentLength := resp.ContentLength
if contentLength > maxFileSize {
return nil, fmt.Errorf("文件过大 (%d bytes),超过最大限制 (%d bytes)",
contentLength, maxFileSize)
}
// 创建保存文件
outFile, err := os.Create(savePath)
if err != nil {
return nil, fmt.Errorf("创建保存文件失败: %w", err)
}
defer func() { _ = outFile.Close() }() // 文件资源清理,Close错误可安全忽略
// 使用带限制的Reader防止过大文件
limitedReader := io.LimitReader(resp.Body, maxFileSize)
// 复制数据
written, err := io.Copy(outFile, limitedReader)
if err != nil {
// 清理部分下载的文件
_ = os.Remove(savePath) // 清理临时文件,Remove错误可忽略
return nil, fmt.Errorf("文件下载失败: %w", err)
}
downloadTime := time.Since(startTime)
// 返回下载信息
downloadInfo := map[string]interface{}{
"save_path": savePath,
"file_size": written,
"content_type": resp.Header.Get("Content-Type"),
"download_time": downloadTime,
}
return downloadInfo, nil
}
// 注册插件
func init() {
RegisterLocalPlugin("downloader", func() Plugin {
return NewDownloaderPlugin()
})
}