summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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) {