diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go index b37d23290..91bd9a502 100644 --- a/caddyconfig/caddyfile/dispenser.go +++ b/caddyconfig/caddyfile/dispenser.go @@ -146,15 +146,15 @@ func (d *Dispenser) NextLine() bool { // // 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 // Dispenser is new and has not already traversed state // 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 // NextBlock() should be contained within a loop over diff --git a/modules/caddyhttp/map/caddyfile.go b/modules/caddyhttp/map/caddyfile.go index f38aff78b..9cc7d8c6d 100644 --- a/modules/caddyhttp/map/caddyfile.go +++ b/modules/caddyhttp/map/caddyfile.go @@ -27,10 +27,10 @@ func init() { // parseCaddyfile sets up the map handler from Caddyfile tokens. Syntax: // -// map [] { -// [~] -// default -// } +// map [] { +// [~] +// default +// } // // If the input value is prefixed with a tilde (~), then the input will be parsed as a // regular expression. @@ -76,7 +76,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) 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() var outs []any for h.NextArg() { diff --git a/modules/caddyhttp/map/map.go b/modules/caddyhttp/map/map.go index bbc1249d1..d41806d43 100644 --- a/modules/caddyhttp/map/map.go +++ b/modules/caddyhttp/map/map.go @@ -62,6 +62,9 @@ func (Handler) CaddyModule() caddy.ModuleInfo { // Provision sets up h. func (h *Handler) Provision(_ caddy.Context) error { 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, "{}") } @@ -106,6 +109,16 @@ func (h *Handler) Validate() error { } 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 nOut := len(m.Outputs) if nOut != nDest { @@ -135,21 +148,22 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt if output == nil { continue } + outputStr := caddy.ToString(output) + + // evaluate regular expression if configured if m.re != nil { var result []byte matches := m.re.FindStringSubmatchIndex(input) if matches == nil { continue } - result = m.re.ExpandString(result, output.(string), input, matches) + result = m.re.ExpandString(result, outputStr, input, matches) return string(result), true } + + // otherwise simple string comparison if input == m.Input { - if outputStr, ok := output.(string); ok { - // 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 + return repl.ReplaceAll(outputStr, ""), true } } diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index 96a341c8a..f681399c8 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -305,7 +305,7 @@ func (TemplateContext) funcStripHTML(s string) string { // funcMarkdown renders the markdown body as HTML. The resulting // HTML is NOT escaped so that it can be rendered as HTML. func (TemplateContext) funcMarkdown(input any) (string, error) { - inputStr := toString(input) + inputStr := caddy.ToString(input) md := goldmark.New( 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 // must be a "stringy" value. func (TemplateContext) funcSplitFrontMatter(input any) (parsedMarkdownDoc, error) { - meta, body, err := extractFrontMatter(toString(input)) + meta, body, err := extractFrontMatter(caddy.ToString(input)) if err != nil { return parsedMarkdownDoc{}, err } @@ -465,19 +465,6 @@ func (h WrappedHeader) Del(field string) string { 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{ New: func() any { return new(bytes.Buffer) diff --git a/replacer.go b/replacer.go index aa943cf73..d30a40616 100644 --- a/replacer.go +++ b/replacer.go @@ -78,11 +78,11 @@ func (r *Replacer) Get(variable string) (any, bool) { return nil, false } -// GetString is the same as Get, but coerces the value to a -// string representation. +// GetString is the same as Get, but coerces the value to a +// string representation as efficiently as possible. func (r *Replacer) GetString(variable string) (string, bool) { s, found := r.Get(variable) - return toString(s), found + return ToString(s), found } // Delete removes a variable with a static value @@ -204,7 +204,7 @@ scan: } // convert val to a string as efficiently as possible - valStr := toString(val) + valStr := ToString(val) // write the value; if it's empty, either return // an error or write a default value @@ -230,7 +230,9 @@ scan: 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) { case nil: return ""