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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user