summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/templates/templates.go
blob: 56c3b6626af3592dd7d52ea633dd5db6408954f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package templates

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"text/template"

	"github.com/caddyserver/caddy"
	"github.com/caddyserver/caddy/modules/caddyhttp"
)

func init() {
	caddy.RegisterModule(caddy.Module{
		Name: "http.middleware.templates",
		New:  func() interface{} { return new(Templates) },
	})
}

// Templates is a middleware which execute response bodies as templates.
type Templates struct {
	FileRoot   string   `json:"file_root,omitempty"`
	Delimiters []string `json:"delimiters,omitempty"`
}

// Validate ensures t has a valid configuration.
func (t *Templates) Validate() error {
	if len(t.Delimiters) != 0 && len(t.Delimiters) != 2 {
		return fmt.Errorf("delimiters must consist of exactly two elements: opening and closing")
	}
	return nil
}

func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	buf := bufPool.Get().(*bytes.Buffer)
	buf.Reset()
	defer bufPool.Put(buf)

	wb := &responseBuffer{
		ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
		buf:                   buf,
	}

	err := next.ServeHTTP(wb, r)
	if err != nil {
		return err
	}

	err = t.executeTemplate(wb, r)
	if err != nil {
		return err
	}

	w.Header().Set("Content-Length", strconv.Itoa(wb.buf.Len()))
	w.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content
	w.Header().Del("Etag")          // don't know a way to quickly generate etag for dynamic content
	w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing

	w.WriteHeader(wb.statusCode)
	io.Copy(w, wb.buf)

	return nil
}

// executeTemplate executes the template contianed
// in wb.buf and replaces it with the results.
func (t *Templates) executeTemplate(wb *responseBuffer, r *http.Request) error {
	tpl := template.New(r.URL.Path)

	if len(t.Delimiters) == 2 {
		tpl.Delims(t.Delimiters[0], t.Delimiters[1])
	}

	parsedTpl, err := tpl.Parse(wb.buf.String())
	if err != nil {
		return caddyhttp.Error(http.StatusInternalServerError, err)
	}

	var fs http.FileSystem
	if t.FileRoot != "" {
		fs = http.Dir(t.FileRoot)
	}
	ctx := &templateContext{
		Root:       fs,
		Req:        r,
		RespHeader: tplWrappedHeader{wb.Header()},
	}

	wb.buf.Reset() // reuse buffer for output
	err = parsedTpl.Execute(wb.buf, ctx)
	if err != nil {
		return caddyhttp.Error(http.StatusInternalServerError, err)
	}

	return nil
}

// responseBuffer buffers the response so that it can be
// executed as a template.
type responseBuffer struct {
	*caddyhttp.ResponseWriterWrapper
	wroteHeader bool
	statusCode  int
	buf         *bytes.Buffer
}

func (rb *responseBuffer) WriteHeader(statusCode int) {
	if rb.wroteHeader {
		return
	}
	rb.statusCode = statusCode
	rb.wroteHeader = true
}

func (rb *responseBuffer) Write(data []byte) (int, error) {
	rb.WriteHeader(http.StatusOK)
	return rb.buf.Write(data)
}

// virtualResponseWriter is used in virtualized HTTP requests.
type virtualResponseWriter struct {
	status int
	header http.Header
	body   *bytes.Buffer
}

func (vrw *virtualResponseWriter) Header() http.Header {
	return vrw.header
}

func (vrw *virtualResponseWriter) WriteHeader(statusCode int) {
	vrw.status = statusCode
}

func (vrw *virtualResponseWriter) Write(data []byte) (int, error) {
	return vrw.body.Write(data)
}

// Interface guards
var (
	_ caddy.Validator             = (*Templates)(nil)
	_ caddyhttp.MiddlewareHandler = (*Templates)(nil)
	_ caddyhttp.HTTPInterfaces    = (*responseBuffer)(nil)
)