map: Coerce val to string, fix #4987

Also prevent infinite recursion, and enforce placeholder syntax.
This commit is contained in:
Matthew Holt 2022-09-01 21:15:20 -06:00
parent 7d5108d132
commit 73d4a8ba02
5 changed files with 38 additions and 35 deletions

View File

@ -146,15 +146,15 @@ func (d *Dispenser) NextLine() bool {
// //
// Proper use of this method looks like this: // Proper use of this method looks like this:
// //
// for nesting := d.Nesting(); d.NextBlock(nesting); { // for nesting := d.Nesting(); d.NextBlock(nesting); {
// } // }
// //
// However, in simple cases where it is known that the // However, in simple cases where it is known that the
// Dispenser is new and has not already traversed state // Dispenser is new and has not already traversed state
// by a loop over NextBlock(), this will do: // by a loop over NextBlock(), this will do:
// //
// for d.NextBlock(0) { // for d.NextBlock(0) {
// } // }
// //
// As with other token parsing logic, a loop over // As with other token parsing logic, a loop over
// NextBlock() should be contained within a loop over // NextBlock() should be contained within a loop over

View File

@ -27,10 +27,10 @@ func init() {
// parseCaddyfile sets up the map handler from Caddyfile tokens. Syntax: // parseCaddyfile sets up the map handler from Caddyfile tokens. Syntax:
// //
// map [<matcher>] <source> <destinations...> { // map [<matcher>] <source> <destinations...> {
// [~]<input> <outputs...> // [~]<input> <outputs...>
// default <defaults...> // default <defaults...>
// } // }
// //
// If the input value is prefixed with a tilde (~), then the input will be parsed as a // If the input value is prefixed with a tilde (~), then the input will be parsed as a
// regular expression. // regular expression.
@ -76,7 +76,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
continue continue
} }
// every other line maps one input to one or more outputs // every line maps an input value to one or more outputs
in := h.Val() in := h.Val()
var outs []any var outs []any
for h.NextArg() { for h.NextArg() {

View File

@ -62,6 +62,9 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
// Provision sets up h. // Provision sets up h.
func (h *Handler) Provision(_ caddy.Context) error { func (h *Handler) Provision(_ caddy.Context) error {
for j, dest := range h.Destinations { for j, dest := range h.Destinations {
if strings.Count(dest, "{") != 1 || !strings.HasPrefix(dest, "{") {
return fmt.Errorf("destination must be a placeholder and only a placeholder")
}
h.Destinations[j] = strings.Trim(dest, "{}") h.Destinations[j] = strings.Trim(dest, "{}")
} }
@ -106,6 +109,16 @@ func (h *Handler) Validate() error {
} }
seen[input] = i seen[input] = i
// prevent infinite recursion
for _, out := range m.Outputs {
for _, dest := range h.Destinations {
if strings.Contains(caddy.ToString(out), dest) ||
strings.Contains(m.Input, dest) {
return fmt.Errorf("mapping %d requires value of {%s} to define value of {%s}: infinite recursion", i, dest, dest)
}
}
}
// ensure mappings have 1:1 output-to-destination correspondence // ensure mappings have 1:1 output-to-destination correspondence
nOut := len(m.Outputs) nOut := len(m.Outputs)
if nOut != nDest { if nOut != nDest {
@ -135,21 +148,22 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt
if output == nil { if output == nil {
continue continue
} }
outputStr := caddy.ToString(output)
// evaluate regular expression if configured
if m.re != nil { if m.re != nil {
var result []byte var result []byte
matches := m.re.FindStringSubmatchIndex(input) matches := m.re.FindStringSubmatchIndex(input)
if matches == nil { if matches == nil {
continue continue
} }
result = m.re.ExpandString(result, output.(string), input, matches) result = m.re.ExpandString(result, outputStr, input, matches)
return string(result), true return string(result), true
} }
// otherwise simple string comparison
if input == m.Input { if input == m.Input {
if outputStr, ok := output.(string); ok { return repl.ReplaceAll(outputStr, ""), true
// NOTE: if the output has a placeholder that has the same key as the input, this is infinite recursion
return repl.ReplaceAll(outputStr, ""), true
}
return output, true
} }
} }

View File

@ -305,7 +305,7 @@ func (TemplateContext) funcStripHTML(s string) string {
// funcMarkdown renders the markdown body as HTML. The resulting // funcMarkdown renders the markdown body as HTML. The resulting
// HTML is NOT escaped so that it can be rendered as HTML. // HTML is NOT escaped so that it can be rendered as HTML.
func (TemplateContext) funcMarkdown(input any) (string, error) { func (TemplateContext) funcMarkdown(input any) (string, error) {
inputStr := toString(input) inputStr := caddy.ToString(input)
md := goldmark.New( md := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
@ -341,7 +341,7 @@ func (TemplateContext) funcMarkdown(input any) (string, error) {
// and returns the separated key-value pairs and the body/content. input // and returns the separated key-value pairs and the body/content. input
// must be a "stringy" value. // must be a "stringy" value.
func (TemplateContext) funcSplitFrontMatter(input any) (parsedMarkdownDoc, error) { func (TemplateContext) funcSplitFrontMatter(input any) (parsedMarkdownDoc, error) {
meta, body, err := extractFrontMatter(toString(input)) meta, body, err := extractFrontMatter(caddy.ToString(input))
if err != nil { if err != nil {
return parsedMarkdownDoc{}, err return parsedMarkdownDoc{}, err
} }
@ -465,19 +465,6 @@ func (h WrappedHeader) Del(field string) string {
return "" return ""
} }
func toString(input any) string {
switch v := input.(type) {
case string:
return v
case fmt.Stringer:
return v.String()
case error:
return v.Error()
default:
return fmt.Sprintf("%v", input)
}
}
var bufPool = sync.Pool{ var bufPool = sync.Pool{
New: func() any { New: func() any {
return new(bytes.Buffer) return new(bytes.Buffer)

View File

@ -78,11 +78,11 @@ func (r *Replacer) Get(variable string) (any, bool) {
return nil, false return nil, false
} }
// GetString is the same as Get, but coerces the value to a // GetString is the same as Get, but coerces the value to a
// string representation. // string representation as efficiently as possible.
func (r *Replacer) GetString(variable string) (string, bool) { func (r *Replacer) GetString(variable string) (string, bool) {
s, found := r.Get(variable) s, found := r.Get(variable)
return toString(s), found return ToString(s), found
} }
// Delete removes a variable with a static value // Delete removes a variable with a static value
@ -204,7 +204,7 @@ scan:
} }
// convert val to a string as efficiently as possible // convert val to a string as efficiently as possible
valStr := toString(val) valStr := ToString(val)
// write the value; if it's empty, either return // write the value; if it's empty, either return
// an error or write a default value // an error or write a default value
@ -230,7 +230,9 @@ scan:
return sb.String(), nil return sb.String(), nil
} }
func toString(val any) string { // ToString returns val as a string, as efficiently as possible.
// EXPERIMENTAL: may be changed or removed later.
func ToString(val any) string {
switch v := val.(type) { switch v := val.(type) {
case nil: case nil:
return "" return ""