mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-05-14 18:26:52 +08:00
473 lines
15 KiB
Go
473 lines
15 KiB
Go
package upstream
|
|
|
|
import (
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/0xJacky/Nginx-UI/settings"
|
|
)
|
|
|
|
// ProxyTarget represents a proxy destination
|
|
type ProxyTarget struct {
|
|
Host string `json:"host"`
|
|
Port string `json:"port"`
|
|
Type string `json:"type"` // "proxy_pass", "grpc_pass" or "upstream"
|
|
Resolver string `json:"resolver"` // DNS resolver address (e.g., "127.0.0.1:8600")
|
|
IsConsul bool `json:"is_consul"` // Whether this is a consul service discovery target
|
|
ServiceURL string `json:"service_url"` // Full service URL for consul (e.g., "service.consul service=redacted-net resolve")
|
|
}
|
|
|
|
// TheUpstreamContext contains upstream-level configuration
|
|
type TheUpstreamContext struct {
|
|
Name string
|
|
Resolver string
|
|
}
|
|
|
|
// ParseResult contains the results of parsing nginx configuration
|
|
type ParseResult struct {
|
|
ProxyTargets []ProxyTarget
|
|
Upstreams map[string][]ProxyTarget // upstream name -> servers
|
|
}
|
|
|
|
// ParseProxyTargetsFromRawContent parses proxy targets from raw nginx configuration content
|
|
func ParseProxyTargetsFromRawContent(content string) []ProxyTarget {
|
|
result := ParseProxyTargetsAndUpstreamsFromRawContent(content)
|
|
return result.ProxyTargets
|
|
}
|
|
|
|
// ParseProxyTargetsAndUpstreamsFromRawContent parses both proxy targets and upstream definitions from raw nginx configuration content
|
|
func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
|
|
var targets []ProxyTarget
|
|
upstreams := make(map[string][]ProxyTarget)
|
|
|
|
// First, collect all upstream names and their contexts
|
|
// Also collect literal variable assignments from `set $var value;`
|
|
upstreamNames := make(map[string]bool)
|
|
upstreamContexts := make(map[string]*TheUpstreamContext)
|
|
upstreamRegex := regexp.MustCompile(`(?s)upstream\s+([^\s]+)\s*\{([^}]+)\}`)
|
|
upstreamMatches := upstreamRegex.FindAllStringSubmatch(content, -1)
|
|
|
|
// Parse upstream blocks and collect upstream names
|
|
for _, match := range upstreamMatches {
|
|
if len(match) >= 3 {
|
|
upstreamName := match[1]
|
|
upstreamNames[upstreamName] = true
|
|
upstreamContent := match[2]
|
|
|
|
// Create upstream context
|
|
ctx := &TheUpstreamContext{
|
|
Name: upstreamName,
|
|
}
|
|
|
|
// Extract resolver information from upstream block
|
|
resolverRegex := regexp.MustCompile(`(?m)^\s*resolver\s+([^;]+);`)
|
|
if resolverMatch := resolverRegex.FindStringSubmatch(upstreamContent); len(resolverMatch) >= 2 {
|
|
// Parse resolver directive (e.g., "127.0.0.1:8600 valid=5s ipv6=off")
|
|
resolverParts := strings.Fields(resolverMatch[1])
|
|
if len(resolverParts) > 0 {
|
|
ctx.Resolver = resolverParts[0] // Take the first part as resolver address
|
|
}
|
|
}
|
|
|
|
upstreamContexts[upstreamName] = ctx
|
|
|
|
serverRegex := regexp.MustCompile(`(?m)^\s*server\s+([^;]+);`)
|
|
serverMatches := serverRegex.FindAllStringSubmatch(upstreamContent, -1)
|
|
|
|
var upstreamServers []ProxyTarget
|
|
for _, serverMatch := range serverMatches {
|
|
if len(serverMatch) >= 2 {
|
|
target := parseServerAddress(strings.TrimSpace(serverMatch[1]), "upstream", ctx)
|
|
if target.Host != "" {
|
|
targets = append(targets, target)
|
|
upstreamServers = append(upstreamServers, target)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store upstream definition
|
|
if len(upstreamServers) > 0 {
|
|
upstreams[upstreamName] = upstreamServers
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect simple literal variables defined via `set $var value;`
|
|
// Only variables with literal values (no nginx variables inside) are recorded.
|
|
variableValues := extractLiteralSetVariables(content)
|
|
|
|
// Parse proxy_pass directives, but skip upstream references
|
|
proxyPassRegex := regexp.MustCompile(`(?m)^\s*proxy_pass\s+([^;]+);`)
|
|
proxyMatches := proxyPassRegex.FindAllStringSubmatch(content, -1)
|
|
|
|
for _, match := range proxyMatches {
|
|
if len(match) >= 2 {
|
|
rawValue := strings.TrimSpace(match[1])
|
|
|
|
// If the value is a single variable like `$target`, try to resolve it from `set $target ...;`
|
|
if resolved, ok := resolveSingleVariable(rawValue, variableValues); ok {
|
|
rawValue = resolved
|
|
}
|
|
|
|
proxyPassURL := rawValue
|
|
// Skip if this proxy_pass references an upstream
|
|
if !isUpstreamReference(proxyPassURL, upstreamNames) {
|
|
target := parseProxyPassURL(proxyPassURL, "proxy_pass")
|
|
if target.Host != "" {
|
|
targets = append(targets, target)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse grpc_pass directives, but skip upstream references
|
|
grpcPassRegex := regexp.MustCompile(`(?m)^\s*grpc_pass\s+([^;]+);`)
|
|
grpcMatches := grpcPassRegex.FindAllStringSubmatch(content, -1)
|
|
|
|
for _, match := range grpcMatches {
|
|
if len(match) >= 2 {
|
|
rawValue := strings.TrimSpace(match[1])
|
|
|
|
// If the value is a single variable like `$target`, try to resolve it from `set $target ...;`
|
|
if resolved, ok := resolveSingleVariable(rawValue, variableValues); ok {
|
|
rawValue = resolved
|
|
}
|
|
|
|
grpcPassURL := rawValue
|
|
// Skip if this grpc_pass references an upstream
|
|
if !isUpstreamReference(grpcPassURL, upstreamNames) {
|
|
target := parseProxyPassURL(grpcPassURL, "grpc_pass")
|
|
if target.Host != "" {
|
|
targets = append(targets, target)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return &ParseResult{
|
|
ProxyTargets: deduplicateTargets(targets),
|
|
Upstreams: upstreams,
|
|
}
|
|
}
|
|
|
|
// parseProxyPassURL parses a proxy_pass or grpc_pass URL and extracts host and port
|
|
func parseProxyPassURL(passURL, passType string) ProxyTarget {
|
|
passURL = strings.TrimSpace(passURL)
|
|
|
|
// Skip URLs that contain Nginx variables
|
|
if strings.Contains(passURL, "$") {
|
|
return ProxyTarget{}
|
|
}
|
|
|
|
// Handle HTTP/HTTPS/gRPC URLs (e.g., "http://backend", "grpc://backend")
|
|
if strings.HasPrefix(passURL, "http://") || strings.HasPrefix(passURL, "https://") || strings.HasPrefix(passURL, "grpc://") || strings.HasPrefix(passURL, "grpcs://") {
|
|
if parsedURL, err := url.Parse(passURL); err == nil {
|
|
host := parsedURL.Hostname()
|
|
port := parsedURL.Port()
|
|
|
|
// Set default ports if not specified
|
|
if port == "" {
|
|
switch parsedURL.Scheme {
|
|
case "https":
|
|
port = "443"
|
|
case "grpcs":
|
|
port = "443"
|
|
case "grpc":
|
|
port = "80"
|
|
default: // http
|
|
port = "80"
|
|
}
|
|
}
|
|
|
|
// Skip if this is the HTTP challenge port used by Let's Encrypt
|
|
if host == "127.0.0.1" && port == settings.CertSettings.HTTPChallengePort {
|
|
return ProxyTarget{}
|
|
}
|
|
|
|
return ProxyTarget{
|
|
Host: host,
|
|
Port: port,
|
|
Type: passType,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle direct address format for stream module (e.g., "127.0.0.1:8080", "backend.example.com:12345")
|
|
// This is used in stream configurations where proxy_pass/grpc_pass doesn't require a protocol
|
|
if !strings.Contains(passURL, "://") {
|
|
target := parseServerAddress(passURL, passType, nil) // No upstream context for this function
|
|
|
|
// Skip if this is the HTTP challenge port used by Let's Encrypt
|
|
if target.Host == "127.0.0.1" && target.Port == settings.CertSettings.HTTPChallengePort {
|
|
return ProxyTarget{}
|
|
}
|
|
|
|
return target
|
|
}
|
|
|
|
return ProxyTarget{}
|
|
}
|
|
|
|
// parseServerAddress parses upstream server address with upstream context
|
|
func parseServerAddress(serverAddr string, targetType string, ctx *TheUpstreamContext) ProxyTarget {
|
|
serverAddr = strings.TrimSpace(serverAddr)
|
|
|
|
// Remove additional parameters (weight, max_fails, etc.)
|
|
parts := strings.Fields(serverAddr)
|
|
if len(parts) == 0 {
|
|
return ProxyTarget{}
|
|
}
|
|
|
|
addr := parts[0]
|
|
target := ProxyTarget{
|
|
Type: targetType,
|
|
}
|
|
|
|
// Add resolver information from upstream context
|
|
if ctx != nil && ctx.Resolver != "" {
|
|
target.Resolver = ctx.Resolver
|
|
}
|
|
|
|
// Check if the address contains Nginx variables - skip if it does
|
|
if strings.Contains(addr, "$") {
|
|
return ProxyTarget{}
|
|
}
|
|
|
|
// Check for consul service discovery patterns
|
|
if isConsulServiceDiscovery(serverAddr) {
|
|
target.IsConsul = true
|
|
target.ServiceURL = serverAddr
|
|
|
|
// Extract consul DNS host (e.g., "service.consul")
|
|
if strings.Contains(addr, "service.consul") {
|
|
target.Host = "service.consul"
|
|
// For consul service discovery, we use a placeholder port since the actual port is dynamic
|
|
target.Port = "dynamic"
|
|
} else {
|
|
// Fallback to regular parsing
|
|
parsed := parseAddressOnly(addr)
|
|
target.Host = parsed.Host
|
|
target.Port = parsed.Port
|
|
}
|
|
|
|
return target
|
|
}
|
|
|
|
// Regular address parsing
|
|
parsed := parseAddressOnly(addr)
|
|
target.Host = parsed.Host
|
|
target.Port = parsed.Port
|
|
|
|
// Skip if this is the HTTP challenge port used by Let's Encrypt
|
|
if target.Host == "127.0.0.1" && target.Port == settings.CertSettings.HTTPChallengePort {
|
|
return ProxyTarget{}
|
|
}
|
|
|
|
return target
|
|
}
|
|
|
|
// isConsulServiceDiscovery checks if the server address is a dynamic service discovery configuration
|
|
// This includes both Consul and standard nginx service= configurations
|
|
func isConsulServiceDiscovery(serverAddr string) bool {
|
|
// Standard nginx service= format: "hostname service=name resolve"
|
|
if strings.Contains(serverAddr, "service=") && strings.Contains(serverAddr, "resolve") {
|
|
return true
|
|
}
|
|
// Legacy consul format: "service.consul service=name resolve"
|
|
return strings.Contains(serverAddr, "service.consul") &&
|
|
(strings.Contains(serverAddr, "service=") || strings.Contains(serverAddr, "resolve"))
|
|
}
|
|
|
|
// parseAddressOnly parses just the address portion without consul-specific logic
|
|
// Supports both IPv4 and IPv6 addresses
|
|
func parseAddressOnly(addr string) ProxyTarget {
|
|
// Handle IPv6 addresses with brackets
|
|
if strings.HasPrefix(addr, "[") {
|
|
// IPv6 format: [::1]:8080 or [2001:db8::1]:8080
|
|
if idx := strings.LastIndex(addr, "]:"); idx != -1 {
|
|
host := addr[1:idx]
|
|
port := addr[idx+2:]
|
|
return ProxyTarget{
|
|
Host: host,
|
|
Port: port,
|
|
}
|
|
}
|
|
// IPv6 without port: [::1] or [2001:db8::1]
|
|
host := strings.Trim(addr, "[]")
|
|
return ProxyTarget{
|
|
Host: host,
|
|
Port: "80",
|
|
}
|
|
}
|
|
|
|
// Check if this might be an IPv6 address without brackets
|
|
// IPv6 addresses contain multiple colons
|
|
colonCount := strings.Count(addr, ":")
|
|
if colonCount > 1 {
|
|
// This is likely an IPv6 address without brackets and without port
|
|
// e.g., ::1, 2001:db8::1, fe80::1%eth0
|
|
return ProxyTarget{
|
|
Host: addr,
|
|
Port: "80",
|
|
}
|
|
}
|
|
|
|
// Handle IPv4 addresses and hostnames with port
|
|
if strings.Contains(addr, ":") {
|
|
parts := strings.Split(addr, ":")
|
|
if len(parts) == 2 {
|
|
return ProxyTarget{
|
|
Host: parts[0],
|
|
Port: parts[1],
|
|
}
|
|
}
|
|
}
|
|
|
|
// No port specified, use default
|
|
return ProxyTarget{
|
|
Host: addr,
|
|
Port: "80",
|
|
}
|
|
}
|
|
|
|
// deduplicateTargets removes duplicate proxy targets
|
|
func deduplicateTargets(targets []ProxyTarget) []ProxyTarget {
|
|
seen := make(map[string]bool)
|
|
var result []ProxyTarget
|
|
|
|
for _, target := range targets {
|
|
// Create a unique key that includes resolver and consul information
|
|
// Use formatSocketAddress for proper IPv6 handling in the key
|
|
socketAddr := formatSocketAddress(target.Host, target.Port)
|
|
key := socketAddr + ":" + target.Type + ":" + target.Resolver
|
|
if target.IsConsul {
|
|
key += ":consul:" + target.ServiceURL
|
|
}
|
|
|
|
if !seen[key] {
|
|
seen[key] = true
|
|
result = append(result, target)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// isUpstreamReference checks if a proxy_pass or grpc_pass URL references an upstream block
|
|
func isUpstreamReference(passURL string, upstreamNames map[string]bool) bool {
|
|
passURL = strings.TrimSpace(passURL)
|
|
|
|
// For HTTP/HTTPS/gRPC URLs, parse the URL to extract the hostname
|
|
if strings.HasPrefix(passURL, "http://") || strings.HasPrefix(passURL, "https://") || strings.HasPrefix(passURL, "grpc://") || strings.HasPrefix(passURL, "grpcs://") {
|
|
// Handle URLs with nginx variables (e.g., "https://myUpStr$request_uri")
|
|
// Extract the scheme and hostname part before any nginx variables
|
|
schemeAndHost := passURL
|
|
if dollarIndex := strings.Index(passURL, "$"); dollarIndex != -1 {
|
|
schemeAndHost = passURL[:dollarIndex]
|
|
}
|
|
|
|
// Try to parse the URL, if it fails, try manual extraction
|
|
if parsedURL, err := url.Parse(schemeAndHost); err == nil {
|
|
hostname := parsedURL.Hostname()
|
|
// Check if the hostname matches any upstream name
|
|
return upstreamNames[hostname]
|
|
} else {
|
|
// Fallback: manually extract hostname for URLs with variables
|
|
// Remove scheme prefix
|
|
withoutScheme := passURL
|
|
if strings.HasPrefix(passURL, "https://") {
|
|
withoutScheme = strings.TrimPrefix(passURL, "https://")
|
|
} else if strings.HasPrefix(passURL, "http://") {
|
|
withoutScheme = strings.TrimPrefix(passURL, "http://")
|
|
} else if strings.HasPrefix(passURL, "grpc://") {
|
|
withoutScheme = strings.TrimPrefix(passURL, "grpc://")
|
|
} else if strings.HasPrefix(passURL, "grpcs://") {
|
|
withoutScheme = strings.TrimPrefix(passURL, "grpcs://")
|
|
}
|
|
|
|
// Extract hostname before any path, port, or variable
|
|
hostname := withoutScheme
|
|
if slashIndex := strings.Index(hostname, "/"); slashIndex != -1 {
|
|
hostname = hostname[:slashIndex]
|
|
}
|
|
if colonIndex := strings.Index(hostname, ":"); colonIndex != -1 {
|
|
hostname = hostname[:colonIndex]
|
|
}
|
|
if dollarIndex := strings.Index(hostname, "$"); dollarIndex != -1 {
|
|
hostname = hostname[:dollarIndex]
|
|
}
|
|
|
|
return upstreamNames[hostname]
|
|
}
|
|
}
|
|
|
|
// For stream module, proxy_pass/grpc_pass can directly reference upstream name without protocol
|
|
// Check if the pass value directly matches an upstream name
|
|
if !strings.Contains(passURL, "://") && !strings.Contains(passURL, ":") {
|
|
return upstreamNames[passURL]
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// extractLiteralSetVariables parses `set $var value;` directives from the entire content and
|
|
// returns a map of variable name to its literal value. Values containing nginx variables are ignored.
|
|
func extractLiteralSetVariables(content string) map[string]string {
|
|
result := make(map[string]string)
|
|
|
|
// Capture variable name and raw value (without trailing semicolon)
|
|
setRegex := regexp.MustCompile(`(?m)^\s*set\s+\$([A-Za-z0-9_]+)\s+([^;]+);`)
|
|
matches := setRegex.FindAllStringSubmatch(content, -1)
|
|
for _, m := range matches {
|
|
if len(m) < 3 {
|
|
continue
|
|
}
|
|
name := m[1]
|
|
value := strings.TrimSpace(m[2])
|
|
|
|
// Remove surrounding quotes if any
|
|
if len(value) >= 2 {
|
|
if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) ||
|
|
(strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) {
|
|
value = strings.Trim(value, `"'`)
|
|
}
|
|
}
|
|
|
|
// Ignore values containing nginx variables unless it is a single variable reference
|
|
if strings.Contains(value, "$") {
|
|
// Support simple indirection: set $a $b;
|
|
if resolved, ok := resolveSingleVariable(value, result); ok {
|
|
result[name] = resolved
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Record literal value
|
|
result[name] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
// resolveSingleVariable resolves an expression that is exactly a single variable like `$target`
|
|
// using the provided map. Returns (resolvedValue, true) if resolvable; otherwise ("", false).
|
|
func resolveSingleVariable(expr string, variables map[string]string) (string, bool) {
|
|
expr = strings.TrimSpace(expr)
|
|
// Match exactly `$varName` with optional surrounding spaces
|
|
varOnlyRegex := regexp.MustCompile(`^\$([A-Za-z0-9_]+)$`)
|
|
sub := varOnlyRegex.FindStringSubmatch(expr)
|
|
if len(sub) < 2 {
|
|
return "", false
|
|
}
|
|
name := sub[1]
|
|
val, ok := variables[name]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
// Guard against cyclic or unresolved values that still contain variables
|
|
if strings.Contains(val, "$") {
|
|
return "", false
|
|
}
|
|
return val, true
|
|
}
|