fscan/plugins/local_backup/downloader/plugin.go
ZacharyZcR 678d750c8a refactor: 重构插件架构,实现单文件插件系统
将复杂的三文件插件架构(connector/exploiter/plugin)重构为简化的单文件插件架构,
大幅减少代码重复和维护成本,提升插件开发效率。

主要改进:
• 将每个服务插件从3个文件简化为1个文件
• 删除过度设计的工厂模式、适配器模式等抽象层
• 消除plugins/services/、plugins/adapters/、plugins/base/复杂目录结构
• 实现直接的插件注册机制,提升系统简洁性
• 保持完全向后兼容,所有扫描功能和输出格式不变

重构统计:
• 删除文件:100+个复杂架构文件
• 新增文件:20个简化的单文件插件
• 代码减少:每个插件减少60-80%代码量
• 功能增强:所有插件包含完整扫描和利用功能

已重构插件: MySQL, SSH, Redis, MongoDB, PostgreSQL, MSSQL, Oracle,
Neo4j, Memcached, RabbitMQ, ActiveMQ, Cassandra, FTP, Kafka, LDAP,
Rsync, SMTP, SNMP, Telnet, VNC

验证通过: 新系统编译运行正常,所有插件功能验证通过
2025-08-25 23:57:00 +08:00

