github.com/ashishbhate/mattermost-server@v5.11.1+incompatible/utils/subpath.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  package utils
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"encoding/base64"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"net/url"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strings"
    17  
    18  	"github.com/pkg/errors"
    19  
    20  	"github.com/mattermost/mattermost-server/mlog"
    21  	"github.com/mattermost/mattermost-server/model"
    22  	"github.com/mattermost/mattermost-server/utils/fileutils"
    23  )
    24  
    25  // getSubpathScript renders the inline script that defines window.publicPath to change how webpack loads assets.
    26  func getSubpathScript(subpath string) string {
    27  	if subpath == "" {
    28  		subpath = "/"
    29  	}
    30  
    31  	newPath := path.Join(subpath, "static") + "/"
    32  
    33  	return fmt.Sprintf("window.publicPath='%s'", newPath)
    34  }
    35  
    36  // GetSubpathScriptHash computes the script-src addition required for the subpath script to bypass CSP protections.
    37  func GetSubpathScriptHash(subpath string) string {
    38  	// No hash is required for the default subpath.
    39  	if subpath == "" || subpath == "/" {
    40  		return ""
    41  	}
    42  
    43  	scriptHash := sha256.Sum256([]byte(getSubpathScript(subpath)))
    44  
    45  	return fmt.Sprintf(" 'sha256-%s'", base64.StdEncoding.EncodeToString(scriptHash[:]))
    46  }
    47  
    48  // UpdateAssetsSubpath rewrites assets in the /client directory to assume the application is hosted
    49  // at the given subpath instead of at the root. No changes are written unless necessary.
    50  func UpdateAssetsSubpath(subpath string) error {
    51  	if subpath == "" {
    52  		subpath = "/"
    53  	}
    54  
    55  	staticDir, found := fileutils.FindDir(model.CLIENT_DIR)
    56  	if !found {
    57  		return errors.New("failed to find client dir")
    58  	}
    59  
    60  	staticDir, err := filepath.EvalSymlinks(staticDir)
    61  	if err != nil {
    62  		return errors.Wrapf(err, "failed to resolve symlinks to %s", staticDir)
    63  	}
    64  
    65  	rootHtmlPath := filepath.Join(staticDir, "root.html")
    66  	oldRootHtml, err := ioutil.ReadFile(rootHtmlPath)
    67  	if err != nil {
    68  		return errors.Wrap(err, "failed to open root.html")
    69  	}
    70  
    71  	oldSubpath := "/"
    72  
    73  	// Determine if a previous subpath had already been rewritten into the assets.
    74  	reWebpackPublicPathScript := regexp.MustCompile("window.publicPath='([^']+/)static/'")
    75  	alreadyRewritten := false
    76  	if matches := reWebpackPublicPathScript.FindStringSubmatch(string(oldRootHtml)); matches != nil {
    77  		oldSubpath = matches[1]
    78  		alreadyRewritten = true
    79  	}
    80  
    81  	pathToReplace := path.Join(oldSubpath, "static") + "/"
    82  	newPath := path.Join(subpath, "static") + "/"
    83  
    84  	mlog.Debug("Rewriting static assets", mlog.String("from_subpath", oldSubpath), mlog.String("to_subpath", subpath))
    85  
    86  	newRootHtml := string(oldRootHtml)
    87  
    88  	reCSP := regexp.MustCompile(`<meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.segment.com/analytics.js/([^"]*)">`)
    89  	if results := reCSP.FindAllString(newRootHtml, -1); len(results) == 0 {
    90  		return fmt.Errorf("failed to find 'Content-Security-Policy' meta tag to rewrite")
    91  	}
    92  
    93  	newRootHtml = reCSP.ReplaceAllLiteralString(newRootHtml, fmt.Sprintf(
    94  		`<meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.segment.com/analytics.js/%s">`,
    95  		GetSubpathScriptHash(subpath),
    96  	))
    97  
    98  	// Rewrite the root.html references to `/static/*` to include the given subpath.
    99  	// This potentially includes a previously injected inline script that needs to
   100  	// be updated (and isn't covered by the cases above).
   101  	newRootHtml = strings.Replace(newRootHtml, pathToReplace, newPath, -1)
   102  
   103  	if alreadyRewritten && subpath == "/" {
   104  		// Remove the injected script since no longer required. Note that the rewrite above
   105  		// will have affected the script, so look for the new subpath, not the old one.
   106  		oldScript := getSubpathScript(subpath)
   107  		newRootHtml = strings.Replace(newRootHtml, fmt.Sprintf("</style><script>%s</script>", oldScript), "</style>", 1)
   108  
   109  	} else if !alreadyRewritten && subpath != "/" {
   110  		// Otherwise, inject the script to define `window.publicPath`.
   111  		script := getSubpathScript(subpath)
   112  		newRootHtml = strings.Replace(newRootHtml, "</style>", fmt.Sprintf("</style><script>%s</script>", script), 1)
   113  	}
   114  
   115  	// Write out the updated root.html.
   116  	if err = ioutil.WriteFile(rootHtmlPath, []byte(newRootHtml), 0); err != nil {
   117  		return errors.Wrapf(err, "failed to update root.html with subpath %s", subpath)
   118  	}
   119  
   120  	// Rewrite the manifest.json and *.css references to `/static/*` (or a previously rewritten subpath).
   121  	err = filepath.Walk(staticDir, func(walkPath string, info os.FileInfo, err error) error {
   122  		if filepath.Base(walkPath) == "manifest.json" || filepath.Ext(walkPath) == ".css" {
   123  			if old, err := ioutil.ReadFile(walkPath); err != nil {
   124  				return errors.Wrapf(err, "failed to open %s", walkPath)
   125  			} else {
   126  				new := strings.Replace(string(old), pathToReplace, newPath, -1)
   127  				if err = ioutil.WriteFile(walkPath, []byte(new), 0); err != nil {
   128  					return errors.Wrapf(err, "failed to update %s with subpath %s", walkPath, subpath)
   129  				}
   130  			}
   131  		}
   132  
   133  		return nil
   134  	})
   135  	if err != nil {
   136  		return errors.Wrapf(err, "error walking %s", staticDir)
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  // UpdateAssetsSubpathFromConfig uses UpdateAssetsSubpath and any path defined in the SiteURL.
   143  func UpdateAssetsSubpathFromConfig(config *model.Config) error {
   144  	// Don't rewrite in development environments, since webpack in developer mode constantly
   145  	// updates the assets and must be configured separately.
   146  	if model.BuildNumber == "dev" {
   147  		mlog.Debug("Skipping update to assets subpath since dev build")
   148  		return nil
   149  	}
   150  
   151  	subpath, err := GetSubpathFromConfig(config)
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	return UpdateAssetsSubpath(subpath)
   157  }
   158  
   159  func GetSubpathFromConfig(config *model.Config) (string, error) {
   160  	if config == nil {
   161  		return "", errors.New("no config provided")
   162  	} else if config.ServiceSettings.SiteURL == nil {
   163  		return "/", nil
   164  	}
   165  
   166  	u, err := url.Parse(*config.ServiceSettings.SiteURL)
   167  	if err != nil {
   168  		return "", errors.Wrap(err, "failed to parse SiteURL from config")
   169  	}
   170  
   171  	if u.Path == "" {
   172  		return "/", nil
   173  	}
   174  
   175  	return path.Clean(u.Path), nil
   176  }