223 lines
4.9 KiB
Go
223 lines
4.9 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
|
|
const (
|
|
TypeHTTP = "http"
|
|
TypeTCP = "tcp"
|
|
TypeICMP = "icmp"
|
|
TypeTLS = "tls"
|
|
)
|
|
|
|
|
|
type Spec struct {
|
|
Type string
|
|
URL string
|
|
Host string
|
|
Port int
|
|
Method string
|
|
ExpectedStatus int
|
|
Keyword string
|
|
TLSWarnDays int
|
|
Insecure bool
|
|
TimeoutSec int
|
|
}
|
|
|
|
|
|
type Result struct {
|
|
Up bool
|
|
LatencyMs int
|
|
Message string
|
|
CertExpiry *time.Time
|
|
}
|
|
|
|
func (s Spec) timeout() time.Duration {
|
|
t := s.TimeoutSec
|
|
if t <= 0 || t > 10 {
|
|
t = 10
|
|
}
|
|
return time.Duration(t) * time.Second
|
|
}
|
|
|
|
|
|
func Run(ctx context.Context, s Spec) Result {
|
|
switch s.Type {
|
|
case TypeHTTP:
|
|
return runHTTP(ctx, s)
|
|
case TypeTCP:
|
|
return runTCP(ctx, s)
|
|
case TypeICMP:
|
|
return runICMP(ctx, s)
|
|
case TypeTLS:
|
|
return runTLS(ctx, s)
|
|
default:
|
|
return Result{Message: "unknown check type: " + s.Type}
|
|
}
|
|
}
|
|
|
|
func runHTTP(ctx context.Context, s Spec) Result {
|
|
method := s.Method
|
|
if method == "" {
|
|
method = http.MethodGet
|
|
}
|
|
expect := s.ExpectedStatus
|
|
if expect == 0 {
|
|
expect = 200
|
|
}
|
|
client := &http.Client{Timeout: s.timeout()}
|
|
if s.Insecure {
|
|
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
|
}
|
|
start := time.Now()
|
|
req, err := http.NewRequestWithContext(ctx, method, s.URL, nil)
|
|
if err != nil {
|
|
return Result{Message: err.Error()}
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return Result{LatencyMs: msSince(start), Message: err.Error()}
|
|
}
|
|
defer resp.Body.Close()
|
|
res := Result{LatencyMs: msSince(start), Up: true}
|
|
if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
|
|
exp := resp.TLS.PeerCertificates[0].NotAfter
|
|
res.CertExpiry = &exp
|
|
}
|
|
if resp.StatusCode != expect {
|
|
return Result{LatencyMs: res.LatencyMs, CertExpiry: res.CertExpiry, Message: fmt.Sprintf("status %d (want %d)", resp.StatusCode, expect)}
|
|
}
|
|
if s.Keyword != "" {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
if !strings.Contains(string(body), s.Keyword) {
|
|
return Result{LatencyMs: res.LatencyMs, CertExpiry: res.CertExpiry, Message: "keyword not found"}
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func runTCP(ctx context.Context, s Spec) Result {
|
|
addr := net.JoinHostPort(s.Host, fmt.Sprint(s.Port))
|
|
start := time.Now()
|
|
d := net.Dialer{Timeout: s.timeout()}
|
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return Result{LatencyMs: msSince(start), Message: err.Error()}
|
|
}
|
|
conn.Close()
|
|
return Result{Up: true, LatencyMs: msSince(start)}
|
|
}
|
|
|
|
func runTLS(ctx context.Context, s Spec) Result {
|
|
port := s.Port
|
|
if port == 0 {
|
|
port = 443
|
|
}
|
|
addr := net.JoinHostPort(s.Host, fmt.Sprint(port))
|
|
start := time.Now()
|
|
d := net.Dialer{Timeout: s.timeout()}
|
|
conn, err := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: s.Host})
|
|
if err != nil {
|
|
return Result{LatencyMs: msSince(start), Message: err.Error()}
|
|
}
|
|
defer conn.Close()
|
|
certs := conn.ConnectionState().PeerCertificates
|
|
if len(certs) == 0 {
|
|
return Result{LatencyMs: msSince(start), Message: "no peer certificate"}
|
|
}
|
|
exp := certs[0].NotAfter
|
|
res := Result{LatencyMs: msSince(start), CertExpiry: &exp}
|
|
warn := s.TLSWarnDays
|
|
if warn <= 0 {
|
|
warn = 14
|
|
}
|
|
remaining := time.Until(exp)
|
|
if remaining <= 0 {
|
|
res.Message = "certificate expired"
|
|
return res
|
|
}
|
|
if remaining <= time.Duration(warn)*24*time.Hour {
|
|
res.Message = fmt.Sprintf("certificate expires in %d days", int(remaining.Hours()/24))
|
|
return res
|
|
}
|
|
res.Up = true
|
|
return res
|
|
}
|
|
|
|
func msSince(t time.Time) int { return int(time.Since(t).Milliseconds()) }
|
|
|
|
|
|
|
|
|
|
func runICMP(ctx context.Context, s Spec) Result {
|
|
dst, err := net.ResolveIPAddr("ip4", s.Host)
|
|
if err != nil {
|
|
return Result{Message: err.Error()}
|
|
}
|
|
conn, err := net.ListenPacket("ip4:icmp", "0.0.0.0")
|
|
if err != nil {
|
|
return Result{Message: "icmp socket: " + err.Error()}
|
|
}
|
|
defer conn.Close()
|
|
|
|
id := os.Getpid() & 0xffff
|
|
pkt := icmpEcho(id, 1)
|
|
deadline := time.Now().Add(s.timeout())
|
|
if d, ok := ctx.Deadline(); ok && d.Before(deadline) {
|
|
deadline = d
|
|
}
|
|
_ = conn.SetDeadline(deadline)
|
|
|
|
start := time.Now()
|
|
if _, err := conn.WriteTo(pkt, dst); err != nil {
|
|
return Result{Message: err.Error()}
|
|
}
|
|
reply := make([]byte, 1500)
|
|
for {
|
|
n, peer, err := conn.ReadFrom(reply)
|
|
if err != nil {
|
|
return Result{LatencyMs: msSince(start), Message: "no reply"}
|
|
}
|
|
|
|
if n < 28 || peer.String() != dst.String() {
|
|
continue
|
|
}
|
|
if reply[20] == 0 {
|
|
return Result{Up: true, LatencyMs: msSince(start)}
|
|
}
|
|
}
|
|
}
|
|
|
|
func icmpEcho(id, seq int) []byte {
|
|
|
|
b := []byte{8, 0, 0, 0, byte(id >> 8), byte(id), byte(seq >> 8), byte(seq)}
|
|
cs := icmpChecksum(b)
|
|
b[2] = byte(cs >> 8)
|
|
b[3] = byte(cs)
|
|
return b
|
|
}
|
|
|
|
func icmpChecksum(b []byte) uint16 {
|
|
var sum uint32
|
|
for i := 0; i < len(b)-1; i += 2 {
|
|
sum += uint32(b[i])<<8 | uint32(b[i+1])
|
|
}
|
|
if len(b)%2 == 1 {
|
|
sum += uint32(b[len(b)-1]) << 8
|
|
}
|
|
for sum>>16 != 0 {
|
|
sum = (sum & 0xffff) + (sum >> 16)
|
|
}
|
|
return ^uint16(sum)
|
|
}
|