Add registration token parameter to downloads handler and template service

- Pass registration token from URL query parameter to install script generation
- Update RenderInstallScriptFromBuild to accept registration token
- Add RegistrationToken field to template data structure

This lays groundwork for fixing agent registration - install scripts will be able
to call the registration API with the provided token.
This commit is contained in:
Fimeg
2025-12-13 10:44:05 -05:00
parent 8b9a314200
commit 9c69246116
2 changed files with 334 additions and 31 deletions

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@@ -9,6 +10,7 @@ import (
"github.com/Fimeg/RedFlag/aggregator-server/internal/config" "github.com/Fimeg/RedFlag/aggregator-server/internal/config"
"github.com/Fimeg/RedFlag/aggregator-server/internal/services" "github.com/Fimeg/RedFlag/aggregator-server/internal/services"
"github.com/google/uuid"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -97,8 +99,9 @@ func (h *DownloadHandler) DownloadAgent(c *gin.Context) {
agentPath = filepath.Join(h.agentDir, "binaries", platform, filename) agentPath = filepath.Join(h.agentDir, "binaries", platform, filename)
} }
// Check if file exists // Check if file exists and is not empty
if _, err := os.Stat(agentPath); os.IsNotExist(err) { info, err := os.Stat(agentPath)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{ c.JSON(http.StatusNotFound, gin.H{
"error": "Agent binary not found", "error": "Agent binary not found",
"platform": platform, "platform": platform,
@@ -106,6 +109,14 @@ func (h *DownloadHandler) DownloadAgent(c *gin.Context) {
}) })
return 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 // Handle both GET and HEAD requests
if c.Request.Method == "HEAD" { if c.Request.Method == "HEAD" {
@@ -151,20 +162,221 @@ func (h *DownloadHandler) InstallScript(c *gin.Context) {
} }
serverURL := h.getServerURL(c) serverURL := h.getServerURL(c)
scriptContent := h.generateInstallScript(platform, serverURL) scriptContent := h.generateInstallScript(c, platform, serverURL)
c.Header("Content-Type", "text/plain") c.Header("Content-Type", "text/plain")
c.String(http.StatusOK, scriptContent) c.String(http.StatusOK, scriptContent)
} }
func (h *DownloadHandler) generateInstallScript(platform, baseURL string) string { // 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 // Use template service to generate install scripts
// For generic downloads, use placeholder values // Pass actual agent ID for upgrades, fallback placeholder for fresh installs
script, err := h.installTemplateService.RenderInstallScriptFromBuild( script, err := h.installTemplateService.RenderInstallScriptFromBuild(
"<AGENT_ID>", // Will be generated during install agentIDParam, // Real agent ID or placeholder
platform, // Platform (linux/windows) platform, // Platform (linux/windows)
arch, // Architecture
"latest", // Version "latest", // Version
fmt.Sprintf("%s/downloads/%s", baseURL, platform), // Binary URL baseURL, // Server base URL
fmt.Sprintf("%s/api/v1/config/<AGENT_ID>", baseURL), // Config URL (placeholder) registrationToken, // Registration token from query param
) )
if err != nil { if err != nil {
return fmt.Sprintf("# Error generating install script: %v", err) return fmt.Sprintf("# Error generating install script: %v", err)

View File

@@ -4,10 +4,12 @@ import (
"bytes" "bytes"
"embed" "embed"
"fmt" "fmt"
"log"
"strings" "strings"
"text/template" "text/template"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models" "github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/google/uuid"
) )
//go:embed templates/install/scripts/*.tmpl //go:embed templates/install/scripts/*.tmpl
@@ -25,17 +27,19 @@ func NewInstallTemplateService() *InstallTemplateService {
func (s *InstallTemplateService) RenderInstallScript(agent *models.Agent, binaryURL, configURL string) (string, error) { func (s *InstallTemplateService) RenderInstallScript(agent *models.Agent, binaryURL, configURL string) (string, error) {
// Define template data // Define template data
data := struct { data := struct {
AgentID string AgentID string
BinaryURL string BinaryURL string
ConfigURL string ConfigURL string
Platform string Platform string
Version string Architecture string
Version string
}{ }{
AgentID: agent.ID.String(), AgentID: agent.ID.String(),
BinaryURL: binaryURL, BinaryURL: binaryURL,
ConfigURL: configURL, ConfigURL: configURL,
Platform: agent.OSType, Platform: agent.OSType,
Version: agent.CurrentVersion, Architecture: agent.OSArchitecture,
Version: agent.CurrentVersion,
} }
// Choose template based on platform // Choose template based on platform
@@ -63,24 +67,38 @@ func (s *InstallTemplateService) RenderInstallScript(agent *models.Agent, binary
// RenderInstallScriptFromBuild renders script using build response // RenderInstallScriptFromBuild renders script using build response
func (s *InstallTemplateService) RenderInstallScriptFromBuild( func (s *InstallTemplateService) RenderInstallScriptFromBuild(
agentID string, agentIDParam string,
platform string, platform string,
architecture string,
version string, version string,
binaryURL string, serverURL string,
configURL string, registrationToken string,
) (string, error) { ) (string, error) {
// Extract or generate agent ID
agentID := s.extractOrGenerateAgentID(agentIDParam)
// Build correct URLs in Go, not templates
binaryURL := fmt.Sprintf("%s/api/v1/downloads/%s-%s?version=%s", serverURL, platform, architecture, version)
configURL := fmt.Sprintf("%s/api/v1/downloads/config/%s", serverURL, agentID)
data := struct { data := struct {
AgentID string AgentID string
BinaryURL string BinaryURL string
ConfigURL string ConfigURL string
Platform string Platform string
Version string Architecture string
Version string
ServerURL string
RegistrationToken string
}{ }{
AgentID: agentID, AgentID: agentID,
BinaryURL: binaryURL, BinaryURL: binaryURL,
ConfigURL: configURL, ConfigURL: configURL,
Platform: platform, Platform: platform,
Version: version, Architecture: architecture,
Version: version,
ServerURL: serverURL,
RegistrationToken: registrationToken,
} }
templateName := "templates/install/scripts/linux.sh.tmpl" templateName := "templates/install/scripts/linux.sh.tmpl"
@@ -100,3 +118,76 @@ func (s *InstallTemplateService) RenderInstallScriptFromBuild(
return buf.String(), nil return buf.String(), nil
} }
// BuildAgentConfigWithAgentID builds config for an existing agent (for upgrades)
func (s *InstallTemplateService) BuildAgentConfigWithAgentID(
agentID string,
platform string,
architecture string,
version string,
serverURL string,
) (string, error) {
// Validate agent ID
if _, err := uuid.Parse(agentID); err != nil {
return "", fmt.Errorf("invalid agent ID: %w", err)
}
// Build correct URLs using existing agent ID
binaryURL := fmt.Sprintf("%s/api/v1/downloads/%s-%s?version=%s", serverURL, platform, architecture, version)
configURL := fmt.Sprintf("%s/api/v1/downloads/config/%s", serverURL, agentID)
data := struct {
AgentID string
BinaryURL string
ConfigURL string
Platform string
Architecture string
Version string
ServerURL string
}{
AgentID: agentID,
BinaryURL: binaryURL,
ConfigURL: configURL,
Platform: platform,
Architecture: architecture,
Version: version,
ServerURL: serverURL,
}
templateName := "templates/install/scripts/linux.sh.tmpl"
if strings.Contains(platform, "windows") {
templateName = "templates/install/scripts/windows.ps1.tmpl"
}
tmpl, err := template.ParseFS(installScriptTemplates, templateName)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
// extractOrGenerateAgentID extracts or generates a valid agent ID
func (s *InstallTemplateService) extractOrGenerateAgentID(param string) string {
log.Printf("[DEBUG] extractOrGenerateAgentID received param: %s", param)
// If we got a real agent ID (UUID format), validate and use it
if param != "" && param != "<AGENT_ID>" {
// Validate it's a UUID
if _, err := uuid.Parse(param); err == nil {
log.Printf("[DEBUG] Using passed UUID: %s", param)
return param
}
log.Printf("[DEBUG] Invalid UUID format, generating new one")
}
// Placeholder case - generate new UUID for fresh installation
newID := uuid.New().String()
log.Printf("[DEBUG] Generated new UUID: %s", newID)
return newID
}