Ctrl K
ComunidadGuías…

internal/server/proxy_advanced_overrides.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: Apache-2.0

package server

import (
	"fmt"
	"regexp"
	"strings"
)

// v2.40.0: the proxy host's Advanced config box wraps its directives in a
// site block and adapts them through Caddy; the resulting handlers run
// before the host's own reverse_proxy. Anything that belongs *inside*
// reverse_proxy — flush_interval, header_up, transport, lb_policy, health
// checks — therefore had no home: typed bare it came back from Caddy as
// "Caddyfile:2: unrecognized directive: flush_interval", and a reverse_proxy
// block was rejected as a second terminal handler. Now a reverse_proxy block
// with no upstream is accepted and its sub-directives are merged into the
// host's generated handler (see caddy.MergeReverseProxyOverrides), and a
// bare sub-directive gets an error that says where it goes.

// reverseProxySubdirectives is every sub-directive of Caddy's reverse_proxy
// directive. Seen at the top level of an Advanced config they are a sure
// sign the user meant the reverse_proxy block.
var reverseProxySubdirectives = []string{
	"to", "dynamic",
	"lb_policy", "lb_retries", "lb_try_duration", "lb_try_interval", "lb_retry_match",
	"health_uri", "health_upstream", "health_port", "health_interval", "health_timeout",
	"health_status", "health_body", "health_method", "health_request_body",
	"health_follow_redirects", "health_headers", "health_passes", "health_fails",
	"fail_duration", "max_fails", "unhealthy_status", "unhealthy_latency", "unhealthy_request_count",
	"flush_interval", "request_buffers", "response_buffers", "stream_timeout", "stream_close_delay",
	"buffer_requests", "buffer_responses", "max_buffer_size", "verbose_logs",
	"trusted_proxies", "header_up", "header_down", "method", "rewrite",
	"transport", "replace_status", "handle_response", "copy_response", "copy_response_headers",
}

// reverseProxySubdirectiveError explains a bare reverse_proxy sub-directive
// at the top level of an Advanced config, or returns "" when there is none.
func reverseProxySubdirectiveError(src string) string {
	bad := scanTopLevelDirective(src, reverseProxySubdirectives)
	if bad == "" {
		return ""
	}
	hint := ""
	switch bad {
	case "flush_interval":
		hint = " This one also has a dedicated option: Streaming → Flush immediately / Flush interval."
	case "header_up", "header_down":
		hint = " Upstream request/response headers also have dedicated options under Headers."
	case "transport":
		hint = " Timeouts, TLS and HTTP version to the upstream also have dedicated options under Upstream."
	case "lb_policy", "lb_retries", "lb_try_duration", "lb_try_interval":
		hint = " Load balancing also has dedicated options under Upstreams."
	}
	return fmt.Sprintf("`%s` is a reverse_proxy sub-directive, not a site directive, so Caddy rejects it here. Wrap it in a reverse_proxy block and CaddyUI merges it into this host's own reverse_proxy:\n\nreverse_proxy {\n    %s …\n}\n\nLeave the upstream address out — it comes from Forward host / port.%s", bad, bad, hint)
}

