Repository: prometheus/blackbox_exporter
Pull request #419 (2019-02-16 13:33:23)

Add regexp matching of HTTP response headers to the http probe
I needed to add the verification of header values for some of the targets probed by the blackbox exporter, but apparently matching is currently only supported for body content.

This pull request adds two new config options: `fail_if_header_matches_regexp` and `fail_if_header_not_matches_regexp`, mirroring the existing `fail_if_matches_regexp` and `fail_if_not_matches_regexp` for the response body. I considered renaming these ones to `fail_if_body_matches_regexp` and `fail_if_body_not_matches_regexp` for consistency, but that would be a breaking change and thus does not seem like a good idea.

I am fairly new to golang, so I just tried to follow the style and apparent conventions in the rest of this project. Please let me know if something should be changed.

I also tested this extensively manually, it worked in all the corner cases I tried.

gvsmirnov: Oh dear, I just checked the list of open pull requests and found not one, but TWO others with the same changes: https://github.com/prometheus/blackbox_exporter/pull/62 and https://github.com/prometheus/blackbox_exporter/pull/332  😕 Apparently they were abandoned by their authors before all the comments were addressed.

Looking at the comments from @brian-brazil, it seems like all of them are addressed in this PR now. The "check that header is missing" can be achieved by

```
fail_if_header_matches_regexp:
  - header: Not-Supposed-To-Be-Here
    regexp: '.*'
```
SuperQ: Thanks for picking up where those other PRs went stale.

Per your above comment, I think you want the regexp to match `.+` in order to enforce some header text. Or maybe I'm misunderstanding.

```yaml
fail_if_header_matches_regexp:
  - header: Not-Supposed-To-Be-Here
    allow_missing: true
    regexp: '.+'
```
gvsmirnov: >  I think you want the regexp to match `.+` in order to enforce some header text.
> EDIT: Are headers with no value after the header name valid?

