From 3c0ae3f7b27bd02d3ab1a31bff8d926412752367 Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Fri, 14 Jul 2023 16:44:36 +0800 Subject: [PATCH 01/10] Added command line option to set arbitrary headers on upstream query to prometheus Added option to set host header on upstream query to prometheus - addresses https://github.com/prometheus-community/prom-label-proxy/issues/135 Signed-off-by: Graeme Christie --- injectproxy/routes.go | 22 ++++++++++++++++++++-- main.go | 13 ++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 266d3226..76c404c4 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "net/http/httputil" "net/url" @@ -261,7 +262,7 @@ func (sle StaticLabelEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler }) } -func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, opts ...Option) (*routes, error) { +func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, extraHttpHeaders []string, rewriteHostHeader string, opts ...Option) (*routes, error) { opt := options{} for _, o := range opts { o.apply(&opt) @@ -271,7 +272,24 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o opt.registerer = prometheus.NewRegistry() } - proxy := httputil.NewSingleHostReverseProxy(upstream) + proxy := &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(upstream) + if len(strings.TrimSpace(rewriteHostHeader)) == 0 { + r.Out.Host = r.In.Host + } else { + r.Out.Host = strings.TrimSpace(rewriteHostHeader) + } + for _, headerArg := range extraHttpHeaders { + header, val, found := strings.Cut(headerArg, ":") + if !found { + log.Printf("Header %s specified but ':' delimited not found", headerArg) + continue + } + r.Out.Header[strings.TrimSpace(header)] = []string{strings.TrimSpace(val)} + } + }, + } r := &routes{ upstream: upstream, diff --git a/main.go b/main.go index 9c3520ff..ad4533ce 100644 --- a/main.go +++ b/main.go @@ -64,6 +64,8 @@ func main() { enableLabelAPIs bool unsafePassthroughPaths string // Comma-delimited string. errorOnReplace bool + extraHttpHeaders arrayFlags + rewriteHostHeader string ) flagset := flag.NewFlagSet(os.Args[0], flag.ExitOnError) @@ -81,6 +83,8 @@ func main() { "This option is checked after Prometheus APIs, you cannot override enforced API endpoints to be not enforced with this option. Use carefully as it can easily cause a data leak if the provided path is an important "+ "API (like /api/v1/configuration) which isn't enforced by prom-label-proxy. NOTE: \"all\" matching paths like \"/\" or \"\" and regex are not allowed.") flagset.BoolVar(&errorOnReplace, "error-on-replace", false, "When specified, the proxy will return HTTP status code 400 if the query already contains a label matcher that differs from the one the proxy would inject.") + flagset.Var(&extraHttpHeaders, "extra-http-header", "Additional HTTP headers to add to the upstream prometheus query in the format 'header: value'. Can be repeated multiple times for additional headers.") + flagset.StringVar(&rewriteHostHeader, "rewrite-host-header-to", "", "Rewrite host header to supplied value when sending the query to the upstream URL.") //nolint: errcheck // Parse() will exit on error. flagset.Parse(os.Args[1:]) @@ -109,6 +113,13 @@ func main() { log.Fatalf("Invalid scheme for upstream URL %q, only 'http' and 'https' are supported", upstream) } + for _, headerArg := range extraHttpHeaders { + header, val, found := strings.Cut(headerArg, ":") + if !found || len(strings.TrimSpace(header)) == 0 || len(strings.TrimSpace(val)) == 0 { + log.Fatalf("extra-http-header %s is not in the format 'key:value'", headerArg) + } + } + reg := prometheus.NewRegistry() reg.MustRegister( collectors.NewGoCollector(), @@ -140,7 +151,7 @@ func main() { { // Run the insecure HTTP server. - routes, err := injectproxy.NewRoutes(upstreamURL, label, extractLabeler, opts...) + routes, err := injectproxy.NewRoutes(upstreamURL, label, extractLabeler, extraHttpHeaders, rewriteHostHeader, opts...) if err != nil { log.Fatalf("Failed to create injectproxy Routes: %v", err) } From 9fef3555e4d6c0dfa1c0ffbee6bd41188b25062f Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Fri, 14 Jul 2023 22:49:22 +0800 Subject: [PATCH 02/10] Refactored to use options pattern for new command line args Signed-off-by: Graeme Christie --- injectproxy/routes.go | 43 +++++++++++++++++++++++++++++-------------- main.go | 10 +++++++++- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 76c404c4..d6c69f77 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -18,7 +18,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "net/http/httputil" "net/url" @@ -50,10 +49,12 @@ type routes struct { } type options struct { - enableLabelAPIs bool - passthroughPaths []string - errorOnReplace bool - registerer prometheus.Registerer + enableLabelAPIs bool + passthroughPaths []string + errorOnReplace bool + registerer prometheus.Registerer + extraHttpHeaders map[string]string + rewriteHostHeader string } type Option interface { @@ -97,6 +98,24 @@ func WithErrorOnReplace() Option { }) } +func WithExtraHttpHeaders(headers []string) Option { + return optionFunc(func(o *options) { + o.extraHttpHeaders = make(map[string]string) + for _, headerArg := range headers { + header, val, found := strings.Cut(headerArg, ":") + if found { + o.extraHttpHeaders[strings.TrimSpace(header)] = strings.TrimSpace(val) + } + } + }) +} + +func WithRewriteHostHeader(host string) Option { + return optionFunc(func(o *options) { + o.rewriteHostHeader = host + }) +} + // mux abstracts away the behavior we expect from the http.ServeMux type in this package. type mux interface { http.Handler @@ -262,7 +281,8 @@ func (sle StaticLabelEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler }) } -func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, extraHttpHeaders []string, rewriteHostHeader string, opts ...Option) (*routes, error) { +func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, opts ...Option) (*routes, error) { + //extraHttpHeaders []string, rewriteHostHeader string opt := options{} for _, o := range opts { o.apply(&opt) @@ -275,17 +295,12 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, e proxy := &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(upstream) - if len(strings.TrimSpace(rewriteHostHeader)) == 0 { + if len(opt.rewriteHostHeader) == 0 { r.Out.Host = r.In.Host } else { - r.Out.Host = strings.TrimSpace(rewriteHostHeader) + r.Out.Host = opt.rewriteHostHeader } - for _, headerArg := range extraHttpHeaders { - header, val, found := strings.Cut(headerArg, ":") - if !found { - log.Printf("Header %s specified but ':' delimited not found", headerArg) - continue - } + for header, val := range opt.extraHttpHeaders { r.Out.Header[strings.TrimSpace(header)] = []string{strings.TrimSpace(val)} } }, diff --git a/main.go b/main.go index ad4533ce..c4534c5b 100644 --- a/main.go +++ b/main.go @@ -137,6 +137,14 @@ func main() { opts = append(opts, injectproxy.WithErrorOnReplace()) } + if len(extraHttpHeaders) > 0 { + opts = append(opts, injectproxy.WithExtraHttpHeaders(extraHttpHeaders)) + } + + if len(rewriteHostHeader) > 0 { + opts = append(opts, injectproxy.WithRewriteHostHeader(rewriteHostHeader)) + } + var extractLabeler injectproxy.ExtractLabeler switch { case len(labelValues) > 0: @@ -151,7 +159,7 @@ func main() { { // Run the insecure HTTP server. - routes, err := injectproxy.NewRoutes(upstreamURL, label, extractLabeler, extraHttpHeaders, rewriteHostHeader, opts...) + routes, err := injectproxy.NewRoutes(upstreamURL, label, extractLabeler, opts...) if err != nil { log.Fatalf("Failed to create injectproxy Routes: %v", err) } From 8e48586fa1f6975a77a79e4917838dc6e9cc6927 Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Sat, 15 Jul 2023 15:53:39 +0800 Subject: [PATCH 03/10] Removed superfluous trimspace() Signed-off-by: Graeme Christie --- injectproxy/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/injectproxy/routes.go b/injectproxy/routes.go index d6c69f77..03f374ea 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -104,7 +104,7 @@ func WithExtraHttpHeaders(headers []string) Option { for _, headerArg := range headers { header, val, found := strings.Cut(headerArg, ":") if found { - o.extraHttpHeaders[strings.TrimSpace(header)] = strings.TrimSpace(val) + o.extraHttpHeaders[header] = val } } }) From 645d6313ddd492cacc9ef8044e72413860f03f61 Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Sat, 15 Jul 2023 15:55:13 +0800 Subject: [PATCH 04/10] Removed superfluous trimspace() Signed-off-by: Graeme Christie --- injectproxy/routes.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 03f374ea..3d833643 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -104,7 +104,7 @@ func WithExtraHttpHeaders(headers []string) Option { for _, headerArg := range headers { header, val, found := strings.Cut(headerArg, ":") if found { - o.extraHttpHeaders[header] = val + o.extraHttpHeaders[strings.TrimSpace(header)] = strings.TrimSpace(val) } } }) @@ -301,7 +301,7 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o r.Out.Host = opt.rewriteHostHeader } for header, val := range opt.extraHttpHeaders { - r.Out.Header[strings.TrimSpace(header)] = []string{strings.TrimSpace(val)} + r.Out.Header[header] = []string{val} } }, } From d4088e8b80851361e89919bf191cbc2546022d53 Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Wed, 1 Nov 2023 17:15:07 +0800 Subject: [PATCH 05/10] Update injectproxy/routes.go Co-authored-by: Simon Pasquier Signed-off-by: Graeme Christie --- injectproxy/routes.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 3d833643..00c0fff8 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -98,15 +98,9 @@ func WithErrorOnReplace() Option { }) } -func WithExtraHttpHeaders(headers []string) Option { +func WithExtraHttpHeader(key, value string) Option { return optionFunc(func(o *options) { - o.extraHttpHeaders = make(map[string]string) - for _, headerArg := range headers { - header, val, found := strings.Cut(headerArg, ":") - if found { - o.extraHttpHeaders[strings.TrimSpace(header)] = strings.TrimSpace(val) - } - } + o.extraHttpHeaders[key] = value }) } From 5820b25c9ba96ae13ef91e84b6ac67557b289306 Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Wed, 1 Nov 2023 17:15:27 +0800 Subject: [PATCH 06/10] Update injectproxy/routes.go Co-authored-by: Simon Pasquier Signed-off-by: Graeme Christie --- injectproxy/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 00c0fff8..87f85405 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -289,7 +289,7 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o proxy := &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(upstream) - if len(opt.rewriteHostHeader) == 0 { + if opt.rewriteHostHeader == "" { r.Out.Host = r.In.Host } else { r.Out.Host = opt.rewriteHostHeader From e3b7c3d5c971665a510509642cb884c77d7e058e Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Wed, 1 Nov 2023 17:15:41 +0800 Subject: [PATCH 07/10] Update injectproxy/routes.go Co-authored-by: Simon Pasquier Signed-off-by: Graeme Christie --- injectproxy/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 87f85405..2f56dd37 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -295,7 +295,7 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o r.Out.Host = opt.rewriteHostHeader } for header, val := range opt.extraHttpHeaders { - r.Out.Header[header] = []string{val} + r.Out.Header.Set(header, val) } }, } From 3578ebec478991083556711b0bd8b78c9a2f04b0 Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Wed, 1 Nov 2023 17:16:03 +0800 Subject: [PATCH 08/10] Update main.go Co-authored-by: Simon Pasquier Signed-off-by: Graeme Christie --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index c4534c5b..a76fb113 100644 --- a/main.go +++ b/main.go @@ -83,7 +83,7 @@ func main() { "This option is checked after Prometheus APIs, you cannot override enforced API endpoints to be not enforced with this option. Use carefully as it can easily cause a data leak if the provided path is an important "+ "API (like /api/v1/configuration) which isn't enforced by prom-label-proxy. NOTE: \"all\" matching paths like \"/\" or \"\" and regex are not allowed.") flagset.BoolVar(&errorOnReplace, "error-on-replace", false, "When specified, the proxy will return HTTP status code 400 if the query already contains a label matcher that differs from the one the proxy would inject.") - flagset.Var(&extraHttpHeaders, "extra-http-header", "Additional HTTP headers to add to the upstream prometheus query in the format 'header: value'. Can be repeated multiple times for additional headers.") + flagset.Var(&extraHttpHeaders, "extra-http-header", "HTTP header to add to the upstream query in the format 'header: value'. Can be repeated multiple times.") flagset.StringVar(&rewriteHostHeader, "rewrite-host-header-to", "", "Rewrite host header to supplied value when sending the query to the upstream URL.") //nolint: errcheck // Parse() will exit on error. From 8a31ee729340240b5af4544684fca6e1a6946632 Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Wed, 1 Nov 2023 17:32:29 +0800 Subject: [PATCH 09/10] Removed unused commented code Signed-off-by: Graeme Christie --- injectproxy/routes.go | 1 - 1 file changed, 1 deletion(-) diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 2f56dd37..09a397ee 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -276,7 +276,6 @@ func (sle StaticLabelEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler } func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, opts ...Option) (*routes, error) { - //extraHttpHeaders []string, rewriteHostHeader string opt := options{} for _, o := range opts { o.apply(&opt) From be913f25088e4502dbabe1cb930e5f2271cd13b5 Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Wed, 1 Nov 2023 18:03:16 +0800 Subject: [PATCH 10/10] Refactored validation/setting of extraHttpHeaders Signed-off-by: Graeme Christie --- main.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index a76fb113..c0d58a18 100644 --- a/main.go +++ b/main.go @@ -113,13 +113,6 @@ func main() { log.Fatalf("Invalid scheme for upstream URL %q, only 'http' and 'https' are supported", upstream) } - for _, headerArg := range extraHttpHeaders { - header, val, found := strings.Cut(headerArg, ":") - if !found || len(strings.TrimSpace(header)) == 0 || len(strings.TrimSpace(val)) == 0 { - log.Fatalf("extra-http-header %s is not in the format 'key:value'", headerArg) - } - } - reg := prometheus.NewRegistry() reg.MustRegister( collectors.NewGoCollector(), @@ -137,8 +130,12 @@ func main() { opts = append(opts, injectproxy.WithErrorOnReplace()) } - if len(extraHttpHeaders) > 0 { - opts = append(opts, injectproxy.WithExtraHttpHeaders(extraHttpHeaders)) + for _, headerArg := range extraHttpHeaders { + header, val, found := strings.Cut(headerArg, ":") + if !found || len(strings.TrimSpace(header)) == 0 || len(strings.TrimSpace(val)) == 0 { + log.Fatalf("extra-http-header %s is not in the format 'key:value'", headerArg) + } + opts = append(opts, injectproxy.WithExtraHttpHeader(header, val)) } if len(rewriteHostHeader) > 0 {