summaryrefslogtreecommitdiff
path: root/modules/caddyhttp
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r--modules/caddyhttp/encode/encode.go26
-rw-r--r--modules/caddyhttp/fileserver/staticfiles.go15
-rw-r--r--modules/caddyhttp/templates/templates.go33
3 files changed, 65 insertions, 9 deletions
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)
)