Files
fscan/plugins/local/downloader.go
T
ZacharyZcR a865e5d45e refactor: 引入 ScanSession,替代全局状态穿透扫描管道 (Phase 1-3)
- 新增 common/session.go: ScanSession 结构体封装 Config/State/Params/Dialer
- RunScan/Strategy/ExecuteScanTasks/executeScanTask 全部接收 session
- Plugin 接口从 Scan(ctx, info, config, state) 改为 Scan(ctx, info, session)
- 48 个插件实现统一更新签名
- Web API 构建 ScanSession 传给 RunScan
- CLI 模式通过 Initialize() 创建 session
2026-04-27 23:00:06 +08:00

252 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, session *common.ScanSession) *plugins.Result {
config := session.Config
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()
})
}