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() }