@marcel91200 / caddy Mirror · read-only / commits / 55a95ad2
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 04:45 UTC

caddyhttp: keep absent optional placeholders empty (#8019)

Zen Dodd committed Oct 3, 2026 at 11:54 UTC 55a95ad25319d76ac4000e9e6fd72886ee61de87
3 files changed +102 -8
modules/caddyhttp/replacer.go
+22 -8
@@ -88,6 +88,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
88 return cookie.Value, true
89 }
90 }
91 + return "", true
92 }
93
94 // http.request.tls.*
@@ -471,10 +472,23 @@ func readRequestBodyForPlaceholder(req *http.Request) ([]byte, error) {
472 }
473
474 func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
474 - if req == nil || req.TLS == nil {
475 + if req == nil {
476 return nil, false
477 }
478 + state := req.TLS
479 + if state == nil {
480 + state = new(tls.ConnectionState)
481 + }
482 + value, known := getTLSReplacement(state, key)
483 + if req.TLS == nil {
484 + // Use the same field parser to recognise valid placeholders but
485 + // do not substitute values from the empty state for a plain HTTP request.
486 + return nil, known
487 + }
488 + return value, known
489 +}
490
491 +func getTLSReplacement(state *tls.ConnectionState, key string) (any, bool) {
492 if len(key) < len(reqTLSReplPrefix) {
493 return nil, false
494 }
@@ -482,7 +496,7 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
496 field := strings.ToLower(key[len(reqTLSReplPrefix):])
497
498 if strings.HasPrefix(field, "client.") {
485 - cert := getTLSPeerCert(req.TLS)
499 + cert := getTLSPeerCert(state)
500 if cert == nil {
501 // Instead of returning (nil, false) here, we set it to a dummy
502 // value to fix #7530. This way, even if there is no client cert,
@@ -587,20 +601,20 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
601
602 switch field {
603 case "version":
590 - return caddytls.ProtocolName(req.TLS.Version), true
604 + return caddytls.ProtocolName(state.Version), true
605 case "cipher_suite":
592 - return tls.CipherSuiteName(req.TLS.CipherSuite), true
606 + return tls.CipherSuiteName(state.CipherSuite), true
607 case "resumed":
594 - return req.TLS.DidResume, true
608 + return state.DidResume, true
609 case "proto":
596 - return req.TLS.NegotiatedProtocol, true
610 + return state.NegotiatedProtocol, true
611 case "proto_mutual":
612 // req.TLS.NegotiatedProtocolIsMutual is deprecated - it's always true.
613 return true, true
614 case "server_name":
601 - return req.TLS.ServerName, true
615 + return state.ServerName, true
616 case "ech":
603 - return req.TLS.ECHAccepted, true
617 + return state.ECHAccepted, true
618 }
619 return nil, false
620 }
modules/caddyhttp/replacer_test.go
+56
@@ -28,6 +28,62 @@ import (
28 "github.com/caddyserver/caddy/v2"
29 )
30
31 +func TestMissingCookiePlaceholder(t *testing.T) {
32 + req := httptest.NewRequest(http.MethodGet, "/", nil)
33 + req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
34 + repl := NewTestReplacer(req)
35 + const input = "before-{http.request.cookie.session}-after"
36 + if got := repl.ReplaceKnown(input, ""); got != "before--after" {
37 + t.Fatalf("missing cookie = %q, want %q", got, "before--after")
38 + }
39 + req.AddCookie(&http.Cookie{Name: "session", Value: "present"})
40 + if got := repl.ReplaceKnown(input, ""); got != "before-present-after" {
41 + t.Fatalf("present cookie = %q, want %q", got, "before-present-after")
42 + }
43 +}
44 +
45 +func TestTLSPlaceholdersWithoutTLS(t *testing.T) {
46 + req := httptest.NewRequest(http.MethodGet, "/", nil)
47 + req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
48 + repl := NewTestReplacer(req)
49 + for _, field := range []string{
50 + "version", "cipher_suite", "resumed", "proto", "proto_mutual", "server_name", "ech",
51 + "client.fingerprint", "client.public_key", "client.public_key_sha256",
52 + "client.issuer", "client.serial", "client.subject", "client.certificate_pem", "client.certificate_der_base64",
53 + "client.san.dns_names", "client.san.emails", "client.san.ips", "client.san.uris",
54 + "client.san.dns_names.0", "client.san.emails.0", "client.san.ips.0", "client.san.uris.0",
55 + } {
56 + t.Run(field, func(t *testing.T) {
57 + key := "http.request.tls." + field
58 + value, known := repl.Get(key)
59 + if !known || caddy.ToString(value) != "" {
60 + t.Fatalf("Get(%q) = %v, %v; want empty, known", key, value, known)
61 + }
62 + if got := repl.ReplaceKnown("before-{"+key+"}-after", ""); got != "before--after" {
63 + t.Fatalf("replacement = %q, want %q", got, "before--after")
64 + }
65 + })
66 + }
67 +}
68 +
69 +func TestUnknownTLSPlaceholdersRemainLiteral(t *testing.T) {
70 + for _, state := range []*tls.ConnectionState{nil, {ServerName: "example.com"}} {
71 + req := httptest.NewRequest(http.MethodGet, "/", nil)
72 + req.TLS = state
73 + req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
74 + repl := NewTestReplacer(req)
75 + for _, field := range []string{
76 + "unknown", "client.unknown", "client.san.unknown",
77 + "client.san.dns_names_extra", "client.san.dns_names.-1", "client.san.dns_names.nope",
78 + } {
79 + input := "{http.request.tls." + field + "}"
80 + if got := repl.ReplaceKnown(input, ""); got != input {
81 + t.Errorf("replacement = %q, want literal %q", got, input)
82 + }
83 + }
84 + }
85 +}
86 +
87 func TestHTTPVarReplacement(t *testing.T) {
88 req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz?a=1&b=2", nil)
89 repl := caddy.NewReplacer()
modules/caddyhttp/staticresp_test.go
+24
@@ -65,6 +65,30 @@ func fakeRequest() *http.Request {
65 return r
66 }
67
68 +func TestStaticResponseHeadersWithAbsentRequestData(t *testing.T) {
69 + req := httptest.NewRequest(http.MethodGet, "/", nil)
70 + req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
71 + NewTestReplacer(req)
72 + response := StaticResponse{Headers: http.Header{
73 + "Location": []string{"/login?session={http.request.cookie.session}"},
74 + "X-Tls": []string{"before-{http.request.tls.server_name}-after"},
75 + "X-Unknown": []string{"before-{unknown}-after"},
76 + }}
77 + w := httptest.NewRecorder()
78 + if err := response.ServeHTTP(w, req, nil); err != nil {
79 + t.Fatal(err)
80 + }
81 + for field, want := range map[string]string{
82 + "Location": "/login?session=",
83 + "X-Tls": "before--after",
84 + "X-Unknown": "before-{unknown}-after",
85 + } {
86 + if got := w.Header().Get(field); got != want {
87 + t.Errorf("%s = %q, want %q", field, got, want)
88 + }
89 + }
90 +}
91 +
92 func TestStaticResponseHeadersKeepUnknownPlaceholders(t *testing.T) {
93 r := fakeRequest()
94 w := httptest.NewRecorder()