9 changed files with 1006 additions and 13 deletions
			
			
		| @ -0,0 +1,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) | |||
| ) | |||
| @ -0,0 +1,413 @@ | |||
| package templates | |||
| 
 | |||
| import ( | |||
| 	"bytes" | |||
| 	"crypto/rand" | |||
| 	"fmt" | |||
| 	"io" | |||
| 	"log" | |||
| 	weakrand "math/rand" | |||
| 	"net" | |||
| 	"net/http" | |||
| 	"path" | |||
| 	"strings" | |||
| 	"sync" | |||
| 	"text/template" | |||
| 	"time" | |||
| 
 | |||
| 	"os" | |||
| 
 | |||
| 	"github.com/caddyserver/caddy/modules/caddyhttp" | |||
| 	"gopkg.in/russross/blackfriday.v2" | |||
| ) | |||
| 
 | |||
| // templateContext is the templateContext with which HTTP templates are executed.
 | |||
| type templateContext struct { | |||
| 	Root       http.FileSystem | |||
| 	Req        *http.Request | |||
| 	Args       []interface{} // defined by arguments to .Include
 | |||
| 	RespHeader tplWrappedHeader | |||
| 	server     http.Handler | |||
| } | |||
| 
 | |||
| // Include returns the contents of filename relative to the site root.
 | |||
| func (c templateContext) Include(filename string, args ...interface{}) (string, error) { | |||
| 	if c.Root == nil { | |||
| 		return "", fmt.Errorf("root file system not specified") | |||
| 	} | |||
| 
 | |||
| 	file, err := c.Root.Open(filename) | |||
| 	if err != nil { | |||
| 		return "", err | |||
| 	} | |||
| 	defer file.Close() | |||
| 
 | |||
| 	bodyBuf := bufPool.Get().(*bytes.Buffer) | |||
| 	bodyBuf.Reset() | |||
| 	defer bufPool.Put(bodyBuf) | |||
| 
 | |||
| 	_, err = io.Copy(bodyBuf, file) | |||
| 	if err != nil { | |||
| 		return "", err | |||
| 	} | |||
| 
 | |||
| 	c.Args = args | |||
| 
 | |||
| 	return c.executeTemplate(filename, bodyBuf.Bytes()) | |||
| } | |||
| 
 | |||
| // HTTPInclude returns the body of a virtual (lightweight) request
 | |||
| // to the given URI on the same server.
 | |||
| func (c templateContext) HTTPInclude(uri string) (string, error) { | |||
| 	if c.Req.Header.Get(recursionPreventionHeader) == "1" { | |||
| 		return "", fmt.Errorf("virtual include cycle") | |||
| 	} | |||
| 
 | |||
| 	buf := bufPool.Get().(*bytes.Buffer) | |||
| 	buf.Reset() | |||
| 	defer bufPool.Put(buf) | |||
| 
 | |||
| 	virtReq, err := http.NewRequest("GET", uri, nil) | |||
| 	if err != nil { | |||
| 		return "", err | |||
| 	} | |||
| 	virtReq.Header.Set(recursionPreventionHeader, "1") | |||
| 
 | |||
| 	vrw := &virtualResponseWriter{body: buf, header: make(http.Header)} | |||
| 	server := c.Req.Context().Value(caddyhttp.ServerCtxKey).(http.Handler) | |||
| 
 | |||
| 	server.ServeHTTP(vrw, virtReq) | |||
| 	if vrw.status >= 400 { | |||
| 		return "", fmt.Errorf("http %d", vrw.status) | |||
| 	} | |||
| 
 | |||
| 	return c.executeTemplate(uri, buf.Bytes()) | |||
| } | |||
| 
 | |||
| func (c templateContext) executeTemplate(tplName string, body []byte) (string, error) { | |||
| 	tpl, err := template.New(tplName).Parse(string(body)) | |||
| 	if err != nil { | |||
| 		return "", err | |||
| 	} | |||
| 
 | |||
| 	buf := bufPool.Get().(*bytes.Buffer) | |||
| 	buf.Reset() | |||
| 	defer bufPool.Put(buf) | |||
| 
 | |||
| 	err = tpl.Execute(buf, c) | |||
| 	if err != nil { | |||
| 		return "", err | |||
| 	} | |||
| 
 | |||
| 	return buf.String(), nil | |||
| } | |||
| 
 | |||
