package nodeprocessor import ( "context" "crypto/tls" "crypto/x509" "fmt" "math" "net/http" "strconv" "strings" "time" "github.com/usual2970/certimate/internal/domain" httputil "github.com/usual2970/certimate/internal/pkg/utils/http" ) type monitorNode struct { node *domain.WorkflowNode *nodeProcessor *nodeOutputer } func NewMonitorNode(node *domain.WorkflowNode) *monitorNode { return &monitorNode{ node: node, nodeProcessor: newNodeProcessor(node), nodeOutputer: newNodeOutputer(), } } func (n *monitorNode) Process(ctx context.Context) error { n.logger.Info("ready to monitor certificate ...") nodeCfg := n.node.GetConfigForMonitor() targetAddr := fmt.Sprintf("%s:%d", nodeCfg.Host, nodeCfg.Port) if nodeCfg.Port == 0 { targetAddr = fmt.Sprintf("%s:443", nodeCfg.Host) } targetDomain := nodeCfg.Domain if targetDomain == "" { targetDomain = nodeCfg.Host } n.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain)) const MAX_ATTEMPTS = 3 const RETRY_INTERVAL = 2 * time.Second var certs []*x509.Certificate var err error for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ { if attempt > 0 { n.logger.Info(fmt.Sprintf("retry %d time(s) ...", attempt, targetAddr)) select { case <-ctx.Done(): return ctx.Err() case <-time.After(RETRY_INTERVAL): } } certs, err = n.tryRetrievePeerCertificates(ctx, targetAddr, targetDomain, nodeCfg.RequestPath) if err == nil { break } } if err != nil { n.logger.Warn("failed to monitor certificate") return err } else { if len(certs) == 0 { n.logger.Warn("no ssl certificates retrieved in http response") n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(false) n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(0, 10) } else { cert := certs[0] // 只取证书链中的第一个证书,即服务器证书 n.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')", cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(), cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339), strings.Join(cert.DNSNames, ";")), ) now := time.Now() isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore) isCertHostMatched := true if err := cert.VerifyHostname(targetDomain); err != nil { isCertHostMatched = false } validated := isCertPeriodValid && isCertHostMatched daysLeft := int(math.Floor(cert.NotAfter.Sub(now).Hours() / 24)) n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(validated) n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) if validated { n.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft)) } else { n.logger.Warn(fmt.Sprintf("the certificate is invalid", validated)) } } } n.logger.Info("monitoring completed") return nil } func (n *monitorNode) tryRetrievePeerCertificates(ctx context.Context, addr, domain, requestPath string) ([]*x509.Certificate, error) { transport := httputil.NewDefaultTransport() if transport.TLSClientConfig == nil { transport.TLSClientConfig = &tls.Config{} } transport.TLSClientConfig.InsecureSkipVerify = true client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: 30 * time.Second, Transport: transport, } url := fmt.Sprintf("https://%s/%s", addr, strings.TrimLeft(requestPath, "/")) req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) if err != nil { err = fmt.Errorf("failed to create http request: %w", err) n.logger.Warn(err.Error()) return nil, err } req.Header.Set("User-Agent", "certimate") resp, err := client.Do(req) if err != nil { err = fmt.Errorf("failed to send http request: %w", err) n.logger.Warn(err.Error()) return nil, err } defer resp.Body.Close() if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { return make([]*x509.Certificate, 0), nil } return resp.TLS.PeerCertificates, nil }