caddyhttp: keep absent optional placeholders empty

pull/8019/head
Zen Dodd 2026-09-14 17:32:58 +10:00
parent 56e3a88efe
commit 512c8de7aa
No known key found for this signature in database
GPG Key ID: 6909546B2C52EC2D
3 changed files with 102 additions and 9 deletions

View File

@ -87,6 +87,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
return cookie.Value, true
}
}
return "", true
}
// http.request.tls.*
@ -417,10 +418,23 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
}
func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
if req == nil || req.TLS == nil {
if req == nil {
return nil, false
}
state := req.TLS
if state == nil {
state = new(tls.ConnectionState)
}
value, known := getTLSReplacement(state, key)
if req.TLS == nil {
// Use the same field parser to recognise valid placeholders but
// do not substitute values from the empty state for a plain HTTP request.
return nil, known
}
return value, known
}
func getTLSReplacement(state *tls.ConnectionState, key string) (any, bool) {
if len(key) < len(reqTLSReplPrefix) {
return nil, false
}
@ -428,7 +442,7 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
field := strings.ToLower(key[len(reqTLSReplPrefix):])
if strings.HasPrefix(field, "client.") {
cert := getTLSPeerCert(req.TLS)
cert := getTLSPeerCert(state)
if cert == nil {
// Instead of returning (nil, false) here, we set it to a dummy
// value to fix #7530. This way, even if there is no client cert,
@ -533,20 +547,20 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
switch field {
case "version":
return caddytls.ProtocolName(req.TLS.Version), true
return caddytls.ProtocolName(state.Version), true
case "cipher_suite":
return tls.CipherSuiteName(req.TLS.CipherSuite), true
return tls.CipherSuiteName(state.CipherSuite), true
case "resumed":
return req.TLS.DidResume, true
return state.DidResume, true
case "proto":
return req.TLS.NegotiatedProtocol, true
return state.NegotiatedProtocol, true
case "proto_mutual":
// req.TLS.NegotiatedProtocolIsMutual is deprecated - it's always true.
return true, true
case "server_name":
return req.TLS.ServerName, true
return state.ServerName, true
case "ech":
return req.TLS.ECHAccepted, true
return state.ECHAccepted, true
}
return nil, false
}

View File

@ -27,6 +27,62 @@ import (
"github.com/caddyserver/caddy/v2"
)
func TestMissingCookiePlaceholder(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
repl := NewTestReplacer(req)
const input = "before-{http.request.cookie.session}-after"
if got := repl.ReplaceKnown(input, ""); got != "before--after" {
t.Fatalf("missing cookie = %q, want %q", got, "before--after")
}
req.AddCookie(&http.Cookie{Name: "session", Value: "present"})
if got := repl.ReplaceKnown(input, ""); got != "before-present-after" {
t.Fatalf("present cookie = %q, want %q", got, "before-present-after")
}
}
func TestTLSPlaceholdersWithoutTLS(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
repl := NewTestReplacer(req)
for _, field := range []string{
"version", "cipher_suite", "resumed", "proto", "proto_mutual", "server_name", "ech",
"client.fingerprint", "client.public_key", "client.public_key_sha256",
"client.issuer", "client.serial", "client.subject", "client.certificate_pem", "client.certificate_der_base64",
"client.san.dns_names", "client.san.emails", "client.san.ips", "client.san.uris",
"client.san.dns_names.0", "client.san.emails.0", "client.san.ips.0", "client.san.uris.0",
} {
t.Run(field, func(t *testing.T) {
key := "http.request.tls." + field
value, known := repl.Get(key)
if !known || caddy.ToString(value) != "" {
t.Fatalf("Get(%q) = %v, %v; want empty, known", key, value, known)
}
if got := repl.ReplaceKnown("before-{"+key+"}-after", ""); got != "before--after" {
t.Fatalf("replacement = %q, want %q", got, "before--after")
}
})
}
}
func TestUnknownTLSPlaceholdersRemainLiteral(t *testing.T) {
for _, state := range []*tls.ConnectionState{nil, {ServerName: "example.com"}} {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.TLS = state
req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
repl := NewTestReplacer(req)
for _, field := range []string{
"unknown", "client.unknown", "client.san.unknown",
"client.san.dns_names_extra", "client.san.dns_names.-1", "client.san.dns_names.nope",
} {
input := "{http.request.tls." + field + "}"
if got := repl.ReplaceKnown(input, ""); got != input {
t.Errorf("replacement = %q, want literal %q", got, input)
}
}
}
}
func TestHTTPVarReplacement(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz?a=1&b=2", nil)
repl := caddy.NewReplacer()
@ -334,4 +390,3 @@ func TestHTTPVarReplacementUUID(t *testing.T) {
t.Errorf("expected stable uuid across references: %q != %q", first, second)
}
}

View File

@ -65,6 +65,30 @@ func fakeRequest() *http.Request {
return r
}
func TestStaticResponseHeadersWithAbsentRequestData(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
NewTestReplacer(req)
response := StaticResponse{Headers: http.Header{
"Location": []string{"/login?session={http.request.cookie.session}"},
"X-Tls": []string{"before-{http.request.tls.server_name}-after"},
"X-Unknown": []string{"before-{unknown}-after"},
}}
w := httptest.NewRecorder()
if err := response.ServeHTTP(w, req, nil); err != nil {
t.Fatal(err)
}
for field, want := range map[string]string{
"Location": "/login?session=",
"X-Tls": "before--after",
"X-Unknown": "before-{unknown}-after",
} {
if got := w.Header().Get(field); got != want {
t.Errorf("%s = %q, want %q", field, got, want)
}
}
}
func TestStaticResponseHeadersKeepUnknownPlaceholders(t *testing.T) {
r := fakeRequest()
w := httptest.NewRecorder()