summaryrefslogtreecommitdiffstats
path: root/utils/subpath.go
blob: be06a73ef1d9df194be2280acc82028f65883a2d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package utils

import (
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"io/ioutil"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/pkg/errors"

	"github.com/mattermost/mattermost-server/mlog"
	"github.com/mattermost/mattermost-server/model"
)

// UpdateAssetsSubpath rewrites assets in the /client directory to assume the application is hosted
// at the given subpath instead of at the root. No changes are written unless necessary.
func UpdateAssetsSubpath(subpath string) error {
	if subpath == "" {
		subpath = "/"
	}

	staticDir, found := FindDir(model.CLIENT_DIR)
	if !found {
		return errors.New("failed to find client dir")
	}

	staticDir, err := filepath.EvalSymlinks(staticDir)
	if err != nil {
		return errors.Wrapf(err, "failed to resolve symlinks to %s", staticDir)
	}

	rootHtmlPath := filepath.Join(staticDir, "root.html")
	oldRootHtml, err := ioutil.ReadFile(rootHtmlPath)
	if err != nil {
		return errors.Wrap(err, "failed to open root.html")
	}

	pathToReplace := "/static/"
	newPath := path.Join(subpath, "static") + "/"

	// Determine if a previous subpath had already been rewritten into the assets.
	reWebpackPublicPathScript := regexp.MustCompile("window.publicPath='([^']+)'")
	alreadyRewritten := false
	if matches := reWebpackPublicPathScript.FindStringSubmatch(string(oldRootHtml)); matches != nil {
		pathToReplace = matches[1]
		alreadyRewritten = true
	}

	if pathToReplace == newPath {
		mlog.Debug("No rewrite required for static assets", mlog.String("path", pathToReplace))
		return nil
	}

	mlog.Debug("Rewriting static assets", mlog.String("from_path", pathToReplace), mlog.String("to_path", newPath))

	newRootHtml := string(oldRootHtml)

	// Compute the sha256 hash for the inline script and reference same in the CSP meta tag.
	// This allows the inline script defining `window.publicPath` to bypass CSP protections.
	script := fmt.Sprintf("window.publicPath='%s'", newPath)
	scriptHash := sha256.Sum256([]byte(script))

	reCSP := regexp.MustCompile(`<meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.segment.com/analytics.js/ 'unsafe-eval'([^"]*)">`)
	newRootHtml = reCSP.ReplaceAllLiteralString(newRootHtml, fmt.Sprintf(
		`<meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.segment.com/analytics.js/ 'unsafe-eval' 'sha256-%s'">`,
		base64.StdEncoding.EncodeToString(scriptHash[:]),
	))

	// Rewrite the root.html references to `/static/*` to include the given subpath. This
	// potentially includes a previously injected inline script.
	newRootHtml = strings.Replace(newRootHtml, pathToReplace, newPath, -1)

	// Inject the script, if needed, to define `window.publicPath`.
	if !alreadyRewritten {
		newRootHtml = strings.Replace(newRootHtml, "</style>", fmt.Sprintf("</style><script>%s</script>", script), 1)
	}

	// Write out the updated root.html.
	if err = ioutil.WriteFile(rootHtmlPath, []byte(newRootHtml), 0); err != nil {
		return errors.Wrapf(err, "failed to update root.html with subpath %s", subpath)
	}

	// Rewrite the manifest.json and *.css references to `/static/*` (or a previously rewritten subpath).
	err = filepath.Walk(staticDir, func(walkPath string, info os.FileInfo, err error) error {
		if filepath.Base(walkPath) == "manifest.json" || filepath.Ext(walkPath) == ".css" {
			if old, err := ioutil.ReadFile(walkPath); err != nil {
				return errors.Wrapf(err, "failed to open %s", walkPath)
			} else {
				new := strings.Replace(string(old), pathToReplace, newPath, -1)
				if err = ioutil.WriteFile(walkPath, []byte(new), 0); err != nil {
					return errors.Wrapf(err, "failed to update %s with subpath %s", walkPath, subpath)
				}
			}
		}

		return nil
	})
	if err != nil {
		return errors.Wrapf(err, "error walking %s", staticDir)
	}

	return nil
}

// UpdateAssetsSubpathFromConfig uses UpdateAssetsSubpath and any path defined in the SiteURL.
func UpdateAssetsSubpathFromConfig(config *model.Config) error {
	// Don't rewrite in development environments, since webpack in developer mode constantly
	// updates the assets and must be configured separately.
	if model.BuildNumber == "dev" {
		mlog.Debug("Skipping update to assets subpath since dev build")
		return nil
	}

	subpath, err := GetSubpathFromConfig(config)
	if err != nil {
		return err
	}

	return UpdateAssetsSubpath(subpath)
}

func GetSubpathFromConfig(config *model.Config) (string, error) {
	if config == nil {
		return "", errors.New("no config provided")
	} else if config.ServiceSettings.SiteURL == nil {
		return "/", nil
	}

	u, err := url.Parse(*config.ServiceSettings.SiteURL)
	if err != nil {
		return "", errors.Wrap(err, "failed to parse SiteURL from config")
	}

	if u.Path == "" {
		return "/", nil
	}

	return path.Clean(u.Path), nil
}