github.com/levb/mattermost-server@v5.3.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 ) 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 }