From a63cb3e3fdea70991a95c3f0bc8f3866a5aec6ef Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 27 Jun 2019 13:09:10 -0600 Subject: Implement etag; fix related bugs in encode and templates middlewares --- modules/caddyhttp/encode/encode.go | 26 +++++++++++++++++------ modules/caddyhttp/fileserver/staticfiles.go | 15 ++++++++++++- modules/caddyhttp/templates/templates.go | 33 +++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 9 deletions(-) (limited to 'modules') diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index b7ab737..7929405 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -135,6 +135,17 @@ func (rw *responseWriter) Write(p []byte) (int, error) { }() } + // before we write to the response, we need to make + // sure the header is written exactly once; we do + // that by checking if a status code has been set, + // and if so, that means we haven't written the + // header OR the default status code will be written + // by the standard library + if rw.statusCode > 0 { + rw.ResponseWriter.WriteHeader(rw.statusCode) + rw.statusCode = 0 + } + switch { case rw.w != nil: n, err = rw.w.Write(p) @@ -148,7 +159,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) { return n, err } -// init should be called before we write a response, if rw.buf is not nil. +// init should be called before we write a response, if rw.buf has contents. func (rw *responseWriter) init() { if rw.Header().Get("Content-Encoding") == "" && rw.buf.Len() >= rw.config.MinLength { rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder) @@ -157,11 +168,6 @@ func (rw *responseWriter) init() { rw.Header().Set("Content-Encoding", rw.encodingName) } rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content - status := rw.statusCode - if status == 0 { - status = http.StatusOK - } - rw.ResponseWriter.WriteHeader(status) } // Close writes any remaining buffered response and @@ -187,6 +193,14 @@ func (rw *responseWriter) Close() error { default: _, err = rw.ResponseWriter.Write(p) } + } else if rw.statusCode != 0 { + // it is possible that a body was not written, and + // a header was not even written yet, even though + // we are closing; ensure the proper status code is + // written exactly once, or we risk breaking requests + // that rely on If-None-Match, for example + rw.ResponseWriter.WriteHeader(rw.statusCode) + rw.statusCode = 0 } if rw.w != nil { err2 := rw.w.Close() diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 49c2be4..bcf8cf7 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -184,7 +184,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) error } defer file.Close() - // TODO: Etag + // set the ETag - note that a conditional If-None-Match request is handled + // by http.ServeContent below, which checks against this ETag value + w.Header().Set("ETag", calculateEtag(info)) if w.Header().Get("Content-Type") == "" { mtyp := mime.TypeByExtension(filepath.Ext(filename)) @@ -419,6 +421,17 @@ func fileHidden(filename string, hide []string) bool { return false } +// calculateEtag produces a strong etag by default, although, for +// efficiency reasons, it does not actually consume the contents +// of the file to make a hash of all the bytes. ¯\_(ツ)_/¯ +// Prefix the etag with "W/" to convert it into a weak etag. +// See: https://tools.ietf.org/html/rfc7232#section-2.3 +func calculateEtag(d os.FileInfo) string { + t := strconv.FormatInt(d.ModTime().Unix(), 36) + s := strconv.FormatInt(d.Size(), 36) + return `"` + t + s + `"` +} + var defaultIndexNames = []string{"index.html"} const minBackoff, maxBackoff = 2, 5 diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index e329e2e..79bbde6 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -22,9 +22,18 @@ func init() { // Templates is a middleware which execute response bodies as templates. type Templates struct { FileRoot string `json:"file_root,omitempty"` + MIMETypes []string `json:"mime_types,omitempty"` Delimiters []string `json:"delimiters,omitempty"` } +// Provision provisions t. +func (t *Templates) Provision(ctx caddy.Context) error { + if t.MIMETypes == nil { + t.MIMETypes = defaultMIMETypes + } + return nil +} + // Validate ensures t has a valid configuration. func (t *Templates) Validate() error { if len(t.Delimiters) != 0 && len(t.Delimiters) != 2 { @@ -38,8 +47,16 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy buf.Reset() defer bufPool.Put(buf) + // shouldBuf determines whether to execute templates on this response, + // since generally we will not want to execute for images or CSS, etc. shouldBuf := func(status int) bool { - return strings.HasPrefix(w.Header().Get("Content-Type"), "text/") + ct := w.Header().Get("Content-Type") + for _, mt := range t.MIMETypes { + if strings.Contains(ct, mt) { + return true + } + } + return false } rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuf) @@ -59,9 +76,14 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy w.Header().Set("Content-Length", strconv.Itoa(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 + // we don't know a way to guickly generate etag for dynamic content, + // but we can convert this to a weak etag to kind of indicate that + if etag := w.Header().Get("ETag"); etag != "" { + w.Header().Set("ETag", "W/"+etag) + } + w.WriteHeader(rec.Status()) io.Copy(w, buf) @@ -110,8 +132,15 @@ func (vrw *virtualResponseWriter) Write(data []byte) (int, error) { return vrw.body.Write(data) } +var defaultMIMETypes = []string{ + "text/html", + "text/plain", + "text/markdown", +} + // Interface guards var ( + _ caddy.Provisioner = (*Templates)(nil) _ caddy.Validator = (*Templates)(nil) _ caddyhttp.MiddlewareHandler = (*Templates)(nil) ) -- cgit v1.2.3