diff --git a/gweb/gzip/gzip.go b/gweb/gzip/gzip.go new file mode 100644 index 0000000..7e95716 --- /dev/null +++ b/gweb/gzip/gzip.go @@ -0,0 +1,46 @@ +// +// gzip.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gzip + +import ( + "compress/gzip" + + "github.com/gin-gonic/gin" +) + +const ( + BestCompression = gzip.BestCompression + BestSpeed = gzip.BestSpeed + DefaultCompression = gzip.DefaultCompression + NoCompression = gzip.NoCompression +) + +func Gzip(level int, options ...Option) gin.HandlerFunc { + return newGzipHandler(level, options...).Handle +} + +type gzipWriter struct { + gin.ResponseWriter + writer *gzip.Writer +} + +func (g *gzipWriter) WriteString(s string) (int, error) { + g.Header().Del("Content-Length") + return g.writer.Write([]byte(s)) +} + +func (g *gzipWriter) Write(data []byte) (int, error) { + g.Header().Del("Content-Length") + return g.writer.Write(data) +} + +// Fix: https://github.com/mholt/caddy/issues/38 +func (g *gzipWriter) WriteHeader(code int) { + g.Header().Del("Content-Length") + g.ResponseWriter.WriteHeader(code) +} diff --git a/gweb/gzip/handler.go b/gweb/gzip/handler.go new file mode 100644 index 0000000..b6e854c --- /dev/null +++ b/gweb/gzip/handler.go @@ -0,0 +1,90 @@ +// +// handler.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gzip + +import ( + "compress/gzip" + "fmt" + "io" + "net/http" + "path/filepath" + "strings" + "sync" + + "github.com/gin-gonic/gin" +) + +type gzipHandler struct { + *Options + gzPool sync.Pool +} + +func newGzipHandler(level int, options ...Option) *gzipHandler { + handler := &gzipHandler{ + Options: DefaultOptions, + gzPool: sync.Pool{ + New: func() interface{} { + gz, err := gzip.NewWriterLevel(io.Discard, level) + if err != nil { + panic(err) + } + return gz + }, + }, + } + for _, setter := range options { + setter(handler.Options) + } + return handler +} + +func (g *gzipHandler) Handle(c *gin.Context) { + if fn := g.DecompressFn; fn != nil && c.Request.Header.Get("Content-Encoding") == "gzip" { + fn(c) + } + + if !g.shouldCompress(c.Request) { + return + } + + gz := g.gzPool.Get().(*gzip.Writer) + defer g.gzPool.Put(gz) + defer gz.Reset(io.Discard) + gz.Reset(c.Writer) + + c.Header("Content-Encoding", "gzip") + c.Header("Vary", "Accept-Encoding") + c.Writer = &gzipWriter{c.Writer, gz} + defer func() { + gz.Close() + c.Header("Content-Length", fmt.Sprint(c.Writer.Size())) + }() + c.Next() +} + +func (g *gzipHandler) shouldCompress(req *http.Request) bool { + if !strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") || + strings.Contains(req.Header.Get("Connection"), "Upgrade") || + strings.Contains(req.Header.Get("Accept"), "text/event-stream") { + return false + } + + extension := filepath.Ext(req.URL.Path) + if g.ExcludedExtensions.Contains(extension) { + return false + } + + if g.ExcludedPaths.Contains(req.URL.Path) { + return false + } + if g.ExcludedPathesRegexs.Contains(req.URL.Path) { + return false + } + + return true +} diff --git a/gweb/gzip/options.go b/gweb/gzip/options.go new file mode 100644 index 0000000..75bca12 --- /dev/null +++ b/gweb/gzip/options.go @@ -0,0 +1,123 @@ +// +// options.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gzip + +import ( + "compress/gzip" + "net/http" + "regexp" + "strings" + + "github.com/gin-gonic/gin" +) + +var ( + DefaultExcludedExtentions = NewExcludedExtensions([]string{ + ".png", ".gif", ".jpeg", ".jpg", + }) + DefaultOptions = &Options{ + ExcludedExtensions: DefaultExcludedExtentions, + } +) + +type Options struct { + ExcludedExtensions ExcludedExtensions + ExcludedPaths ExcludedPaths + ExcludedPathesRegexs ExcludedPathesRegexs + DecompressFn func(c *gin.Context) +} + +type Option func(*Options) + +func WithExcludedExtensions(args []string) Option { + return func(o *Options) { + o.ExcludedExtensions = NewExcludedExtensions(args) + } +} + +func WithExcludedPaths(args []string) Option { + return func(o *Options) { + o.ExcludedPaths = NewExcludedPaths(args) + } +} + +func WithExcludedPathsRegexs(args []string) Option { + return func(o *Options) { + o.ExcludedPathesRegexs = NewExcludedPathesRegexs(args) + } +} + +func WithDecompressFn(decompressFn func(c *gin.Context)) Option { + return func(o *Options) { + o.DecompressFn = decompressFn + } +} + +// Using map for better lookup performance +type ExcludedExtensions map[string]bool + +func NewExcludedExtensions(extensions []string) ExcludedExtensions { + res := make(ExcludedExtensions) + for _, e := range extensions { + res[e] = true + } + return res +} + +func (e ExcludedExtensions) Contains(target string) bool { + _, ok := e[target] + return ok +} + +type ExcludedPaths []string + +func NewExcludedPaths(paths []string) ExcludedPaths { + return ExcludedPaths(paths) +} + +func (e ExcludedPaths) Contains(requestURI string) bool { + for _, path := range e { + if strings.HasPrefix(requestURI, path) { + return true + } + } + return false +} + +type ExcludedPathesRegexs []*regexp.Regexp + +func NewExcludedPathesRegexs(regexs []string) ExcludedPathesRegexs { + result := make([]*regexp.Regexp, len(regexs)) + for i, reg := range regexs { + result[i] = regexp.MustCompile(reg) + } + return result +} + +func (e ExcludedPathesRegexs) Contains(requestURI string) bool { + for _, reg := range e { + if reg.MatchString(requestURI) { + return true + } + } + return false +} + +func DefaultDecompressHandle(c *gin.Context) { + if c.Request.Body == nil { + return + } + r, err := gzip.NewReader(c.Request.Body) + if err != nil { + _ = c.AbortWithError(http.StatusBadRequest, err) + return + } + c.Request.Header.Del("Content-Encoding") + c.Request.Header.Del("Content-Length") + c.Request.Body = r +} diff --git a/gweb/gzip/readme.md b/gweb/gzip/readme.md new file mode 100644 index 0000000..fee182a --- /dev/null +++ b/gweb/gzip/readme.md @@ -0,0 +1,93 @@ +# GZIP gin's middleware + + +Gin middleware to enable `GZIP` support. + +## Usage + +Customized Excluded Extensions + + +```go +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-contrib/gzip" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp4"}))) + r.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) + }) + + // Listen and Server in 0.0.0.0:8080 + if err := r.Run(":8080"); err != nil { + log.Fatal(err) + } +} +``` + +Customized Excluded Paths + +```go +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-contrib/gzip" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{"/api/"}))) + r.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) + }) + + // Listen and Server in 0.0.0.0:8080 + if err := r.Run(":8080"); err != nil { + log.Fatal(err) + } +} +``` + +Customized Excluded Paths + +```go +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-contrib/gzip" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPathsRegexs([]string{".*"}))) + r.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) + }) + + // Listen and Server in 0.0.0.0:8080 + if err := r.Run(":8080"); err != nil { + log.Fatal(err) + } +} +``` + +