Ctrl K
ComunidadGuías…

internal/server/proxy_host_preview_test.go

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

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

package server

import (
	"encoding/base64"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"github.com/X4Applegate/caddyui/internal/caddy"
)

func TestAPIProxyHostPreviewReturnsCaddyfileAndRouteJSON(t *testing.T) {
	form := url.Values{
		"domains":                   {"preview.example.com"},
		"forward_scheme":            {"https"},
		"forward_host":              {"app.internal"},
		"forward_port":              {"8443"},
		"ssl_enabled":               {"on"},
		"enabled":                   {"on"},
		"extra_upstream":            {"app-backup.internal:8443"},
		"basicauth_enabled":         {"on"},
		"basicauth_realm":           {"Members"},
		"basicauth_user":            {"alice"},
		"basicauth_hash":            {"saved-secret-bcrypt-hash"},
		"api_key_header":            {"X-Preview-Key"},
		"api_key_value":             {"live-api-secret"},
		"sticky_sessions":           {"on"},
		"lb_cookie_secret":          {"live-cookie-secret"},
		"http_basic_auth_upstream":  {"upstream-user:upstream-pass"},
		"health_check_uri":          {"/health"},
		"health_check_query_params": {"token=health-query-secret;mode=full"},
		"health_check_basic_auth":   {"probe-user:probe-pass"},
		"forward_proxy_url":         {"http://proxy-user:proxy-pass@proxy.internal:3128?api_key=proxy-query-secret"},
		"forward_auth_url":          {"https://auth.internal/check?credential=auth-query-secret&mode=strict"},
	}
	req := httptest.NewRequest(http.MethodPost, "/api/proxy-hosts/preview", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	recorder := httptest.NewRecorder()

	(&Server{}).apiPreviewProxyHost(recorder, req)

	if recorder.Code != http.StatusOK {
		t.Fatalf("preview status = %d, want %d", recorder.Code, http.StatusOK)
	}
	var response struct {
		Caddyfile string         `json:"caddyfile"`
		Route     map[string]any `json:"route"`
		Error     string         `json:"error"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatalf("decode preview response: %v", err)
	}
	if response.Error != "" {
		t.Fatalf("preview error = %q", response.Error)
	}
	for _, expected := range []string{
		"preview.example.com {",
		"reverse_proxy https://app.internal:8443 app-backup.internal:8443",
		"basic auth users",
	} {
		if !strings.Contains(response.Caddyfile, expected) {
			t.Fatalf("Caddyfile preview missing %q:\n%s", expected, response.Caddyfile)
		}
	}
	if len(response.Route) == 0 {
		t.Fatal("route JSON preview is empty")
	}
	responseJSON := recorder.Body.String()
	for _, expected := range []string{`"handler": "authentication"`, `"username": "alice"`, `"realm": "Members"`} {
		if !strings.Contains(responseJSON, expected) {
			t.Fatalf("Basic Auth preview missing %q:\n%s", expected, responseJSON)
		}
	}
	handlers, ok := response.Route["handle"].([]any)
	if !ok || len(handlers) == 0 {
		t.Fatalf("route handlers missing from preview: %#v", response.Route)
	}
	var authHandler map[string]any
	for _, handler := range handlers {
		candidate, _ := handler.(map[string]any)
		if candidate["handler"] == "authentication" {
			authHandler = candidate
			break
		}
	}
	if authHandler == nil {
		t.Fatalf("Basic Auth handler missing from preview: %#v", handlers)
	}
	providers, _ := authHandler["providers"].(map[string]any)
	httpBasic, _ := providers["http_basic"].(map[string]any)
	accounts, _ := httpBasic["accounts"].([]any)
	if len(accounts) != 1 {
		t.Fatalf("Basic Auth accounts = %#v, want one redacted account", accounts)
	}
	account, _ := accounts[0].(map[string]any)
	if account["password"] != "<redacted>" {
		t.Fatalf("Basic Auth password preview = %#v, want redacted", account["password"])
	}
	if strings.Contains(responseJSON, "saved-secret-bcrypt-hash") {
		t.Fatal("Basic Auth preview leaked the saved bcrypt hash")
	}
	for _, secret := range []string{
		"live-api-secret",
		"live-cookie-secret",
		"upstream-user:upstream-pass",
		base64.StdEncoding.EncodeToString([]byte("upstream-user:upstream-pass")),
		"probe-user:probe-pass",
		base64.StdEncoding.EncodeToString([]byte("probe-user:probe-pass")),
		"proxy-user",
		"proxy-pass",
		"health-query-secret",
		"proxy-query-secret",
		"auth-query-secret",
	} {
		if strings.Contains(responseJSON, secret) {
			t.Fatalf("route preview leaked credential %q", secret)
		}
	}
	if !strings.Contains(responseJSON, "mode=full") || !strings.Contains(responseJSON, "mode=strict") {
		t.Fatalf("route preview removed non-sensitive URL query parameters: %s", responseJSON)
	}
}

func TestRedactPreviewURLFailsClosedForMalformedQueryKey(t *testing.T) {
	got := redactPreviewURL("/health?to%ZZken=malformed-query-secret;mode=full")
	if strings.Contains(got, "malformed-query-secret") {
		t.Fatalf("malformed URL query leaked its value: %q", got)
	}
	if !strings.Contains(got, "mode=full") {
		t.Fatalf("malformed URL query removed harmless parameters: %q", got)
	}
}

func TestRedactPreviewURLScrubsUserinfoBeforeParsing(t *testing.T) {
	got := redactPreviewURL("http://live-user:live-password@proxy.internal/%ZZ?mode=full")
	if strings.Contains(got, "live-user") || strings.Contains(got, "live-password") {
		t.Fatalf("malformed URL leaked userinfo: %q", got)
	}
	if !strings.Contains(got, "redacted:redacted@proxy.internal") || !strings.Contains(got, "mode=full") {
		t.Fatalf("malformed URL did not preserve its safe structure: %q", got)
	}
}

func TestAPIProxyHostPreviewIncludesAdaptedAdvancedHandlers(t *testing.T) {
	adapter := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/adapt" {
			http.NotFound(w, r)
			return
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"result": map[string]any{
				"apps": map[string]any{
					"http": map[string]any{
						"servers": map[string]any{
							"srv0": map[string]any{
								"routes": []any{map[string]any{
									"handle": []any{map[string]any{"handler": "headers"}},
								}},
							},
						},
					},
				},
			},
		})
	}))
	t.Cleanup(adapter.Close)

	form := url.Values{
		"domains":         {"advanced.example.com"},
		"forward_scheme":  {"http"},
		"forward_host":    {"app.internal"},
		"forward_port":    {"8080"},
		"advanced_config": {"header X-Preview yes"},
	}
	req := httptest.NewRequest(http.MethodPost, "/api/proxy-hosts/preview", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	recorder := httptest.NewRecorder()

	(&Server{Caddy: caddy.New(adapter.URL, "", "")}).apiPreviewProxyHost(recorder, req)

	var response struct {
		Route         map[string]any `json:"route"`
		AdvancedError string         `json:"advanced_error"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatalf("decode advanced preview response: %v", err)
	}
	if response.AdvancedError != "" {
		t.Fatalf("advanced preview error = %q", response.AdvancedError)
	}
	handlers, _ := response.Route["handle"].([]any)
	if len(handlers) < 2 {
		t.Fatalf("advanced preview handlers = %#v, want advanced + reverse proxy", handlers)
	}
	advanced, _ := handlers[0].(map[string]any)
	if advanced["handler"] != "headers" {
		t.Fatalf("first preview handler = %#v, want adapted headers handler", advanced)
	}
}

func TestAPIProxyHostPreviewRejectsTerminalAdvancedHandlers(t *testing.T) {
	form := url.Values{
		"domains":         {"terminal.example.com"},
		"forward_scheme":  {"http"},
		"forward_host":    {"app.internal"},
		"forward_port":    {"8080"},
		"advanced_config": {"respond \"not deployable here\" 200"},
	}
	req := httptest.NewRequest(http.MethodPost, "/api/proxy-hosts/preview", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	recorder := httptest.NewRecorder()

	(&Server{}).apiPreviewProxyHost(recorder, req)

	var response struct {
		Route         map[string]any `json:"route"`
		AdvancedError string         `json:"advanced_error"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatalf("decode terminal advanced preview response: %v", err)
	}
	if !strings.Contains(response.AdvancedError, "can't contain `respond`") {
		t.Fatalf("advanced preview error = %q, want terminal-directive rejection", response.AdvancedError)
	}
	handlers, _ := response.Route["handle"].([]any)
	if len(handlers) != 1 {
		t.Fatalf("terminal preview handlers = %#v, want only reverse proxy", handlers)
	}
	reverseProxy, _ := handlers[0].(map[string]any)
	if reverseProxy["handler"] != "reverse_proxy" {
		t.Fatalf("terminal preview handler = %#v, want reverse proxy", reverseProxy)
	}
}

func TestAPIProxyHostPreviewPadsIncompleteRequiredFields(t *testing.T) {
	req := httptest.NewRequest(http.MethodPost, "/api/proxy-hosts/preview", strings.NewReader(""))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	recorder := httptest.NewRecorder()

	(&Server{}).apiPreviewProxyHost(recorder, req)

	var response struct {
		Caddyfile string `json:"caddyfile"`
		Error     string `json:"error"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatalf("decode preview response: %v", err)
	}
	if response.Error != "" {
		t.Fatalf("preview error = %q", response.Error)
	}
	for _, expected := range []string{"example.com {", "(your-upstream):0"} {
		if !strings.Contains(response.Caddyfile, expected) {
			t.Fatalf("placeholder Caddyfile missing %q:\n%s", expected, response.Caddyfile)
		}
	}
}