Add regexp matching of HTTP response headers to the http probe (#419)
authorGleb Smirnov <me@gvsmirnov.ru>
Thu, 21 Feb 2019 13:28:52 +0000 (21:28 +0800)
committerBrian Brazil <brian.brazil@robustperception.io>
Thu, 21 Feb 2019 13:28:52 +0000 (13:28 +0000)
Signed-off-by: Gleb Smirnov <me@gvsmirnov.ru>
CONFIGURATION.md
config/config.go
config/config_test.go
config/testdata/blackbox-good.yml
config/testdata/invalid-http-header-match.yml [new file with mode: 0644]
example.yml
prober/http.go
prober/http_test.go

index c261c75ea13f303d41f267a73a34ccc97b2276eb..7610bd4cf295c1dd05bdb5b46f2203d56a309218 100644 (file)
@@ -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
index afb7ecf8c08082b9acb5ba31ba66fe5863c3f7a8..3652f86f8f0da65e1112f118b1778958e68b2040 100644 (file)
@@ -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
+}
index 94b0248bfd8b8fad48021a2a3b38dfc66a9a9b5d..d7c071934be209b2d23af0e3fea58918105d2f25 100644 (file)
@@ -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)
index d0a1f0cc83633d5f07b57bcfbd0994d2f565953c..8615d3c65ea4d6436b67cdb12552553b463e9788 100644 (file)
@@ -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 (file)
index 0000000..ae56c7d
--- /dev/null
@@ -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
index 39786fb168bf85f6a4b744c979d25888551a865a..6c720ff6b457074f8d2512f87810dbf0c67bc475 100644 (file)
@@ -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"
index 9aa61045350d5610cfeec5f76789e4b6989c2cd8..1c5ed236687ab880bf68f6a7d197b7113ceb4a16 100644 (file)
@@ -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)
index a7deaa89433ac46756b97c844a1498fbe73248cc..b45c96b20348095a3131cf85b4c6e149be78d3ac 100644 (file)
@@ -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",