2023-06-19 23:42:47 +02:00
|
|
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
2018-10-08 03:42:43 +02:00
|
|
|
|
2023-08-11 04:46:45 +02:00
|
|
|
package response // import "miniflux.app/v2/internal/http/response"
|
2018-10-08 03:42:43 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"compress/flate"
|
|
|
|
"compress/gzip"
|
|
|
|
"fmt"
|
2019-08-14 17:54:02 +02:00
|
|
|
"io"
|
2023-09-25 01:32:09 +02:00
|
|
|
"log/slog"
|
2018-10-08 03:42:43 +02:00
|
|
|
"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"
|
2023-10-13 12:45:06 +02:00
|
|
|
b.headers["Expires"] = time.Now().Add(duration).UTC().Format(http.TimeFormat)
|
2018-10-08 03:42:43 +02:00
|
|
|
|
|
|
|
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()))
|
2019-08-14 17:54:02 +02:00
|
|
|
case io.Reader:
|
|
|
|
// Compression not implemented in this case
|
|
|
|
b.writeHeaders()
|
2023-02-25 09:36:19 +01:00
|
|
|
_, err := io.Copy(b.w, v)
|
|
|
|
if err != nil {
|
2023-09-25 01:32:09 +02:00
|
|
|
slog.Error("Unable to write response body", slog.Any("error", err))
|
2023-02-25 09:36:19 +01:00
|
|
|
}
|
2018-10-08 03:42:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Builder) writeHeaders() {
|
|
|
|
b.headers["X-Content-Type-Options"] = "nosniff"
|
|
|
|
b.headers["X-Frame-Options"] = "DENY"
|
2021-02-14 20:16:06 +01:00
|
|
|
b.headers["Referrer-Policy"] = "no-referrer"
|
2018-10-08 03:42:43 +02:00
|
|
|
|
|
|
|
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}
|
|
|
|
}
|