From 0836f373e367180432c1c5dea4a08adee6e07adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lian=20Garcia?= Date: Tue, 4 Feb 2025 17:36:14 +0100 Subject: [PATCH] fix: Allow OPTIONS method on every path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Célian Garcia --- injectproxy/routes.go | 32 +++++++++++++++------------- injectproxy/routes_test.go | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 61a2a89b..5120cdeb 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -315,21 +315,21 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o mux := newStrictMux(newInstrumentedMux(http.NewServeMux(), opt.registerer)) errs := merrors.New( - mux.Handle("/federate", r.el.ExtractLabel(enforceMethods(r.matcher, "GET"))), - mux.Handle("/api/v1/query", r.el.ExtractLabel(enforceMethods(r.query, "GET", "POST"))), - mux.Handle("/api/v1/query_range", r.el.ExtractLabel(enforceMethods(r.query, "GET", "POST"))), - mux.Handle("/api/v1/alerts", r.el.ExtractLabel(enforceMethods(r.passthrough, "GET"))), - mux.Handle("/api/v1/rules", r.el.ExtractLabel(enforceMethods(r.passthrough, "GET"))), - mux.Handle("/api/v1/series", r.el.ExtractLabel(enforceMethods(r.matcher, "GET", "POST"))), - mux.Handle("/api/v1/query_exemplars", r.el.ExtractLabel(enforceMethods(r.query, "GET", "POST"))), + mux.Handle("/federate", r.el.ExtractLabel(r.enforceMethods(r.matcher, "GET"))), + mux.Handle("/api/v1/query", r.el.ExtractLabel(r.enforceMethods(r.query, "GET", "POST"))), + mux.Handle("/api/v1/query_range", r.el.ExtractLabel(r.enforceMethods(r.query, "GET", "POST"))), + mux.Handle("/api/v1/alerts", r.el.ExtractLabel(r.enforceMethods(r.passthrough, "GET"))), + mux.Handle("/api/v1/rules", r.el.ExtractLabel(r.enforceMethods(r.passthrough, "GET"))), + mux.Handle("/api/v1/series", r.el.ExtractLabel(r.enforceMethods(r.matcher, "GET", "POST"))), + mux.Handle("/api/v1/query_exemplars", r.el.ExtractLabel(r.enforceMethods(r.query, "GET", "POST"))), ) if opt.enableLabelAPIs { errs.Add( - mux.Handle("/api/v1/labels", r.el.ExtractLabel(enforceMethods(r.matcher, "GET", "POST"))), + mux.Handle("/api/v1/labels", r.el.ExtractLabel(r.enforceMethods(r.matcher, "GET", "POST"))), // Full path is /api/v1/label//values but http mux does not support patterns. // This is fine though as we don't care about name for matcher injector. - mux.Handle("/api/v1/label/", r.el.ExtractLabel(enforceMethods(r.matcher, "GET"))), + mux.Handle("/api/v1/label/", r.el.ExtractLabel(r.enforceMethods(r.matcher, "GET"))), ) } @@ -338,7 +338,7 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o // semantics of the Silences API don't support multi-label matchers. mux.Handle("/api/v2/silences", r.el.ExtractLabel( r.errorIfRegexpMatch( - enforceMethods( + r.enforceMethods( assertSingleLabelValue(r.silences), "GET", "POST", ), @@ -346,14 +346,14 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o )), mux.Handle("/api/v2/silence/", r.el.ExtractLabel( r.errorIfRegexpMatch( - enforceMethods( + r.enforceMethods( assertSingleLabelValue(r.deleteSilence), "DELETE", ), ), )), - mux.Handle("/api/v2/alerts/groups", r.el.ExtractLabel(enforceMethods(r.enforceFilterParameter, "GET"))), - mux.Handle("/api/v2/alerts", r.el.ExtractLabel(enforceMethods(r.alerts, "GET"))), + mux.Handle("/api/v2/alerts/groups", r.el.ExtractLabel(r.enforceMethods(r.enforceFilterParameter, "GET"))), + mux.Handle("/api/v2/alerts", r.el.ExtractLabel(r.enforceMethods(r.alerts, "GET"))), ) errs.Add( @@ -422,8 +422,12 @@ func (r *routes) errorHandler(rw http.ResponseWriter, _ *http.Request, err error rw.WriteHeader(http.StatusBadGateway) } -func enforceMethods(h http.HandlerFunc, methods ...string) http.HandlerFunc { +func (r *routes) enforceMethods(h http.HandlerFunc, methods ...string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodOptions { + r.passthrough(w, req) + return + } for _, m := range methods { if m == req.Method { h(w, req) diff --git a/injectproxy/routes_test.go b/injectproxy/routes_test.go index e24716b5..afe9d065 100644 --- a/injectproxy/routes_test.go +++ b/injectproxy/routes_test.go @@ -23,6 +23,8 @@ import ( "strings" "testing" "time" + + "gotest.tools/v3/assert" ) var okResponse = []byte(`ok`) @@ -1334,3 +1336,44 @@ func TestQuery(t *testing.T) { } } } + +func TestOptionsHTTPMethod(t *testing.T) { + m := newMockUpstream(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Write(okResponse) + })) + defer m.Close() + r, err := NewRoutes(m.url, proxyLabel, StaticLabelEnforcer{proxyLabel}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + for _, tcase := range []struct { + url string + method string + expCode int + }{ + { + url: "http://prometheus.example.com/api/v1/query", method: http.MethodOptions, + expCode: http.StatusOK, + }, + { + url: "http://prometheus.example.com/foo/bar", method: http.MethodOptions, + expCode: http.StatusNotFound, + }, + } { + t.Run(tcase.url, func(t *testing.T) { + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(tcase.method, tcase.url, nil)) + resp := w.Result() + + if resp.StatusCode != tcase.expCode { + b, err := io.ReadAll(resp.Body) + fmt.Println(string(b), err) + t.Fatalf("expected status code %v, got %d", tcase.expCode, resp.StatusCode) + } else if resp.StatusCode == http.StatusOK { + assert.Equal(t, resp.Header.Get("Access-Control-Allow-Origin"), "*") + } + }) + } +}