Add smtp and imap service types

pull/1118/head
Assis Ngolo 2023-07-10 02:21:35 +01:00
parent 4b450537ab
commit 63b3f1cf2a
7 changed files with 288 additions and 16 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea .idea
.vscode
snap snap
prime prime
stage stage

View File

@ -40,6 +40,9 @@ up:
docker compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans docker compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans
make print_details make print_details
local: clean frontend-build build
export STATPING_DIR=./docker/statping/sqlite && ./statping --port 8080
down: down:
docker compose -f docker-compose.yml -f dev/docker-compose.full.yml down --volumes --remove-orphans docker compose -f docker-compose.yml -f dev/docker-compose.full.yml down --volumes --remove-orphans

View File

@ -19,6 +19,8 @@
<option value="udp">UDP {{ $t('service') }}</option> <option value="udp">UDP {{ $t('service') }}</option>
<option value="icmp">ICMP Ping</option> <option value="icmp">ICMP Ping</option>
<option value="grpc">gRPC {{ $t('service') }}</option> <option value="grpc">gRPC {{ $t('service') }}</option>
<option value="smtp">SMTP {{ $t('service') }}</option>
<option value="imap">IMAP {{ $t('service') }}</option>
<option value="static">Static {{ $t('service') }}</option> <option value="static">Static {{ $t('service') }}</option>
</select> </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> <small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
@ -88,6 +90,12 @@
<input v-model.number="service.port" type="number" name="port" class="form-control" id="service_port" placeholder="8080"> <input v-model.number="service.port" type="number" name="port" class="form-control" id="service_port" placeholder="8080">
</div> </div>
</div> </div>
<div v-if="service.type.match(/^(smtp|imap)$/)" 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="587">
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row"> <div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('service_check') }}</label> <label class="col-sm-4 col-form-label">{{ $t('service_check') }}</label>
@ -132,6 +140,13 @@
<small class="form-text text-muted">Comma delimited list of HTTP Headers (KEY=VALUE,KEY=VALUE)</small> <small class="form-text text-muted">Comma delimited list of HTTP Headers (KEY=VALUE,KEY=VALUE)</small>
</div> </div>
</div> </div>
<div v-if="service.type.match(/^(smtp|imap)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Credentials</label>
<div class="col-sm-8">
<input v-model="service.headers" class="form-control" autocapitalize="none" spellcheck="false" placeholder='Username=user@domain.com,Password=secretpassword'>
<small class="form-text text-muted">Comma delimited list of IMAP/SMTP credentials (Username=user@domain.com,Password=secretpassword)</small>
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row"> <div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('expected_resp') }} (Regex)</label> <label class="col-sm-4 col-form-label">{{ $t('expected_resp') }} (Regex)</label>
<div class="col-sm-8"> <div class="col-sm-8">
@ -156,7 +171,7 @@
</span> </span>
</div> </div>
</div> </div>
<div v-if="service.type.match(/^(http|grpc)$/)" class="form-group row"> <div v-if="service.type.match(/^(http|grpc|smtp|imap)$/)" class="form-group row">
<label class="col-12 col-md-4 col-form-label">{{ $t('verify_ssl') }}</label> <label class="col-12 col-md-4 col-form-label">{{ $t('verify_ssl') }}</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0"> <div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left"> <span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left">
@ -194,7 +209,7 @@
</div> </div>
</div> </div>
<div v-if="service.type.match(/^(tcp|http)$/)" class="form-group row"> <div v-if="service.type.match(/^(tcp|smtp|imap|http)$/)" class="form-group row">
<label class="col-12 col-md-4 col-form-label">{{ $t('tls_cert') }}</label> <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"> <div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="use_tls = !!use_tls" class="switch float-left"> <span @click="use_tls = !!use_tls" class="switch float-left">

2
go.mod
View File

