forked from aegis/pyserveX
- 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.
159 lines
4.0 KiB
Go
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)
|
|
}
|