If I read [RFC 7230 section 3.2](https://tools.ietf.org/html/rfc7230#section-3.2) correctly, header value is not optional, but an empty value is valid. That seems to be the generally accepted interpretation, judging by the discussions on the Internet that I found. However, `http.Header.Get()` returns an empty string (`""`) if the header is not set, which is ambiguous. Thankfully, the mapping can be accessed directly, so it's possible to just do something like `len(textproto.MIMEHeader(header)[key]) > 0`.

This leads to another consideration: as per section [section 3.2.2](https://tools.ietf.org/html/rfc7230#section-3.2.2), there may be multiple header fields with the same field name. Judging by the doc on `http.Response.Header`, they may be concatenated with comma delimiters. It is not clear to me in which cases it would be populated by the http client as a list with multiple string values, and in which cases it would be in a single string value as a comma-separated list.

I would propose the following behaviour:

1. Verify the `allow_missing` by checking if the header with given name is present at least once. If one or more values are empty, do not consider it to be missing.
2. Verify the `regexp` against all the header values for a given header name.  For "fail if matches", at least one match fails the probe. For "fail if does not match", at least one mismatch fails the probe.
3. If multiple values are concatenated into a single comma-separated string, do not do any special handling. Let the user deal with such cases on their own.

I have doubts about the behaviour in (2) for the "fail if does not match" for multi-value headers. "fail if none match" may be more desirable than "fail if any does not match".

What do you think?
gvsmirnov: After some consideration, the most common use of multi-value headers is probably `Set-Cookie` (the RFC even mentions it explicitly). The following use cases seem to be plausible:

1. Verify that a specific cookie is set: the user would want the probe to fail if a desired cookie is not present, but would not want the probe to fail if there are other cookies:
```
fail_if_header_not_matches_regexp:
  header: Set-Cookie
  regexp: '.*Domain=\.example\.com.*'
```
2. Verify that no cookies are set
```
fail_if_header_matches_regexp:
  header: Set-Cookie
  allow_missing: true
  regexp: '.*'
```

Another common multi-value header is `Accept-Encoding`, but in practice it's mostly represented as a single comma-separated value, not as multiple values.

I pushed the changes implementing the behaviour described above. Please let me know if you think it is reasonable.
gvsmirnov: > It's probably worth it.

Alright, done. The breaking change would be caught at the configuration parsing stage, so it's a fail-fast scenario.
gvsmirnov: @brian-brazil @SuperQ is there anything else I can do to get this merged? I would love to use the upstream version instead of own fork.
gvsmirnov: Huh, travis build failed when fetching dependencies. Is it possible to rerun it?
gvsmirnov: @brian-brazil @SuperQ sorry to pester, but I ask again: anything else I can do to get this merged?

Patch:
diff --git a/CONFIGURATION.md b/CONFIGURATION.md
index c261c75..7610bd4 100644
--- a/CONFIGURATION.md
+++ b/CONFIGURATION.md
@@ -58,14 +58,22 @@ The other placeholders are specified separately.
   # Probe fails if SSL is not present.
   [ fail_if_not_ssl: <boolean> | default = false ]
 
-  # Probe fails if response matches regex.
-  fail_if_matches_regexp:
+  # Probe fails if response body matches regex.
+  fail_if_body_matches_regexp:
     [ - <regex>, ... ]
 
-  # Probe fails if response does not match regex.
-  fail_if_not_matches_regexp:
+  # Probe fails if response body does not match regex.
+  fail_if_body_not_matches_regexp:
     [ - <regex>, ... ]
 
+  # Probe fails if response header matches regex. For headers with multiple values, fails if *at least one* matches.
+  fail_if_header_matches:
+    [ - <http_header_match_spec>, ... ]
+
+  # Probe fails if response header does not match regex. For headers with multiple values, fails if *none* match.
+  fail_if_header_not_matches:
+    [ - <http_header_match_spec>, ... ]
+
   # Configuration for TLS protocol of HTTP probe.
   tls_config:
     [ <tls_config> ]
@@ -86,7 +94,7 @@ The other placeholders are specified separately.
 
   # The IP protocol of the HTTP probe (ip4, ip6).
   [ preferred_ip_protocol: <string> | default = "ip6" ]
-  [ ip_protocol_fallback: <boolean | default = true> ]
+  [ ip_protocol_fallback: <boolean> | default = true ]
 
   # The body of the HTTP request used in probe.
   body: [ <string> ]
@@ -94,6 +102,14 @@ The other placeholders are specified separately.
 
 ```
 
+#### <http_header_match_spec>
+
+```yml
+header: <string>,
+regexp: <regex>,
+[ allow_missing: <boolean> | default = false ]
+```
+
 ### <tcp_probe>
 
 ```yml
diff --git a/config/config.go b/config/config.go
index afb7ecf..3652f86 100644
--- a/config/config.go
+++ b/config/config.go
@@ -80,19 +80,27 @@ type Module struct {
 
 type HTTPProbe struct {
 	// Defaults to 2xx.
-	ValidStatusCodes       []int                   `yaml:"valid_status_codes,omitempty"`
-	ValidHTTPVersions      []string                `yaml:"valid_http_versions,omitempty"`
-	IPProtocol             string                  `yaml:"preferred_ip_protocol,omitempty"`
-	IPProtocolFallback     bool                    `yaml:"ip_protocol_fallback,omitempty"`
-	NoFollowRedirects      bool                    `yaml:"no_follow_redirects,omitempty"`
-	FailIfSSL              bool                    `yaml:"fail_if_ssl,omitempty"`
-	FailIfNotSSL           bool                    `yaml:"fail_if_not_ssl,omitempty"`
-	Method                 string                  `yaml:"method,omitempty"`
-	Headers                map[string]string       `yaml:"headers,omitempty"`
-	FailIfMatchesRegexp    []string                `yaml:"fail_if_matches_regexp,omitempty"`
-	FailIfNotMatchesRegexp []string                `yaml:"fail_if_not_matches_regexp,omitempty"`
-	Body                   string                  `yaml:"body,omitempty"`
-	HTTPClientConfig       config.HTTPClientConfig `yaml:"http_client_config,inline"`
+	ValidStatusCodes             []int                   `yaml:"valid_status_codes,omitempty"`
+	ValidHTTPVersions            []string                `yaml:"valid_http_versions,omitempty"`
+	IPProtocol                   string                  `yaml:"preferred_ip_protocol,omitempty"`
+	IPProtocolFallback           bool                    `yaml:"ip_protocol_fallback,omitempty"`
+	NoFollowRedirects            bool                    `yaml:"no_follow_redirects,omitempty"`
+	FailIfSSL                    bool                    `yaml:"fail_if_ssl,omitempty"`
+	FailIfNotSSL                 bool                    `yaml:"fail_if_not_ssl,omitempty"`
+	Method                       string                  `yaml:"method,omitempty"`
+	Headers                      map[string]string       `yaml:"headers,omitempty"`
+	FailIfBodyMatchesRegexp      []string                `yaml:"fail_if_body_matches_regexp,omitempty"`
+	FailIfBodyNotMatchesRegexp   []string                `yaml:"fail_if_body_not_matches_regexp,omitempty"`
+	FailIfHeaderMatchesRegexp    []HeaderMatch           `yaml:"fail_if_header_matches,omitempty"`
+	FailIfHeaderNotMatchesRegexp []HeaderMatch           `yaml:"fail_if_header_not_matches,omitempty"`
+	Body                         string                  `yaml:"body,omitempty"`
+	HTTPClientConfig             config.HTTPClientConfig `yaml:"http_client_config,inline"`
+}
+
+type HeaderMatch struct {
+	Header       string `yaml:"header,omitempty"`
+	Regexp       string `yaml:"regexp,omitempty"`
+	AllowMissing bool   `yaml:"allow_missing,omitempty"`
 }
 
 type QueryResponse struct {
@@ -217,3 +225,21 @@ func (s *QueryResponse) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	}
 	return nil
 }
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (s *HeaderMatch) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	type plain HeaderMatch
+	if err := unmarshal((*plain)(s)); err != nil {
+		return err
+	}
+
+	if s.Header == "" {
+		return errors.New("header name must be set for HTTP header matchers")
+	}
+
+	if s.Regexp == "" {
+		return errors.New("regexp must be set for HTTP header matchers")
+	}
+
+	return nil
+}
diff --git a/example.yml b/example.yml
index 39786fb..6c720ff 100644
--- a/example.yml
+++ b/example.yml
@@ -9,13 +9,21 @@ modules:
       headers:
         Host: vhost.example.com
         Accept-Language: en-US
+        Origin: example.com
       no_follow_redirects: false
       fail_if_ssl: false
       fail_if_not_ssl: false
-      fail_if_matches_regexp:
+      fail_if_body_matches_regexp:
         - "Could not connect to database"
-      fail_if_not_matches_regexp:
+      fail_if_body_not_matches_regexp:
         - "Download the latest version here"
+      fail_if_header_matches: # Verifies that no cookies are set
+        - header: Set-Cookie
+          allow_missing: true
+          regexp: '.*'
+      fail_if_header_not_matches:
+        - header: Access-Control-Allow-Origin
+          regexp: '(\*|example\.com)'
       tls_config:
         insecure_skip_verify: false
       preferred_ip_protocol: "ip4" # defaults to "ip6"
diff --git a/prober/http.go b/prober/http.go
index 9aa6104..1c5ed23 100644
--- a/prober/http.go
+++ b/prober/http.go
@@ -23,6 +23,7 @@ import (
 	"net/http"
 	"net/http/cookiejar"
 	"net/http/httptrace"
+	"net/textproto"
 	"net/url"
 	"regexp"
 	"strconv"
@@ -44,7 +45,7 @@ func matchRegularExpressions(reader io.Reader, httpConfig config.HTTPProbe, logg
 		level.Error(logger).Log("msg", "Error reading HTTP body", "err", err)
 		return false
 	}
-	for _, expression := range httpConfig.FailIfMatchesRegexp {
+	for _, expression := range httpConfig.FailIfBodyMatchesRegexp {
 		re, err := regexp.Compile(expression)
 		if err != nil {
 			level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err)
@@ -55,7 +56,7 @@ func matchRegularExpressions(reader io.Reader, httpConfig config.HTTPProbe, logg
 			return false
 		}
 	}
-	for _, expression := range httpConfig.FailIfNotMatchesRegexp {
+	for _, expression := range httpConfig.FailIfBodyNotMatchesRegexp {
 		re, err := regexp.Compile(expression)
 		if err != nil {
 			level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err)
@@ -69,6 +70,68 @@ func matchRegularExpressions(reader io.Reader, httpConfig config.HTTPProbe, logg
 	return true
 }
 
+func matchRegularExpressionsOnHeaders(header http.Header, httpConfig config.HTTPProbe, logger log.Logger) bool {
+	for _, headerMatchSpec := range httpConfig.FailIfHeaderMatchesRegexp {
+		values := header[textproto.CanonicalMIMEHeaderKey(headerMatchSpec.Header)]
+		if len(values) == 0 {
+			if !headerMatchSpec.AllowMissing {
+				level.Error(logger).Log("msg", "Missing required header", "header", headerMatchSpec.Header)
+				return false
+			} else {
+				continue // No need to match any regex on missing headers.
+			}
+		}
+
+		re, err := regexp.Compile(headerMatchSpec.Regexp)
+		if err != nil {
+			level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", headerMatchSpec.Regexp, "err", err)
+			return false
+		}
+
+		for _, val := range values {
+			if re.MatchString(val) {
+				level.Error(logger).Log("msg", "Header matched regular expression", "header", headerMatchSpec.Header,
+					"regexp", headerMatchSpec.Regexp, "value_count", len(values))
+				return false
+			}
+		}
+	}
+	for _, headerMatchSpec := range httpConfig.FailIfHeaderNotMatchesRegexp {
+		values := header[textproto.CanonicalMIMEHeaderKey(headerMatchSpec.Header)]
+		if len(values) == 0 {
+			if !headerMatchSpec.AllowMissing {
+				level.Error(logger).Log("msg", "Missing required header", "header", headerMatchSpec.Header)
+				return false
+			} else {
+				continue // No need to match any regex on missing headers.
+			}
+		}
+
+		re, err := regexp.Compile(headerMatchSpec.Regexp)
+		if err != nil {
+			level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", headerMatchSpec.Regexp, "err", err)
+			return false
+		}
+
+		anyHeaderValueMatched := false
+
+		for _, val := range values {
+			if re.MatchString(val) {
+				anyHeaderValueMatched = true
+				break
+			}
+		}
+
+		if !anyHeaderValueMatched {
+			level.Error(logger).Log("msg", "Header did not match regular expression", "header", headerMatchSpec.Header,
+				"regexp", headerMatchSpec.Regexp, "value_count", len(values))
+			return false
+		}
+	}
+
+	return true
+}
+
 // roundTripTrace holds timings for a single HTTP roundtrip.
 type roundTripTrace struct {
 	tls           bool
@@ -320,7 +383,16 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
 			level.Info(logger).Log("msg", "Invalid HTTP response status code, wanted 2xx", "status_code", resp.StatusCode)
 		}
 
-		if success && (len(httpConfig.FailIfMatchesRegexp) > 0 || len(httpConfig.FailIfNotMatchesRegexp) > 0) {
+		if success && (len(httpConfig.FailIfHeaderMatchesRegexp) > 0 || len(httpConfig.FailIfHeaderNotMatchesRegexp) > 0) {
+			success = matchRegularExpressionsOnHeaders(resp.Header, httpConfig, logger)
+			if success {
+				probeFailedDueToRegex.Set(0)
+			} else {
+				probeFailedDueToRegex.Set(1)
+			}
+		}
+
+		if success && (len(httpConfig.FailIfBodyMatchesRegexp) > 0 || len(httpConfig.FailIfBodyNotMatchesRegexp) > 0) {
 			success = matchRegularExpressions(resp.Body, httpConfig, logger)
 			if success {
 				probeFailedDueToRegex.Set(0)


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 94b0248..d7c0719 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -38,6 +38,10 @@ func TestLoadBadConfigs(t *testing.T) {
 			ConfigFile:    "testdata/invalid-dns-module.yml",
 			ExpectedError: "error parsing config file: query name must be set for DNS module",
 		},
+		{
+			ConfigFile:    "testdata/invalid-http-header-match.yml",
+			ExpectedError: "error parsing config file: regexp must be set for HTTP header matchers",
+		},
 	}
 	for i, test := range tests {
 		err := sc.ReloadConfig(test.ConfigFile)
diff --git a/config/testdata/blackbox-good.yml b/config/testdata/blackbox-good.yml
index d0a1f0c..8615d3c 100644
--- a/config/testdata/blackbox-good.yml
+++ b/config/testdata/blackbox-good.yml
@@ -66,3 +66,14 @@ modules:
       ip_protocol_fallback: false
       validate_answer_rrs:
         fail_if_matches_regexp: [test]
+  http_header_match_origin:
+    prober: http
+    timeout: 5s
+    http:
+      method: GET
+      headers:
+        Origin: example.com
+      fail_if_header_not_matches:
+        - header: Access-Control-Allow-Origin
+          regexp: '(\*|example\.com)'
+          allow_missing: false
diff --git a/config/testdata/invalid-http-header-match.yml b/config/testdata/invalid-http-header-match.yml
new file mode 100644
index 0000000..ae56c7d
--- /dev/null
+++ b/config/testdata/invalid-http-header-match.yml
@@ -0,0 +1,8 @@
+modules:
+  http_headers:
+    prober: http
+    timeout: 5s
+    http:
+      fail_if_header_not_matches:
+        - header: Access-Control-Allow-Origin
+          allow_missing: false
diff --git a/prober/http_test.go b/prober/http_test.go
index a7deaa8..b45c96b 100644
--- a/prober/http_test.go
+++ b/prober/http_test.go
@@ -234,7 +234,7 @@ func TestFailIfNotSSL(t *testing.T) {
 	checkRegistryResults(expectedResults, mfs, t)
 }
 
-func TestFailIfMatchesRegexp(t *testing.T) {
+func TestFailIfBodyMatchesRegexp(t *testing.T) {
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, "Bad news: could not connect to database server")
 	}))
@@ -245,7 +245,7 @@ func TestFailIfMatchesRegexp(t *testing.T) {
 	testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()
 	result := ProbeHTTP(testCTX, ts.URL,
-		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger())
+		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger())
 	body := recorder.Body.String()
 	if result {
 		t.Fatalf("Regexp test succeeded unexpectedly, got %s", body)
@@ -267,7 +267,7 @@ func TestFailIfMatchesRegexp(t *testing.T) {
 	recorder = httptest.NewRecorder()
 	registry = prometheus.NewRegistry()
 	result = ProbeHTTP(testCTX, ts.URL,
-		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger())
+		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger())
 	body = recorder.Body.String()
 	if !result {
 		t.Fatalf("Regexp test failed unexpectedly, got %s", body)
@@ -291,7 +291,7 @@ func TestFailIfMatchesRegexp(t *testing.T) {
 	recorder = httptest.NewRecorder()
 	registry = prometheus.NewRegistry()
 	result = ProbeHTTP(testCTX, ts.URL,
-		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger())
+		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger())
 	body = recorder.Body.String()
 	if result {
 		t.Fatalf("Regexp test succeeded unexpectedly, got %s", body)
@@ -305,14 +305,14 @@ func TestFailIfMatchesRegexp(t *testing.T) {
 	recorder = httptest.NewRecorder()
 	registry = prometheus.NewRegistry()
 	result = ProbeHTTP(testCTX, ts.URL,
-		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger())
+		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger())
 	body = recorder.Body.String()
 	if !result {
 		t.Fatalf("Regexp test failed unexpectedly, got %s", body)
 	}
 }
 
-func TestFailIfNotMatchesRegexp(t *testing.T) {
+func TestFailIfBodyNotMatchesRegexp(t *testing.T) {
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, "Bad news: could not connect to database server")
 	}))
@@ -323,7 +323,7 @@ func TestFailIfNotMatchesRegexp(t *testing.T) {
 	testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()
 	result := ProbeHTTP(testCTX, ts.URL,
-		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger())
+		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger())
 	body := recorder.Body.String()
 	if result {
 		t.Fatalf("Regexp test succeeded unexpectedly, got %s", body)
@@ -337,7 +337,7 @@ func TestFailIfNotMatchesRegexp(t *testing.T) {
 	recorder = httptest.NewRecorder()
 	registry = prometheus.NewRegistry()
 	result = ProbeHTTP(testCTX, ts.URL,
-		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger())
+		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger())
 	body = recorder.Body.String()
 	if !result {
 		t.Fatalf("Regexp test failed unexpectedly, got %s", body)
@@ -353,7 +353,7 @@ func TestFailIfNotMatchesRegexp(t *testing.T) {
 	recorder = httptest.NewRecorder()
 	registry = prometheus.NewRegistry()
 	result = ProbeHTTP(testCTX, ts.URL,
-		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger())
+		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger())
 	body = recorder.Body.String()
 	if result {
 		t.Fatalf("Regexp test succeeded unexpectedly, got %s", body)
@@ -367,13 +367,110 @@ func TestFailIfNotMatchesRegexp(t *testing.T) {
 	recorder = httptest.NewRecorder()
 	registry = prometheus.NewRegistry()
 	result = ProbeHTTP(testCTX, ts.URL,
-		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger())
+		config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger())
 	body = recorder.Body.String()
 	if !result {
 		t.Fatalf("Regexp test failed unexpectedly, got %s", body)
 	}
 }
 
+func TestFailIfHeaderMatchesRegexp(t *testing.T) {
+	tests := []struct {
+		Rule          config.HeaderMatch
+		Values        []string
+		ShouldSucceed bool
+	}{
+		{config.HeaderMatch{"Content-Type", "text/javascript", false}, []string{"text/javascript"}, false},
+		{config.HeaderMatch{"Content-Type", "text/javascript", false}, []string{"application/octet-stream"}, true},
+		{config.HeaderMatch{"content-type", "text/javascript", false}, []string{"application/octet-stream"}, true},
+		{config.HeaderMatch{"Content-Type", ".*", false}, []string{""}, false},
+		{config.HeaderMatch{"Content-Type", ".*", false}, []string{}, false},
+		{config.HeaderMatch{"Content-Type", ".*", true}, []string{""}, false},
+		{config.HeaderMatch{"Content-Type", ".*", true}, []string{}, true},
+		{config.HeaderMatch{"Set-Cookie", ".*Domain=\\.example\\.com.*", false}, []string{"gid=1; Expires=Tue, 19-Mar-2019 20:08:29 GMT; Domain=.example.com; Path=/"}, false},
+		{config.HeaderMatch{"Set-Cookie", ".*Domain=\\.example\\.com.*", false}, []string{"zz=4; expires=Mon, 01-Jan-1990 00:00:00 GMT; Domain=www.example.com; Path=/", "gid=1; Expires=Tue, 19-Mar-2019 20:08:29 GMT; Domain=.example.com; Path=/"}, false},
+	}
+
+	for i, test := range tests {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			for _, val := range test.Values {
+				w.Header().Add(test.Rule.Header, val)
+			}
+		}))
+		defer ts.Close()
+		registry := prometheus.NewRegistry()
+		testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+
+		result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfHeaderMatchesRegexp: []config.HeaderMatch{test.Rule}}}, registry, log.NewNopLogger())
+		if result != test.ShouldSucceed {
+			t.Fatalf("Test %d had unexpected result: succeeded: %t, expected: %+v", i, result, test)
+		}
+
+		mfs, err := registry.Gather()
+		if err != nil {
+			t.Fatal(err)
+		}
+		expectedResults := map[string]float64{
+			"probe_failed_due_to_regex": 1,
+		}
+
+		if test.ShouldSucceed {
+			expectedResults["probe_failed_due_to_regex"] = 0
+		}
+
+		checkRegistryResults(expectedResults, mfs, t)
+	}
+}
+
+func TestFailIfHeaderNotMatchesRegexp(t *testing.T) {
+	tests := []struct {
+		Rule          config.HeaderMatch
+		Values        []string
+		ShouldSucceed bool
+	}{
+		{config.HeaderMatch{"Content-Type", "text/javascript", false}, []string{"text/javascript"}, true},
+		{config.HeaderMatch{"content-type", "text/javascript", false}, []string{"text/javascript"}, true},
+		{config.HeaderMatch{"Content-Type", "text/javascript", false}, []string{"application/octet-stream"}, false},
+		{config.HeaderMatch{"Content-Type", ".*", false}, []string{""}, true},
+		{config.HeaderMatch{"Content-Type", ".*", false}, []string{}, false},
+		{config.HeaderMatch{"Content-Type", ".*", true}, []string{}, true},
+		{config.HeaderMatch{"Set-Cookie", ".*Domain=\\.example\\.com.*", false}, []string{"zz=4; expires=Mon, 01-Jan-1990 00:00:00 GMT; Domain=www.example.com; Path=/"}, false},
+		{config.HeaderMatch{"Set-Cookie", ".*Domain=\\.example\\.com.*", false}, []string{"zz=4; expires=Mon, 01-Jan-1990 00:00:00 GMT; Domain=www.example.com; Path=/", "gid=1; Expires=Tue, 19-Mar-2019 20:08:29 GMT; Domain=.example.com; Path=/"}, true},
+	}
+
+	for i, test := range tests {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			for _, val := range test.Values {
+				w.Header().Add(test.Rule.Header, val)
+			}
+		}))
+		defer ts.Close()
+		registry := prometheus.NewRegistry()
+		testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+
+		result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfHeaderNotMatchesRegexp: []config.HeaderMatch{test.Rule}}}, registry, log.NewNopLogger())
+		if result != test.ShouldSucceed {
+			t.Fatalf("Test %d had unexpected result: succeeded: %t, expected: %+v", i, result, test)
+		}
+
+		mfs, err := registry.Gather()
+		if err != nil {
+			t.Fatal(err)
+		}
+		expectedResults := map[string]float64{
+			"probe_failed_due_to_regex": 1,
+		}
+
+		if test.ShouldSucceed {
+			expectedResults["probe_failed_due_to_regex"] = 0
+		}
+
+		checkRegistryResults(expectedResults, mfs, t)
+	}
+}
+
 func TestHTTPHeaders(t *testing.T) {
 	headers := map[string]string{
 		"Host":            "my-secret-vhost.com",