package handlers import ( "fmt" "log" "net/http" "os" "path/filepath" "strings" "github.com/Fimeg/RedFlag/aggregator-server/internal/config" "github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries" "github.com/Fimeg/RedFlag/aggregator-server/internal/services" "github.com/google/uuid" "github.com/gin-gonic/gin" ) // DownloadHandler handles agent binary downloads type DownloadHandler struct { agentDir string config *config.Config installTemplateService *services.InstallTemplateService packageQueries *queries.PackageQueries } func NewDownloadHandler(agentDir string, cfg *config.Config, packageQueries *queries.PackageQueries) *DownloadHandler { return &DownloadHandler{ agentDir: agentDir, config: cfg, installTemplateService: services.NewInstallTemplateService(), packageQueries: packageQueries, } } // getServerURL determines the server URL with proper protocol detection func (h *DownloadHandler) getServerURL(c *gin.Context) string { // Priority 1: Use configured public URL if set if h.config.Server.PublicURL != "" { return h.config.Server.PublicURL } // Priority 2: Construct API server URL from configuration scheme := "http" host := h.config.Server.Host port := h.config.Server.Port // Use HTTPS if TLS is enabled in config if h.config.Server.TLS.Enabled { scheme = "https" } // For default host (0.0.0.0), use localhost for client connections if host == "0.0.0.0" { host = "localhost" } // Only include port if it's not the default for the protocol if (scheme == "http" && port != 80) || (scheme == "https" && port != 443) { return fmt.Sprintf("%s://%s:%d", scheme, host, port) } return fmt.Sprintf("%s://%s", scheme, host) } // DownloadAgent serves agent binaries for different platforms func (h *DownloadHandler) DownloadAgent(c *gin.Context) { platform := c.Param("platform") version := c.Query("version") // Optional version parameter for signed binaries // Validate platform to prevent directory traversal validPlatforms := map[string]bool{ "linux-amd64": true, "linux-arm64": true, "windows-amd64": true, "windows-arm64": true, } if !validPlatforms[platform] { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or unsupported platform"}) return } // Build filename based on platform filename := "redflag-agent" if strings.HasPrefix(platform, "windows") { filename += ".exe" } var agentPath string // Try to serve signed package first if version is specified // TODO: Implement database lookup for signed packages // if version != "" { // signedPackage, err := h.packageQueries.GetSignedPackage(version, platform) // if err == nil && fileExists(signedPackage.BinaryPath) { // agentPath = signedPackage.BinaryPath // } // } // Fallback to unsigned generic binary if agentPath == "" { agentPath = filepath.Join(h.agentDir, "binaries", platform, filename) } // Check if file exists and is not empty info, err := os.Stat(agentPath) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "error": "Agent binary not found", "platform": platform, "version": version, }) return } if info.Size() == 0 { c.JSON(http.StatusNotFound, gin.H{ "error": "Agent binary not found (empty file)", "platform": platform, "version": version, }) return } // Handle both GET and HEAD requests if c.Request.Method == "HEAD" { c.Status(http.StatusOK) return } c.File(agentPath) } // DownloadUpdatePackage serves signed agent update packages func (h *DownloadHandler) DownloadUpdatePackage(c *gin.Context) { packageID := c.Param("package_id") // Validate package ID format (UUID) if len(packageID) != 36 { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid package ID format"}) return } parsedPackageID, err := uuid.Parse(packageID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid package ID format"}) return } // Fetch package from database pkg, err := h.packageQueries.GetSignedPackageByID(parsedPackageID) if err != nil { if err.Error() == "update package not found" { c.JSON(http.StatusNotFound, gin.H{ "error": "Package not found", "package_id": packageID, }) return } log.Printf("[ERROR] Failed to fetch package %s: %v", packageID, err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to retrieve package", "package_id": packageID, }) return } // Verify file exists on disk if _, err := os.Stat(pkg.BinaryPath); os.IsNotExist(err) { log.Printf("[ERROR] Package file not found on disk: %s", pkg.BinaryPath) c.JSON(http.StatusNotFound, gin.H{ "error": "Package file not found on disk", "package_id": packageID, }) return } // Set appropriate headers c.Header("Content-Type", "application/octet-stream") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(pkg.BinaryPath))) c.Header("X-Package-Version", pkg.Version) c.Header("X-Package-Platform", pkg.Platform) c.Header("X-Package-Architecture", pkg.Architecture) if pkg.Signature != "" { c.Header("X-Package-Signature", pkg.Signature) } if pkg.Checksum != "" { c.Header("X-Package-Checksum", pkg.Checksum) } // Serve the file c.File(pkg.BinaryPath) } // InstallScript serves the installation script func (h *DownloadHandler) InstallScript(c *gin.Context) { platform := c.Param("platform") // Validate platform validPlatforms := map[string]bool{ "linux": true, "windows": true, } if !validPlatforms[platform] { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or unsupported platform"}) return } serverURL := h.getServerURL(c) scriptContent := h.generateInstallScript(c, platform, serverURL) c.Header("Content-Type", "text/plain") c.String(http.StatusOK, scriptContent) } // parseAgentID extracts agent ID from header → path → query with security priority func parseAgentID(c *gin.Context) string { // 1. Header → Secure (preferred) if agentID := c.GetHeader("X-Agent-ID"); agentID != "" { if _, err := uuid.Parse(agentID); err == nil { log.Printf("[DEBUG] Parsed agent ID from header: %s", agentID) return agentID } log.Printf("[DEBUG] Invalid UUID in header: %s", agentID) } // 2. Path parameter → Legacy compatible if agentID := c.Param("agent_id"); agentID != "" { if _, err := uuid.Parse(agentID); err == nil { log.Printf("[DEBUG] Parsed agent ID from path: %s", agentID) return agentID } log.Printf("[DEBUG] Invalid UUID in path: %s", agentID) } // 3. Query parameter → Fallback if agentID := c.Query("agent_id"); agentID != "" { if _, err := uuid.Parse(agentID); err == nil { log.Printf("[DEBUG] Parsed agent ID from query: %s", agentID) return agentID } log.Printf("[DEBUG] Invalid UUID in query: %s", agentID) } // Return placeholder for fresh installs log.Printf("[DEBUG] No valid agent ID found, using placeholder") return "" } // HandleConfigDownload serves agent configuration templates with updated schema // The install script injects the agent's actual credentials locally after download func (h *DownloadHandler) HandleConfigDownload(c *gin.Context) { agentIDParam := c.Param("agent_id") // Validate UUID format parsedAgentID, err := uuid.Parse(agentIDParam) if err != nil { log.Printf("Invalid agent ID format for config download: %s, error: %v", agentIDParam, err) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid agent ID format", }) return } // Log for security monitoring log.Printf("Config template download requested - agent_id: %s, remote_addr: %s", parsedAgentID.String(), c.ClientIP()) // Get server URL for config serverURL := h.getServerURL(c) // Build config template with schema only (no sensitive credentials) // Credentials are preserved locally by the install script configTemplate := map[string]interface{}{ "version": 5, // Current schema version (v5 as of 0.1.23+) "agent_version": "0.2.0", "server_url": serverURL, // Placeholder credentials - will be replaced by install script "agent_id": "00000000-0000-0000-0000-000000000000", "token": "", "refresh_token": "", "registration_token": "", "machine_id": "", // Standard configuration with all subsystems "check_in_interval": 300, "rapid_polling_enabled": false, "rapid_polling_until": "0001-01-01T00:00:00Z", "network": map[string]interface{}{ "timeout": 30000000000, "retry_count": 3, "retry_delay": 5000000000, "max_idle_conn": 10, }, "proxy": map[string]interface{}{ "enabled": false, }, "tls": map[string]interface{}{ "enabled": false, "insecure_skip_verify": false, }, "logging": map[string]interface{}{ "level": "info", "max_size": 100, "max_backups": 3, "max_age": 28, }, "subsystems": map[string]interface{}{ "system": map[string]interface{}{ "enabled": true, "timeout": 10000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, "filesystem": map[string]interface{}{ "enabled": true, "timeout": 10000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, "network": map[string]interface{}{ "enabled": true, "timeout": 30000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, "processes": map[string]interface{}{ "enabled": true, "timeout": 30000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, "updates": map[string]interface{}{ "enabled": true, "timeout": 30000000000, "circuit_breaker": map[string]interface{}{ "enabled": false, "failure_threshold": 0, "failure_window": 0, "open_duration": 0, "half_open_attempts": 0, }, }, "storage": map[string]interface{}{ "enabled": true, "timeout": 10000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, }, "security": map[string]interface{}{ "ed25519_verification": true, "nonce_validation": true, "machine_id_binding": true, }, } // Return config template as JSON c.Header("Content-Type", "application/json") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"redflag-config.json\"")) c.JSON(http.StatusOK, configTemplate) } func (h *DownloadHandler) generateInstallScript(c *gin.Context, platform, baseURL string) string { // Parse agent ID with defense-in-depth priority agentIDParam := parseAgentID(c) // Extract registration token from query parameters registrationToken := c.Query("token") if registrationToken == "" { return "# Error: registration token is required\n# Please include token in URL: ?token=YOUR_TOKEN\n" } // Determine architecture based on platform string var arch string switch platform { case "linux": arch = "amd64" // Default for generic linux downloads case "windows": arch = "amd64" // Default for generic windows downloads default: arch = "amd64" // Fallback } // Use template service to generate install scripts // Pass actual agent ID for upgrades, fallback placeholder for fresh installs script, err := h.installTemplateService.RenderInstallScriptFromBuild( agentIDParam, // Real agent ID or placeholder platform, // Platform (linux/windows) arch, // Architecture "latest", // Version baseURL, // Server base URL registrationToken, // Registration token from query param ) if err != nil { return fmt.Sprintf("# Error generating install script: %v", err) } return script }