From 389077e0fa3bd83aa12eaf385c0bcda9c3551be0 Mon Sep 17 00:00:00 2001 From: Andrew Gaul Date: Thu, 12 Jan 2017 23:17:24 -0800 Subject: [PATCH] Transcode via Brotli Brotli offers 20% better compression than gzip. --- README.md | 2 +- compy.go | 5 ++-- compy_test.go | 20 ++++++++++++++- transcoder/gzip.go | 54 --------------------------------------- transcoder/zip.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 58 deletions(-) delete mode 100644 transcoder/gzip.go create mode 100644 transcoder/zip.go diff --git a/README.md b/README.md index 4271249..87374a6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Features: - HTTPS proxy (encrypted connection between client and proxy) - man in the middle support (compress HTTPS traffic) - HTTP2 support (over TLS) -- gzip compression +- Brotli and gzip compression - transcode animated GIFs to static images - transcode JPEG images to desired quality using libjpeg - transcode PNG and JPEG images to WebP diff --git a/compy.go b/compy.go index 1654813..1068285 100644 --- a/compy.go +++ b/compy.go @@ -18,6 +18,7 @@ var ( ca = flag.String("ca", "", "CA path") caKey = flag.String("cakey", "", "CA key path") + brotli = flag.Int("brotli", -1, "Brotli compression level (0-11, default 6)") jpeg = flag.Int("jpeg", 50, "jpeg quality (1-100, 0 to disable)") gif = flag.Bool("gif", true, "transcode gifs into static images") gzip = flag.Int("gzip", -1, "gzip compression level (0-9, default 6)") @@ -56,9 +57,9 @@ func main() { var ttc proxy.Transcoder if *minify { - ttc = &tc.Gzip{tc.NewMinifier(), *gzip, false} + ttc = &tc.Zip{tc.NewMinifier(), *brotli, *gzip, false} } else { - ttc = &tc.Gzip{&tc.Identity{}, *gzip, true} + ttc = &tc.Zip{&tc.Identity{}, *brotli, *gzip, true} } p.AddTranscoder("text/css", ttc) diff --git a/compy_test.go b/compy_test.go index db12280..a728b8b 100644 --- a/compy_test.go +++ b/compy_test.go @@ -15,6 +15,7 @@ import ( "github.com/barnacs/compy/proxy" tc "github.com/barnacs/compy/transcoder" "github.com/chai2010/webp" + brotlidec "gopkg.in/kothar/brotli-go.v0/dec" ) func Test(t *testing.T) { @@ -34,7 +35,7 @@ func (s *CompyTest) SetUpSuite(c *C) { s.proxy = proxy.New() s.proxy.AddTranscoder("image/jpeg", tc.NewJpeg(50)) - s.proxy.AddTranscoder("text/html", &tc.Gzip{&tc.Identity{}, *gzip, true}) + s.proxy.AddTranscoder("text/html", &tc.Zip{&tc.Identity{}, *brotli, *gzip, true}) go func() { err := s.proxy.Start(*host) if err != nil { @@ -91,6 +92,23 @@ func (s *CompyTest) TestGzip(c *C) { c.Assert(err, IsNil) } +func (s *CompyTest) TestBrotli(c *C) { + req, err := http.NewRequest("GET", s.server.URL+"/html", nil) + c.Assert(err, IsNil) + req.Header.Add("Accept-Encoding", "br, gzip") + + resp, err := s.client.Do(req) + c.Assert(err, IsNil) + defer resp.Body.Close() + c.Assert(resp.StatusCode, Equals, 200) + c.Assert(resp.Header.Get("Content-Encoding"), Equals, "br") + + brr := brotlidec.NewBrotliReader(resp.Body) + defer brr.Close() + _, err = ioutil.ReadAll(brr) + c.Assert(err, IsNil) +} + func (s *CompyTest) TestJpeg(c *C) { req, err := http.NewRequest("GET", s.server.URL+"/image/jpeg", nil) c.Assert(err, IsNil) diff --git a/transcoder/gzip.go b/transcoder/gzip.go deleted file mode 100644 index c088691..0000000 --- a/transcoder/gzip.go +++ /dev/null @@ -1,54 +0,0 @@ -package transcoder - -import ( - "compress/gzip" - "github.com/barnacs/compy/proxy" - "net/http" - "strings" -) - -type Gzip struct { - proxy.Transcoder - CompressionLevel int - SkipGzipped bool -} - -func (t *Gzip) Transcode(w *proxy.ResponseWriter, r *proxy.ResponseReader, headers http.Header) error { - if t.decompress(r) { - gzr, err := gzip.NewReader(r.Reader) - if err != nil { - return err - } - defer gzr.Close() - r.Reader = gzr - r.Header().Del("Content-Encoding") - w.Header().Del("Content-Encoding") - } - - shouldGzip := false - for _, v := range strings.Split(headers.Get("Accept-Encoding"), ", ") { - if strings.SplitN(v, ";", 2)[0] == "gzip" { - shouldGzip = true - break - } - } - - if shouldGzip && compress(r) { - gzw, err := gzip.NewWriterLevel(w.Writer, t.CompressionLevel) - if err != nil { - return err - } - defer gzw.Close() - w.Writer = gzw - w.Header().Set("Content-Encoding", "gzip") - } - return t.Transcoder.Transcode(w, r, headers) -} - -func (t *Gzip) decompress(r *proxy.ResponseReader) bool { - return !t.SkipGzipped && r.Header().Get("Content-Encoding") == "gzip" -} - -func compress(r *proxy.ResponseReader) bool { - return r.Header().Get("Content-Encoding") == "" -} diff --git a/transcoder/zip.go b/transcoder/zip.go new file mode 100644 index 0000000..70c2b99 --- /dev/null +++ b/transcoder/zip.go @@ -0,0 +1,63 @@ +package transcoder + +import ( + "compress/gzip" + "github.com/barnacs/compy/proxy" + brotlienc "gopkg.in/kothar/brotli-go.v0/enc" + "net/http" + "strings" +) + +type Zip struct { + proxy.Transcoder + BrotliCompressionLevel int + GzipCompressionLevel int + SkipGzipped bool +} + +func (t *Zip) Transcode(w *proxy.ResponseWriter, r *proxy.ResponseReader, headers http.Header) error { + shouldBrotli := false + shouldGzip := false + for _, v := range strings.Split(headers.Get("Accept-Encoding"), ", ") { + switch strings.SplitN(v, ";", 2)[0] { + case "br": + shouldBrotli = true + case "gzip": + shouldGzip = true + } + } + + // always gunzip if the client supports Brotli + if r.Header().Get("Content-Encoding") == "gzip" && (shouldBrotli || !t.SkipGzipped) { + gzr, err := gzip.NewReader(r.Reader) + if err != nil { + return err + } + defer gzr.Close() + r.Reader = gzr + r.Header().Del("Content-Encoding") + w.Header().Del("Content-Encoding") + } + + if shouldBrotli && compress(r) { + params := brotlienc.NewBrotliParams() + params.SetQuality(t.BrotliCompressionLevel) + brw := brotlienc.NewBrotliWriter(params, w.Writer) + defer brw.Close() + w.Writer = brw + w.Header().Set("Content-Encoding", "br") + } else if shouldGzip && compress(r) { + gzw, err := gzip.NewWriterLevel(w.Writer, t.GzipCompressionLevel) + if err != nil { + return err + } + defer gzw.Close() + w.Writer = gzw + w.Header().Set("Content-Encoding", "gzip") + } + return t.Transcoder.Transcode(w, r, headers) +} + +func compress(r *proxy.ResponseReader) bool { + return r.Header().Get("Content-Encoding") == "" +}