Skip to content

Commit 44246c9

Browse files
committed
enhance(upstream): proxy parser to support grpc_pass
1 parent d37a463 commit 44246c9

File tree

3 files changed

+209
-87
lines changed

3 files changed

+209
-87
lines changed

internal/cron/upstream_availability.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,24 @@ func executeUpstreamAvailabilityTest() {
3737

3838
targetCount := service.GetTargetCount()
3939
if targetCount == 0 {
40-
// logger.Debug("No upstream targets to test")
40+
logger.Debug("No upstream targets to test")
4141
return
4242
}
4343

4444
// Check if we should skip this test due to active WebSocket connections
4545
// (WebSocket connections trigger more frequent checks)
4646
if hasActiveWebSocketConnections() {
47-
// logger.Debug("Skipping scheduled test due to active WebSocket connections")
47+
logger.Debug("Skipping scheduled test due to active WebSocket connections")
4848
return
4949
}
5050

51-
// start := time.Now()
52-
// logger.Debug("Starting scheduled upstream availability test for", targetCount, "targets")
51+
start := time.Now()
52+
logger.Debug("Starting scheduled upstream availability test for", targetCount, "targets")
5353

54-
// service.PerformAvailabilityTest()
54+
service.PerformAvailabilityTest()
5555

56-
// duration := time.Since(start)
57-
// logger.Debug("Upstream availability test completed in", duration)
56+
duration := time.Since(start)
57+
logger.Debug("Upstream availability test completed in", duration)
5858
}
5959

6060
// hasActiveWebSocketConnections checks if there are active WebSocket connections

internal/upstream/proxy_parser.go

Lines changed: 54 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@ import (
55
"regexp"
66
"strings"
77

8-
"github.com/0xJacky/Nginx-UI/internal/nginx"
98
"github.com/0xJacky/Nginx-UI/settings"
109
)
1110

1211
// ProxyTarget represents a proxy destination
1312
type ProxyTarget struct {
1413
Host string `json:"host"`
1514
Port string `json:"port"`
16-
Type string `json:"type"` // "proxy_pass" or "upstream"
15+
Type string `json:"type"` // "proxy_pass", "grpc_pass" or "upstream"
1716
Resolver string `json:"resolver"` // DNS resolver address (e.g., "127.0.0.1:8600")
1817
IsConsul bool `json:"is_consul"` // Whether this is a consul service discovery target
1918
ServiceURL string `json:"service_url"` // Full service URL for consul (e.g., "service.consul service=redacted-net resolve")
@@ -82,88 +81,59 @@ func ParseProxyTargetsFromRawContent(content string) []ProxyTarget {
8281
proxyPassURL := strings.TrimSpace(match[1])
8382
// Skip if this proxy_pass references an upstream
8483
if !isUpstreamReference(proxyPassURL, upstreamNames) {
85-
target := parseProxyPassURL(proxyPassURL)
84+
target := parseProxyPassURL(proxyPassURL, "proxy_pass")
8685
if target.Host != "" {
8786
targets = append(targets, target)
8887
}
8988
}
9089
}
9190
}
9291

