diff --git a/README.md b/README.md index 70c6bde7..7331c8cb 100644 --- a/README.md +++ b/README.md @@ -61,4 +61,5 @@ Third party libraries and resources: * [Firebase Admin SDK](https://github.com/firebase/firebase-admin-go) (Apache 2.0) is used to send FCM messages * [github/gemoji](https://github.com/github/gemoji) (MIT) is used for emoji support (specifically the [emoji.json](https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json) file) * [Lightbox with vanilla JS](https://yossiabramov.com/blog/vanilla-js-lightbox) as a lightbox on the landing page +* [HTTP middleware for gzip compression](https://gist.github.com/CJEnright/bc2d8b8dc0c1389a9feeddb110f822d7) (MIT) is used for serving static files * [Statically linking go-sqlite3](https://www.arp242.net/static-go.html) diff --git a/server/server.go b/server/server.go index a2de50d5..181f060f 100644 --- a/server/server.go +++ b/server/server.go @@ -351,12 +351,12 @@ var config = { func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error { r.URL.Path = webSiteDir + r.URL.Path - http.FileServer(http.FS(webFsCached)).ServeHTTP(w, r) + util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r) return nil } func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error { - http.FileServer(http.FS(docsStaticCached)).ServeHTTP(w, r) + util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r) return nil } diff --git a/util/gzip_handler.go b/util/gzip_handler.go new file mode 100644 index 00000000..613df48e --- /dev/null +++ b/util/gzip_handler.go @@ -0,0 +1,52 @@ +package util + +import ( + "compress/gzip" + "io" + "io/ioutil" + "net/http" + "strings" + "sync" +) + +// Gzip is a HTTP middleware to transparently compress responses using gzip. +// Original code from https://gist.github.com/CJEnright/bc2d8b8dc0c1389a9feeddb110f822d7 (MIT) +func Gzip(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + next.ServeHTTP(w, r) + return + } + w.Header().Set("Content-Encoding", "gzip") + + gz := gzPool.Get().(*gzip.Writer) + defer gzPool.Put(gz) + + gz.Reset(w) + defer gz.Close() + + r.Header.Del("Accept-Encoding") // prevent double-gzipping + next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r) + }) +} + +var gzPool = sync.Pool{ + New: func() interface{} { + w := gzip.NewWriter(ioutil.Discard) + return w + }, +} + +type gzipResponseWriter struct { + io.Writer + http.ResponseWriter +} + +func (w *gzipResponseWriter) WriteHeader(status int) { + w.Header().Del("Content-Length") + w.ResponseWriter.WriteHeader(status) +} + +func (w *gzipResponseWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} diff --git a/util/gzip_handler_test.go b/util/gzip_handler_test.go new file mode 100644 index 00000000..21b2358b --- /dev/null +++ b/util/gzip_handler_test.go @@ -0,0 +1,40 @@ +package util + +import ( + "compress/gzip" + "github.com/stretchr/testify/require" + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestGzipHandler(t *testing.T) { + s := Gzip(http.FileServer(http.FS(testFs))) + + rr := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/embedfs/test.txt", nil) + req.Header.Set("Accept-Encoding", "gzip, deflate") + s.ServeHTTP(rr, req) + require.Equal(t, 200, rr.Code) + require.Equal(t, "gzip", rr.Header().Get("Content-Encoding")) + require.Equal(t, "", rr.Header().Get("Content-Length")) + + gz, _ := gzip.NewReader(rr.Body) + b, _ := io.ReadAll(gz) + require.Equal(t, "This is a test file for embedfs_test.go\n", string(b)) +} + +func TestGzipHandler_NoGzip(t *testing.T) { + s := Gzip(http.FileServer(http.FS(testFs))) + + rr := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/embedfs/test.txt", nil) + s.ServeHTTP(rr, req) + require.Equal(t, 200, rr.Code) + require.Equal(t, "", rr.Header().Get("Content-Encoding")) + require.Equal(t, "40", rr.Header().Get("Content-Length")) + + b, _ := io.ReadAll(rr.Body) + require.Equal(t, "This is a test file for embedfs_test.go\n", string(b)) +}