154 lines
4.0 KiB
Go
154 lines
4.0 KiB
Go
package installer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
"yizhisec.com/hsv2/forge/pkg/logger"
|
|
)
|
|
|
|
type installer struct {
|
|
workdir string
|
|
target string
|
|
}
|
|
|
|
func (i *installer) buildCommand(ctx context.Context, cmds ...string) *exec.Cmd {
|
|
if len(cmds) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if i.target == "self" {
|
|
return exec.CommandContext(ctx, cmds[0], cmds[1:]...)
|
|
}
|
|
|
|
sshArgs := append([]string{i.target}, cmds...)
|
|
return exec.CommandContext(ctx, "ssh", sshArgs...)
|
|
}
|
|
|
|
// ExecuteCommand implements syscheck.CommandExecutor interface
|
|
func (i *installer) ExecuteCommand(ctx context.Context, cmds ...string) (string, error) {
|
|
cmd := i.buildCommand(ctx, cmds...)
|
|
if cmd == nil {
|
|
return "", fmt.Errorf("failed to build command")
|
|
}
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("command failed: %w, output: %s", err, string(output))
|
|
}
|
|
|
|
return string(output), nil
|
|
}
|
|
|
|
type CopyFileOption func(*copyFileOptions)
|
|
type copyFileOptions struct {
|
|
addExecutable bool
|
|
}
|
|
|
|
func withCopyFileExecutable() func(*copyFileOptions) {
|
|
return func(o *copyFileOptions) {
|
|
o.addExecutable = true
|
|
}
|
|
}
|
|
|
|
func (i *installer) copyFile(ctx context.Context, src, dst string, opts ...CopyFileOption) error {
|
|
logger.Debug("☑️ installer.copyFile: Copying file from %s to %s (target: %s)", src, dst, i.target)
|
|
|
|
var (
|
|
err error
|
|
o = ©FileOptions{}
|
|
srcFile, dstFile *os.File
|
|
srcInfo os.FileInfo
|
|
)
|
|
|
|
for _, fn := range opts {
|
|
fn(o)
|
|
}
|
|
|
|
if i.target == "self" {
|
|
// Simply copy file locally
|
|
logger.Debug("Copying file locally: %s -> %s", src, dst)
|
|
|
|
// Open source file
|
|
if srcFile, err = os.Open(src); err != nil {
|
|
logger.Error("❌ Failed to open source file %s: %v", src, err)
|
|
return fmt.Errorf("failed to open source file: %w", err)
|
|
}
|
|
defer srcFile.Close()
|
|
|
|
// Create destination directory if needed
|
|
dstDir := filepath.Dir(dst)
|
|
if err = os.MkdirAll(dstDir, 0755); err != nil {
|
|
logger.Error("❌ Failed to create destination directory %s: %v", dstDir, err)
|
|
return fmt.Errorf("failed to create destination directory: %w", err)
|
|
}
|
|
|
|
// Create destination file
|
|
if dstFile, err = os.Create(dst); err != nil {
|
|
logger.Error("❌ Failed to create destination file %s: %v", dst, err)
|
|
return fmt.Errorf("failed to create destination file: %w", err)
|
|
}
|
|
defer dstFile.Close()
|
|
|
|
// Copy file content
|
|
if _, err = io.Copy(dstFile, srcFile); err != nil {
|
|
logger.Error("❌ Failed to copy file content: %v", err)
|
|
return fmt.Errorf("failed to copy file content: %w", err)
|
|
}
|
|
|
|
// Get source file permissions
|
|
if srcInfo, err = os.Stat(src); err == nil {
|
|
if err = os.Chmod(dst, srcInfo.Mode()); err != nil {
|
|
logger.Debug("⚠️ Failed to set file permissions: %v", err)
|
|
}
|
|
}
|
|
|
|
logger.Info("✅ File copied locally: %s -> %s", src, dst)
|
|
return nil
|
|
}
|
|
|
|
// Copy file via scp to remote target
|
|
logger.Debug("Copying file via scp: %s -> %s:%s", src, i.target, dst)
|
|
|
|
// Format: scp <src> <target>:<dst>
|
|
cmd := exec.CommandContext(ctx, "scp", src, fmt.Sprintf("%s:%s", i.target, dst))
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
logger.Error("❌ Failed to copy file via scp: %v, output: %s", err, string(output))
|
|
return fmt.Errorf("failed to copy file via scp: %w, output: %s", err, string(output))
|
|
}
|
|
|
|
logger.Info("✅ File copied via scp: %s -> %s:%s", src, i.target, dst)
|
|
return nil
|
|
}
|
|
|
|
func (i *installer) targetOK(ctx context.Context) error {
|
|
cmd := i.buildCommand(ctx, "whoami")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
logger.Debug("❌ installer.targetOK: check target %s failed, err = %v", i.target, err)
|
|
return err
|
|
}
|
|
|
|
if string(output) != "root\n" {
|
|
logger.Debug("❌ installer.targetOK: check target %s failed, output = %s", i.target, string(output))
|
|
return errors.New("target is not root user")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func NewInstaller(workdir, target string) *installer {
|
|
if target == "" {
|
|
logger.Warn("🎯 NewInstaller: target empty, set to default(self)")
|
|
target = "self"
|
|
}
|
|
|
|
return &installer{workdir: workdir, target: target}
|
|
}
|