| // Now returns the current timestamp.
 | |||
| func (c templateContext) Now() time.Time { | |||
| 	return time.Now() | |||
| } | |||
| 
 | |||
| // Cookie gets the value of a cookie with name name.
 | |||
| func (c templateContext) Cookie(name string) string { | |||
| 	cookies := c.Req.Cookies() | |||
| 	for _, cookie := range cookies { | |||
| 		if cookie.Name == name { | |||
| 			return cookie.Value | |||
| 		} | |||
| 	} | |||
| 	return "" | |||
| } | |||
| 
 | |||
| // ReqHeader gets the value of a request header with field name.
 | |||
| func (c templateContext) ReqHeader(name string) string { | |||
| 	return c.Req.Header.Get(name) | |||
| } | |||
| 
 | |||
| // Hostname gets the (remote) hostname of the client making the request.
 | |||
| func (c templateContext) Hostname() string { | |||
| 	ip := c.IP() | |||
| 
 | |||
| 	hostnameList, err := net.LookupAddr(ip) | |||
| 	if err != nil || len(hostnameList) == 0 { | |||
| 		return c.Req.RemoteAddr | |||
| 	} | |||
| 
 | |||
| 	return hostnameList[0] | |||
| } | |||
| 
 | |||
| // Env gets a map of the environment variables.
 | |||
| func (c templateContext) Env() map[string]string { | |||
| 	osEnv := os.Environ() | |||
| 	envVars := make(map[string]string, len(osEnv)) | |||
| 	for _, env := range osEnv { | |||
| 		data := strings.SplitN(env, "=", 2) | |||
| 		if len(data) == 2 && len(data[0]) > 0 { | |||
| 			envVars[data[0]] = data[1] | |||
| 		} | |||
| 	} | |||
| 	return envVars | |||
| } | |||
| 
 | |||
| // IP gets the (remote) IP address of the client making the request.
 | |||
| func (c templateContext) IP() string { | |||
| 	ip, _, err := net.SplitHostPort(c.Req.RemoteAddr) | |||
| 	if err != nil { | |||
| 		return c.Req.RemoteAddr | |||
| 	} | |||
| 	return ip | |||
| } | |||
| 
 | |||
| // Host returns the hostname portion of the Host header
 | |||
| // from the HTTP request.
 | |||
| func (c templateContext) Host() (string, error) { | |||
| 	host, _, err := net.SplitHostPort(c.Req.Host) | |||
| 	if err != nil { | |||
| 		if !strings.Contains(c.Req.Host, ":") { | |||
| 			// common with sites served on the default port 80
 | |||
| 			return c.Req.Host, nil | |||
| 		} | |||
| 		return "", err | |||
| 	} | |||
| 	return host, nil | |||
| } | |||
| 
 | |||
| // Truncate truncates the input string to the given length.
 | |||
| // If length is negative, it returns that many characters
 | |||
| // starting from the end of the string. If the absolute value
 | |||
| // of length is greater than len(input), the whole input is
 | |||
| // returned.
 | |||
| func (c templateContext) Truncate(input string, length int) string { | |||
| 	if length < 0 && len(input)+length > 0 { | |||
| 		return input[len(input)+length:] | |||
| 	} | |||
| 	if length >= 0 && len(input) > length { | |||
| 		return input[:length] | |||
| 	} | |||
| 	return input | |||
| } | |||
| 
 | |||
| // StripHTML returns s without HTML tags. It is fairly naive
 | |||
| // but works with most valid HTML inputs.
 | |||
