Skip to content

Commit

Permalink
Merge branch 'master' into dependabot/go_modules/golang.org/x/net-0.15.0
Browse files Browse the repository at this point in the history
  • Loading branch information
TwiN authored Sep 23, 2023
2 parents 1bbd307 + 15c81f9 commit 81362ad
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 8 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/publish-latest-to-ghcr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ on:
branches: [master]
types: [completed]
concurrency:
group: ${{ github.workflow }}
group: ${{ github.event.workflow_run.head_repository.full_name }}::${{ github.event.workflow_run.head_branch }} - ${{ github.workflow }}
cancel-in-progress: true
jobs:
publish-latest-to-ghcr:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.pull_request.head.repo.full_name == github.repository) }}
permissions:
contents: read
packages: write
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ on:
branches: [master]
types: [completed]
concurrency:
group: ${{ github.workflow }}
group: ${{ github.event.workflow_run.head_repository.full_name }}::${{ github.event.workflow_run.head_branch }} - ${{ github.workflow }}
cancel-in-progress: true
jobs:
publish-latest:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.pull_request.head.repo.full_name == github.repository) }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Monitoring a WebSocket endpoint](#monitoring-a-websocket-endpoint)
- [Monitoring an endpoint using ICMP](#monitoring-an-endpoint-using-icmp)
- [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries)
- [Monitoring an endpoint using SSH](#monitoring-an-endpoint-using-ssh)
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
- [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls)
- [Monitoring domain expiration](#monitoring-domain-expiration)
Expand Down Expand Up @@ -214,6 +215,9 @@ If you want to test it locally, see [Docker](#docker).
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
| `endpoints[].dns.query-type` | Query type (e.g. MX) | `""` |
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
| `endpoints[].ssh` | Configuration for an endpoint of type SSH. <br />See [Monitoring an endpoint using SSH](#monitoring-an-endpoint-using-ssh). | `""` |
| `endpoints[].ssh.username` | SSH username (e.g. example) | Required `""` |
| `endpoints[].ssh.password` | SSH password (e.g. password) | Required `""` |
| `endpoints[].alerts[].type` | Type of alert. <br />See [Alerting](#alerting) for all valid types. | Required `""` |
| `endpoints[].alerts[].enabled` | Whether to enable the alert. | `true` |
| `endpoints[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
Expand Down Expand Up @@ -1585,6 +1589,28 @@ There are two placeholders that can be used in the conditions for endpoints of t
- The placeholder `[DNS_RCODE]` resolves to the name associated to the response code returned by the query, such as
`NOERROR`, `FORMERR`, `SERVFAIL`, `NXDOMAIN`, etc.

### Monitoring an endpoint using SSH
You can monitor endpoints using SSH by prefixing `endpoints[].url` with `ssh:\\`:
```yaml
endpoints:
- name: ssh-example
url: "ssh://example.com:22" # port is optional. Default is 22.
ssh:
username: "username"
password: "password"
body: |
{
"command": "uptime"
}
interval: 1m
conditions:
- "[CONNECTED] == true"
- "[STATUS] == 0"
```

The following placeholders are supported for endpoints of type SSH:
- `[CONNECTED]` resolves to `true` if the SSH connection was successful, `false` otherwise
- `[STATUS]` resolves the exit code of the command executed on the remote server (e.g. `0` for success)

### Monitoring an endpoint using STARTTLS
If you have an email server that you want to ensure there are no problems with, monitoring it through STARTTLS
Expand All @@ -1601,7 +1627,6 @@ endpoints:
- "[CERTIFICATE_EXPIRATION] > 48h"
```


### Monitoring an endpoint using TLS
Monitoring endpoints using SSL/TLS encryption, such as LDAP over TLS, can help detect certificate expiration:
```yaml
Expand Down
5 changes: 3 additions & 2 deletions alerting/provider/email/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,

// buildMessageSubjectAndBody builds the message subject and body
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
var message, results string
subject := fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
var subject, message, results string
if resolved {
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
} else {
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
}
for _, conditionResult := range result.ConditionResults {
Expand Down
2 changes: 1 addition & 1 deletion alerting/provider/email/email_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedSubject: "[endpoint-name] Alert triggered",
ExpectedSubject: "[endpoint-name] Alert resolved",
ExpectedBody: "An alert for endpoint-name has been resolved after passing successfully 5 time(s) in a row\n\nAlert description: description-2\n\nCondition results:\n✅ [CONNECTED] == true\n✅ [STATUS] == 200\n",
},
}
Expand Down
70 changes: 70 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"golang.org/x/net/websocket"
Expand All @@ -17,6 +18,7 @@ import (
"github.com/TwiN/whois"
"github.com/ishidawataru/sctp"
ping "github.com/prometheus-community/pro-bing"
"golang.org/x/crypto/ssh"
)

var (
Expand Down Expand Up @@ -161,6 +163,74 @@ func CanPerformTLS(address string, config *Config) (connected bool, certificate
return true, verifiedChains[0][0], nil
}

// CanCreateSSHConnection checks whether a connection can be established and a command can be executed to an address
// using the SSH protocol.
func CanCreateSSHConnection(address, username, password string, config *Config) (bool, *ssh.Client, error) {
var port string
if strings.Contains(address, ":") {
addressAndPort := strings.Split(address, ":")
if len(addressAndPort) != 2 {
return false, nil, errors.New("invalid address for ssh, format must be host:port")
}
address = addressAndPort[0]
port = addressAndPort[1]
} else {
port = "22"
}

cli, err := ssh.Dial("tcp", strings.Join([]string{address, port}, ":"), &ssh.ClientConfig{
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
Timeout: config.Timeout,
})
if err != nil {
return false, nil, err
}

return true, cli, nil
}

// ExecuteSSHCommand executes a command to an address using the SSH protocol.
func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool, int, error) {
type Body struct {
Command string `json:"command"`
}

defer sshClient.Close()

var b Body
if err := json.Unmarshal([]byte(body), &b); err != nil {
return false, 0, err
}

sess, err := sshClient.NewSession()
if err != nil {
return false, 0, err
}

err = sess.Start(b.Command)
if err != nil {
return false, 0, err
}

defer sess.Close()

err = sess.Wait()
if err == nil {
return true, 0, nil
}

e, ok := err.(*ssh.ExitError)
if !ok {
return false, 0, err
}

return true, e.ExitStatus(), nil
}

// Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged
//
// Note that this function takes at least 100ms, even if the address is 127.0.0.1
Expand Down
13 changes: 13 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
endpoints:
- name: ssh
group: core
url: "ssh://example.org"
ssh:
username: "example"
password: "example"
body: |
{
"command": "uptime"
}
interval: 1m
conditions:
- "[STATUS] == 0"
- name: front-end
group: core
url: "https://twin.sh/health"
Expand Down
45 changes: 45 additions & 0 deletions core/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core/ui"
"github.com/TwiN/gatus/v5/util"
"golang.org/x/crypto/ssh"
)

type EndpointType string
Expand All @@ -43,6 +44,7 @@ const (
EndpointTypeTLS EndpointType = "TLS"
EndpointTypeHTTP EndpointType = "HTTP"
EndpointTypeWS EndpointType = "WEBSOCKET"
EndpointTypeSSH EndpointType = "SSH"
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
)

Expand Down Expand Up @@ -70,6 +72,10 @@ var (
// 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)")
// ErrEndpointWithoutSSHUsername is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a user.
ErrEndpointWithoutSSHUsername = errors.New("you must specify a username for each endpoint with SSH")
// ErrEndpointWithoutSSHPassword is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a password.
ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each endpoint with SSH")
)

// Endpoint is the configuration of a monitored
Expand Down Expand Up @@ -121,6 +127,27 @@ type Endpoint struct {

// NumberOfSuccessesInARow is the number of successful evaluations in a row
NumberOfSuccessesInARow int `yaml:"-"`

// SSH is the configuration of SSH monitoring.
SSH *SSH `yaml:"ssh,omitempty"`
}

type SSH struct {
// Username is the username to use when connecting to the SSH server.
Username string `yaml:"username,omitempty"`
// Password is the password to use when connecting to the SSH server.
Password string `yaml:"password,omitempty"`
}

// Validate validates the endpoint
func (s *SSH) ValidateAndSetDefaults() error {
if s.Username == "" {
return ErrEndpointWithoutSSHUsername
}
if s.Password == "" {
return ErrEndpointWithoutSSHPassword
}
return nil
}

// IsEnabled returns whether the endpoint is enabled or not
Expand Down Expand Up @@ -152,6 +179,8 @@ func (endpoint Endpoint) Type() EndpointType {
return EndpointTypeHTTP
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
return EndpointTypeWS
case strings.HasPrefix(endpoint.URL, "ssh://"):
return EndpointTypeSSH
default:
return EndpointTypeUNKNOWN
}
Expand Down Expand Up @@ -228,6 +257,9 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
if err != nil {
return err
}
if endpoint.SSH != nil {
return endpoint.SSH.ValidateAndSetDefaults()
}
return nil
}

Expand Down Expand Up @@ -350,6 +382,19 @@ func (endpoint *Endpoint) call(result *Result) {
result.AddError(err.Error())
return
}
} else if endpointType == EndpointTypeSSH {
var cli *ssh.Client
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(endpoint.URL, "ssh://"), endpoint.SSH.Username, endpoint.SSH.Password, endpoint.ClientConfig)
if err != nil {
result.AddError(err.Error())
return
}
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, endpoint.Body, endpoint.ClientConfig)
if err != nil {
result.AddError(err.Error())
return
}
result.Duration = time.Since(startTime)
} else {
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
result.Duration = time.Since(startTime)
Expand Down
Loading

0 comments on commit 81362ad

Please sign in to comment.