Skip to content

Add processing for route prefixes, external urls, and pprof mux #293

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions web/kingpinflag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ func AddFlags(a flagGroup, defaultAddress string) *web.FlagConfig {
"Use systemd socket activation listeners instead of port listeners (Linux only).",
).Bool()
}
webExternalURL := a.Flag(
"web.external-url",
"The URL under which the exporter is externally reachable (for example, if served via a reverse proxy). Used for generating relative and absolute links back to the exporter itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by the exporter. If omitted, relevant URL components will be derived automatically.",
).PlaceHolder("<url>").String()
flags := web.FlagConfig{
WebListenAddresses: a.Flag(
"web.listen-address",
Expand All @@ -46,6 +50,9 @@ func AddFlags(a flagGroup, defaultAddress string) *web.FlagConfig {
"web.config.file",
"Path to configuration file that can enable TLS or authentication. See: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md",
).Default("").String(),
WebExternalURL: webExternalURL,
WebRoutePrefix: a.Flag("web.route-prefix", "Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url.").Default(*webExternalURL).String(),
WebMetricsPath: a.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String(),
}
return &flags
}
143 changes: 124 additions & 19 deletions web/landing_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,40 @@
import (
"bytes"
_ "embed"
"errors"
"fmt"
"net"
"net/http"
"net/http/pprof"
"net/url"
"os"
"path"
"strings"
"text/template"
)

// Config represents the configuration of the web listener.
type LandingConfig struct {
RoutePrefix string // The route prefix for the exporter.
HeaderColor string // Used for the landing page header.
CSS string // CSS style tag for the landing page.
Name string // The name of the exporter, generally suffixed by _exporter.
Description string // A short description about the exporter.
Form LandingForm // A POST form.
Links []LandingLinks // Links displayed on the landing page.
ExtraHTML string // Additional HTML to be embedded.
ExtraCSS string // Additional CSS to be embedded.
Version string // The version displayed.
RoutePrefix string // The route prefix for the exporter.
ExternalURL string // The external URL for the exporter.
ListenAddresses []string // The listen address for the exporter.
UseSystemdSocket bool // Use systemd socket.
HeaderColor string // Used for the landing page header.
CSS string // CSS style tag for the landing page.
Name string // The name of the exporter, generally suffixed by _exporter.
Description string // A short description about the exporter.
Form LandingForm // A POST form.
Links []LandingLinks // Links displayed on the landing page.
ExtraHTML string // Additional HTML to be embedded.
ExtraCSS string // Additional CSS to be embedded.
Version string // The version displayed.
Logger Logger // Logging interface
}

type Logger interface {
Error(msg string, keysAndValues ...interface{})
Info(msg string, keysAndValues ...interface{})
Debug(msg string, keysAndValues ...interface{})
}

// LandingForm provides a configuration struct for creating a POST form on the landing page.
Expand Down Expand Up @@ -65,6 +82,7 @@
type LandingPageHandler struct {
landingPage []byte
routePrefix string
pprofMux *http.ServeMux
}

var (
Expand All @@ -74,18 +92,99 @@
landingPagecssContent string
)

