Add SSH checker

pull/943/head
Luis Gustavo S. Barreto 2021-01-15 16:53:54 -03:00
parent b6ca1a5c66
commit fdeb7fe31a
3 changed files with 158 additions and 3 deletions

View File

@ -19,6 +19,7 @@
<option value="udp">UDP {{ $t('service') }}</option>
<option value="icmp">ICMP Ping</option>
<option value="grpc">gRPC {{ $t('service') }}</option>
<option value="ssh">SSH {{ $t('service') }}</option>
<option value="static">Static {{ $t('service') }}</option>
</select>
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
@ -82,7 +83,7 @@
</div>
</div>
<div v-if="service.type.match(/^(tcp|udp|grpc)$/)" class="form-group row">
<div v-if="service.type.match(/^(tcp|udp|grpc|ssh)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Port</label>
<div class="col-sm-8">
<input v-model.number="service.port" type="number" name="port" class="form-control" id="service_port" placeholder="8080">
@ -193,6 +194,55 @@
</div>
</div>
<div v-if="service.type.match(/^(ssh)$/)" class="form-group row">
<label for="service_username" class="col-sm-4 col-form-label">{{ $t('username') }}</label>
<div class="col-sm-8">
<input v-model="service.username" type="text" name="service_username" class="form-control" id="service_username">
</div>
</div>
<div v-if="service.type.match(/^(ssh)$/)" class="form-group row">
<label for="service_password" class="col-sm-4 col-form-label">{{ $t('password') }}</label>
<div class="col-sm-8">
<input v-model="service.password" type="text" name="service_password" class="form-control" id="service_password">
</div>
</div>
<div v-if="service.type.match(/^(ssh)$/)" class="form-group row">
<label class="col-12 col-md-4 col-form-label">SSH Health Check</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.ssh_health_check = !!service.ssh_health_check" class="switch float-left">
<input v-model="service.ssh_health_check" type="checkbox" name="ssh_health_check-option" class="switch" id="switch-ssh-health-check" v-bind:checked="service.ssh_health_check">
<label for="switch-ssh-health-check" v-if="service.ssh_health_check">Check against SSH health check command.</label>
<label for="switch-ssh-health-check" v-if="!service.ssh_health_check">Only checks if SSH connection can be established.</label>
</span>
</div>
</div>
<div v-if="service.ssh_health_check" class="form-group row">
<label class="col-sm-4 col-form-label">Expected Response</label>
<div class="col-sm-8">
<textarea v-model="service.expected" class="form-control" rows="3" autocapitalize="none" spellcheck="false"></textarea>
<small class="form-text text-muted">Only stdout will be parsed</small>
</div>
</div>
<div v-if="service.ssh_health_check" class="form-group row">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Exit Code</label>
<div class="col-sm-8">
<input v-model="service.expected_status" type="number" name="expected_status" class="form-control" placeholder="1" id="service_response_code">
<small class="form-text text-muted">By convention a command exit code 0 indicates success</small>
</div>
</div>
<div v-if="service.type.match(/^(ssh)$/) && service.ssh_health_check" class="form-group row">
<label for="check_command" class="col-sm-4 col-form-label">Check Command</label>
<div class="col-sm-8">
<input v-model="service.check_command" type="text" name="check_command" class="form-control" id="check_command">
<small class="form-text text-muted">Check command to be executed on remote server</small>
</div>
</div>
<div v-if="service.type.match(/^(tcp|http)$/)" class="form-group row">
<label class="col-12 col-md-4 col-form-label">{{ $t('tls_cert') }}</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
@ -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 = ""

View File

@ -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)
}
}

View File

@ -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"`