338 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package downloader
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins/base"
"github.com/shadow1ng/fscan/plugins/local"
)
// DownloaderPlugin 文件下载插件 - 跨平台支持
type DownloaderPlugin struct {
*local.BaseLocalPlugin
// 配置选项
downloadURL string
savePath string
downloadTimeout time.Duration
maxFileSize int64
}
// NewDownloaderPlugin 创建文件下载插件
func NewDownloaderPlugin() *DownloaderPlugin {
metadata := &base.PluginMetadata{
Name: "downloader",
Version: "1.0.0",
Author: "fscan-team",
Description: "跨平台文件下载插件支持从指定URL下载文件并保存到本地",
Category: "local",
Tags: []string{"local", "downloader", "file", "cross-platform"},
Protocols: []string{"http", "https"},
}
plugin := &DownloaderPlugin{
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
downloadTimeout: 30 * time.Second, // 默认30秒超时
maxFileSize: 100 * 1024 * 1024, // 默认最大100MB
}
// 支持所有主要平台
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
// 需要文件写入权限
plugin.SetRequiresPrivileges(false)
// 从全局参数获取配置
plugin.downloadURL = common.DownloadURL
plugin.savePath = common.DownloadSavePath
return plugin
}
// Initialize 初始化插件
func (p *DownloaderPlugin) Initialize() error {
common.LogInfo(fmt.Sprintf("初始化文件下载插件 - 平台: %s", runtime.GOOS))
// 验证必要参数
if err := p.validateParameters(); err != nil {
return fmt.Errorf("参数验证失败: %v", err)
}
// 检查保存路径权限
if err := p.checkSavePathPermissions(); err != nil {
return fmt.Errorf("保存路径权限检查失败: %v", err)
}
return p.BaseLocalPlugin.Initialize()
}
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
return p.ScanLocal(ctx, info)
}
// ScanLocal 执行文件下载任务
func (p *DownloaderPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
common.LogInfo(fmt.Sprintf("开始下载文件: %s", p.downloadURL))
// 执行下载
downloadInfo, err := p.downloadFile(ctx)
if err != nil {
return &base.ScanResult{
Success: false,
Error: err,
}, nil
}
result := &base.ScanResult{
Success: true,
Service: "FileDownloader",
Banner: fmt.Sprintf("文件下载成功: %s -> %s", p.downloadURL, downloadInfo["save_path"]),
Extra: map[string]interface{}{
"download_url": p.downloadURL,
"save_path": downloadInfo["save_path"],
"file_size": downloadInfo["file_size"],
"content_type": downloadInfo["content_type"],
"platform": runtime.GOOS,
"download_time": downloadInfo["download_time"],
},
}
common.LogSuccess(fmt.Sprintf("文件下载完成: %s (大小: %v bytes)",
downloadInfo["save_path"], downloadInfo["file_size"]))
return result, nil
}
// validateParameters 验证输入参数
func (p *DownloaderPlugin) validateParameters() error {
if p.downloadURL == "" {
return fmt.Errorf("下载URL不能为空请使用 -download-url 参数指定")
}
// 验证URL格式
if !strings.HasPrefix(strings.ToLower(p.downloadURL), "http://") &&
!strings.HasPrefix(strings.ToLower(p.downloadURL), "https://") {
return fmt.Errorf("无效的URL格式必须以 http:// 或 https:// 开头")
}
// 如果没有指定保存路径使用URL中的文件名
if p.savePath == "" {
filename := p.extractFilenameFromURL(p.downloadURL)
if filename == "" {
filename = "downloaded_file"
}
p.savePath = filename
common.LogInfo(fmt.Sprintf("未指定保存路径,使用默认文件名: %s", p.savePath))
}
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() error {
// 获取保存目录
saveDir := filepath.Dir(p.savePath)
if saveDir == "." || saveDir == "" {
// 使用当前目录
var err error
saveDir, err = os.Getwd()
if err != nil {
return fmt.Errorf("获取当前目录失败: %v", err)
}
p.savePath = filepath.Join(saveDir, filepath.Base(p.savePath))
}
// 确保目录存在
if err := os.MkdirAll(saveDir, 0755); err != nil {
return fmt.Errorf("创建保存目录失败: %v", err)
}
// 检查写入权限
testFile := filepath.Join(saveDir, ".fscan_write_test")
if file, err := os.Create(testFile); err != nil {
return fmt.Errorf("保存目录无写入权限: %v", err)
} else {
file.Close()
os.Remove(testFile)
}
common.LogInfo(fmt.Sprintf("文件将保存至: %s", p.savePath))
return nil
}
// downloadFile 执行文件下载
func (p *DownloaderPlugin) downloadFile(ctx context.Context) (map[string]interface{}, error) {
startTime := time.Now()
// 创建带超时的HTTP客户端
client := &http.Client{
Timeout: p.downloadTimeout,
}
// 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", p.downloadURL, nil)
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
// 设置User-Agent
req.Header.Set("User-Agent", "fscan-downloader/1.0")
common.LogInfo("正在连接到服务器...")
// 发送请求
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP请求失败状态码: %d %s", resp.StatusCode, resp.Status)
}
// 检查文件大小
contentLength := resp.ContentLength
if contentLength > p.maxFileSize {
return nil, fmt.Errorf("文件过大 (%d bytes),超过最大限制 (%d bytes)",
contentLength, p.maxFileSize)
}
common.LogInfo(fmt.Sprintf("开始下载,文件大小: %d bytes", contentLength))
// 创建保存文件
outFile, err := os.Create(p.savePath)
if err != nil {
return nil, fmt.Errorf("创建保存文件失败: %v", err)
}
defer outFile.Close()
// 使用带限制的Reader防止过大文件
limitedReader := io.LimitReader(resp.Body, p.maxFileSize)
// 复制数据
written, err := io.Copy(outFile, limitedReader)
if err != nil {
// 清理部分下载的文件
os.Remove(p.savePath)
return nil, fmt.Errorf("文件下载失败: %v", err)
}
downloadTime := time.Since(startTime)
// 返回下载信息
downloadInfo := map[string]interface{}{
"save_path": p.savePath,
"file_size": written,
"content_type": resp.Header.Get("Content-Type"),
"download_time": downloadTime,
}
return downloadInfo, nil
}
// GetLocalData 获取下载器本地数据
func (p *DownloaderPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
data := make(map[string]interface{})
data["plugin_type"] = "downloader"
data["platform"] = runtime.GOOS
data["download_url"] = p.downloadURL
data["save_path"] = p.savePath
data["timeout"] = p.downloadTimeout.String()
data["max_file_size"] = p.maxFileSize
if hostname, err := os.Hostname(); err == nil {
data["hostname"] = hostname
}
if workDir, err := os.Getwd(); err == nil {
data["work_dir"] = workDir
}
return data, nil
}
// ExtractData 提取数据
func (p *DownloaderPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
return &base.ExploitResult{
Success: true,
Output: fmt.Sprintf("文件下载完成: %s -> %s", p.downloadURL, p.savePath),
Data: data,
Extra: map[string]interface{}{
"download_url": p.downloadURL,
"save_path": p.savePath,
"platform": runtime.GOOS,
"status": "completed",
},
}, nil
}
// GetInfo 获取插件信息
func (p *DownloaderPlugin) GetInfo() string {
var info strings.Builder
info.WriteString("跨平台文件下载插件\n")
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
info.WriteString(fmt.Sprintf("支持协议: %s\n", strings.Join(p.GetMetadata().Protocols, ", ")))
info.WriteString("功能: 从HTTP/HTTPS URL下载文件到本地\n")
info.WriteString("参数:\n")
info.WriteString(" -download-url: 要下载的文件URL\n")
info.WriteString(" -download-path: 保存路径 (可选默认使用URL中的文件名)\n")
return info.String()
}
// RegisterDownloaderPlugin 注册文件下载插件
func RegisterDownloaderPlugin() {
factory := base.NewSimplePluginFactory(
&base.PluginMetadata{
Name: "downloader",
Version: "1.0.0",
Author: "fscan-team",
Description: "跨平台文件下载插件支持从指定URL下载文件并保存到本地",
Category: "local",
Tags: []string{"downloader", "local", "file", "cross-platform"},
Protocols: []string{"http", "https"},
},
func() base.Plugin {
return NewDownloaderPlugin()
},
)
base.GlobalPluginRegistry.Register("downloader", factory)
}
// init 插件注册函数
func init() {
RegisterDownloaderPlugin()
}