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  }