github.com/lologarithm/mattermost-server@v5.3.2-0.20181002060438-c82a84ed765b+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  )
    23  
    24  // UpdateAssetsSubpath rewrites assets in the /client directory to assume the application is hosted
    25  // at the given subpath instead of at the root. No changes are written unless necessary.
    26  func UpdateAssetsSubpath(subpath string) error {
    27  	if subpath == "" {
    28  		subpath = "/"
    29  	}
    30  
    31  	staticDir, found := FindDir(model.CLIENT_DIR)
    32  	if !found {
    33  		return errors.New("failed to find client dir")
    34  	}
    35  
    36  	staticDir, err := filepath.EvalSymlinks(staticDir)
    37  	if err != nil {
    38  		return errors.Wrapf(err, "failed to resolve symlinks to %s", staticDir)
    39  	}
    40  
    41  	rootHtmlPath := filepath.Join(staticDir, "root.html")
    42  	oldRootHtml, err := ioutil.ReadFile(rootHtmlPath)
    43  	if err != nil {
    44  		return errors.Wrap(err, "failed to open root.html")
    45  	}
    46  
    47  	pathToReplace := "/static/"
    48  	newPath := path.Join(subpath, "static") + "/"
    49  
    50  	// Determine if a previous subpath had already been rewritten into the assets.
    51  	reWebpackPublicPathScript := regexp.MustCompile("window.publicPath='([^']+)'")
    52  	alreadyRewritten := false
    53  	if matches := reWebpackPublicPathScript.FindStringSubmatch(string(oldRootHtml)); matches != nil {
    54  		pathToReplace = matches[1]
    55  		alreadyRewritten = true
    56  	}
    57  
    58  	if pathToReplace == newPath {
    59  		mlog.Debug("No rewrite required for static assets", mlog.String("path", pathToReplace))
    60  		return nil
    61  	}
    62  
    63  	mlog.Debug("Rewriting static assets", mlog.String("from_path", pathToReplace), mlog.String("to_path", newPath))
    64  
    65  	newRootHtml := string(oldRootHtml)
    66  
    67  	// Compute the sha256 hash for the inline script and reference same in the CSP meta tag.
    68  	// This allows the inline script defining `window.publicPath` to bypass CSP protections.
    69  	script := fmt.Sprintf("window.publicPath='%s'", newPath)
    70  	scriptHash := sha256.Sum256([]byte(script))
    71  
    72  	reCSP := regexp.MustCompile(`<meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.segment.com/analytics.js/ 'unsafe-eval'([^"]*)">`)
    73  	newRootHtml = reCSP.ReplaceAllLiteralString(newRootHtml, fmt.Sprintf(
    74  		`<meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.segment.com/analytics.js/ 'unsafe-eval' 'sha256-%s'">`,
    75  		base64.StdEncoding.EncodeToString(scriptHash[:]),
    76  	))
    77  
    78  	// Rewrite the root.html references to `/static/*` to include the given subpath. This
    79  	// potentially includes a previously injected inline script.
    80  	newRootHtml = strings.Replace(newRootHtml, pathToReplace, newPath, -1)
    81  
    82  	// Inject the script, if needed, to define `window.publicPath`.
    83  	if !alreadyRewritten {
    84  		newRootHtml = strings.Replace(newRootHtml, "</style>", fmt.Sprintf("</style><script>%s</script>", script), 1)
    85  	}
    86  
    87  	// Write out the updated root.html.
    88  	if err = ioutil.WriteFile(rootHtmlPath, []byte(newRootHtml), 0); err != nil {
    89  		return errors.Wrapf(err, "failed to update root.html with subpath %s", subpath)
    90  	}
    91  
    92  	// Rewrite the manifest.json and *.css references to `/static/*` (or a previously rewritten subpath).
    93  	err = filepath.Walk(staticDir, func(walkPath string, info os.FileInfo, err error) error {
    94  		if filepath.Base(walkPath) == "manifest.json" || filepath.Ext(walkPath) == ".css" {
    95  			if old, err := ioutil.ReadFile(walkPath); err != nil {
    96  				return errors.Wrapf(err, "failed to open %s", walkPath)
    97  			} else {
    98  				new := strings.Replace(string(old), pathToReplace, newPath, -1)
    99  				if err = ioutil.WriteFile(walkPath, []byte(new), 0); err != nil {
   100  					return errors.Wrapf(err, "failed to update %s with subpath %s", walkPath, subpath)
   101  				}
   102  			}
   103  		}
   104  
   105  		return nil
   106  	})
   107  	if err != nil {
   108  		return errors.Wrapf(err, "error walking %s", staticDir)
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  // UpdateAssetsSubpathFromConfig uses UpdateAssetsSubpath and any path defined in the SiteURL.
   115  func UpdateAssetsSubpathFromConfig(config *model.Config) error {
   116  	// Don't rewrite in development environments, since webpack in developer mode constantly
   117  	// updates the assets and must be configured separately.
   118  	if model.BuildNumber == "dev" {
   119  		mlog.Debug("Skipping update to assets subpath since dev build")
   120  		return nil
   121  	}
   122  
   123  	subpath, err := GetSubpathFromConfig(config)
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	return UpdateAssetsSubpath(subpath)
   129  }
   130  
   131  func GetSubpathFromConfig(config *model.Config) (string, error) {
   132  	if config == nil {
   133  		return "", errors.New("no config provided")
   134  	} else if config.ServiceSettings.SiteURL == nil {
   135  		return "/", nil
   136  	}
   137  
   138  	u, err := url.Parse(*config.ServiceSettings.SiteURL)
   139  	if err != nil {
   140  		return "", errors.Wrap(err, "failed to parse SiteURL from config")
   141  	}
   142  
   143  	if u.Path == "" {
   144  		return "/", nil
   145  	}
   146  
   147  	return path.Clean(u.Path), nil
   148  }