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)