Илья Глазунов 881028c1e6 feat: Add reverse proxy functionality with enhanced routing capabilities
- Introduced IgnoreRequestPath option in proxy configuration to allow exact match routing.
- Implemented proxy_pass directive in routing extension to handle backend requests.
- Enhanced error handling for backend unavailability and timeouts.
- Added integration tests for reverse proxy, including basic requests, exact match routes, regex routes, header forwarding, and query string preservation.
- Created helper functions for setting up test servers and backends, along with assertion utilities for response validation.
- Updated server initialization to support extension management and middleware chaining.
- Improved logging for debugging purposes during request handling.
2025-12-12 00:38:30 +03:00

159 lines
4.0 KiB
Go

// Package server provides the HTTP server implementation
package server
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/konduktor/konduktor/internal/config"
"github.com/konduktor/konduktor/internal/extension"
"github.com/konduktor/konduktor/internal/logging"
"github.com/konduktor/konduktor/internal/middleware"
)
const Version = "0.2.0"
// Server represents the Konduktor HTTP server
type Server struct {
config *config.Config
httpServer *http.Server
extensionManager *extension.Manager
logger *logging.Logger
}
// New creates a new server instance
func New(cfg *config.Config) (*Server, error) {
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid configuration: %w", err)
}
logger, err := logging.NewFromConfig(cfg.Logging)
if err != nil {
return nil, fmt.Errorf("failed to create logger: %w", err)
}
// Create extension manager
extManager := extension.NewManager(logger)
// Load extensions from config
for _, extCfg := range cfg.Extensions {
// Add static_dir to routing config if not present
if extCfg.Type == "routing" {
if extCfg.Config == nil {
extCfg.Config = make(map[string]interface{})
}
if _, ok := extCfg.Config["static_dir"]; !ok {
extCfg.Config["static_dir"] = cfg.HTTP.StaticDir
}
}
if err := extManager.LoadExtension(extCfg.Type, extCfg.Config); err != nil {
logger.Error("Failed to load extension", "type", extCfg.Type, "error", err)
// Continue loading other extensions
}
}
srv := &Server{
config: cfg,
extensionManager: extManager,
logger: logger,
}
return srv, nil
}
// Run starts the server and blocks until shutdown
func (s *Server) Run() error {
// Build handler chain with middleware
handler := s.buildHandler()
// Create HTTP server
addr := fmt.Sprintf("%s:%d", s.config.Server.Host, s.config.Server.Port)
s.httpServer = &http.Server{
Addr: addr,
Handler: handler,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Start server in goroutine
errChan := make(chan error, 1)
go func() {
s.logger.Info("Server starting", "addr", addr, "version", Version)
var err error
if s.config.SSL.Enabled {
err = s.httpServer.ListenAndServeTLS(s.config.SSL.CertFile, s.config.SSL.KeyFile)
} else {
err = s.httpServer.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
errChan <- err
}
}()
// Wait for shutdown signal
return s.waitForShutdown(errChan)
}
// buildHandler builds the HTTP handler chain
func (s *Server) buildHandler() http.Handler {
// Create base handler that returns 404
baseHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
})
// Wrap with extension manager
var handler http.Handler = s.extensionManager.Handler(baseHandler)
// Add middleware (applied in reverse order)
handler = middleware.AccessLog(handler, s.logger)
handler = middleware.ServerHeader(handler, Version)
handler = middleware.Recovery(handler, s.logger)
return handler
}
// waitForShutdown waits for shutdown signal and gracefully stops the server
func (s *Server) waitForShutdown(errChan <-chan error) error {
// Listen for shutdown signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
select {
case err := <-errChan:
return err
case sig := <-sigChan:
s.logger.Info("Shutdown signal received", "signal", sig.String())
}
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.logger.Info("Shutting down server...")
// Cleanup extensions
s.extensionManager.Cleanup()
if err := s.httpServer.Shutdown(ctx); err != nil {
s.logger.Error("Error during shutdown", "error", err)
return err
}
s.logger.Info("Server stopped gracefully")
return nil
}
// Shutdown gracefully shuts down the server
func (s *Server) Shutdown(ctx context.Context) error {
return s.httpServer.Shutdown(ctx)
}