mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00

将复杂的三文件插件架构(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 验证通过: 新系统编译运行正常,所有插件功能验证通过
338 lines
9.4 KiB
Go
338 lines
9.4 KiB
Go
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()
|
||
} |