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()