diff --git a/internal/api/router.go b/internal/api/router.go index 2433741..fb29540 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -36,11 +36,11 @@ import ( // Router handles HTTP routing type Router struct { - mux *http.ServeMux - config *config.Config - monitor *monitoring.Monitor - alertHandlers *AlertHandlers - configHandlers *ConfigHandlers + mux *http.ServeMux + config *config.Config + monitor *monitoring.Monitor + alertHandlers *AlertHandlers + configHandlers *ConfigHandlers notificationHandlers *NotificationHandlers notificationQueueHandlers *NotificationQueueHandlers dockerAgentHandlers *DockerAgentHandlers @@ -48,14 +48,14 @@ type Router struct { temperatureProxyHandlers *TemperatureProxyHandlers systemSettingsHandler *SystemSettingsHandler wsHub *websocket.Hub - reloadFunc func() error - updateManager *updates.Manager - exportLimiter *RateLimiter - persistence *config.ConfigPersistence - oidcMu sync.Mutex - oidcService *OIDCService - wrapped http.Handler - projectRoot string + reloadFunc func() error + updateManager *updates.Manager + exportLimiter *RateLimiter + persistence *config.ConfigPersistence + oidcMu sync.Mutex + oidcService *OIDCService + wrapped http.Handler + projectRoot string // Cached system settings to avoid loading from disk on every request settingsMu sync.RWMutex cachedAllowEmbedding bool @@ -3381,18 +3381,32 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques ) } + checkedPaths := make([]string, 0, len(searchPaths)*2) + shouldCheckWindowsExe := func(path string) bool { + base := strings.ToLower(filepath.Base(path)) + return strings.Contains(base, "windows") && !strings.HasSuffix(base, ".exe") + } + for _, candidate := range searchPaths { if candidate == "" { 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) + pathsToCheck := []string{candidate} + if shouldCheckWindowsExe(candidate) { + pathsToCheck = append(pathsToCheck, candidate+".exe") + } + + for _, path := range pathsToCheck { + checkedPaths = append(checkedPaths, path) + if info, err := os.Stat(path); err == nil && !info.IsDir() { + // Check if this is a checksum request + if strings.HasSuffix(req.URL.Path, ".sha256") { + r.serveChecksum(w, req, path) + return + } + http.ServeFile(w, req, path) return } - http.ServeFile(w, req, candidate) - return } } @@ -3406,7 +3420,7 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques errorMsg.WriteString(fmt.Sprintf(" GOOS=%s GOARCH=%s go build -o pulse-host-agent-%s-%s ./cmd/pulse-host-agent\n", platformParam, archParam, platformParam, archParam)) errorMsg.WriteString(fmt.Sprintf(" sudo mv pulse-host-agent-%s-%s /opt/pulse/bin/\n\n", platformParam, archParam)) errorMsg.WriteString("Searched locations:\n") - for _, path := range searchPaths { + for _, path := range checkedPaths { errorMsg.WriteString(fmt.Sprintf(" - %s\n", path)) } diff --git a/internal/api/router_download_test.go b/internal/api/router_download_test.go new file mode 100644 index 0000000..f3be422 --- /dev/null +++ b/internal/api/router_download_test.go @@ -0,0 +1,68 @@ +package api + +import ( + "crypto/sha256" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestHandleDownloadHostAgentServesWindowsExe(t *testing.T) { + t.Cleanup(func() { + _ = os.Remove("/opt/pulse/bin/pulse-host-agent-windows-unit-test.exe") + }) + + filePath := filepath.Join("/opt/pulse/bin", "pulse-host-agent-windows-unit-test.exe") + if err := os.WriteFile(filePath, []byte("exe-binary"), 0o755); err != nil { + t.Fatalf("failed to write test binary: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "/download/pulse-host-agent?platform=windows&arch=unit-test", nil) + rr := httptest.NewRecorder() + + router := &Router{} + router.handleDownloadHostAgent(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200 OK, got %d", rr.Code) + } + + if got := rr.Body.String(); got != "exe-binary" { + t.Fatalf("unexpected response body: %q", got) + } +} + +func TestHandleDownloadHostAgentServesChecksumForWindowsExe(t *testing.T) { + const ( + arch = "unit-sha" + filename = "pulse-host-agent-windows-" + arch + ".exe" + ) + filePath := filepath.Join("/opt/pulse/bin", filename) + t.Cleanup(func() { + _ = os.Remove(filePath) + }) + + payload := []byte("checksum-data") + if err := os.WriteFile(filePath, payload, 0o755); err != nil { + t.Fatalf("failed to write test binary: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/download/pulse-host-agent.sha256?platform=windows&arch=%s", arch), nil) + rr := httptest.NewRecorder() + + router := &Router{} + router.handleDownloadHostAgent(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200 OK, got %d", rr.Code) + } + + expected := fmt.Sprintf("%x", sha256.Sum256(payload)) + if got := strings.TrimSpace(rr.Body.String()); got != expected { + t.Fatalf("unexpected checksum body: got %q want %q", got, expected) + } +}