package gateway import ( "context" "fmt" "net" "net/http" "net/http/httputil" "net/url" "strings" "sync" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "go.uber.org/zap" "github.com/DeBrosOfficial/network/pkg/config" "github.com/DeBrosOfficial/network/pkg/logging" ) // HTTPGateway is the main reverse proxy router type HTTPGateway struct { logger *logging.ColoredLogger config *config.HTTPGatewayConfig router chi.Router reverseProxies map[string]*httputil.ReverseProxy mu sync.RWMutex server *http.Server } // NewHTTPGateway creates a new HTTP reverse proxy gateway func NewHTTPGateway(logger *logging.ColoredLogger, cfg *config.HTTPGatewayConfig) (*HTTPGateway, error) { if !cfg.Enabled { return nil, nil } if logger == nil { var err error logger, err = logging.NewColoredLogger(logging.ComponentGeneral, true) if err != nil { return nil, fmt.Errorf("failed to create logger: %w", err) } } gateway := &HTTPGateway{ logger: logger, config: cfg, router: chi.NewRouter(), reverseProxies: make(map[string]*httputil.ReverseProxy), } // Set up router middleware gateway.router.Use(middleware.RequestID) gateway.router.Use(middleware.Logger) gateway.router.Use(middleware.Recoverer) gateway.router.Use(middleware.Timeout(30 * time.Second)) // Add health check endpoint gateway.router.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"status":"ok","node":"%s"}`, cfg.NodeName) }) // Initialize reverse proxies and routes if err := gateway.initializeRoutes(); err != nil { return nil, fmt.Errorf("failed to initialize routes: %w", err) } gateway.logger.ComponentInfo(logging.ComponentGeneral, "HTTP Gateway initialized", zap.String("node_name", cfg.NodeName), zap.String("listen_addr", cfg.ListenAddr), zap.Int("routes", len(cfg.Routes)), ) return gateway, nil } // initializeRoutes sets up all reverse proxy routes func (hg *HTTPGateway) initializeRoutes() error { hg.mu.Lock() defer hg.mu.Unlock() for routeName, routeConfig := range hg.config.Routes { // Validate backend URL _, err := url.Parse(routeConfig.BackendURL) if err != nil { return fmt.Errorf("invalid backend URL for route %s: %w", routeName, err) } // Create reverse proxy with custom transport proxy := &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { // Keep original host for Host header r.Out.Host = r.In.Host // Set X-Forwarded-For header for logging r.Out.Header.Set("X-Forwarded-For", getClientIP(r.In)) }, ErrorHandler: hg.proxyErrorHandler(routeName), } // Set timeout on transport if routeConfig.Timeout > 0 { proxy.Transport = &http.Transport{ Dial: (&net.Dialer{ Timeout: routeConfig.Timeout, }).Dial, ResponseHeaderTimeout: routeConfig.Timeout, } } hg.reverseProxies[routeName] = proxy // Register route handler hg.registerRouteHandler(routeName, routeConfig, proxy) hg.logger.ComponentInfo(logging.ComponentGeneral, "Route initialized", zap.String("name", routeName), zap.String("path", routeConfig.PathPrefix), zap.String("backend", routeConfig.BackendURL), ) } return nil } // registerRouteHandler registers a route handler with the router func (hg *HTTPGateway) registerRouteHandler(name string, routeConfig config.RouteConfig, proxy *httputil.ReverseProxy) { pathPrefix := strings.TrimSuffix(routeConfig.PathPrefix, "/") // Use Mount instead of Route for wildcard path handling hg.router.Mount(pathPrefix, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { hg.handleProxyRequest(w, req, routeConfig, proxy) })) } // handleProxyRequest handles a reverse proxy request func (hg *HTTPGateway) handleProxyRequest(w http.ResponseWriter, req *http.Request, routeConfig config.RouteConfig, proxy *httputil.ReverseProxy) { // Strip path prefix before forwarding originalPath := req.URL.Path pathPrefix := strings.TrimSuffix(routeConfig.PathPrefix, "/") if strings.HasPrefix(req.URL.Path, pathPrefix) { // Remove the prefix but keep leading slash strippedPath := strings.TrimPrefix(req.URL.Path, pathPrefix) if strippedPath == "" { strippedPath = "/" } req.URL.Path = strippedPath } // Update request URL to point to backend backendURL, _ := url.Parse(routeConfig.BackendURL) req.URL.Scheme = backendURL.Scheme req.URL.Host = backendURL.Host // Log the proxy request hg.logger.ComponentInfo(logging.ComponentGeneral, "Proxy request", zap.String("original_path", originalPath), zap.String("stripped_path", req.URL.Path), zap.String("backend", routeConfig.BackendURL), zap.String("method", req.Method), zap.String("client_ip", getClientIP(req)), ) // Handle WebSocket upgrades if configured if routeConfig.WebSocket && isWebSocketRequest(req) { hg.logger.ComponentInfo(logging.ComponentGeneral, "WebSocket upgrade detected", zap.String("path", originalPath), ) } // Forward the request proxy.ServeHTTP(w, req) } // proxyErrorHandler returns an error handler for the reverse proxy func (hg *HTTPGateway) proxyErrorHandler(routeName string) func(http.ResponseWriter, *http.Request, error) { return func(w http.ResponseWriter, r *http.Request, err error) { hg.logger.ComponentError(logging.ComponentGeneral, "Proxy error", zap.String("route", routeName), zap.String("path", r.URL.Path), zap.String("method", r.Method), zap.Error(err), ) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadGateway) fmt.Fprintf(w, `{"error":"gateway error","route":"%s","detail":"%s"}`, routeName, err.Error()) } } // Start starts the HTTP gateway server func (hg *HTTPGateway) Start(ctx context.Context) error { if hg == nil || !hg.config.Enabled { return nil } hg.server = &http.Server{ Addr: hg.config.ListenAddr, Handler: hg.router, } // Listen for connections listener, err := net.Listen("tcp", hg.config.ListenAddr) if err != nil { return fmt.Errorf("failed to listen on %s: %w", hg.config.ListenAddr, err) } hg.logger.ComponentInfo(logging.ComponentGeneral, "HTTP Gateway server starting", zap.String("node_name", hg.config.NodeName), zap.String("listen_addr", hg.config.ListenAddr), ) // Serve in a goroutine go func() { if err := hg.server.Serve(listener); err != nil && err != http.ErrServerClosed { hg.logger.ComponentError(logging.ComponentGeneral, "HTTP Gateway server error", zap.Error(err)) } }() // Wait for context cancellation <-ctx.Done() return hg.Stop() } // Stop gracefully stops the HTTP gateway server func (hg *HTTPGateway) Stop() error { if hg == nil || hg.server == nil { return nil } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() hg.logger.ComponentInfo(logging.ComponentGeneral, "HTTP Gateway shutting down") if err := hg.server.Shutdown(ctx); err != nil { hg.logger.ComponentError(logging.ComponentGeneral, "HTTP Gateway shutdown error", zap.Error(err)) return err } hg.logger.ComponentInfo(logging.ComponentGeneral, "HTTP Gateway shutdown complete") return nil } // Router returns the chi router for testing or extension func (hg *HTTPGateway) Router() chi.Router { return hg.router } // isWebSocketRequest checks if a request is a WebSocket upgrade request func isWebSocketRequest(r *http.Request) bool { return r.Header.Get("Connection") == "Upgrade" && r.Header.Get("Upgrade") == "websocket" }