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.
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.
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.
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.
`
diff --git a/web/static.go b/web/static.go
index 487526fdf1..1f76b2725f 100644
--- a/web/static.go
+++ b/web/static.go
@@ -18,6 +18,8 @@ import (
func (w *Web) InitStatic() {
if *w.App.Config().ServiceSettings.WebserverMode != "disabled" {
+ UpdateAssetsSubpathFromConfig(w.App.Config())
+
staticDir, _ := utils.FindDir(model.CLIENT_DIR)
mlog.Debug(fmt.Sprintf("Using client directory at %v", staticDir))
diff --git a/web/subpath.go b/web/subpath.go
new file mode 100644
index 0000000000..1bd7412c91
--- /dev/null
+++ b/web/subpath.go
@@ -0,0 +1,130 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package web
+
+import (
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "io/ioutil"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/pkg/errors"
+
+ "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
+// at the given subpath instead of at the root. No changes are written unless necessary.
+func UpdateAssetsSubpath(subpath string) error {
+ if subpath == "" {
+ subpath = "/"
+ }
+
+ staticDir, found := utils.FindDir(model.CLIENT_DIR)
+ if !found {
+ return errors.New("failed to find client dir")
+ }
+
+ staticDir, err := filepath.EvalSymlinks(staticDir)
+ if err != nil {
+ return errors.Wrapf(err, "failed to resolve symlinks to %s", staticDir)
+ }
+
+ rootHtmlPath := filepath.Join(staticDir, "root.html")
+ oldRootHtml, err := ioutil.ReadFile(rootHtmlPath)
+ if err != nil {
+ return errors.Wrap(err, "failed to open root.html")
+ }
+
+ pathToReplace := "/static/"
+ newPath := path.Join(subpath, "static") + "/"
+
+ // Determine if a previous subpath had already been rewritten into the assets.
+ reWebpackPublicPathScript := regexp.MustCompile("window.publicPath='([^']+)'")
+ alreadyRewritten := false
+ if matches := reWebpackPublicPathScript.FindStringSubmatch(string(oldRootHtml)); matches != nil {
+ pathToReplace = matches[1]
+ alreadyRewritten = true
+ }
+
+ if pathToReplace == newPath {
+ mlog.Debug("No rewrite required for static assets", mlog.String("path", pathToReplace))
+ return nil
+ }
+
+ mlog.Debug("Rewriting static assets", mlog.String("from_path", pathToReplace), mlog.String("to_path", newPath))
+
+ newRootHtml := string(oldRootHtml)
+
+ // Compute the sha256 hash for the inline script and reference same in the CSP meta tag.
+ // This allows the inline script defining `window.publicPath` to bypass CSP protections.
+ script := fmt.Sprintf("window.publicPath='%s'", newPath)
+ scriptHash := sha256.Sum256([]byte(script))
+
+ reCSP := regexp.MustCompile(``)
+ newRootHtml = reCSP.ReplaceAllLiteralString(newRootHtml, fmt.Sprintf(
+ ``,
+ base64.StdEncoding.EncodeToString(scriptHash[:]),
+ ))
+
+ // Rewrite the root.html references to `/static/*` to include the given subpath. This
+ // potentially includes a previously injected inline script.
+ newRootHtml = strings.Replace(newRootHtml, pathToReplace, newPath, -1)
+
+ // Inject the script, if needed, to define `window.publicPath`.
+ if !alreadyRewritten {
+ newRootHtml = strings.Replace(newRootHtml, "", fmt.Sprintf("", script), 1)
+ }
+
+ // Write out the updated root.html.
+ if err = ioutil.WriteFile(rootHtmlPath, []byte(newRootHtml), 0); err != nil {
+ return errors.Wrapf(err, "failed to update root.html with subpath %s", subpath)
+ }
+
+ // Rewrite the *.css references to `/static/*` (or a previously rewritten subpath).
+ err = filepath.Walk(staticDir, func(walkPath string, info os.FileInfo, err error) error {
+ if filepath.Ext(walkPath) == ".css" {
+ if oldCss, err := ioutil.ReadFile(walkPath); err != nil {
+ return errors.Wrapf(err, "failed to open %s", walkPath)
+ } else {
+ newCss := strings.Replace(string(oldCss), pathToReplace, newPath, -1)
+ if err = ioutil.WriteFile(walkPath, []byte(newCss), 0); err != nil {
+ return errors.Wrapf(err, "failed to update %s with subpath %s", walkPath, subpath)
+ }
+ }
+ }
+
+ return nil
+ })
+ if err != nil {
+ return errors.Wrapf(err, "error walking %s", staticDir)
+ }
+
+ return nil
+}
+
+// UpdateAssetsSubpathFromConfig uses UpdateAssetsSubpath and any path defined in the SiteURL.
+func UpdateAssetsSubpathFromConfig(config *model.Config) error {
+ // Don't rewrite in development environments, since webpack in developer mode constantly
+ // updates the assets and must be configured separately.
+ if model.BuildNumber == "dev" {
+ mlog.Debug("Skipping update to assets subpath since dev build")
+ return nil
+ }
+
+ u, err := url.Parse(*config.ServiceSettings.SiteURL)
+ if err != nil {
+ return errors.Wrap(err, "failed to parse SiteURL from config")
+ }
+
+ return UpdateAssetsSubpath(u.Path)
+}
diff --git a/web/subpath_test.go b/web/subpath_test.go
new file mode 100644
index 0000000000..92b1a5d3c1
--- /dev/null
+++ b/web/subpath_test.go
@@ -0,0 +1,103 @@
+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))
+
+ })
+ }
+ })
+}