// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . //go:build plus package minifiers import ( "bytes" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/iwind/TeaGo/types" "github.com/tdewolff/minify/v2" "github.com/tdewolff/minify/v2/css" "github.com/tdewolff/minify/v2/html" "github.com/tdewolff/minify/v2/js" "io" "net/http" "strings" "sync" ) var instanceLocker = &sync.Mutex{} var limiter = make(chan bool, 64) // MinifyResponse minify response body func MinifyResponse(config *serverconfigs.HTTPPageOptimizationConfig, url string, resp *http.Response) error { if !config.IsOn() { return nil } var contentType = resp.Header.Get("Content-Type") if len(contentType) == 0 { return nil } // validate content length if resp.ContentLength <= 0 || resp.ContentLength > (1<<20) { return nil } contentType, _, _ = strings.Cut(contentType, ";") var mimeType = "" switch contentType { case "text/html": if config.HTML != nil && config.HTML.IsOn && config.HTML.MatchURL(url) { mimeType = serverconfigs.HTTPPageOptimizationMimeTypeHTML } case "text/javascript", "application/javascript": if config.Javascript != nil && config.Javascript.IsOn && config.Javascript.MatchURL(url) { mimeType = serverconfigs.HTTPPageOptimizationMimeTypeJavascript } case "text/css": if config.CSS != nil && config.CSS.IsOn && config.CSS.MatchURL(url) { mimeType = serverconfigs.HTTPPageOptimizationMimeTypeCSS } default: return nil } if len(mimeType) == 0 { return nil } var minifier = getMinifier(config) if minifier == nil { return nil } // concurrent limiter, to prevent memory overflow select { case limiter <- true: defer func() { <-limiter }() var contentLength int64 var err error resp.Body, contentLength, err = minifyReader(minifier, mimeType, resp.Body, resp.ContentLength) if err != nil { return err } // fix resp.ContentLength and Content-Length header resp.ContentLength = contentLength resp.Header.Set("Content-Length", types.String(contentLength)) default: } return nil } func getMinifier(config *serverconfigs.HTTPPageOptimizationConfig) *minify.M { if !config.IsOn() { return nil } // check existence { var instance = config.InternalInstance() if instance != nil { return instance.(*minify.M) } } instanceLocker.Lock() defer instanceLocker.Unlock() // check again { var instance = config.InternalInstance() if instance != nil { return instance.(*minify.M) } } var instance = minify.New() if config.HTML != nil && config.HTML.IsOn { instance.Add(serverconfigs.HTTPPageOptimizationMimeTypeHTML, asHTMLModifier(config.HTML)) } if config.Javascript != nil && config.Javascript.IsOn { instance.Add(serverconfigs.HTTPPageOptimizationMimeTypeJavascript, asJavascriptMinifier(config.Javascript)) } if config.CSS != nil && config.CSS.IsOn { instance.Add(serverconfigs.HTTPPageOptimizationMimeTypeCSS, asCSSMinifier(config.CSS)) } config.SetInternalInstance(instance) return instance } func minifyReader(instance *minify.M, mimeType serverconfigs.HTTPPageOptimizationMimeType, rawReader io.ReadCloser, oldContentLength int64) (newReader io.ReadCloser, newContentLength int64, err error) { if instance == nil { newReader = rawReader newContentLength = oldContentLength return } var rawData []byte rawData, err = io.ReadAll(rawReader) if err != nil { return } resultData, err := instance.Bytes(mimeType, rawData) if err != nil { return io.NopCloser(bytes.NewReader(rawData)), int64(len(rawData)), nil // return rawData, and ignore error } return io.NopCloser(bytes.NewReader(resultData)), int64(len(resultData)), nil } func asHTMLModifier(config *serverconfigs.HTTPHTMLOptimizationConfig) *html.Minifier { return &html.Minifier{ KeepComments: config.KeepComments, KeepConditionalComments: config.KeepConditionalComments, KeepDefaultAttrVals: config.KeepDefaultAttrVals, KeepDocumentTags: config.KeepDocumentTags, KeepEndTags: config.KeepEndTags, KeepQuotes: config.KeepQuotes, KeepWhitespace: config.KeepWhitespace, } } func asJavascriptMinifier(config *serverconfigs.HTTPJavascriptOptimizationConfig) *js.Minifier { return &js.Minifier{ Precision: config.Precision, KeepVarNames: config.KeepVarNames, Version: config.Version, } } func asCSSMinifier(config *serverconfigs.HTTPCSSOptimizationConfig) *css.Minifier { return &css.Minifier{ KeepCSS2: config.KeepCSS2, Precision: config.Precision, } }