diff --git a/web/handler.go b/web/handler.go index 0a2718d5..22931c59 100644 --- a/web/handler.go +++ b/web/handler.go @@ -95,16 +95,18 @@ func (u *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // Configure http headers. This happens before any request is turned away, + // so that the configured headers are present on every response and not + // only on the ones that reach the wrapped handler. + for k, v := range c.HTTPConfig.Header { + w.Header().Set(k, v) + } + if u.limiter != nil && !u.limiter.Allow() { http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) return } - // Configure http headers. - for k, v := range c.HTTPConfig.Header { - w.Header().Set(k, v) - } - if len(c.Users) == 0 { u.handler.ServeHTTP(w, r) return diff --git a/web/headers_test.go b/web/headers_test.go new file mode 100644 index 00000000..a25e51a6 --- /dev/null +++ b/web/headers_test.go @@ -0,0 +1,65 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package web + +import ( + "net/http" + "testing" +) + +// TestHeadersOnRateLimitedResponse checks that the configured headers are set +// on a response that the rate limiter turns away. A header policy such as +// Strict-Transport-Security is meant to hold for every response from the +// endpoint, not only for the ones that are served. +func TestHeadersOnRateLimitedResponse(t *testing.T) { + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("Hello World!")) + }), + } + + addr := startTLSProtocolServer(t, server, "testdata/web_config_rate_limiter_headers.yaml") + + // The burst is 1 over an interval of an hour, so the first request is + // served and every one after it is rate limited. + get := func() *http.Response { + t.Helper() + r, err := http.Get("http://" + addr) + if err != nil { + t.Fatal(err) + } + r.Body.Close() + return r + } + + served := get() + if served.StatusCode != http.StatusOK { + t.Fatalf("first request: got status %d, expected %d", served.StatusCode, http.StatusOK) + } + + limited := get() + if limited.StatusCode != http.StatusTooManyRequests { + t.Fatalf("second request: got status %d, expected %d", limited.StatusCode, http.StatusTooManyRequests) + } + + for _, header := range []string{"X-Frame-Options", "Strict-Transport-Security"} { + want := served.Header.Get(header) + if want == "" { + t.Fatalf("%s was not set on the served response", header) + } + if got := limited.Header.Get(header); got != want { + t.Errorf("%s on the rate limited response = %q, expected %q", header, got, want) + } + } +} diff --git a/web/testdata/web_config_rate_limiter_headers.yaml b/web/testdata/web_config_rate_limiter_headers.yaml new file mode 100644 index 00000000..6a8c64f4 --- /dev/null +++ b/web/testdata/web_config_rate_limiter_headers.yaml @@ -0,0 +1,7 @@ +rate_limit: + interval: 1h + burst: 1 +http_server_config: + headers: + X-Frame-Options: deny + Strict-Transport-Security: max-age=31536000; includeSubDomains