325 lines
7.2 KiB
Go
325 lines
7.2 KiB
Go
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"mime"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func addVary(header http.Header, value string) {
|
|
for _, line := range header.Values("Vary") {
|
|
for _, existing := range strings.Split(line, ",") {
|
|
if strings.EqualFold(strings.TrimSpace(existing), value) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
header.Add("Vary", value)
|
|
}
|
|
|
|
//nolint:unused
|
|
func isCompressibleContentType(value string) bool {
|
|
mediaType, _, err := mime.ParseMediaType(value)
|
|
if err != nil {
|
|
mediaType = strings.TrimSpace(strings.Split(value, ";")[0])
|
|
}
|
|
mediaType = strings.ToLower(mediaType)
|
|
if strings.HasPrefix(mediaType, "text/") || strings.HasSuffix(mediaType, "+json") || strings.HasSuffix(mediaType, "+xml") {
|
|
return true
|
|
}
|
|
switch mediaType {
|
|
case "application/javascript", "application/json", "application/manifest+json", "application/x-javascript", "application/xhtml+xml", "application/xml", "image/svg+xml":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isCompressedPath(path string) bool {
|
|
switch strings.ToLower(filepath.Ext(path)) {
|
|
case ".7z", ".avif", ".br", ".gif", ".gz", ".ico", ".jpeg", ".jpg", ".m4a", ".m4v", ".mov", ".mp3", ".mp4", ".oga", ".ogg", ".ogv", ".pdf", ".png", ".rar", ".webm", ".webp", ".woff", ".woff2", ".zip":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isImagePath(path string) bool {
|
|
switch strings.ToLower(filepath.Ext(path)) {
|
|
case ".avif", ".gif", ".ico", ".jpeg", ".jpg", ".png", ".svg", ".webp":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func staticCachePolicy(r *http.Request) string {
|
|
path := r.URL.Path
|
|
switch {
|
|
case strings.HasPrefix(path, "/dist/"):
|
|
if r.URL.Query().Get("v") != "" {
|
|
return "public, max-age=31536000, immutable"
|
|
}
|
|
return "public, max-age=0, must-revalidate"
|
|
case strings.HasPrefix(path, "/static/assets/") && isImagePath(path):
|
|
return "public, max-age=86400"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
type cacheControlWriter struct {
|
|
gin.ResponseWriter
|
|
policy string
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *cacheControlWriter) WriteHeader(status int) {
|
|
w.apply(status)
|
|
w.ResponseWriter.WriteHeader(status)
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *cacheControlWriter) WriteHeaderNow() {
|
|
w.apply(w.Status())
|
|
w.ResponseWriter.WriteHeaderNow()
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *cacheControlWriter) Write(data []byte) (int, error) {
|
|
if !w.Written() {
|
|
w.apply(w.Status())
|
|
}
|
|
return w.ResponseWriter.Write(data)
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *cacheControlWriter) WriteString(data string) (int, error) {
|
|
if !w.Written() {
|
|
w.apply(w.Status())
|
|
}
|
|
return w.ResponseWriter.WriteString(data)
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *cacheControlWriter) Flush() {
|
|
if !w.Written() {
|
|
w.apply(w.Status())
|
|
}
|
|
w.ResponseWriter.Flush()
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *cacheControlWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
return w.ResponseWriter.Hijack()
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *cacheControlWriter) apply(status int) {
|
|
if status >= http.StatusOK && status < http.StatusMultipleChoices || status == http.StatusNotModified {
|
|
w.Header().Set("Cache-Control", w.policy)
|
|
} else {
|
|
w.Header().Del("Cache-Control")
|
|
}
|
|
}
|
|
|
|
//nolint:unused
|
|
func StaticCacheMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead {
|
|
return
|
|
}
|
|
|
|
policy := staticCachePolicy(c.Request)
|
|
if policy == "" {
|
|
return
|
|
}
|
|
|
|
c.Writer = &cacheControlWriter{ResponseWriter: c.Writer, policy: policy}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
//nolint:unused
|
|
type compressionMode uint8
|
|
|
|
const (
|
|
compressionUndecided compressionMode = iota //nolint:unused
|
|
compressionPlain //nolint:unused
|
|
compressionGzip //nolint:unused
|
|
)
|
|
|
|
//nolint:unused
|
|
const compressionMinLength = 1024
|
|
|
|
//nolint:unused
|
|
func acceptsGzip(value string) bool {
|
|
wildcard := false
|
|
for _, part := range strings.Split(value, ",") {
|
|
params := strings.Split(part, ";")
|
|
encoding := strings.TrimSpace(strings.ToLower(params[0]))
|
|
quality := 1.0
|
|
for _, param := range params[1:] {
|
|
name, raw, ok := strings.Cut(strings.TrimSpace(param), "=")
|
|
if !ok || !strings.EqualFold(name, "q") {
|
|
continue
|
|
}
|
|
parsed, err := strconv.ParseFloat(raw, 64)
|
|
if err != nil {
|
|
quality = 0
|
|
} else {
|
|
quality = parsed
|
|
}
|
|
}
|
|
if encoding == "gzip" {
|
|
return quality > 0
|
|
}
|
|
if encoding == "*" && quality > 0 {
|
|
wildcard = true
|
|
}
|
|
}
|
|
return wildcard
|
|
}
|
|
|
|
//nolint:unused
|
|
func canNegotiateCompression(r *http.Request) bool {
|
|
if r.Method == http.MethodHead || r.Header.Get("Range") != "" {
|
|
return false
|
|
}
|
|
if strings.EqualFold(r.Header.Get("Connection"), "Upgrade") {
|
|
return false
|
|
}
|
|
if r.URL.Path == "/watch/proxy/stream" || r.URL.Path == "/watch/proxy/subtitle" {
|
|
return false
|
|
}
|
|
return !isCompressedPath(r.URL.Path)
|
|
}
|
|
|
|
//nolint:unused
|
|
type compressionWriter struct {
|
|
gin.ResponseWriter
|
|
buffer bytes.Buffer
|
|
gzip *gzip.Writer
|
|
mode compressionMode
|
|
status int
|
|
wroteBody bool
|
|
acceptsGzip bool
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) WriteHeader(status int) {
|
|
if status > 0 && w.mode == compressionUndecided && !w.wroteBody {
|
|
w.status = status
|
|
}
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) WriteHeaderNow() {
|
|
if w.mode == compressionUndecided {
|
|
w.startPlain()
|
|
_ = w.flushBuffer()
|
|
}
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) WriteString(data string) (int, error) {
|
|
return w.Write([]byte(data))
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) Status() int {
|
|
if w.status != 0 {
|
|
return w.status
|
|
}
|
|
return w.ResponseWriter.Status()
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) Size() int {
|
|
if size := w.ResponseWriter.Size(); size >= 0 {
|
|
return size
|
|
}
|
|
if w.buffer.Len() > 0 {
|
|
return w.buffer.Len()
|
|
}
|
|
return -1
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) Written() bool {
|
|
return w.wroteBody || w.ResponseWriter.Written()
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) startPlain() {
|
|
if w.mode != compressionUndecided {
|
|
return
|
|
}
|
|
w.mode = compressionPlain
|
|
w.ResponseWriter.WriteHeader(w.Status())
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) flushBuffer() error {
|
|
if w.buffer.Len() == 0 {
|
|
return nil
|
|
}
|
|
data := w.buffer.Bytes()
|
|
w.buffer.Reset()
|
|
var err error
|
|
if w.mode == compressionGzip {
|
|
_, err = w.gzip.Write(data)
|
|
} else {
|
|
//nolint:staticcheck
|
|
_, err = w.ResponseWriter.Write(data)
|
|
}
|
|
return err
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) writePlain(data []byte) (int, error) {
|
|
w.startPlain()
|
|
if err := w.flushBuffer(); err != nil {
|
|
return 0, err
|
|
}
|
|
//nolint:staticcheck
|
|
return w.ResponseWriter.Write(data)
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) startGzip() {
|
|
if w.mode != compressionUndecided {
|
|
return
|
|
}
|
|
w.mode = compressionGzip
|
|
w.Header().Del("Content-Length")
|
|
if etag := w.Header().Get("ETag"); etag != "" && !strings.HasPrefix(etag, "W/") {
|
|
w.Header().Set("ETag", "W/"+etag)
|
|
}
|
|
addVary(w.Header(), "Accept-Encoding")
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
w.ResponseWriter.WriteHeader(w.Status())
|
|
w.gzip = gzip.NewWriter(w.ResponseWriter)
|
|
}
|
|
|
|
//nolint:unused
|
|
func (w *compressionWriter) knownContentLength() (compress, known bool) {
|
|
contentLength := w.Header().Get("Content-Length")
|
|
if contentLength == "" {
|
|
return false, false
|
|
}
|
|
length, err := strconv.ParseInt(contentLength, 10, 64)
|
|
if err != nil {
|
|
return false, false
|
|
}
|
|
return length >= compressionMinLength, true
|
|
}
|