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