| func (c templateContext) StripHTML(s string) string { | |||
| 	var buf bytes.Buffer | |||
| 	var inTag, inQuotes bool | |||
| 	var tagStart int | |||
| 	for i, ch := range s { | |||
| 		if inTag { | |||
| 			if ch == '>' && !inQuotes { | |||
| 				inTag = false | |||
| 			} else if ch == '<' && !inQuotes { | |||
| 				// false start
 | |||
| 				buf.WriteString(s[tagStart:i]) | |||
| 				tagStart = i | |||
| 			} else if ch == '"' { | |||
| 				inQuotes = !inQuotes | |||
| 			} | |||
| 			continue | |||
| 		} | |||
| 		if ch == '<' { | |||
| 			inTag = true | |||
| 			tagStart = i | |||
| 			continue | |||
| 		} | |||
| 		buf.WriteRune(ch) | |||
| 	} | |||
| 	if inTag { | |||
| 		// false start
 | |||
| 		buf.WriteString(s[tagStart:]) | |||
| 	} | |||
| 	return buf.String() | |||
| } | |||
| 
 | |||
| // Markdown renders the markdown body as HTML.
 | |||
| func (c templateContext) Markdown(body string) string { | |||
| 	return string(blackfriday.Run([]byte(body))) | |||
| } | |||
| 
 | |||
| // Ext returns the suffix beginning at the final dot in the final
 | |||
| // slash-separated element of the pathStr (or in other words, the
 | |||
| // file extension).
 | |||
| func (c templateContext) Ext(pathStr string) string { | |||
| 	return path.Ext(pathStr) | |||
| } | |||
| 
 | |||
| // StripExt returns the input string without the extension,
 | |||
| // which is the suffix starting with the final '.' character
 | |||
| // but not before the final path separator ('/') character.
 | |||
| // If there is no extension, the whole input is returned.
 | |||
| func (c templateContext) StripExt(path string) string { | |||
| 	for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- { | |||
| 		if path[i] == '.' { | |||
| 			return path[:i] | |||
| 		} | |||
| 	} | |||
| 	return path | |||
| } | |||
| 
 | |||
| // Replace replaces instances of find in input with replacement.
 | |||
| func (c templateContext) Replace(input, find, replacement string) string { | |||
| 	return strings.Replace(input, find, replacement, -1) | |||
| } | |||
| 
 | |||
| // HasPrefix returns true if s starts with prefix.
 | |||
| func (c templateContext) HasPrefix(s, prefix string) bool { | |||
| 	return strings.HasPrefix(s, prefix) | |||
| } | |||
| 
 | |||
| // ToLower will convert the given string to lower case.
 | |||
| func (c templateContext) ToLower(s string) string { | |||
| 	return strings.ToLower(s) | |||
| } | |||
| 
 | |||
| // ToUpper will convert the given string to upper case.
 | |||
| func (c templateContext) ToUpper(s string) string { | |||
| 	return strings.ToUpper(s) | |||
| } | |||
| 
 | |||
| // Split is a pass-through to strings.Split. It will split
 | |||
| // the first argument at each instance of the separator and
 | |||
| // return a slice of strings.
 | |||
| func (c templateContext) Split(s string, sep string) []string { | |||
| 	return strings.Split(s, sep) | |||
| } | |||
| 
 | |||
| // Join is a pass-through to strings.Join. It will join the
 | |||
| // first argument slice with the separator in the second
 | |||
| // argument and return the result.
 | |||
| func (c templateContext) Join(a []string, sep string) string { | |||
| 	return strings.Join(a, sep) | |||
| } | |||
| 
 | |||
| // Slice will convert the given arguments into a slice.
 | |||
| func (c templateContext) Slice(elems ...interface{}) []interface{} { | |||
| 	return elems | |||
| } | |||
| 
 | |||
| // Dict will convert the arguments into a dictionary (map). It expects
 | |||
| // alternating keys and values of string types. This is useful since you
 | |||
| // cannot express map literals directly in Go templates.
 | |||
| func (c templateContext) Dict(values ...interface{}) (map[string]interface{}, error) { | |||
| 	if len(values)%2 != 0 { | |||
| 		return nil, fmt.Errorf("expected even number of arguments") | |||
| 	} | |||
| 	dict := make(map[string]interface{}, len(values)/2) | |||
| 	for i := 0; i < len(values); i += 2 { | |||
| 		key, ok := values[i].(string) | |||
| 		if !ok { | |||
| 			return nil, fmt.Errorf("argument %d: map keys must be strings", i) | |||
| 		} | |||
| 		dict[key] = values[i+1] | |||
| 	} | |||
| 	return dict, nil | |||
| } | |||
| 
 | |||
