interactivity with huh
This commit is contained in:
128
main.go
128
main.go
@ -5,7 +5,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// maxRedirects defines the maximum number of redirects to follow.
|
||||
@ -21,7 +24,6 @@ func isValidURLAndFetchContent(url string, redirectCount int) ([]byte, bool, err
|
||||
|
||||
// Check if the URL ends with .sh
|
||||
if strings.HasSuffix(strings.ToLower(url), ".sh") {
|
||||
// If it ends with .sh, try to download it directly
|
||||
fmt.Printf("Attempting to download: %s\n", url)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
@ -36,11 +38,9 @@ func isValidURLAndFetchContent(url string, redirectCount int) ([]byte, bool, err
|
||||
}
|
||||
return content, true, nil
|
||||
}
|
||||
// If status is not OK, but it ended with .sh, we treat it as a failed download for a .sh file.
|
||||
return nil, false, fmt.Errorf("failed to download %s: status code %d", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
// If it doesn't end with .sh, check for redirect
|
||||
fmt.Printf("URL %s does not end with .sh, checking for redirect...\n", url)
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
@ -48,7 +48,7 @@ func isValidURLAndFetchContent(url string, redirectCount int) ([]byte, bool, err
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Get(url) // Using Get to be able to read body if needed, though Head is often used for just status/headers
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to request %s: %w", url, err)
|
||||
}
|
||||
@ -63,7 +63,6 @@ func isValidURLAndFetchContent(url string, redirectCount int) ([]byte, bool, err
|
||||
fmt.Printf("Redirected to: %s\n", location.String())
|
||||
return isValidURLAndFetchContent(location.String(), redirectCount+1)
|
||||
default:
|
||||
// Not a .sh and not a redirect status code
|
||||
return nil, false, fmt.Errorf("URL %s does not end in .sh and did not redirect (status: %d)", url, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@ -73,6 +72,48 @@ func hasShebang(content []byte) bool {
|
||||
return len(content) >= 2 && content[0] == '#' && content[1] == '!'
|
||||
}
|
||||
|
||||
// executeScript saves the script content to a temporary file and executes it.
|
||||
func executeScript(content []byte) error {
|
||||
tmpFile, err := os.CreateTemp("", "script-*.sh")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temporary file: %w", err)
|
||||
}
|
||||
scriptPath := tmpFile.Name()
|
||||
defer os.Remove(scriptPath) // Clean up the temp file
|
||||
|
||||
if _, err := tmpFile.Write(content); err != nil {
|
||||
tmpFile.Close() // Close before attempting remove on error
|
||||
return fmt.Errorf("failed to write to temporary file: %w", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temporary file: %w", err)
|
||||
}
|
||||
|
||||
// Make the script executable
|
||||
if err := os.Chmod(scriptPath, 0700); err != nil {
|
||||
return fmt.Errorf("failed to make script executable: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n--- EXECUTING SCRIPT (%s) ---\n", scriptPath)
|
||||
// The command to execute. On Unix-like systems, the kernel will use the shebang.
|
||||
// For Windows, or if a specific interpreter is needed universally,
|
||||
// one might need to parse the shebang and prepend the interpreter.
|
||||
cmd := exec.Command(scriptPath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin // Allow script to read from stdin
|
||||
|
||||
err = cmd.Run()
|
||||
fmt.Println("\n--- EXECUTION FINISHED ---")
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return fmt.Errorf("script execution failed with exit code %d", exitErr.ExitCode())
|
||||
}
|
||||
return fmt.Errorf("script execution failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: go run script_downloader.go <URL>")
|
||||
@ -92,13 +133,78 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if hasShebang(content) {
|
||||
fmt.Println("Valid script found. Content:")
|
||||
fmt.Println("--- SCRIPT START ---")
|
||||
if !hasShebang(content) {
|
||||
fmt.Fprintf(os.Stderr, "Error: The file from URL does not start with a shebang '#!'.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Valid script found.")
|
||||
|
||||
var choice string
|
||||
scriptActionForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("What would you like to do with the script?").
|
||||
Options(
|
||||
huh.NewOption("Read the script", "read"),
|
||||
huh.NewOption("Execute the script (Potentially DANGEROUS!)", "execute"),
|
||||
huh.NewOption("Exit", "exit"),
|
||||
).
|
||||
Value(&choice),
|
||||
),
|
||||
)
|
||||
|
||||
err = scriptActionForm.Run()
|
||||
if err != nil {
|
||||
// This can happen if the user cancels the form (e.g., Ctrl+C)
|
||||
if err == huh.ErrUserAborted {
|
||||
fmt.Println("Operation cancelled by user. Exiting.")
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Error running selection form: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch choice {
|
||||
case "read":
|
||||
fmt.Println("\n--- SCRIPT CONTENT START ---")
|
||||
fmt.Print(string(content))
|
||||
fmt.Println("--- SCRIPT END ---")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error: The file from URL %s does not start with a shebang '#!'.\n", initialURL)
|
||||
fmt.Println("--- SCRIPT CONTENT END ---")
|
||||
case "execute":
|
||||
fmt.Println("Attempting to execute the script...")
|
||||
// Add an additional confirmation for execution due to security risks
|
||||
var confirmExecute bool
|
||||
confirmForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title("DANGER: You are about to execute a script from the internet.").
|
||||
Description("Only proceed if you FULLY TRUST the source of this script.\nAre you sure you want to execute it?").
|
||||
Affirmative("Yes, execute it!").
|
||||
Negative("No, cancel.").
|
||||
Value(&confirmExecute),
|
||||
),
|
||||
)
|
||||
err := confirmForm.Run()
|
||||
if err != nil || !confirmExecute {
|
||||
if err == huh.ErrUserAborted || !confirmExecute {
|
||||
fmt.Println("Execution cancelled by user.")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error during confirmation: %v\n", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
|
||||
if err := executeScript(content); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error during script execution: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "exit":
|
||||
fmt.Println("Exiting.")
|
||||
os.Exit(0)
|
||||
default:
|
||||
// This path should ideally not be reached if huh.Select is used correctly
|
||||
fmt.Fprintf(os.Stderr, "Invalid choice. Exiting.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user