Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 01:00 UTC
| 1 | // Copyright 2015 Matthew Holt and The Caddy Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package templates |
| 16 | |
| 17 | import ( |
| 18 | "bytes" |
| 19 | "errors" |
| 20 | "fmt" |
| 21 | "io" |
| 22 | "io/fs" |
| 23 | "net" |
| 24 | "net/http" |
| 25 | "net/url" |
| 26 | "os" |
| 27 | "path" |
| 28 | "reflect" |
| 29 | "strconv" |
| 30 | "strings" |
| 31 | "sync" |
| 32 | "text/template" |
| 33 | "time" |
| 34 | |
| 35 | "github.com/Masterminds/sprig/v3" |
| 36 | chromahtml "github.com/alecthomas/chroma/v2/formatters/html" |
| 37 | "github.com/dustin/go-humanize" |
| 38 | "github.com/yuin/goldmark" |
| 39 | highlighting "github.com/yuin/goldmark-highlighting/v2" |
| 40 | "github.com/yuin/goldmark/extension" |
| 41 | "github.com/yuin/goldmark/parser" |
| 42 | gmhtml "github.com/yuin/goldmark/renderer/html" |
| 43 | "go.uber.org/zap" |
| 44 | |
| 45 | "github.com/caddyserver/caddy/v2" |
| 46 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 47 | ) |
| 48 | |
| 49 | // TemplateContext is the TemplateContext with which HTTP templates are executed. |
| 50 | type TemplateContext struct { |
| 51 | Root http.FileSystem |
| 52 | Req *http.Request |
| 53 | Args []any // defined by arguments to funcInclude |
| 54 | RespHeader WrappedHeader |
| 55 | CustomFuncs []template.FuncMap // functions added by plugins |
| 56 | |
| 57 | config *Templates |
| 58 | tpl *template.Template |
| 59 | } |
| 60 | |
| 61 | // NewTemplate returns a new template intended to be evaluated with this |
| 62 | // context, as it is initialized with configuration from this context. |
| 63 | func (c *TemplateContext) NewTemplate(tplName string) *template.Template { |
| 64 | c.tpl = template.New(tplName).Option("missingkey=zero") |
| 65 | |
| 66 | // customize delimiters, if applicable |
| 67 | if c.config != nil && len(c.config.Delimiters) == 2 { |
| 68 | c.tpl.Delims(c.config.Delimiters[0], c.config.Delimiters[1]) |
| 69 | } |
| 70 | |
| 71 | // add sprig library |
| 72 | c.tpl.Funcs(sprigFuncMap) |
| 73 | |
| 74 | // add all custom functions |
| 75 | for _, funcMap := range c.CustomFuncs { |
| 76 | c.tpl.Funcs(funcMap) |
| 77 | } |
| 78 | |
| 79 | // add our own library |
| 80 | c.tpl.Funcs(template.FuncMap{ |
| 81 | "include": c.funcInclude, |
| 82 | "readFile": c.funcReadFile, |
| 83 | "import": c.funcImport, |
| 84 | "httpInclude": c.funcHTTPInclude, |
| 85 | "stripHTML": c.funcStripHTML, |
| 86 | "markdown": c.funcMarkdown, |
| 87 | "splitFrontMatter": c.funcSplitFrontMatter, |
| 88 | "listFiles": c.funcListFiles, |
| 89 | "fileStat": c.funcFileStat, |
| 90 | "env": c.funcEnv, |
| 91 | "placeholder": c.funcPlaceholder, |
| 92 | "ph": c.funcPlaceholder, // shortcut |
| 93 | "fileExists": c.funcFileExists, |
| 94 | "httpError": c.funcHTTPError, |
| 95 | "humanize": c.funcHumanize, |
| 96 | "maybe": c.funcMaybe, |
| 97 | "pathEscape": url.PathEscape, |
| 98 | }) |
| 99 | return c.tpl |
| 100 | } |
| 101 | |
| 102 | // OriginalReq returns the original, unmodified, un-rewritten request as |
| 103 | // it originally came in over the wire. |
| 104 | func (c TemplateContext) OriginalReq() http.Request { |
| 105 | or, _ := c.Req.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request) |
| 106 | return or |
| 107 | } |
| 108 | |
| 109 | // funcInclude returns the contents of filename relative to the site root and renders it in place. |
| 110 | // Note that included files are NOT escaped, so you should only include |
| 111 | // trusted files. If it is not trusted, be sure to use escaping functions |
| 112 | // in your template. |
| 113 | func (c TemplateContext) funcInclude(filename string, args ...any) (string, error) { |
| 114 | bodyBuf := bufPool.Get().(*bytes.Buffer) |
| 115 | bodyBuf.Reset() |
| 116 | defer bufPool.Put(bodyBuf) |
| 117 | |
| 118 | err := c.readFileToBuffer(filename, bodyBuf) |
| 119 | if err != nil { |
| 120 | return "", err |
| 121 | } |
| 122 | |
| 123 | c.Args = args |
| 124 | |
| 125 | err = c.executeTemplateInBuffer(filename, bodyBuf) |
| 126 | if err != nil { |
| 127 | return "", err |
| 128 | } |
| 129 | |
| 130 | return bodyBuf.String(), nil |
| 131 | } |
| 132 | |
| 133 | // funcReadFile returns the contents of a filename relative to the site root. |
| 134 | // Note that included files are NOT escaped, so you should only include |
| 135 | // trusted files. If it is not trusted, be sure to use escaping functions |
| 136 | // in your template. |
| 137 | func (c TemplateContext) funcReadFile(filename string) (string, error) { |
| 138 | bodyBuf := bufPool.Get().(*bytes.Buffer) |
| 139 | bodyBuf.Reset() |
| 140 | defer bufPool.Put(bodyBuf) |
| 141 | |
| 142 | err := c.readFileToBuffer(filename, bodyBuf) |
| 143 | if err != nil { |
| 144 | return "", err |
| 145 | } |
| 146 | |
| 147 | return bodyBuf.String(), nil |
| 148 | } |
| 149 | |
| 150 | // readFileToBuffer reads a file into a buffer |
| 151 | func (c TemplateContext) readFileToBuffer(filename string, bodyBuf *bytes.Buffer) error { |
| 152 | if c.Root == nil { |
| 153 | return fmt.Errorf("root file system not specified") |
| 154 | } |
| 155 | |
| 156 | file, err := c.Root.Open(filename) |
| 157 | if err != nil { |
| 158 | return err |
| 159 | } |
| 160 | defer file.Close() |
| 161 | |
| 162 | _, err = io.Copy(bodyBuf, file) |
| 163 | if err != nil { |
| 164 | return err |
| 165 | } |
| 166 | |
| 167 | return nil |
| 168 | } |
| 169 | |
| 170 | // funcHTTPInclude returns the body of a virtual (lightweight) request |
| 171 | // to the given URI on the same server. Note that included bodies |
| 172 | // are NOT escaped, so you should only include trusted resources. |
| 173 | // If it is not trusted, be sure to use escaping functions yourself. |
| 174 | func (c TemplateContext) funcHTTPInclude(uri string) (string, error) { |
| 175 | // prevent virtual request loops by counting how many levels |
| 176 | // deep we are; and if we get too deep, return an error |
| 177 | recursionCount := 1 |
| 178 | if numStr := c.Req.Header.Get(recursionPreventionHeader); numStr != "" { |
| 179 | num, err := strconv.Atoi(numStr) |
| 180 | if err != nil { |
| 181 | return "", fmt.Errorf("parsing %s: %v", recursionPreventionHeader, err) |
| 182 | } |
| 183 | if num >= 3 { |
| 184 | return "", fmt.Errorf("virtual request cycle") |
| 185 | } |
| 186 | recursionCount = num + 1 |
| 187 | } |
| 188 | |
| 189 | buf := bufPool.Get().(*bytes.Buffer) |
| 190 | buf.Reset() |
| 191 | defer bufPool.Put(buf) |
| 192 | |
| 193 | virtReq, err := http.NewRequest("GET", uri, nil) |
| 194 | if err != nil { |
| 195 | return "", err |
| 196 | } |
| 197 | virtReq.Host = c.Req.Host |
| 198 | virtReq.RemoteAddr = "127.0.0.1:10000" // https://github.com/caddyserver/caddy/issues/5835 |
| 199 | virtReq.Header = c.Req.Header.Clone() |
| 200 | virtReq.Header.Set("Accept-Encoding", "identity") // https://github.com/caddyserver/caddy/issues/4352 |
| 201 | virtReq.Trailer = c.Req.Trailer.Clone() |
| 202 | virtReq.Header.Set(recursionPreventionHeader, strconv.Itoa(recursionCount)) |
| 203 | |
| 204 | vrw := &virtualResponseWriter{body: buf, header: make(http.Header)} |
| 205 | server := c.Req.Context().Value(caddyhttp.ServerCtxKey).(http.Handler) |
| 206 | |
| 207 | server.ServeHTTP(vrw, virtReq) |
| 208 | if vrw.status >= 400 { |
| 209 | return "", fmt.Errorf("http %d", vrw.status) |
| 210 | } |
| 211 | |
| 212 | err = c.executeTemplateInBuffer(uri, buf) |
| 213 | if err != nil { |
| 214 | return "", err |
| 215 | } |
| 216 | |
| 217 | return buf.String(), nil |
| 218 | } |
| 219 | |
| 220 | // funcImport parses the filename into the current template stack. The imported |
| 221 | // file will be rendered within the current template by calling {{ block }} or |
| 222 | // {{ template }} from the standard template library. If the imported file has |
| 223 | // no {{ define }} blocks, the name of the import will be the path |
| 224 | func (c *TemplateContext) funcImport(filename string) (string, error) { |
| 225 | bodyBuf := bufPool.Get().(*bytes.Buffer) |
| 226 | bodyBuf.Reset() |
| 227 | defer bufPool.Put(bodyBuf) |
| 228 | |
| 229 | err := c.readFileToBuffer(filename, bodyBuf) |
| 230 | if err != nil { |
| 231 | return "", err |
| 232 | } |
| 233 | |
| 234 | _, err = c.tpl.Parse(bodyBuf.String()) |
| 235 | if err != nil { |
| 236 | return "", err |
| 237 | } |
| 238 | return "", nil |
| 239 | } |
| 240 | |
| 241 | func (c *TemplateContext) executeTemplateInBuffer(tplName string, buf *bytes.Buffer) error { |
| 242 | c.NewTemplate(tplName) |
| 243 | |
| 244 | _, err := c.tpl.Parse(buf.String()) |
| 245 | if err != nil { |
| 246 | return err |
| 247 | } |
| 248 | |
| 249 | buf.Reset() // reuse buffer for output |
| 250 | |
| 251 | return c.tpl.Execute(buf, c) |
| 252 | } |
| 253 | |
| 254 | func (c TemplateContext) funcPlaceholder(name string) (string, error) { |
| 255 | repl := c.Req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 256 | |
| 257 | // For safety, we don't want to allow the file placeholder in |
| 258 | // templates because it could be used to read arbitrary files |
| 259 | // if the template contents were not trusted. |
| 260 | repl = repl.WithoutFile() |
| 261 | |
| 262 | value, _ := repl.Get(name) |
| 263 | |
| 264 | // propagate the request-body limit marker (a 413 from the request_body |
| 265 | // max_size limit when {http.request.body} is truncated) so the template |
| 266 | // aborts with that status rather than rendering a silently-truncated |
| 267 | // body; the marker is wrapped in a status-carrying handler error so the |
| 268 | // server renders the 413, and any other value, including unrelated |
| 269 | // errors, is converted to a string as before |
| 270 | if err, ok := value.(error); ok { |
| 271 | if bodyLimit, ok := errors.AsType[caddyhttp.RequestBodyLimitError](err); ok { |
| 272 | return "", caddyhttp.HandlerError{Err: bodyLimit, StatusCode: bodyLimit.StatusCode()} |
| 273 | } |
| 274 | } |
| 275 | return caddy.ToString(value), nil |
| 276 | } |
| 277 | |
| 278 | func (TemplateContext) funcEnv(varName string) string { |
| 279 | return os.Getenv(varName) |
| 280 | } |
| 281 | |
| 282 | // Cookie gets the value of a cookie with name. |
| 283 | func (c TemplateContext) Cookie(name string) string { |
| 284 | cookies := c.Req.Cookies() |
| 285 | for _, cookie := range cookies { |
| 286 | if cookie.Name == name { |
| 287 | return cookie.Value |
| 288 | } |
| 289 | } |
| 290 | return "" |
| 291 | } |
| 292 | |
| 293 | // RemoteIP gets the IP address of the connection's remote IP. |
| 294 | func (c TemplateContext) RemoteIP() string { |
| 295 | ip, _, err := net.SplitHostPort(c.Req.RemoteAddr) |
| 296 | if err != nil { |
| 297 | return c.Req.RemoteAddr |
| 298 | } |
| 299 | return ip |
| 300 | } |
| 301 | |
| 302 | // ClientIP gets the IP address of the real client making the request |
| 303 | // if the request is trusted (see trusted_proxies), otherwise returns |
| 304 | // the connection's remote IP. |
| 305 | func (c TemplateContext) ClientIP() string { |
| 306 | address := caddyhttp.GetVar(c.Req.Context(), caddyhttp.ClientIPVarKey).(string) |
| 307 | clientIP, _, err := net.SplitHostPort(address) |
| 308 | if err != nil { |
| 309 | clientIP = address // no port |
| 310 | } |
| 311 | return clientIP |
| 312 | } |
| 313 | |
| 314 | // Host returns the hostname portion of the Host header |
| 315 | // from the HTTP request. |
| 316 | func (c TemplateContext) Host() (string, error) { |
| 317 | host, _, err := net.SplitHostPort(c.Req.Host) |
| 318 | if err != nil { |
| 319 | if !strings.Contains(c.Req.Host, ":") { |
| 320 | // common with sites served on the default port 80 |
| 321 | return c.Req.Host, nil |
| 322 | } |
| 323 | return "", err |
| 324 | } |
| 325 | return host, nil |
| 326 | } |
| 327 | |
| 328 | // funcStripHTML returns s without HTML tags. Similar to PHP's strip_tags() |
| 329 | func (TemplateContext) funcStripHTML(s string) string { |
| 330 | var buf bytes.Buffer |
| 331 | depth := 0 |
| 332 | var quoteChar rune |
| 333 | for _, ch := range s { |
| 334 | switch { |
| 335 | case depth > 0 && quoteChar == 0 && (ch == '"' || ch == '\''): |
| 336 | // entering a quoted attribute value |
| 337 | quoteChar = ch |
| 338 | case depth > 0 && ch == quoteChar: |
| 339 | // leaving a quoted attribute value |
| 340 | quoteChar = 0 |
| 341 | case ch == '<' && quoteChar == 0: |
| 342 | depth++ |
| 343 | case ch == '>' && quoteChar == 0: |
| 344 | if depth > 0 { |
| 345 | depth-- |
| 346 | } else { |
| 347 | buf.WriteRune(ch) // stray '>' with no opening '<', keep it |
| 348 | } |
| 349 | default: |
| 350 | if depth == 0 { |
| 351 | buf.WriteRune(ch) |
| 352 | } |
| 353 | } |
| 354 | } |
| 355 | return buf.String() |
| 356 | } |
| 357 | |
| 358 | // funcMarkdown renders the markdown body as HTML. The resulting |
| 359 | // HTML is NOT escaped so that it can be rendered as HTML. |
| 360 | func (TemplateContext) funcMarkdown(input any) (string, error) { |
| 361 | inputStr := caddy.ToString(input) |
| 362 | |
| 363 | md := goldmark.New( |
| 364 | goldmark.WithExtensions( |
| 365 | extension.GFM, |
| 366 | extension.Footnote, |
| 367 | highlighting.NewHighlighting( |
| 368 | highlighting.WithFormatOptions( |
| 369 | chromahtml.WithClasses(true), |
| 370 | ), |
| 371 | ), |
| 372 | ), |
| 373 | goldmark.WithParserOptions( |
| 374 | parser.WithAutoHeadingID(), |
| 375 | ), |
| 376 | goldmark.WithRendererOptions( |
| 377 | gmhtml.WithUnsafe(), // TODO: this is not awesome, maybe should be configurable? |
| 378 | ), |
| 379 | ) |
| 380 | |
| 381 | buf := bufPool.Get().(*bytes.Buffer) |
| 382 | buf.Reset() |
| 383 | defer bufPool.Put(buf) |
| 384 | |
| 385 | err := md.Convert([]byte(inputStr), buf) |
| 386 | if err != nil { |
| 387 | return "", err |
| 388 | } |
| 389 | |
| 390 | return buf.String(), nil |
| 391 | } |
| 392 | |
| 393 | // funcSplitFrontMatter parses front matter out from the beginning of input, |
| 394 | // and returns the separated key-value pairs and the body/content. input |
| 395 | // must be a "stringy" value. |
| 396 | func (TemplateContext) funcSplitFrontMatter(input any) (parsedMarkdownDoc, error) { |
| 397 | meta, body, err := extractFrontMatter(caddy.ToString(input)) |
| 398 | if err != nil { |
| 399 | return parsedMarkdownDoc{}, err |
| 400 | } |
| 401 | return parsedMarkdownDoc{Meta: meta, Body: body}, nil |
| 402 | } |
| 403 | |
| 404 | // funcListFiles reads and returns a slice of names from the given |
| 405 | // directory relative to the root of c. |
| 406 | func (c TemplateContext) funcListFiles(name string) ([]string, error) { |
| 407 | if c.Root == nil { |
| 408 | return nil, fmt.Errorf("root file system not specified") |
| 409 | } |
| 410 | |
| 411 | dir, err := c.Root.Open(path.Clean(name)) |
| 412 | if err != nil { |
| 413 | return nil, err |
| 414 | } |
| 415 | defer dir.Close() |
| 416 | |
| 417 | stat, err := dir.Stat() |
| 418 | if err != nil { |
| 419 | return nil, err |
| 420 | } |
| 421 | if !stat.IsDir() { |
| 422 | return nil, fmt.Errorf("%v is not a directory", name) |
| 423 | } |
| 424 | |
| 425 | dirInfo, err := dir.Readdir(0) |
| 426 | if err != nil { |
| 427 | return nil, err |
| 428 | } |
| 429 | |
| 430 | names := make([]string, len(dirInfo)) |
| 431 | for i, fileInfo := range dirInfo { |
| 432 | names[i] = fileInfo.Name() |
| 433 | } |
| 434 | |
| 435 | return names, nil |
| 436 | } |
| 437 | |
| 438 | // funcFileExists returns true if filename can be opened successfully. |
| 439 | func (c TemplateContext) funcFileExists(filename string) (bool, error) { |
| 440 | if c.Root == nil { |
| 441 | return false, fmt.Errorf("root file system not specified") |
| 442 | } |
| 443 | file, err := c.Root.Open(filename) |
| 444 | if err == nil { |
| 445 | file.Close() |
| 446 | return true, nil |
| 447 | } |
| 448 | return false, nil |
| 449 | } |
| 450 | |
| 451 | // funcFileStat returns Stat of a filename |
| 452 | func (c TemplateContext) funcFileStat(filename string) (fs.FileInfo, error) { |
| 453 | if c.Root == nil { |
| 454 | return nil, fmt.Errorf("root file system not specified") |
| 455 | } |
| 456 | |
| 457 | file, err := c.Root.Open(path.Clean(filename)) |
| 458 | if err != nil { |
| 459 | return nil, err |
| 460 | } |
| 461 | defer file.Close() |
| 462 | |
| 463 | return file.Stat() |
| 464 | } |
| 465 | |
| 466 | // funcHTTPError returns a structured HTTP handler error. EXPERIMENTAL; SUBJECT TO CHANGE. |
| 467 | // Example usage: `{{if not (fileExists $includeFile)}}{{httpError 404}}{{end}}` |
| 468 | func (c TemplateContext) funcHTTPError(statusCode int) (bool, error) { |
| 469 | // Delete some headers that may have been set by the underlying |
| 470 | // handler (such as file_server) which may break the error response. |
| 471 | c.RespHeader.Header.Del("Content-Length") |
| 472 | c.RespHeader.Header.Del("Content-Type") |
| 473 | c.RespHeader.Header.Del("Etag") |
| 474 | c.RespHeader.Header.Del("Last-Modified") |
| 475 | c.RespHeader.Header.Del("Accept-Ranges") |
| 476 | c.RespHeader.Header.Del("Content-Encoding") |
| 477 | c.RespHeader.Header.Del("Content-Range") |
| 478 | |
| 479 | return false, caddyhttp.Error(statusCode, nil) |
| 480 | } |
| 481 | |
| 482 | // funcHumanize transforms size and time inputs to a human readable format. |
| 483 | // |
| 484 | // Size inputs are expected to be integers, and are formatted as a |
| 485 | // byte size, such as "83 MB". |
| 486 | // |
| 487 | // Time inputs are parsed using the given layout (default layout is RFC1123Z) |
| 488 | // and are formatted as a relative time, such as "2 weeks ago". |
| 489 | // See https://pkg.go.dev/time#pkg-constants for time layout docs. |
| 490 | func (c TemplateContext) funcHumanize(formatType, data string) (string, error) { |
| 491 | // The format type can optionally be followed |
| 492 | // by a colon to provide arguments for the format |
| 493 | parts := strings.SplitN(formatType, ":", 2) |
| 494 | |
| 495 | switch parts[0] { |
| 496 | case "size": |
| 497 | dataint, dataerr := strconv.ParseUint(data, 10, 64) |
| 498 | if dataerr != nil { |
| 499 | return "", fmt.Errorf("humanize: size cannot be parsed: %s", dataerr.Error()) |
| 500 | } |
| 501 | return humanize.Bytes(dataint), nil |
| 502 | |
| 503 | case "time": |
| 504 | timelayout := time.RFC1123Z |
| 505 | if len(parts) > 1 { |
| 506 | timelayout = parts[1] |
| 507 | } |
| 508 | |
| 509 | dataint, dataerr := time.Parse(timelayout, data) |
| 510 | if dataerr != nil { |
| 511 | return "", fmt.Errorf("humanize: time cannot be parsed: %s", dataerr.Error()) |
| 512 | } |
| 513 | return humanize.Time(dataint), nil |
| 514 | } |
| 515 | |
| 516 | return "", fmt.Errorf("no know function was given") |
| 517 | } |
| 518 | |
| 519 | // funcMaybe invokes the plugged-in function named functionName if it is plugged in |
| 520 | // (is a module in the 'http.handlers.templates.functions' namespace). If it is not |
| 521 | // available, a log message is emitted. |
| 522 | // |
| 523 | // The first argument is the function name, and the rest of the arguments are |
| 524 | // passed on to the actual function. |
| 525 | // |
| 526 | // This function is useful for executing templates that use components that may be |
| 527 | // considered as optional in some cases (like during local development) where you do |
| 528 | // not want to require everyone to have a custom Caddy build to be able to execute |
| 529 | // your template. |
| 530 | // |
| 531 | // NOTE: This function is EXPERIMENTAL and subject to change or removal. |
| 532 | func (c TemplateContext) funcMaybe(functionName string, args ...any) (any, error) { |
| 533 | for _, funcMap := range c.CustomFuncs { |
| 534 | if fn, ok := funcMap[functionName]; ok { |
| 535 | val := reflect.ValueOf(fn) |
| 536 | if val.Kind() != reflect.Func { |
| 537 | continue |
| 538 | } |
| 539 | argVals := make([]reflect.Value, len(args)) |
| 540 | for i, arg := range args { |
| 541 | argVals[i] = reflect.ValueOf(arg) |
| 542 | } |
| 543 | returnVals := val.Call(argVals) |
| 544 | switch len(returnVals) { |
| 545 | case 0: |
| 546 | return "", nil |
| 547 | case 1: |
| 548 | return returnVals[0].Interface(), nil |
| 549 | case 2: |
| 550 | var err error |
| 551 | if !returnVals[1].IsNil() { |
| 552 | err = returnVals[1].Interface().(error) |
| 553 | } |
| 554 | return returnVals[0].Interface(), err |
| 555 | default: |
| 556 | return nil, fmt.Errorf("maybe %s: invalid number of return values: %d", functionName, len(returnVals)) |
| 557 | } |
| 558 | } |
| 559 | } |
| 560 | c.config.logger.Named("maybe").Warn("template function could not be found; ignoring invocation", zap.String("name", functionName)) |
| 561 | return "", nil |
| 562 | } |
| 563 | |
| 564 | // WrappedHeader wraps niladic functions so that they |
| 565 | // can be used in templates. (Template functions must |
| 566 | // return a value.) |
| 567 | type WrappedHeader struct{ http.Header } |
| 568 | |
| 569 | // Add adds a header field value, appending val to |
| 570 | // existing values for that field. It returns an |
| 571 | // empty string. |
| 572 | func (h WrappedHeader) Add(field, val string) string { |
| 573 | h.Header.Add(field, val) |
| 574 | return "" |
| 575 | } |
| 576 | |
| 577 | // Set sets a header field value, overwriting any |
| 578 | // other values for that field. It returns an |
| 579 | // empty string. |
| 580 | func (h WrappedHeader) Set(field, val string) string { |
| 581 | h.Header.Set(field, val) |
| 582 | return "" |
| 583 | } |
| 584 | |
| 585 | // Del deletes a header field. It returns an empty string. |
| 586 | func (h WrappedHeader) Del(field string) string { |
| 587 | h.Header.Del(field) |
| 588 | return "" |
| 589 | } |
| 590 | |
| 591 | var bufPool = sync.Pool{ |
| 592 | New: func() any { |
| 593 | return new(bytes.Buffer) |
| 594 | }, |
| 595 | } |
| 596 | |
| 597 | // at time of writing, sprig.FuncMap() makes a copy, thus |
| 598 | // involves iterating the whole map, so do it just once |
| 599 | var sprigFuncMap = sprig.TxtFuncMap() |
| 600 | |
| 601 | const recursionPreventionHeader = "Caddy-Templates-Include" |