diff --git a/api4/handlers.go b/api4/handlers.go index 80a8c72de6..acdf1d03d5 100644 --- a/api4/handlers.go +++ b/api4/handlers.go @@ -6,6 +6,7 @@ package api4 import ( "net/http" + "github.com/NYTimes/gziphandler" "github.com/mattermost/mattermost-server/web" ) @@ -14,7 +15,7 @@ type Context = web.Context // ApiHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be // granted. func (api *API) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &web.Handler{ + handler := &web.Handler{ GetGlobalAppOptions: api.GetGlobalAppOptions, HandleFunc: h, RequireSession: false, @@ -22,12 +23,16 @@ func (api *API) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) RequireMfa: false, IsStatic: false, } + if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler } // ApiSessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to // be granted. func (api *API) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &web.Handler{ + handler := &web.Handler{ GetGlobalAppOptions: api.GetGlobalAppOptions, HandleFunc: h, RequireSession: true, @@ -35,13 +40,18 @@ func (api *API) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.R RequireMfa: true, IsStatic: false, } + if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler + } // ApiSessionRequiredMfa provides a handler for API endpoints which require a logged-in user session but when accessed, // if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA // authentication must be waived. func (api *API) ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &web.Handler{ + handler := &web.Handler{ GetGlobalAppOptions: api.GetGlobalAppOptions, HandleFunc: h, RequireSession: true, @@ -49,13 +59,18 @@ func (api *API) ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *htt RequireMfa: false, IsStatic: false, } + if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler + } // ApiHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are // allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the // websocket. func (api *API) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &web.Handler{ + handler := &web.Handler{ GetGlobalAppOptions: api.GetGlobalAppOptions, HandleFunc: h, RequireSession: false, @@ -63,12 +78,17 @@ func (api *API) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, * RequireMfa: false, IsStatic: false, } + if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler + } // ApiSessionRequiredTrustRequester provides a handler for API endpoints which do require the user to be logged in and // are allowed to be requested directly rather than via javascript/XMLHttpRequest, such as emoji or file uploads. func (api *API) ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &web.Handler{ + handler := &web.Handler{ GetGlobalAppOptions: api.GetGlobalAppOptions, HandleFunc: h, RequireSession: true, @@ -76,4 +96,9 @@ func (api *API) ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseW RequireMfa: true, IsStatic: false, } + if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler + } diff --git a/api4/handlers_test.go b/api4/handlers_test.go new file mode 100644 index 0000000000..6a013f5146 --- /dev/null +++ b/api4/handlers_test.go @@ -0,0 +1,90 @@ +package api4 + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/model" +) + +func handlerForGzip(c *Context, w http.ResponseWriter, r *http.Request) { + // gziphandler default requires body size greater than 1400 bytes + var body [1400]byte + w.Write(body[:]) +} + +func testAPIHandlerGzipMode(t *testing.T, name string, h http.Handler, token string) { + t.Run("Handler: "+name+" No Accept-Encoding", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/v4/test", nil) + req.Header.Set(model.HEADER_AUTH, "Bearer "+token) + h.ServeHTTP(resp, req) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "", resp.Header().Get("Content-Encoding")) + }) + + t.Run("Handler: "+name+" With Accept-Encoding", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/v4/test", nil) + req.Header.Set("Accept-Encoding", "gzip") + req.Header.Set(model.HEADER_AUTH, "Bearer "+token) + + h.ServeHTTP(resp, req) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "gzip", resp.Header().Get("Content-Encoding")) + }) +} + +func testAPIHandlerNoGzipMode(t *testing.T, name string, h http.Handler, token string) { + t.Run("Handler: "+name+" No Accept-Encoding", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/v4/test", nil) + req.Header.Set(model.HEADER_AUTH, "Bearer "+token) + + h.ServeHTTP(resp, req) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "", resp.Header().Get("Content-Encoding")) + }) + + t.Run("Handler: "+name+" With Accept-Encoding", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/v4/test", nil) + req.Header.Set("Accept-Encoding", "gzip") + req.Header.Set(model.HEADER_AUTH, "Bearer "+token) + + h.ServeHTTP(resp, req) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "", resp.Header().Get("Content-Encoding")) + }) +} + +func TestAPIHandlersWithGzip(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + api := Init(th.Server, th.Server.AppOptions, th.Server.Router) + session, _ := th.App.GetSession(th.Client.AuthToken) + + t.Run("with WebserverMode == \"gzip\"", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.WebserverMode = "gzip" }) + + testAPIHandlerGzipMode(t, "ApiHandler", api.ApiHandler(handlerForGzip), "") + testAPIHandlerGzipMode(t, "ApiSessionRequired", api.ApiSessionRequired(handlerForGzip), session.Token) + testAPIHandlerGzipMode(t, "ApiSessionRequiredMfa", api.ApiSessionRequiredMfa(handlerForGzip), session.Token) + testAPIHandlerGzipMode(t, "ApiHandlerTrustRequester", api.ApiHandlerTrustRequester(handlerForGzip), "") + testAPIHandlerGzipMode(t, "ApiSessionRequiredTrustRequester", api.ApiSessionRequiredTrustRequester(handlerForGzip), session.Token) + }) + + t.Run("with WebserverMode == \"nogzip\"", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.WebserverMode = "nogzip" }) + + testAPIHandlerNoGzipMode(t, "ApiHandler", api.ApiHandler(handlerForGzip), "") + testAPIHandlerNoGzipMode(t, "ApiSessionRequired", api.ApiSessionRequired(handlerForGzip), session.Token) + testAPIHandlerNoGzipMode(t, "ApiSessionRequiredMfa", api.ApiSessionRequiredMfa(handlerForGzip), session.Token) + testAPIHandlerNoGzipMode(t, "ApiHandlerTrustRequester", api.ApiHandlerTrustRequester(handlerForGzip), "") + testAPIHandlerNoGzipMode(t, "ApiSessionRequiredTrustRequester", api.ApiSessionRequiredTrustRequester(handlerForGzip), session.Token) + }) +}