Commit f1064171 authored by Jesse Hallam's avatar Jesse Hallam Committed by Christopher Speller

MM-10367: rewrite subpath assets on startup (#8944)

Examine ServiceSettings.SiteURL on startup and rewrite assets
accordingly if not in a development environment.

Also export `mattermost config subpath` command to manually do same.

This accompanies a webapp PR to use the updated `root.html` to define
the necessary webpack asset path for dynamically loading assets.
parent d0cda050
......@@ -5,12 +5,14 @@ package commands
import (
"encoding/json"
"errors"
"os"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/web"
)
var ConfigCmd = &cobra.Command{
......@@ -25,9 +27,22 @@ var ValidateConfigCmd = &cobra.Command{
RunE: configValidateCmdF,
}
var ConfigSubpathCmd = &cobra.Command{
Use: "subpath",
Short: "Update client asset loading to use the configured subpath",
Long: "Update the hard-coded production client asset paths to take into account Mattermost running on a subpath.",
Example: ` config subpath
config subpath --path /mattermost
config subpath --path /`,
RunE: configSubpathCmdF,
}
func init() {
ConfigSubpathCmd.Flags().String("path", "", "Optional subpath; defaults to value in SiteURL")
ConfigCmd.AddCommand(
ValidateConfigCmd,
ConfigSubpathCmd,
)
RootCmd.AddCommand(ConfigCmd)
}
......@@ -65,3 +80,22 @@ func configValidateCmdF(command *cobra.Command, args []string) error {
CommandPrettyPrintln("The document is valid")
return nil
}
func configSubpathCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command)
if err != nil {
return err
}
defer a.Shutdown()
path, err := command.Flags().GetString("path")
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 errors.Wrap(err, "failed to update assets subpath")
}
return nil
}
This diff is collapsed.
......@@ -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))
......
// 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(`<meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.segment.com/analytics.js/ 'unsafe-eval'([^"]*)">`)
newRootHtml = reCSP.ReplaceAllLiteralString(newRootHtml, fmt.Sprintf(
`<meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.segment.com/analytics.js/ 'unsafe-eval' 'sha256-%s'">`,
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, "</style>", fmt.Sprintf("</style><script>%s</script>", 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)
}
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))
})
}
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment