From f106417103b036e8c349531f25487e526252d084 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Thu, 14 Jun 2018 11:26:22 -0400 Subject: 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. --- web/subpath.go | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 web/subpath.go (limited to 'web/subpath.go') diff --git a/web/subpath.go b/web/subpath.go new file mode 100644 index 000000000..1bd7412c9 --- /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) +} -- cgit v1.2.3-1-g7c22