Commit f1eaae9b authored by Matthew Fay's avatar Matthew Fay Committed by Matt Holt

httpserver: Rework Replacer loop to ignore escaped braces (#2075)

* httpserver.Replacer: Rework loop to ignore escaped placeholder braces

* Fix typo and ineffectual assignment to ret

* Remove redundant idxOffset declaration, simplify escape check

* Add benchmark tests for new Replacer code
parent 2716e272
...@@ -141,6 +141,14 @@ func canLogRequest(r *http.Request) bool { ...@@ -141,6 +141,14 @@ func canLogRequest(r *http.Request) bool {
return false return false
} }
// unescapeBraces finds escaped braces in s and returns
// a string with those braces unescaped.
func unescapeBraces(s string) string {
s = strings.Replace(s, "\\{", "{", -1)
s = strings.Replace(s, "\\}", "}", -1)
return s
}
// Replace performs a replacement of values on s and returns // Replace performs a replacement of values on s and returns
// the string with the replaced values. // the string with the replaced values.
func (r *replacer) Replace(s string) string { func (r *replacer) Replace(s string) string {
...@@ -150,32 +158,59 @@ func (r *replacer) Replace(s string) string { ...@@ -150,32 +158,59 @@ func (r *replacer) Replace(s string) string {
} }
result := "" result := ""
Placeholders: // process each placeholder in sequence
for { for {
idxStart := strings.Index(s, "{") var idxStart, idxEnd int
idxOffset := 0
for { // find first unescaped opening brace
searchSpace := s[idxOffset:]
idxStart = strings.Index(searchSpace, "{")
if idxStart == -1 { if idxStart == -1 {
// no placeholder anymore // no more placeholders
break Placeholders
}
if idxStart == 0 || searchSpace[idxStart-1] != '\\' {
// preceding character is not an escape
idxStart += idxOffset
break break
} }
idxEnd := strings.Index(s[idxStart:], "}") // the brace we found was escaped
// search the rest of the string next
idxOffset += idxStart + 1
}
idxOffset = 0
for { // find first unescaped closing brace
searchSpace := s[idxStart+idxOffset:]
idxEnd = strings.Index(searchSpace, "}")
if idxEnd == -1 { if idxEnd == -1 {
// unpaired placeholder // unpaired placeholder
break Placeholders
}
if idxEnd == 0 || searchSpace[idxEnd-1] != '\\' {
// preceding character is not an escape
idxEnd += idxOffset + idxStart
break break
} }
idxEnd += idxStart // the brace we found was escaped
// search the rest of the string next
idxOffset += idxEnd + 1
}
// get a replacement // get a replacement for the unescaped placeholder
placeholder := s[idxStart : idxEnd+1] placeholder := unescapeBraces(s[idxStart : idxEnd+1])
replacement := r.getSubstitution(placeholder) replacement := r.getSubstitution(placeholder)
// append prefix + replacement // append unescaped prefix + replacement
result += s[:idxStart] + replacement result += strings.TrimPrefix(unescapeBraces(s[:idxStart]), "\\") + replacement
// strip out scanned parts // strip out scanned parts
s = s[idxEnd+1:] s = s[idxEnd+1:]
} }
// append unscanned parts // append unscanned parts
return result + s return result + unescapeBraces(s)
} }
func roundDuration(d time.Duration) time.Duration { func roundDuration(d time.Duration) time.Duration {
......
...@@ -112,6 +112,7 @@ func TestReplace(t *testing.T) { ...@@ -112,6 +112,7 @@ func TestReplace(t *testing.T) {
{"Query string is {query}", "Query string is foo=bar"}, {"Query string is {query}", "Query string is foo=bar"},
{"Query string value for foo is {?foo}", "Query string value for foo is bar"}, {"Query string value for foo is {?foo}", "Query string value for foo is bar"},
{"Missing query string argument is {?missing}", "Missing query string argument is "}, {"Missing query string argument is {?missing}", "Missing query string argument is "},
{"\\{ 'hostname': '{hostname}' \\}", "{ 'hostname': '" + hostname + "' }"},
} }
for _, c := range testCases { for _, c := range testCases {
...@@ -144,6 +145,70 @@ func TestReplace(t *testing.T) { ...@@ -144,6 +145,70 @@ func TestReplace(t *testing.T) {
} }
} }
func BenchmarkReplace(b *testing.B) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
reader := strings.NewReader(`{"username": "dennis"}`)
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
if err != nil {
b.Fatalf("Failed to make request: %v", err)
}
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
request = request.WithContext(ctx)
request.Header.Set("Custom", "foobarbaz")
request.Header.Set("ShorterVal", "1")
repl := NewReplacer(request, recordRequest, "-")
// add some headers after creating replacer
request.Header.Set("CustomAdd", "caddy")
request.Header.Set("Cookie", "foo=bar; taste=delicious")
// add some respons headers
recordRequest.Header().Set("Custom", "CustomResponseHeader")
now = func() time.Time {
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
repl.Replace("This hostname is {hostname}")
}
}
func BenchmarkReplaceEscaped(b *testing.B) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
reader := strings.NewReader(`{"username": "dennis"}`)
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
if err != nil {
b.Fatalf("Failed to make request: %v", err)
}
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
request = request.WithContext(ctx)
request.Header.Set("Custom", "foobarbaz")
request.Header.Set("ShorterVal", "1")
repl := NewReplacer(request, recordRequest, "-")
// add some headers after creating replacer
request.Header.Set("CustomAdd", "caddy")
request.Header.Set("Cookie", "foo=bar; taste=delicious")
// add some respons headers
recordRequest.Header().Set("Custom", "CustomResponseHeader")
now = func() time.Time {
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
repl.Replace("\\{ 'hostname': '{hostname}' \\}")
}
}
func TestResponseRecorderNil(t *testing.T) { func TestResponseRecorderNil(t *testing.T) {
reader := strings.NewReader(`{"username": "dennis"}`) reader := strings.NewReader(`{"username": "dennis"}`)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment