@marcel91200 / caddy Mirror · read-only / commits / 6706c922
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 14, 05:15 UTC

Implement templates handler; various minor cleanups and bug fixes

Matthew Holt committed Jun 18, 2019 at 11:13 UTC 6706c9225a8dcb976785bdf2c35eb151d54ac18c
9 files changed +1006 -13
cmd/caddy/main.go renamed
+1
@@ -16,6 +16,7 @@ import (
16 _ "github.com/caddyserver/caddy/modules/caddyhttp/requestbody"
17 _ "github.com/caddyserver/caddy/modules/caddyhttp/reverseproxy"
18 _ "github.com/caddyserver/caddy/modules/caddyhttp/rewrite"
19 + _ "github.com/caddyserver/caddy/modules/caddyhttp/templates"
20 _ "github.com/caddyserver/caddy/modules/caddytls"
21 _ "github.com/caddyserver/caddy/modules/caddytls/standardstek"
22 )
modules/caddyhttp/caddyhttp.go
+1 -1
@@ -33,7 +33,7 @@ func init() {
33 type App struct {
34 HTTPPort int `json:"http_port,omitempty"`
35 HTTPSPort int `json:"https_port,omitempty"`
36 - GracePeriod caddy.Duration `json:"grace_period,omitempty"`
36 + GracePeriod caddy.Duration `json:"grace_period,omitempty"`
37 Servers map[string]*Server `json:"servers,omitempty"`
38
39 servers []*http.Server
modules/caddyhttp/encode/encode.go
+9 -3
@@ -148,7 +148,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
148 return n, err
149 }
150
151 -// init should be called once we know we are writing an encoded response.
151 +// init should be called before we write a response, if rw.buf is not nil.
152 func (rw *responseWriter) init() {
153 if rw.Header().Get("Content-Encoding") == "" && rw.buf.Len() >= rw.config.MinLength {
154 rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
@@ -164,7 +164,13 @@ func (rw *responseWriter) init() {
164 // deallocates any active resources.
165 func (rw *responseWriter) Close() error {
166 var err error
167 - if rw.buf != nil {
167 + // only attempt to write the remaining buffered response
168 + // if there are any bytes left to write; otherwise, if
169 + // the handler above us returned an error without writing
170 + // anything, we'd write to the response when we instead
171 + // should simply let the error propagate back down; this
172 + // is why the check for rw.buf.Len() > 0 is crucial
173 + if rw.buf != nil && rw.buf.Len() > 0 {
174 rw.init()
175 p := rw.buf.Bytes()
176 defer func() {
@@ -280,7 +286,7 @@ const defaultMinLength = 512
286
287 // Interface guards
288 var (
283 - _ caddy.Provisioner = (*Encode)(nil)
289 + _ caddy.Provisioner = (*Encode)(nil)
290 _ caddyhttp.MiddlewareHandler = (*Encode)(nil)
291 _ caddyhttp.HTTPInterfaces = (*responseWriter)(nil)
292 )
modules/caddyhttp/routes.go
+7 -3
@@ -186,17 +186,21 @@ type middlewareResponseWriter struct {
186
187 func (mrw middlewareResponseWriter) WriteHeader(statusCode int) {
188 if !mrw.allowWrites {
189 - panic("WriteHeader: middleware cannot write to the response")
189 + // technically, this is not true: middleware can write headers,
190 + // but only after the responder handler has returned; either the
191 + // responder did nothing with the response (sad face), or the
192 + // middleware wrapped the response and deferred the write
193 + panic("WriteHeader: middleware cannot write response headers")
194 }
195 mrw.ResponseWriterWrapper.WriteHeader(statusCode)
196 }
197
198 func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
199 if !mrw.allowWrites {
196 - panic("Write: middleware cannot write to the response")
200 + panic("Write: middleware cannot write to the response before responder")
201 }
202 return mrw.ResponseWriterWrapper.Write(b)
203 }
204
205 // Interface guard
202 -var _ HTTPInterfaces = middlewareResponseWriter{}
206 +var _ HTTPInterfaces = (*middlewareResponseWriter)(nil)
modules/caddyhttp/server.go
+8 -5
@@ -15,10 +15,10 @@ import (
15 // Server is an HTTP server.
16 type Server struct {
17 Listen []string `json:"listen,omitempty"`
18 - ReadTimeout caddy.Duration `json:"read_timeout,omitempty"`
19 - ReadHeaderTimeout caddy.Duration `json:"read_header_timeout,omitempty"`
20 - WriteTimeout caddy.Duration `json:"write_timeout,omitempty"`
21 - IdleTimeout caddy.Duration `json:"idle_timeout,omitempty"`
18 + ReadTimeout caddy.Duration `json:"read_timeout,omitempty"`
19 + ReadHeaderTimeout caddy.Duration `json:"read_header_timeout,omitempty"`
20 + WriteTimeout caddy.Duration `json:"write_timeout,omitempty"`
21 + IdleTimeout caddy.Duration `json:"idle_timeout,omitempty"`
22 MaxHeaderBytes int `json:"max_header_bytes,omitempty"`
23 Routes RouteList `json:"routes,omitempty"`
24 Errors *httpErrorConfig `json:"errors,omitempty"`
@@ -40,6 +40,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
40 // set up the context for the request
41 repl := caddy.NewReplacer()
42 ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
43 + ctx = context.WithValue(ctx, ServerCtxKey, s)
44 ctx = context.WithValue(ctx, TableCtxKey, make(map[string]interface{})) // TODO: Implement this
45 r = r.WithContext(ctx)
46
@@ -126,5 +127,7 @@ type httpErrorConfig struct {
127 // the logging configuration first.
128 }
129
129 -// TableCtxKey is the context key for the request's variable table.
130 +const ServerCtxKey caddy.CtxKey = "server"
131 +
132 +// TableCtxKey is the context key for the request's variable table. TODO: implement this
133 const TableCtxKey caddy.CtxKey = "table"
modules/caddyhttp/templates/templates.go new
+146
@@ -0,0 +1,146 @@
1 +package templates
2 +
3 +import (
4 + "bytes"
5 + "fmt"
6 + "io"
7 + "net/http"
8 + "strconv"
9 + "text/template"
10 +
11 + "github.com/caddyserver/caddy"
12 + "github.com/caddyserver/caddy/modules/caddyhttp"
13 +)
14 +
15 +func init() {
16 + caddy.RegisterModule(caddy.Module{
17 + Name: "http.middleware.templates",
18 + New: func() interface{} { return new(Templates) },
19 + })
20 +}
21 +
22 +// Templates is a middleware which execute response bodies as templates.
23 +type Templates struct {
24 + FileRoot string `json:"file_root,omitempty"`
25 + Delimiters []string `json:"delimiters,omitempty"`
26 +}
27 +
28 +// Validate ensures t has a valid configuration.
29 +func (t *Templates) Validate() error {
30 + if len(t.Delimiters) != 0 && len(t.Delimiters) != 2 {
31 + return fmt.Errorf("delimiters must consist of exactly two elements: opening and closing")
32 + }
33 + return nil
34 +}
35 +
36 +func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
37 + buf := bufPool.Get().(*bytes.Buffer)
38 + buf.Reset()
39 + defer bufPool.Put(buf)
40 +
41 + wb := &responseBuffer{
42 + ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
43 + buf: buf,
44 + }
45 +
46 + err := next.ServeHTTP(wb, r)
47 + if err != nil {
48 + return err
49 + }
50 +
51 + err = t.executeTemplate(wb, r)
52 + if err != nil {
53 + return err
54 + }
55 +
56 + w.Header().Set("Content-Length", strconv.Itoa(wb.buf.Len()))
57 + w.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content
58 + w.Header().Del("Etag") // don't know a way to quickly generate etag for dynamic content
59 + w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
60 +
61 + w.WriteHeader(wb.statusCode)
62 + io.Copy(w, wb.buf)
63 +
64 + return nil
65 +}
66 +
67 +// executeTemplate executes the template contianed
68 +// in wb.buf and replaces it with the results.
69 +func (t *Templates) executeTemplate(wb *responseBuffer, r *http.Request) error {
70 + tpl := template.New(r.URL.Path)
71 +
72 + if len(t.Delimiters) == 2 {
73 + tpl.Delims(t.Delimiters[0], t.Delimiters[1])
74 + }
75 +
76 + parsedTpl, err := tpl.Parse(wb.buf.String())
77 + if err != nil {
78 + return caddyhttp.Error(http.StatusInternalServerError, err)
79 + }
80 +
81 + var fs http.FileSystem
82 + if t.FileRoot != "" {
83 + fs = http.Dir(t.FileRoot)
84 + }
85 + ctx := &templateContext{
86 + Root: fs,
87 + Req: r,
88 + RespHeader: tplWrappedHeader{wb.Header()},
89 + }
90 +
91 + wb.buf.Reset() // reuse buffer for output
92 + err = parsedTpl.Execute(wb.buf, ctx)
93 + if err != nil {
94 + return caddyhttp.Error(http.StatusInternalServerError, err)
95 + }
96 +
97 + return nil
98 +}
99 +
100 +// responseBuffer buffers the response so that it can be
101 +// executed as a template.
102 +type responseBuffer struct {
103 + *caddyhttp.ResponseWriterWrapper
104 + wroteHeader bool
105 + statusCode int
106 + buf *bytes.Buffer
107 +}
108 +
109 +func (rb *responseBuffer) WriteHeader(statusCode int) {
110 + if rb.wroteHeader {
111 + return
112 + }
113 + rb.statusCode = statusCode
114 + rb.wroteHeader = true
115 +}
116 +
117 +func (rb *responseBuffer) Write(data []byte) (int, error) {
118 + rb.WriteHeader(http.StatusOK)
119 + return rb.buf.Write(data)
120 +}
121 +
122 +// virtualResponseWriter is used in virtualized HTTP requests.
123 +type virtualResponseWriter struct {
124 + status int
125 + header http.Header
126 + body *bytes.Buffer
127 +}
128 +
129 +func (vrw *virtualResponseWriter) Header() http.Header {
130 + return vrw.header
131 +}
132 +
133 +func (vrw *virtualResponseWriter) WriteHeader(statusCode int) {
134 + vrw.status = statusCode
135 +}
136 +
137 +func (vrw *virtualResponseWriter) Write(data []byte) (int, error) {
138 + return vrw.body.Write(data)
139 +}
140 +
141 +// Interface guards
142 +var (
143 + _ caddy.Validator = (*Templates)(nil)
144 + _ caddyhttp.MiddlewareHandler = (*Templates)(nil)
145 + _ caddyhttp.HTTPInterfaces = (*responseBuffer)(nil)
146 +)
modules/caddyhttp/templates/tplcontext.go new
+413
@@ -0,0 +1,413 @@
1 +package templates
2 +
3 +import (
4 + "bytes"
5 + "crypto/rand"
6 + "fmt"
7 + "io"
8 + "log"
9 + weakrand "math/rand"
10 + "net"
11 + "net/http"
12 + "path"
13 + "strings"
14 + "sync"
15 + "text/template"
16 + "time"
17 +
18 + "os"
19 +
20 + "github.com/caddyserver/caddy/modules/caddyhttp"
21 + "gopkg.in/russross/blackfriday.v2"
22 +)
23 +
24 +// templateContext is the templateContext with which HTTP templates are executed.
25 +type templateContext struct {
26 + Root http.FileSystem
27 + Req *http.Request
28 + Args []interface{} // defined by arguments to .Include
29 + RespHeader tplWrappedHeader
30 + server http.Handler
31 +}
32 +
33 +// Include returns the contents of filename relative to the site root.
34 +func (c templateContext) Include(filename string, args ...interface{}) (string, error) {
35 + if c.Root == nil {
36 + return "", fmt.Errorf("root file system not specified")
37 + }
38 +
39 + file, err := c.Root.Open(filename)
40 + if err != nil {
41 + return "", err
42 + }
43 + defer file.Close()
44 +
45 + bodyBuf := bufPool.Get().(*bytes.Buffer)
46 + bodyBuf.Reset()
47 + defer bufPool.Put(bodyBuf)
48 +
49 + _, err = io.Copy(bodyBuf, file)
50 + if err != nil {
51 + return "", err
52 + }
53 +
54 + c.Args = args
55 +
56 + return c.executeTemplate(filename, bodyBuf.Bytes())
57 +}
58 +
59 +// HTTPInclude returns the body of a virtual (lightweight) request
60 +// to the given URI on the same server.
61 +func (c templateContext) HTTPInclude(uri string) (string, error) {
62 + if c.Req.Header.Get(recursionPreventionHeader) == "1" {
63 + return "", fmt.Errorf("virtual include cycle")
64 + }
65 +
66 + buf := bufPool.Get().(*bytes.Buffer)
67 + buf.Reset()
68 + defer bufPool.Put(buf)
69 +
70 + virtReq, err := http.NewRequest("GET", uri, nil)
71 + if err != nil {
72 + return "", err
73 + }
74 + virtReq.Header.Set(recursionPreventionHeader, "1")
75 +
76 + vrw := &virtualResponseWriter{body: buf, header: make(http.Header)}
77 + server := c.Req.Context().Value(caddyhttp.ServerCtxKey).(http.Handler)
78 +
79 + server.ServeHTTP(vrw, virtReq)
80 + if vrw.status >= 400 {
81 + return "", fmt.Errorf("http %d", vrw.status)
82 + }
83 +
84 + return c.executeTemplate(uri, buf.Bytes())
85 +}
86 +
87 +func (c templateContext) executeTemplate(tplName string, body []byte) (string, error) {
88 + tpl, err := template.New(tplName).Parse(string(body))
89 + if err != nil {
90 + return "", err
91 + }
92 +
93 + buf := bufPool.Get().(*bytes.Buffer)
94 + buf.Reset()
95 + defer bufPool.Put(buf)
96 +
97 + err = tpl.Execute(buf, c)
98 + if err != nil {
99 + return "", err
100 + }
101 +
102 + return buf.String(), nil
103 +}
104 +
105 +// Now returns the current timestamp.
106 +func (c templateContext) Now() time.Time {
107 + return time.Now()
108 +}
109 +
110 +// Cookie gets the value of a cookie with name name.
111 +func (c templateContext) Cookie(name string) string {
112 + cookies := c.Req.Cookies()
113 + for _, cookie := range cookies {
114 + if cookie.Name == name {
115 + return cookie.Value
116 + }
117 + }
118 + return ""
119 +}
120 +
121 +// ReqHeader gets the value of a request header with field name.
122 +func (c templateContext) ReqHeader(name string) string {
123 + return c.Req.Header.Get(name)
124 +}
125 +
126 +// Hostname gets the (remote) hostname of the client making the request.
127 +func (c templateContext) Hostname() string {
128 + ip := c.IP()
129 +
130 + hostnameList, err := net.LookupAddr(ip)
131 + if err != nil || len(hostnameList) == 0 {
132 + return c.Req.RemoteAddr
133 + }
134 +
135 + return hostnameList[0]
136 +}
137 +
138 +// Env gets a map of the environment variables.
139 +func (c templateContext) Env() map[string]string {
140 + osEnv := os.Environ()
141 + envVars := make(map[string]string, len(osEnv))
142 + for _, env := range osEnv {
143 + data := strings.SplitN(env, "=", 2)
144 + if len(data) == 2 && len(data[0]) > 0 {
145 + envVars[data[0]] = data[1]
146 + }
147 + }
148 + return envVars
149 +}
150 +
151 +// IP gets the (remote) IP address of the client making the request.
152 +func (c templateContext) IP() string {
153 + ip, _, err := net.SplitHostPort(c.Req.RemoteAddr)
154 + if err != nil {
155 + return c.Req.RemoteAddr
156 + }
157 + return ip
158 +}
159 +
160 +// Host returns the hostname portion of the Host header
161 +// from the HTTP request.
162 +func (c templateContext) Host() (string, error) {
163 + host, _, err := net.SplitHostPort(c.Req.Host)
164 + if err != nil {
165 + if !strings.Contains(c.Req.Host, ":") {
166 + // common with sites served on the default port 80
167 + return c.Req.Host, nil
168 + }
169 + return "", err
170 + }
171 + return host, nil
172 +}
173 +
174 +// Truncate truncates the input string to the given length.
175 +// If length is negative, it returns that many characters
176 +// starting from the end of the string. If the absolute value
177 +// of length is greater than len(input), the whole input is
178 +// returned.
179 +func (c templateContext) Truncate(input string, length int) string {
180 + if length < 0 && len(input)+length > 0 {
181 + return input[len(input)+length:]
182 + }
183 + if length >= 0 && len(input) > length {
184 + return input[:length]
185 + }
186 + return input
187 +}
188 +
189 +// StripHTML returns s without HTML tags. It is fairly naive
190 +// but works with most valid HTML inputs.
191 +func (c templateContext) StripHTML(s string) string {
192 + var buf bytes.Buffer
193 + var inTag, inQuotes bool
194 + var tagStart int
195 + for i, ch := range s {
196 + if inTag {
197 + if ch == '>' && !inQuotes {
198 + inTag = false
199 + } else if ch == '<' && !inQuotes {
200 + // false start
201 + buf.WriteString(s[tagStart:i])
202 + tagStart = i
203 + } else if ch == '"' {
204 + inQuotes = !inQuotes
205 + }
206 + continue
207 + }
208 + if ch == '<' {
209 + inTag = true
210 + tagStart = i
211 + continue
212 + }
213 + buf.WriteRune(ch)
214 + }
215 + if inTag {
216 + // false start
217 + buf.WriteString(s[tagStart:])
218 + }
219 + return buf.String()
220 +}
221 +
222 +// Markdown renders the markdown body as HTML.
223 +func (c templateContext) Markdown(body string) string {
224 + return string(blackfriday.Run([]byte(body)))
225 +}
226 +
227 +// Ext returns the suffix beginning at the final dot in the final
228 +// slash-separated element of the pathStr (or in other words, the
229 +// file extension).
230 +func (c templateContext) Ext(pathStr string) string {
231 + return path.Ext(pathStr)
232 +}
233 +
234 +// StripExt returns the input string without the extension,
235 +// which is the suffix starting with the final '.' character
236 +// but not before the final path separator ('/') character.
237 +// If there is no extension, the whole input is returned.
238 +func (c templateContext) StripExt(path string) string {
239 + for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
240 + if path[i] == '.' {
241 + return path[:i]
242 + }
243 + }
244 + return path
245 +}
246 +
247 +// Replace replaces instances of find in input with replacement.
248 +func (c templateContext) Replace(input, find, replacement string) string {
249 + return strings.Replace(input, find, replacement, -1)
250 +}
251 +
252 +// HasPrefix returns true if s starts with prefix.
253 +func (c templateContext) HasPrefix(s, prefix string) bool {
254 + return strings.HasPrefix(s, prefix)
255 +}
256 +
257 +// ToLower will convert the given string to lower case.
258 +func (c templateContext) ToLower(s string) string {
259 + return strings.ToLower(s)
260 +}
261 +
262 +// ToUpper will convert the given string to upper case.
263 +func (c templateContext) ToUpper(s string) string {
264 + return strings.ToUpper(s)
265 +}
266 +
267 +// Split is a pass-through to strings.Split. It will split
268 +// the first argument at each instance of the separator and
269 +// return a slice of strings.
270 +func (c templateContext) Split(s string, sep string) []string {
271 + return strings.Split(s, sep)
272 +}
273 +
274 +// Join is a pass-through to strings.Join. It will join the
275 +// first argument slice with the separator in the second
276 +// argument and return the result.
277 +func (c templateContext) Join(a []string, sep string) string {
278 + return strings.Join(a, sep)
279 +}
280 +
281 +// Slice will convert the given arguments into a slice.
282 +func (c templateContext) Slice(elems ...interface{}) []interface{} {
283 + return elems
284 +}
285 +
286 +// Dict will convert the arguments into a dictionary (map). It expects
287 +// alternating keys and values of string types. This is useful since you
288 +// cannot express map literals directly in Go templates.
289 +func (c templateContext) Dict(values ...interface{}) (map[string]interface{}, error) {
290 + if len(values)%2 != 0 {
291 + return nil, fmt.Errorf("expected even number of arguments")
292 + }
293 + dict := make(map[string]interface{}, len(values)/2)
294 + for i := 0; i < len(values); i += 2 {
295 + key, ok := values[i].(string)
296 + if !ok {
297 + return nil, fmt.Errorf("argument %d: map keys must be strings", i)
298 + }
299 + dict[key] = values[i+1]
300 + }
301 + return dict, nil
302 +}
303 +
304 +// ListFiles reads and returns a slice of names from the given
305 +// directory relative to the root of c.
306 +func (c templateContext) ListFiles(name string) ([]string, error) {
307 + if c.Root == nil {
308 + return nil, fmt.Errorf("root file system not specified")
309 + }
310 +
311 + dir, err := c.Root.Open(path.Clean(name))
312 + if err != nil {
313 + return nil, err
314 + }
315 + defer dir.Close()
316 +
317 + stat, err := dir.Stat()
318 + if err != nil {
319 + return nil, err
320 + }
321 + if !stat.IsDir() {
322 + return nil, fmt.Errorf("%v is not a directory", name)
323 + }
324 +
325 + dirInfo, err := dir.Readdir(0)
326 + if err != nil {
327 + return nil, err
328 + }
329 +
330 + names := make([]string, len(dirInfo))
331 + for i, fileInfo := range dirInfo {
332 + names[i] = fileInfo.Name()
333 + }
334 +
335 + return names, nil
336 +}
337 +
338 +// RandomString generates a random string of random length given
339 +// length bounds. Thanks to http://stackoverflow.com/a/35615565/1048862
340 +// for the clever technique that is fairly fast, secure, and maintains
341 +// proper distributions over the dictionary.
342 +func (c templateContext) RandomString(minLen, maxLen int) string {
343 + const (
344 + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
345 + letterIdxBits = 6 // 6 bits to represent 64 possibilities (indexes)
346 + letterIdxMask = 1<<letterIdxBits - 1 // all 1-bits, as many as letterIdxBits
347 + )
348 +
349 + if minLen < 0 || maxLen < 0 || maxLen < minLen {
350 + return ""
351 + }
352 +
353 + n := weakrand.Intn(maxLen-minLen+1) + minLen // choose actual length
354 +
355 + // secureRandomBytes returns a number of bytes using crypto/rand.
356 + secureRandomBytes := func(numBytes int) []byte {
357 + randomBytes := make([]byte, numBytes)
358 + if _, err := rand.Read(randomBytes); err != nil {
359 + // TODO: what to do with the logs (throughout whole file) (could return as error? might get rendered though...)
360 + log.Println("[ERROR] failed to read bytes: ", err)
361 + }
362 + return randomBytes
363 + }
364 +
365 + result := make([]byte, n)
366 + bufferSize := int(float64(n) * 1.3)
367 + for i, j, randomBytes := 0, 0, []byte{}; i < n; j++ {
368 + if j%bufferSize == 0 {
369 + randomBytes = secureRandomBytes(bufferSize)
370 + }
371 + if idx := int(randomBytes[j%n] & letterIdxMask); idx < len(letterBytes) {
372 + result[i] = letterBytes[idx]
373 + i++
374 + }
375 + }
376 +
377 + return string(result)
378 +}
379 +
380 +// tplWrappedHeader wraps niladic functions so that they
381 +// can be used in templates. (Template functions must
382 +// return a value.)
383 +type tplWrappedHeader struct{ http.Header }
384 +
385 +// Add adds a header field value, appending val to
386 +// existing values for that field. It returns an
387 +// empty string.
388 +func (h tplWrappedHeader) Add(field, val string) string {
389 + h.Header.Add(field, val)
390 + return ""
391 +}
392 +
393 +// Set sets a header field value, overwriting any
394 +// other values for that field. It returns an
395 +// empty string.
396 +func (h tplWrappedHeader) Set(field, val string) string {
397 + h.Header.Set(field, val)
398 + return ""
399 +}
400 +
401 +// Del deletes a header field. It returns an empty string.
402 +func (h tplWrappedHeader) Del(field string) string {
403 + h.Header.Del(field)
404 + return ""
405 +}
406 +
407 +var bufPool = sync.Pool{
408 + New: func() interface{} {
409 + return new(bytes.Buffer)
410 + },
411 +}
412 +
413 +const recursionPreventionHeader = "Caddy-Templates-Include"
modules/caddyhttp/templates/tplcontext_test.go new
+420
@@ -0,0 +1,420 @@
1 +// Copyright 2015 Light Code Labs, LLC
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 + "fmt"
20 + "io/ioutil"
21 + "net/http"
22 + "os"
23 + "path/filepath"
24 + "reflect"
25 + "sort"
26 + "strings"
27 + "testing"
28 + "time"
29 +)
30 +
31 +func TestMarkdown(t *testing.T) {
32 + context := getContextOrFail(t)
33 +
34 + for i, test := range []struct {
35 + body string
36 + expect string
37 + }{
38 + {
39 + body: "- str1\n- str2\n",
40 + expect: "<ul>\n<li>str1</li>\n<li>str2</li>\n</ul>\n",
41 + },
42 + } {
43 + result := context.Markdown(test.body)
44 + if result != test.expect {
45 + t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expect, result)
46 + }
47 + }
48 +}
49 +
50 +func TestCookie(t *testing.T) {
51 + for i, test := range []struct {
52 + cookie *http.Cookie
53 + cookieName string
54 + expect string
55 + }{
56 + {
57 + // happy path
58 + cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"},
59 + cookieName: "cookieName",
60 + expect: "cookieValue",
61 + },
62 + {
63 + // try to get a non-existing cookie
64 + cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"},
65 + cookieName: "notExisting",
66 + expect: "",
67 + },
68 + {
69 + // partial name match
70 + cookie: &http.Cookie{Name: "cookie", Value: "cookieValue"},
71 + cookieName: "cook",
72 + expect: "",
73 + },
74 + {
75 + // cookie with optional fields
76 + cookie: &http.Cookie{Name: "cookie", Value: "cookieValue", Path: "/path", Domain: "https://localhost", Expires: (time.Now().Add(10 * time.Minute)), MaxAge: 120},
77 + cookieName: "cookie",
78 + expect: "cookieValue",
79 + },
80 + } {
81 + context := getContextOrFail(t)
82 + context.Req.AddCookie(test.cookie)
83 + actual := context.Cookie(test.cookieName)
84 + if actual != test.expect {
85 + t.Errorf("Test %d: Expected cookie value '%s' but got '%s' for cookie with name '%s'",
86 + i, test.expect, actual, test.cookieName)
87 + }
88 + }
89 +}
90 +
91 +func TestCookieMultipleCookies(t *testing.T) {
92 + context := getContextOrFail(t)
93 +
94 + cookieNameBase, cookieValueBase := "cookieName", "cookieValue"
95 +
96 + for i := 0; i < 10; i++ {
97 + context.Req.AddCookie(&http.Cookie{
98 + Name: fmt.Sprintf("%s%d", cookieNameBase, i),
99 + Value: fmt.Sprintf("%s%d", cookieValueBase, i),
100 + })
101 + }
102 +
103 + for i := 0; i < 10; i++ {
104 + expectedCookieVal := fmt.Sprintf("%s%d", cookieValueBase, i)
105 + actualCookieVal := context.Cookie(fmt.Sprintf("%s%d", cookieNameBase, i))
106 + if actualCookieVal != expectedCookieVal {
107 + t.Errorf("Expected cookie value %s, found %s", expectedCookieVal, actualCookieVal)
108 + }
109 + }
110 +}
111 +
112 +func TestEnv(t *testing.T) {
113 + context := getContextOrFail(t)
114 +
115 + name := "ENV_TEST_NAME"
116 + testValue := "TEST_VALUE"
117 + os.Setenv(name, testValue)
118 +
119 + notExisting := "ENV_TEST_NOT_EXISTING"
120 + os.Unsetenv(notExisting)
121 +
122 + invalidName := "ENV_TEST_INVALID_NAME"
123 + os.Setenv("="+invalidName, testValue)
124 +
125 + env := context.Env()
126 + if value := env[name]; value != testValue {
127 + t.Errorf("Expected env-variable %s value '%s', found '%s'",
128 + name, testValue, value)
129 + }
130 +
131 + if value, ok := env[notExisting]; ok {
132 + t.Errorf("Expected empty env-variable %s, found '%s'",
133 + notExisting, value)
134 + }
135 +
136 + for k, v := range env {
137 + if strings.Contains(k, invalidName) {
138 + t.Errorf("Expected invalid name not to be included in Env %s, found in key '%s'", invalidName, k)
139 + }
140 + if strings.Contains(v, invalidName) {
141 + t.Errorf("Expected invalid name not be be included in Env %s, found in value '%s'", invalidName, v)
142 + }
143 + }
144 +
145 + os.Unsetenv("=" + invalidName)
146 +}
147 +
148 +func TestIP(t *testing.T) {
149 + context := getContextOrFail(t)
150 + for i, test := range []struct {
151 + inputRemoteAddr string
152 + expect string
153 + }{
154 + {"1.1.1.1:1111", "1.1.1.1"},
155 + {"1.1.1.1", "1.1.1.1"},
156 + {"[::1]:11", "::1"},
157 + {"[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]"},
158 + {`[fe80:1::3%eth0]:44`, `fe80:1::3%eth0`},
159 + } {
160 + context.Req.RemoteAddr = test.inputRemoteAddr
161 + if actual := context.IP(); actual != test.expect {
162 + t.Errorf("Test %d: Expected %s but got %s", i, test.expect, actual)
163 + }
164 + }
165 +}
166 +
167 +func TestTruncate(t *testing.T) {
168 + context := getContextOrFail(t)
169 +
170 + for i, test := range []struct {
171 + input string
172 + length int
173 + expect string
174 + }{
175 + {
176 + input: "string",
177 + length: 1,
178 + expect: "s",
179 + },
180 + {
181 + input: "string",
182 + length: 6,
183 + expect: "string",
184 + },
185 + {
186 + input: "string",
187 + length: 10,
188 + expect: "string",
189 + },
190 + {
191 + input: "string",
192 + length: 0,
193 + expect: "",
194 + },
195 + {
196 + input: "string",
197 + length: -5,
198 + expect: "tring",
199 + },
200 + {
201 + input: "string",
202 + length: -6,
203 + expect: "string",
204 + },
205 + {
206 + input: "string",
207 + length: -7,
208 + expect: "string",
209 + },
210 + } {
211 + actual := context.Truncate(test.input, test.length)
212 + if actual != test.expect {
213 + t.Errorf("Test %d: Expected '%s' but got '%s'", i, test.expect, actual)
214 + }
215 + }
216 +}
217 +
218 +func TestStripHTML(t *testing.T) {
219 + context := getContextOrFail(t)
220 +
221 + for i, test := range []struct {
222 + input string
223 + expect string
224 + }{
225 + {
226 + // no tags
227 + input: `h1`,
228 + expect: `h1`,
229 + },
230 + {
231 + // happy path
232 + input: `<h1>h1</h1>`,
233 + expect: `h1`,
234 + },
235 + {
236 + // tag in quotes
237 + input: `<h1">">h1</h1>`,
238 + expect: `h1`,
239 + },
240 + {
241 + // multiple tags
242 + input: `<h1><b>h1</b></h1>`,
243 + expect: `h1`,
244 + },
245 + {
246 + // tags not closed
247 + input: `<h1`,
248 + expect: `<h1`,
249 + },
250 + {
251 + // false start
252 + input: `<h1<b>hi`,
253 + expect: `<h1hi`,
254 + },
255 + } {
256 + actual := context.StripHTML(test.input)
257 + if actual != test.expect {
258 + t.Errorf("Test %d: Expected %s, found %s. Input was StripHTML(%s)", i, test.expect, actual, test.input)
259 + }
260 + }
261 +}
262 +
263 +func TestStripExt(t *testing.T) {
264 + context := getContextOrFail(t)
265 + tests := []struct {
266 + input string
267 + expect string
268 + }{
269 + {
270 + input: "",
271 + expect: "",
272 + },
273 + {
274 + input: "file.ext",
275 + expect: "file",
276 + },
277 + {
278 + input: "file",
279 + expect: "file",
280 + },
281 + {
282 + input: "/file",
283 + expect: "/file",
284 + },
285 + {
286 + input: "/file.ext",
287 + expect: "/file",
288 + },
289 + {
290 + input: "/dir.ext/",
291 + expect: "/dir.ext/",
292 + },
293 + {
294 + input: "/dir.ext/file.ext",
295 + expect: "/dir.ext/file",
296 + },
297 + }
298 +
299 + for i, test := range tests {
300 + actual := context.StripExt(test.input)
301 + if actual != test.expect {
302 + t.Errorf("Test %d: Expected %s but got %s", i, test.expect, actual)
303 + }
304 + }
305 +}
306 +
307 +func TestFileListing(t *testing.T) {
308 + for i, test := range []struct {
309 + fileNames []string
310 + inputBase string
311 + shouldErr bool
312 + verifyErr func(error) bool
313 + }{
314 + {
315 + // directory and files exist
316 + fileNames: []string{"file1", "file2"},
317 + shouldErr: false,
318 + },
319 + {
320 + // directory exists, no files
321 + fileNames: []string{},
322 + shouldErr: false,
323 + },
324 + {
325 + // file or directory does not exist
326 + fileNames: nil,
327 + inputBase: "doesNotExist",
328 + shouldErr: true,
329 + verifyErr: os.IsNotExist,
330 + },
331 + {
332 + // directory and files exist, but path to a file
333 + fileNames: []string{"file1", "file2"},
334 + inputBase: "file1",
335 + shouldErr: true,
336 + verifyErr: func(err error) bool {
337 + return strings.HasSuffix(err.Error(), "is not a directory")
338 + },
339 + },
340 + {
341 + // try to escape Context Root
342 + fileNames: nil,
343 + inputBase: filepath.Join("..", "..", "..", "..", "..", "etc"),
344 + shouldErr: true,
345 + verifyErr: os.IsNotExist,
346 + },
347 + } {
348 + context := getContextOrFail(t)
349 + var dirPath string
350 + var err error
351 +
352 + // create files for test case
353 + if test.fileNames != nil {
354 + dirPath, err = ioutil.TempDir(fmt.Sprintf("%s", context.Root), "caddy_ctxtest")
355 + if err != nil {
356 + t.Fatalf("Test %d: Expected no error creating directory, got: '%s'", i, err.Error())
357 + }
358 + for _, name := range test.fileNames {
359 + absFilePath := filepath.Join(dirPath, name)
360 + if err = ioutil.WriteFile(absFilePath, []byte(""), os.ModePerm); err != nil {
361 + os.RemoveAll(dirPath)
362 + t.Fatalf("Test %d: Expected no error creating file, got: '%s'", i, err.Error())
363 + }
364 + }
365 + }
366 +
367 + // perform test
368 + input := filepath.ToSlash(filepath.Join(filepath.Base(dirPath), test.inputBase))
369 + actual, err := context.ListFiles(input)
370 + if err != nil {
371 + if !test.shouldErr {
372 + t.Errorf("Test %d: Expected no error, got: '%s'", i, err)
373 + } else if !test.verifyErr(err) {
374 + t.Errorf("Test %d: Could not verify error content, got: '%s'", i, err)
375 + }
376 + } else if test.shouldErr {
377 + t.Errorf("Test %d: Expected error but had none", i)
378 + } else {
379 + numFiles := len(test.fileNames)
380 + // reflect.DeepEqual does not consider two empty slices to be equal
381 + if numFiles == 0 && len(actual) != 0 {
382 + t.Errorf("Test %d: Expected files %v, got: %v",
383 + i, test.fileNames, actual)
384 + } else {
385 + sort.Strings(actual)
386 + if numFiles > 0 && !reflect.DeepEqual(test.fileNames, actual) {
387 + t.Errorf("Test %d: Expected files %v, got: %v",
388 + i, test.fileNames, actual)
389 + }
390 + }
391 + }
392 +
393 + if dirPath != "" {
394 + if err := os.RemoveAll(dirPath); err != nil && !os.IsNotExist(err) {
395 + t.Fatalf("Test %d: Expected no error removing temporary test directory, got: %v", i, err)
396 + }
397 + }
398 + }
399 +}
400 +
401 +func getContextOrFail(t *testing.T) templateContext {
402 + context, err := initTestContext()
403 + if err != nil {
404 + t.Fatalf("failed to prepare test context: %v", err)
405 + }
406 + return context
407 +}
408 +
409 +func initTestContext() (templateContext, error) {
410 + body := bytes.NewBufferString("request body")
411 + request, err := http.NewRequest("GET", "https://example.com/foo/bar", body)
412 + if err != nil {
413 + return templateContext{}, err
414 + }
415 + return templateContext{
416 + Root: http.Dir(os.TempDir()),
417 + Req: request,
418 + RespHeader: tplWrappedHeader{make(http.Header)},
419 + }, nil
420 +}
modules/caddytls/matchers.go
+1 -1
@@ -28,4 +28,4 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
28 }
29
30 // Interface guard
31 -var _ ConnectionMatcher = MatchServerName{}
31 +var _ ConnectionMatcher = (*MatchServerName)(nil)