| // ListFiles reads and returns a slice of names from the given
 | |||
| // directory relative to the root of c.
 | |||
| func (c templateContext) ListFiles(name string) ([]string, error) { | |||
| 	if c.Root == nil { | |||
| 		return nil, fmt.Errorf("root file system not specified") | |||
| 	} | |||
| 
 | |||
| 	dir, err := c.Root.Open(path.Clean(name)) | |||
| 	if err != nil { | |||
| 		return nil, err | |||
| 	} | |||
| 	defer dir.Close() | |||
| 
 | |||
| 	stat, err := dir.Stat() | |||
| 	if err != nil { | |||
| 		return nil, err | |||
| 	} | |||
| 	if !stat.IsDir() { | |||
| 		return nil, fmt.Errorf("%v is not a directory", name) | |||
| 	} | |||
| 
 | |||
| 	dirInfo, err := dir.Readdir(0) | |||
| 	if err != nil { | |||
| 		return nil, err | |||
| 	} | |||
| 
 | |||
| 	names := make([]string, len(dirInfo)) | |||
| 	for i, fileInfo := range dirInfo { | |||
| 		names[i] = fileInfo.Name() | |||
| 	} | |||
| 
 | |||
| 	return names, nil | |||
| } | |||
| 
 | |||
| // RandomString generates a random string of random length given
 | |||
| // length bounds. Thanks to http://stackoverflow.com/a/35615565/1048862
 | |||
| // for the clever technique that is fairly fast, secure, and maintains
 | |||
| // proper distributions over the dictionary.
 | |||
