diff --git a/README.md b/README.md index fa4fa3ef2e..59fc7e385e 100644 --- a/README.md +++ b/README.md @@ -2058,6 +2058,23 @@ endpoints: - "[STATUS] == 0" ``` +you can also use no authentication to monitor the endpoint by not specifying the username +and password fields. + +```yaml +endpoints: + - name: ssh-example + url: "ssh://example.com:22" # port is optional. Default is 22. + ssh: + username: "" + password: "" + + 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) diff --git a/client/client.go b/client/client.go index 299e1e22cd..0cee1a10c5 100644 --- a/client/client.go +++ b/client/client.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "net/http" "net/smtp" @@ -197,6 +198,34 @@ func CanCreateSSHConnection(address, username, password string, config *Config) return true, cli, nil } +func CheckSSHBanner(address string, cfg *Config) (bool, int, error) { + var port string + if strings.Contains(address, ":") { + addressAndPort := strings.Split(address, ":") + if len(addressAndPort) != 2 { + return false, 1, errors.New("invalid address for ssh, format must be ssh://host:port") + } + address = addressAndPort[0] + port = addressAndPort[1] + } else { + port = "22" + } + dialer := net.Dialer{} + connStr := net.JoinHostPort(address, port) + conn, err := dialer.Dial("tcp", connStr) + if err != nil { + return false, 1, err + } + defer conn.Close() + conn.SetReadDeadline(time.Now().Add(time.Second)) + buf := make([]byte, 256) + _, err = io.ReadAtLeast(conn, buf, 1) + if err != nil { + return false, 1, err + } + return true, 0, err +} + // 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 { diff --git a/client/client_test.go b/client/client_test.go index 2f9b6dd4ec..46b08229d4 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -474,3 +474,38 @@ func TestQueryDNS(t *testing.T) { time.Sleep(10 * time.Millisecond) } } + +func TestCheckSSHBanner(t *testing.T) { + cfg := &Config{Timeout: 3} + + t.Run("no-auth-ssh", func(t *testing.T) { + connected, status, err := CheckSSHBanner("tty.sdf.org", cfg) + + if err != nil { + t.Errorf("Expected: error != nil, got: %v ", err) + } + + if connected == false { + t.Errorf("Expected: connected == true, got: %v", connected) + } + if status != 0 { + t.Errorf("Expected: 0, got: %v", status) + } + }) + + t.Run("invalid-address", func(t *testing.T) { + connected, status, err := CheckSSHBanner("idontplaytheodds.com", cfg) + + if err == nil { + t.Errorf("Expected: error, got: %v ", err) + } + + if connected != false { + t.Errorf("Expected: connected == false, got: %v", connected) + } + if status != 1 { + t.Errorf("Expected: 1, got: %v", status) + } + }) + +} diff --git a/config/endpoint/endpoint.go b/config/endpoint/endpoint.go index ac765c1a5b..65e45019dc 100644 --- a/config/endpoint/endpoint.go +++ b/config/endpoint/endpoint.go @@ -363,6 +363,18 @@ func (e *Endpoint) call(result *Result) { } result.Duration = time.Since(startTime) } else if endpointType == TypeSSH { + // If there's no username/password specified, attempt to validate just the SSH banner + if len(e.SSHConfig.Username) == 0 && len(e.SSHConfig.Password) == 0 { + result.Connected, result.HTTPStatus, err = + client.CheckSSHBanner(strings.TrimPrefix(e.URL, "ssh://"), e.ClientConfig) + if err != nil { + result.AddError(err.Error()) + return + } + result.Success = result.Connected + result.Duration = time.Since(startTime) + return + } var cli *ssh.Client result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(e.URL, "ssh://"), e.SSHConfig.Username, e.SSHConfig.Password, e.ClientConfig) if err != nil { diff --git a/config/endpoint/ssh/ssh.go b/config/endpoint/ssh/ssh.go index 8863647347..4759e1d3d5 100644 --- a/config/endpoint/ssh/ssh.go +++ b/config/endpoint/ssh/ssh.go @@ -19,6 +19,10 @@ type Config struct { // Validate the SSH configuration func (cfg *Config) Validate() error { + // If there's no username and password, this endpoint can still check the SSH banner, so the endpoint is still valid + if len(cfg.Username) == 0 && len(cfg.Password) == 0 { + return nil + } if len(cfg.Username) == 0 { return ErrEndpointWithoutSSHUsername } diff --git a/config/endpoint/ssh/ssh_test.go b/config/endpoint/ssh/ssh_test.go index ed563028fa..d26fca9069 100644 --- a/config/endpoint/ssh/ssh_test.go +++ b/config/endpoint/ssh/ssh_test.go @@ -7,10 +7,8 @@ import ( func TestSSH_validate(t *testing.T) { cfg := &Config{} - if err := cfg.Validate(); err == nil { - t.Error("expected an error") - } else if !errors.Is(err, ErrEndpointWithoutSSHUsername) { - t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHUsername, err) + if err := cfg.Validate(); err != nil { + t.Error("didn't expect an error") } cfg.Username = "username" if err := cfg.Validate(); err == nil {