Skip to content

Commit 0c66918

Browse files
authored
Merge branch 'master' into reqSize
2 parents c4b4379 + 790f3e0 commit 0c66918

File tree

4 files changed

+205
-0
lines changed

4 files changed

+205
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
:80 {
2+
header Test-Static ":443" "STATIC-WORKS"
3+
header Test-Dynamic ":{http.request.local.port}" "DYNAMIC-WORKS"
4+
header Test-Complex "port-{http.request.local.port}-end" "COMPLEX-{http.request.method}"
5+
}
6+
----------
7+
{
8+
"apps": {
9+
"http": {
10+
"servers": {
11+
"srv0": {
12+
"listen": [
13+
":80"
14+
],
15+
"routes": [
16+
{
17+
"handle": [
18+
{
19+
"handler": "headers",
20+
"response": {
21+
"replace": {
22+
"Test-Static": [
23+
{
24+
"replace": "STATIC-WORKS",
25+
"search_regexp": ":443"
26+
}
27+
]
28+
}
29+
}
30+
},
31+
{
32+
"handler": "headers",
33+
"response": {
34+
"replace": {
35+
"Test-Dynamic": [
36+
{
37+
"replace": "DYNAMIC-WORKS",
38+
"search_regexp": ":{http.request.local.port}"
39+
}
40+
]
41+
}
42+
}
43+
},
44+
{
45+
"handler": "headers",
46+
"response": {
47+
"replace": {
48+
"Test-Complex": [
49+
{
50+
"replace": "COMPLEX-{http.request.method}",
51+
"search_regexp": "port-{http.request.local.port}-end"
52+
}
53+
]
54+
}
55+
}
56+
}
57+
]
58+
}
59+
]
60+
}
61+
}
62+
}
63+
}
64+
}

modules/caddyhttp/fileserver/browse.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,7 @@ <h1>
11761176
</footer>
11771177

11781178
<script {{ $nonceAttribute }}>
1179+
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
11791180
const filterEl = document.getElementById('filter');
11801181
filterEl?.focus({ preventScroll: true });
11811182

@@ -1265,6 +1266,7 @@ <h1>
12651266
}
12661267
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
12671268
timeList.forEach(localizeDatetime);
1269+
// @license-end
12681270
</script>
12691271
</body>
12701272
</html>

modules/caddyhttp/headers/headers.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
141141
if r.SearchRegexp == "" {
142142
continue
143143
}
144+
145+
// Check if it contains placeholders
146+
if containsPlaceholders(r.SearchRegexp) {
147+
// Contains placeholders, skips precompilation, and recompiles at runtime
148+
continue
149+
}
150+
151+
// Does not contain placeholders, safe to precompile
144152
re, err := regexp.Compile(r.SearchRegexp)
145153
if err != nil {
146154
return fmt.Errorf("replacement %d for header field '%s': %v", i, fieldName, err)
@@ -151,6 +159,20 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
151159
return nil
152160
}
153161

