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