Skip to content

Commit 2c7ebea

Browse files
committed
enhance: upstream parser #1127
1 parent a042b66 commit 2c7ebea

File tree

2 files changed

+193
-2
lines changed

2 files changed

+193
-2
lines changed

internal/upstream/proxy_parser.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,13 @@ func parseProxyPassURL(proxyPass string) ProxyTarget {
154154

155155
// Handle HTTP/HTTPS URLs (e.g., "http://backend")
156156
if strings.HasPrefix(proxyPass, "http://") || strings.HasPrefix(proxyPass, "https://") {
157-
if parsedURL, err := url.Parse(proxyPass); err == nil {
157+
// Handle URLs with nginx variables by extracting the base URL before variables
158+
baseURL := proxyPass
159+
if dollarIndex := strings.Index(proxyPass, "$"); dollarIndex != -1 {
160+
baseURL = proxyPass[:dollarIndex]
161+
}
162+
163+
if parsedURL, err := url.Parse(baseURL); err == nil {
158164
host := parsedURL.Hostname()
159165
port := parsedURL.Port()
160166

@@ -282,9 +288,40 @@ func isUpstreamReference(proxyPass string, upstreamNames map[string]bool) bool {
282288

283289
// For HTTP/HTTPS URLs, parse the URL to extract the hostname
284290
if strings.HasPrefix(proxyPass, "http://") || strings.HasPrefix(proxyPass, "https://") {
285-
if parsedURL, err := url.Parse(proxyPass); err == nil {
291+
// Handle URLs with nginx variables (e.g., "https://myUpStr$request_uri")
292+
// Extract the scheme and hostname part before any nginx variables
293+
schemeAndHost := proxyPass
294+
if dollarIndex := strings.Index(proxyPass, "$"); dollarIndex != -1 {
295+
schemeAndHost = proxyPass[:dollarIndex]
296+
}
297+
298+
// Try to parse the URL, if it fails, try manual extraction
299+
if parsedURL, err := url.Parse(schemeAndHost); err == nil {
286300
hostname := parsedURL.Hostname()
287301
// Check if the hostname matches any upstream name
302+
return upstreamNames[hostname]
303+
} else {
304+
// Fallback: manually extract hostname for URLs with variables
305+
// Remove scheme prefix
306+
withoutScheme := proxyPass
307+
if strings.HasPrefix(proxyPass, "https://") {
308+
withoutScheme = strings.TrimPrefix(proxyPass, "https://")
309+
} else if strings.HasPrefix(proxyPass, "http://") {
310+
withoutScheme = strings.TrimPrefix(proxyPass, "http://")
311+
}
312+
313+
// Extract hostname before any path, port, or variable
314+
hostname := withoutScheme
315+
if slashIndex := strings.Index(hostname, "/"); slashIndex != -1 {
316+
hostname = hostname[:slashIndex]
317+
}
318+
if colonIndex := strings.Index(hostname, ":"); colonIndex != -1 {
319+
hostname = hostname[:colonIndex]
320+
}
321+
if dollarIndex := strings.Index(hostname, "$"); dollarIndex != -1 {
322+
hostname = hostname[:dollarIndex]
323+
}
324+
288325
return upstreamNames[hostname]
289326
}
290327
}

internal/upstream/proxy_parser_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func TestIsUpstreamReference(t *testing.T) {
125125
"api-1": true,
126126
"api-2": true,
127127
"backend": true,
128+
"myUpStr": true,
128129
}
129130

130131
tests := []struct {
@@ -138,6 +139,15 @@ func TestIsUpstreamReference(t *testing.T) {
138139
{"http://127.0.0.1:8080", false},
139140
{"https://example.com", false},
140141
{"http://unknown-upstream", false},
142+
// Test cases for nginx variables
143+
{"https://myUpStr$request_uri", true},
144+
{"http://api-1$request_uri", true},
145+
{"https://backend$server_name", true},
146+
{"http://unknown-upstream$request_uri", false},
147+
{"https://example.com$request_uri", false},
148+
// Test cases for URLs with variables and paths
149+
{"https://myUpStr/api$request_uri", true},
150+
{"http://api-1:8080$request_uri", true},
141151
}
142152

143153
for _, test := range tests {
@@ -379,3 +389,147 @@ server {
379389
}
380390
}
381391
}
392+
393+
func TestParseProxyTargetsWithNginxVariables(t *testing.T) {
394+
config := `map $http_upgrade $connection_upgrade {
395+
default upgrade;
396+
'' close;
397+
}
398+
upstream myUpStr {
399+
keepalive 32;
400+
keepalive_timeout 600s;
401+
server 192.168.1.100:8080;
402+
}
403+
server {
404+
listen 80;
405+
listen [::]:80;
406+
server_name my.domain.tld;
407+
return 307 https://$server_name$request_uri;
408+
}
409+
server {
410+
listen 443 ssl http2;
411+
listen [::]:443 ssl http2;
412+
server_name my.domain.tld;
413+
ssl_certificate /path/to/cert;
414+
ssl_certificate_key /path/to/key;
415+
location / {
416+
proxy_http_version 1.1;
417+
proxy_set_header Upgrade $http_upgrade;
418+
proxy_set_header Connection $connection_upgrade;
419+
client_max_body_size 1000m;
420+
proxy_redirect off;
421+
add_header X-Served-By $host;
422+
proxy_set_header Host $host;
423+
proxy_set_header X-Forwarded-Scheme $scheme;
424+
proxy_set_header X-Forwarded-Proto $scheme;
425+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
426+
proxy_set_header X-Real-IP $remote_addr;
427+
proxy_set_header X-Forwarded-Host $host:$server_port;
428+
proxy_set_header X-Forwarded-Server $host;
429+
proxy_pass https://myUpStr$request_uri;
430+
}
431+
}`
432+
433+
targets := ParseProxyTargetsFromRawContent(config)
434+
435+
// Expected targets:
436+
// - 1 upstream server from myUpStr
437+
// - proxy_pass https://myUpStr$request_uri should be ignored since it references an upstream
438+
expectedTargets := []ProxyTarget{
439+
{Host: "192.168.1.100", Port: "8080", Type: "upstream"},
440+
}
441+
442+
if len(targets) != len(expectedTargets) {
443+
t.Errorf("Expected %d targets, got %d", len(expectedTargets), len(targets))
444+
for i, target := range targets {
445+
t.Logf("Target %d: %+v", i, target)
446+
}
447+
return
448+
}
449+
450+
// Create a map for easier comparison
451+
targetMap := make(map[string]ProxyTarget)
452+
for _, target := range targets {
453+
key := target.Host + ":" + target.Port + ":" + target.Type
454+
targetMap[key] = target
455+
}
456+
457+
for _, expected := range expectedTargets {
458+
key := expected.Host + ":" + expected.Port + ":" + expected.Type
459+
if _, found := targetMap[key]; !found {
460+
t.Errorf("Expected target not found: %+v", expected)
461+
}
462+
}
463+
}
464+
465+
func TestParseProxyTargetsWithComplexNginxVariables(t *testing.T) {
466+
config := `upstream backend_api {
467+
server api1.example.com:8080;
468+
server api2.example.com:8080;
469+
}
470+
471+
upstream backend_ws {
472+
server ws1.example.com:9000;
473+
server ws2.example.com:9000;
474+
}
475+
476+
server {
477+
listen 80;
478+
server_name example.com;
479+
480+
location /api/ {
481+
proxy_pass http://backend_api$request_uri;
482+
}
483+
484+
location /ws/ {
485+
proxy_pass http://backend_ws/websocket$request_uri;
486+
}
487+
488+
location /external/ {
489+
proxy_pass https://external.example.com:8443$request_uri;
490+
}
491+
492+
location /static/ {
493+
proxy_pass http://static.example.com$uri;
494+
}
495+
}`
496+
497+
targets := ParseProxyTargetsFromRawContent(config)
498+
499+
// Expected targets:
500+
// - 2 upstream servers from backend_api
501+
// - 2 upstream servers from backend_ws
502+
// - 1 direct proxy_pass (external.example.com:8443)
503+
// - 1 direct proxy_pass (static.example.com:80)
504+
// - proxy_pass with upstream references should be ignored
505+
expectedTargets := []ProxyTarget{
506+
{Host: "api1.example.com", Port: "8080", Type: "upstream"},
507+
{Host: "api2.example.com", Port: "8080", Type: "upstream"},
508+
{Host: "ws1.example.com", Port: "9000", Type: "upstream"},
509+
{Host: "ws2.example.com", Port: "9000", Type: "upstream"},
510+
{Host: "external.example.com", Port: "8443", Type: "proxy_pass"},
511+
{Host: "static.example.com", Port: "80", Type: "proxy_pass"},
512+
}
513+
514+
if len(targets) != len(expectedTargets) {
515+
t.Errorf("Expected %d targets, got %d", len(expectedTargets), len(targets))
516+
for i, target := range targets {
517+
t.Logf("Target %d: %+v", i, target)
518+
}
519+
return
520+
}
521+
522+
// Create a map for easier comparison
523+
targetMap := make(map[string]ProxyTarget)
524+
for _, target := range targets {
525+
key := target.Host + ":" + target.Port + ":" + target.Type
526+
targetMap[key] = target
527+
}
528+
529+
for _, expected := range expectedTargets {
530+
key := expected.Host + ":" + expected.Port + ":" + expected.Type
531+
if _, found := targetMap[key]; !found {
532+
t.Errorf("Expected target not found: %+v", expected)
533+
}
534+
}
535+
}

0 commit comments

Comments
 (0)