package gateway import ( "crypto/subtle" "fmt" "log" "net/http" "net/http/httputil" "net/url" "os" "strings" "sync" "time" httperrors "github.com/explorer/backend/libs/go-http-errors" httpmiddleware "github.com/explorer/backend/libs/go-http-middleware" ) // Gateway represents the API gateway type Gateway struct { apiURL *url.URL rateLimiter *RateLimiter auth *AuthMiddleware } // NewGateway creates a new API gateway func NewGateway(apiURL string) (*Gateway, error) { parsedURL, err := url.Parse(apiURL) if err != nil { return nil, fmt.Errorf("invalid API URL: %w", err) } return &Gateway{ apiURL: parsedURL, rateLimiter: NewRateLimiter(), auth: NewAuthMiddleware(), }, nil } // Start starts the gateway server func (g *Gateway) Start(port int) error { mux := http.NewServeMux() // Proxy to API server proxy := httputil.NewSingleHostReverseProxy(g.apiURL) mux.HandleFunc("/", g.handleRequest(proxy)) addr := fmt.Sprintf(":%d", port) log.Printf("Starting API Gateway on %s", addr) return http.ListenAndServe(addr, mux) } // handleRequest handles incoming requests with middleware func (g *Gateway) handleRequest(proxy *httputil.ReverseProxy) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Add security headers g.addSecurityHeaders(w) // Authentication if !g.auth.Authenticate(r) { httperrors.WriteJSON(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unauthorized") return } // Rate limiting if !g.rateLimiter.Allow(r) { httperrors.WriteJSON(w, http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED", "Rate limit exceeded") return } // Add headers if clientIP := httpmiddleware.ClientIP(r); clientIP != "" { r.Header.Set("X-Forwarded-For", clientIP) } if apiKey := g.auth.GetAPIKey(r); apiKey != "" { r.Header.Set("X-API-Key", apiKey) } // Add branding header w.Header().Set("X-Explorer-Name", "SolaceScan") w.Header().Set("X-Explorer-Version", "1.0.0") // Proxy request proxy.ServeHTTP(w, r) } } // addSecurityHeaders adds security headers to responses func (g *Gateway) addSecurityHeaders(w http.ResponseWriter) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") // CSP will be set per route if needed w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") } // RateLimiter handles rate limiting type RateLimiter struct { // Simple in-memory rate limiter (should use Redis in production) mu sync.Mutex limits map[string]*limitEntry } type limitEntry struct { count int resetAt time.Time } const gatewayRequestsPerMinute = 120 func NewRateLimiter() *RateLimiter { return &RateLimiter{ limits: make(map[string]*limitEntry), } } func (rl *RateLimiter) Allow(r *http.Request) bool { clientIP := httpmiddleware.ClientIP(r) if clientIP == "" { clientIP = r.RemoteAddr } now := time.Now() rl.mu.Lock() defer rl.mu.Unlock() entry, ok := rl.limits[clientIP] if !ok || now.After(entry.resetAt) { rl.limits[clientIP] = &limitEntry{ count: 1, resetAt: now.Add(time.Minute), } return true } if entry.count >= gatewayRequestsPerMinute { return false } entry.count++ return true } // AuthMiddleware handles authentication type AuthMiddleware struct { allowAnonymous bool apiKeys []string } func NewAuthMiddleware() *AuthMiddleware { return &AuthMiddleware{ allowAnonymous: parseBoolEnv("GATEWAY_ALLOW_ANONYMOUS"), apiKeys: splitNonEmptyEnv("GATEWAY_API_KEYS"), } } func (am *AuthMiddleware) Authenticate(r *http.Request) bool { apiKey := am.GetAPIKey(r) if apiKey == "" { return am.allowAnonymous } if len(am.apiKeys) == 0 { return am.allowAnonymous } for _, allowedKey := range am.apiKeys { if subtle.ConstantTimeCompare([]byte(apiKey), []byte(allowedKey)) == 1 { return true } } return false } func (am *AuthMiddleware) GetAPIKey(r *http.Request) string { // Check header first if key := r.Header.Get("X-API-Key"); key != "" { return key } // Check query parameter if key := r.URL.Query().Get("api_key"); key != "" { return key } return "" } func parseBoolEnv(key string) bool { value := strings.TrimSpace(os.Getenv(key)) return strings.EqualFold(value, "1") || strings.EqualFold(value, "true") || strings.EqualFold(value, "yes") || strings.EqualFold(value, "on") } func splitNonEmptyEnv(key string) []string { raw := strings.TrimSpace(os.Getenv(key)) if raw == "" { return nil } parts := strings.Split(raw, ",") values := make([]string, 0, len(parts)) for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != "" { values = append(values, trimmed) } } return values }