| func (c templateContext) RandomString(minLen, maxLen int) string { | |||
| 	const ( | |||
| 		letterBytes   = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" | |||
| 		letterIdxBits = 6                    // 6 bits to represent 64 possibilities (indexes)
 | |||
| 		letterIdxMask = 1<<letterIdxBits - 1 // all 1-bits, as many as letterIdxBits
 | |||
| 	) | |||
| 
 | |||
| 	if minLen < 0 || maxLen < 0 || maxLen < minLen { | |||
| 		return "" | |||
| 	} | |||
| 
 | |||
| 	n := weakrand.Intn(maxLen-minLen+1) + minLen // choose actual length
 | |||
| 
 | |||
| 	// secureRandomBytes returns a number of bytes using crypto/rand.
 | |||
| 	secureRandomBytes := func(numBytes int) []byte { | |||
| 		randomBytes := make([]byte, numBytes) | |||
| 		if _, err := rand.Read(randomBytes); err != nil { | |||
| 			// TODO: what to do with the logs (throughout whole file) (could return as error? might get rendered though...)
 | |||
| 			log.Println("[ERROR] failed to read bytes: ", err) | |||
| 		} | |||
| 		return randomBytes | |||
| 	} | |||
| 
 | |||
| 	result := make([]byte, n) | |||
| 	bufferSize := int(float64(n) * 1.3) | |||
| 	for i, j, randomBytes := 0, 0, []byte{}; i < n; j++ { | |||
| 		if j%bufferSize == 0 { | |||
| 			randomBytes = secureRandomBytes(bufferSize) | |||
| 		} | |||
| 		if idx := int(randomBytes[j%n] & letterIdxMask); idx < len(letterBytes) { | |||
| 			result[i] = letterBytes[idx] | |||
| 			i++ | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	return string(result) | |||
| } | |||
| 
 | |||
| // tplWrappedHeader wraps niladic functions so that they
 | |||
| // can be used in templates. (Template functions must
 | |||
| // return a value.)
 | |||
| type tplWrappedHeader struct{ http.Header } | |||
| 
 | |||
| // Add adds a header field value, appending val to
 | |||
| // existing values for that field. It returns an
 | |||
| // empty string.
 | |||
| func (h tplWrappedHeader) Add(field, val string) string { | |||
| 	h.Header.Add(field, val) | |||
| 	return "" | |||
| } | |||
| 
 | |||
| // Set sets a header field value, overwriting any
 | |||
| // other values for that field. It returns an
 | |||
| // empty string.
 | |||
| func (h tplWrappedHeader) Set(field, val string) string { | |||
| 	h.Header.Set(field, val) | |||
| 	return "" | |||
| } | |||
| 
 | |||
| // Del deletes a header field. It returns an empty string.
 | |||
| func (h tplWrappedHeader) Del(field string) string { | |||
| 	h.Header.Del(field) | |||
| 	return "" | |||
| } | |||
| 
 | |||
| var bufPool = sync.Pool{ | |||
| 	New: func() interface{} { | |||
| 		return new(bytes.Buffer) | |||
| 	}, | |||
| } | |||
| 
 | |||
| const recursionPreventionHeader = "Caddy-Templates-Include" | |||
| @ -0,0 +1,420 @@ | |||
| // Copyright 2015 Light Code Labs, LLC
 | |||
| //
 | |||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |||
| // you may not use this file except in compliance with the License.
 | |||
| // You may obtain a copy of the License at
 | |||
| //
 | |||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |||
| //
 | |||
| // Unless required by applicable law or agreed to in writing, software
 | |||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |||
| // See the License for the specific language governing permissions and
 | |||
| // limitations under the License.
 | |||
| 
 | |||
| package templates | |||
| 
 | |||
| import ( | |||
| 	"bytes" | |||
| 	"fmt" | |||
| 	"io/ioutil" | |||
| 	"net/http" | |||
| 	"os" | |||
| 	"path/filepath" | |||
| 	"reflect" | |||
| 	"sort" | |||
| 	"strings" | |||
| 	"testing" | |||
| 	"time" | |||
| ) | |||
| 
 | |||
| func TestMarkdown(t *testing.T) { | |||
| 	context := getContextOrFail(t) | |||
| 
 | |||
| 	for i, test := range []struct { | |||
| 		body   string | |||
| 		expect string | |||
| 	}{ | |||
| 		{ | |||
| 			body:   "- str1\n- str2\n", | |||
| 			expect: "<ul>\n<li>str1</li>\n<li>str2</li>\n</ul>\n", | |||
| 		}, | |||
| 	} { | |||
| 		result := context.Markdown(test.body) | |||
| 		if result != test.expect { | |||
| 			t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expect, result) | |||
| 		} | |||
| 	} | |||
| } | |||
| 
 | |||
| func TestCookie(t *testing.T) { | |||
| 	for i, test := range []struct { | |||
| 		cookie     *http.Cookie | |||
| 		cookieName string | |||
| 		expect     string | |||
| 	}{ | |||
| 		{ | |||
| 			// happy path
 | |||
| 			cookie:     &http.Cookie{Name: "cookieName", Value: "cookieValue"}, | |||
| 			cookieName: "cookieName", | |||
| 			expect:     "cookieValue", | |||
| 		}, | |||
| 		{ | |||
| 			// try to get a non-existing cookie
 | |||
| 			cookie:     &http.Cookie{Name: "cookieName", Value: "cookieValue"}, | |||
| 			cookieName: "notExisting", | |||
| 			expect:     "", | |||
| 		}, | |||
| 		{ | |||
| 			// partial name match
 | |||
| 			cookie:     &http.Cookie{Name: "cookie", Value: "cookieValue"}, | |||
| 			cookieName: "cook", | |||
| 			expect:     "", | |||
| 		}, | |||
| 		{ | |||
| 			// cookie with optional fields
 | |||
| 			cookie:     &http.Cookie{Name: "cookie", Value: "cookieValue", Path: "/path", Domain: "https://localhost", Expires: (time.Now().Add(10 * time.Minute)), MaxAge: 120}, | |||
| 			cookieName: "cookie", | |||
| 			expect:     "cookieValue", | |||
| 		}, | |||
| 	} { | |||
| 		context := getContextOrFail(t) | |||
| 		context.Req.AddCookie(test.cookie) | |||
| 		actual := context.Cookie(test.cookieName) | |||
| 		if actual != test.expect { | |||
| 			t.Errorf("Test %d: Expected cookie value '%s' but got '%s' for cookie with name '%s'", | |||
| 				i, test.expect, actual, test.cookieName) | |||
| 		} | |||
| 	} | |||
| } | |||
| 
 | |||
| func TestCookieMultipleCookies(t *testing.T) { | |||
| 	context := getContextOrFail(t) | |||
| 
 | |||
| 	cookieNameBase, cookieValueBase := "cookieName", "cookieValue" | |||
| 
 | |||
| 	for i := 0; i < 10; i++ { | |||
| 		context.Req.AddCookie(&http.Cookie{ | |||
| 			Name:  fmt.Sprintf("%s%d", cookieNameBase, i), | |||
| 			Value: fmt.Sprintf("%s%d", cookieValueBase, i), | |||
| 		}) | |||
| 	} | |||
| 
 | |||
| 	for i := 0; i < 10; i++ { | |||
| 		expectedCookieVal := fmt.Sprintf("%s%d", cookieValueBase, i) | |||
| 		actualCookieVal := context.Cookie(fmt.Sprintf("%s%d", cookieNameBase, i)) | |||
| 		if actualCookieVal != expectedCookieVal { | |||
| 			t.Errorf("Expected cookie value %s, found %s", expectedCookieVal, actualCookieVal) | |||
| 		} | |||
| 	} | |||
| } | |||
| 
 | |||
| func TestEnv(t *testing.T) { | |||
| 	context := getContextOrFail(t) | |||
| 
 | |||
| 	name := "ENV_TEST_NAME" | |||
| 	testValue := "TEST_VALUE" | |||
| 	os.Setenv(name, testValue) | |||
| 
 | |||
| 	notExisting := "ENV_TEST_NOT_EXISTING" | |||
| 	os.Unsetenv(notExisting) | |||
| 
 | |||
| 	invalidName := "ENV_TEST_INVALID_NAME" | |||
| 	os.Setenv("="+invalidName, testValue) | |||
| 
 | |||
| 	env := context.Env() | |||
| 	if value := env[name]; value != testValue { | |||
| 		t.Errorf("Expected env-variable %s value '%s', found '%s'", | |||
| 			name, testValue, value) | |||
| 	} | |||
| 
 | |||
| 	if value, ok := env[notExisting]; ok { | |||
| 		t.Errorf("Expected empty env-variable %s, found '%s'", | |||
| 			notExisting, value) | |||
| 	} | |||
| 
 | |||
| 	for k, v := range env { | |||
| 		if strings.Contains(k, invalidName) { | |||
| 			t.Errorf("Expected invalid name not to be included in Env %s, found in key '%s'", invalidName, k) | |||
| 		} | |||
| 		if strings.Contains(v, invalidName) { | |||
| 			t.Errorf("Expected invalid name not be be included in Env %s, found in value '%s'", invalidName, v) | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	os.Unsetenv("=" + invalidName) | |||
| } | |||
| 
 | |||
| func TestIP(t *testing.T) { | |||
| 	context := getContextOrFail(t) | |||
| 	for i, test := range []struct { | |||
| 		inputRemoteAddr string | |||
| 		expect          string | |||
| 	}{ | |||
| 		{"1.1.1.1:1111", "1.1.1.1"}, | |||
| 		{"1.1.1.1", "1.1.1.1"}, | |||
| 		{"[::1]:11", "::1"}, | |||
| 		{"[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]"}, | |||
| 		{`[fe80:1::3%eth0]:44`, `fe80:1::3%eth0`}, | |||
| 	} { | |||
| 		context.Req.RemoteAddr = test.inputRemoteAddr | |||
| 		if actual := context.IP(); actual != test.expect { | |||
| 			t.Errorf("Test %d: Expected %s but got %s", i, test.expect, actual) | |||
| 		} | |||
| 	} | |||
| } | |||
| 
 | |||
| func TestTruncate(t *testing.T) { | |||
| 	context := getContextOrFail(t) | |||
| 
 | |||
| 	for i, test := range []struct { | |||
| 		input  string | |||
| 		length int | |||
| 		expect string | |||
| 	}{ | |||
| 		{ | |||
| 			input:  "string", | |||
| 			length: 1, | |||
| 			expect: "s", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "string", | |||
| 			length: 6, | |||
| 			expect: "string", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "string", | |||
| 			length: 10, | |||
| 			expect: "string", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "string", | |||
| 			length: 0, | |||
| 			expect: "", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "string", | |||
| 			length: -5, | |||
| 			expect: "tring", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "string", | |||
| 			length: -6, | |||
| 			expect: "string", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "string", | |||
| 			length: -7, | |||
| 			expect: "string", | |||
| 		}, | |||
| 	} { | |||
| 		actual := context.Truncate(test.input, test.length) | |||
| 		if actual != test.expect { | |||
| 			t.Errorf("Test %d: Expected '%s' but got '%s'", i, test.expect, actual) | |||
| 		} | |||
| 	} | |||
| } | |||
| 
 | |||
| func TestStripHTML(t *testing.T) { | |||
| 	context := getContextOrFail(t) | |||
| 
 | |||
| 	for i, test := range []struct { | |||
| 		input  string | |||
| 		expect string | |||
| 	}{ | |||
| 		{ | |||
| 			// no tags
 | |||
| 			input:  `h1`, | |||
| 			expect: `h1`, | |||
| 		}, | |||
| 		{ | |||
| 			// happy path
 | |||
| 			input:  `<h1>h1</h1>`, | |||
| 			expect: `h1`, | |||
| 		}, | |||
| 		{ | |||
| 			// tag in quotes
 | |||
| 			input:  `<h1">">h1</h1>`, | |||
| 			expect: `h1`, | |||
| 		}, | |||
| 		{ | |||
| 			// multiple tags
 | |||
| 			input:  `<h1><b>h1</b></h1>`, | |||
| 			expect: `h1`, | |||
| 		}, | |||
| 		{ | |||
| 			// tags not closed
 | |||
| 			input:  `<h1`, | |||
| 			expect: `<h1`, | |||
| 		}, | |||
| 		{ | |||
| 			// false start
 | |||
| 			input:  `<h1<b>hi`, | |||
| 			expect: `<h1hi`, | |||
| 		}, | |||
| 	} { | |||
| 		actual := context.StripHTML(test.input) | |||
| 		if actual != test.expect { | |||
| 			t.Errorf("Test %d: Expected %s, found %s. Input was StripHTML(%s)", i, test.expect, actual, test.input) | |||
| 		} | |||
| 	} | |||
| } | |||
| 
 | |||
| func TestStripExt(t *testing.T) { | |||
| 	context := getContextOrFail(t) | |||
| 	tests := []struct { | |||
| 		input  string | |||
| 		expect string | |||
| 	}{ | |||
| 		{ | |||
| 			input:  "", | |||
| 			expect: "", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "file.ext", | |||
| 			expect: "file", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "file", | |||
| 			expect: "file", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "/file", | |||
| 			expect: "/file", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "/file.ext", | |||
| 			expect: "/file", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "/dir.ext/", | |||
| 			expect: "/dir.ext/", | |||
| 		}, | |||
| 		{ | |||
| 			input:  "/dir.ext/file.ext", | |||
| 			expect: "/dir.ext/file", | |||
| 		}, | |||
| 	} | |||
| 
 | |||
| 	for i, test := range tests { | |||
| 		actual := context.StripExt(test.input) | |||
| 		if actual != test.expect { | |||
| 			t.Errorf("Test %d: Expected %s but got %s", i, test.expect, actual) | |||
| 		} | |||
| 	} | |||
| } | |||
| 
 | |||
| func TestFileListing(t *testing.T) { | |||
| 	for i, test := range []struct { | |||
| 		fileNames []string | |||
| 		inputBase string | |||
| 		shouldErr bool | |||
| 		verifyErr func(error) bool | |||
| 	}{ | |||
| 		{ | |||
| 			// directory and files exist
 | |||
| 			fileNames: []string{"file1", "file2"}, | |||
| 			shouldErr: false, | |||
| 		}, | |||
| 		{ | |||
| 			// directory exists, no files
 | |||
| 			fileNames: []string{}, | |||
| 			shouldErr: false, | |||
| 		}, | |||
| 		{ | |||
| 			// file or directory does not exist
 | |||
| 			fileNames: nil, | |||
| 			inputBase: "doesNotExist", | |||
| 			shouldErr: true, | |||
| 			verifyErr: os.IsNotExist, | |||
| 		}, | |||
| 		{ | |||
| 			// directory and files exist, but path to a file
 | |||
| 			fileNames: []string{"file1", "file2"}, | |||
| 			inputBase: "file1", | |||
| 			shouldErr: true, | |||
| 			verifyErr: func(err error) bool { | |||
| 				return strings.HasSuffix(err.Error(), "is not a directory") | |||
| 			}, | |||
| 		}, | |||
| 		{ | |||
| 			// try to escape Context Root
 | |||
| 			fileNames: nil, | |||
| 			inputBase: filepath.Join("..", "..", "..", "..", "..", "etc"), | |||
| 			shouldErr: true, | |||
| 			verifyErr: os.IsNotExist, | |||
| 		}, | |||
| 	} { | |||
| 		context := getContextOrFail(t) | |||
| 		var dirPath string | |||
| 		var err error | |||
| 
 | |||
| 		// create files for test case
 | |||
| 		if test.fileNames != nil { | |||
| 			dirPath, err = ioutil.TempDir(fmt.Sprintf("%s", context.Root), "caddy_ctxtest") | |||
| 			if err != nil { | |||
| 				t.Fatalf("Test %d: Expected no error creating directory, got: '%s'", i, err.Error()) | |||
| 			} | |||
| 			for _, name := range test.fileNames { | |||
| 				absFilePath := filepath.Join(dirPath, name) | |||
| 				if err = ioutil.WriteFile(absFilePath, []byte(""), os.ModePerm); err != nil { | |||
| 					os.RemoveAll(dirPath) | |||
| 					t.Fatalf("Test %d: Expected no error creating file, got: '%s'", i, err.Error()) | |||
| 				} | |||
| 			} | |||
| 		} | |||
| 
 | |||
| 		// perform test
 | |||
| 		input := filepath.ToSlash(filepath.Join(filepath.Base(dirPath), test.inputBase)) | |||
| 		actual, err := context.ListFiles(input) | |||
| 		if err != nil { | |||
| 			if !test.shouldErr { | |||
| 				t.Errorf("Test %d: Expected no error, got: '%s'", i, err) | |||
| 			} else if !test.verifyErr(err) { | |||
| 				t.Errorf("Test %d: Could not verify error content, got: '%s'", i, err) | |||
| 			} | |||
| 		} else if test.shouldErr { | |||
| 			t.Errorf("Test %d: Expected error but had none", i) | |||
| 		} else { | |||
| 			numFiles := len(test.fileNames) | |||
| 			// reflect.DeepEqual does not consider two empty slices to be equal
 | |||
| 			if numFiles == 0 && len(actual) != 0 { | |||
| 				t.Errorf("Test %d: Expected files %v, got: %v", | |||
| 					i, test.fileNames, actual) | |||
| 			} else { | |||
| 				sort.Strings(actual) | |||
| 				if numFiles > 0 && !reflect.DeepEqual(test.fileNames, actual) { | |||
| 					t.Errorf("Test %d: Expected files %v, got: %v", | |||
| 						i, test.fileNames, actual) | |||
| 				} | |||
| 			} | |||
| 		} | |||
| 
 | |||
| 		if dirPath != "" { | |||
| 			if err := os.RemoveAll(dirPath); err != nil && !os.IsNotExist(err) { | |||
| 				t.Fatalf("Test %d: Expected no error removing temporary test directory, got: %v", i, err) | |||
| 			} | |||
| 		} | |||
| 	} | |||
| } | |||
| 
 | |||
| func getContextOrFail(t *testing.T) templateContext { | |||
| 	context, err := initTestContext() | |||
| 	if err != nil { | |||
| 		t.Fatalf("failed to prepare test context: %v", err) | |||
| 	} | |||
| 	return context | |||
| } | |||
| 
 | |||
| func initTestContext() (templateContext, error) { | |||
| 	body := bytes.NewBufferString("request body") | |||
| 	request, err := http.NewRequest("GET", "https://example.com/foo/bar", body) | |||
| 	if err != nil { | |||
| 		return templateContext{}, err | |||
| 	} | |||
| 	return templateContext{ | |||
| 		Root:       http.Dir(os.TempDir()), | |||
| 		Req:        request, | |||
| 		RespHeader: tplWrappedHeader{make(http.Header)}, | |||
| 	}, nil | |||
| } | |||
					Loading…
					
					
				
		Reference in new issue