func NewLandingPage(c LandingConfig) (*LandingPageHandler, error) {
func NewLandingPage(c LandingConfig) (*LandingPageHandler, string, error) {
var buf bytes.Buffer

c.Form.Action = strings.TrimPrefix(c.Form.Action, "/")

// Setup URL and Prefix logic
if c.ExternalURL == "" && c.UseSystemdSocket {
return nil, "", fmt.Errorf("cannot automatically infer external URL with systemd socket listener")
}

if c.ExternalURL == "" && len(c.ListenAddresses) > 1 {
c.Logger.Info("Inferring external URL from first provided listen address")
}

if len(c.ListenAddresses) == 0 {
return nil, "", fmt.Errorf("no listen addresses provided")
}

// Compute external URL
var parsedExternalURL *url.URL
var err error
if c.ExternalURL == "" {
hostname, err := os.Hostname()
if err != nil {
return nil, "", err
}
_, port, err := net.SplitHostPort(c.ListenAddresses[0])
if err != nil {
return nil, "", err
}
c.ExternalURL = fmt.Sprintf("http://%s:%s/", hostname, port)
}

if strings.HasPrefix(c.ExternalURL, "\"") || strings.HasPrefix(c.ExternalURL, "'") ||
strings.HasSuffix(c.ExternalURL, "\"") || strings.HasSuffix(c.ExternalURL, "'") {
return nil, "", errors.New("URL must not begin or end with quotes")
}

parsedExternalURL, err = url.Parse(c.ExternalURL)
if err != nil {
return nil, "", fmt.Errorf("failed to determine external URL: %w", err)
}

// Ensure Path component of ExternalURL is formatted without trailing slashes,
// contains a leading slash, and is not empty
pathPrefix := strings.TrimRight(parsedExternalURL.Path, "/")
if pathPrefix != "" && !strings.HasPrefix(pathPrefix, "/") {

Check warning

Code scanning / CodeQL

Bad redirect check Medium

This is a check that
this value
, which flows into a
redirect
, has a leading slash, but not that it does not have '/' or '' in its second position.
pathPrefix = "/" + pathPrefix
}
parsedExternalURL.Path = pathPrefix

if c.RoutePrefix == "" {
c.RoutePrefix = parsedExternalURL.Path
c.Logger.Info("RoutePrefix is empty, defaulting to ExternalURL path", "url", parsedExternalURL.Path)
} else {
c.Logger.Info("RoutePrefix is set", "RoutePrefix", c.RoutePrefix)
}

c.RoutePrefix = "/" + strings.Trim(c.RoutePrefix, "/")
if c.RoutePrefix != "/" {
c.RoutePrefix += "/"
}

if c.RoutePrefix == "" {
c.RoutePrefix = "/"
} else if !strings.HasSuffix(c.RoutePrefix, "/") {
c.RoutePrefix += "/"
}

// Validate RoutePrefix
if !strings.HasPrefix(c.RoutePrefix, "/") {
return nil, "", fmt.Errorf("route prefix must start with '/'")
}

// Redirect over externalURL for root path only if routePrefix is different from "/"
if c.RoutePrefix != "/" {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
http.Redirect(w, r, parsedExternalURL.String(), http.StatusFound)
})
}

length := 0
for _, input := range c.Form.Inputs {
inputLength := len(input.Label)
if inputLength > length {
length = inputLength
}
}

c.Form.Width = (float64(length) + 1) / 2
if c.CSS == "" {
if c.HeaderColor == "" {
Expand All @@ -94,15 +193,11 @@
}
cssTemplate := template.Must(template.New("landing css").Parse(landingPagecssContent))
if err := cssTemplate.Execute(&buf, c); err != nil {
return nil, err
return nil, "", err
}
c.CSS = buf.String()
}
if c.RoutePrefix == "" {
c.RoutePrefix = "/"
} else if !strings.HasSuffix(c.RoutePrefix, "/") {
c.RoutePrefix += "/"
}

// Strip leading '/' from Links if present
for i, link := range c.Links {
c.Links[i].Address = strings.TrimPrefix(link.Address, "/")
Expand All @@ -111,16 +206,26 @@

buf.Reset()
if err := t.Execute(&buf, c); err != nil {
return nil, err
return nil, "", err
}

// Create a new ServeMux for pprof endpoints in the LandingPage
pprofMux := http.NewServeMux()
pprofMux.HandleFunc(path.Join(c.RoutePrefix, "debug/pprof/profile"), pprof.Profile)
pprofMux.HandleFunc(path.Join(c.RoutePrefix, "debug/pprof/heap"), pprof.Handler("heap").ServeHTTP)

return &LandingPageHandler{
landingPage: buf.Bytes(),
routePrefix: c.RoutePrefix,
}, nil
pprofMux: pprofMux,
}, c.RoutePrefix, nil
}

func (h *LandingPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, path.Join(h.routePrefix, "debug/pprof/")) {
h.pprofMux.ServeHTTP(w, r)
return
}
if r.URL.Path != h.routePrefix {
http.NotFound(w, r)
return
Expand Down
3 changes: 3 additions & 0 deletions web/tls_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ type FlagConfig struct {
WebListenAddresses *[]string
WebSystemdSocket *bool
WebConfigFile *string
WebExternalURL *string
WebRoutePrefix *string
WebMetricsPath *string
}

// SetDirectory joins any relative file paths with dir.
Expand Down
Loading