| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 | package upstreamimport (	"net/url"	"regexp"	"strings"	"github.com/0xJacky/Nginx-UI/settings")// ProxyTarget represents a proxy destinationtype 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 configurationtype TheUpstreamContext struct {	Name     string	Resolver string}// ParseResult contains the results of parsing nginx configurationtype ParseResult struct {	ProxyTargets []ProxyTarget	Upstreams    map[string][]ProxyTarget // upstream name -> servers}// ParseProxyTargetsFromRawContent parses proxy targets from raw nginx configuration contentfunc ParseProxyTargetsFromRawContent(content string) []ProxyTarget {	result := ParseProxyTargetsAndUpstreamsFromRawContent(content)	return result.ProxyTargets}// ParseProxyTargetsAndUpstreamsFromRawContent parses both proxy targets and upstream definitions from raw nginx configuration contentfunc ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {	var targets []ProxyTarget	upstreams := make(map[string][]ProxyTarget)	// First, collect all upstream names and their contexts	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			}		}	}	// 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 {			proxyPassURL := strings.TrimSpace(match[1])			// 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 {			grpcPassURL := strings.TrimSpace(match[1])			// 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 portfunc 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 contextfunc 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= configurationsfunc 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 addressesfunc 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 targetsfunc 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 blockfunc 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}
 |