feat: support ARI

This commit is contained in:
Fu Diwei
2025-01-20 02:27:59 +08:00
parent 11d654e902
commit fa8ba061fb
9 changed files with 100 additions and 63 deletions

View File

@@ -14,10 +14,11 @@ import (
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/lego"
"golang.org/x/exp/slices"
"golang.org/x/time/rate"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/slices"
uslices "github.com/usual2970/certimate/internal/pkg/utils/slices"
"github.com/usual2970/certimate/internal/repository"
)
@@ -45,7 +46,9 @@ type applicantOptions struct {
DnsPropagationTimeout int32
DnsTTL int32
DisableFollowCNAME bool
DisableARI bool
SkipBeforeExpiryDays int32
ReplacedARICertId string
}
func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
@@ -54,32 +57,49 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
}
nodeConfig := node.GetConfigForApply()
accessRepo := repository.NewAccessRepository()
access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId)
if err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
}
accessConfig, err := access.UnmarshalConfigToMap()
if err != nil {
return nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
options := &applicantOptions{
Domains: slices.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
Domains: uslices.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
ContactEmail: nodeConfig.ContactEmail,
Provider: domain.ApplyDNSProviderType(nodeConfig.Provider),
ProviderAccessConfig: accessConfig,
ProviderApplyConfig: nodeConfig.ProviderConfig,
KeyAlgorithm: nodeConfig.KeyAlgorithm,
Nameservers: slices.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
Nameservers: uslices.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
DnsTTL: nodeConfig.DnsTTL,
DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
DisableARI: nodeConfig.DisableARI,
SkipBeforeExpiryDays: nodeConfig.SkipBeforeExpiryDays,
}
accessRepo := repository.NewAccessRepository()
if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
} else {
accessConfig, err := access.UnmarshalConfigToMap()
if err != nil {
return nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
options.ProviderAccessConfig = accessConfig
}
certRepo := repository.NewCertificateRepository()
lastCertificate, _ := certRepo.GetByWorkflowNodeId(context.Background(), node.Id)
if lastCertificate != nil {
newCertSan := slices.Clone(options.Domains)
oldCertSan := strings.Split(lastCertificate.SubjectAltNames, ";")
slices.Sort(newCertSan)
slices.Sort(oldCertSan)
if slices.Equal(newCertSan, oldCertSan) {
lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate))
if lastCertX509 != nil {
replacedARICertId, _ := certificate.MakeARICertID(lastCertX509)
options.ReplacedARICertId = replacedARICertId
}
}
}
applicant, err := createApplicant(options)
if err != nil {
return nil, err
@@ -109,7 +129,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
sslProviderConfig.Provider = defaultSSLProvider
}
myUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail)
acmeUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail)
if err != nil {
return nil, err
}
@@ -118,39 +138,42 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
// link: https://github.com/go-acme/lego/issues/1867
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(options.DisableFollowCNAME))
config := lego.NewConfig(myUser)
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
// Create an ACME client config
config := lego.NewConfig(acmeUser)
config.CADirURL = sslProviderUrls[sslProviderConfig.Provider]
config.Certificate.KeyType = parseKeyAlgorithm(options.KeyAlgorithm)
// A client facilitates communication with the CA server.
// Create an ACME client
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
// Set the DNS01 challenge provider
challengeOptions := make([]dns01.ChallengeOption, 0)
if len(options.Nameservers) > 0 {
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(dns01.ParseNameservers(options.Nameservers)))
challengeOptions = append(challengeOptions, dns01.DisableAuthoritativeNssPropagationRequirement())
}
client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...)
// New users will need to register
if !myUser.hasRegistration() {
reg, err := registerAcmeUser(client, sslProviderConfig, myUser)
// New users need to register first
if !acmeUser.hasRegistration() {
reg, err := registerAcmeUser(client, sslProviderConfig, acmeUser)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
}
myUser.Registration = reg
acmeUser.Registration = reg
}
// Obtain a certificate
certRequest := certificate.ObtainRequest{
Domains: options.Domains,
Bundle: true,
}
if !options.DisableARI {
certRequest.ReplacesCertID = options.ReplacedARICertId
}
certResource, err := client.Certificate.Obtain(certRequest)
if err != nil {
return nil, err
@@ -162,7 +185,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
ACMECertUrl: certResource.CertURL,
ACMECertStableUrl: certResource.CertStableURL,
CSR: string(certResource.CSR),
CSR: strings.TrimSpace(string(certResource.CSR)),
}, nil
}

View File

@@ -68,6 +68,7 @@ type WorkflowNodeConfigForApply struct {
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(默认取决于提供商)
DnsTTL int32 `json:"dnsTTL"` // DNS TTL默认取决于提供商
DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否禁用 CNAME 跟随
DisableARI bool `json:"disableARI"` // 是否禁用 ARI
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期默认值30
}
@@ -124,6 +125,7 @@ func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
DnsPropagationTimeout: n.getConfigValueAsInt32("dnsPropagationTimeout"),
DnsTTL: n.getConfigValueAsInt32("dnsTTL"),
DisableFollowCNAME: n.getConfigValueAsBool("disableFollowCNAME"),
DisableARI: n.getConfigValueAsBool("disableARI"),
SkipBeforeExpiryDays: skipBeforeExpiryDays,
}
}

View File

@@ -2,6 +2,7 @@ package nodeprocessor
import (
"context"
"fmt"
"strings"
"time"
@@ -131,8 +132,9 @@ func (a *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
lastCertificate, _ := a.certRepo.GetByWorkflowNodeId(ctx, a.node.Id)
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
if lastCertificate != nil && time.Until(lastCertificate.ExpireAt) > renewalInterval {
return true, "已申请过证书,且证书尚未临近过期"
expirationTime := time.Until(lastCertificate.ExpireAt)
if lastCertificate != nil && expirationTime > renewalInterval {
return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,距 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays)
}
}