mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-07 21:19:51 +00:00
192 lines
4.4 KiB
Go
192 lines
4.4 KiB
Go
package nodeprocessor
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"math"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"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("entering inspect certificate node...")
|
|
|
|
nodeConfig := n.node.GetConfigForInspect()
|
|
|
|
err := n.inspect(ctx, nodeConfig)
|
|
if err != nil {
|
|
n.logger.Warn("inspect 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 lastError error
|
|
var certInfo *x509.Certificate
|
|
|
|
host := nodeConfig.Host
|
|
|
|
port := nodeConfig.Port
|
|
if port == "" {
|
|
port = "443"
|
|
}
|
|
|
|
domain := nodeConfig.Domain
|
|
if domain == "" {
|
|
domain = host
|
|
}
|
|
|
|
path := nodeConfig.Path
|
|
if path != "" && !strings.HasPrefix(path, "/") {
|
|
path = "/" + path
|
|
}
|
|
|
|
targetAddr := fmt.Sprintf("%s:%s", host, port)
|
|
n.logger.Info(fmt.Sprintf("Inspecting certificate at %s (validating domain: %s)", targetAddr, domain))
|
|
|
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
|
if attempt > 0 {
|
|
n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, targetAddr))
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(retryInterval):
|
|
// Wait for retry interval
|
|
}
|
|
}
|
|
|
|
transport := &http.Transport{
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 10 * time.Second,
|
|
}).DialContext,
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
ServerName: domain, // Set SNI to domain for proper certificate selection
|
|
},
|
|
ForceAttemptHTTP2: false,
|
|
DisableKeepAlives: true,
|
|
}
|
|
|
|
client := &http.Client{
|
|
Transport: transport,
|
|
Timeout: 15 * time.Second,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
|
|
scheme := "https"
|
|
urlStr := fmt.Sprintf("%s://%s", scheme, targetAddr)
|
|
if path != "" {
|
|
urlStr = urlStr + path
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "HEAD", urlStr, nil)
|
|
if err != nil {
|
|
lastError = fmt.Errorf("failed to create HTTP request: %w", err)
|
|
n.logger.Warn(fmt.Sprintf("Request creation attempt #%d failed: %s", attempt+1, lastError.Error()))
|
|
continue
|
|
}
|
|
|
|
if domain != host {
|
|
req.Host = domain
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "CertificateValidator/1.0")
|
|
req.Header.Set("Accept", "*/*")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
lastError = fmt.Errorf("HTTP request failed: %w", err)
|
|
n.logger.Warn(fmt.Sprintf("Connection attempt #%d failed: %s", attempt+1, lastError.Error()))
|
|
continue
|
|
}
|
|
|
|
if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
|
|
resp.Body.Close()
|
|
lastError = fmt.Errorf("no TLS certificates received in HTTP response")
|
|
n.logger.Warn(fmt.Sprintf("Certificate retrieval attempt #%d failed: %s", attempt+1, lastError.Error()))
|
|
continue
|
|
}
|
|
|
|
certInfo = resp.TLS.PeerCertificates[0]
|
|
resp.Body.Close()
|
|
|
|
lastError = nil
|
|
n.logger.Info(fmt.Sprintf("Successfully retrieved certificate from %s", targetAddr))
|
|
break
|
|
}
|
|
|
|
if lastError != nil {
|
|
return fmt.Errorf("failed to retrieve certificate after %d attempts: %w", maxRetries, lastError)
|
|
}
|
|
|
|
if certInfo == nil {
|
|
outputs := map[string]any{
|
|
outputCertificateValidatedKey: "false",
|
|
outputCertificateDaysLeftKey: "0",
|
|
}
|
|
n.setOutputs(outputs)
|
|
return nil
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
isValidTime := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore)
|
|
|
|
domainMatch := true
|
|
if err := certInfo.VerifyHostname(domain); err != nil {
|
|
domainMatch = false
|
|
}
|
|
|
|
isValid := isValidTime && domainMatch
|
|
|
|
daysRemaining := math.Floor(certInfo.NotAfter.Sub(now).Hours() / 24)
|
|
|
|
isValidStr := "false"
|
|
if isValid {
|
|
isValidStr = "true"
|
|
}
|
|
|
|
outputs := map[string]any{
|
|
outputCertificateValidatedKey: isValidStr,
|
|
outputCertificateDaysLeftKey: fmt.Sprintf("%d", int(daysRemaining)),
|
|
}
|
|
|
|
n.setOutputs(outputs)
|
|
|
|
n.logger.Info(fmt.Sprintf("Certificate inspection completed - Target: %s, Domain: %s, Valid: %s, Days Remaining: %d",
|
|
targetAddr, domain, isValidStr, int(daysRemaining)))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *inspectNode) setOutputs(outputs map[string]any) {
|
|
n.outputs = outputs
|
|
}
|