Update UI Config passing to not use an inline script (#8645)

* Update UI Config passing to not use an inline script

* Update agent/http.go

* Fix incorrect placeholder name
This commit is contained in:
Paul Banks 2020-09-15 20:57:37 +01:00 committed by GitHub
parent 296340e13f
commit 0062106c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 166 additions and 143 deletions

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,6 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"text/template"
"time" "time"
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
@ -88,64 +87,64 @@ type HTTPServer struct {
denylist *Denylist denylist *Denylist
} }
type templatedFile struct { // bufferedFile implements os.File and allows us to modify a file from disk by
// writing out the new version into a buffer and then serving file reads from
// that. It assumes you are modifying a real file and presents the actual file's
// info when queried.
type bufferedFile struct {
templated *bytes.Reader templated *bytes.Reader
name string info os.FileInfo
mode os.FileMode
modTime time.Time
} }
func newTemplatedFile(buf *bytes.Buffer, raw http.File) *templatedFile { func newBufferedFile(buf *bytes.Buffer, raw http.File) *bufferedFile {
info, _ := raw.Stat() info, _ := raw.Stat()
return &templatedFile{ return &bufferedFile{
templated: bytes.NewReader(buf.Bytes()), templated: bytes.NewReader(buf.Bytes()),
name: info.Name(), info: info,
mode: info.Mode(),
modTime: info.ModTime(),
} }
} }
func (t *templatedFile) Read(p []byte) (n int, err error) { func (t *bufferedFile) Read(p []byte) (n int, err error) {
return t.templated.Read(p) return t.templated.Read(p)
} }
func (t *templatedFile) Seek(offset int64, whence int) (int64, error) { func (t *bufferedFile) Seek(offset int64, whence int) (int64, error) {
return t.templated.Seek(offset, whence) return t.templated.Seek(offset, whence)
} }
func (t *templatedFile) Close() error { func (t *bufferedFile) Close() error {
return nil return nil
} }
func (t *templatedFile) Readdir(count int) ([]os.FileInfo, error) { func (t *bufferedFile) Readdir(count int) ([]os.FileInfo, error) {
return nil, errors.New("not a directory") return nil, errors.New("not a directory")
} }
func (t *templatedFile) Stat() (os.FileInfo, error) { func (t *bufferedFile) Stat() (os.FileInfo, error) {
return t, nil return t, nil
} }
func (t *templatedFile) Name() string { func (t *bufferedFile) Name() string {
return t.name return t.info.Name()
} }
func (t *templatedFile) Size() int64 { func (t *bufferedFile) Size() int64 {
return int64(t.templated.Len()) return int64(t.templated.Len())
} }
func (t *templatedFile) Mode() os.FileMode { func (t *bufferedFile) Mode() os.FileMode {
return t.mode return t.info.Mode()
} }
func (t *templatedFile) ModTime() time.Time { func (t *bufferedFile) ModTime() time.Time {
return t.modTime return t.info.ModTime()
} }
func (t *templatedFile) IsDir() bool { func (t *bufferedFile) IsDir() bool {
return false return false
} }
func (t *templatedFile) Sys() interface{} { func (t *bufferedFile) Sys() interface{} {
return nil return nil
} }
@ -161,28 +160,56 @@ func (fs *redirectFS) Open(name string) (http.File, error) {
return file, err return file, err
} }
type templatedIndexFS struct { type settingsInjectedIndexFS struct {
fs http.FileSystem fs http.FileSystem
templateVars func() map[string]interface{} UISettings map[string]interface{}
} }
func (fs *templatedIndexFS) Open(name string) (http.File, error) { func (fs *settingsInjectedIndexFS) Open(name string) (http.File, error) {
file, err := fs.fs.Open(name) file, err := fs.fs.Open(name)
if err != nil || name != "/index.html" { if err != nil || name != "/index.html" {
return file, err return file, err
} }
content, _ := ioutil.ReadAll(file) content, err := ioutil.ReadAll(file)
file.Seek(0, 0)
t, err := template.New("fmtedindex").Parse(string(content))
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed reading index.html: %s", err)
} }
var out bytes.Buffer file.Seek(0, 0)
if err := t.Execute(&out, fs.templateVars()); err != nil {
return nil, err // Replace the placeholder in the meta ENV with the actual UI config settings.
// Ember passes the ENV with URL encoded JSON in a meta tag. We are replacing
// a key and value that is the encoded version of
// `"CONSUL_UI_SETTINGS_PLACEHOLDER":"__CONSUL_UI_SETTINGS_GO_HERE__"`
// with a URL-encoded JSON blob representing the actual config.
// First built an escaped, JSON blob from the settings passed.
bs, err := json.Marshal(fs.UISettings)
if err != nil {
return nil, fmt.Errorf("failed marshalling UI settings JSON: %s", err)
} }
return newTemplatedFile(&out, file), nil // We want to remove the first and last chars which will be the { and } since
// we are injecting these variabled into the middle of an existing object.
bs = bytes.Trim(bs, "{}")
// We use PathEscape because we don't want spaces to be turned into "+" like
// QueryEscape does.
escaped := url.PathEscape(string(bs))
content = bytes.Replace(content,
[]byte("%22CONSUL_UI_SETTINGS_PLACEHOLDER%22%3A%22__CONSUL_UI_SETTINGS_GO_HERE__%22"),
[]byte(escaped), 1)
// We also need to inject the content path. This used to be a go template
// hence the syntax but for now simple string replacement is fine esp. since
// all the other templated stuff above can't easily be done that was as we are
// replacing an entire placeholder element in an encoded JSON blob with
// multiple encoded JSON elements.
if path, ok := fs.UISettings["CONSUL_CONTENT_PATH"].(string); ok {
content = bytes.Replace(content, []byte("{{.ContentPath}}"), []byte(path), -1)
}
return newBufferedFile(bytes.NewBuffer(content), file), nil
} }
// endpoint is a Consul-specific HTTP handler that takes the usual arguments in // endpoint is a Consul-specific HTTP handler that takes the usual arguments in
@ -332,7 +359,7 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler {
uifs = fs uifs = fs
} }
uifs = &redirectFS{fs: &templatedIndexFS{fs: uifs, templateVars: s.GenerateHTMLTemplateVars}} uifs = &redirectFS{fs: &settingsInjectedIndexFS{fs: uifs, UISettings: s.GetUIENVFromConfig()}}
// create a http handler using the ui file system // create a http handler using the ui file system
// and the headers specified by the http_config.response_headers user config // and the headers specified by the http_config.response_headers user config
uifsWithHeaders := serveHandlerWithHeaders( uifsWithHeaders := serveHandlerWithHeaders(
@ -366,13 +393,13 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler {
} }
} }
func (s *HTTPServer) GenerateHTMLTemplateVars() map[string]interface{} { func (s *HTTPServer) GetUIENVFromConfig() map[string]interface{} {
vars := map[string]interface{}{ vars := map[string]interface{}{
"ContentPath": s.agent.config.UIContentPath, "CONSUL_CONTENT_PATH": s.agent.config.UIContentPath,
"ACLsEnabled": s.agent.config.ACLsEnabled, "CONSUL_ACLS_ENABLED": s.agent.config.ACLsEnabled,
} }
s.addEnterpriseHTMLTemplateVars(vars) s.addEnterpriseUIENVVars(vars)
return vars return vars
} }

