summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Hallam <jesse.hallam@gmail.com>2018-06-21 14:31:51 -0400
committerChristopher Speller <crspeller@gmail.com>2018-06-21 11:31:51 -0700
commitdd35ad43caab407cc70ef3b153b3f94d57242ed9 (patch)
tree67afe0fb6ad784e740d8b2a99d5de5c04342661c
parent46f969e5ddbe4404dbc82dbe78ab2fa101d9922e (diff)
downloadchat-dd35ad43caab407cc70ef3b153b3f94d57242ed9.tar.gz
chat-dd35ad43caab407cc70ef3b153b3f94d57242ed9.tar.bz2
chat-dd35ad43caab407cc70ef3b153b3f94d57242ed9.zip
MM-10370: serve subpath (#8968)
* factor out GetSubpathFromConfig * mv web/subpath.go to utils/subpath.go * serve up web, api and ws on /subpath if configured * pass config to utils.RenderWeb(App)?Error This allows the methods to extract the configured subpath and redirect to the appropriate `/subpath/error` handler. * ensure GetSubpathFromConfig returns trailing slashes deterministically * fix error 404 handling * redirect /subpath to /subpath/ This is necessary for the static handler to match, otherwise none of the registered routes find anything. This also makes it no longer necessary to add trailing slashes in the root router.
-rw-r--r--api4/file.go4
-rw-r--r--api4/oauth.go18
-rw-r--r--app/app.go22
-rw-r--r--app/server.go17
-rw-r--r--cmd/mattermost/commands/config.go5
-rw-r--r--utils/api.go13
-rw-r--r--utils/api_test.go4
-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.go4
-rw-r--r--web/handlers.go4
-rw-r--r--web/static.go18
-rw-r--r--web/subpath_test.go103
-rw-r--r--web/web.go11
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) {