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.
Этот коммит содержится в:
коммит произвёл
Christopher Speller
родитель
46f969e5dd
Коммит
dd35ad43ca
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
22
app/app.go
22
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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
13
utils/api.go
13
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
u, err := url.Parse(*config.ServiceSettings.SiteURL)
|
||||
subpath, err := GetSubpathFromConfig(config)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse SiteURL from config")
|
||||
return err
|
||||
}
|
||||
|
||||
return UpdateAssetsSubpath(u.Path)
|
||||
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")
|
||||
}
|
||||
|
||||
if u.Path == "" {
|
||||
return "/", nil
|
||||
}
|
||||
|
||||
return path.Clean(u.Path), nil
|
||||
}
|
||||
@@ -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>`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
11
web/web.go
11
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) {
|
||||
|
||||
Ссылка в новой задаче
Block a user