@marcel91200 / caddy Mirror · read-only
Fork 0

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

master
go 601 lines 17.2 KB
Raw
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"