mirror of
https://github.com/certimate-go/certimate.git
synced 2026-06-22 21:05:48 +08:00
196 lines
6.7 KiB
Go
196 lines
6.7 KiB
Go
package engine
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/certimate-go/certimate/internal/repository"
|
|
xhttp "github.com/certimate-go/certimate/pkg/utils/http"
|
|
xtls "github.com/certimate-go/certimate/pkg/utils/tls"
|
|
)
|
|
|
|
/**
|
|
* Variables:
|
|
* - "certificate.domain": string
|
|
* - "certificate.domains": string
|
|
* - "certificate.notBefore": datetime
|
|
* - "certificate.notAfter": datetime
|
|
* - "certificate.hoursLeft": number
|
|
* - "certificate.daysLeft": number
|
|
* - "certificate.validity": boolean
|
|
*/
|
|
type bizMonitorNodeExecutor struct {
|
|
nodeExecutor
|
|
|
|
certificateRepo certificateRepository
|
|
}
|
|
|
|
func (ne *bizMonitorNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {
|
|
execRes := newNodeExecutionResult(execCtx.Node)
|
|
|
|
nodeCfg := execCtx.Node.Data.Config.AsBizMonitor()
|
|
ne.logger.Info("ready to monitor certificate ...", slog.Any("config", nodeCfg))
|
|
|
|
targetAddr := net.JoinHostPort(nodeCfg.Host, strconv.Itoa(int(nodeCfg.Port)))
|
|
if nodeCfg.Port == 0 {
|
|
targetAddr = net.JoinHostPort(nodeCfg.Host, "443")
|
|
}
|
|
|
|
targetDomain := nodeCfg.Domain
|
|
if targetDomain == "" {
|
|
targetDomain = nodeCfg.Host
|
|
}
|
|
|
|
ne.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain))
|
|
|
|
const MAX_ATTEMPTS = 3
|
|
const RETRY_INTERVAL = 2 * time.Second
|
|
var err error
|
|
var certs []*x509.Certificate
|
|
for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ {
|
|
if attempt > 0 {
|
|
ne.logger.Info(fmt.Sprintf("retry %d time(s) ...", attempt))
|
|
|
|
select {
|
|
case <-execCtx.ctx.Done():
|
|
return execRes, execCtx.ctx.Err()
|
|
case <-time.After(RETRY_INTERVAL):
|
|
}
|
|
}
|
|
|
|
certs, err = ne.tryRetrievePeerCertificates(execCtx, targetAddr, targetDomain, nodeCfg.RequestPath)
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
ne.logger.Warn("could not retrieve certificate")
|
|
return execRes, err
|
|
} else {
|
|
if len(certs) == 0 {
|
|
ne.logger.Warn("no ssl certificates retrieved in http response")
|
|
|
|
ne.setVariablesOfResult(execCtx, execRes, nil)
|
|
} else {
|
|
cert := certs[0] // 只取证书链中的第一个证书,即服务器证书
|
|
ne.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, ";")),
|
|
)
|
|
ne.setVariablesOfResult(execCtx, execRes, cert)
|
|
|
|
now := time.Now()
|
|
isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore)
|
|
isCertHostMatched := cert.VerifyHostname(targetDomain) == nil
|
|
daysLeft := int32(math.Floor(time.Until(cert.NotAfter).Hours() / 24))
|
|
validated := isCertPeriodValid && isCertHostMatched
|
|
|
|
if validated {
|
|
ne.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft))
|
|
} else {
|
|
if !isCertHostMatched {
|
|
ne.logger.Warn("the certificate is invalid, because it is not matched the host")
|
|
} else if !isCertPeriodValid {
|
|
ne.logger.Warn("the certificate is invalid, because it is either expired or not yet valid")
|
|
} else {
|
|
ne.logger.Warn("the certificate is invalid")
|
|
}
|
|
|
|
// 除了验证证书有效期,还要确保证书与域名匹配
|
|
execRes.AddVariable(stateVarKeyCertificateValidity, false, "boolean")
|
|
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, false, "boolean")
|
|
}
|
|
}
|
|
}
|
|
|
|
ne.logger.Info("monitoring completed")
|
|
return execRes, nil
|
|
}
|
|
|
|
func (ne *bizMonitorNodeExecutor) tryRetrievePeerCertificates(execCtx *NodeExecutionContext, addr, domain, requestPath string) ([]*x509.Certificate, error) {
|
|
transport := xhttp.NewDefaultTransport()
|
|
transport.TLSClientConfig = xtls.NewInsecureConfig()
|
|
|
|
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(execCtx.ctx, http.MethodHead, url, nil)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to create http request: %w", err)
|
|
ne.logger.Warn(err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Host", domain)
|
|
req.Header.Set("User-Agent", "certimate")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to send http request: %w", err)
|
|
ne.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
|
|
}
|
|
|
|
func (ne *bizMonitorNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certX509 *x509.Certificate) {
|
|
var vDomain string
|
|
var vDomains string
|
|
var vNotBefore time.Time
|
|
var vNotAfter time.Time
|
|
var vHoursLeft int32
|
|
var vDaysLeft int32
|
|
var vValidity bool
|
|
|
|
if certX509 != nil {
|
|
vDomain = certX509.Subject.CommonName
|
|
vDomains = strings.Join(certX509.DNSNames, ";")
|
|
vNotBefore = certX509.NotBefore
|
|
vNotAfter = certX509.NotAfter
|
|
vHoursLeft = int32(math.Floor(time.Until(certX509.NotAfter).Hours()))
|
|
vDaysLeft = int32(math.Floor(time.Until(certX509.NotAfter).Hours() / 24))
|
|
vValidity = certX509.NotAfter.After(time.Now())
|
|
}
|
|
|
|
execRes.AddVariable(stateVarKeyCertificateDomain, vDomain, "string")
|
|
execRes.AddVariable(stateVarKeyCertificateDomains, vDomains, "string")
|
|
execRes.AddVariable(stateVarKeyCertificateNotBefore, vNotBefore, "datetime")
|
|
execRes.AddVariable(stateVarKeyCertificateNotAfter, vNotAfter, "datetime")
|
|
execRes.AddVariable(stateVarKeyCertificateHoursLeft, vHoursLeft, "number")
|
|
execRes.AddVariable(stateVarKeyCertificateDaysLeft, vDaysLeft, "number")
|
|
execRes.AddVariable(stateVarKeyCertificateValidity, vValidity, "boolean")
|
|
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomain, vDomain, "string")
|
|
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomains, vDomains, "string")
|
|
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotBefore, vNotBefore, "datetime")
|
|
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotAfter, vNotAfter, "datetime")
|
|
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateHoursLeft, vHoursLeft, "number")
|
|
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, vDaysLeft, "number")
|
|
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, vValidity, "boolean")
|
|
}
|
|
|
|
func newBizMonitorNodeExecutor() NodeExecutor {
|
|
return &bizMonitorNodeExecutor{
|
|
nodeExecutor: nodeExecutor{logger: slog.Default()},
|
|
certificateRepo: repository.NewCertificateRepository(),
|
|
}
|
|
}
|