@ -6,6 +6,8 @@ require (
github.com/GeertJohan/go.rice v1.0.3 github.com/GeertJohan/go.rice v1.0.3
github.com/aws/aws-sdk-go v1.30.20 github.com/aws/aws-sdk-go v1.30.20
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/emersion/go-imap v1.0.6
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/fatih/structs v1.1.0 github.com/fatih/structs v1.1.0
github.com/foomo/simplecert v1.7.5 github.com/foomo/simplecert v1.7.5
github.com/foomo/tlsconfig v0.0.0-20180418120404-b67861b076c9 github.com/foomo/tlsconfig v0.0.0-20180418120404-b67861b076c9

9
go.sum
View File

@ -159,6 +159,14 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/emersion/go-imap v1.0.6 h1:N9+o5laOGuntStBo+BOgfEB5evPsPD+K5+M0T2dctIc=
github.com/emersion/go-imap v1.0.6/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -392,6 +400,7 @@ github.com/liquidweb/liquidweb-go v1.6.1/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVL
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=

View File

@ -6,27 +6,29 @@ import (
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io/ioutil"
"sort"
"strconv"
"time"
"github.com/statping-ng/statping-ng/types" "github.com/statping-ng/statping-ng/types"
"github.com/statping-ng/statping-ng/types/errors" "github.com/statping-ng/statping-ng/types/errors"
"github.com/statping-ng/statping-ng/types/failures" "github.com/statping-ng/statping-ng/types/failures"
"github.com/statping-ng/statping-ng/types/hits" "github.com/statping-ng/statping-ng/types/hits"
"github.com/statping-ng/statping-ng/utils" "github.com/statping-ng/statping-ng/utils"
"io/ioutil"
"sort"
"strconv"
"time"
) )
const limitedFailures = 25 const limitedFailures = 25
func (s *Service) LoadTLSCert() (*tls.Config, error) { func (s *Service) LoadTLSCert() (config *tls.Config, err error) {
config, err = s.configureTLS()
if s.TLSCert.String == "" || s.TLSCertKey.String == "" { if s.TLSCert.String == "" || s.TLSCertKey.String == "" {
return nil, nil return
} }
// load TLS cert and key from file path or PEM format // load TLS cert and key from file path or PEM format
var cert tls.Certificate var cert tls.Certificate
var err error
tlsCertExtension := utils.FileExtension(s.TLSCert.String) tlsCertExtension := utils.FileExtension(s.TLSCert.String)
tlsCertKeyExtension := utils.FileExtension(s.TLSCertKey.String) tlsCertKeyExtension := utils.FileExtension(s.TLSCertKey.String)
if tlsCertExtension == "" && tlsCertKeyExtension == "" { if tlsCertExtension == "" && tlsCertKeyExtension == "" {
@ -38,13 +40,14 @@ func (s *Service) LoadTLSCert() (*tls.Config, error) {
return nil, errors.Wrap(err, "issue loading X509KeyPair") return nil, errors.Wrap(err, "issue loading X509KeyPair")
} }
config := &tls.Config{ if config == nil {
Certificates: []tls.Certificate{cert}, config = &tls.Config{}
InsecureSkipVerify: s.TLSCertRoot.String == "",
} }
config.Certificates = []tls.Certificate{cert}
config.InsecureSkipVerify = config.InsecureSkipVerify || s.TLSCertRoot.String == ""
if s.TLSCertRoot.String == "" { if s.TLSCertRoot.String == "" {
return config, nil return
} }
// create Root CA pool or use Root CA provided // create Root CA pool or use Root CA provided
@ -58,7 +61,23 @@ func (s *Service) LoadTLSCert() (*tls.Config, error) {
config.RootCAs = caCertPool config.RootCAs = caCertPool
return config, nil return
}
func (s Service) configureTLS() (config *tls.Config, err error) {
if !s.requiresTLS() {
return nil, nil
}
config = &tls.Config{
ServerName: s.Domain,
InsecureSkipVerify: false,
}
return
}
func (s Service) requiresTLS() bool {
return s.VerifySSL.Bool || ((s.Type == "smtp" || s.Type == "imap") && (s.Port == 465 || s.Port == 587 || s.Port == 993))
} }
func (s Service) Duration() time.Duration { func (s Service) Duration() time.Duration {

View File

@ -4,9 +4,11 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/smtp"
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
@ -17,6 +19,7 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
"github.com/emersion/go-imap/client"
"github.com/statping-ng/statping-ng/types/failures" "github.com/statping-ng/statping-ng/types/failures"
"github.com/statping-ng/statping-ng/types/hits" "github.com/statping-ng/statping-ng/types/hits"
"github.com/statping-ng/statping-ng/utils" "github.com/statping-ng/statping-ng/utils"
@ -58,7 +61,7 @@ CheckLoop:
} }
func parseHost(s *Service) string { 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 == "smtp" || s.Type == "imap" {
return s.Domain return s.Domain
} else { } else {
u, err := url.Parse(s.Domain) u, err := url.Parse(s.Domain)
@ -74,7 +77,7 @@ func dnsCheck(s *Service) (int64, error) {
var err error var err error
t1 := utils.Now() t1 := utils.Now()
host := parseHost(s) 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 == "smtp" {
_, err = net.LookupHost(host) _, err = net.LookupHost(host)
} else { } else {
_, err = net.LookupIP(host) _, err = net.LookupIP(host)
@ -293,6 +296,222 @@ func CheckTcp(s *Service, record bool) (*Service, error) {
return s, nil return s, nil
} }
// checkSmtp will check an SMTP service
func CheckSmtp(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 %s service %v, %v", strings.ToUpper(s.Type), 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)
}
}
tlsConfig, err := s.LoadTLSCert()
if err != nil {
log.Errorln(err)
}
var c *smtp.Client
var headers []string
var username, password string
if s.Headers.Valid {
headers = strings.Split(s.Headers.String, ",")
} else {
headers = nil
}
// check if 'Content-Type' header was defined
for _, header := range headers {
switch strings.ToLower(strings.Split(header, "=")[0]) {
case "username":
username = strings.Split(header, "=")[1]
case "password":
password = strings.Split(header, "=")[1]
}
}
if s.requiresTLS() || s.TLSCert.String != "" {
// test TCP connection if TLS Certificate was set
dialer := &net.Dialer{
KeepAlive: time.Duration(s.Timeout) * time.Second,
Timeout: time.Duration(s.Timeout) * time.Second,
}
conn, err := tls.DialWithDialer(dialer, "tcp", domain, tlsConfig)
if err != nil {
if record {
RecordFailure(s, fmt.Sprintf("Dial Error: %v", err), "tls")
}
return s, err
}
defer conn.Close()
c, err = smtp.NewClient(conn, s.Domain)
if err != nil {
if record {
RecordFailure(s, fmt.Sprintf("%s Connection Error: %v", strings.ToUpper(s.Type), err), s.Type)
}
return s, err
}
} else {
// test TCP connection if there is no TLS Certificate set
conn, err := net.DialTimeout("tcp", domain, time.Duration(s.Timeout)*time.Second)
if err != nil {
if record {
RecordFailure(s, fmt.Sprintf("Dial Error: %v", err), "tls")
}
return s, err
}
defer conn.Close()
c, err = smtp.NewClient(conn, s.Domain)
if err != nil {
if record {
RecordFailure(s, fmt.Sprintf("%s Connection Error: %v", strings.ToUpper(s.Type), err), s.Type)
}
return s, err
}
}
// Auth
if s.Port != 25 {
if username == "" || password == "" {
err = errors.New("no credentials configured")
if record {
RecordFailure(s, fmt.Sprintf("%s Authentication Error: %v", strings.ToUpper(s.Type), err), s.Type)
}
return s, err
}
if err = c.Auth(smtp.PlainAuth("", username, password, s.Domain)); err != nil {
if record {
RecordFailure(s, fmt.Sprintf("%s Authentication Error: %v", strings.ToUpper(s.Type), err), s.Type)
}
return s, err
}
}
s.Latency = utils.Now().Sub(t1).Microseconds()
s.LastResponse = ""
s.Online = true
if record {
RecordSuccess(s)
}
return s, nil
}
func CheckImap(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 %s service %v, %v", strings.ToUpper(s.Type), 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)
}
}
tlsConfig, err := s.LoadTLSCert()
if err != nil {
log.Errorln(err)
}
var headers []string
var username, password string
if s.Headers.Valid {
headers = strings.Split(s.Headers.String, ",")
} else {
headers = nil
}
// check if 'Content-Type' header was defined
for _, header := range headers {
switch strings.ToLower(strings.Split(header, "=")[0]) {
case "username":
username = strings.Split(header, "=")[1]
case "password":
password = strings.Split(header, "=")[1]
}
}
var conn *client.Client
if s.requiresTLS() || s.TLSCert.String != "" {
// test TCP connection if TLS Certificate was set
dialer := &net.Dialer{
KeepAlive: time.Duration(s.Timeout) * time.Second,
Timeout: time.Duration(s.Timeout) * time.Second,
}
conn, err = client.DialWithDialerTLS(dialer, domain, tlsConfig)
if err != nil {
if record {
RecordFailure(s, fmt.Sprintf("Dial Error: %v", err), "tls")
}
return s, err
}
} else {
// test TCP connection if there is no TLS Certificate set
dialer := &net.Dialer{
KeepAlive: time.Duration(s.Timeout) * time.Second,
Timeout: time.Duration(s.Timeout) * time.Second,
}
conn, err = client.DialWithDialer(dialer, domain)
if err != nil {
if record {
RecordFailure(s, fmt.Sprintf("Dial Error: %v", err), "tls")
}
return s, err
}
}
defer conn.Logout()
// Auth
if s.Port != 143 {
if username == "" || password == "" {
err = errors.New("no credentials configured")
if record {
RecordFailure(s, fmt.Sprintf("%s Authentication Error: %v", strings.ToUpper(s.Type), err), s.Type)
}
return s, err
}
if err = conn.Login(username, password); err != nil {
if record {
RecordFailure(s, fmt.Sprintf("%s Authentication Error: %v", strings.ToUpper(s.Type), err), s.Type)
}
return s, err
}
}
s.Latency = utils.Now().Sub(t1).Microseconds()
s.LastResponse = ""
s.Online = true
if record {
RecordSuccess(s)
}
return s, nil
}
func (s *Service) updateLastCheck() { func (s *Service) updateLastCheck() {
s.LastCheck = time.Now() s.LastCheck = time.Now()
} }
@ -460,5 +679,9 @@ func (s *Service) CheckService(record bool) {
CheckGrpc(s, record) CheckGrpc(s, record)
case "icmp": case "icmp":
CheckIcmp(s, record) CheckIcmp(s, record)
case "smtp":
CheckSmtp(s, record)
case "imap":
CheckImap(s, record)
} }
} }