113 lines
2.6 KiB
Go
113 lines
2.6 KiB
Go
// Keywarden - Centralized SSH Key Management and Deployment
|
|
// Copyright (C) 2026 Patrick Asmus (scriptos)
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package security
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// compressibleTypes lists MIME types that benefit from gzip compression.
|
|
// Binary formats like woff2, images, etc. are already compressed.
|
|
var compressibleTypes = map[string]bool{
|
|
"text/html": true,
|
|
"text/css": true,
|
|
"text/plain": true,
|
|
"text/javascript": true,
|
|
"application/javascript": true,
|
|
"application/json": true,
|
|
"application/xml": true,
|
|
"image/svg+xml": true,
|
|
}
|
|
|
|
var gzipWriterPool = sync.Pool{
|
|
New: func() interface{} {
|
|
w, _ := gzip.NewWriterLevel(io.Discard, gzip.BestSpeed)
|
|
return w
|
|
},
|
|
}
|
|
|
|
// GzipMiddleware compresses HTTP responses for clients that accept gzip.
|
|
// Only compressible content types (text, CSS, JS, JSON, SVG) are compressed;
|
|
// already-compressed formats (woff2, images) are passed through unchanged.
|
|
func GzipMiddleware() func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
gz := gzipWriterPool.Get().(*gzip.Writer)
|
|
gz.Reset(w)
|
|
|
|
grw := &gzipResponseWriter{
|
|
ResponseWriter: w,
|
|
gz: gz,
|
|
}
|
|
|
|
next.ServeHTTP(grw, r)
|
|
|
|
if grw.compressed {
|
|
gz.Close()
|
|
}
|
|
gzipWriterPool.Put(gz)
|
|
})
|
|
}
|
|
}
|
|
|
|
type gzipResponseWriter struct {
|
|
http.ResponseWriter
|
|
gz *gzip.Writer
|
|
compressed bool
|
|
decided bool
|
|
}
|
|
|
|
func (w *gzipResponseWriter) WriteHeader(code int) {
|
|
if w.decided {
|
|
w.ResponseWriter.WriteHeader(code)
|
|
return
|
|
}
|
|
w.decided = true
|
|
|
|
// Only compress successful full responses (not 304, 206, redirects, errors)
|
|
if code == http.StatusOK {
|
|
ct := w.Header().Get("Content-Type")
|
|
if idx := strings.Index(ct, ";"); idx >= 0 {
|
|
ct = strings.TrimSpace(ct[:idx])
|
|
}
|
|
if compressibleTypes[ct] {
|
|
w.compressed = true
|
|
w.Header().Del("Content-Length")
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
w.Header().Add("Vary", "Accept-Encoding")
|
|
}
|
|
}
|
|
w.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
|
|
if !w.decided {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
if w.compressed {
|
|
return w.gz.Write(b)
|
|
}
|
|
return w.ResponseWriter.Write(b)
|
|
}
|
|
|
|
// Flush implements http.Flusher for streaming responses.
|
|
func (w *gzipResponseWriter) Flush() {
|
|
if w.compressed {
|
|
w.gz.Flush()
|
|
}
|
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|