2024-05-10 02:56:16 +00:00
package endpoint
2021-10-23 20:47:12 +00:00
import (
"bytes"
"crypto/x509"
"encoding/json"
"errors"
2022-12-06 06:37:05 +00:00
"fmt"
2021-12-03 06:44:17 +00:00
"io"
2021-10-23 20:47:12 +00:00
"net"
"net/http"
"net/url"
"strings"
"time"
2022-12-06 06:41:09 +00:00
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
2024-05-10 02:56:16 +00:00
"github.com/TwiN/gatus/v5/config/endpoint/dns"
sshconfig "github.com/TwiN/gatus/v5/config/endpoint/ssh"
"github.com/TwiN/gatus/v5/config/endpoint/ui"
2023-09-23 17:37:24 +00:00
"golang.org/x/crypto/ssh"
2021-10-23 20:47:12 +00:00
)
2024-05-10 02:56:16 +00:00
type Type string
2022-05-17 01:10:45 +00:00
2021-10-23 20:47:12 +00:00
const (
// HostHeader is the name of the header used to specify the host
HostHeader = "Host"
// ContentTypeHeader is the name of the header used to specify the content type
ContentTypeHeader = "Content-Type"
// UserAgentHeader is the name of the header used to specify the request's user agent
UserAgentHeader = "User-Agent"
// GatusUserAgent is the default user agent that Gatus uses to send requests.
GatusUserAgent = "Gatus/1.0"
2022-05-17 01:10:45 +00:00
2024-05-10 02:56:16 +00:00
TypeDNS Type = "DNS"
TypeTCP Type = "TCP"
TypeSCTP Type = "SCTP"
TypeUDP Type = "UDP"
TypeICMP Type = "ICMP"
TypeSTARTTLS Type = "STARTTLS"
TypeTLS Type = "TLS"
TypeHTTP Type = "HTTP"
TypeWS Type = "WEBSOCKET"
TypeSSH Type = "SSH"
TypeUNKNOWN Type = "UNKNOWN"
2021-10-23 20:47:12 +00:00
)
var (
// ErrEndpointWithNoCondition is the error with which Gatus will panic if an endpoint is configured with no conditions
ErrEndpointWithNoCondition = errors . New ( "you must specify at least one condition per endpoint" )
// ErrEndpointWithNoURL is the error with which Gatus will panic if an endpoint is configured with no url
ErrEndpointWithNoURL = errors . New ( "you must specify an url for each endpoint" )
2022-09-02 00:59:09 +00:00
// ErrUnknownEndpointType is the error with which Gatus will panic if an endpoint has an unknown type
ErrUnknownEndpointType = errors . New ( "unknown endpoint type" )
2022-09-16 01:23:14 +00:00
2022-12-06 06:37:05 +00:00
// ErrInvalidConditionFormat is the error with which Gatus will panic if a condition has an invalid format
ErrInvalidConditionFormat = errors . New ( "invalid condition format: does not match '<VALUE> <COMPARATOR> <VALUE>'" )
2022-09-16 01:23:14 +00:00
// 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)" )
2021-10-23 20:47:12 +00:00
)
2024-04-09 01:00:40 +00:00
// Endpoint is the configuration of a service to be monitored
2021-10-23 20:47:12 +00:00
type Endpoint struct {
// Enabled defines whether to enable the monitoring of the endpoint
Enabled * bool ` yaml:"enabled,omitempty" `
// Name of the endpoint. Can be anything.
Name string ` yaml:"name" `
// Group the endpoint is a part of. Used for grouping multiple endpoints together on the front end.
Group string ` yaml:"group,omitempty" `
// URL to send the request to
URL string ` yaml:"url" `
// Method of the request made to the url of the endpoint
Method string ` yaml:"method,omitempty" `
// Body of the request
Body string ` yaml:"body,omitempty" `
// GraphQL is whether to wrap the body in a query param ({"query":"$body"})
GraphQL bool ` yaml:"graphql,omitempty" `
// Headers of the request
Headers map [ string ] string ` yaml:"headers,omitempty" `
// Interval is the duration to wait between every status check
Interval time . Duration ` yaml:"interval,omitempty" `
// Conditions used to determine the health of the endpoint
2022-06-13 23:15:30 +00:00
Conditions [ ] Condition ` yaml:"conditions" `
2021-10-23 20:47:12 +00:00
// Alerts is the alerting configuration for the endpoint in case of failure
2021-10-24 19:03:41 +00:00
Alerts [ ] * alert . Alert ` yaml:"alerts,omitempty" `
2021-10-23 20:47:12 +00:00
2024-05-10 02:56:16 +00:00
// DNSConfig is the configuration for DNS monitoring
DNSConfig * dns . Config ` yaml:"dns,omitempty" `
// SSH is the configuration for SSH monitoring
SSHConfig * sshconfig . Config ` yaml:"ssh,omitempty" `
2021-10-23 20:47:12 +00:00
// ClientConfig is the configuration of the client used to communicate with the endpoint's target
2021-10-24 19:03:41 +00:00
ClientConfig * client . Config ` yaml:"client,omitempty" `
2021-10-23 20:47:12 +00:00
// UIConfig is the configuration for the UI
2021-10-24 19:03:41 +00:00
UIConfig * ui . Config ` yaml:"ui,omitempty" `
2021-10-23 20:47:12 +00:00
// NumberOfFailuresInARow is the number of unsuccessful evaluations in a row
2021-10-24 19:03:41 +00:00
NumberOfFailuresInARow int ` yaml:"-" `
2021-10-23 20:47:12 +00:00
// NumberOfSuccessesInARow is the number of successful evaluations in a row
2021-10-24 19:03:41 +00:00
NumberOfSuccessesInARow int ` yaml:"-" `
2021-10-23 20:47:12 +00:00
}
// IsEnabled returns whether the endpoint is enabled or not
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) IsEnabled ( ) bool {
if e . Enabled == nil {
2021-10-23 20:47:12 +00:00
return true
}
2024-05-10 02:56:16 +00:00
return * e . Enabled
2021-10-23 20:47:12 +00:00
}
2022-05-17 01:10:45 +00:00
// Type returns the endpoint type
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) Type ( ) Type {
2022-05-17 01:10:45 +00:00
switch {
2024-05-10 02:56:16 +00:00
case e . DNSConfig != nil :
return TypeDNS
case strings . HasPrefix ( e . URL , "tcp://" ) :
return TypeTCP
case strings . HasPrefix ( e . URL , "sctp://" ) :
return TypeSCTP
case strings . HasPrefix ( e . URL , "udp://" ) :
return TypeUDP
case strings . HasPrefix ( e . URL , "icmp://" ) :
return TypeICMP
case strings . HasPrefix ( e . URL , "starttls://" ) :
return TypeSTARTTLS
case strings . HasPrefix ( e . URL , "tls://" ) :
return TypeTLS
case strings . HasPrefix ( e . URL , "http://" ) || strings . HasPrefix ( e . URL , "https://" ) :
return TypeHTTP
case strings . HasPrefix ( e . URL , "ws://" ) || strings . HasPrefix ( e . URL , "wss://" ) :
return TypeWS
case strings . HasPrefix ( e . URL , "ssh://" ) :
return TypeSSH
2022-09-02 00:59:09 +00:00
default :
2024-05-10 02:56:16 +00:00
return TypeUNKNOWN
2022-05-17 01:10:45 +00:00
}
}
2022-09-07 01:22:02 +00:00
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) ValidateAndSetDefaults ( ) error {
if err := validateEndpointNameGroupAndAlerts ( e . Name , e . Group , e . Alerts ) ; err != nil {
2024-04-09 01:00:40 +00:00
return err
}
2024-05-10 02:56:16 +00:00
if len ( e . URL ) == 0 {
2024-04-09 01:00:40 +00:00
return ErrEndpointWithNoURL
}
2024-05-10 02:56:16 +00:00
if e . ClientConfig == nil {
e . ClientConfig = client . GetDefaultConfig ( )
2021-10-23 20:47:12 +00:00
} else {
2024-05-10 02:56:16 +00:00
if err := e . ClientConfig . ValidateAndSetDefaults ( ) ; err != nil {
2022-03-10 01:53:51 +00:00
return err
}
2021-10-23 20:47:12 +00:00
}
2024-05-10 02:56:16 +00:00
if e . UIConfig == nil {
e . UIConfig = ui . GetDefaultConfig ( )
2022-08-11 01:05:34 +00:00
} else {
2024-05-10 02:56:16 +00:00
if err := e . UIConfig . ValidateAndSetDefaults ( ) ; err != nil {
2022-08-11 01:05:34 +00:00
return err
}
2021-10-23 20:47:12 +00:00
}
2024-05-10 02:56:16 +00:00
if e . Interval == 0 {
e . Interval = 1 * time . Minute
2021-10-23 20:47:12 +00:00
}
2024-05-10 02:56:16 +00:00
if len ( e . Method ) == 0 {
e . Method = http . MethodGet
2021-10-23 20:47:12 +00:00
}
2024-05-10 02:56:16 +00:00
if len ( e . Headers ) == 0 {
e . Headers = make ( map [ string ] string )
2021-10-23 20:47:12 +00:00
}
// Automatically add user agent header if there isn't one specified in the endpoint configuration
2024-05-10 02:56:16 +00:00
if _ , userAgentHeaderExists := e . Headers [ UserAgentHeader ] ; ! userAgentHeaderExists {
e . Headers [ UserAgentHeader ] = GatusUserAgent
2021-10-23 20:47:12 +00:00
}
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
// and endpoint.GraphQL is set to true
2024-05-10 02:56:16 +00:00
if _ , contentTypeHeaderExists := e . Headers [ ContentTypeHeader ] ; ! contentTypeHeaderExists && e . GraphQL {
e . Headers [ ContentTypeHeader ] = "application/json"
2021-10-23 20:47:12 +00:00
}
2024-05-10 02:56:16 +00:00
if len ( e . Conditions ) == 0 {
2021-10-23 20:47:12 +00:00
return ErrEndpointWithNoCondition
}
2024-05-10 02:56:16 +00:00
for _ , c := range e . Conditions {
if e . Interval < 5 * time . Minute && c . hasDomainExpirationPlaceholder ( ) {
2022-12-06 06:37:05 +00:00
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
}
if err := c . Validate ( ) ; err != nil {
return fmt . Errorf ( "%v: %w" , ErrInvalidConditionFormat , err )
2022-09-16 01:23:14 +00:00
}
}
2024-05-10 02:56:16 +00:00
if e . DNSConfig != nil {
return e . DNSConfig . ValidateAndSetDefault ( )
2021-10-23 20:47:12 +00:00
}
2024-05-10 02:56:16 +00:00
if e . SSHConfig != nil {
return e . SSHConfig . Validate ( )
2024-04-09 01:00:40 +00:00
}
2024-05-10 02:56:16 +00:00
if e . Type ( ) == TypeUNKNOWN {
2022-09-02 00:59:09 +00:00
return ErrUnknownEndpointType
}
2021-10-23 20:47:12 +00:00
// Make sure that the request can be created
2024-05-10 02:56:16 +00:00
_ , err := http . NewRequest ( e . Method , e . URL , bytes . NewBuffer ( [ ] byte ( e . Body ) ) )
2021-10-23 20:47:12 +00:00
if err != nil {
return err
}
return nil
}
2021-12-12 21:28:24 +00:00
// DisplayName returns an identifier made up of the Name and, if not empty, the Group.
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) DisplayName ( ) string {
if len ( e . Group ) > 0 {
return e . Group + "/" + e . Name
2021-12-12 21:28:24 +00:00
}
2024-05-10 02:56:16 +00:00
return e . Name
2021-12-12 21:28:24 +00:00
}
2021-10-23 20:47:12 +00:00
// Key returns the unique key for the Endpoint
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) Key ( ) string {
return ConvertGroupAndEndpointNameToKey ( e . Group , e . Name )
2021-10-23 20:47:12 +00:00
}
2024-04-09 01:00:40 +00:00
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
// on configuration reload.
// More context on https://github.com/TwiN/gatus/issues/536
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) Close ( ) {
if e . Type ( ) == TypeHTTP {
client . GetHTTPClient ( e . ClientConfig ) . CloseIdleConnections ( )
2024-04-09 01:00:40 +00:00
}
}
2021-10-23 20:47:12 +00:00
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) EvaluateHealth ( ) * Result {
2021-10-23 20:47:12 +00:00
result := & Result { Success : true , Errors : [ ] string { } }
2022-09-07 01:22:02 +00:00
// Parse or extract hostname from URL
2024-05-10 02:56:16 +00:00
if e . DNSConfig != nil {
result . Hostname = strings . TrimSuffix ( e . URL , ":53" )
2022-09-07 01:22:02 +00:00
} else {
2024-05-10 02:56:16 +00:00
urlObject , err := url . Parse ( e . URL )
2022-09-07 01:22:02 +00:00
if err != nil {
result . AddError ( err . Error ( ) )
} else {
result . Hostname = urlObject . Hostname ( )
}
}
// Retrieve IP if necessary
2024-05-10 02:56:16 +00:00
if e . needsToRetrieveIP ( ) {
e . getIP ( result )
2022-09-07 01:22:02 +00:00
}
2022-11-16 02:35:22 +00:00
// Retrieve domain expiration if necessary
2024-05-10 02:56:16 +00:00
if e . needsToRetrieveDomainExpiration ( ) && len ( result . Hostname ) > 0 {
2022-11-16 02:35:22 +00:00
var err error
if result . DomainExpiration , err = client . GetDomainExpiration ( result . Hostname ) ; err != nil {
result . AddError ( err . Error ( ) )
}
}
// Call the endpoint (if there's no errors)
2021-10-23 20:47:12 +00:00
if len ( result . Errors ) == 0 {
2024-05-10 02:56:16 +00:00
e . call ( result )
2021-10-23 20:47:12 +00:00
} else {
result . Success = false
}
2022-11-16 02:35:22 +00:00
// Evaluate the conditions
2024-05-10 02:56:16 +00:00
for _ , condition := range e . Conditions {
success := condition . evaluate ( result , e . UIConfig . DontResolveFailedConditions )
2021-10-23 20:47:12 +00:00
if ! success {
result . Success = false
}
}
result . Timestamp = time . Now ( )
// Clean up parameters that we don't need to keep in the results
2024-05-10 02:56:16 +00:00
if e . UIConfig . HideURL {
2022-06-16 21:53:03 +00:00
for errIdx , errorString := range result . Errors {
2024-05-10 02:56:16 +00:00
result . Errors [ errIdx ] = strings . ReplaceAll ( errorString , e . URL , "<redacted>" )
2022-06-16 21:53:03 +00:00
}
}
2024-05-10 02:56:16 +00:00
if e . UIConfig . HideHostname {
2022-03-16 00:17:57 +00:00
for errIdx , errorString := range result . Errors {
2022-03-16 00:51:59 +00:00
result . Errors [ errIdx ] = strings . ReplaceAll ( errorString , result . Hostname , "<redacted>" )
2022-03-16 00:17:57 +00:00
}
2021-10-23 20:47:12 +00:00
result . Hostname = ""
}
2024-05-10 02:56:16 +00:00
if e . UIConfig . HideConditions {
2024-04-19 00:57:07 +00:00
result . ConditionResults = nil
}
2021-10-23 20:47:12 +00:00
return result
}
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) getIP ( result * Result ) {
2022-11-16 02:35:22 +00:00
if ips , err := net . LookupIP ( result . Hostname ) ; err != nil {
2021-10-23 20:47:12 +00:00
result . AddError ( err . Error ( ) )
return
2022-09-07 01:22:02 +00:00
} else {
2022-11-16 02:35:22 +00:00
result . IP = ips [ 0 ] . String ( )
2022-09-07 01:22:02 +00:00
}
}
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) call ( result * Result ) {
2021-10-23 20:47:12 +00:00
var request * http . Request
var response * http . Response
var err error
var certificate * x509 . Certificate
2024-05-10 02:56:16 +00:00
endpointType := e . Type ( )
if endpointType == TypeHTTP {
request = e . buildHTTPRequest ( )
2021-10-23 20:47:12 +00:00
}
startTime := time . Now ( )
2024-05-10 02:56:16 +00:00
if endpointType == TypeDNS {
result . Connected , result . DNSRCode , result . Body , err = client . QueryDNS ( e . DNSConfig . QueryType , e . DNSConfig . QueryName , e . URL )
if err != nil {
result . AddError ( err . Error ( ) )
return
}
2021-10-23 20:47:12 +00:00
result . Duration = time . Since ( startTime )
2024-05-10 02:56:16 +00:00
} else if endpointType == TypeSTARTTLS || endpointType == TypeTLS {
if endpointType == TypeSTARTTLS {
result . Connected , certificate , err = client . CanPerformStartTLS ( strings . TrimPrefix ( e . URL , "starttls://" ) , e . ClientConfig )
2021-10-23 20:47:12 +00:00
} else {
2024-05-10 02:56:16 +00:00
result . Connected , certificate , err = client . CanPerformTLS ( strings . TrimPrefix ( e . URL , "tls://" ) , e . ClientConfig )
2021-10-23 20:47:12 +00:00
}
if err != nil {
result . AddError ( err . Error ( ) )
return
}
result . Duration = time . Since ( startTime )
result . CertificateExpiration = time . Until ( certificate . NotAfter )
2024-05-10 02:56:16 +00:00
} else if endpointType == TypeTCP {
result . Connected = client . CanCreateTCPConnection ( strings . TrimPrefix ( e . URL , "tcp://" ) , e . ClientConfig )
2021-10-23 20:47:12 +00:00
result . Duration = time . Since ( startTime )
2024-05-10 02:56:16 +00:00
} else if endpointType == TypeUDP {
result . Connected = client . CanCreateUDPConnection ( strings . TrimPrefix ( e . URL , "udp://" ) , e . ClientConfig )
2022-11-10 00:22:13 +00:00
result . Duration = time . Since ( startTime )
2024-05-10 02:56:16 +00:00
} else if endpointType == TypeSCTP {
result . Connected = client . CanCreateSCTPConnection ( strings . TrimPrefix ( e . URL , "sctp://" ) , e . ClientConfig )
2022-11-10 00:22:13 +00:00
result . Duration = time . Since ( startTime )
2024-05-10 02:56:16 +00:00
} else if endpointType == TypeICMP {
result . Connected , result . Duration = client . Ping ( strings . TrimPrefix ( e . URL , "icmp://" ) , e . ClientConfig )
} else if endpointType == TypeWS {
result . Connected , result . Body , err = client . QueryWebSocket ( e . URL , e . Body , e . ClientConfig )
2023-08-17 01:48:57 +00:00
if err != nil {
result . AddError ( err . Error ( ) )
return
}
2023-09-28 22:35:18 +00:00
result . Duration = time . Since ( startTime )
2024-05-10 02:56:16 +00:00
} else if endpointType == TypeSSH {
2023-09-23 17:37:24 +00:00
var cli * ssh . Client
2024-05-10 02:56:16 +00:00
result . Connected , cli , err = client . CanCreateSSHConnection ( strings . TrimPrefix ( e . URL , "ssh://" ) , e . SSHConfig . Username , e . SSHConfig . Password , e . ClientConfig )
2023-09-23 17:37:24 +00:00
if err != nil {
result . AddError ( err . Error ( ) )
return
}
2024-05-10 02:56:16 +00:00
result . Success , result . HTTPStatus , err = client . ExecuteSSHCommand ( cli , e . Body , e . ClientConfig )
2023-09-23 17:37:24 +00:00
if err != nil {
result . AddError ( err . Error ( ) )
return
}
result . Duration = time . Since ( startTime )
2021-10-23 20:47:12 +00:00
} else {
2024-05-10 02:56:16 +00:00
response , err = client . GetHTTPClient ( e . ClientConfig ) . Do ( request )
2021-10-23 20:47:12 +00:00
result . Duration = time . Since ( startTime )
if err != nil {
result . AddError ( err . Error ( ) )
return
}
defer response . Body . Close ( )
if response . TLS != nil && len ( response . TLS . PeerCertificates ) > 0 {
certificate = response . TLS . PeerCertificates [ 0 ]
result . CertificateExpiration = time . Until ( certificate . NotAfter )
}
result . HTTPStatus = response . StatusCode
result . Connected = response . StatusCode > 0
2023-03-15 00:02:31 +00:00
// Only read the Body if there's a condition that uses the BodyPlaceholder
2024-05-10 02:56:16 +00:00
if e . needsToReadBody ( ) {
2023-03-15 00:02:31 +00:00
result . Body , err = io . ReadAll ( response . Body )
2021-10-23 20:47:12 +00:00
if err != nil {
2022-09-07 01:22:02 +00:00
result . AddError ( "error reading response body:" + err . Error ( ) )
2021-10-23 20:47:12 +00:00
}
}
}
}
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) buildHTTPRequest ( ) * http . Request {
2021-10-23 20:47:12 +00:00
var bodyBuffer * bytes . Buffer
2024-05-10 02:56:16 +00:00
if e . GraphQL {
2021-10-23 20:47:12 +00:00
graphQlBody := map [ string ] string {
2024-05-10 02:56:16 +00:00
"query" : e . Body ,
2021-10-23 20:47:12 +00:00
}
body , _ := json . Marshal ( graphQlBody )
bodyBuffer = bytes . NewBuffer ( body )
} else {
2024-05-10 02:56:16 +00:00
bodyBuffer = bytes . NewBuffer ( [ ] byte ( e . Body ) )
2021-10-23 20:47:12 +00:00
}
2024-05-10 02:56:16 +00:00
request , _ := http . NewRequest ( e . Method , e . URL , bodyBuffer )
for k , v := range e . Headers {
2021-10-23 20:47:12 +00:00
request . Header . Set ( k , v )
if k == HostHeader {
request . Host = v
}
}
return request
}
2023-03-15 00:02:31 +00:00
// needsToReadBody checks if there's any condition that requires the response Body to be read
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) needsToReadBody ( ) bool {
for _ , condition := range e . Conditions {
2021-10-23 20:47:12 +00:00
if condition . hasBodyPlaceholder ( ) {
return true
}
}
return false
}
2022-09-07 01:22:02 +00:00
// needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) needsToRetrieveDomainExpiration ( ) bool {
for _ , condition := range e . Conditions {
2022-09-07 01:22:02 +00:00
if condition . hasDomainExpirationPlaceholder ( ) {
return true
}
}
return false
}
// needsToRetrieveIP checks if there's any condition that requires an IP lookup
2024-05-10 02:56:16 +00:00
func ( e * Endpoint ) needsToRetrieveIP ( ) bool {
for _ , condition := range e . Conditions {
2022-09-07 01:22:02 +00:00
if condition . hasIPPlaceholder ( ) {
return true
}
}
return false
}