93-
return deduplicateTargets(targets)
94-
}
95-
96-
// parseUpstreamServers extracts server addresses from upstream blocks
97-
func parseUpstreamServers(upstream *nginx.NgxUpstream) []ProxyTarget {
98-
var targets []ProxyTarget
99-
100-
// Create upstream context for this upstream block
101-
ctx := &UpstreamContext{
102-
Name: upstream.Name,
103-
}
104-
105-
// Extract resolver from upstream directives
106-
for _, directive := range upstream.Directives {
107-
if directive.Directive == "resolver" {
108-
resolverParts := strings.Fields(directive.Params)
109-
if len(resolverParts) > 0 {
110-
ctx.Resolver = resolverParts[0]
111-
}
112-
}
113-
}
114-
115-
for _, directive := range upstream.Directives {
116-
if directive.Directive == "server" {
117-
target := parseServerAddress(directive.Params, "upstream", ctx)
118-
if target.Host != "" {
119-
targets = append(targets, target)
120-
}
121-
}
122-
}
123-
124-
return targets
125-
}
126-
127-
// parseLocationProxyPass extracts proxy_pass from location content
128-
func parseLocationProxyPass(content string) []ProxyTarget {
129-
var targets []ProxyTarget
130-
131-
// Use regex to find proxy_pass directives
132-
proxyPassRegex := regexp.MustCompile(`(?m)^\s*proxy_pass\s+([^;]+);`)
133-
matches := proxyPassRegex.FindAllStringSubmatch(content, -1)
92+
// Parse grpc_pass directives, but skip upstream references
93+
grpcPassRegex := regexp.MustCompile(`(?m)^\s*grpc_pass\s+([^;]+);`)
94+
grpcMatches := grpcPassRegex.FindAllStringSubmatch(content, -1)
13495

135-
for _, match := range matches {
96+
for _, match := range grpcMatches {
13697
if len(match) >= 2 {
137-
target := parseProxyPassURL(strings.TrimSpace(match[1]))
138-
if target.Host != "" {
139-
targets = append(targets, target)
98+
grpcPassURL := strings.TrimSpace(match[1])
99+
// Skip if this grpc_pass references an upstream
100+
if !isUpstreamReference(grpcPassURL, upstreamNames) {
101+
target := parseProxyPassURL(grpcPassURL, "grpc_pass")
102+
if target.Host != "" {
103+
targets = append(targets, target)
104+
}
140105
}
141106
}
142107
}
143108

144-
return targets
109+
return deduplicateTargets(targets)
145110
}
146111

147-
// parseProxyPassURL parses a proxy_pass URL and extracts host and port
148-
func parseProxyPassURL(proxyPass string) ProxyTarget {
149-
proxyPass = strings.TrimSpace(proxyPass)
112+
// parseProxyPassURL parses a proxy_pass or grpc_pass URL and extracts host and port
113+
func parseProxyPassURL(passURL, passType string) ProxyTarget {
114+
passURL = strings.TrimSpace(passURL)
150115

151116
// Skip URLs that contain Nginx variables
152-
if strings.Contains(proxyPass, "$") {
117+
if strings.Contains(passURL, "$") {
153118
return ProxyTarget{}
154119
}
155120

156-
// Handle HTTP/HTTPS URLs (e.g., "http://backend")
157-
if strings.HasPrefix(proxyPass, "http://") || strings.HasPrefix(proxyPass, "https://") {
158-
if parsedURL, err := url.Parse(proxyPass); err == nil {
121+
// Handle HTTP/HTTPS/gRPC URLs (e.g., "http://backend", "grpc://backend")
122+
if strings.HasPrefix(passURL, "http://") || strings.HasPrefix(passURL, "https://") || strings.HasPrefix(passURL, "grpc://") || strings.HasPrefix(passURL, "grpcs://") {
123+
if parsedURL, err := url.Parse(passURL); err == nil {
159124
host := parsedURL.Hostname()
160125
port := parsedURL.Port()
161126

162127
// Set default ports if not specified
163128
if port == "" {
164-
if parsedURL.Scheme == "https" {
129+
switch parsedURL.Scheme {
130+
case "https":
131+
port = "443"
132+
case "grpcs":
165133
port = "443"
166-
} else {
134+
case "grpc":
135+
port = "80"
136+
default: // http
167137
port = "80"
168138
}
169139
}
@@ -176,15 +146,15 @@ func parseProxyPassURL(proxyPass string) ProxyTarget {
176146
return ProxyTarget{
177147
Host: host,
178148
Port: port,
179-
Type: "proxy_pass",
149+
Type: passType,
180150
}
181151
}
182152
}
183153

184154
// Handle direct address format for stream module (e.g., "127.0.0.1:8080", "backend.example.com:12345")
185-
// This is used in stream configurations where proxy_pass doesn't require a protocol
186-
if !strings.Contains(proxyPass, "://") {
187-
target := parseServerAddress(proxyPass, "proxy_pass", nil) // No upstream context for this function
155+
// This is used in stream configurations where proxy_pass/grpc_pass doesn't require a protocol
156+
if !strings.Contains(passURL, "://") {
157+
target := parseServerAddress(passURL, passType, nil) // No upstream context for this function
188158

189159
// Skip if this is the HTTP challenge port used by Let's Encrypt
190160
if target.Host == "127.0.0.1" && target.Port == settings.CertSettings.HTTPChallengePort {
@@ -262,7 +232,7 @@ func isConsulServiceDiscovery(serverAddr string) bool {
262232
if strings.Contains(serverAddr, "service=") && strings.Contains(serverAddr, "resolve") {
263233
return true
264234
}
265-
// Legacy consul format: "service.consul service=name resolve"
235+
// Legacy consul format: "service.consul service=name resolve"
266236
return strings.Contains(serverAddr, "service.consul") &&
267237
(strings.Contains(serverAddr, "service=") || strings.Contains(serverAddr, "resolve"))
268238
}
@@ -327,17 +297,17 @@ func deduplicateTargets(targets []ProxyTarget) []ProxyTarget {
327297
return result
328298
}
329299

330-
// isUpstreamReference checks if a proxy_pass URL references an upstream block
331-
func isUpstreamReference(proxyPass string, upstreamNames map[string]bool) bool {
332-
proxyPass = strings.TrimSpace(proxyPass)
300+
// isUpstreamReference checks if a proxy_pass or grpc_pass URL references an upstream block
301+
func isUpstreamReference(passURL string, upstreamNames map[string]bool) bool {
302+
passURL = strings.TrimSpace(passURL)
333303

334-
// For HTTP/HTTPS URLs, parse the URL to extract the hostname
335-
if strings.HasPrefix(proxyPass, "http://") || strings.HasPrefix(proxyPass, "https://") {
304+
// For HTTP/HTTPS/gRPC URLs, parse the URL to extract the hostname
305+
if strings.HasPrefix(passURL, "http://") || strings.HasPrefix(passURL, "https://") || strings.HasPrefix(passURL, "grpc://") || strings.HasPrefix(passURL, "grpcs://") {
336306
// Handle URLs with nginx variables (e.g., "https://myUpStr$request_uri")
337307
// Extract the scheme and hostname part before any nginx variables
338-
schemeAndHost := proxyPass
339-
if dollarIndex := strings.Index(proxyPass, "$"); dollarIndex != -1 {
340-
schemeAndHost = proxyPass[:dollarIndex]
308+
schemeAndHost := passURL
309+
if dollarIndex := strings.Index(passURL, "$"); dollarIndex != -1 {
310+
schemeAndHost = passURL[:dollarIndex]
341311
}
342312

343313
// Try to parse the URL, if it fails, try manual extraction
@@ -348,11 +318,15 @@ func isUpstreamReference(proxyPass string, upstreamNames map[string]bool) bool {
348318
} else {
349319
// Fallback: manually extract hostname for URLs with variables
350320
// Remove scheme prefix
351-
withoutScheme := proxyPass
352-
if strings.HasPrefix(proxyPass, "https://") {
353-
withoutScheme = strings.TrimPrefix(proxyPass, "https://")
354-
} else if strings.HasPrefix(proxyPass, "http://") {
355-
withoutScheme = strings.TrimPrefix(proxyPass, "http://")
321+
withoutScheme := passURL
322+
if strings.HasPrefix(passURL, "https://") {
323+
withoutScheme = strings.TrimPrefix(passURL, "https://")
324+
} else if strings.HasPrefix(passURL, "http://") {
325+
withoutScheme = strings.TrimPrefix(passURL, "http://")
326+
} else if strings.HasPrefix(passURL, "grpc://") {
327+
withoutScheme = strings.TrimPrefix(passURL, "grpc://")
328+
} else if strings.HasPrefix(passURL, "grpcs://") {
329+
withoutScheme = strings.TrimPrefix(passURL, "grpcs://")
356330
}
357331

358332
// Extract hostname before any path, port, or variable
@@ -371,10 +345,10 @@ func isUpstreamReference(proxyPass string, upstreamNames map[string]bool) bool {
371345
}
372346
}
373347

374-
// For stream module, proxy_pass can directly reference upstream name without protocol
375-
// Check if the proxy_pass value directly matches an upstream name
376-
if !strings.Contains(proxyPass, "://") && !strings.Contains(proxyPass, ":") {
377-
return upstreamNames[proxyPass]
348+
// For stream module, proxy_pass/grpc_pass can directly reference upstream name without protocol
349+
// Check if the pass value directly matches an upstream name
350+
if !strings.Contains(passURL, "://") && !strings.Contains(passURL, ":") {
351+
return upstreamNames[passURL]
378352
}
379353

380354
return false

0 commit comments

Comments
 (0)