mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-05-06 22:12:23 +08:00
enhance: upstream parser for variables #1402
This commit is contained in:
@@ -68,6 +68,15 @@ export const useSiteEditorStore = defineStore('siteEditor', () => {
|
|||||||
await buildConfig()
|
await buildConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.value.sync_node_ids === null) {
|
||||||
|
data.value.sync_node_ids = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error allow comparing with empty string for legacy data
|
||||||
|
if (data.value.namespace_id === '') {
|
||||||
|
data.value.namespace_id = 0
|
||||||
|
}
|
||||||
|
|
||||||
const response = await site.updateItem(encodeURIComponent(name.value), {
|
const response = await site.updateItem(encodeURIComponent(name.value), {
|
||||||
content: configText.value,
|
content: configText.value,
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ export const useStreamEditorStore = defineStore('streamEditor', () => {
|
|||||||
await buildConfig()
|
await buildConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.value.sync_node_ids === null) {
|
||||||
|
data.value.sync_node_ids = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error allow comparing with empty string for legacy data
|
||||||
|
if (data.value.namespace_id === '') {
|
||||||
|
data.value.namespace_id = 0
|
||||||
|
}
|
||||||
|
|
||||||
const response = await stream.updateItem(encodeURIComponent(name.value), {
|
const response = await stream.updateItem(encodeURIComponent(name.value), {
|
||||||
content: configText.value,
|
content: configText.value,
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
|
|||||||
upstreams := make(map[string][]ProxyTarget)
|
upstreams := make(map[string][]ProxyTarget)
|
||||||
|
|
||||||
// First, collect all upstream names and their contexts
|
// First, collect all upstream names and their contexts
|
||||||
|
// Also collect literal variable assignments from `set $var value;`
|
||||||
upstreamNames := make(map[string]bool)
|
upstreamNames := make(map[string]bool)
|
||||||
upstreamContexts := make(map[string]*TheUpstreamContext)
|
upstreamContexts := make(map[string]*TheUpstreamContext)
|
||||||
upstreamRegex := regexp.MustCompile(`(?s)upstream\s+([^\s]+)\s*\{([^}]+)\}`)
|
upstreamRegex := regexp.MustCompile(`(?s)upstream\s+([^\s]+)\s*\{([^}]+)\}`)
|
||||||
@@ -92,13 +93,24 @@ func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Parse proxy_pass directives, but skip upstream references
|
||||||
proxyPassRegex := regexp.MustCompile(`(?m)^\s*proxy_pass\s+([^;]+);`)
|
proxyPassRegex := regexp.MustCompile(`(?m)^\s*proxy_pass\s+([^;]+);`)
|
||||||
proxyMatches := proxyPassRegex.FindAllStringSubmatch(content, -1)
|
proxyMatches := proxyPassRegex.FindAllStringSubmatch(content, -1)
|
||||||
|
|
||||||
for _, match := range proxyMatches {
|
for _, match := range proxyMatches {
|
||||||
if len(match) >= 2 {
|
if len(match) >= 2 {
|
||||||
proxyPassURL := strings.TrimSpace(match[1])
|
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
|
// Skip if this proxy_pass references an upstream
|
||||||
if !isUpstreamReference(proxyPassURL, upstreamNames) {
|
if !isUpstreamReference(proxyPassURL, upstreamNames) {
|
||||||
target := parseProxyPassURL(proxyPassURL, "proxy_pass")
|
target := parseProxyPassURL(proxyPassURL, "proxy_pass")
|
||||||
@@ -115,7 +127,14 @@ func ParseProxyTargetsAndUpstreamsFromRawContent(content string) *ParseResult {
|
|||||||
|
|
||||||
for _, match := range grpcMatches {
|
for _, match := range grpcMatches {
|
||||||
if len(match) >= 2 {
|
if len(match) >= 2 {
|
||||||
grpcPassURL := strings.TrimSpace(match[1])
|
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
|
// Skip if this grpc_pass references an upstream
|
||||||
if !isUpstreamReference(grpcPassURL, upstreamNames) {
|
if !isUpstreamReference(grpcPassURL, upstreamNames) {
|
||||||
target := parseProxyPassURL(grpcPassURL, "grpc_pass")
|
target := parseProxyPassURL(grpcPassURL, "grpc_pass")
|
||||||
@@ -391,3 +410,63 @@ func isUpstreamReference(passURL string, upstreamNames map[string]bool) bool {
|
|||||||
|
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -753,3 +753,155 @@ func TestGrpcPassPortDefaults(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New tests covering `set $var ...;` with proxy_pass/grpc_pass
|
||||||
|
func TestSetVariableProxyPass_HTTP(t *testing.T) {
|
||||||
|
config := `
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
set $target http://example.com;
|
||||||
|
location / {
|
||||||
|
proxy_pass $target;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
targets := ParseProxyTargetsFromRawContent(config)
|
||||||
|
|
||||||
|
expected := ProxyTarget{Host: "example.com", Port: "80", Type: "proxy_pass"}
|
||||||
|
if len(targets) != 1 {
|
||||||
|
t.Fatalf("Expected 1 target, got %d", len(targets))
|
||||||
|
}
|
||||||
|
got := targets[0]
|
||||||
|
if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
|
||||||
|
t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetVariableProxyPass_HTTPS(t *testing.T) {
|
||||||
|
config := `
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
set $target https://example.com;
|
||||||
|
location / {
|
||||||
|
proxy_pass $target;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
targets := ParseProxyTargetsFromRawContent(config)
|
||||||
|
|
||||||
|
expected := ProxyTarget{Host: "example.com", Port: "443", Type: "proxy_pass"}
|
||||||
|
if len(targets) != 1 {
|
||||||
|
t.Fatalf("Expected 1 target, got %d", len(targets))
|
||||||
|
}
|
||||||
|
got := targets[0]
|
||||||
|
if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
|
||||||
|
t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetVariableProxyPass_QuotedValue(t *testing.T) {
|
||||||
|
config := `
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
set $target "http://example.com:9090";
|
||||||
|
location / {
|
||||||
|
proxy_pass $target;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
targets := ParseProxyTargetsFromRawContent(config)
|
||||||
|
|
||||||
|
expected := ProxyTarget{Host: "example.com", Port: "9090", Type: "proxy_pass"}
|
||||||
|
if len(targets) != 1 {
|
||||||
|
t.Fatalf("Expected 1 target, got %d", len(targets))
|
||||||
|
}
|
||||||
|
got := targets[0]
|
||||||
|
if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
|
||||||
|
t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetVariableProxyPass_UnresolvableIgnored(t *testing.T) {
|
||||||
|
config := `
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
set $target http://example.com$request_uri;
|
||||||
|
location / {
|
||||||
|
proxy_pass $target;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
targets := ParseProxyTargetsFromRawContent(config)
|
||||||
|
|
||||||
|
// Because the variable value contains nginx variables, it should be ignored
|
||||||
|
if len(targets) != 0 {
|
||||||
|
t.Errorf("Expected 0 targets, got %d", len(targets))
|
||||||
|
for i, target := range targets {
|
||||||
|
t.Logf("Target %d: %+v", i, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetVariableProxyPass_UpstreamReferenceIgnored(t *testing.T) {
|
||||||
|
config := `
|
||||||
|
upstream api-1 {
|
||||||
|
server 127.0.0.1:9000;
|
||||||
|
keepalive 16;
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
set $target http://api-1/;
|
||||||
|
location / {
|
||||||
|
proxy_pass $target;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
targets := ParseProxyTargetsFromRawContent(config)
|
||||||
|
|
||||||
|
// Expect only upstream servers, and proxy_pass via $target should be ignored
|
||||||
|
expectedTargets := []ProxyTarget{
|
||||||
|
{Host: "127.0.0.1", Port: "9000", Type: "upstream"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(targets) != len(expectedTargets) {
|
||||||
|
t.Errorf("Expected %d targets, got %d", len(expectedTargets), len(targets))
|
||||||
|
for i, target := range targets {
|
||||||
|
t.Logf("Target %d: %+v", i, target)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetMap := make(map[string]ProxyTarget)
|
||||||
|
for _, target := range targets {
|
||||||
|
key := formatSocketAddress(target.Host, target.Port) + ":" + target.Type
|
||||||
|
targetMap[key] = target
|
||||||
|
}
|
||||||
|
for _, expected := range expectedTargets {
|
||||||
|
key := formatSocketAddress(expected.Host, expected.Port) + ":" + expected.Type
|
||||||
|
if _, found := targetMap[key]; !found {
|
||||||
|
t.Errorf("Expected target not found: %+v", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetVariableGrpcPass(t *testing.T) {
|
||||||
|
config := `
|
||||||
|
server {
|
||||||
|
listen 80 http2;
|
||||||
|
set $g grpc://127.0.0.1:9090;
|
||||||
|
location /svc/ {
|
||||||
|
grpc_pass $g;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
targets := ParseProxyTargetsFromRawContent(config)
|
||||||
|
|
||||||
|
expected := ProxyTarget{Host: "127.0.0.1", Port: "9090", Type: "grpc_pass"}
|
||||||
|
if len(targets) != 1 {
|
||||||
|
t.Fatalf("Expected 1 target, got %d", len(targets))
|
||||||
|
}
|
||||||
|
got := targets[0]
|
||||||
|
if got.Host != expected.Host || got.Port != expected.Port || got.Type != expected.Type {
|
||||||
|
t.Errorf("Unexpected target: got=%+v expected=%+v", got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user