Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
:80 {
header Test-Static ":443" "STATIC-WORKS"
header Test-Dynamic ":{http.request.local.port}" "DYNAMIC-WORKS"
header Test-Complex "port-{http.request.local.port}-end" "COMPLEX-{http.request.method}"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "headers",
"response": {
"replace": {
"Test-Static": [
{
"replace": "STATIC-WORKS",
"search_regexp": ":443"
}
]
}
}
},
{
"handler": "headers",
"response": {
"replace": {
"Test-Dynamic": [
{
"replace": "DYNAMIC-WORKS",
"search_regexp": ":{http.request.local.port}"
}
]
}
}
},
{
"handler": "headers",
"response": {
"replace": {
"Test-Complex": [
{
"replace": "COMPLEX-{http.request.method}",
"search_regexp": "port-{http.request.local.port}-end"
}
]
}
}
}
]
}
]
}
}
}
}
}
35 changes: 35 additions & 0 deletions modules/caddyhttp/headers/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
if r.SearchRegexp == "" {
continue
}

// Check if it contains placeholders
if containsPlaceholders(r.SearchRegexp) {
// Contains placeholders, skips precompilation, and recompiles at runtime
continue
}

// Does not contain placeholders, safe to precompile
re, err := regexp.Compile(r.SearchRegexp)
if err != nil {
return fmt.Errorf("replacement %d for header field '%s': %v", i, fieldName, err)
Expand All @@ -151,6 +159,20 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
return nil
}

// containsCaddyPlaceholders checks if the string contains Caddy placeholder syntax {key}
func containsPlaceholders(s string) bool {
openIdx := strings.Index(s, "{")
if openIdx == -1 {
return false
}
closeIdx := strings.Index(s[openIdx+1:], "}")
if closeIdx == -1 {
return false
}
// Make sure there is content between the brackets
return closeIdx > 0
}

func (ops HeaderOps) validate() error {
for fieldName, replacements := range ops.Replace {
for _, r := range replacements {
Expand Down Expand Up @@ -269,7 +291,15 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
for fieldName, vals := range hdr {
for i := range vals {
if r.re != nil {
// Use precompiled regular expressions
hdr[fieldName][i] = r.re.ReplaceAllString(hdr[fieldName][i], replace)
} else if r.SearchRegexp != "" {
// Runtime compilation of regular expressions
searchRegexp := repl.ReplaceKnown(r.SearchRegexp, "")
if re, err := regexp.Compile(searchRegexp); err == nil {
hdr[fieldName][i] = re.ReplaceAllString(hdr[fieldName][i], replace)
}
// If compilation fails, skip this replacement
} else {
hdr[fieldName][i] = strings.ReplaceAll(hdr[fieldName][i], search, replace)
}
Expand All @@ -291,6 +321,11 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
for i := range vals {
if r.re != nil {
hdr[hdrFieldName][i] = r.re.ReplaceAllString(hdr[hdrFieldName][i], replace)
} else if r.SearchRegexp != "" {
searchRegexp := repl.ReplaceKnown(r.SearchRegexp, "")
if re, err := regexp.Compile(searchRegexp); err == nil {
hdr[hdrFieldName][i] = re.ReplaceAllString(hdr[hdrFieldName][i], replace)
}
} else {
hdr[hdrFieldName][i] = strings.ReplaceAll(hdr[hdrFieldName][i], search, replace)
}
Expand Down
104 changes: 104 additions & 0 deletions modules/caddyhttp/headers/headers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,107 @@ type nextHandler func(http.ResponseWriter, *http.Request) error
func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
return f(w, r)
}

func TestContainsPlaceholders(t *testing.T) {
for i, tc := range []struct {
input string
expected bool
}{
{"static", false},
{"{placeholder}", true},
{"prefix-{placeholder}-suffix", true},
{"{}", false},
{"no-braces", false},
{"{unclosed", false},
{"unopened}", false},
} {
actual := containsPlaceholders(tc.input)
if actual != tc.expected {
t.Errorf("Test %d: containsPlaceholders(%q) = %v, expected %v", i, tc.input, actual, tc.expected)
}
}
}

func TestHeaderProvisionSkipsPlaceholders(t *testing.T) {
ops := &HeaderOps{
Replace: map[string][]Replacement{
"Static": {
Replacement{SearchRegexp: ":443", Replace: "STATIC"},
},
"Dynamic": {
Replacement{SearchRegexp: ":{http.request.local.port}", Replace: "DYNAMIC"},
},
},
}

err := ops.Provision(caddy.Context{})
if err != nil {
t.Fatalf("Provision failed: %v", err)
}

// Static regex should be precompiled
if ops.Replace["Static"][0].re == nil {
t.Error("Expected static regex to be precompiled")
}

// Dynamic regex with placeholder should not be precompiled
if ops.Replace["Dynamic"][0].re != nil {
t.Error("Expected dynamic regex with placeholder to not be precompiled")
}
}

func TestPlaceholderInSearchRegexp(t *testing.T) {
handler := Handler{
Response: &RespHeaderOps{
HeaderOps: &HeaderOps{
Replace: map[string][]Replacement{
"Test-Header": {
Replacement{
SearchRegexp: ":{http.request.local.port}",
Replace: "PLACEHOLDER-WORKS",
},
},
},
},
},
}

// Provision the handler
err := handler.Provision(caddy.Context{})
if err != nil {
t.Fatalf("Provision failed: %v", err)
}

replacement := handler.Response.HeaderOps.Replace["Test-Header"][0]
t.Logf("After provision - SearchRegexp: %q, re: %v", replacement.SearchRegexp, replacement.re)

rr := httptest.NewRecorder()

req := httptest.NewRequest("GET", "http://localhost:443/", nil)
repl := caddy.NewReplacer()
repl.Set("http.request.local.port", "443")

ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)

rr.Header().Set("Test-Header", "prefix:443suffix")
t.Logf("Initial header: %v", rr.Header())

next := nextHandler(func(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(200)
return nil
})

err = handler.ServeHTTP(rr, req, next)
if err != nil {
t.Fatalf("ServeHTTP failed: %v", err)
}

t.Logf("Final header: %v", rr.Header())

result := rr.Header().Get("Test-Header")
expected := "prefixPLACEHOLDER-WORKSsuffix"
if result != expected {
t.Errorf("Expected header value %q, got %q", expected, result)
}
}
Loading