Skip to content

Commit

Permalink
Add regexp matching of HTTP response headers to the http probe (#419)
Browse files Browse the repository at this point in the history
Signed-off-by: Gleb Smirnov <[email protected]>
  • Loading branch information
gvsmirnov authored and brian-brazil committed Feb 21, 2019
1 parent d47e1ee commit c453ee3
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 33 deletions.
26 changes: 21 additions & 5 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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> ]
Expand All @@ -86,14 +94,22 @@ 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> ]


```

#### <http_header_match_spec>

```yml
header: <string>,
regexp: <regex>,
[ allow_missing: <boolean> | default = false ]
```
### <tcp_probe>
```yml
Expand Down
52 changes: 39 additions & 13 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions config/testdata/blackbox-good.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions config/testdata/invalid-http-header-match.yml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 10 additions & 2 deletions example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
78 changes: 75 additions & 3 deletions prober/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/http"
"net/http/cookiejar"
"net/http/httptrace"
"net/textproto"
"net/url"
"regexp"
"strconv"
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit c453ee3

Please sign in to comment.