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

经Linus式架构审计,发现并修复插件系统中的具体问题: ## 核心修复 ### 1. 消除local插件GetPorts()方法冗余 - 删除21个local插件中无意义的GetPorts()方法 - 简化local.Plugin接口:移除端口概念 - 理由:本地插件不涉及网络,端口概念完全多余 ### 2. 消除web插件GetPorts()方法冗余 - 删除2个web插件中无用的GetPorts()方法 - 简化web.WebPlugin接口:专注智能HTTP检测 - 理由:Web插件使用动态HTTP检测,预定义端口无价值 ### 3. 统一插件命名规范 - 统一所有插件接口使用Name()方法(符合Go惯例) - 消除GetName()与Name()不一致问题 - 简化适配器:不再需要方法名转换 ## 技术改进 接口精简: - local插件:GetName() + GetPorts() → Name() - web插件:GetName() + GetPorts() → Name() - services插件:GetName() → Name()(保留GetPorts(),业务必需) 代码减少: - 删除23个无用GetPorts()方法 - 重命名52个Name()方法 - 简化3个插件接口定义 ## 影响范围 修改文件:55个插件文件 代码变更:-155行 +61行(净减少94行) 功能影响:零破坏性,保持所有业务逻辑不变 这是基于业务需求分析的精准重构,消除真正多余的部分, 保持系统架构合理性和向后兼容性。
254 lines
6.4 KiB
Go
254 lines
6.4 KiB
Go
package local
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/shadow1ng/fscan/common"
|
||
)
|
||
|
||
// DownloaderPlugin 文件下载插件 - Linus式简化版本
|
||
//
|
||
// 设计哲学:直接实现,删除过度设计
|
||
// - 删除复杂的继承体系
|
||
// - 直接实现文件下载功能
|
||
// - 保持原有功能逻辑
|
||
type DownloaderPlugin struct {
|
||
name string
|
||
downloadURL string
|
||
savePath string
|
||
downloadTimeout time.Duration
|
||
maxFileSize int64
|
||
}
|
||
|
||
// NewDownloaderPlugin 创建文件下载插件
|
||
func NewDownloaderPlugin() *DownloaderPlugin {
|
||
return &DownloaderPlugin{
|
||
name: "downloader",
|
||
downloadURL: common.DownloadURL,
|
||
savePath: common.DownloadSavePath,
|
||
downloadTimeout: 30 * time.Second,
|
||
maxFileSize: 100 * 1024 * 1024, // 100MB
|
||
}
|
||
}
|
||
|
||
// GetName 实现Plugin接口
|
||
func (p *DownloaderPlugin) Name() string {
|
||
return p.name
|
||
}
|
||
|
||
|
||
// Scan 执行文件下载任务 - 直接实现
|
||
func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||
var output strings.Builder
|
||
|
||
output.WriteString("=== 文件下载 ===\n")
|
||
|
||
// 验证参数
|
||
if err := p.validateParameters(); err != nil {
|
||
output.WriteString(fmt.Sprintf("参数验证失败: %v\n", err))
|
||
return &ScanResult{
|
||
Success: false,
|
||
Output: output.String(),
|
||
Error: err,
|
||
}
|
||
}
|
||
|
||
output.WriteString(fmt.Sprintf("下载URL: %s\n", p.downloadURL))
|
||
output.WriteString(fmt.Sprintf("保存路径: %s\n", p.savePath))
|
||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||
|
||
// 检查保存路径权限
|
||
if err := p.checkSavePathPermissions(); err != nil {
|
||
output.WriteString(fmt.Sprintf("保存路径检查失败: %v\n", err))
|
||
return &ScanResult{
|
||
Success: false,
|
||
Output: output.String(),
|
||
Error: err,
|
||
}
|
||
}
|
||
|
||
// 执行下载
|
||
downloadInfo, err := p.downloadFile(ctx)
|
||
if err != nil {
|
||
output.WriteString(fmt.Sprintf("下载失败: %v\n", err))
|
||
return &ScanResult{
|
||
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(fmt.Sprintf("文件下载完成: %s -> %s (大小: %v bytes)",
|
||
p.downloadURL, p.savePath, downloadInfo["file_size"]))
|
||
|
||
return &ScanResult{
|
||
Success: true,
|
||
Output: output.String(),
|
||
Error: 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
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
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")
|
||
|
||
// 发送请求
|
||
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)
|
||
}
|
||
|
||
// 创建保存文件
|
||
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
|
||
}
|
||
|
||
// 注册插件
|
||
func init() {
|
||
RegisterLocalPlugin("downloader", func() Plugin {
|
||
return NewDownloaderPlugin()
|
||
})
|
||
} |