2025-01-15 22:45:29 +08:00

190 lines
5.6 KiB
Go

package applicant
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/lego"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/slices"
"github.com/usual2970/certimate/internal/repository"
)
type ApplyCertResult struct {
CertificateChain string
IssuerCertificate string
PrivateKey string
ACMECertUrl string
ACMECertStableUrl string
CSR string
}
type Applicant interface {
Apply() (*ApplyCertResult, error)
}
type applicantOptions struct {
Domains []string
ContactEmail string
Provider domain.ApplyDNSProviderType
ProviderAccessConfig map[string]any
ProviderApplyConfig map[string]any
KeyAlgorithm string
Nameservers []string
PropagationTimeout int32
DisableFollowCNAME bool
}
func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
if node.Type != domain.WorkflowNodeTypeApply {
return nil, fmt.Errorf("node type is not apply")
}
accessRepo := repository.NewAccessRepository()
accessId := node.GetConfigString("providerAccessId")
access, err := accessRepo.GetById(context.Background(), accessId)
if err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", accessId, 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(node.GetConfigString("domains"), ";"), func(s string) bool { return s != "" }),
ContactEmail: node.GetConfigString("contactEmail"),
Provider: domain.ApplyDNSProviderType(node.GetConfigString("provider")),
ProviderAccessConfig: accessConfig,
ProviderApplyConfig: node.GetConfigMap("providerConfig"),
KeyAlgorithm: node.GetConfigString("keyAlgorithm"),
Nameservers: slices.Filter(strings.Split(node.GetConfigString("nameservers"), ";"), func(s string) bool { return s != "" }),
PropagationTimeout: node.GetConfigInt32("propagationTimeout"),
DisableFollowCNAME: node.GetConfigBool("disableFollowCNAME"),
}
applicant, err := createApplicant(options)
if err != nil {
return nil, err
}
return &proxyApplicant{
applicant: applicant,
options: options,
}, nil
}
func apply(challengeProvider challenge.Provider, options *applicantOptions) (*ApplyCertResult, error) {
settingsRepo := repository.NewSettingsRepository()
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
sslProviderConfig := &acmeSSLProviderConfig{
Config: acmeSSLProviderConfigContent{},
Provider: defaultSSLProvider,
}
if settings != nil {
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
return nil, err
}
}
if sslProviderConfig.Provider == "" {
sslProviderConfig.Provider = defaultSSLProvider
}
myUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail)
if err != nil {
return nil, err
}
// Some unified lego environment variables are configured here.
// 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.
config.CADirURL = sslProviderUrls[sslProviderConfig.Provider]
config.Certificate.KeyType = parseKeyAlgorithm(options.KeyAlgorithm)
// A client facilitates communication with the CA server.
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
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)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
}
myUser.Registration = reg
}
certRequest := certificate.ObtainRequest{
Domains: options.Domains,
Bundle: true,
}
certResource, err := client.Certificate.Obtain(certRequest)
if err != nil {
return nil, err
}
return &ApplyCertResult{
CertificateChain: strings.TrimSpace(string(certResource.Certificate)),
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
ACMECertUrl: certResource.CertURL,
ACMECertStableUrl: certResource.CertStableURL,
CSR: string(certResource.CSR),
}, nil
}
func parseKeyAlgorithm(algo string) certcrypto.KeyType {
switch algo {
case "RSA2048":
return certcrypto.RSA2048
case "RSA3072":
return certcrypto.RSA3072
case "RSA4096":
return certcrypto.RSA4096
case "RSA8192":
return certcrypto.RSA8192
case "EC256":
return certcrypto.EC256
case "EC384":
return certcrypto.EC384
default:
return certcrypto.RSA2048
}
}
// TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑
type proxyApplicant struct {
applicant challenge.Provider
options *applicantOptions
}
func (d *proxyApplicant) Apply() (*ApplyCertResult, error) {
return apply(d.applicant, d.options)
}