435 lines
12 KiB
Go
435 lines
12 KiB
Go
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 "<AGENT_ID>"
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|