diff --git a/frontend-modern/public/install-host-agent.sh b/frontend-modern/public/install-host-agent.sh index a18ddbf..c24ea4f 100755 --- a/frontend-modern/public/install-host-agent.sh +++ b/frontend-modern/public/install-host-agent.sh @@ -66,6 +66,7 @@ PULSE_TOKEN="" INTERVAL="30s" UNINSTALL="false" PLATFORM="" +FORCE=false while [[ $# -gt 0 ]]; do case "$1" in @@ -89,6 +90,10 @@ while [[ $# -gt 0 ]]; do UNINSTALL="true" shift ;; + --force|-f) + FORCE=true + shift + ;; *) echo "Unknown option: $1" exit 1 @@ -121,21 +126,23 @@ fi print_header -# Interactive prompts if parameters not provided +# Interactive prompts if parameters not provided (unless --force is used) if [[ -z "$PULSE_URL" ]]; then - log_info "Interactive Installation Mode" - echo "" - read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL - PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes + if [[ "$FORCE" == false ]]; then + log_info "Interactive Installation Mode" + echo "" + read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL + PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes + fi fi if [[ -z "$PULSE_URL" ]]; then log_error "Pulse URL is required" - echo "Usage: $0 --url --token [--interval 30s] [--platform linux|darwin|windows]" + echo "Usage: $0 --url --token [--interval 30s] [--platform linux|darwin|windows] [--force]" exit 1 fi -if [[ -z "$PULSE_TOKEN" ]]; then +if [[ -z "$PULSE_TOKEN" ]] && [[ "$FORCE" == false ]]; then log_warn "No API token provided - agent will attempt to connect without authentication" read -p "Enter API token (or press Enter to skip): " PULSE_TOKEN @@ -224,12 +231,17 @@ if [[ -f "$AGENT_PATH" ]]; then fi fi - read -p "Reinstall/update agent? (Y/n): " REINSTALL - if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then - log_info "Installation cancelled" - exit 0 + if [[ "$FORCE" == false ]]; then + read -p "Reinstall/update agent? (Y/n): " REINSTALL + if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then + log_info "Installation cancelled" + exit 0 + fi + echo "" + else + log_info "Force mode: automatically reinstalling/updating agent" + echo "" fi - echo "" fi # Download agent binary from Pulse server @@ -322,10 +334,17 @@ if [[ -n "$EXPECTED_CHECKSUM" ]]; then echo " Got: $ACTUAL_CHECKSUM" echo "" log_warn "The downloaded binary may be corrupted or tampered with." - read -p "Continue anyway? (y/N): " CONTINUE_ANYWAY - if [[ "$CONTINUE_ANYWAY" != "y" ]] && [[ "$CONTINUE_ANYWAY" != "Y" ]]; then + + if [[ "$FORCE" == false ]]; then + read -p "Continue anyway? (y/N): " CONTINUE_ANYWAY + if [[ "$CONTINUE_ANYWAY" != "y" ]] && [[ "$CONTINUE_ANYWAY" != "Y" ]]; then + rm -f "$TEMP_BINARY" + log_error "Installation cancelled" + exit 1 + fi + else + log_error "Force mode: aborting due to checksum mismatch (security risk)" rm -f "$TEMP_BINARY" - log_error "Installation cancelled" exit 1 fi fi diff --git a/internal/api/router.go b/internal/api/router.go index 24da91a..d8f9bf5 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -3,9 +3,12 @@ package api import ( "bufio" "context" + "crypto/sha256" "encoding/base64" + "encoding/hex" "encoding/json" "fmt" + "io" "net" "net/http" "os" @@ -881,6 +884,7 @@ func (r *Router) setupRoutes() { // Host agent download endpoints r.mux.HandleFunc("/install-host-agent.sh", r.handleDownloadHostAgentInstallScript) r.mux.HandleFunc("/install-host-agent.ps1", r.handleDownloadHostAgentInstallScriptPS) + r.mux.HandleFunc("/uninstall-host-agent.sh", r.handleDownloadHostAgentUninstallScript) r.mux.HandleFunc("/uninstall-host-agent.ps1", r.handleDownloadHostAgentUninstallScriptPS) r.mux.HandleFunc("/download/pulse-host-agent", r.handleDownloadHostAgent) @@ -1223,6 +1227,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { "/download/pulse-docker-agent", // Agent binary download should not require auth "/install-host-agent.sh", // Host agent bootstrap script must be public "/install-host-agent.ps1", // Host agent PowerShell script must be public + "/uninstall-host-agent.sh", // Host agent uninstall script must be public "/uninstall-host-agent.ps1", // Host agent uninstall script must be public "/download/pulse-host-agent", // Host agent binary download should not require auth "/api/agent/version", // Agent update checks need to work before auth @@ -3259,6 +3264,22 @@ func (r *Router) handleDownloadHostAgentInstallScriptPS(w http.ResponseWriter, r http.ServeFile(w, req, scriptPath) } +// handleDownloadHostAgentUninstallScript serves the bash uninstallation script for Linux/macOS +func (r *Router) handleDownloadHostAgentUninstallScript(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Prevent caching - always serve the latest version + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + + scriptPath := "/opt/pulse/scripts/uninstall-host-agent.sh" + http.ServeFile(w, req, scriptPath) +} + // handleDownloadHostAgentUninstallScriptPS serves the PowerShell uninstallation script for Windows func (r *Router) handleDownloadHostAgentUninstallScriptPS(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { @@ -3322,6 +3343,11 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques continue } if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + // Check if this is a checksum request + if strings.HasSuffix(req.URL.Path, ".sha256") { + r.serveChecksum(w, req, candidate) + return + } http.ServeFile(w, req, candidate) return } @@ -3330,6 +3356,26 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques http.Error(w, "Host agent binary not found. Please build from source: go build ./cmd/pulse-host-agent", http.StatusNotFound) } +// serveChecksum computes and serves the SHA256 checksum of a file +func (r *Router) serveChecksum(w http.ResponseWriter, req *http.Request, filepath string) { + file, err := os.Open(filepath) + if err != nil { + http.Error(w, "Failed to open file", http.StatusInternalServerError) + return + } + defer file.Close() + + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + http.Error(w, "Failed to compute checksum", http.StatusInternalServerError) + return + } + + checksum := hex.EncodeToString(hasher.Sum(nil)) + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintf(w, "%s\n", checksum) +} + func (r *Router) handleDiagnosticsRegisterProxyNodes(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed", nil) diff --git a/scripts/install-host-agent.sh b/scripts/install-host-agent.sh index a18ddbf..c24ea4f 100755 --- a/scripts/install-host-agent.sh +++ b/scripts/install-host-agent.sh @@ -66,6 +66,7 @@ PULSE_TOKEN="" INTERVAL="30s" UNINSTALL="false" PLATFORM="" +FORCE=false while [[ $# -gt 0 ]]; do case "$1" in @@ -89,6 +90,10 @@ while [[ $# -gt 0 ]]; do UNINSTALL="true" shift ;; + --force|-f) + FORCE=true + shift + ;; *) echo "Unknown option: $1" exit 1 @@ -121,21 +126,23 @@ fi print_header -# Interactive prompts if parameters not provided +# Interactive prompts if parameters not provided (unless --force is used) if [[ -z "$PULSE_URL" ]]; then - log_info "Interactive Installation Mode" - echo "" - read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL - PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes + if [[ "$FORCE" == false ]]; then + log_info "Interactive Installation Mode" + echo "" + read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL + PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes + fi fi if [[ -z "$PULSE_URL" ]]; then log_error "Pulse URL is required" - echo "Usage: $0 --url --token [--interval 30s] [--platform linux|darwin|windows]" + echo "Usage: $0 --url --token [--interval 30s] [--platform linux|darwin|windows] [--force]" exit 1 fi -if [[ -z "$PULSE_TOKEN" ]]; then +if [[ -z "$PULSE_TOKEN" ]] && [[ "$FORCE" == false ]]; then log_warn "No API token provided - agent will attempt to connect without authentication" read -p "Enter API token (or press Enter to skip): " PULSE_TOKEN @@ -224,12 +231,17 @@ if [[ -f "$AGENT_PATH" ]]; then fi fi - read -p "Reinstall/update agent? (Y/n): " REINSTALL - if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then - log_info "Installation cancelled" - exit 0 + if [[ "$FORCE" == false ]]; then + read -p "Reinstall/update agent? (Y/n): " REINSTALL + if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then + log_info "Installation cancelled" + exit 0 + fi + echo "" + else + log_info "Force mode: automatically reinstalling/updating agent" + echo "" fi - echo "" fi # Download agent binary from Pulse server @@ -322,10 +334,17 @@ if [[ -n "$EXPECTED_CHECKSUM" ]]; then echo " Got: $ACTUAL_CHECKSUM" echo "" log_warn "The downloaded binary may be corrupted or tampered with." - read -p "Continue anyway? (y/N): " CONTINUE_ANYWAY - if [[ "$CONTINUE_ANYWAY" != "y" ]] && [[ "$CONTINUE_ANYWAY" != "Y" ]]; then + + if [[ "$FORCE" == false ]]; then + read -p "Continue anyway? (y/N): " CONTINUE_ANYWAY + if [[ "$CONTINUE_ANYWAY" != "y" ]] && [[ "$CONTINUE_ANYWAY" != "Y" ]]; then + rm -f "$TEMP_BINARY" + log_error "Installation cancelled" + exit 1 + fi + else + log_error "Force mode: aborting due to checksum mismatch (security risk)" rm -f "$TEMP_BINARY" - log_error "Installation cancelled" exit 1 fi fi