Files
website/app/internal/middleware/middleware.go
Jimmy 6c215d40e6 Add contact antispam and fix gallery video playback.
English-only messages, rate limiting, min fill time, and normalized email
validation; improve modal video serving with posters, correct MIME types, and
no gzip on gallery media.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 00:38:48 +12:00

104 lines
2.6 KiB
Go

package middleware
import (
"compress/gzip"
"io"
"mime"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
)
type gzipResponseWriter struct {
http.ResponseWriter
io.Writer
}
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
// Gzip compresses responses when the client accepts it.
func Gzip(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") || shouldSkipGzip(r) {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Vary", "Accept-Encoding")
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
})
}
// CacheStatic wraps a file server with long-lived cache headers.
func CacheStatic(dir string, maxAge time.Duration) http.Handler {
fs := http.FileServer(http.Dir(dir))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, max-age="+formatMaxAge(maxAge)+", immutable")
fs.ServeHTTP(w, r)
})
}
// CacheImages wraps image serving; thumbs cache longer than originals.
func CacheImages(dir string) http.Handler {
fs := http.FileServer(http.Dir(dir))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rel := strings.TrimPrefix(r.URL.Path, "/")
if strings.HasPrefix(rel, "thumbs/") || strings.HasPrefix(rel, "hero/") {
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
} else {
w.Header().Set("Cache-Control", "public, max-age=86400")
}
if ct := mediaContentType(r.URL.Path); ct != "" {
w.Header().Set("Content-Type", ct)
}
fs.ServeHTTP(w, r)
})
}
func mediaContentType(urlPath string) string {
switch strings.ToLower(filepath.Ext(urlPath)) {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".mp4":
return "video/mp4"
case ".webm":
return "video/webm"
case ".mov":
return "video/quicktime"
default:
return ""
}
}
func formatMaxAge(d time.Duration) string {
return strconv.Itoa(int(d.Seconds()))
}
func shouldSkipGzip(r *http.Request) bool {
// Range requests must stay byte-accurate for media seeking/playback.
if r.Header.Get("Range") != "" {
return true
}
// Gallery originals and derivatives must never be gzip-compressed.
if strings.HasPrefix(r.URL.Path, "/images/") {
return true
}
ext := strings.ToLower(filepath.Ext(r.URL.Path))
if ext == "" {
return false
}
ct := mime.TypeByExtension(ext)
return strings.HasPrefix(ct, "image/") ||
strings.HasPrefix(ct, "video/") ||
strings.HasPrefix(ct, "audio/")
}