Files
3x-ui/internal/web/middleware/gzip_static.go
T
MHSanaei 3526a65f5d fix(web): bound the panel's open connections and share asset gzip
The engine-wide gzip middleware gave every in-flight request its own
compressor of about 1 MiB, and the public /assets bundles let an
anonymous client keep a handler blocked in Write until WriteTimeout, so
a thousand stalled downloads held a gigabyte. The listener itself took
any number of connections, each holding buffers and a goroutine before
any login.

The bundles are now served from a gzip copy made once per file, with
the assets prefix left out of the per-request middleware, and the panel
listener closes a new connection once 2048 are open or its source (an
IPv4 address or IPv6 /64) holds 256; a loopback reverse proxy counts
only toward the total.
2026-11-01 01:01:38 +01:00

65 lines
1.7 KiB
Go

package middleware
import (
"bytes"
"compress/gzip"
"io/fs"
"net/http"
"path"
"strconv"
"strings"
"sync"
"github.com/gin-gonic/gin"
)
// GzipStatic serves a static route's "*filepath" from a gzip copy made once per
// file, so a stalled download holds shared bytes instead of its own compressor.
func GzipStatic(files fs.FS) gin.HandlerFunc {
var compressed sync.Map // name -> func() []byte
return func(c *gin.Context) {
if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead {
return
}
c.Writer.Header().Add("Vary", "Accept-Encoding")
if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") {
return
}
name := strings.TrimPrefix(path.Clean("/"+c.Param("filepath")), "/")
// Only names that exist get an entry, so requests cannot grow the map.
info, err := fs.Stat(files, name)
if err != nil || info.IsDir() {
return
}
once, _ := compressed.LoadOrStore(name, sync.OnceValue(func() []byte {
return gzipFile(files, name)
}))
body := once.(func() []byte)()
if body == nil {
return
}
c.Header("Content-Encoding", "gzip")
// ServeContent leaves Content-Length unset once Content-Encoding is present.
c.Header("Content-Length", strconv.Itoa(len(body)))
http.ServeContent(c.Writer, c.Request, name, info.ModTime(), bytes.NewReader(body))
c.Abort()
}
}
// gzipFile returns the file compressed, or nil to leave it to the static route.
func gzipFile(files fs.FS, name string) []byte {
raw, err := fs.ReadFile(files, name)
if err != nil {
return nil
}
var buf bytes.Buffer
zw, _ := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if _, err := zw.Write(raw); err != nil {
return nil
}
if err := zw.Close(); err != nil {
return nil
}
return buf.Bytes()
}