github.com/thanos-io/thanos@v0.32.5/pkg/ui/ui.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package ui 5 6 import ( 7 "bytes" 8 "html/template" 9 "net/http" 10 "net/url" 11 "os" 12 "path" 13 "path/filepath" 14 "strings" 15 16 "github.com/go-kit/log" 17 "github.com/go-kit/log/level" 18 "github.com/prometheus/common/route" 19 "github.com/prometheus/common/version" 20 21 "github.com/thanos-io/thanos/pkg/component" 22 extpromhttp "github.com/thanos-io/thanos/pkg/extprom/http" 23 ) 24 25 var reactRouterPaths = []string{ 26 "/", 27 "/alerts", 28 "/blocks", 29 "/config", 30 "/flags", 31 "/global", 32 "/graph", 33 "/loaded", 34 "/rules", 35 "/service-discovery", 36 "/status", 37 "/stores", 38 "/targets", 39 "/tsdb-status", 40 } 41 42 type BaseUI struct { 43 logger log.Logger 44 tmplFuncs template.FuncMap 45 tmplVariables map[string]string 46 externalPrefix, prefixHeader string 47 component component.Component 48 } 49 50 func NewBaseUI(logger log.Logger, funcMap template.FuncMap, tmplVariables map[string]string, externalPrefix, prefixHeader string, component component.Component) *BaseUI { 51 funcMap["pathPrefix"] = func() string { return "" } 52 funcMap["buildVersion"] = func() string { return version.Revision } 53 54 return &BaseUI{logger: logger, tmplFuncs: funcMap, tmplVariables: tmplVariables, externalPrefix: externalPrefix, prefixHeader: prefixHeader, component: component} 55 } 56 57 func (bu *BaseUI) serveReactUI(w http.ResponseWriter, req *http.Request) { 58 bu.serveReactIndex("pkg/ui/static/react/index.html", w, req) 59 } 60 61 func (bu *BaseUI) serveReactIndex(index string, w http.ResponseWriter, req *http.Request) { 62 _, file, err := bu.getAssetFile(index) 63 if err != nil { 64 level.Warn(bu.logger).Log("msg", "Could not get file", "err", err, "file", index) 65 w.WriteHeader(http.StatusNotFound) 66 return 67 } 68 prefix := GetWebPrefix(bu.logger, bu.externalPrefix, bu.prefixHeader, req) 69 70 tmpl, err := template.New("").Funcs(bu.tmplFuncs). 71 Funcs(template.FuncMap{"pathPrefix": absolutePrefix(prefix)}). 72 Parse(string(file)) 73 74 if err != nil { 75 http.Error(w, err.Error(), http.StatusInternalServerError) 76 return 77 } 78 if err := tmpl.Execute(w, bu.tmplVariables); err != nil { 79 level.Warn(bu.logger).Log("msg", "template expansion failed", "err", err) 80 } 81 } 82 83 func (bu *BaseUI) getAssetFile(filename string) (os.FileInfo, []byte, error) { 84 info, err := AssetInfo(filename) 85 if err != nil { 86 return nil, nil, err 87 } 88 file, err := Asset(filename) 89 if err != nil { 90 return nil, nil, err 91 } 92 return info, file, nil 93 } 94 95 func (bu *BaseUI) serveAsset(fp string, w http.ResponseWriter, req *http.Request) error { 96 info, file, err := bu.getAssetFile(fp) 97 if err != nil { 98 return err 99 } 100 http.ServeContent(w, req, info.Name(), info.ModTime(), bytes.NewReader(file)) 101 return nil 102 } 103 104 func absolutePrefix(prefix string) func() string { 105 return func() string { 106 if prefix == "" { 107 return "" 108 } 109 return path.Join("/", prefix) 110 } 111 } 112 113 // GetWebPrefix sanitizes an external URL path prefix value. 114 // A value provided by web.external-prefix flag is preferred over the one supplied through an HTTP header. 115 func GetWebPrefix(logger log.Logger, externalPrefix, prefixHeader string, r *http.Request) string { 116 prefix := r.Header.Get(prefixHeader) 117 118 // Ignore web.prefix-header value if web.external-prefix is defined. 119 if len(externalPrefix) > 0 { 120 prefix = externalPrefix 121 } 122 123 // Even if rfc2616 suggests that Location header "value consists of a single absolute URI", browsers 124 // support relative location too. So for extra security, scheme and host parts are stripped from a dynamic prefix. 125 prefix, err := SanitizePrefix(prefix) 126 if err != nil { 127 level.Warn(logger).Log("msg", "Could not parse value of UI external prefix", "prefix", prefix, "err", err) 128 } 129 130 return prefix 131 } 132 133 func instrf(name string, ins extpromhttp.InstrumentationMiddleware, next func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc { 134 return ins.NewHandler(name, http.HandlerFunc(next)) 135 } 136 137 func registerReactApp(r *route.Router, ins extpromhttp.InstrumentationMiddleware, bu *BaseUI) { 138 for _, p := range reactRouterPaths { 139 r.Get(p, instrf("react-static", ins, bu.serveReactUI)) 140 } 141 142 // The favicon and manifest are bundled as part of the React app, but we want to serve 143 // them on the root. 144 for _, p := range []string{"/favicon.ico", "/manifest.json"} { 145 assetPath := "pkg/ui/static/react" + p 146 r.Get(p, func(w http.ResponseWriter, r *http.Request) { 147 if err := bu.serveAsset(assetPath, w, r); err != nil { 148 level.Warn(bu.logger).Log("msg", "Could not get file", "err", err, "file", assetPath) 149 w.WriteHeader(http.StatusNotFound) 150 } 151 }) 152 } 153 154 // Static files required by the React app. 155 r.Get("/static/*filepath", func(w http.ResponseWriter, r *http.Request) { 156 fp := route.Param(r.Context(), "filepath") 157 fp = filepath.Join("pkg/ui/static/react/static", fp) 158 if err := bu.serveAsset(fp, w, r); err != nil { 159 level.Warn(bu.logger).Log("msg", "Could not get file", "err", err, "file", fp) 160 w.WriteHeader(http.StatusNotFound) 161 } 162 }) 163 } 164 165 // SanitizePrefix makes sure that path prefix value is valid. 166 // A prefix is returned without a trailing slash. Hence empty string is returned for the root path. 167 func SanitizePrefix(prefix string) (string, error) { 168 u, err := url.Parse(prefix) 169 if err != nil { 170 return "", err 171 } 172 173 // Remove double slashes, convert to absolute path. 174 sanitizedPrefix := strings.TrimPrefix(path.Clean(u.Path), ".") 175 sanitizedPrefix = strings.TrimSuffix(sanitizedPrefix, "/") 176 177 return sanitizedPrefix, nil 178 }