From 9caba86389f74208b8dd61754f3d18d27a30fd7f Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Mon, 24 Nov 2025 15:16:14 +0000 Subject: [PATCH] Fix #735: Allow HEAD requests for download endpoints and fix routing - Allow HEAD requests in addition to GET for all download handlers (install scripts, binaries, checksums) to prevent 405 errors - Add /uninstall-host-agent.sh to special routes in ServeHTTP - Add test coverage for HEAD request handling - Resolves 'method not allowed' errors during agent installation --- internal/api/router.go | 27 ++++++++++++++------------- internal/api/router_download_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/internal/api/router.go b/internal/api/router.go index 6653b97..2ddc1d0 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1508,6 +1508,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { req.URL.Path == "/install-container-agent.sh" || req.URL.Path == "/install-host-agent.sh" || req.URL.Path == "/install-host-agent.ps1" || + req.URL.Path == "/uninstall-host-agent.sh" || req.URL.Path == "/uninstall-host-agent.ps1" { // Use the mux for API and special routes r.mux.ServeHTTP(w, req) @@ -3287,7 +3288,7 @@ func (r *Router) backgroundUpdateChecker() { // handleDownloadInstallScript serves the Docker agent installation script func (r *Router) handleDownloadInstallScript(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -3315,7 +3316,7 @@ func (r *Router) handleDownloadInstallScript(w http.ResponseWriter, req *http.Re // handleDownloadContainerAgentInstallScript serves the container agent install script func (r *Router) handleDownloadContainerAgentInstallScript(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -3331,7 +3332,7 @@ func (r *Router) handleDownloadContainerAgentInstallScript(w http.ResponseWriter // handleDownloadAgent serves the Docker agent binary func (r *Router) handleDownloadAgent(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -3401,7 +3402,7 @@ func (r *Router) handleDownloadAgent(w http.ResponseWriter, req *http.Request) { // handleDownloadHostAgentInstallScript serves the Host agent installation script func (r *Router) handleDownloadHostAgentInstallScript(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -3429,7 +3430,7 @@ func (r *Router) handleDownloadHostAgentInstallScript(w http.ResponseWriter, req // handleDownloadHostAgentInstallScriptPS serves the PowerShell installation script for Windows func (r *Router) handleDownloadHostAgentInstallScriptPS(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -3446,7 +3447,7 @@ func (r *Router) handleDownloadHostAgentInstallScriptPS(w http.ResponseWriter, r // handleDownloadHostAgentUninstallScript serves the bash uninstallation script for Linux/macOS func (r *Router) handleDownloadHostAgentUninstallScript(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -3462,7 +3463,7 @@ func (r *Router) handleDownloadHostAgentUninstallScript(w http.ResponseWriter, r // handleDownloadHostAgentUninstallScriptPS serves the PowerShell uninstallation script for Windows func (r *Router) handleDownloadHostAgentUninstallScriptPS(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -3479,7 +3480,7 @@ func (r *Router) handleDownloadHostAgentUninstallScriptPS(w http.ResponseWriter, // handleDownloadHostAgent serves the Host agent binary func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -3783,7 +3784,7 @@ func (r *Router) handleDiagnosticsDockerPrepareToken(w http.ResponseWriter, req } func (r *Router) handleDownloadPulseSensorProxy(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed", nil) return } @@ -3909,7 +3910,7 @@ func (r *Router) handleDownloadPulseSensorProxy(w http.ResponseWriter, req *http } func (r *Router) handleDownloadInstallerScript(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed", nil) return } @@ -3936,7 +3937,7 @@ func (r *Router) handleDownloadInstallerScript(w http.ResponseWriter, req *http. } func (r *Router) handleDownloadDockerInstallerScript(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed", nil) return } @@ -3963,7 +3964,7 @@ func (r *Router) handleDownloadDockerInstallerScript(w http.ResponseWriter, req } func (r *Router) handleDownloadMigrationScript(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed", nil) return } @@ -3988,7 +3989,7 @@ func (r *Router) handleDownloadMigrationScript(w http.ResponseWriter, req *http. } func (r *Router) handleDownloadTemperatureProxyMigrationScript(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { + if req.Method != http.MethodGet && req.Method != http.MethodHead { writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed", nil) return } diff --git a/internal/api/router_download_test.go b/internal/api/router_download_test.go index d9b5f44..cc3ed82 100644 --- a/internal/api/router_download_test.go +++ b/internal/api/router_download_test.go @@ -91,3 +91,29 @@ func TestHandleDownloadHostAgentServesChecksumForWindowsExe(t *testing.T) { t.Fatalf("unexpected checksum body: got %q want %q", got, expected) } } + +func TestHandleDownloadHostAgentAllowsHEAD(t *testing.T) { + binDir := setupTempPulseBin(t) + filePath := filepath.Join(binDir, "pulse-host-agent-linux-amd64") + if err := os.WriteFile(filePath, []byte("binary"), 0o755); err != nil { + t.Fatalf("failed to write test binary: %v", err) + } + + req := httptest.NewRequest(http.MethodHead, "/download/pulse-host-agent?platform=linux&arch=amd64", nil) + rr := httptest.NewRecorder() + + router := &Router{} + router.handleDownloadHostAgent(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200 OK for HEAD, got %d", rr.Code) + } + + // HEAD response should have Content-Length but no body + // Note: httptest.ResponseRecorder might capture body even for HEAD if handler writes it, + // but standard http.Server suppresses it. + // However, our handler uses http.ServeContent which respects HEAD. + if rr.Body.Len() > 0 { + t.Fatalf("expected empty body for HEAD, got %d bytes", rr.Body.Len()) + } +}