diff --git a/README.md b/README.md index 3301b620..4a59b384 100644 --- a/README.md +++ b/README.md @@ -1356,8 +1356,8 @@ endpoints: **NOTE**: The usage of the `[DOMAIN_EXPIRATION]` placeholder requires Gatus to send a request to the official IANA WHOIS service [through a library](https://github.com/TwiN/whois) and in some cases, a secondary request to a TLD-specific WHOIS server (e.g. `whois.nic.sh`). -You are also responsible for sending requests at a reasonable rate, as the WHOIS service may throttle your IP address if you send too many requests. -The duration taken by the WHOIS request(s) is excluded from the request's response time. +To prevent the WHOIS service from throttling your IP address if you send too many requests, Gatus will prevent you from +using the `[DOMAIN_EXPIRATION]` placeholder on an endpoint with an interval of less than `5m`. ### disable-monitoring-lock diff --git a/core/endpoint.go b/core/endpoint.go index 1d7c90e2..8dfa74d4 100644 --- a/core/endpoint.go +++ b/core/endpoint.go @@ -58,6 +58,12 @@ var ( // ErrUnknownEndpointType is the error with which Gatus will panic if an endpoint has an unknown type ErrUnknownEndpointType = errors.New("unknown endpoint type") + + // ErrInvalidEndpointIntervalForDomainExpirationPlaceholder is the error with which Gatus will panic if an endpoint + // has both an interval smaller than 5 minutes and a condition with DomainExpirationPlaceholder. + // This is because the free whois service we are using should not be abused, especially considering the fact that + // the data takes a while to be updated. + ErrInvalidEndpointIntervalForDomainExpirationPlaceholder = errors.New("the minimum interval for an endpoint with a condition using the " + DomainExpirationPlaceholder + " placeholder is 300s (5m)") ) // Endpoint is the configuration of a monitored @@ -191,6 +197,13 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error { if len(endpoint.Conditions) == 0 { return ErrEndpointWithNoCondition } + if endpoint.Interval < 5*time.Minute { + for _, condition := range endpoint.Conditions { + if condition.hasDomainExpirationPlaceholder() { + return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder + } + } + } if endpoint.DNS != nil { return endpoint.DNS.validateAndSetDefault() } diff --git a/core/endpoint_test.go b/core/endpoint_test.go index b2be7f2a..ee03449c 100644 --- a/core/endpoint_test.go +++ b/core/endpoint_test.go @@ -372,11 +372,10 @@ func TestEndpoint_ValidateAndSetDefaults(t *testing.T) { } func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) { - condition := Condition("[STATUS] == 200") endpoint := Endpoint{ Name: "website-health", URL: "https://twin.sh/health", - Conditions: []Condition{condition}, + Conditions: []Condition{Condition("[STATUS] == 200")}, ClientConfig: &client.Config{ Insecure: true, IgnoreRedirect: true, @@ -399,51 +398,10 @@ func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) { } } -func TestEndpoint_ValidateAndSetDefaultsWithNoName(t *testing.T) { - defer func() { recover() }() - condition := Condition("[STATUS] == 200") - endpoint := &Endpoint{ - Name: "", - URL: "http://example.com", - Conditions: []Condition{condition}, - } - err := endpoint.ValidateAndSetDefaults() - if err == nil { - t.Fatal("Should've returned an error because endpoint didn't have a name, which is a mandatory field") - } -} - -func TestEndpoint_ValidateAndSetDefaultsWithNoUrl(t *testing.T) { - defer func() { recover() }() - condition := Condition("[STATUS] == 200") - endpoint := &Endpoint{ - Name: "example", - URL: "", - Conditions: []Condition{condition}, - } - err := endpoint.ValidateAndSetDefaults() - if err == nil { - t.Fatal("Should've returned an error because endpoint didn't have an url, which is a mandatory field") - } -} - -func TestEndpoint_ValidateAndSetDefaultsWithNoConditions(t *testing.T) { - defer func() { recover() }() - endpoint := &Endpoint{ - Name: "example", - URL: "http://example.com", - Conditions: nil, - } - err := endpoint.ValidateAndSetDefaults() - if err == nil { - t.Fatal("Should've returned an error because endpoint didn't have at least 1 condition") - } -} - func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) { endpoint := &Endpoint{ Name: "dns-test", - URL: "http://example.com", + URL: "https://example.com", DNS: &DNS{ QueryType: "A", QueryName: "example.com", @@ -452,13 +410,70 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) { } err := endpoint.ValidateAndSetDefaults() if err != nil { - + t.Error("did not expect an error, got", err) } if endpoint.DNS.QueryName != "example.com." { t.Error("Endpoint.dns.query-name should be formatted with . suffix") } } +func TestEndpoint_ValidateAndSetDefaultsWithSimpleErrors(t *testing.T) { + scenarios := []struct { + endpoint *Endpoint + expectedErr error + }{ + { + endpoint: &Endpoint{ + Name: "", + URL: "https://example.com", + Conditions: []Condition{Condition("[STATUS] == 200")}, + }, + expectedErr: ErrEndpointWithNoName, + }, + { + endpoint: &Endpoint{ + Name: "endpoint-with-no-url", + URL: "", + Conditions: []Condition{Condition("[STATUS] == 200")}, + }, + expectedErr: ErrEndpointWithNoURL, + }, + { + endpoint: &Endpoint{ + Name: "endpoint-with-no-conditions", + URL: "https://example.com", + Conditions: nil, + }, + expectedErr: ErrEndpointWithNoCondition, + }, + { + endpoint: &Endpoint{ + Name: "domain-expiration-with-bad-interval", + URL: "https://example.com", + Interval: time.Minute, + Conditions: []Condition{Condition("[DOMAIN_EXPIRATION] > 720h")}, + }, + expectedErr: ErrInvalidEndpointIntervalForDomainExpirationPlaceholder, + }, + { + endpoint: &Endpoint{ + Name: "domain-expiration-with-good-interval", + URL: "https://example.com", + Interval: 5 * time.Minute, + Conditions: []Condition{Condition("[DOMAIN_EXPIRATION] > 720h")}, + }, + expectedErr: nil, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.endpoint.Name, func(t *testing.T) { + if err := scenario.endpoint.ValidateAndSetDefaults(); err != scenario.expectedErr { + t.Errorf("Expected error %v, got %v", scenario.expectedErr, err) + } + }) + } +} + func TestEndpoint_buildHTTPRequest(t *testing.T) { condition := Condition("[STATUS] == 200") endpoint := Endpoint{