package template import ( "bytes" "context" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "github.com/danielmiessler/fabric/internal/i18n" ) // ExtensionExecutor handles the secure execution of extensions // It uses the registry to verify extensions before running them type ExtensionExecutor struct { registry *ExtensionRegistry } // NewExtensionExecutor creates a new executor instance // It requires a registry to verify extensions func NewExtensionExecutor(registry *ExtensionRegistry) *ExtensionExecutor { return &ExtensionExecutor{ registry: registry, } } // Execute runs an extension with the given operation and value string // name: the registered name of the extension // operation: the operation to perform // value: the input value(s) for the operation // In extension_executor.go func (e *ExtensionExecutor) Execute(name, operation, value string) (string, error) { // Get and verify extension from registry ext, err := e.registry.GetExtension(name) if err != nil { return "", fmt.Errorf(i18n.T("extension_failed_get_extension"), err) } // Format the command using our template system cmdStr, err := e.formatCommand(ext, operation, value) if err != nil { return "", fmt.Errorf(i18n.T("extension_failed_format_command"), err) } // Split the command string into command and arguments cmdParts := strings.Fields(cmdStr) if len(cmdParts) < 1 { return "", fmt.Errorf("%s", i18n.T("extension_empty_command")) } // Create command with the Executable and formatted arguments cmd := exec.Command("sh", "-c", cmdStr) //cmd := exec.Command(cmdParts[0], cmdParts[1:]...) // Set up environment if specified if len(ext.Env) > 0 { cmd.Env = append(os.Environ(), ext.Env...) } // Execute based on output method outputMethod := ext.GetOutputMethod() if outputMethod == "file" { return e.executeWithFile(cmd, ext) } return e.executeStdout(cmd, ext) } // formatCommand uses fabric's template system to format the command // It creates a variables map for the template system using the input values func (e *ExtensionExecutor) formatCommand(ext *ExtensionDefinition, operation string, value string) (string, error) { // Get operation config opConfig, exists := ext.Operations[operation] if !exists { return "", fmt.Errorf("%s", fmt.Sprintf(i18n.T("extension_operation_not_found"), operation, ext.Name)) } vars := make(map[string]string) vars["executable"] = ext.Executable vars["operation"] = operation vars["value"] = value // Split on pipe for numbered variables values := strings.Split(value, "|") for i, val := range values { vars[fmt.Sprintf("%d", i+1)] = val } return ApplyTemplate(opConfig.CmdTemplate, vars, "") } // executeStdout runs the command and captures its stdout func (e *ExtensionExecutor) executeStdout(cmd *exec.Cmd, ext *ExtensionDefinition) (string, error) { var stdout bytes.Buffer var stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr //debug output fmt.Printf(i18n.T("extension_executing_command"), cmd.String()) if err := cmd.Run(); err != nil { return "", fmt.Errorf(i18n.T("extension_execution_failed_stderr"), err, stderr.String()) } return stdout.String(), nil } // executeWithFile runs the command and handles file-based output func (e *ExtensionExecutor) executeWithFile(cmd *exec.Cmd, ext *ExtensionDefinition) (string, error) { // Parse timeout - this is now a first-class field timeout, err := time.ParseDuration(ext.Timeout) if err != nil { return "", fmt.Errorf(i18n.T("extension_invalid_timeout_format"), err) } // Create context with timeout ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // Store the original environment originalEnv := cmd.Env // Create a new command with context. This might reset Env, depending on the Go version. cmd = exec.CommandContext(ctx, cmd.Path, cmd.Args[1:]...) // Restore the environment variables explicitly cmd.Env = originalEnv fileConfig := ext.GetFileConfig() if fileConfig == nil { return "", fmt.Errorf("%s", i18n.T("extension_no_file_config")) } // Handle path from stdout case if pathFromStdout, ok := fileConfig["path_from_stdout"].(bool); ok && pathFromStdout { return e.handlePathFromStdout(cmd, ext) } // Handle fixed file case workDir, _ := fileConfig["work_dir"].(string) outputFile, _ := fileConfig["output_file"].(string) if outputFile == "" { return "", fmt.Errorf("%s", i18n.T("extension_no_output_file")) } // Set working directory if specified if workDir != "" { cmd.Dir = workDir } var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { if ctx.Err() == context.DeadlineExceeded { return "", fmt.Errorf("%s", fmt.Sprintf(i18n.T("extension_execution_timed_out"), timeout)) } return "", fmt.Errorf(i18n.T("extension_execution_failed_err"), err, stderr.String()) } // Construct full file path outputPath := outputFile if workDir != "" { outputPath = filepath.Join(workDir, outputFile) } content, err := os.ReadFile(outputPath) if err != nil { return "", fmt.Errorf(i18n.T("extension_failed_read_output_file"), err) } // Handle cleanup if enabled if ext.IsCleanupEnabled() { defer os.Remove(outputPath) } return string(content), nil } // Helper method to handle path from stdout case func (e *ExtensionExecutor) handlePathFromStdout(cmd *exec.Cmd, ext *ExtensionDefinition) (string, error) { var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return "", fmt.Errorf(i18n.T("extension_failed_get_output_path"), err, stderr.String()) } outputPath := strings.TrimSpace(stdout.String()) content, err := os.ReadFile(outputPath) if err != nil { return "", fmt.Errorf(i18n.T("extension_failed_read_output_file"), err) } if ext.IsCleanupEnabled() { defer os.Remove(outputPath) } return string(content), nil }