View File

@ -55,7 +55,7 @@ func (s *HTTPServer) rewordUnknownEnterpriseFieldError(err error) error {
return err return err
} }
func (s *HTTPServer) addEnterpriseHTMLTemplateVars(vars map[string]interface{}) {} func (s *HTTPServer) addEnterpriseUIENVVars(vars map[string]interface{}) {}
func parseACLAuthMethodEnterpriseMeta(req *http.Request, _ *structs.ACLAuthMethodEnterpriseMeta) error { func parseACLAuthMethodEnterpriseMeta(req *http.Request, _ *structs.ACLAuthMethodEnterpriseMeta) error {
if methodNS := req.URL.Query().Get("authmethod-ns"); methodNS != "" { if methodNS := req.URL.Query().Get("authmethod-ns"); methodNS != "" {

View File

@ -130,10 +130,13 @@ module.exports = function(environment, $ = process.env) {
// Make sure all templated variables check for existence first // Make sure all templated variables check for existence first
// before outputting them, this means they all should be conditionals // before outputting them, this means they all should be conditionals
ENV = Object.assign({}, ENV, { ENV = Object.assign({}, ENV, {
CONSUL_ACLS_ENABLED: '{{ if .ACLsEnabled }}{{.ACLsEnabled}}{{ else }}false{{ end }}', // This ENV var is a special placeholder that Consul will replace
CONSUL_SSO_ENABLED: '{{ if .SSOEnabled }}{{.SSOEnabled}}{{ else }}false{{ end }}', // entirely with multiple vars from the runtime config for example
CONSUL_NSPACES_ENABLED: // CONSUL_ACLs_ENABLED and CONSUL_NSPACES_ENABLED. The actual key here
'{{ if .NamespacesEnabled }}{{.NamespacesEnabled}}{{ else }}false{{ end }}', // won't really exist in the actual ember ENV when it's being served
// through Consul. See settingsInjectedIndexFS.Open in Go code for the
// details.
CONSUL_UI_SETTINGS_PLACEHOLDER: "__CONSUL_UI_SETTINGS_GO_HERE__",
}); });
break; break;
} }

View File

@ -1,35 +1,5 @@
module.exports = ({ appName, environment, rootURL, config }) => ` module.exports = ({ appName, environment, rootURL, config }) => `
<!-- CONSUL_VERSION: ${config.CONSUL_VERSION} --> <!-- CONSUL_VERSION: ${config.CONSUL_VERSION} -->
<script>
var setConfig = function(appName, config) {
var $meta = document.querySelector('meta[name="' + appName + '/config/environment"]');
var defaultConfig = JSON.parse(decodeURIComponent($meta.getAttribute('content')));
(
function set(blob, config) {
Object.keys(config).forEach(
function(key) {
var value = config[key];
if(Object.prototype.toString.call(value) === '[object Object]') {
set(blob[key], config[key]);
} else {
blob[key] = config[key];
}
}
);
}
)(defaultConfig, config);
$meta.setAttribute('content', encodeURIComponent(JSON.stringify(defaultConfig)));
}
setConfig(
'${appName}',
{
rootURL: '${rootURL}',
CONSUL_ACLS_ENABLED: ${config.CONSUL_ACLS_ENABLED},
CONSUL_NSPACES_ENABLED: ${config.CONSUL_NSPACES_ENABLED},
CONSUL_SSO_ENABLED: ${config.CONSUL_SSO_ENABLED}
}
);
</script>
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-32x32.png" sizes="32x32"> <link rel="icon" type="image/png" href="${rootURL}assets/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-16x16.png" sizes="16x16"> <link rel="icon" type="image/png" href="${rootURL}assets/favicon-16x16.png" sizes="16x16">
<link integrity="" rel="stylesheet" href="${rootURL}assets/vendor.css"> <link integrity="" rel="stylesheet" href="${rootURL}assets/vendor.css">