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.
This commit is contained in:
Илья Глазунов
2025-12-12 00:38:30 +03:00
parent 8f5b9a5cd1
commit 881028c1e6
17 changed files with 3574 additions and 176 deletions
+101 -4
View File
@@ -2,6 +2,7 @@
package routing
import (
"fmt"
"net/http"
"os"
"path/filepath"
@@ -11,6 +12,7 @@ import (
"github.com/konduktor/konduktor/internal/config"
"github.com/konduktor/konduktor/internal/logging"
"github.com/konduktor/konduktor/internal/proxy"
)
// RouteMatch represents a matched route with captured parameters
@@ -55,6 +57,24 @@ func New(cfg *config.Config, logger *logging.Logger) *Router {
regexRoutes: make([]*RegexRoute, 0),
}
// Load routes from extensions
if cfg != nil {
for _, ext := range cfg.Extensions {
if ext.Type == "routing" && ext.Config != nil {
if locations, ok := ext.Config["regex_locations"].(map[string]interface{}); ok {
for pattern, routeCfg := range locations {
if rc, ok := routeCfg.(map[string]interface{}); ok {
r.AddRoute(pattern, rc)
if logger != nil {
logger.Debug("Added route", "pattern", pattern)
}
}
}
}
}
}
}
r.setupRoutes()
return r
}
@@ -250,17 +270,27 @@ func (r *Router) defaultHandler(w http.ResponseWriter, req *http.Request) {
// Try to match against configured routes
match := r.Match(path)
fmt.Printf("DEBUG defaultHandler: path=%q match=%v defaultRoute=%v\n", path, match != nil, r.defaultRoute != nil)
if match != nil {
fmt.Printf("DEBUG: matched config: %v\n", match.Config)
r.handleRouteMatch(w, req, match)
return
}
// Try to serve static file
if r.staticDir != "" {
filePath := filepath.Join(r.staticDir, path)
// Get absolute path for static dir
absStaticDir, err := filepath.Abs(r.staticDir)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Prevent directory traversal
if !strings.HasPrefix(filepath.Clean(filePath), filepath.Clean(r.staticDir)) {
filePath := filepath.Join(absStaticDir, filepath.Clean("/"+path))
cleanPath := filepath.Clean(filePath)
// Prevent directory traversal - ensure path is within static dir
if !strings.HasPrefix(cleanPath+string(filepath.Separator), absStaticDir+string(filepath.Separator)) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
@@ -290,6 +320,12 @@ func (r *Router) defaultHandler(w http.ResponseWriter, req *http.Request) {
func (r *Router) handleRouteMatch(w http.ResponseWriter, req *http.Request, match *RouteMatch) {
cfg := match.Config
// Handle proxy_pass directive
if proxyTarget, ok := cfg["proxy_pass"].(string); ok {
r.handleProxyPass(w, req, proxyTarget, cfg, match.Params)
return
}
// Handle "return" directive
if ret, ok := cfg["return"].(string); ok {
parts := strings.SplitN(ret, " ", 2)
@@ -338,7 +374,26 @@ func (r *Router) handleRouteMatch(w http.ResponseWriter, req *http.Request, matc
}
}
filePath := filepath.Join(root, path)
// Get absolute path for root dir
absRoot, err := filepath.Abs(root)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
filePath := filepath.Join(absRoot, filepath.Clean("/"+path))
cleanPath := filepath.Clean(filePath)
// DEBUG
fmt.Printf("DEBUG: path=%q absRoot=%q filePath=%q cleanPath=%q\n", path, absRoot, filePath, cleanPath)
fmt.Printf("DEBUG: check1=%q check2=%q\n", cleanPath+string(filepath.Separator), absRoot+string(filepath.Separator))
// Prevent directory traversal
if !strings.HasPrefix(cleanPath+string(filepath.Separator), absRoot+string(filepath.Separator)) {
fmt.Printf("DEBUG: FORBIDDEN - path not within root\n")
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
if cacheControl, ok := cfg["cache_control"].(string); ok {
w.Header().Set("Cache-Control", cacheControl)
@@ -379,6 +434,48 @@ func (r *Router) handleRouteMatch(w http.ResponseWriter, req *http.Request, matc
http.NotFound(w, req)
}
// handleProxyPass proxies the request to the target backend
func (r *Router) handleProxyPass(w http.ResponseWriter, req *http.Request, target string, cfg map[string]interface{}, params map[string]string) {
// Substitute params in target URL (e.g., {version} -> actual version)
for key, value := range params {
target = strings.ReplaceAll(target, "{"+key+"}", value)
}
// Create proxy
proxyConfig := &proxy.Config{
Target: target,
Headers: make(map[string]string),
}
// Parse headers from config
if headers, ok := cfg["headers"].([]interface{}); ok {
for _, h := range headers {
if header, ok := h.(string); ok {
parts := strings.SplitN(header, ": ", 2)
if len(parts) == 2 {
// Substitute params in header values
headerValue := parts[1]
for key, value := range params {
headerValue = strings.ReplaceAll(headerValue, "{"+key+"}", value)
}
proxyConfig.Headers[parts[0]] = headerValue
}
}
}
}
p, err := proxy.New(proxyConfig, r.logger)
if err != nil {
if r.logger != nil {
r.logger.Error("Failed to create proxy", "target", target, "error", err)
}
http.Error(w, "Bad Gateway", http.StatusBadGateway)
return
}
p.ProxyRequest(w, req, params)
}
// CreateRouterFromConfig creates a router from extension config
func CreateRouterFromConfig(cfg map[string]interface{}) *Router {
router := NewRouter()