github.com/fretkak/mattermost-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 }