// extractReverseProxyOverrides removes the reverse_proxy handler that a
// `reverse_proxy { … }` block in the Advanced config adapted to — walking
// into the subroute Caddy wraps site directives in — and returns the
// remaining handlers (which still run before the proxy) plus that handler's
// fields, ready for caddy.MergeReverseProxyOverrides. It is an error for the
// block to name an upstream or to sit behind a matcher: neither can be
// folded into the host's single generated handler.
func extractReverseProxyOverrides(handlers []any) ([]any, map[string]any, error) {
	var overrides map[string]any
	var walk func(hs []any, matched bool) ([]any, error)
	walk = func(hs []any, matched bool) ([]any, error) {
		out := make([]any, 0, len(hs))
		for _, h := range hs {
			hm, ok := h.(map[string]any)
			if !ok {
				out = append(out, h)
				continue
			}
			switch hm["handler"] {
			case "reverse_proxy":
				if matched {
					return nil, fmt.Errorf("a reverse_proxy block behind a matcher can't be merged into this host's reverse_proxy — create an Advanced route for a matched proxy")
				}
				if overrides != nil {
					return nil, fmt.Errorf("Advanced config may contain only one reverse_proxy block")
				}
				if ups, _ := hm["upstreams"].([]any); len(ups) > 0 {
					return nil, fmt.Errorf("the reverse_proxy block in Advanced config must not name an upstream — the address comes from Forward host / port; keep only sub-directives such as flush_interval, header_up or transport")
				}
				overrides = map[string]any{}
				for k, v := range hm {
					if k != "handler" && k != "upstreams" {
						overrides[k] = v
					}
				}
			case "subroute":
				routes, _ := hm["routes"].([]any)
				kept := make([]any, 0, len(routes))
				for _, r := range routes {
					rm, ok := r.(map[string]any)
					if !ok {
						kept = append(kept, r)
						continue
					}
					_, hasMatch := rm["match"]
					inner, _ := rm["handle"].([]any)
					newInner, err := walk(inner, matched || hasMatch)
					if err != nil {
						return nil, err
					}
					if len(inner) > 0 && len(newInner) == 0 {
						continue // the block was this route's only handler
					}
					copyRoute := make(map[string]any, len(rm))
					for k, v := range rm {
						copyRoute[k] = v
					}
					copyRoute["handle"] = newInner
					kept = append(kept, copyRoute)
				}
				if len(routes) > 0 && len(kept) == 0 {
					continue
				}
				copyHandler := make(map[string]any, len(hm))
				for k, v := range hm {
					copyHandler[k] = v
				}
				copyHandler["routes"] = kept
				out = append(out, copyHandler)
			default:
				out = append(out, hm)
			}
		}
		return out, nil
	}
	rest, err := walk(handlers, false)
	if err != nil {
		return nil, nil, err
	}
	return rest, overrides, nil
}

// describeOverrides lists the merged sub-directive keys for log lines.
func describeOverrides(overrides map[string]any) string {
	if len(overrides) == 0 {
		return ""
	}
	keys := make([]string, 0, len(overrides))
	for k := range overrides {
		keys = append(keys, k)
	}
	return strings.Join(keys, ", ")
}

// wrapBareReverseProxySubdirectives (v2.42.1) moves reverse_proxy
// sub-directives typed at the top level of an Advanced config into a
// `reverse_proxy { … }` block — merged into an existing one when there is
// one — so `flush_interval -1` on its own simply works instead of being
// explained back to the user. Multi-line sub-directives (a transport block)
// move with their braces. Sources that need no change come back unchanged.
func wrapBareReverseProxySubdirectives(src string) string {
	out, _ := wrapBareReverseProxySubdirectivesNamed(src)
	return out
}

func wrapBareReverseProxySubdirectivesNamed(src string) (string, []string) {
	isSub := map[string]bool{}
	for _, d := range reverseProxySubdirectives {
		isSub[d] = true
	}
	stripped := func(line string) string {
		t := strings.TrimSpace(line)
		if i := strings.Index(t, "#"); i >= 0 {
			t = strings.TrimSpace(t[:i])
		}
		return t
	}
	braces := func(line string) int {
		d := 0
		for _, ch := range stripped(line) {
			switch ch {
			case '{':
				d++
			case '}':
				d--
			}
		}
		return d
	}
	lines := strings.Split(src, "\n")
	var top, moved, names []string
	depth := 0
	for i := 0; i < len(lines); {
		line := lines[i]
		t := stripped(line)
		first := ""
		if t != "" {
			first = strings.Fields(t)[0]
		}
		if depth == 0 && first != "" && isSub[first] {
			names = append(names, first)
			moved = append(moved, "\t"+strings.TrimSpace(line))
			d := braces(line)
			i++
			for d > 0 && i < len(lines) {
				moved = append(moved, "\t"+strings.TrimSpace(lines[i]))
				d += braces(lines[i])
				i++
			}
			continue
		}
		top = append(top, line)
		depth += braces(line)
		i++
	}
	if len(moved) == 0 {
		return src, nil
	}
	// Merge into an existing top-level reverse_proxy block when there is one.
	depth = 0
	for i, line := range top {
		t := stripped(line)
		if depth == 0 && t != "" && strings.Fields(t)[0] == "reverse_proxy" && strings.HasSuffix(t, "{") {
			d := braces(line)
			for j := i + 1; j < len(top); j++ {
				d += braces(top[j])
				if d == 0 {
					merged := append([]string{}, top[:j]...)
					merged = append(merged, moved...)
					merged = append(merged, top[j:]...)
					return strings.TrimRight(strings.Join(merged, "\n"), "\n") + "\n", names
				}
			}
		}
		depth += braces(line)
	}
	body := strings.TrimRight(strings.Join(top, "\n"), "\n")
	block := "reverse_proxy {\n" + strings.Join(moved, "\n") + "\n}\n"
	if strings.TrimSpace(body) == "" {
		return block, names
	}
	return body + "\n" + block, names
}

