Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add regexp matching of HTTP response headers to the http probe #419

Merged
merged 11 commits into from
Feb 21, 2019
Merged
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>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is optional

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it optional? It is not possible to match a header without a regex to match against.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless you allow it to be missing

Copy link
Contributor Author

@gvsmirnov gvsmirnov Feb 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if you allow it to be missing, then what is the point of having a match rule at all?

fail_if_header_matches:
  - header: Transfer-Encoding
    allow_missing: true
fail_if_header_not_matches:
  - header: Transfer-Encoding
    allow_missing: true

Neither of these rules make sense to me. But looking at the checks made in config.go, apparently they used to. So I think I should change the config check, so it always required regexp to be set, regardless of allow_missing.

Would that be OK?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a stroke of speculative execution, did just that.

[ 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