From fdeb7fe31a8d1d0c07b7f395854c2963a2686503 Mon Sep 17 00:00:00 2001 From: "Luis Gustavo S. Barreto" Date: Fri, 15 Jan 2021 16:53:54 -0300 Subject: [PATCH] Add SSH checker --- frontend/src/forms/Service.vue | 59 +++++++++++++++++++- types/services/routine.go | 98 +++++++++++++++++++++++++++++++++- types/services/struct.go | 4 ++ 3 files changed, 158 insertions(+), 3 deletions(-) diff --git a/frontend/src/forms/Service.vue b/frontend/src/forms/Service.vue index 32bb78a4..6306cdca 100644 --- a/frontend/src/forms/Service.vue +++ b/frontend/src/forms/Service.vue @@ -19,6 +19,7 @@ + Use HTTP if you are checking a website or use TCP if you are checking a server @@ -82,7 +83,7 @@ -
+
@@ -193,6 +194,55 @@
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + + + + +
+
+ +
+ +
+ + Only stdout will be parsed +
+
+ +
+ +
+ + By convention a command exit code 0 indicates success +
+
+ +
+ +
+ + Check command to be executed on remote server +
+
+
@@ -303,6 +353,10 @@ order: 1, verify_ssl: true, grpc_health_check: false, + ssh_health_check: false, + username: "", + password: "", + check_command: "", redirect: true, allow_notifications: true, notify_all_changes: true, @@ -351,6 +405,9 @@ this.service.port = 50051 this.service.verify_ssl = false this.service.method = "" + } else if (this.service.type == "ssh") { + this.service.port = 22; + this.service.expected_status = 0; } else { this.service.expected_status = 200 this.service.expected = "" diff --git a/types/services/routine.go b/types/services/routine.go index 50468945..ff298c94 100644 --- a/types/services/routine.go +++ b/types/services/routine.go @@ -14,6 +14,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/statping/statping/types/metrics" + "golang.org/x/crypto/ssh" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -58,7 +59,7 @@ CheckLoop: } func parseHost(s *Service) string { - if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" { + if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" || s.Type == "ssh" { return s.Domain } else { u, err := url.Parse(s.Domain) @@ -74,7 +75,7 @@ func dnsCheck(s *Service) (int64, error) { var err error t1 := utils.Now() host := parseHost(s) - if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" { + if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" || s.Type == "ssh" { _, err = net.LookupHost(host) } else { _, err = net.LookupIP(host) @@ -393,6 +394,97 @@ func CheckHttp(s *Service, record bool) (*Service, error) { return s, err } +// CheckSsh will check a SSH service +func CheckSsh(s *Service, record bool) (*Service, error) { + defer s.updateLastCheck() + timer := prometheus.NewTimer(metrics.ServiceTimer(s.Name)) + defer timer.ObserveDuration() + + dnsLookup, err := dnsCheck(s) + if err != nil { + if record { + RecordFailure(s, fmt.Sprintf("Could not get IP address for SSH service %v, %v", s.Domain, err), "lookup") + } + return s, err + } + s.PingTime = dnsLookup + t1 := utils.Now() + domain := fmt.Sprintf("%v", s.Domain) + if s.Port != 0 { + domain = fmt.Sprintf("%v:%v", s.Domain, s.Port) + if isIPv6(s.Domain) { + domain = fmt.Sprintf("[%v]:%v", s.Domain, s.Port) + } + } + + sshConfig := &ssh.ClientConfig{ + User: s.Username, + Auth: []ssh.AuthMethod{ssh.Password(s.Password)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: time.Duration(s.Timeout) * time.Second, + } + + client, err := ssh.Dial("tcp", domain, sshConfig) + if err != nil { + if record { + RecordFailure(s, fmt.Sprintf("Dial Error: %v", err), "ssh") + } + return s, err + } + + defer client.Close() + + session, err := client.NewSession() + if err != nil { + if record { + RecordFailure(s, fmt.Sprintf("Failed to create SSH session: %v", err), "ssh") + } + return s, err + } + + if s.SshHealthCheck.Bool { + output, err := session.Output(s.CheckCommand) + if err != nil { + if exitError, ok := err.(*ssh.ExitError); ok { + s.LastStatusCode = exitError.Waitmsg.ExitStatus() + } else { + if record { + RecordFailure(s, fmt.Sprintf("Failed to execute check command: %v", err), "ssh") + return s, err + } + } + } + + s.LastResponse = strings.TrimSpace(string(output)) + } + + s.Latency = utils.Now().Sub(t1).Microseconds() + s.Online = true + + if s.SshHealthCheck.Bool { + if s.ExpectedStatus != s.LastStatusCode { + if record { + RecordFailure(s, fmt.Sprintf("SSH Service: '%s', Exit Code: expected '%v', got '%v'", s.Name, s.ExpectedStatus, s.LastStatusCode), "response_code") + } + return s, nil + } + + if s.Expected.String != s.LastResponse { + log.Warnln(fmt.Sprintf("SSH Service: '%s', Command output: expected '%v', got '%v'", s.Name, s.Expected.String, s.LastResponse)) + if record { + RecordFailure(s, fmt.Sprintf("SSH Command output '%v' did not match '%v'", s.LastResponse, s.Expected.String), "response_body") + } + return s, nil + } + } + + if record { + RecordSuccess(s) + } + + return s, nil +} + // RecordSuccess will create a new 'hit' record in the database for a successful/online service func RecordSuccess(s *Service) { s.LastOnline = utils.Now() @@ -460,5 +552,7 @@ func (s *Service) CheckService(record bool) { CheckGrpc(s, record) case "icmp": CheckIcmp(s, record) + case "ssh": + CheckSsh(s, record) } } diff --git a/types/services/struct.go b/types/services/struct.go index ca55aaba..0594c019 100644 --- a/types/services/struct.go +++ b/types/services/struct.go @@ -26,6 +26,10 @@ type Service struct { Order int `gorm:"default:0;column:order_id" json:"order_id" yaml:"order_id"` VerifySSL null.NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin" yaml:"verify_ssl"` GrpcHealthCheck null.NullBool `gorm:"default:false;column:grpc_health_check" json:"grpc_health_check" scope:"user,admin" yaml:"grpc_health_check"` + Username string `gorm:"column:username" json:"username" yaml:"username" scope:"user,admin"` + Password string `gorm:"column:password" json:"password" yaml:"password" scope:"user,admin"` + CheckCommand string `gorm:"column:check_command" json:"check_command" yaml:"check_command" scope:"user,admin"` + SshHealthCheck null.NullBool `gorm:"default:false;column:ssh_health_check" json:"ssh_health_check" scope:"user,admin" yaml:"ssh_health_check"` Public null.NullBool `gorm:"default:true;column:public" json:"public" yaml:"public"` GroupId int `gorm:"default:0;column:group_id" json:"group_id" yaml:"group_id"` TLSCert null.NullString `gorm:"column:tls_cert" json:"tls_cert" scope:"user,admin" yaml:"tls_cert"`