mirror of
https://github.com/usual2970/certimate.git
synced 2025-08-10 11:21:46 +00:00
add certificate mornitoring node
This commit is contained in:
159
internal/workflow/node-processor/inspect_node.go
Normal file
159
internal/workflow/node-processor/inspect_node.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type inspectNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*nodeProcessor
|
||||
*nodeOutputer
|
||||
}
|
||||
|
||||
func NewInspectNode(node *domain.WorkflowNode) *inspectNode {
|
||||
return &inspectNode{
|
||||
node: node,
|
||||
nodeProcessor: newNodeProcessor(node),
|
||||
nodeOutputer: newNodeOutputer(),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *inspectNode) Process(ctx context.Context) error {
|
||||
n.logger.Info("enter inspect website certificate node ...")
|
||||
|
||||
nodeConfig := n.node.GetConfigForInspect()
|
||||
|
||||
err := n.inspect(ctx, nodeConfig)
|
||||
if err != nil {
|
||||
n.logger.Warn("inspect website certificate failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNodeConfigForInspect) error {
|
||||
// 定义重试参数
|
||||
maxRetries := 3
|
||||
retryInterval := 2 * time.Second
|
||||
|
||||
var cert *tls.Certificate
|
||||
var lastError error
|
||||
|
||||
domainWithPort := nodeConfig.Domain + ":" + nodeConfig.Port
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, domainWithPort))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(retryInterval):
|
||||
// Wait for retry interval
|
||||
}
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", domainWithPort, &tls.Config{
|
||||
InsecureSkipVerify: true, // Allow self-signed certificates
|
||||
})
|
||||
if err != nil {
|
||||
lastError = fmt.Errorf("failed to connect to %s: %w", domainWithPort, err)
|
||||
n.logger.Warn(fmt.Sprintf("Connection attempt #%d failed: %s", attempt+1, lastError.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get certificate information
|
||||
certInfo := conn.ConnectionState().PeerCertificates[0]
|
||||
conn.Close()
|
||||
|
||||
// Certificate information retrieved successfully
|
||||
cert = &tls.Certificate{
|
||||
Certificate: [][]byte{certInfo.Raw},
|
||||
Leaf: certInfo,
|
||||
}
|
||||
lastError = nil
|
||||
n.logger.Info(fmt.Sprintf("Successfully retrieved certificate information for %s", domainWithPort))
|
||||
break
|
||||
}
|
||||
|
||||
if lastError != nil {
|
||||
return fmt.Errorf("failed to retrieve certificate after %d attempts: %w", maxRetries, lastError)
|
||||
}
|
||||
|
||||
certInfo := cert.Leaf
|
||||
now := time.Now()
|
||||
|
||||
isValid := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore)
|
||||
|
||||
// Check domain matching
|
||||
domainMatch := false
|
||||
if len(certInfo.DNSNames) > 0 {
|
||||
for _, dnsName := range certInfo.DNSNames {
|
||||
if matchDomain(nodeConfig.Domain, dnsName) {
|
||||
domainMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if matchDomain(nodeConfig.Domain, certInfo.Subject.CommonName) {
|
||||
domainMatch = true
|
||||
}
|
||||
|
||||
isValid = isValid && domainMatch
|
||||
|
||||
daysRemaining := math.Floor(certInfo.NotAfter.Sub(now).Hours() / 24)
|
||||
|
||||
// Set node outputs
|
||||
outputs := map[string]any{
|
||||
"certificate.validated": isValid,
|
||||
"certificate.daysLeft": daysRemaining,
|
||||
}
|
||||
n.setOutputs(outputs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *inspectNode) setOutputs(outputs map[string]any) {
|
||||
n.outputs = outputs
|
||||
}
|
||||
|
||||
func matchDomain(requestDomain, certDomain string) bool {
|
||||
if requestDomain == certDomain {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(certDomain) > 2 && certDomain[0] == '*' && certDomain[1] == '.' {
|
||||
|
||||
wildcardSuffix := certDomain[1:]
|
||||
requestDomainLen := len(requestDomain)
|
||||
suffixLen := len(wildcardSuffix)
|
||||
|
||||
if requestDomainLen > suffixLen && requestDomain[requestDomainLen-suffixLen:] == wildcardSuffix {
|
||||
remainingPart := requestDomain[:requestDomainLen-suffixLen]
|
||||
if len(remainingPart) > 0 && !contains(remainingPart, '.') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func contains(s string, c byte) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
39
internal/workflow/node-processor/inspect_node_test.go
Normal file
39
internal/workflow/node-processor/inspect_node_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
func Test_inspectWebsiteCertificateNode_inspect(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
nodeConfig domain.WorkflowNodeConfigForInspect
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
nodeConfig: domain.WorkflowNodeConfigForInspect{
|
||||
Domain: "baidu.com",
|
||||
Port: "443",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
n := NewInspectNode(&domain.WorkflowNode{})
|
||||
if err := n.inspect(tt.args.ctx, tt.args.nodeConfig); (err != nil) != tt.wantErr {
|
||||
t.Errorf("inspectWebsiteCertificateNode.inspect() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -86,6 +86,8 @@ func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) {
|
||||
return NewExecuteSuccessNode(node), nil
|
||||
case domain.WorkflowNodeTypeExecuteFailure:
|
||||
return NewExecuteFailureNode(node), nil
|
||||
case domain.WorkflowNodeTypeInspect:
|
||||
return NewInspectNode(node), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("supported node type: %s", string(node.Type))
|
||||
|
Reference in New Issue
Block a user