// v2.42.2: users copy option names from Caddy's JSON (read_buffer_size) or
// put transport options straight under reverse_proxy; Caddy's adapter
// rejects both with "unrecognized subdirective". Both are unambiguous, so
// they are repaired rather than reported: JSON-style names become their
// Caddyfile spelling, and transport-only options found directly under
// reverse_proxy move into its `transport http { … }` block.

// transportAliases maps JSON field names people type into the Caddyfile
// spelling of the same `transport http` option.
var transportAliases = map[string]string{
	"read_buffer_size":         "read_buffer",
	"write_buffer_size":        "write_buffer",
	"max_response_header_size": "max_response_header",
}

// transportSubdirectives are options that only exist inside `transport http`.
var transportSubdirectives = []string{
	"read_buffer", "write_buffer", "max_response_header", "proxy_protocol",
	"dial_timeout", "dial_fallback_delay", "response_header_timeout", "expect_continue_timeout",
	"resolvers", "tls", "tls_client_auth", "tls_insecure_skip_verify", "tls_timeout", "tls_trusted_ca_certs",
	"tls_trust_pool", "tls_server_name", "tls_renegotiation", "tls_except_ports", "tls_curves",
	"keepalive", "keepalive_interval", "keepalive_idle_conns", "keepalive_idle_conns_per_host",
	"versions", "compression", "max_conns_per_host", "forward_proxy_url", "network_proxy", "local_address",
}

// normalizeProxyAdvancedConfig is every repair applied to an Advanced config
// before it is adapted or saved: bare reverse_proxy sub-directives wrapped
// (v2.42.1), JSON-style transport names respelled, and transport options
// under reverse_proxy moved into its transport block.
func normalizeProxyAdvancedConfig(src string) string {
	return relocateTransportSubdirectives(respellTransportAliases(wrapBareReverseProxySubdirectives(src)))
}

func advancedFirstToken(line string) string {
	t := strings.TrimSpace(line)
	if i := strings.Index(t, "#"); i >= 0 {
		t = strings.TrimSpace(t[:i])
	}
	if t == "" {
		return ""
	}
	return strings.Fields(t)[0]
}

func advancedBraceDelta(line string) int {
	t := strings.TrimSpace(line)
	if i := strings.Index(t, "#"); i >= 0 {
		t = t[:i]
	}
	d := 0
	for _, ch := range t {
		switch ch {
		case '{':
			d++
		case '}':
			d--
		}
	}
	return d
}

// respellTransportAliases rewrites the first token of any line that uses a
// JSON-style transport option name.
func respellTransportAliases(src string) string {
	lines := strings.Split(src, "\n")
	changed := false
	for i, line := range lines {
		first := advancedFirstToken(line)
		alias, ok := transportAliases[first]
		if !ok {
			continue
		}
		indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
		lines[i] = indent + alias + strings.TrimPrefix(strings.TrimLeft(line, " \t"), first)
		changed = true
	}
	if !changed {
		return src
	}
	return strings.Join(lines, "\n")
}

