diff --git a/services/config-scripts/gen-postfix-conf.sh b/services/config-scripts/gen-postfix-conf.sh index c61095f..2787306 100755 --- a/services/config-scripts/gen-postfix-conf.sh +++ b/services/config-scripts/gen-postfix-conf.sh @@ -22,15 +22,15 @@ MAIL_HOSTNAME=${MAIL_HOSTNAME:-mail.$MAIL_DOMAIN} mkdir -p ${CONFIGS_PATH} -# Note: Using socketmap service for all virtual maps +# Note: Using Raven-hosted socketmap service for all virtual maps # - Virtual domains: validates which domains we accept mail for # - Virtual users: validates which user mailboxes exist # - Virtual aliases: resolves email aliases to destination addresses echo -e "SMTP configuration will use:" -echo " - Socketmap for domains: socketmap-server:9100" -echo " - Socketmap for users: socketmap-server:9100" -echo " - Socketmap for aliases: socketmap-server:9100" +echo " - Socketmap for domains: raven:9100" +echo " - Socketmap for users: raven:9100" +echo " - Socketmap for aliases: raven:9100" # --- Generate main.cf content --- cat >"${CONFIGS_PATH}/main.cf" <-group@domainname`. - -**Caching:** Positive results cached for 5 minutes. Negative results NOT cached (new users/groups immediately accessible). - -## Troubleshooting - -```bash -# Check service -docker compose ps socketmap-server -docker compose logs -f socketmap-server - -# Test connectivity -docker exec smtp nc -zv socketmap-server 9100 - -# Rebuild if needed -docker compose build --no-cache socketmap-server -``` diff --git a/services/socketmap/config/config.go b/services/socketmap/config/config.go deleted file mode 100644 index 430fc7a..0000000 --- a/services/socketmap/config/config.go +++ /dev/null @@ -1,47 +0,0 @@ -package config - -import ( - "log" - "os" - "strconv" -) - -// Config holds all configuration for the socketmap service -type Config struct { - Host string - Port string - ThunderHost string - ThunderPort string - CacheTTLSeconds int - TokenRefreshSeconds int -} - -// Load reads configuration from environment variables -func Load() *Config { - return &Config{ - Host: getEnv("SOCKETMAP_HOST", "127.0.0.1"), - Port: getEnv("SOCKETMAP_PORT", "9100"), - ThunderHost: getEnv("THUNDER_HOST", "thunder-server"), - ThunderPort: getEnv("THUNDER_PORT", "8090"), - CacheTTLSeconds: getEnvInt("CACHE_TTL_SECONDS", 300), // 5 minutes default - TokenRefreshSeconds: getEnvInt("TOKEN_REFRESH_SECONDS", 3300), // 55 minutes default - } -} - -func getEnv(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} - -func getEnvInt(key string, defaultValue int) int { - if value := os.Getenv(key); value != "" { - if intVal, err := strconv.Atoi(value); err == nil { - return intVal - } else { - log.Printf("Warning: could not parse env var %s value %q as int. Using default %d. Error: %v", key, value, defaultValue, err) - } - } - return defaultValue -} diff --git a/services/socketmap/go.mod b/services/socketmap/go.mod deleted file mode 100644 index b3f474b..0000000 --- a/services/socketmap/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module socketmap - -go 1.21 diff --git a/services/socketmap/internal/cache/cache.go b/services/socketmap/internal/cache/cache.go deleted file mode 100644 index 6afdec4..0000000 --- a/services/socketmap/internal/cache/cache.go +++ /dev/null @@ -1,54 +0,0 @@ -package cache - -import ( - "sync" - "time" -) - -// Entry represents a cached item -type Entry struct { - Exists bool - Data string // For storing additional data (e.g., alias destination) - Expires time.Time - LastUpdate time.Time -} - -// Cache provides thread-safe caching with TTL -type Cache struct { - store map[string]Entry - mutex sync.RWMutex - ttl time.Duration -} - -// New creates a new Cache instance -func New(ttlSeconds int) *Cache { - return &Cache{ - store: make(map[string]Entry), - ttl: time.Duration(ttlSeconds) * time.Second, - } -} - -// Get retrieves an entry from cache -func (c *Cache) Get(key string) (Entry, bool) { - c.mutex.RLock() - defer c.mutex.RUnlock() - entry, found := c.store[key] - return entry, found -} - -// Set stores an entry in cache -func (c *Cache) Set(key string, entry Entry) { - c.mutex.Lock() - defer c.mutex.Unlock() - c.store[key] = entry -} - -// IsExpired checks if an entry has expired -func (c *Cache) IsExpired(entry Entry) bool { - return time.Now().After(entry.Expires) -} - -// GetTTL returns the cache TTL duration -func (c *Cache) GetTTL() time.Duration { - return c.ttl -} diff --git a/services/socketmap/internal/handler/alias.go b/services/socketmap/internal/handler/alias.go deleted file mode 100644 index ec4dc77..0000000 --- a/services/socketmap/internal/handler/alias.go +++ /dev/null @@ -1,108 +0,0 @@ -package handler - -import ( - "fmt" - "log" - "strings" - "time" - - "socketmap/config" - "socketmap/internal/cache" -) - -// ResolveAlias checks if an alias exists and returns its destination -func ResolveAlias(address string, cfg *config.Config, cacheManager *cache.Cache) string { - log.Printf(" ┌─ Alias Lookup ──────────────────") - log.Printf(" │ Address: %s", address) - - // Check cache first (read lock) - cacheKey := "alias:" + address - entry, found := cacheManager.Get(cacheKey) - - now := time.Now() - - if found { - // Cache hit - check if still valid - if !cacheManager.IsExpired(entry) { - log.Printf(" │ ✓ CACHE HIT (fresh)") - log.Printf(" │ Destination: %s", entry.Data) - log.Printf(" │ Expires: %s", entry.Expires.Format("15:04:05")) - log.Printf(" └─────────────────────────────────") - return entry.Data - } - - // Cache expired - refresh - cacheAge := now.Sub(entry.LastUpdate).Seconds() - log.Printf(" │ ✓ CACHE HIT (stale)") - log.Printf(" │ Age: %.0f seconds", cacheAge) - log.Printf(" │ Refreshing from database...") - } else { - log.Printf(" │ ✗ CACHE MISS") - log.Printf(" │ Querying alias database...") - } - - // Query database for alias - destination := checkAliasInTestDB(address) - - log.Printf(" │ Database result: destination=%s", destination) - - // Update cache (write lock) - cacheManager.Set(cacheKey, cache.Entry{ - Exists: destination != "", - Data: destination, - Expires: now.Add(cacheManager.GetTTL()), - LastUpdate: now, - }) - - log.Printf(" │ Cached for %d seconds", cfg.CacheTTLSeconds) - log.Printf(" └─────────────────────────────────") - - return destination -} - -// checkAliasInTestDB queries the test alias database -func checkAliasInTestDB(address string) string { - log.Printf(" ┌─ Test Alias DB Lookup ───────") - log.Printf(" │ Checking: %s", address) - - // Parse address to get domain - parts := strings.Split(strings.ToLower(address), "@") - if len(parts) != 2 { - log.Printf(" │ ✗ Invalid email format") - log.Printf(" └──────────────────────────────") - return "" - } - - localPart := parts[0] - domain := parts[1] - - // Only handle postmaster@domain → admin@domain - if localPart == "postmaster" { - destination := fmt.Sprintf("admin@%s", domain) - log.Printf(" │ ✓ Postmaster alias: %s → %s", address, destination) - log.Printf(" └──────────────────────────────") - return destination - } - - log.Printf(" │ ✗ No alias found (only postmaster@domain supported)") - log.Printf(" └──────────────────────────────") - return "" -} - -// HandleVirtualAliasesMap handles the virtual-aliases map lookup -func HandleVirtualAliasesMap(address string, cfg *config.Config, cacheManager *cache.Cache) string { - log.Printf(" │ Checking if alias exists...") - destination := ResolveAlias(address, cfg, cacheManager) - - if destination != "" { - log.Printf(" │ ✓ ALIAS FOUND: %s -> %s", address, destination) - log.Printf(" │ Response: OK %s", destination) - log.Printf(" └─────────────────────────────────────") - return fmt.Sprintf("OK %s", destination) - } - - log.Printf(" │ ✗ ALIAS NOT FOUND: %s", address) - log.Printf(" │ Response: NOTFOUND") - log.Printf(" └─────────────────────────────────────") - return "NOTFOUND" -} diff --git a/services/socketmap/internal/handler/domain.go b/services/socketmap/internal/handler/domain.go deleted file mode 100644 index b879fcf..0000000 --- a/services/socketmap/internal/handler/domain.go +++ /dev/null @@ -1,86 +0,0 @@ -package handler - -import ( - "log" - "time" - - "socketmap/config" - "socketmap/internal/cache" - "socketmap/internal/thunder" -) - -// DomainExists checks if a domain exists in Thunder IDP -func DomainExists(domain string, cfg *config.Config, cacheManager *cache.Cache) bool { - log.Printf(" ┌─ Domain Lookup ─────────────────") - log.Printf(" │ Domain: %s", domain) - - // Check cache first (read lock) - cacheKey := "domain:" + domain - entry, found := cacheManager.Get(cacheKey) - - now := time.Now() - - if found { - // Cache hit - check if still valid - if !cacheManager.IsExpired(entry) { - log.Printf(" │ ✓ CACHE HIT (fresh)") - log.Printf(" │ Cached result: exists=%v", entry.Exists) - log.Printf(" │ Expires: %s", entry.Expires.Format("15:04:05")) - log.Printf(" └─────────────────────────────────") - return entry.Exists - } - - // Cache expired but exists - check if we should refresh - cacheAge := now.Sub(entry.LastUpdate).Seconds() - log.Printf(" │ ✓ CACHE HIT (stale)") - log.Printf(" │ Age: %.0f seconds", cacheAge) - log.Printf(" │ Refreshing from IDP...") - } else { - log.Printf(" │ ✗ CACHE MISS") - log.Printf(" │ Querying IDP...") - } - - // Query Thunder IDP for domain validation - exists, err := thunder.ValidateDomain(domain, cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds) - if err != nil { - log.Printf(" │ ⚠ IDP query failed: %v", err) - log.Printf(" │ Domain not found - Thunder unavailable") - exists = false - } - - log.Printf(" │ IDP result: exists=%v", exists) - - // Only cache positive results (exists=true) - if exists { - cacheManager.Set(cacheKey, cache.Entry{ - Exists: true, - Expires: now.Add(cacheManager.GetTTL()), - LastUpdate: now, - }) - log.Printf(" │ ✓ Cached positive result for %d seconds", cfg.CacheTTLSeconds) - } else { - log.Printf(" │ ℹ Negative result NOT cached (will query IDP next time)") - } - - log.Printf(" └─────────────────────────────────") - - return exists -} - -// HandleVirtualDomainsMap handles the virtual-domains map lookup -func HandleVirtualDomainsMap(domain string, cfg *config.Config, cacheManager *cache.Cache) string { - log.Printf(" │ Checking if domain is valid...") - exists := DomainExists(domain, cfg, cacheManager) - - if exists { - log.Printf(" │ ✓ DOMAIN FOUND: %s", domain) - log.Printf(" │ Response: OK") - log.Printf(" └─────────────────────────────────────") - return "OK 1" - } - - log.Printf(" │ ✗ DOMAIN NOT FOUND: %s", domain) - log.Printf(" │ Response: NOTFOUND") - log.Printf(" └─────────────────────────────────────") - return "NOTFOUND" -} diff --git a/services/socketmap/internal/handler/handler.go b/services/socketmap/internal/handler/handler.go deleted file mode 100644 index c7b68d7..0000000 --- a/services/socketmap/internal/handler/handler.go +++ /dev/null @@ -1,49 +0,0 @@ -package handler - -import ( - "log" - "strings" - - "socketmap/config" - "socketmap/internal/cache" -) - -// ProcessRequest processes a socketmap request and returns the response -func ProcessRequest(line string, cfg *config.Config, cacheManager *cache.Cache) string { - log.Printf(" ┌─ Processing Request ─────────────────") - log.Printf(" │ Raw input: %q", line) - - parts := strings.Fields(line) - log.Printf(" │ Split into %d parts: %v", len(parts), parts) - - // Postfix socketmap protocol sends: - if len(parts) != 2 { - log.Printf(" │ ⚠ INVALID REQUEST FORMAT") - log.Printf(" │ Expected: ") - log.Printf(" │ Got: %d parts", len(parts)) - log.Printf(" └─────────────────────────────────────") - return "PERM invalid request format" - } - - mapname := parts[0] - key := parts[1] - - log.Printf(" │ Map: %q", mapname) - log.Printf(" │ Key: %q", key) - - // Route to appropriate handler based on map name - switch mapname { - case "user-exists": - return HandleUserExistsMap(key, cfg, cacheManager) - case "virtual-domains": - return HandleVirtualDomainsMap(key, cfg, cacheManager) - case "virtual-aliases": - return HandleVirtualAliasesMap(key, cfg, cacheManager) - default: - log.Printf(" │ ⚠ UNKNOWN MAP") - log.Printf(" │ Supported maps: user-exists, virtual-domains, virtual-aliases") - log.Printf(" │ Got: %q", mapname) - log.Printf(" └─────────────────────────────────────") - return "NOTFOUND" - } -} diff --git a/services/socketmap/internal/handler/user.go b/services/socketmap/internal/handler/user.go deleted file mode 100644 index 02d17f4..0000000 --- a/services/socketmap/internal/handler/user.go +++ /dev/null @@ -1,105 +0,0 @@ -package handler - -import ( - "fmt" - "log" - "strings" - "time" - - "socketmap/config" - "socketmap/internal/cache" - "socketmap/internal/thunder" -) - -// UserExists checks if a user exists in Thunder IDP -func UserExists(email string, cfg *config.Config, cacheManager *cache.Cache) bool { - log.Printf(" ┌─ User Lookup ───────────────────") - log.Printf(" │ Email: %s", email) - - // Check cache first (read lock) - cacheKey := "user:" + email - entry, found := cacheManager.Get(cacheKey) - - now := time.Now() - - if found { - // Cache hit - check if still valid - if !cacheManager.IsExpired(entry) { - log.Printf(" │ ✓ CACHE HIT (fresh)") - log.Printf(" │ Cached result: exists=%v", entry.Exists) - log.Printf(" │ Expires: %s", entry.Expires.Format("15:04:05")) - log.Printf(" └─────────────────────────────────") - return entry.Exists - } - - // Cache expired - check if we should refresh - cacheAge := now.Sub(entry.LastUpdate).Seconds() - log.Printf(" │ ✓ CACHE HIT (stale)") - log.Printf(" │ Age: %.0f seconds", cacheAge) - log.Printf(" │ Refreshing from IDP...") - } else { - log.Printf(" │ ✗ CACHE MISS") - log.Printf(" │ Querying IDP...") - } - - // Query Thunder IDP for user validation first. - exists, err := thunder.ValidateUser(email, cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds) - if err != nil { - log.Printf(" │ ⚠ User lookup failed: %v", err) - exists = false - } - - // Treat group addresses as mailbox identities in user-exists map. - if !exists && strings.Contains(email, "@") { - groupExists, groupErr := thunder.ValidateGroupAddress(email, cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds) - if groupErr != nil { - log.Printf(" │ ⚠ Group lookup failed: %v", groupErr) - } else if groupExists { - log.Printf(" │ ✓ Group found; treating as existing user") - exists = true - } - } - - if !exists { - log.Printf(" │ User/group not found - Thunder unavailable or no match") - } - - log.Printf(" │ IDP result: exists=%v", exists) - - // Only cache positive results (exists=true) - if exists { - cacheManager.Set(cacheKey, cache.Entry{ - Exists: true, - Expires: now.Add(cacheManager.GetTTL()), - LastUpdate: now, - }) - log.Printf(" │ ✓ Cached positive result for %d seconds", cfg.CacheTTLSeconds) - } else { - log.Printf(" │ ℹ Negative result NOT cached (will query IDP next time)") - } - - log.Printf(" └─────────────────────────────────") - - return exists -} - -// HandleUserExistsMap handles the user-exists map lookup -func HandleUserExistsMap(key string, cfg *config.Config, cacheManager *cache.Cache) string { - log.Printf(" │ Checking if user exists...") - exists := UserExists(key, cfg, cacheManager) - - if exists { - // For virtual_mailbox_maps, Postfix expects a mailbox pathname - mailboxPath := key - - log.Printf(" │ ✓ USER FOUND: %s", key) - log.Printf(" │ Response: OK %s", mailboxPath) - log.Printf(" └─────────────────────────────────────") - return fmt.Sprintf("OK %s", mailboxPath) - } - - log.Printf(" │ ✗ USER NOT FOUND: %s", key) - log.Printf(" │ Response: NOTFOUND") - log.Printf(" └─────────────────────────────────────") - return "NOTFOUND" -} diff --git a/services/socketmap/internal/protocol/netstring.go b/services/socketmap/internal/protocol/netstring.go deleted file mode 100644 index bdda3b4..0000000 --- a/services/socketmap/internal/protocol/netstring.go +++ /dev/null @@ -1,65 +0,0 @@ -package protocol - -import ( - "bufio" - "fmt" - "io" - "log" - "net" - "strconv" - "strings" -) - -// ReadNetstring reads a netstring from the reader -// Netstring format: :, -// Example: "5:hello," represents the string "hello" -func ReadNetstring(reader *bufio.Reader) (string, error) { - // Read length prefix (digits before ':') - lengthStr, err := reader.ReadString(':') - if err != nil { - return "", fmt.Errorf("failed to read length: %w", err) - } - - // Remove the ':' and parse length - lengthStr = strings.TrimSuffix(lengthStr, ":") - length, err := strconv.Atoi(lengthStr) - if err != nil { - return "", fmt.Errorf("invalid length: %w", err) - } - - // Validate length to prevent memory exhaustion attacks - // Maximum allowed netstring size: 10MB (reasonable limit for email-related data) - const maxNetstringLength = 10 * 1024 * 1024 - if length < 0 { - return "", fmt.Errorf("invalid length: negative value %d", length) - } - if length > maxNetstringLength { - return "", fmt.Errorf("invalid length: %d exceeds maximum allowed size of %d bytes", length, maxNetstringLength) - } - - log.Printf(" Netstring length: %d", length) - - // Read exactly 'length' bytes of data using io.ReadFull - data := make([]byte, length) - if _, err := io.ReadFull(reader, data); err != nil { - return "", fmt.Errorf("failed to read data: %w", err) - } - - // Read and verify the trailing comma - comma, err := reader.ReadByte() - if err != nil { - return "", fmt.Errorf("failed to read comma: %w", err) - } - if comma != ',' { - return "", fmt.Errorf("expected comma, got %c", comma) - } - - return string(data), nil -} - -// WriteNetstring writes a netstring to the connection -func WriteNetstring(conn net.Conn, data string) error { - netstr := fmt.Sprintf("%d:%s,", len(data), data) - _, err := conn.Write([]byte(netstr)) - return err -} diff --git a/services/socketmap/internal/server/server.go b/services/socketmap/internal/server/server.go deleted file mode 100644 index 817f52e..0000000 --- a/services/socketmap/internal/server/server.go +++ /dev/null @@ -1,131 +0,0 @@ -package server - -import ( - "bufio" - "errors" - "fmt" - "io" - "log" - "net" - "sync" - "time" - - "socketmap/config" - "socketmap/internal/cache" - "socketmap/internal/handler" - "socketmap/internal/protocol" -) - -// Server represents the socketmap TCP server -type Server struct { - cfg *config.Config - cache *cache.Cache - listener net.Listener - activeConns sync.WaitGroup -} - -// New creates a new Server instance -func New(cfg *config.Config, cacheManager *cache.Cache) *Server { - return &Server{ - cfg: cfg, - cache: cacheManager, - } -} - -// Start starts the TCP server -func (s *Server) Start() error { - bindAddr := fmt.Sprintf("%s:%s", s.cfg.Host, s.cfg.Port) - - listener, err := net.Listen("tcp", bindAddr) - if err != nil { - return fmt.Errorf("failed to start listener: %w", err) - } - - s.listener = listener - log.Printf("✓ Socketmap service listening on %s", bindAddr) - log.Println("Ready to accept connections from Postfix") - log.Println("") - - // Accept connections - for { - conn, err := listener.Accept() - if err != nil { - log.Printf("⚠ Error accepting connection: %v", err) - continue - } - - log.Printf("╔════════════════════════════════════════════════╗") - log.Printf("║ New connection from %s", conn.RemoteAddr()) - log.Printf("╚════════════════════════════════════════════════╝") - - // Handle connection in goroutine - s.activeConns.Add(1) - go func() { - defer s.activeConns.Done() - s.handleConnection(conn) - }() - } -} - -// handleConnection handles a single client connection -func (s *Server) handleConnection(conn net.Conn) { - defer conn.Close() - defer log.Printf("Connection closed: %s", conn.RemoteAddr()) - - log.Printf(" Connection established, using netstring protocol...") - reader := bufio.NewReader(conn) - - for { - // Set read timeout to prevent hanging connections - conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - - log.Printf(" Waiting to read netstring from connection...") - - // Read request using netstring protocol - request, err := protocol.ReadNetstring(reader) - if err != nil { - if errors.Is(err, io.EOF) { - log.Printf(" Connection closed by client (EOF)") - } else { - log.Printf("⚠ Error reading netstring from %s: %v", conn.RemoteAddr(), err) - log.Printf(" Possible causes:") - log.Printf(" 1. Client sent non-netstring data") - log.Printf(" 2. Connection interrupted") - log.Printf(" 3. Protocol version mismatch") - } - return - } - - // Log raw request received - log.Printf("← Received netstring: %q (length: %d)", request, len(request)) - - if request == "" { - log.Printf("⚠ Received empty request, skipping...") - continue - } - - log.Printf(" Processing request: %q", request) - - // Process the request - response := handler.ProcessRequest(request, s.cfg, s.cache) - log.Printf("→ Preparing response: %q", response) - - // Send response using netstring protocol - conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) - err = protocol.WriteNetstring(conn, response) - if err != nil { - log.Printf("⚠ Error writing netstring to %s: %v", conn.RemoteAddr(), err) - return - } - log.Printf(" Successfully sent netstring response (length: %d)", len(response)) - } -} - -// Close gracefully shuts down the server -func (s *Server) Close() error { - if s.listener != nil { - s.listener.Close() - } - s.activeConns.Wait() - return nil -} diff --git a/services/socketmap/internal/thunder/auth.go b/services/socketmap/internal/thunder/auth.go deleted file mode 100644 index 544bc28..0000000 --- a/services/socketmap/internal/thunder/auth.go +++ /dev/null @@ -1,214 +0,0 @@ -package thunder - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "os" - "os/exec" - "regexp" - "strings" - "sync" - "time" -) - -var ( - thunderAuth *Auth - thunderAuthMutex sync.RWMutex -) - -// getDevelopAppIDFromThunderSetup extracts DEVELOP App ID from thunder-setup container -func getDevelopAppIDFromThunderSetup() (string, error) { - log.Printf(" │ Extracting DEVELOP_APP_ID from thunder-setup container...") - - // Execute: docker logs thunder-setup 2>&1 | grep 'DEVELOP_APP_ID:' | head -n1 - cmd := exec.Command("docker", "logs", "thunder-setup") - output, err := cmd.CombinedOutput() - if err != nil { - // Check if docker command doesn't exist - if strings.Contains(err.Error(), "executable file not found") { - return "", fmt.Errorf("docker command not available in PATH") - } - // Docker command exists but failed - might be permission issue - log.Printf(" │ ⚠ Warning: docker logs command failed: %v", err) - log.Printf(" │ This might be due to:") - log.Printf(" │ - thunder-setup container doesn't exist") - log.Printf(" │ - No permission to access Docker") - log.Printf(" │ - Running inside a container without Docker socket") - } - - // Search for "DEVELOP_APP_ID:" in logs - // Log format: [INFO] DEVELOP_APP_ID: 019cdc47-3537-74ee-951e-3f50e48786ab - lines := strings.Split(string(output), "\n") - for _, line := range lines { - // Look for line containing DEVELOP_APP_ID (case-insensitive) - if strings.Contains(line, "DEVELOP_APP_ID") || strings.Contains(line, "develop_app_id") { - // Extract UUID pattern: [a-f0-9-]{36} - re := regexp.MustCompile(`[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}`) - match := re.FindString(line) - if match != "" { - log.Printf(" │ ✓ DEVELOP_APP_ID extracted: %s", match) - return match, nil - } - } - } - - return "", fmt.Errorf("DEVELOP_APP_ID not found in thunder-setup logs") -} - -// Authenticate performs the full authentication flow with Thunder IDP -func Authenticate(host, port string, tokenRefreshSeconds int) (*Auth, error) { - log.Printf(" ┌─ Thunder Authentication ─────────") - - // Step 1: Get DEVELOP App ID - developAppID := os.Getenv("THUNDER_DEVELOP_APP_ID") - - if developAppID != "" { - log.Printf(" │ Using DEVELOP App ID from environment variable") - log.Printf(" │ DEVELOP_APP_ID: %s", developAppID) - } else { - log.Printf(" │ THUNDER_DEVELOP_APP_ID not set") - log.Printf(" │ Attempting to extract from thunder-setup container logs...") - - var err error - developAppID, err = getDevelopAppIDFromThunderSetup() - if err != nil { - log.Printf(" │ ✗ Failed to extract DEVELOP_APP_ID: %v", err) - log.Printf(" │") - log.Printf(" │ Please ensure Thunder setup container has completed successfully") - log.Printf(" │") - log.Printf(" │ To fix this issue:") - log.Printf(" │ 1. Check thunder-setup logs: docker logs thunder-setup") - log.Printf(" │ 2. Extract App ID manually and set environment:") - log.Printf(" │ export THUNDER_DEVELOP_APP_ID=$(docker logs thunder-setup 2>&1 | grep 'DEVELOP_APP_ID:' | grep -o '[a-f0-9-]\\{36\\}')") - log.Printf(" │ 3. Or if running in Docker, mount the Docker socket:") - log.Printf(" │ volumes: ['/var/run/docker.sock:/var/run/docker.sock']") - log.Printf(" └───────────────────────────────────") - return nil, fmt.Errorf("failed to get DEVELOP App ID: %w", err) - } - } - - client := GetHTTPClient() - baseURL := fmt.Sprintf("https://%s:%s", host, port) - - // Step 2: Start authentication flow - log.Printf(" │ Starting authentication flow...") - flowPayload := map[string]interface{}{ - "applicationId": developAppID, - "flowType": "AUTHENTICATION", - } - flowData, err := json.Marshal(flowPayload) - if err != nil { - log.Printf(" │ ✗ Failed to marshal flow payload: %v", err) - log.Printf(" └───────────────────────────────────") - return nil, fmt.Errorf("failed to marshal flow payload: %w", err) - } - - resp, err := client.Post(baseURL+"/flow/execute", "application/json", bytes.NewBuffer(flowData)) - if err != nil { - log.Printf(" │ ✗ Failed to start flow: %v", err) - log.Printf(" └───────────────────────────────────") - return nil, fmt.Errorf("failed to start flow: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - log.Printf(" │ ✗ Flow start failed (HTTP %d)", resp.StatusCode) - log.Printf(" └───────────────────────────────────") - return nil, fmt.Errorf("flow start failed with status %d", resp.StatusCode) - } - - var flowResp FlowStartResponse - if err := json.NewDecoder(resp.Body).Decode(&flowResp); err != nil { - log.Printf(" │ ✗ Failed to parse flow response: %v", err) - log.Printf(" └───────────────────────────────────") - return nil, fmt.Errorf("failed to parse flow response: %w", err) - } - - log.Printf(" │ ✓ Flow started (ID: %s)", flowResp.FlowID) - - // Step 3: Complete authentication flow - log.Printf(" │ Completing authentication...") - authPayload := map[string]interface{}{ - "flowId": flowResp.FlowID, - "inputs": map[string]string{ - "username": "admin", - "password": "admin", - "requested_permissions": "system", - }, - "action": "action_001", - } - authData, _ := json.Marshal(authPayload) - - resp2, err := client.Post(baseURL+"/flow/execute", "application/json", bytes.NewBuffer(authData)) - if err != nil { - log.Printf(" │ ✗ Failed to complete auth: %v", err) - log.Printf(" └───────────────────────────────────") - return nil, fmt.Errorf("failed to complete auth: %w", err) - } - defer resp2.Body.Close() - - if resp2.StatusCode != 200 { - log.Printf(" │ ✗ Auth completion failed (HTTP %d)", resp2.StatusCode) - log.Printf(" └───────────────────────────────────") - return nil, fmt.Errorf("auth completion failed with status %d", resp2.StatusCode) - } - - var authResp FlowCompleteResponse - if err := json.NewDecoder(resp2.Body).Decode(&authResp); err != nil { - log.Printf(" │ ✗ Failed to parse auth response: %v", err) - log.Printf(" └───────────────────────────────────") - return nil, fmt.Errorf("failed to parse auth response: %w", err) - } - - log.Printf(" │ ✓ Authentication successful") - log.Printf(" └───────────────────────────────────") - - auth := &Auth{ - DevelopAppID: developAppID, - FlowID: flowResp.FlowID, - BearerToken: authResp.Assertion, - ExpiresAt: time.Now().Add(time.Duration(tokenRefreshSeconds) * time.Second), - LastRefresh: time.Now(), - } - - return auth, nil -} - -// GetAuth returns a valid Thunder auth token, refreshing if needed -func GetAuth(host, port string, tokenRefreshSeconds int) (*Auth, error) { - thunderAuthMutex.RLock() - auth := thunderAuth - thunderAuthMutex.RUnlock() - - // Check if we have a valid token - if auth != nil && time.Now().Before(auth.ExpiresAt) { - return auth, nil - } - - // Need to authenticate or refresh - thunderAuthMutex.Lock() - defer thunderAuthMutex.Unlock() - - // Double-check after acquiring write lock - if thunderAuth != nil && time.Now().Before(thunderAuth.ExpiresAt) { - return thunderAuth, nil - } - - // Authenticate - newAuth, err := Authenticate(host, port, tokenRefreshSeconds) - if err != nil { - return nil, err - } - - thunderAuth = newAuth - return thunderAuth, nil -} - -// SetAuth sets the global auth state (for initialization) -func SetAuth(auth *Auth) { - thunderAuthMutex.Lock() - defer thunderAuthMutex.Unlock() - thunderAuth = auth -} diff --git a/services/socketmap/internal/thunder/client.go b/services/socketmap/internal/thunder/client.go deleted file mode 100644 index 68529a0..0000000 --- a/services/socketmap/internal/thunder/client.go +++ /dev/null @@ -1,17 +0,0 @@ -package thunder - -import ( - "crypto/tls" - "net/http" - "time" -) - -// GetHTTPClient returns an HTTP client with TLS verification disabled for self-signed certs -func GetHTTPClient() *http.Client { - return &http.Client{ - Timeout: 10 * time.Second, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } -} diff --git a/services/socketmap/internal/thunder/domain.go b/services/socketmap/internal/thunder/domain.go deleted file mode 100644 index 3da93de..0000000 --- a/services/socketmap/internal/thunder/domain.go +++ /dev/null @@ -1,213 +0,0 @@ -package thunder - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "slices" - "strings" - "sync" -) - -// sanitizeDomainPart validates and sanitizes a domain part to prevent path traversal attacks -func sanitizeDomainPart(part string) error { - // Check for empty parts - if part == "" { - return fmt.Errorf("empty domain part") - } - - // Check for path traversal characters - if strings.Contains(part, "..") || strings.Contains(part, "/") || strings.Contains(part, "\\") { - return fmt.Errorf("invalid characters in domain part: %s", part) - } - - // Validate that domain part contains only valid characters (alphanumeric, hyphens, underscores) - for _, char := range part { - if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || - (char >= '0' && char <= '9') || char == '-' || char == '_') { - return fmt.Errorf("invalid character in domain part: %c", char) - } - } - - return nil -} - -// buildOUPath constructs an OU path from a domain, validating all parts -func buildOUPath(domain string) (string, error) { - // Parse domain into parts - parts := strings.Split(domain, ".") - if len(parts) < 2 { - return "", fmt.Errorf("invalid domain format: minimum 2 parts required") - } - - // Validate all domain parts to prevent path traversal attacks - for _, part := range parts { - if err := sanitizeDomainPart(part); err != nil { - return "", fmt.Errorf("invalid domain part: %v", err) - } - } - - // Build OU path - // So we need to reverse the subdomain parts - var ouPath string - if len(parts) == 2 { - ouPath = domain - } else { - rootDomain := strings.Join(parts[len(parts)-2:], ".") - subdomains := parts[:len(parts)-2] - - // Reverse the subdomain parts - slices.Reverse(subdomains) - - ouPath = rootDomain + "/" + strings.Join(subdomains, "/") - } - - return ouPath, nil -} - -// Cache for OU ID lookups (domain -> OU ID mapping) -var ( - ouCache = make(map[string]string) - ouCacheMutex sync.RWMutex -) - -// ValidateDomain checks if a domain exists in Thunder IDP -func ValidateDomain(domain, host, port string, tokenRefreshSeconds int) (bool, error) { - log.Printf(" ┌─ Thunder Domain Validation ──") - log.Printf(" │ Domain: %s", domain) - - // Get authentication token - auth, err := GetAuth(host, port, tokenRefreshSeconds) - if err != nil { - log.Printf(" │ ⚠ Auth failed: %v", err) - log.Printf(" └──────────────────────────────") - return false, err - } - - // Build OU path - ouPath, err := buildOUPath(domain) - if err != nil { - log.Printf(" │ ✗ Invalid domain: %v", err) - log.Printf(" └──────────────────────────────") - return false, nil - } - - log.Printf(" │ OU Path: %s", ouPath) - - // Query Thunder API - client := GetHTTPClient() - url := fmt.Sprintf("https://%s:%s/organization-units/tree/%s", host, port, ouPath) - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Printf(" │ ✗ Failed to create request: %v", err) - log.Printf(" └──────────────────────────────") - return false, err - } - - req.Header.Set("Authorization", "Bearer "+auth.BearerToken) - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - log.Printf(" │ ✗ Request failed: %v", err) - log.Printf(" └──────────────────────────────") - return false, err - } - defer resp.Body.Close() - - if resp.StatusCode == 404 { - log.Printf(" │ ✗ Domain not found in Thunder") - log.Printf(" └──────────────────────────────") - return false, nil - } - - if resp.StatusCode != 200 { - log.Printf(" │ ⚠ Unexpected status: %d", resp.StatusCode) - log.Printf(" └──────────────────────────────") - return false, fmt.Errorf("unexpected status: %d", resp.StatusCode) - } - - var ouResp OrgUnitResponse - if err := json.NewDecoder(resp.Body).Decode(&ouResp); err != nil { - log.Printf(" │ ✗ Failed to parse response: %v", err) - log.Printf(" └──────────────────────────────") - return false, err - } - - log.Printf(" │ ✓ Domain found in Thunder") - log.Printf(" │ OU ID: %s", ouResp.ID) - log.Printf(" │ OU Name: %s", ouResp.Name) - log.Printf(" └──────────────────────────────") - - // Cache the OU ID for future user lookups - ouCacheMutex.Lock() - ouCache[domain] = ouResp.ID - ouCacheMutex.Unlock() - - return true, nil -} - -// GetOrgUnitIDForDomain retrieves the OU ID for a domain from Thunder or cache -func GetOrgUnitIDForDomain(domain, host, port string, tokenRefreshSeconds int) (string, error) { - // Check cache first - ouCacheMutex.RLock() - ouID, found := ouCache[domain] - ouCacheMutex.RUnlock() - - if found { - return ouID, nil - } - - // Need to query Thunder to get OU ID - log.Printf(" │ Fetching OU ID for domain: %s", domain) - - // Get authentication token - auth, err := GetAuth(host, port, tokenRefreshSeconds) - if err != nil { - return "", err - } - - // Build OU path - ouPath, err := buildOUPath(domain) - if err != nil { - return "", err - } - - // Query Thunder API for OU - client := GetHTTPClient() - url := fmt.Sprintf("https://%s:%s/organization-units/tree/%s", host, port, ouPath) - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err - } - - req.Header.Set("Authorization", "Bearer "+auth.BearerToken) - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return "", fmt.Errorf("OU not found or error: %d", resp.StatusCode) - } - - var ouResp OrgUnitResponse - if err := json.NewDecoder(resp.Body).Decode(&ouResp); err != nil { - return "", err - } - - // Cache the result - ouCacheMutex.Lock() - ouCache[domain] = ouResp.ID - ouCacheMutex.Unlock() - - log.Printf(" │ ✓ OU ID cached: %s", ouResp.ID) - - return ouResp.ID, nil -} diff --git a/services/socketmap/internal/thunder/group.go b/services/socketmap/internal/thunder/group.go deleted file mode 100644 index 88da16c..0000000 --- a/services/socketmap/internal/thunder/group.go +++ /dev/null @@ -1,109 +0,0 @@ -package thunder - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "strings" -) - -// ValidateGroupAddress checks if an address matches the group pattern and exists in Thunder IDP. -// Expected format: -group@ -func ValidateGroupAddress(email, host, port string, tokenRefreshSeconds int) (bool, error) { - log.Printf(" ┌─ Thunder Group Validation ────") - log.Printf(" │ Email: %s", email) - defer log.Printf(" └──────────────────────────────") - - parts := strings.Split(email, "@") - if len(parts) != 2 { - log.Printf(" │ ✗ Invalid email format") - return false, nil - } - - localPart := parts[0] - domain := parts[1] - - if !strings.HasSuffix(localPart, "-group") { - log.Printf(" │ ✗ Not a group address") - return false, nil - } - - groupName := strings.TrimSuffix(localPart, "-group") - if groupName == "" { - log.Printf(" │ ✗ Empty group name") - return false, nil - } - - log.Printf(" │ Group: %s", groupName) - log.Printf(" │ Domain: %s", domain) - - auth, err := GetAuth(host, port, tokenRefreshSeconds) - if err != nil { - log.Printf(" │ ⚠ Auth failed: %v", err) - return false, err - } - - ouID, err := GetOrgUnitIDForDomain(domain, host, port, tokenRefreshSeconds) - if err != nil { - log.Printf(" │ ⚠ Failed to get OU ID: %v", err) - return false, err - } - - log.Printf(" │ OU ID: %s", ouID) - - client := GetHTTPClient() - escapedGroupName := escapeFilterValue(groupName) - filter := fmt.Sprintf("name eq \"%s\"", escapedGroupName) - - baseURL := fmt.Sprintf("https://%s:%s/groups", host, port) - req, err := http.NewRequest("GET", baseURL, nil) - if err != nil { - log.Printf(" │ ✗ Failed to create request: %v", err) - return false, err - } - - q := req.URL.Query() - q.Add("filter", filter) - req.URL.RawQuery = q.Encode() - - req.Header.Set("Authorization", "Bearer "+auth.BearerToken) - req.Header.Set("Content-Type", "application/json") - - log.Printf(" │ Query: %s", req.URL.String()) - - resp, err := client.Do(req) - if err != nil { - log.Printf(" │ ✗ Request failed: %v", err) - return false, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - log.Printf(" │ ⚠ Unexpected status: %d", resp.StatusCode) - return false, fmt.Errorf("unexpected status: %d", resp.StatusCode) - } - - var groupsResp GroupsResponse - if err := json.NewDecoder(resp.Body).Decode(&groupsResp); err != nil { - log.Printf(" │ ✗ Failed to parse response: %v", err) - return false, err - } - - log.Printf(" │ Total results: %d", groupsResp.TotalResults) - - if groupsResp.TotalResults == 0 { - log.Printf(" │ ✗ Group not found in Thunder") - return false, nil - } - - for _, group := range groupsResp.Groups { - if group.OrganizationUnitID == ouID && group.Name == groupName { - log.Printf(" │ ✓ Group found and OU matches") - return true, nil - } - } - - log.Printf(" │ ✗ Group found but OU/name mismatch") - return false, nil -} \ No newline at end of file diff --git a/services/socketmap/internal/thunder/types.go b/services/socketmap/internal/thunder/types.go deleted file mode 100644 index 1d033fd..0000000 --- a/services/socketmap/internal/thunder/types.go +++ /dev/null @@ -1,66 +0,0 @@ -package thunder - -import ( - "time" -) - -// Auth holds Thunder authentication state -type Auth struct { - DevelopAppID string - FlowID string - BearerToken string - ExpiresAt time.Time - LastRefresh time.Time -} - -// FlowStartResponse represents the response from flow start -type FlowStartResponse struct { - FlowID string `json:"flowId"` -} - -// FlowCompleteResponse represents the response from flow completion -type FlowCompleteResponse struct { - Assertion string `json:"assertion"` -} - -// OrgUnitResponse represents an organization unit from Thunder -type OrgUnitResponse struct { - ID string `json:"id"` - Handle string `json:"handle"` - Name string `json:"name"` - Description string `json:"description"` - Parent *string `json:"parent"` -} - -// UsersResponse represents the response from Thunder Users API -type UsersResponse struct { - TotalResults int `json:"totalResults"` - StartIndex int `json:"startIndex"` - Count int `json:"count"` - Users []User `json:"users"` - Links []interface{} `json:"links"` -} - -// User represents a Thunder user -type User struct { - ID string `json:"id"` - OrganizationUnit string `json:"organizationUnit"` - Type string `json:"type"` - Attributes map[string]interface{} `json:"attributes"` -} - -// GroupsResponse represents the response from Thunder Groups API -type GroupsResponse struct { - TotalResults int `json:"totalResults"` - StartIndex int `json:"startIndex"` - Count int `json:"count"` - Groups []Group `json:"groups"` - Links []interface{} `json:"links"` -} - -// Group represents a Thunder group -type Group struct { - ID string `json:"id"` - Name string `json:"name"` - OrganizationUnitID string `json:"organizationUnitId"` -} diff --git a/services/socketmap/internal/thunder/user.go b/services/socketmap/internal/thunder/user.go deleted file mode 100644 index 27ad012..0000000 --- a/services/socketmap/internal/thunder/user.go +++ /dev/null @@ -1,127 +0,0 @@ -package thunder - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "strings" -) - -// escapeFilterValue escapes special characters in filter values to prevent injection attacks -func escapeFilterValue(value string) string { - // Escape backslashes first, then double quotes - value = strings.ReplaceAll(value, "\\", "\\\\") - value = strings.ReplaceAll(value, "\"", "\\\"") - return value -} - -// ValidateUser checks if a user exists in Thunder IDP -func ValidateUser(email, host, port string, tokenRefreshSeconds int) (bool, error) { - log.Printf(" ┌─ Thunder User Validation ─────") - log.Printf(" │ Email: %s", email) - - // Parse email to get username and domain - parts := strings.Split(email, "@") - if len(parts) != 2 { - log.Printf(" │ ✗ Invalid email format") - log.Printf(" └──────────────────────────────") - return false, nil - } - - username := parts[0] - domain := parts[1] - - log.Printf(" │ Username: %s", username) - log.Printf(" │ Domain: %s", domain) - - // Get authentication token - auth, err := GetAuth(host, port, tokenRefreshSeconds) - if err != nil { - log.Printf(" │ ⚠ Auth failed: %v", err) - log.Printf(" └──────────────────────────────") - return false, err - } - - // Get the OU ID for the domain - ouID, err := GetOrgUnitIDForDomain(domain, host, port, tokenRefreshSeconds) - if err != nil { - log.Printf(" │ ⚠ Failed to get OU ID: %v", err) - log.Printf(" └──────────────────────────────") - return false, err - } - - log.Printf(" │ OU ID: %s", ouID) - - // Query Thunder Users API with filter - client := GetHTTPClient() - // Escape username to prevent filter injection attacks - escapedUsername := escapeFilterValue(username) - filter := fmt.Sprintf("username eq \"%s\"", escapedUsername) - - baseURL := fmt.Sprintf("https://%s:%s/users", host, port) - - req, err := http.NewRequest("GET", baseURL, nil) - if err != nil { - log.Printf(" │ ✗ Failed to create request: %v", err) - log.Printf(" └──────────────────────────────") - return false, err - } - - // Add the filter as a query parameter (will be automatically URL encoded) - q := req.URL.Query() - q.Add("filter", filter) - req.URL.RawQuery = q.Encode() - - log.Printf(" │ Query: %s", req.URL.String()) - - req.Header.Set("Authorization", "Bearer "+auth.BearerToken) - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - log.Printf(" │ ✗ Request failed: %v", err) - log.Printf(" └──────────────────────────────") - return false, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - log.Printf(" │ ⚠ Unexpected status: %d", resp.StatusCode) - log.Printf(" └──────────────────────────────") - return false, fmt.Errorf("unexpected status: %d", resp.StatusCode) - } - - var usersResp UsersResponse - if err := json.NewDecoder(resp.Body).Decode(&usersResp); err != nil { - log.Printf(" │ ✗ Failed to parse response: %v", err) - log.Printf(" └──────────────────────────────") - return false, err - } - - log.Printf(" │ Total results: %d", usersResp.TotalResults) - - if usersResp.TotalResults == 0 { - log.Printf(" │ ✗ User not found in Thunder") - log.Printf(" └──────────────────────────────") - return false, nil - } - - // Validate that the user belongs to the correct OU - for _, user := range usersResp.Users { - log.Printf(" │ Found user ID: %s", user.ID) - log.Printf(" │ User OU: %s", user.OrganizationUnit) - - if user.OrganizationUnit == ouID { - log.Printf(" │ ✓ User found and OU matches!") - log.Printf(" └──────────────────────────────") - return true, nil - } else { - log.Printf(" │ ⚠ OU mismatch (expected: %s, got: %s)", ouID, user.OrganizationUnit) - } - } - - log.Printf(" │ ✗ User found but OU doesn't match") - log.Printf(" └──────────────────────────────") - return false, nil -} diff --git a/services/socketmap/main.go b/services/socketmap/main.go deleted file mode 100644 index 7f47fef..0000000 --- a/services/socketmap/main.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "log" - "time" - - "socketmap/config" - "socketmap/internal/cache" - "socketmap/internal/server" - "socketmap/internal/thunder" -) - -func main() { - log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) - log.Println("╔════════════════════════════════════════════════════════════╗") - log.Println("║ Socketmap Service - Postfix Virtual Mailbox Maps ║") - log.Println("╚════════════════════════════════════════════════════════════╝") - log.Println("") - - // Load configuration - cfg := config.Load() - - // Authenticate with Thunder at startup with retry logic - log.Println("┌─ Thunder Authentication ─────────") - - var auth *thunder.Auth - var err error - maxRetries := 5 - retryDelay := 2 * time.Second - - for attempt := 1; attempt <= maxRetries; attempt++ { - if attempt > 1 { - log.Printf("│ Retry attempt %d/%d (waiting %v)...", attempt, maxRetries, retryDelay) - time.Sleep(retryDelay) - retryDelay *= 2 // Exponential backoff - } - - auth, err = thunder.Authenticate(cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds) - if err == nil { - thunder.SetAuth(auth) - log.Println("└───────────────────────────────────") - break - } - - if attempt < maxRetries { - log.Printf("│ ⚠ Authentication attempt %d failed: %v", attempt, err) - } - } - - if err != nil { - log.Printf("│ ⚠ Initial authentication failed after %d attempts: %v", maxRetries, err) - log.Printf("│ Service will attempt to authenticate on first request") - log.Println("└───────────────────────────────────") - } - log.Println("") - - // Start token refresh goroutine - go func() { - ticker := time.NewTicker(time.Duration(cfg.TokenRefreshSeconds) * time.Second) - defer ticker.Stop() - - for range ticker.C { - log.Println("⏰ Token refresh timer triggered") - newAuth, err := thunder.Authenticate(cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds) - if err != nil { - log.Printf("⚠ Token refresh failed: %v", err) - } else { - thunder.SetAuth(newAuth) - log.Println("✓ Token refreshed successfully") - } - } - }() - - // Initialize cache - cacheManager := cache.New(cfg.CacheTTLSeconds) - - // Display configuration - log.Printf("Starting socketmap service on %s:%s", cfg.Host, cfg.Port) - log.Printf("Configuration:") - log.Printf(" • Thunder Host: %s:%s", cfg.ThunderHost, cfg.ThunderPort) - log.Printf(" • Cache TTL: %d seconds", cfg.CacheTTLSeconds) - log.Printf(" • Token Refresh: %d seconds", cfg.TokenRefreshSeconds) - log.Println("") - - // Create and start server - srv := server.New(cfg, cacheManager) - if err := srv.Start(); err != nil { - log.Fatalf("✗ Failed to start server: %v", err) - } -}