mirror of
https://github.com/hhftechnology/middleware-manager.git
synced 2026-07-18 20:33:25 +00:00
Replace direct defers that ignored returned errors with explicit defer func() { _ = ... }() and use blank-assignment for Setenv/RemoveAll/Close to satisfy error-checking and ensure closures run. Convert several if-else chains to switch for clarity (status filter, test server path matching), switch strings.Replace to strings.ReplaceAll, and normalize some error/log message casing. These changes are minor refactors across handlers, services, database and tests to improve lint compliance and readability.
997 lines
28 KiB
Go
997 lines
28 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hhftechnology/middleware-manager/database"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// DefaultMiddleware represents a default middleware template
|
|
type DefaultMiddleware struct {
|
|
ID string `yaml:"id"`
|
|
Name string `yaml:"name"`
|
|
Type string `yaml:"type"`
|
|
Config map[string]interface{} `yaml:"config"`
|
|
}
|
|
|
|
// DefaultTemplates represents the structure of the templates.yaml file
|
|
type DefaultTemplates struct {
|
|
Middlewares []DefaultMiddleware `yaml:"middlewares"`
|
|
}
|
|
|
|
// LoadDefaultTemplates loads the default middleware templates
|
|
func LoadDefaultTemplates(db *database.DB) error {
|
|
// Determine the path to the templates file
|
|
templatesFile := "config/templates.yaml"
|
|
|
|
// Check if the file exists in the current directory
|
|
if _, err := os.Stat(templatesFile); os.IsNotExist(err) {
|
|
// Try to find it in different locations
|
|
possiblePaths := []string{
|
|
"/app/config/templates.yaml", // Docker container path
|
|
"templates.yaml", // Current directory
|
|
}
|
|
|
|
found := false
|
|
for _, path := range possiblePaths {
|
|
if _, err := os.Stat(path); err == nil {
|
|
templatesFile = path
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
log.Printf("Warning: templates.yaml not found, skipping default templates")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Read the templates file
|
|
data, err := os.ReadFile(templatesFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse the YAML
|
|
var templates DefaultTemplates
|
|
if err := yaml.Unmarshal(data, &templates); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Process templates to ensure proper value preservation based on middleware type
|
|
for i := range templates.Middlewares {
|
|
// Apply middleware-specific processing based on type
|
|
switch templates.Middlewares[i].Type {
|
|
case "headers":
|
|
processHeadersMiddleware(&templates.Middlewares[i].Config)
|
|
case "redirectRegex", "redirectScheme", "replacePath", "replacePathRegex", "stripPrefix", "stripPrefixRegex":
|
|
processPathMiddleware(&templates.Middlewares[i].Config, templates.Middlewares[i].Type)
|
|
case "basicAuth", "digestAuth", "forwardAuth":
|
|
processAuthMiddleware(&templates.Middlewares[i].Config, templates.Middlewares[i].Type)
|
|
case "plugin":
|
|
processPluginMiddleware(&templates.Middlewares[i].Config)
|
|
case "chain":
|
|
processChainingMiddleware(&templates.Middlewares[i].Config)
|
|
default:
|
|
// General processing for other middleware types
|
|
templates.Middlewares[i].Config = preserveTraefikValues(templates.Middlewares[i].Config).(map[string]interface{})
|
|
}
|
|
}
|
|
|
|
// Add templates to the database if they don't exist
|
|
for _, middleware := range templates.Middlewares {
|
|
// Check if the middleware already exists
|
|
var exists int
|
|
err := db.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", middleware.ID).Scan(&exists)
|
|
if err == nil {
|
|
// Middleware exists, skip
|
|
continue
|
|
}
|
|
|
|
// Check if user has explicitly deleted this template - don't re-create it
|
|
var deleted int
|
|
err = db.QueryRow("SELECT 1 FROM deleted_templates WHERE id = ? AND type = 'middleware'", middleware.ID).Scan(&deleted)
|
|
if err == nil {
|
|
// User deleted this template, skip
|
|
continue
|
|
}
|
|
|
|
// Convert config to JSON string
|
|
configJSON, err := json.Marshal(middleware.Config)
|
|
if err != nil {
|
|
log.Printf("Failed to marshal config for %s: %v", middleware.Name, err)
|
|
continue
|
|
}
|
|
|
|
// Insert into database
|
|
_, err = db.Exec(
|
|
"INSERT INTO middlewares (id, name, type, config) VALUES (?, ?, ?, ?)",
|
|
middleware.ID, middleware.Name, middleware.Type, string(configJSON),
|
|
)
|
|
|
|
if err != nil {
|
|
log.Printf("Failed to insert middleware %s: %v", middleware.Name, err)
|
|
continue
|
|
}
|
|
|
|
log.Printf("Added default middleware: %s", middleware.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// preserveTraefikValues ensures all values in Traefik configurations are properly handled
|
|
// This handles special cases in different middleware types and ensures precise value preservation
|
|
func preserveTraefikValues(data interface{}) interface{} {
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
|
|
switch v := data.(type) {
|
|
case map[string]interface{}:
|
|
// Process each key-value pair in the map
|
|
for key, val := range v {
|
|
// Process values based on key names that might need special handling
|
|
switch {
|
|
// URL or path related fields
|
|
case key == "path" || key == "url" || key == "address" || strings.HasSuffix(key, "Path"):
|
|
// Ensure path strings keep their exact format
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
// Keep exact string formatting
|
|
v[key] = strVal
|
|
} else {
|
|
v[key] = preserveTraefikValues(val)
|
|
}
|
|
|
|
// Regex and replacement patterns
|
|
case key == "regex" || key == "replacement" || strings.HasSuffix(key, "Regex"):
|
|
// Ensure regex patterns are preserved exactly
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
// Keep exact string formatting with special characters
|
|
v[key] = strVal
|
|
} else {
|
|
v[key] = preserveTraefikValues(val)
|
|
}
|
|
|
|
// API keys and security tokens
|
|
case key == "key" || key == "token" || key == "secret" ||
|
|
strings.Contains(key, "Key") || strings.Contains(key, "Token") ||
|
|
strings.Contains(key, "Secret") || strings.Contains(key, "Password"):
|
|
// Ensure API keys and tokens are preserved exactly
|
|
if strVal, ok := val.(string); ok {
|
|
// Always preserve keys/tokens exactly as-is, even if empty
|
|
v[key] = strVal
|
|
} else {
|
|
v[key] = preserveTraefikValues(val)
|
|
}
|
|
|
|
// Empty header values (common in security headers middleware)
|
|
case key == "Server" || key == "X-Powered-By" || strings.HasPrefix(key, "X-"):
|
|
// Empty string values are often used to remove headers
|
|
if strVal, ok := val.(string); ok {
|
|
// Preserve empty strings exactly
|
|
v[key] = strVal
|
|
} else {
|
|
v[key] = preserveTraefikValues(val)
|
|
}
|
|
|
|
// IP addresses and networks
|
|
case key == "ip" || key == "clientIP" || strings.Contains(key, "IP") ||
|
|
key == "sourceRange" || key == "excludedIPs":
|
|
// IP addresses often need exact formatting
|
|
v[key] = preserveTraefikValues(val)
|
|
|
|
// Boolean flags that control behavior
|
|
case strings.HasPrefix(key, "is") || strings.HasPrefix(key, "has") ||
|
|
strings.HasPrefix(key, "enable") || strings.HasSuffix(key, "enabled") ||
|
|
strings.HasSuffix(key, "Enabled") || key == "permanent" || key == "forceSlash":
|
|
// Ensure boolean values are preserved as actual booleans
|
|
if boolVal, ok := val.(bool); ok {
|
|
v[key] = boolVal
|
|
} else if strVal, ok := val.(string); ok {
|
|
// Convert string "true"/"false" to actual boolean if needed
|
|
switch strVal {
|
|
case "true":
|
|
v[key] = true
|
|
case "false":
|
|
v[key] = false
|
|
default:
|
|
v[key] = strVal // Keep as is if not a boolean string
|
|
}
|
|
} else {
|
|
v[key] = preserveTraefikValues(val)
|
|
}
|
|
|
|
// Integer values like timeouts, ports, limits
|
|
case key == "amount" || key == "burst" || key == "port" ||
|
|
strings.HasSuffix(key, "Seconds") || strings.HasSuffix(key, "Limit") ||
|
|
strings.HasSuffix(key, "Timeout") || strings.HasSuffix(key, "Size") ||
|
|
key == "depth" || key == "priority" || key == "statusCode" ||
|
|
key == "attempts" || key == "responseCode":
|
|
// Ensure numeric values are preserved as numbers
|
|
switch numVal := val.(type) {
|
|
case int:
|
|
v[key] = numVal
|
|
case float64:
|
|
// Keep as float if it has decimal part, otherwise convert to int
|
|
if numVal == float64(int(numVal)) {
|
|
v[key] = int(numVal)
|
|
} else {
|
|
v[key] = numVal
|
|
}
|
|
case string:
|
|
// Try to convert string to number if it looks like one
|
|
if i, err := strconv.Atoi(numVal); err == nil {
|
|
v[key] = i
|
|
} else if f, err := strconv.ParseFloat(numVal, 64); err == nil {
|
|
v[key] = f
|
|
} else {
|
|
v[key] = numVal // Keep as string if not numeric
|
|
}
|
|
default:
|
|
v[key] = preserveTraefikValues(val)
|
|
}
|
|
|
|
// Default handling for other keys
|
|
default:
|
|
v[key] = preserveTraefikValues(val)
|
|
}
|
|
}
|
|
return v
|
|
|
|
case []interface{}:
|
|
// Process each element in the array
|
|
for i, item := range v {
|
|
v[i] = preserveTraefikValues(item)
|
|
}
|
|
return v
|
|
|
|
case string:
|
|
// Preserve all string values exactly as they are
|
|
return v
|
|
|
|
case int, float64, bool:
|
|
// Preserve primitive types as they are
|
|
return v
|
|
|
|
default:
|
|
// For any other type, return as is
|
|
return v
|
|
}
|
|
}
|
|
|
|
// processHeadersMiddleware handles the headers middleware special processing
|
|
func processHeadersMiddleware(config *map[string]interface{}) {
|
|
// Special handling for response headers which might contain empty strings
|
|
if customResponseHeaders, ok := (*config)["customResponseHeaders"].(map[string]interface{}); ok {
|
|
for key, value := range customResponseHeaders {
|
|
// Ensure empty strings are preserved exactly
|
|
if strValue, ok := value.(string); ok {
|
|
customResponseHeaders[key] = strValue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special handling for request headers which might contain empty strings
|
|
if customRequestHeaders, ok := (*config)["customRequestHeaders"].(map[string]interface{}); ok {
|
|
for key, value := range customRequestHeaders {
|
|
// Ensure empty strings are preserved exactly
|
|
if strValue, ok := value.(string); ok {
|
|
customRequestHeaders[key] = strValue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process header fields that are often quoted strings
|
|
specialStringFields := []string{
|
|
"customFrameOptionsValue", "contentSecurityPolicy",
|
|
"referrerPolicy", "permissionsPolicy",
|
|
}
|
|
|
|
for _, field := range specialStringFields {
|
|
if value, ok := (*config)[field].(string); ok {
|
|
// Preserve string exactly, especially if it contains quotes
|
|
(*config)[field] = value
|
|
}
|
|
}
|
|
|
|
// Process other header configuration values
|
|
*config = preserveTraefikValues(*config).(map[string]interface{})
|
|
}
|
|
|
|
// processChainingMiddleware handles chain middleware special processing
|
|
func processChainingMiddleware(config *map[string]interface{}) {
|
|
if middlewares, ok := (*config)["middlewares"].([]interface{}); ok {
|
|
for i, middleware := range middlewares {
|
|
if middlewareStr, ok := middleware.(string); ok {
|
|
// If this is not already a fully qualified middleware reference
|
|
if !strings.Contains(middlewareStr, "@") {
|
|
// Assume it's from our file provider
|
|
middlewares[i] = fmt.Sprintf("%s@file", middlewareStr)
|
|
}
|
|
}
|
|
}
|
|
(*config)["middlewares"] = middlewares
|
|
}
|
|
|
|
// Process other chain configuration values
|
|
*config = preserveTraefikValues(*config).(map[string]interface{})
|
|
}
|
|
|
|
// processPathMiddleware handles path manipulation middlewares
|
|
func processPathMiddleware(config *map[string]interface{}, middlewareType string) {
|
|
// Special handling for regex patterns - these need exact preservation
|
|
if regex, ok := (*config)["regex"].(string); ok {
|
|
// Preserve regex pattern exactly
|
|
(*config)["regex"] = regex
|
|
} else if regexList, ok := (*config)["regex"].([]interface{}); ok {
|
|
// Handle regex arrays (like in stripPrefixRegex)
|
|
for i, r := range regexList {
|
|
if regexStr, ok := r.(string); ok {
|
|
regexList[i] = regexStr
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special handling for replacement patterns
|
|
if replacement, ok := (*config)["replacement"].(string); ok {
|
|
// Preserve replacement pattern exactly
|
|
(*config)["replacement"] = replacement
|
|
}
|
|
|
|
// Special handling for path values
|
|
if path, ok := (*config)["path"].(string); ok {
|
|
// Preserve path exactly
|
|
(*config)["path"] = path
|
|
}
|
|
|
|
// Special handling for prefixes arrays
|
|
if prefixes, ok := (*config)["prefixes"].([]interface{}); ok {
|
|
for i, prefix := range prefixes {
|
|
if prefixStr, ok := prefix.(string); ok {
|
|
prefixes[i] = prefixStr
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special handling for scheme
|
|
if scheme, ok := (*config)["scheme"].(string); ok {
|
|
// Preserve scheme exactly
|
|
(*config)["scheme"] = scheme
|
|
}
|
|
|
|
// Process boolean options
|
|
if forceSlash, ok := (*config)["forceSlash"].(bool); ok {
|
|
(*config)["forceSlash"] = forceSlash
|
|
}
|
|
|
|
if permanent, ok := (*config)["permanent"].(bool); ok {
|
|
(*config)["permanent"] = permanent
|
|
}
|
|
|
|
// Process other path manipulation configuration values
|
|
*config = preserveTraefikValues(*config).(map[string]interface{})
|
|
}
|
|
|
|
// processAuthMiddleware handles authentication middleware special processing
|
|
func processAuthMiddleware(config *map[string]interface{}, middlewareType string) {
|
|
// ForwardAuth middleware special handling
|
|
if middlewareType == "forwardAuth" {
|
|
if address, ok := (*config)["address"].(string); ok {
|
|
// Preserve address URL exactly
|
|
(*config)["address"] = address
|
|
}
|
|
|
|
// Process trust settings
|
|
if trustForward, ok := (*config)["trustForwardHeader"].(bool); ok {
|
|
(*config)["trustForwardHeader"] = trustForward
|
|
}
|
|
|
|
// Process headers array
|
|
if headers, ok := (*config)["authResponseHeaders"].([]interface{}); ok {
|
|
for i, header := range headers {
|
|
if headerStr, ok := header.(string); ok {
|
|
headers[i] = headerStr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// BasicAuth/DigestAuth middleware special handling
|
|
if middlewareType == "basicAuth" || middlewareType == "digestAuth" {
|
|
// Preserve exact format of users array
|
|
if users, ok := (*config)["users"].([]interface{}); ok {
|
|
for i, user := range users {
|
|
if userStr, ok := user.(string); ok {
|
|
users[i] = userStr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process other auth configuration values
|
|
*config = preserveTraefikValues(*config).(map[string]interface{})
|
|
}
|
|
|
|
// processPluginMiddleware handles plugin middleware special processing
|
|
func processPluginMiddleware(config *map[string]interface{}) {
|
|
// Process plugins (including CrowdSec)
|
|
for pluginName, pluginCfg := range *config {
|
|
if pluginConfig, ok := pluginCfg.(map[string]interface{}); ok {
|
|
// Process special fields in plugin configurations
|
|
|
|
// Process API keys and secrets - must be preserved exactly
|
|
keyFields := []string{
|
|
"crowdsecLapiKey", "apiKey", "token", "secret", "password",
|
|
"key", "accessKey", "secretKey", "captchaSiteKey", "captchaSecretKey",
|
|
}
|
|
|
|
for _, field := range keyFields {
|
|
if val, exists := pluginConfig[field]; exists {
|
|
if strVal, ok := val.(string); ok {
|
|
// Ensure keys are preserved exactly as-is
|
|
pluginConfig[field] = strVal
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process boolean options
|
|
boolFields := []string{
|
|
"enabled", "failureBlock", "unreachableBlock", "insecureVerify",
|
|
"allowLocalRequests", "logLocalRequests", "logAllowedRequests",
|
|
"logApiRequests", "silentStartUp", "forceMonthlyUpdate",
|
|
"allowUnknownCountries", "blackListMode", "addCountryHeader",
|
|
}
|
|
|
|
for _, field := range boolFields {
|
|
for configKey, val := range pluginConfig {
|
|
if strings.Contains(configKey, field) {
|
|
if boolVal, ok := val.(bool); ok {
|
|
pluginConfig[configKey] = boolVal
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process arrays
|
|
arrayFields := []string{
|
|
"forwardedHeadersTrustedIPs", "clientTrustedIPs", "countries",
|
|
}
|
|
|
|
for _, field := range arrayFields {
|
|
for configKey, val := range pluginConfig {
|
|
if strings.Contains(configKey, field) {
|
|
if arrayVal, ok := val.([]interface{}); ok {
|
|
for i, item := range arrayVal {
|
|
if strItem, ok := item.(string); ok {
|
|
arrayVal[i] = strItem
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process remaining fields
|
|
(*config)[pluginName] = preserveTraefikValues(pluginConfig).(map[string]interface{})
|
|
}
|
|
}
|
|
|
|
// Process the entire config
|
|
*config = preserveTraefikValues(*config).(map[string]interface{})
|
|
}
|
|
|
|
// EnsureConfigDirectory ensures the configuration directory exists
|
|
func EnsureConfigDirectory(path string) error {
|
|
return os.MkdirAll(path, 0755)
|
|
}
|
|
|
|
// SaveTemplateFile saves the default templates file if it doesn't exist
|
|
func SaveTemplateFile(templatesDir string) error {
|
|
templatesFile := filepath.Join(templatesDir, "templates.yaml")
|
|
|
|
// Check if file already exists
|
|
if _, err := os.Stat(templatesFile); err == nil {
|
|
// File exists, skip
|
|
return nil
|
|
}
|
|
|
|
// Create default templates
|
|
templates := DefaultTemplates{
|
|
Middlewares: []DefaultMiddleware{
|
|
// Authentication middlewares
|
|
{
|
|
ID: "authelia",
|
|
Name: "Authelia",
|
|
Type: "forwardAuth",
|
|
Config: map[string]interface{}{
|
|
"address": "http://authelia:9091/api/authz/forward-auth",
|
|
"trustForwardHeader": true,
|
|
"authResponseHeaders": []string{
|
|
"Remote-User",
|
|
"Remote-Groups",
|
|
"Remote-Name",
|
|
"Remote-Email",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "authentik",
|
|
Name: "Authentik",
|
|
Type: "forwardAuth",
|
|
Config: map[string]interface{}{
|
|
"address": "http://authentik:9000/outpost.goauthentik.io/auth/traefik",
|
|
"trustForwardHeader": true,
|
|
"authResponseHeaders": []string{
|
|
"X-authentik-username",
|
|
"X-authentik-groups",
|
|
"X-authentik-email",
|
|
"X-authentik-name",
|
|
"X-authentik-uid",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "basic-auth",
|
|
Name: "Basic Auth",
|
|
Type: "basicAuth",
|
|
Config: map[string]interface{}{
|
|
"users": []string{
|
|
"admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "digest-auth",
|
|
Name: "Digest Auth",
|
|
Type: "digestAuth",
|
|
Config: map[string]interface{}{
|
|
"users": []string{
|
|
"test:traefik:a2688e031edb4be6a3797f3882655c05",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "jwt-auth",
|
|
Name: "JWT Authentication",
|
|
Type: "forwardAuth",
|
|
Config: map[string]interface{}{
|
|
"address": "http://jwt-auth:8080/verify",
|
|
"trustForwardHeader": true,
|
|
"authResponseHeaders": []string{
|
|
"X-JWT-Sub",
|
|
"X-JWT-Name",
|
|
"X-JWT-Email",
|
|
},
|
|
},
|
|
},
|
|
|
|
// Security middlewares
|
|
{
|
|
ID: "ip-allowlist",
|
|
Name: "IP Allow List",
|
|
Type: "ipAllowList",
|
|
Config: map[string]interface{}{
|
|
"sourceRange": []string{
|
|
"127.0.0.1/32",
|
|
"192.168.1.0/24",
|
|
"10.0.0.0/8",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "rate-limit",
|
|
Name: "Rate Limit",
|
|
Type: "rateLimit",
|
|
Config: map[string]interface{}{
|
|
"average": int(100),
|
|
"burst": int(50),
|
|
},
|
|
},
|
|
{
|
|
ID: "headers-standard",
|
|
Name: "Standard Security Headers",
|
|
Type: "headers",
|
|
Config: map[string]interface{}{
|
|
"accessControlAllowMethods": []string{
|
|
"GET",
|
|
"OPTIONS",
|
|
"PUT",
|
|
},
|
|
"browserXssFilter": true,
|
|
"contentTypeNosniff": true,
|
|
"customFrameOptionsValue": "SAMEORIGIN",
|
|
"customResponseHeaders": map[string]string{
|
|
"X-Forwarded-Proto": "https",
|
|
"X-Robots-Tag": "none,noarchive,nosnippet,notranslate,noimageindex",
|
|
"Server": "", // Empty string to remove header
|
|
},
|
|
"forceSTSHeader": true,
|
|
"hostsProxyHeaders": []string{
|
|
"X-Forwarded-Host",
|
|
},
|
|
"permissionsPolicy": "camera=(), microphone=(), geolocation=(), payment=(), usb=(), vr=()",
|
|
"referrerPolicy": "same-origin",
|
|
"sslProxyHeaders": map[string]string{
|
|
"X-Forwarded-Proto": "https",
|
|
},
|
|
"stsIncludeSubdomains": true,
|
|
"stsPreload": true,
|
|
"stsSeconds": int(63072000),
|
|
},
|
|
},
|
|
{
|
|
ID: "in-flight-req",
|
|
Name: "In-Flight Request Limiter",
|
|
Type: "inFlightReq",
|
|
Config: map[string]interface{}{
|
|
"amount": int(10),
|
|
"sourceCriterion": map[string]interface{}{
|
|
"ipStrategy": map[string]interface{}{
|
|
"depth": int(2),
|
|
"excludedIPs": []string{
|
|
"127.0.0.1/32",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "pass-tls-cert",
|
|
Name: "Pass TLS Client Certificate",
|
|
Type: "passTLSClientCert",
|
|
Config: map[string]interface{}{
|
|
"pem": true,
|
|
},
|
|
},
|
|
|
|
// Path manipulation middlewares - with properly formatted regex patterns
|
|
{
|
|
ID: "add-prefix",
|
|
Name: "Add Prefix",
|
|
Type: "addPrefix",
|
|
Config: map[string]interface{}{
|
|
"prefix": "/api",
|
|
},
|
|
},
|
|
{
|
|
ID: "strip-prefix",
|
|
Name: "Strip Prefix",
|
|
Type: "stripPrefix",
|
|
Config: map[string]interface{}{
|
|
"prefixes": []string{
|
|
"/api",
|
|
"/v1",
|
|
},
|
|
"forceSlash": true,
|
|
},
|
|
},
|
|
{
|
|
ID: "strip-prefix-regex",
|
|
Name: "Strip Prefix Regex",
|
|
Type: "stripPrefixRegex",
|
|
Config: map[string]interface{}{
|
|
"regex": []string{
|
|
"/foo/[a-z0-9]+/[0-9]+/",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "replace-path",
|
|
Name: "Replace Path",
|
|
Type: "replacePath",
|
|
Config: map[string]interface{}{
|
|
"path": "/api",
|
|
},
|
|
},
|
|
{
|
|
ID: "replace-path-regex",
|
|
Name: "Replace Path Regex",
|
|
Type: "replacePathRegex",
|
|
Config: map[string]interface{}{
|
|
"regex": "^/foo/(.*)",
|
|
"replacement": "/bar/$1",
|
|
},
|
|
},
|
|
|
|
// Redirect middlewares - with properly formatted regex patterns
|
|
{
|
|
ID: "redirect-regex",
|
|
Name: "Redirect Regex",
|
|
Type: "redirectRegex",
|
|
Config: map[string]interface{}{
|
|
"regex": "^http://localhost/(.*)",
|
|
"replacement": "https://example.com/${1}",
|
|
"permanent": true,
|
|
},
|
|
},
|
|
{
|
|
ID: "redirect-scheme",
|
|
Name: "Redirect to HTTPS",
|
|
Type: "redirectScheme",
|
|
Config: map[string]interface{}{
|
|
"scheme": "https",
|
|
"port": "443",
|
|
"permanent": true,
|
|
},
|
|
},
|
|
|
|
// Content processing middlewares
|
|
{
|
|
ID: "compress",
|
|
Name: "Compress Response",
|
|
Type: "compress",
|
|
Config: map[string]interface{}{
|
|
"excludedContentTypes": []string{
|
|
"text/event-stream",
|
|
},
|
|
"includedContentTypes": []string{
|
|
"text/html",
|
|
"text/plain",
|
|
"application/json",
|
|
},
|
|
"minResponseBodyBytes": int(1024),
|
|
"encodings": []string{
|
|
"gzip",
|
|
"br",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "buffering",
|
|
Name: "Request/Response Buffering",
|
|
Type: "buffering",
|
|
Config: map[string]interface{}{
|
|
"maxRequestBodyBytes": int(5000000),
|
|
"memRequestBodyBytes": int(2000000),
|
|
"maxResponseBodyBytes": int(5000000),
|
|
"memResponseBodyBytes": int(2000000),
|
|
"retryExpression": "IsNetworkError() && Attempts() < 2",
|
|
},
|
|
},
|
|
{
|
|
ID: "content-type",
|
|
Name: "Content Type Auto-Detector",
|
|
Type: "contentType",
|
|
Config: map[string]interface{}{},
|
|
},
|
|
|
|
// Error handling and reliability middlewares
|
|
{
|
|
ID: "circuit-breaker",
|
|
Name: "Circuit Breaker",
|
|
Type: "circuitBreaker",
|
|
Config: map[string]interface{}{
|
|
"expression": "NetworkErrorRatio() > 0.20 || ResponseCodeRatio(500, 600, 0, 600) > 0.25",
|
|
"checkPeriod": "10s",
|
|
"fallbackDuration": "30s",
|
|
"recoveryDuration": "60s",
|
|
"responseCode": int(503),
|
|
},
|
|
},
|
|
{
|
|
ID: "retry",
|
|
Name: "Retry Failed Requests",
|
|
Type: "retry",
|
|
Config: map[string]interface{}{
|
|
"attempts": int(3),
|
|
"initialInterval": "100ms",
|
|
},
|
|
},
|
|
{
|
|
ID: "error-pages",
|
|
Name: "Custom Error Pages",
|
|
Type: "errors",
|
|
Config: map[string]interface{}{
|
|
"status": []string{
|
|
"500-599",
|
|
},
|
|
"service": "error-handler-service",
|
|
"query": "/{status}.html",
|
|
},
|
|
},
|
|
{
|
|
ID: "grpc-web",
|
|
Name: "gRPC Web",
|
|
Type: "grpcWeb",
|
|
Config: map[string]interface{}{
|
|
"allowOrigins": []string{
|
|
"*",
|
|
},
|
|
},
|
|
},
|
|
|
|
// Chain middlewares
|
|
{
|
|
ID: "security-chain",
|
|
Name: "Security Chain",
|
|
Type: "chain",
|
|
Config: map[string]interface{}{
|
|
"middlewares": []string{
|
|
"rate-limit",
|
|
"ip-whitelist",
|
|
},
|
|
},
|
|
},
|
|
|
|
// Crowdsec plugin middleware with proper API key handling
|
|
{
|
|
ID: "crowdsec",
|
|
Name: "Crowdsec",
|
|
Type: "plugin",
|
|
Config: map[string]interface{}{
|
|
"crowdsec": map[string]interface{}{
|
|
"enabled": true,
|
|
"logLevel": "INFO",
|
|
"updateIntervalSeconds": int(15),
|
|
"updateMaxFailure": int(0),
|
|
"defaultDecisionSeconds": int(15),
|
|
"httpTimeoutSeconds": int(10),
|
|
"crowdsecMode": "live",
|
|
"crowdsecAppsecEnabled": true,
|
|
"crowdsecAppsecHost": "crowdsec:7422",
|
|
"crowdsecAppsecFailureBlock": true,
|
|
"crowdsecAppsecUnreachableBlock": true,
|
|
"crowdsecAppsecBodyLimit": int(10485760), // Use int instead of integer to avoid scientific notation
|
|
"crowdsecLapiKey": "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK",
|
|
"crowdsecLapiHost": "crowdsec:8080",
|
|
"crowdsecLapiScheme": "http",
|
|
"forwardedHeadersTrustedIPs": []string{
|
|
"0.0.0.0/0",
|
|
},
|
|
"clientTrustedIPs": []string{
|
|
"10.0.0.0/8",
|
|
"172.16.0.0/12",
|
|
"192.168.0.0/16",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
// Special use case middlewares - with properly formatted regex pattern
|
|
{
|
|
ID: "nextcloud-dav",
|
|
Name: "Nextcloud WebDAV Redirect",
|
|
Type: "replacePathRegex",
|
|
Config: map[string]interface{}{
|
|
"regex": "^/.well-known/ca(l|rd)dav",
|
|
"replacement": "/remote.php/dav/",
|
|
},
|
|
},
|
|
|
|
// Custom headers example with empty string preservation
|
|
{
|
|
ID: "custom-headers-example",
|
|
Name: "Custom Headers Example",
|
|
Type: "headers",
|
|
Config: map[string]interface{}{
|
|
"customRequestHeaders": map[string]string{
|
|
"X-Script-Name": "test",
|
|
"X-Custom-Value": "value with spaces",
|
|
"X-Custom-Request-Header": "", // Empty string to remove header
|
|
},
|
|
"customResponseHeaders": map[string]string{
|
|
"X-Custom-Response-Header": "value",
|
|
"Server": "", // Empty string to remove header
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Process all templates to ensure proper value preservation
|
|
for i := range templates.Middlewares {
|
|
// Apply middleware-specific processing based on type
|
|
switch templates.Middlewares[i].Type {
|
|
case "headers":
|
|
processHeadersMiddleware(&templates.Middlewares[i].Config)
|
|
case "redirectRegex", "redirectScheme", "replacePath", "replacePathRegex", "stripPrefix", "stripPrefixRegex":
|
|
processPathMiddleware(&templates.Middlewares[i].Config, templates.Middlewares[i].Type)
|
|
case "basicAuth", "digestAuth", "forwardAuth":
|
|
processAuthMiddleware(&templates.Middlewares[i].Config, templates.Middlewares[i].Type)
|
|
case "plugin":
|
|
processPluginMiddleware(&templates.Middlewares[i].Config)
|
|
case "chain":
|
|
processChainingMiddleware(&templates.Middlewares[i].Config)
|
|
default:
|
|
// General processing for other middleware types
|
|
templates.Middlewares[i].Config = preserveTraefikValues(templates.Middlewares[i].Config).(map[string]interface{})
|
|
}
|
|
}
|
|
|
|
// Create a custom YAML encoder that preserves string formatting
|
|
yamlNode := &yaml.Node{}
|
|
err := yamlNode.Encode(templates)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to encode templates to YAML node: %w", err)
|
|
}
|
|
|
|
// Apply additional string preservation to the YAML node
|
|
preserveStringsInYamlNode(yamlNode)
|
|
|
|
// Marshal the processed node
|
|
data, err := yaml.Marshal(yamlNode)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal YAML node: %w", err)
|
|
}
|
|
|
|
// Write to file
|
|
return os.WriteFile(templatesFile, data, 0644)
|
|
}
|
|
|
|
// preserveStringsInYamlNode ensures that string values, especially empty strings,
|
|
// are preserved correctly in the YAML node structure before marshaling
|
|
func preserveStringsInYamlNode(node *yaml.Node) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
|
|
// Process node based on its kind
|
|
switch node.Kind {
|
|
case yaml.DocumentNode, yaml.SequenceNode:
|
|
// Process all content/items
|
|
for i := range node.Content {
|
|
preserveStringsInYamlNode(node.Content[i])
|
|
}
|
|
|
|
case yaml.MappingNode:
|
|
// Process all key-value pairs
|
|
for i := 0; i < len(node.Content); i += 2 {
|
|
// Get key and value
|
|
keyNode := node.Content[i]
|
|
valueNode := node.Content[i+1]
|
|
|
|
// Process based on key content
|
|
if keyNode.Value == "Server" || keyNode.Value == "X-Powered-By" ||
|
|
strings.HasPrefix(keyNode.Value, "X-") {
|
|
// These are likely header fields where empty strings are important
|
|
if valueNode.Kind == yaml.ScalarNode && valueNode.Value == "" {
|
|
// Ensure empty strings are properly encoded
|
|
valueNode.Style = yaml.DoubleQuotedStyle
|
|
}
|
|
}
|
|
|
|
// Special handling for known fields that need exact string preservation
|
|
if containsSpecialField(keyNode.Value) && valueNode.Kind == yaml.ScalarNode {
|
|
// Use double quotes for these fields to ensure proper encoding
|
|
valueNode.Style = yaml.DoubleQuotedStyle
|
|
}
|
|
|
|
// Continue recursion
|
|
preserveStringsInYamlNode(keyNode)
|
|
preserveStringsInYamlNode(valueNode)
|
|
}
|
|
|
|
case yaml.ScalarNode:
|
|
// For scalar nodes (including strings), ensure empty strings are properly quoted
|
|
if node.Value == "" {
|
|
node.Style = yaml.DoubleQuotedStyle
|
|
}
|
|
}
|
|
}
|
|
|
|
// containsSpecialField checks if a field name is one that needs special handling
|
|
// for correct string value preservation
|
|
func containsSpecialField(fieldName string) bool {
|
|
specialFields := []string{
|
|
"key", "token", "secret", "apiKey", "Key", "Token", "Secret", "Password",
|
|
"regex", "replacement", "Regex", "path", "scheme", "url", "address", "Path",
|
|
"prefix", "prefixes", "expression", "rule",
|
|
}
|
|
|
|
for _, field := range specialFields {
|
|
if strings.Contains(fieldName, field) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|