// relocateTransportSubdirectives moves transport-only options that sit
// directly inside a top-level reverse_proxy block into that block's
// `transport http { … }` (merged into an existing one). Anything else is
// left exactly as typed.
func relocateTransportSubdirectives(src string) string {
	isTransport := map[string]bool{}
	for _, d := range transportSubdirectives {
		isTransport[d] = true
	}
	lines := strings.Split(src, "\n")
	// Find the top-level reverse_proxy block.
	depth, start, end := 0, -1, -1
	for i, line := range lines {
		first := advancedFirstToken(line)
		if depth == 0 && first == "reverse_proxy" && strings.HasSuffix(strings.TrimSpace(strings.SplitN(line, "#", 2)[0]), "{") {
			start = i
			d := advancedBraceDelta(line)
			for j := i + 1; j < len(lines); j++ {
				d += advancedBraceDelta(lines[j])
				if d == 0 {
					end = j
					break
				}
			}
			break
		}
		depth += advancedBraceDelta(line)
	}
	if start < 0 || end < 0 {
		return src
	}
	// Walk the block body at relative depth 1: pull out transport options,
	// remember an existing transport block.
	var body, moved []string
	transportOpen, transportClose := -1, -1 // indexes into body
	rel := 0
	for i := start + 1; i < end; {
		line := lines[i]
		first := advancedFirstToken(line)
		if rel == 0 && first == "transport" && transportOpen < 0 {
			transportOpen = len(body)
			body = append(body, line)
			d := advancedBraceDelta(line)
			i++
			for d > 0 && i < end {
				body = append(body, lines[i])
				d += advancedBraceDelta(lines[i])
				i++
			}
			transportClose = len(body) - 1
			continue
		}
		if rel == 0 && isTransport[first] {
			moved = append(moved, "\t\t"+strings.TrimSpace(line))
			d := advancedBraceDelta(line)
			i++
			for d > 0 && i < end {
				moved = append(moved, "\t\t"+strings.TrimSpace(lines[i]))
				d += advancedBraceDelta(lines[i])
				i++
			}
			continue
		}
		body = append(body, line)
		rel += advancedBraceDelta(line)
		i++
	}
	if len(moved) == 0 {
		return src
	}
	var newBody []string
	if transportOpen >= 0 && transportClose > transportOpen {
		newBody = append(newBody, body[:transportClose]...)
		newBody = append(newBody, moved...)
		newBody = append(newBody, body[transportClose:]...)
	} else {
		newBody = append(newBody, body...)
		newBody = append(newBody, "\ttransport http {")
		newBody = append(newBody, moved...)
		newBody = append(newBody, "\t}")
	}
	out := append([]string{}, lines[:start+1]...)
	out = append(out, newBody...)
	out = append(out, lines[end:]...)
	return strings.Join(out, "\n")
}

var unrecognizedSubdirective = regexp.MustCompile(`unrecognized subdirective ([A-Za-z0-9_]+)`)

// friendlyAdvancedRejection turns Caddy's adapter error into advice when it
// is about an unknown sub-directive — the one people hit when they mix JSON
// names, Caddyfile names and transport options.
func friendlyAdvancedRejection(err error) string {
	msg := err.Error()
	m := unrecognizedSubdirective.FindStringSubmatch(msg)
	if m == nil {
		return "Advanced config rejected by Caddy: " + msg
	}
	name := m[1]
	hint := fmt.Sprintf("`%s` is not an option Caddy knows there.", name)
	if alias, ok := transportAliases[name]; ok {
		hint += fmt.Sprintf(" In a Caddyfile it is spelled `%s`, inside `transport http { … }`.", alias)
	}
	hint += " Caddyfile names differ from the JSON ones (read_buffer_size → read_buffer, write_buffer_size → write_buffer, max_response_header_size → max_response_header); transport options belong inside `transport http { … }` within the reverse_proxy block; and most have a dedicated option in this form — Upstream → timeouts and buffers, Streaming → Flush immediately — which is the simplest place for them."
	return "Advanced config rejected by Caddy: " + hint + " Caddy said: " + msg
}