aboutsummaryrefslogtreecommitdiffhomepage
path: root/http/response/builder.go
diff options
context:
space:
mode:
Diffstat (limited to 'http/response/builder.go')
-rw-r--r--http/response/builder.go134
1 files changed, 134 insertions, 0 deletions
diff --git a/http/response/builder.go b/http/response/builder.go
new file mode 100644
index 0000000..f17c62e
--- /dev/null
+++ b/http/response/builder.go
@@ -0,0 +1,134 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package response // import "miniflux.app/http/response"
+
+import (
+ "compress/flate"
+ "compress/gzip"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+)
+
+const compressionThreshold = 1024
+
+// Builder generates HTTP responses.
+type Builder struct {
+ w http.ResponseWriter
+ r *http.Request
+ statusCode int
+ headers map[string]string
+ enableCompression bool
+ body interface{}
+}
+
+// WithStatus uses the given status code to build the response.
+func (b *Builder) WithStatus(statusCode int) *Builder {
+ b.statusCode = statusCode
+ return b
+}
+
+// WithHeader adds the given HTTP header to the response.
+func (b *Builder) WithHeader(key, value string) *Builder {
+ b.headers[key] = value
+ return b
+}
+
+// WithBody uses the given body to build the response.
+func (b *Builder) WithBody(body interface{}) *Builder {
+ b.body = body
+ return b
+}
+
+// WithAttachment forces the document to be downloaded by the web browser.
+func (b *Builder) WithAttachment(filename string) *Builder {
+ b.headers["Content-Disposition"] = fmt.Sprintf("attachment; filename=%s", filename)
+ return b
+}
+
+// WithoutCompression disables HTTP compression.
+func (b *Builder) WithoutCompression() *Builder {
+ b.enableCompression = false
+ return b
+}
+
+// WithCaching adds caching headers to the response.
+func (b *Builder) WithCaching(etag string, duration time.Duration, callback func(*Builder)) {
+ b.headers["ETag"] = etag
+ b.headers["Cache-Control"] = "public"
+ b.headers["Expires"] = time.Now().Add(duration).Format(time.RFC1123)
+
+ if etag == b.r.Header.Get("If-None-Match") {
+ b.statusCode = http.StatusNotModified
+ b.body = nil
+ b.Write()
+ } else {
+ callback(b)
+ }
+}
+
+// Write generates the HTTP response.
+func (b *Builder) Write() {
+ if b.body == nil {
+ b.writeHeaders()
+ return
+ }
+
+ switch v := b.body.(type) {
+ case []byte:
+ b.compress(v)
+ case string:
+ b.compress([]byte(v))
+ case error:
+ b.compress([]byte(v.Error()))
+ }
+}
+
+func (b *Builder) writeHeaders() {
+ b.headers["X-XSS-Protection"] = "1; mode=block"
+ b.headers["X-Content-Type-Options"] = "nosniff"
+ b.headers["X-Frame-Options"] = "DENY"
+ b.headers["Content-Security-Policy"] = "default-src 'self'; img-src *; media-src *; frame-src *; child-src *"
+
+ for key, value := range b.headers {
+ b.w.Header().Set(key, value)
+ }
+
+ b.w.WriteHeader(b.statusCode)
+}
+
+func (b *Builder) compress(data []byte) {
+ if b.enableCompression && len(data) > compressionThreshold {
+ acceptEncoding := b.r.Header.Get("Accept-Encoding")
+
+ switch {
+ case strings.Contains(acceptEncoding, "gzip"):
+ b.headers["Content-Encoding"] = "gzip"
+ b.writeHeaders()
+
+ gzipWriter := gzip.NewWriter(b.w)
+ defer gzipWriter.Close()
+ gzipWriter.Write(data)
+ return
+ case strings.Contains(acceptEncoding, "deflate"):
+ b.headers["Content-Encoding"] = "deflate"
+ b.writeHeaders()
+
+ flateWriter, _ := flate.NewWriter(b.w, -1)
+ defer flateWriter.Close()
+ flateWriter.Write(data)
+ return
+ }
+ }
+
+ b.writeHeaders()
+ b.w.Write(data)
+}
+
+// New creates a new response builder.
+func New(w http.ResponseWriter, r *http.Request) *Builder {
+ return &Builder{w: w, r: r, statusCode: http.StatusOK, headers: make(map[string]string), enableCompression: true}
+}