162+
// containsCaddyPlaceholders checks if the string contains Caddy placeholder syntax {key}
163+
func containsPlaceholders(s string) bool {
164+
openIdx := strings.Index(s, "{")
165+
if openIdx == -1 {
166+
return false
167+
}
168+
closeIdx := strings.Index(s[openIdx+1:], "}")
169+
if closeIdx == -1 {
170+
return false
171+
}
172+
// Make sure there is content between the brackets
173+
return closeIdx > 0
174+
}
175+
154176
func (ops HeaderOps) validate() error {
155177
for fieldName, replacements := range ops.Replace {
156178
for _, r := range replacements {
@@ -269,7 +291,15 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
269291
for fieldName, vals := range hdr {
270292
for i := range vals {
271293
if r.re != nil {
294+
// Use precompiled regular expressions
272295
hdr[fieldName][i] = r.re.ReplaceAllString(hdr[fieldName][i], replace)
296+
} else if r.SearchRegexp != "" {
297+
// Runtime compilation of regular expressions
298+
searchRegexp := repl.ReplaceKnown(r.SearchRegexp, "")
299+
if re, err := regexp.Compile(searchRegexp); err == nil {
300+
hdr[fieldName][i] = re.ReplaceAllString(hdr[fieldName][i], replace)
301+
}
302+
// If compilation fails, skip this replacement
273303
} else {
274304
hdr[fieldName][i] = strings.ReplaceAll(hdr[fieldName][i], search, replace)
275305
}
@@ -291,6 +321,11 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
291321
for i := range vals {
292322
if r.re != nil {
293323
hdr[hdrFieldName][i] = r.re.ReplaceAllString(hdr[hdrFieldName][i], replace)
324+
} else if r.SearchRegexp != "" {
325+
searchRegexp := repl.ReplaceKnown(r.SearchRegexp, "")
326+
if re, err := regexp.Compile(searchRegexp); err == nil {
327+
hdr[hdrFieldName][i] = re.ReplaceAllString(hdr[hdrFieldName][i], replace)
328+
}
294329
} else {
295330
hdr[hdrFieldName][i] = strings.ReplaceAll(hdr[hdrFieldName][i], search, replace)
296331
}

modules/caddyhttp/headers/headers_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,107 @@ type nextHandler func(http.ResponseWriter, *http.Request) error
272272
func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
273273
return f(w, r)
274274
}
275+
276+
func TestContainsPlaceholders(t *testing.T) {
277+
for i, tc := range []struct {
278+
input string
279+
expected bool
280+
}{
281+
{"static", false},
282+
{"{placeholder}", true},
283+
{"prefix-{placeholder}-suffix", true},
284+
{"{}", false},
285+
{"no-braces", false},
286+
{"{unclosed", false},
287+
{"unopened}", false},
288+
} {
289+
actual := containsPlaceholders(tc.input)
290+
if actual != tc.expected {
291+
t.Errorf("Test %d: containsPlaceholders(%q) = %v, expected %v", i, tc.input, actual, tc.expected)
292+
}
293+
}
294+
}
295+
296+
func TestHeaderProvisionSkipsPlaceholders(t *testing.T) {
297+
ops := &HeaderOps{
298+
Replace: map[string][]Replacement{
299+
"Static": {
300+
Replacement{SearchRegexp: ":443", Replace: "STATIC"},
301+
},
302+
"Dynamic": {
303+
Replacement{SearchRegexp: ":{http.request.local.port}", Replace: "DYNAMIC"},
304+
},
305+
},
306+
}
307+
308+
err := ops.Provision(caddy.Context{})
309+
if err != nil {
310+
t.Fatalf("Provision failed: %v", err)
311+
}
312+
313+
// Static regex should be precompiled
314+
if ops.Replace["Static"][0].re == nil {
315+
t.Error("Expected static regex to be precompiled")
316+
}
317+
318+
// Dynamic regex with placeholder should not be precompiled
319+
if ops.Replace["Dynamic"][0].re != nil {
320+
t.Error("Expected dynamic regex with placeholder to not be precompiled")
321+
}
322+
}
323+
324+
func TestPlaceholderInSearchRegexp(t *testing.T) {
325+
handler := Handler{
326+
Response: &RespHeaderOps{
327+
HeaderOps: &HeaderOps{
328+
Replace: map[string][]Replacement{
329+
"Test-Header": {
330+
Replacement{
331+
SearchRegexp: ":{http.request.local.port}",
332+
Replace: "PLACEHOLDER-WORKS",
333+
},
334+
},
335+
},
336+
},
337+
},
338+
}
339+
340+
// Provision the handler
341+
err := handler.Provision(caddy.Context{})
342+
if err != nil {
343+
t.Fatalf("Provision failed: %v", err)
344+
}
345+
346+
replacement := handler.Response.HeaderOps.Replace["Test-Header"][0]
347+
t.Logf("After provision - SearchRegexp: %q, re: %v", replacement.SearchRegexp, replacement.re)
348+
349+
rr := httptest.NewRecorder()
350+
351+
req := httptest.NewRequest("GET", "http://localhost:443/", nil)
352+
repl := caddy.NewReplacer()
353+
repl.Set("http.request.local.port", "443")
354+
355+
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
356+
req = req.WithContext(ctx)
357+
358+
rr.Header().Set("Test-Header", "prefix:443suffix")
359+
t.Logf("Initial header: %v", rr.Header())
360+
361+
next := nextHandler(func(w http.ResponseWriter, r *http.Request) error {
362+
w.WriteHeader(200)
363+
return nil
364+
})
365+
366+
err = handler.ServeHTTP(rr, req, next)
367+
if err != nil {
368+
t.Fatalf("ServeHTTP failed: %v", err)
369+
}
370+
371+
t.Logf("Final header: %v", rr.Header())
372+
373+
result := rr.Header().Get("Test-Header")
374+
expected := "prefixPLACEHOLDER-WORKSsuffix"
375+
if result != expected {
376+
t.Errorf("Expected header value %q, got %q", expected, result)
377+
}
378+
}

0 commit comments

Comments
 (0)