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  }