diff options
-rw-r--r-- | api4/file.go | 4 | ||||
-rw-r--r-- | api4/oauth.go | 18 | ||||
-rw-r--r-- | app/app.go | 22 | ||||
-rw-r--r-- | app/server.go | 17 | ||||
-rw-r--r-- | cmd/mattermost/commands/config.go | 5 | ||||
-rw-r--r-- | utils/api.go | 13 | ||||
-rw-r--r-- | utils/api_test.go | 4 | ||||
-rw-r--r-- | utils/subpath.go (renamed from web/subpath.go) | 28 | ||||
-rw-r--r-- | utils/subpath_test.go (renamed from web/helpers_test.go) | 179 | ||||
-rw-r--r-- | web/context.go | 4 | ||||
-rw-r--r-- | web/handlers.go | 4 | ||||
-rw-r--r-- | web/static.go | 18 | ||||
-rw-r--r-- | web/subpath_test.go | 103 | ||||
-rw-r--r-- | web/web.go | 11 |
14 files changed, 282 insertions, 148 deletions
diff --git a/api4/file.go b/api4/file.go index 0b0973b30..bd8c46405 100644 --- a/api4/file.go +++ b/api4/file.go @@ -312,13 +312,13 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { if len(hash) == 0 { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) - utils.RenderWebAppError(w, r, c.Err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey()) return } if hash != app.GeneratePublicLinkHash(info.Id, *c.App.Config().FileSettings.PublicLinkSalt) { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) - utils.RenderWebAppError(w, r, c.Err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey()) return } diff --git a/api4/oauth.go b/api4/oauth.go index af3d83e17..b858267ee 100644 --- a/api4/oauth.go +++ b/api4/oauth.go @@ -314,7 +314,7 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { if !c.App.Config().ServiceSettings.EnableOAuthServiceProvider { err := model.NewAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "", http.StatusNotImplemented) - utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) return } @@ -327,13 +327,13 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { } if err := authRequest.IsValid(); err != nil { - utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) return } oauthApp, err := c.App.GetOAuthApp(authRequest.ClientId) if err != nil { - utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) return } @@ -345,7 +345,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { if !oauthApp.IsValidRedirectURL(authRequest.RedirectUri) { err := model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest) - utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) return } @@ -362,7 +362,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { redirectUrl, err := c.App.AllowOAuthAppAccessToUser(c.Session.UserId, authRequest) if err != nil { - utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) return } @@ -443,7 +443,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") if len(code) == 0 { - utils.RenderWebError(w, r, http.StatusTemporaryRedirect, url.Values{ + utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{ "type": []string{"oauth_missing_code"}, "service": []string{strings.Title(service)}, }, c.App.AsymmetricSigningKey()) @@ -467,7 +467,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { if action == model.OAUTH_ACTION_MOBILE { w.Write([]byte(err.ToJson())) } else { - utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) } return } @@ -479,7 +479,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { if action == model.OAUTH_ACTION_MOBILE { w.Write([]byte(err.ToJson())) } else { - utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) } return } @@ -564,7 +564,7 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } if !*c.App.Config().TeamSettings.EnableUserCreation { - utils.RenderWebError(w, r, http.StatusBadRequest, url.Values{ + utils.RenderWebError(c.App.Config(), w, r, http.StatusBadRequest, url.Values{ "message": []string{utils.T("api.oauth.singup_with_oauth.disabled.app_error")}, }, c.App.AsymmetricSigningKey()) return diff --git a/app/app.go b/app/app.go index 3f70974cf..2041a24fe 100644 --- a/app/app.go +++ b/app/app.go @@ -9,6 +9,7 @@ import ( "html/template" "net" "net/http" + "path" "reflect" "strings" "sync" @@ -108,10 +109,12 @@ func New(options ...Option) (outApp *App, outErr error) { panic("Only one App should exist at a time. Did you forget to call Shutdown()?") } + rootRouter := mux.NewRouter() + app := &App{ goroutineExitSignal: make(chan struct{}, 1), Srv: &Server{ - Router: mux.NewRouter(), + RootRouter: rootRouter, }, sessionCache: utils.NewLru(model.SESSION_CACHE_SIZE), configFile: "config.json", @@ -206,10 +209,21 @@ func New(options ...Option) (outApp *App, outErr error) { app.initJobs() - app.initBuiltInPlugins() + subpath, err := utils.GetSubpathFromConfig(app.Config()) + if err != nil { + return nil, errors.Wrap(err, "failed to parse SiteURL subpath") + } + app.Srv.Router = app.Srv.RootRouter.PathPrefix(subpath).Subrouter() app.Srv.Router.HandleFunc("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}", app.ServePluginRequest) app.Srv.Router.HandleFunc("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}/{anything:.*}", app.ServePluginRequest) + // If configured with a subpath, redirect 404s at the root back into the subpath. + if subpath != "/" { + app.Srv.RootRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.URL.Path = path.Join(subpath, r.URL.Path) + http.Redirect(w, r, r.URL.String(), http.StatusFound) + }) + } app.Srv.Router.NotFoundHandler = http.HandlerFunc(app.Handle404) app.Srv.WebSocketRouter = &WebSocketRouter{ @@ -217,6 +231,8 @@ func New(options ...Option) (outApp *App, outErr error) { handlers: make(map[string]webSocketHandler), } + app.initBuiltInPlugins() + return app, nil } @@ -510,7 +526,7 @@ func (a *App) Handle404(w http.ResponseWriter, r *http.Request) { mlog.Debug(fmt.Sprintf("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r))) - utils.RenderWebAppError(w, r, err, a.AsymmetricSigningKey()) + utils.RenderWebAppError(a.Config(), w, r, err, a.AsymmetricSigningKey()) } // This function migrates the default built in roles from code/config to the database. diff --git a/app/server.go b/app/server.go index 7d229201d..d71a884d2 100644 --- a/app/server.go +++ b/app/server.go @@ -29,10 +29,17 @@ import ( type Server struct { Store store.Store WebSocketRouter *WebSocketRouter - Router *mux.Router - Server *http.Server - ListenAddr *net.TCPAddr - RateLimiter *RateLimiter + + // RootRouter is the starting point for all HTTP requests to the server. + RootRouter *mux.Router + + // Router is the starting point for all web, api4 and ws requests to the server. It differs + // from RootRouter only if the SiteURL contains a /subpath. + Router *mux.Router + + Server *http.Server + ListenAddr *net.TCPAddr + RateLimiter *RateLimiter didFinishListen chan struct{} } @@ -99,7 +106,7 @@ func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) { func (a *App) StartServer() error { mlog.Info("Starting Server...") - var handler http.Handler = &CorsWrapper{a.Config, a.Srv.Router} + var handler http.Handler = &CorsWrapper{a.Config, a.Srv.RootRouter} if *a.Config().RateLimitSettings.Enable { mlog.Info("RateLimiter is enabled") diff --git a/cmd/mattermost/commands/config.go b/cmd/mattermost/commands/config.go index 0b0e00f35..d9881b050 100644 --- a/cmd/mattermost/commands/config.go +++ b/cmd/mattermost/commands/config.go @@ -12,7 +12,6 @@ import ( "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" - "github.com/mattermost/mattermost-server/web" ) var ConfigCmd = &cobra.Command{ @@ -92,8 +91,8 @@ func configSubpathCmdF(command *cobra.Command, args []string) error { if err != nil { return errors.Wrap(err, "failed reading path") } else if path == "" { - return web.UpdateAssetsSubpathFromConfig(a.Config()) - } else if err := web.UpdateAssetsSubpath(path); err != nil { + return utils.UpdateAssetsSubpathFromConfig(a.Config()) + } else if err := utils.UpdateAssetsSubpath(path); err != nil { return errors.Wrap(err, "failed to update assets subpath") } diff --git a/utils/api.go b/utils/api.go index b5e490eb7..d14f316b6 100644 --- a/utils/api.go +++ b/utils/api.go @@ -11,6 +11,7 @@ import ( "html/template" "net/http" "net/url" + "path" "strings" "github.com/mattermost/mattermost-server/model" @@ -35,24 +36,26 @@ func OriginChecker(allowedOrigins string) func(*http.Request) bool { } } -func RenderWebAppError(w http.ResponseWriter, r *http.Request, err *model.AppError, s crypto.Signer) { - RenderWebError(w, r, err.StatusCode, url.Values{ +func RenderWebAppError(config *model.Config, w http.ResponseWriter, r *http.Request, err *model.AppError, s crypto.Signer) { + RenderWebError(config, w, r, err.StatusCode, url.Values{ "message": []string{err.Message}, }, s) } -func RenderWebError(w http.ResponseWriter, r *http.Request, status int, params url.Values, s crypto.Signer) { +func RenderWebError(config *model.Config, w http.ResponseWriter, r *http.Request, status int, params url.Values, s crypto.Signer) { queryString := params.Encode() + subpath, _ := GetSubpathFromConfig(config) + h := crypto.SHA256 sum := h.New() - sum.Write([]byte("/error?" + queryString)) + sum.Write([]byte(path.Join(subpath, "error") + "?" + queryString)) signature, err := s.Sign(rand.Reader, sum.Sum(nil), h) if err != nil { http.Error(w, "", http.StatusInternalServerError) return } - destination := "/error?" + queryString + "&s=" + base64.URLEncoding.EncodeToString(signature) + destination := path.Join(subpath, "error") + "?" + queryString + "&s=" + base64.URLEncoding.EncodeToString(signature) if status >= 300 && status < 400 { http.Redirect(w, r, destination, status) diff --git a/utils/api_test.go b/utils/api_test.go index 5e41c7bfe..d84207eaa 100644 --- a/utils/api_test.go +++ b/utils/api_test.go @@ -18,6 +18,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" ) func TestRenderWebError(t *testing.T) { @@ -25,7 +27,7 @@ func TestRenderWebError(t *testing.T) { w := httptest.NewRecorder() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - RenderWebError(w, r, http.StatusTemporaryRedirect, url.Values{ + RenderWebError(&model.Config{}, w, r, http.StatusTemporaryRedirect, url.Values{ "foo": []string{"bar"}, }, key) diff --git a/web/subpath.go b/utils/subpath.go index 1bd7412c9..cddc90fa4 100644 --- a/web/subpath.go +++ b/utils/subpath.go @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package web +package utils import ( "crypto/sha256" @@ -19,7 +19,6 @@ import ( "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) // UpdateAssetsSubpath rewrites assets in the /client directory to assume the application is hosted @@ -29,7 +28,7 @@ func UpdateAssetsSubpath(subpath string) error { subpath = "/" } - staticDir, found := utils.FindDir(model.CLIENT_DIR) + staticDir, found := FindDir(model.CLIENT_DIR) if !found { return errors.New("failed to find client dir") } @@ -121,10 +120,29 @@ func UpdateAssetsSubpathFromConfig(config *model.Config) error { return nil } + subpath, err := GetSubpathFromConfig(config) + if err != nil { + return err + } + + return UpdateAssetsSubpath(subpath) +} + +func GetSubpathFromConfig(config *model.Config) (string, error) { + if config == nil { + return "", errors.New("no config provided") + } else if config.ServiceSettings.SiteURL == nil { + return "/", nil + } + u, err := url.Parse(*config.ServiceSettings.SiteURL) if err != nil { - return errors.Wrap(err, "failed to parse SiteURL from config") + return "", errors.Wrap(err, "failed to parse SiteURL from config") + } + + if u.Path == "" { + return "/", nil } - return UpdateAssetsSubpath(u.Path) + return path.Clean(u.Path), nil } diff --git a/web/helpers_test.go b/utils/subpath_test.go index 4e6a7ff6a..ee518d5f6 100644 --- a/web/helpers_test.go +++ b/utils/subpath_test.go @@ -1,4 +1,181 @@ -package web_test +package utils_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" +) + +func TestUpdateAssetsSubpath(t *testing.T) { + t.Run("no client dir", func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "test_update_assets_subpath") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + os.Chdir(tempDir) + + err = utils.UpdateAssetsSubpath("/") + require.Error(t, err) + }) + + t.Run("valid", func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "test_update_assets_subpath") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + os.Chdir(tempDir) + + err = os.Mkdir(model.CLIENT_DIR, 0700) + require.NoError(t, err) + + testCases := []struct { + Description string + RootHTML string + MainCSS string + Subpath string + ExpectedRootHTML string + ExpectedMainCSS string + }{ + { + "no changes required, empty subpath provided", + baseRootHtml, + baseCss, + "", + baseRootHtml, + baseCss, + }, + { + "no changes required", + baseRootHtml, + baseCss, + "/", + baseRootHtml, + baseCss, + }, + { + "subpath", + baseRootHtml, + baseCss, + "/subpath", + subpathRootHtml, + subpathCss, + }, + { + "new subpath from old", + subpathRootHtml, + subpathCss, + "/nested/subpath", + newSubpathRootHtml, + newSubpathCss, + }, + { + "resetting to /", + subpathRootHtml, + subpathCss, + "/", + resetRootHtml, + baseCss, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + ioutil.WriteFile(filepath.Join(tempDir, model.CLIENT_DIR, "root.html"), []byte(testCase.RootHTML), 0700) + ioutil.WriteFile(filepath.Join(tempDir, model.CLIENT_DIR, "main.css"), []byte(testCase.MainCSS), 0700) + err := utils.UpdateAssetsSubpath(testCase.Subpath) + require.NoError(t, err) + + contents, err := ioutil.ReadFile(filepath.Join(tempDir, model.CLIENT_DIR, "root.html")) + require.NoError(t, err) + require.Equal(t, testCase.ExpectedRootHTML, string(contents)) + + contents, err = ioutil.ReadFile(filepath.Join(tempDir, model.CLIENT_DIR, "main.css")) + require.NoError(t, err) + require.Equal(t, testCase.ExpectedMainCSS, string(contents)) + + }) + } + }) +} + +func TestGetSubpathFromConfig(t *testing.T) { + sToP := func(s string) *string { + return &s + } + + testCases := []struct { + Description string + SiteURL *string + ExpectedError bool + ExpectedSubpath string + }{ + { + "empty SiteURL", + sToP(""), + false, + "/", + }, + { + "invalid SiteURL", + sToP("cache_object:foo/bar"), + true, + "", + }, + { + "nil SiteURL", + nil, + false, + "/", + }, + { + "no trailing slash", + sToP("http://localhost:8065"), + false, + "/", + }, + { + "trailing slash", + sToP("http://localhost:8065/"), + false, + "/", + }, + { + "subpath, no trailing slash", + sToP("http://localhost:8065/subpath"), + false, + "/subpath", + }, + { + "trailing slash", + sToP("http://localhost:8065/subpath/"), + false, + "/subpath", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + config := &model.Config{ + ServiceSettings: model.ServiceSettings{ + SiteURL: testCase.SiteURL, + }, + } + + subpath, err := utils.GetSubpathFromConfig(config) + if testCase.ExpectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + require.Equal(t, testCase.ExpectedSubpath, subpath) + }) + } +} const baseRootHtml = `<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.segment.com/analytics.js/ 'unsafe-eval'"> <meta http-equiv=X-UA-Compatible content="IE=edge"> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"> <meta name=robots content="noindex, nofollow"> <meta name=referrer content=no-referrer> <title>Mattermost</title> <meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-status-bar-style content=default> <meta name=mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-title content=Mattermost> <meta name=application-name content=Mattermost> <meta name=format-detection content="telephone=no"> <link rel=apple-touch-icon sizes=57x57 href=/static/files/78b7e73b41b8731ce2c41c870ecc8886.png> <link rel=apple-touch-icon sizes=60x60 href=/static/files/51d00ffd13afb6d74fd8f6dfdeef768a.png> <link rel=apple-touch-icon sizes=72x72 href=/static/files/23645596f8f78f017bd4d457abb855c4.png> <link rel=apple-touch-icon sizes=76x76 href=/static/files/26e9d72f472663a00b4b206149459fab.png> <link rel=apple-touch-icon sizes=144x144 href=/static/files/7bd91659bf3fc8c68fcd45fc1db9c630.png> <link rel=apple-touch-icon sizes=120x120 href=/static/files/fa69ffe11eb334aaef5aece8d848ca62.png> <link rel=apple-touch-icon sizes=152x152 href=/static/files/f046777feb6ab12fc43b8f9908b1db35.png> <link rel=icon type=image/png sizes=16x16 href=/static/files/02b96247d275680adaaabf01c71c571d.png> <link rel=icon type=image/png sizes=32x32 href=/static/files/1d9020f201a6762421cab8d30624fdd8.png> <link rel=icon type=image/png sizes=96x96 href=/static/files/fe23af39ae98d77dc26ae8586565970f.png> <link rel=icon type=image/png sizes=192x192 href=/static/files/d7ff68a7675f84337cc154c3d4abe713.png> <link rel=manifest href=/static/files/a985ad72552ad069537d6eea81e719c7.json> <link rel=stylesheet class=code_theme> <style>.error-screen{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding-top:50px;max-width:750px;font-size:14px;color:#333;margin:auto;display:none;line-height:1.5}.error-screen h2{font-size:30px;font-weight:400;line-height:1.2}.error-screen ul{padding-left:15px;line-height:1.7;margin-top:0;margin-bottom:10px}.error-screen hr{color:#ddd;margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.error-screen-visible{display:block}</style> <link href="/static/main.364fd054d7a6d741efc6.css" rel="stylesheet"><script type="text/javascript" src="/static/main.e49599ac425584ffead5.js"></script></head> <body class=font--open_sans> <div id=root> <div class=error-screen> <h2>Cannot connect to Mattermost</h2> <hr/> <p>We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.</p> <br/> </div> <div class=loading-screen style=position:relative> <div class=loading__content> <div class="round round-1"></div> <div class="round round-2"></div> <div class="round round-3"></div> </div> </div> </div> <noscript> To use Mattermost, please enable JavaScript. </noscript> </body> </html>` diff --git a/web/context.go b/web/context.go index 7e4318233..5eb8c94d5 100644 --- a/web/context.go +++ b/web/context.go @@ -5,6 +5,7 @@ package web import ( "net/http" + "path" "regexp" "strings" @@ -126,7 +127,8 @@ func (c *Context) MfaRequired() { } // Special case to let user get themself - if c.Path == "/api/v4/users/me" { + subpath, _ := utils.GetSubpathFromConfig(c.App.Config()) + if c.Path == path.Join(subpath, "/api/v4/users/me") { return } diff --git a/web/handlers.go b/web/handlers.go index fe77241e3..c12466fee 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -157,11 +157,11 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.Err.IsOAuth = false } - if IsApiCall(r) || len(r.Header.Get("X-Mobile-App")) > 0 { + if IsApiCall(c.App, r) || len(r.Header.Get("X-Mobile-App")) > 0 { w.WriteHeader(c.Err.StatusCode) w.Write([]byte(c.Err.ToJson())) } else { - utils.RenderWebAppError(w, r, c.Err, c.App.AsymmetricSigningKey()) + utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey()) } if c.App.Metrics != nil { diff --git a/web/static.go b/web/static.go index 1f76b2725..08da9e95e 100644 --- a/web/static.go +++ b/web/static.go @@ -6,6 +6,7 @@ package web import ( "fmt" "net/http" + "path" "path/filepath" "strings" @@ -18,13 +19,15 @@ import ( func (w *Web) InitStatic() { if *w.App.Config().ServiceSettings.WebserverMode != "disabled" { - UpdateAssetsSubpathFromConfig(w.App.Config()) + utils.UpdateAssetsSubpathFromConfig(w.App.Config()) staticDir, _ := utils.FindDir(model.CLIENT_DIR) mlog.Debug(fmt.Sprintf("Using client directory at %v", staticDir)) - staticHandler := staticHandler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) - pluginHandler := pluginHandler(w.App.Config, http.StripPrefix("/static/plugins/", http.FileServer(http.Dir(*w.App.Config().PluginSettings.ClientDirectory)))) + subpath, _ := utils.GetSubpathFromConfig(w.App.Config()) + + staticHandler := staticHandler(http.StripPrefix(path.Join(subpath, "static"), http.FileServer(http.Dir(staticDir)))) + pluginHandler := pluginHandler(w.App.Config, http.StripPrefix(path.Join(subpath, "plugins"), http.FileServer(http.Dir(*w.App.Config().PluginSettings.ClientDirectory)))) if *w.App.Config().ServiceSettings.WebserverMode == "gzip" { staticHandler = gziphandler.GzipHandler(staticHandler) @@ -34,6 +37,13 @@ func (w *Web) InitStatic() { w.MainRouter.PathPrefix("/static/plugins/").Handler(pluginHandler) w.MainRouter.PathPrefix("/static/").Handler(staticHandler) w.MainRouter.Handle("/{anything:.*}", w.NewStaticHandler(root)).Methods("GET") + + // When a subpath is defined, it's necessary to handle redirects without a + // trailing slash. We don't want to use StrictSlash on the w.MainRouter and affect + // all routes, just /subpath -> /subpath/. + w.MainRouter.HandleFunc("", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, r.URL.String()+"/", http.StatusFound) + })) } } @@ -48,7 +58,7 @@ func root(c *Context, w http.ResponseWriter, r *http.Request) { return } - if IsApiCall(r) { + if IsApiCall(c.App, r) { Handle404(c.App, w, r) return } diff --git a/web/subpath_test.go b/web/subpath_test.go deleted file mode 100644 index 92b1a5d3c..000000000 --- a/web/subpath_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package web_test - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/web" -) - -func TestUpdateAssetsSubpath(t *testing.T) { - t.Run("no client dir", func(t *testing.T) { - tempDir, err := ioutil.TempDir("", "test_update_assets_subpath") - require.NoError(t, err) - defer os.RemoveAll(tempDir) - os.Chdir(tempDir) - - err = web.UpdateAssetsSubpath("/") - require.Error(t, err) - }) - - t.Run("valid", func(t *testing.T) { - tempDir, err := ioutil.TempDir("", "test_update_assets_subpath") - require.NoError(t, err) - defer os.RemoveAll(tempDir) - os.Chdir(tempDir) - - err = os.Mkdir(model.CLIENT_DIR, 0700) - require.NoError(t, err) - - testCases := []struct { - Description string - RootHTML string - MainCSS string - Subpath string - ExpectedRootHTML string - ExpectedMainCSS string - }{ - { - "no changes required, empty subpath provided", - baseRootHtml, - baseCss, - "", - baseRootHtml, - baseCss, - }, - { - "no changes required", - baseRootHtml, - baseCss, - "/", - baseRootHtml, - baseCss, - }, - { - "subpath", - baseRootHtml, - baseCss, - "/subpath", - subpathRootHtml, - subpathCss, - }, - { - "new subpath from old", - subpathRootHtml, - subpathCss, - "/nested/subpath", - newSubpathRootHtml, - newSubpathCss, - }, - { - "resetting to /", - subpathRootHtml, - subpathCss, - "/", - resetRootHtml, - baseCss, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.Description, func(t *testing.T) { - ioutil.WriteFile(filepath.Join(tempDir, model.CLIENT_DIR, "root.html"), []byte(testCase.RootHTML), 0700) - ioutil.WriteFile(filepath.Join(tempDir, model.CLIENT_DIR, "main.css"), []byte(testCase.MainCSS), 0700) - err := web.UpdateAssetsSubpath(testCase.Subpath) - require.NoError(t, err) - - contents, err := ioutil.ReadFile(filepath.Join(tempDir, model.CLIENT_DIR, "root.html")) - require.NoError(t, err) - require.Equal(t, testCase.ExpectedRootHTML, string(contents)) - - contents, err = ioutil.ReadFile(filepath.Join(tempDir, model.CLIENT_DIR, "main.css")) - require.NoError(t, err) - require.Equal(t, testCase.ExpectedMainCSS, string(contents)) - - }) - } - }) -} diff --git a/web/web.go b/web/web.go index 5c1836818..479f439fb 100644 --- a/web/web.go +++ b/web/web.go @@ -6,6 +6,7 @@ package web import ( "fmt" "net/http" + "path" "strings" "github.com/avct/uasurfer" @@ -60,17 +61,19 @@ func Handle404(a *app.App, w http.ResponseWriter, r *http.Request) { mlog.Debug(fmt.Sprintf("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r))) - if IsApiCall(r) { + if IsApiCall(a, r) { w.WriteHeader(err.StatusCode) err.DetailedError = "There doesn't appear to be an api call for the url='" + r.URL.Path + "'. Typo? are you missing a team_id or user_id as part of the url?" w.Write([]byte(err.ToJson())) } else { - utils.RenderWebAppError(w, r, err, a.AsymmetricSigningKey()) + utils.RenderWebAppError(a.Config(), w, r, err, a.AsymmetricSigningKey()) } } -func IsApiCall(r *http.Request) bool { - return strings.Index(r.URL.Path, "/api/") == 0 +func IsApiCall(a *app.App, r *http.Request) bool { + subpath, _ := utils.GetSubpathFromConfig(a.Config()) + + return strings.Index(r.URL.Path, path.Join(subpath, "api")+"/") == 0 } func ReturnStatusOK(w http.ResponseWriter) { |