forked from aegis/pyserveX
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user