From 61ef754a1d882f1521c1321e1845f4223cb27c0b Mon Sep 17 00:00:00 2001 From: CityFun <31820853+zhengkunwang223@users.noreply.github.com> Date: Fri, 23 May 2025 18:52:13 +0800 Subject: [PATCH] feat: Refactor manual mode certificate application to increase succes rate (#8799) --- agent/app/dto/request/website_ssl.go | 9 +- agent/app/service/website_ssl.go | 140 ++++---- agent/utils/ssl/acme.go | 66 +++- agent/utils/ssl/client.go | 107 +----- agent/utils/ssl/custom_manual_provider.go | 33 -- agent/utils/ssl/manual_client.go | 308 ++++++++++++++++++ frontend/src/api/interface/website.ts | 2 +- .../src/views/website/ssl/apply/index.vue | 8 +- 8 files changed, 443 insertions(+), 230 deletions(-) delete mode 100644 agent/utils/ssl/custom_manual_provider.go create mode 100644 agent/utils/ssl/manual_client.go diff --git a/agent/app/dto/request/website_ssl.go b/agent/app/dto/request/website_ssl.go index a8ca63673..06a662607 100644 --- a/agent/app/dto/request/website_ssl.go +++ b/agent/app/dto/request/website_ssl.go @@ -30,8 +30,8 @@ type WebsiteSSLCreate struct { } type WebsiteDNSReq struct { - Domains []string `json:"domains" validate:"required"` - AcmeAccountID uint `json:"acmeAccountId" validate:"required"` + AcmeAccountID uint `json:"acmeAccountId" validate:"required"` + WebsiteSSLID uint `json:"websiteSSLId" validate:"required"` } type WebsiteSSLRenew struct { @@ -45,6 +45,11 @@ type WebsiteSSLApply struct { DisableLog bool `json:"disableLog"` } +type WebsiteSSLObtain struct { + ID uint `json:"ID" validate:"required"` + TXTRecords map[string]string +} + type WebsiteAcmeAccountCreate struct { Email string `json:"email" validate:"required"` Type string `json:"type" validate:"required,oneof=letsencrypt zerossl buypass google custom"` diff --git a/agent/app/service/website_ssl.go b/agent/app/service/website_ssl.go index 5af8dd67a..4eb3d4c0f 100644 --- a/agent/app/service/website_ssl.go +++ b/agent/app/service/website_ssl.go @@ -2,10 +2,10 @@ package service import ( "context" - "crypto" "crypto/x509" "encoding/pem" "fmt" + "github.com/go-acme/lego/v4/certificate" "log" "os" "path" @@ -26,7 +26,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/req_helper" "github.com/1Panel-dev/1Panel/agent/utils/ssl" - "github.com/go-acme/lego/v4/certcrypto" legoLogger "github.com/go-acme/lego/v4/log" "github.com/jinzhu/gorm" ) @@ -230,10 +229,13 @@ func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) { func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error { var ( - err error - websiteSSL *model.WebsiteSSL - acmeAccount *model.WebsiteAcmeAccount - dnsAccount *model.WebsiteDnsAccount + err error + websiteSSL *model.WebsiteSSL + acmeAccount *model.WebsiteAcmeAccount + dnsAccount *model.WebsiteDnsAccount + client *ssl.AcmeClient + manualClient *ssl.ManualClient + resource certificate.Resource ) websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(apply.ID)) @@ -244,75 +246,43 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error { if err != nil { return err } - client, err := ssl.NewAcmeClient(acmeAccount, getSystemProxy(acmeAccount.UseProxy)) - if err != nil { - return err - } - domains := []string{websiteSSL.PrimaryDomain} if websiteSSL.Domains != "" { domains = append(domains, strings.Split(websiteSSL.Domains, ",")...) } + if websiteSSL.Provider != constant.DnsManual { + client, err = ssl.NewAcmeClient(acmeAccount, getSystemProxy(acmeAccount.UseProxy)) + if err != nil { + return err + } - switch websiteSSL.Provider { - case constant.DNSAccount: - dnsAccount, err = websiteDnsRepo.GetFirst(repo.WithByID(websiteSSL.DnsAccountID)) - if err != nil { - return err - } - if err = client.UseDns(ssl.DnsType(dnsAccount.Type), dnsAccount.Authorization, *websiteSSL); err != nil { - return err - } - case constant.Http: - appInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - if gorm.IsRecordNotFoundError(err) { - return buserr.New("ErrOpenrestyNotFound") + switch websiteSSL.Provider { + case constant.DNSAccount: + dnsAccount, err = websiteDnsRepo.GetFirst(repo.WithByID(websiteSSL.DnsAccountID)) + if err != nil { + return err } - return err - } - for _, domain := range domains { - if strings.Contains(domain, "*") { - return buserr.New("ErrWildcardDomain") + if err = client.UseDns(ssl.DnsType(dnsAccount.Type), dnsAccount.Authorization, *websiteSSL); err != nil { + return err + } + case constant.Http: + appInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + if gorm.IsRecordNotFoundError(err) { + return buserr.New("ErrOpenrestyNotFound") + } + return err + } + for _, domain := range domains { + if strings.Contains(domain, "*") { + return buserr.New("ErrWildcardDomain") + } + } + if err := client.UseHTTP(path.Join(appInstall.GetPath(), "root")); err != nil { + return err } - } - if err := client.UseHTTP(path.Join(appInstall.GetPath(), "root")); err != nil { - return err - } - case constant.DnsManual: - if err := client.UseManualDns(*websiteSSL); err != nil { - return err } } - - var privateKey crypto.PrivateKey - if websiteSSL.PrivateKey == "" { - privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(websiteSSL.KeyType)) - if err != nil { - return err - } - } else { - block, _ := pem.Decode([]byte(websiteSSL.PrivateKey)) - if block == nil { - return buserr.New("invalid PEM block") - } - var privKey crypto.PrivateKey - keyType := ssl.KeyType(websiteSSL.KeyType) - switch keyType { - case certcrypto.EC256, certcrypto.EC384: - privKey, err = x509.ParseECPrivateKey(block.Bytes) - if err != nil { - return nil - } - case certcrypto.RSA2048, certcrypto.RSA3072, certcrypto.RSA4096: - privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil - } - } - privateKey = privKey - } - websiteSSL.Status = constant.SSLApply err = websiteSSLRepo.Save(websiteSSL) if err != nil { @@ -329,13 +299,32 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error { if websiteSSL.Provider == constant.DNSAccount { startMsg = startMsg + i18n.GetMsgWithMap("DNSAccountName", map[string]interface{}{"name": dnsAccount.Name, "type": dnsAccount.Type}) } - legoLogger.Logger.Println(startMsg) + logger.Println(startMsg) } - resource, err := client.ObtainSSL(domains, privateKey) - if err != nil { - handleError(websiteSSL, err) - return + if websiteSSL.Provider != constant.DnsManual { + privateKey, err := ssl.GetPrivateKeyByType(websiteSSL.KeyType, websiteSSL.PrivateKey) + if err != nil { + handleError(websiteSSL, err) + return + } + resource, err = client.ObtainSSL(domains, privateKey) + if err != nil { + handleError(websiteSSL, err) + return + } + } else { + manualClient, err = ssl.NewCustomAcmeClient(acmeAccount, logger) + if err != nil { + handleError(websiteSSL, err) + return + } + resource, err = manualClient.RequestCertificate(context.Background(), websiteSSL) + if err != nil { + handleError(websiteSSL, err) + return + } } + websiteSSL.PrivateKey = string(resource.PrivateKey) websiteSSL.Pem = string(resource.Certificate) websiteSSL.CertURL = resource.CertURL @@ -414,12 +403,15 @@ func (w WebsiteSSLService) GetDNSResolve(req request.WebsiteDNSReq) ([]response. if err != nil { return nil, err } - - client, err := ssl.NewAcmeClient(acmeAccount, getSystemProxy(acmeAccount.UseProxy)) + client, err := ssl.NewCustomAcmeClient(acmeAccount, nil) if err != nil { return nil, err } - resolves, err := client.GetDNSResolve(req.Domains) + websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(req.WebsiteSSLID)) + if err != nil { + return nil, err + } + resolves, err := client.GetDNSResolve(context.TODO(), websiteSSL) if err != nil { return nil, err } diff --git a/agent/utils/ssl/acme.go b/agent/utils/ssl/acme.go index 8adb04e4b..aec5ebc97 100644 --- a/agent/utils/ssl/acme.go +++ b/agent/utils/ssl/acme.go @@ -10,6 +10,8 @@ import ( "encoding/pem" "fmt" "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "golang.org/x/crypto/acme" "io" "net" "net/http" @@ -25,6 +27,8 @@ import ( "github.com/go-acme/lego/v4/registration" ) +var Orders = make(map[uint]*acme.Order) + type domainError struct { Domain string Error error @@ -173,13 +177,8 @@ func NewRegisterClient(acmeAccount *model.WebsiteAcmeAccount, proxy *dto.SystemP return acmeClient, nil } -func NewConfigWithProxy(user registration.User, accountType, customCaURL string, systemProxy *dto.SystemProxy) *lego.Config { - var ( - caDirURL string - proxyURL string - proxyUser string - proxyPassword string - ) +func getCaDirURL(accountType, customCaURL string) string { + var caDirURL string switch accountType { case "letsencrypt": caDirURL = "https://acme-v02.api.letsencrypt.org/directory" @@ -194,6 +193,17 @@ func NewConfigWithProxy(user registration.User, accountType, customCaURL string, case "custom": caDirURL = customCaURL } + return caDirURL +} + +func NewConfigWithProxy(user registration.User, accountType, customCaURL string, systemProxy *dto.SystemProxy) *lego.Config { + var ( + caDirURL string + proxyURL string + proxyUser string + proxyPassword string + ) + caDirURL = getCaDirURL(accountType, customCaURL) if systemProxy != nil { proxyURL = fmt.Sprintf("%s://%s:%s", systemProxy.Type, systemProxy.URL, systemProxy.Port) proxyUser = systemProxy.User @@ -305,3 +315,45 @@ func getZeroSSLEabCredentials(email string) (*zeroSSLRes, error) { return &result, nil } + +func GetPrivateKeyByType(keyType, sslPrivateKey string) (crypto.PrivateKey, error) { + var ( + privateKey crypto.PrivateKey + err error + ) + kType := KeyType(keyType) + if sslPrivateKey == "" { + privateKey, err = certcrypto.GeneratePrivateKey(kType) + if err != nil { + return nil, err + } + return privateKey, nil + } + block, _ := pem.Decode([]byte(sslPrivateKey)) + if block == nil { + return nil, buserr.New("invalid PEM block") + } + var privKey crypto.PrivateKey + switch kType { + case certcrypto.EC256, certcrypto.EC384: + privKey, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + case certcrypto.RSA2048, certcrypto.RSA3072, certcrypto.RSA4096: + privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + } + privateKey = privKey + return privateKey, nil +} + +func getWebsiteSSLDomains(websiteSSL *model.WebsiteSSL) []string { + domains := []string{websiteSSL.PrimaryDomain} + if websiteSSL.Domains != "" { + domains = append(domains, strings.Split(websiteSSL.Domains, ",")...) + } + return domains +} diff --git a/agent/utils/ssl/client.go b/agent/utils/ssl/client.go index f50b261a0..f0c13ec66 100644 --- a/agent/utils/ssl/client.go +++ b/agent/utils/ssl/client.go @@ -3,19 +3,13 @@ package ssl import ( "crypto" "github.com/1Panel-dev/1Panel/agent/app/dto" - "os" - "strings" - "time" - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/go-acme/lego/v4/acme" - "github.com/go-acme/lego/v4/acme/api" "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/go-acme/lego/v4/providers/http/webroot" "github.com/pkg/errors" + "os" ) type AcmeClientOption func(*AcmeClientOptions) @@ -70,38 +64,6 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web ) } -func (c *AcmeClient) UseManualDns(websiteSSL model.WebsiteSSL) error { - p, err := NewCustomDNSProviderManual(&ManualConfig{ - PropagationTimeout: 20 * time.Minute, - PollingInterval: pollingInterval, - TTL: ttl, - }) - if err != nil { - return err - } - var nameservers []string - if websiteSSL.Nameserver1 != "" { - nameservers = append(nameservers, websiteSSL.Nameserver1) - } - if websiteSSL.Nameserver2 != "" { - nameservers = append(nameservers, websiteSSL.Nameserver2) - } - if websiteSSL.DisableCNAME { - _ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true") - } else { - _ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "false") - } - if err = c.Client.Challenge.SetDNS01Provider(p, - dns01.CondOption(len(nameservers) > 0, - dns01.AddRecursiveNameservers(nameservers)), - dns01.CondOption(websiteSSL.SkipDNS, - dns01.DisableAuthoritativeNssPropagationRequirement()), - dns01.AddDNSTimeout(dnsTimeOut)); err != nil { - return err - } - return nil -} - func (c *AcmeClient) UseHTTP(path string) error { httpProvider, err := webroot.NewHTTPProvider(path) if err != nil { @@ -133,70 +95,3 @@ func (c *AcmeClient) ObtainSSL(domains []string, privateKey crypto.PrivateKey) ( func (c *AcmeClient) RevokeSSL(pemSSL []byte) error { return c.Client.Certificate.Revoke(pemSSL) } - -type Resolve struct { - Key string - Value string - Err string -} - -func (c *AcmeClient) GetDNSResolve(domains []string) (map[string]Resolve, error) { - core, err := api.New(c.Config.HTTPClient, c.Config.UserAgent, c.Config.CADirURL, c.User.Registration.URI, c.User.Key) - if err != nil { - return nil, err - } - order, err := core.Orders.New(domains) - if err != nil { - return nil, err - } - resolves := make(map[string]Resolve) - resc, errc := make(chan acme.Authorization), make(chan domainError) - for _, authzURL := range order.Authorizations { - go func(authzURL string) { - authz, err := core.Authorizations.Get(authzURL) - if err != nil { - errc <- domainError{Domain: authz.Identifier.Value, Error: err} - return - } - resc <- authz - }(authzURL) - } - - var responses []acme.Authorization - for i := 0; i < len(order.Authorizations); i++ { - select { - case res := <-resc: - responses = append(responses, res) - case err := <-errc: - resolves[err.Domain] = Resolve{Err: err.Error.Error()} - } - } - close(resc) - close(errc) - - for _, auth := range responses { - domain := challenge.GetTargetedDomain(auth) - chlng, err := challenge.FindChallenge(challenge.DNS01, auth) - if err != nil { - resolves[domain] = Resolve{Err: err.Error()} - continue - } - keyAuth, err := core.GetKeyAuthorization(chlng.Token) - if err != nil { - resolves[domain] = Resolve{Err: err.Error()} - continue - } - challengeInfo := dns01.GetChallengeInfo(domain, keyAuth) - fqdn := challengeInfo.FQDN - if strings.HasPrefix(domain, "*.") && strings.Contains(fqdn, "*.") { - fqdn = strings.Replace(fqdn, "*.", "", 1) - } - _, _ = dns01.FindZoneByFqdn(challengeInfo.EffectiveFQDN) - resolves[domain] = Resolve{ - Key: fqdn, - Value: challengeInfo.Value, - } - } - - return resolves, nil -} diff --git a/agent/utils/ssl/custom_manual_provider.go b/agent/utils/ssl/custom_manual_provider.go deleted file mode 100644 index 1b0d25292..000000000 --- a/agent/utils/ssl/custom_manual_provider.go +++ /dev/null @@ -1,33 +0,0 @@ -package ssl - -import "time" - -type ManualConfig struct { - TTL int - PropagationTimeout time.Duration - PollingInterval time.Duration -} - -type CustomManualDnsProvider struct { - config *ManualConfig -} - -func NewCustomDNSProviderManual(config *ManualConfig) (*CustomManualDnsProvider, error) { - return &CustomManualDnsProvider{config}, nil -} - -func (p *CustomManualDnsProvider) Present(domain, token, keyAuth string) error { - return nil -} - -func (p *CustomManualDnsProvider) CleanUp(domain, token, keyAuth string) error { - return nil -} - -func (p *CustomManualDnsProvider) Sequential() time.Duration { - return manualDnsTimeout -} - -func (p *CustomManualDnsProvider) Timeout() (timeout, interval time.Duration) { - return p.config.PropagationTimeout, p.config.PollingInterval -} diff --git a/agent/utils/ssl/manual_client.go b/agent/utils/ssl/manual_client.go new file mode 100644 index 000000000..d7e7ee5a1 --- /dev/null +++ b/agent/utils/ssl/manual_client.go @@ -0,0 +1,308 @@ +package ssl + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/go-acme/lego/v4/certificate" + "golang.org/x/crypto/acme" + "log" + "net" + "strings" + "time" +) + +type ManualClient struct { + client *acme.Client + account *model.WebsiteAcmeAccount + logger *log.Logger +} + +type RequestCertRequest struct { + WebsiteSSL *model.WebsiteSSL +} + +func NewCustomAcmeClient(acmeAccount *model.WebsiteAcmeAccount, logger *log.Logger) (*ManualClient, error) { + var ( + key crypto.PrivateKey + err error + ) + switch KeyType(acmeAccount.KeyType) { + case KeyEC256, KeyEC384: + block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) + key, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + case KeyRSA2048, KeyRSA3072, KeyRSA4096: + block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) + key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + } + if logger == nil { + logger = log.Default() + } + + client := &acme.Client{ + Key: key.(crypto.Signer), + DirectoryURL: getCaDirURL(acmeAccount.Type, acmeAccount.CaDirURL), + } + return &ManualClient{ + client: client, + account: acmeAccount, + logger: logger, + }, nil +} + +type Resolve struct { + Key string + Value string + Err string +} + +func (c *ManualClient) GetDNSResolve(ctx context.Context, websiteSSL *model.WebsiteSSL) (map[string]Resolve, error) { + order, err := c.client.AuthorizeOrder(ctx, acme.DomainIDs(getWebsiteSSLDomains(websiteSSL)...)) + if err != nil { + return nil, err + } + Orders[websiteSSL.ID] = order + + records := make(map[string]Resolve) + + for _, authzURL := range order.AuthzURLs { + authz, err := c.client.GetAuthorization(ctx, authzURL) + if err != nil { + return nil, err + } + domain := authz.Identifier.Value + + var dnsChallenge *acme.Challenge + for _, challenge := range authz.Challenges { + if challenge.Type == "dns-01" { + dnsChallenge = challenge + break + } + } + + if dnsChallenge == nil { + return nil, fmt.Errorf("no DNS-01 challenge found for domain %s", domain) + } + + txtValue, err := c.client.DNS01ChallengeRecord(dnsChallenge.Token) + if err != nil { + return nil, err + } + + records[domain] = Resolve{ + Key: fmt.Sprintf("_acme-challenge.%s", domain), + Value: txtValue, + } + } + return records, nil +} + +func queryDNSRecords(domain string) (map[string]string, error) { + recordName := fmt.Sprintf("_acme-challenge.%s", domain) + txts, err := net.LookupTXT(recordName) + if err != nil { + return nil, err + } + records := make(map[string]string) + if len(txts) > 0 { + records[recordName] = txts[0] + } + return records, nil +} + +func (c *ManualClient) handleAuthorization(ctx context.Context, authzURL string) error { + authz, err := c.client.GetAuthorization(ctx, authzURL) + if err != nil { + return fmt.Errorf("failed to get authorization: %v", err) + } + + domain := authz.Identifier.Value + c.logger.Printf("[INFO] [%s] AuthURL: %s", domain, authzURL) + + if authz.Status == acme.StatusValid { + return nil + } + + var dnsChallenge *acme.Challenge + for _, challenge := range authz.Challenges { + if challenge.Type == "dns-01" { + dnsChallenge = challenge + break + } + } + + c.logger.Printf("[INFO] [%s] acme: use dns-01 solver", domain) + if dnsChallenge == nil { + return fmt.Errorf("no DNS-01 challenge found for domain %s", domain) + } + + deadline := time.Now().Add(manualDnsTimeout) + expectedRecord, err := c.client.DNS01ChallengeRecord(dnsChallenge.Token) + if err != nil { + return fmt.Errorf("failed to compute DNS challenge record: %v", err) + } + c.logger.Printf("[INFO] [%s] acme: Checking TXT record %s", domain, expectedRecord) + + for { + c.logger.Printf("[INFO] [%s] acme: Checking DNS record propagation.", domain) + currentRecords, err := queryDNSRecords(domain) + if err != nil { + return fmt.Errorf("failed to query DNS records: %v", err) + } + recordName := fmt.Sprintf("_acme-challenge.%s", domain) + providedRecord, exists := currentRecords[recordName] + if exists && providedRecord == expectedRecord { + break + } + if time.Now().After(deadline) { + if !exists { + return fmt.Errorf("TXT record not provided for domain %s after retrying", domain) + } + c.logger.Printf("[INFO] [%s] TXT record mismatch for %s: expected %s, got %s\"", domain, domain, expectedRecord, providedRecord) + return fmt.Errorf("TXT record mismatch for %s: expected %s, got %s", domain, expectedRecord, providedRecord) + } + time.Sleep(pollingInterval) + } + + _, err = c.client.Accept(ctx, dnsChallenge) + if err != nil { + return fmt.Errorf("failed to accept challenge: %v", err) + } + for { + time.Sleep(pollingInterval) + authz, err = c.client.GetAuthorization(ctx, authzURL) + if err != nil { + return fmt.Errorf("failed to get authorization while polling: %v", err) + } + if authz.Status == acme.StatusValid { + break + } else if authz.Status == acme.StatusInvalid { + return fmt.Errorf("authorization failed for domain %s", domain) + } + } + return nil +} + +func (c *ManualClient) createCSR(keyType string, privateKey string, domains []string) ([]byte, crypto.PrivateKey, error) { + var certKey crypto.PrivateKey + var err error + certKey, err = GetPrivateKeyByType(keyType, privateKey) + if err != nil { + return nil, nil, err + } + template := x509.CertificateRequest{ + Subject: pkix.Name{CommonName: domains[0]}, + DNSNames: domains, + } + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, certKey) + if err != nil { + return nil, nil, err + } + return csrBytes, certKey, nil +} + +func (c *ManualClient) encodePrivateKey(key crypto.PrivateKey) (string, error) { + var keyBytes []byte + var keyType string + var err error + + switch k := key.(type) { + case *ecdsa.PrivateKey: + keyBytes, err = x509.MarshalECPrivateKey(k) + keyType = "EC PRIVATE KEY" + case *rsa.PrivateKey: + keyBytes = x509.MarshalPKCS1PrivateKey(k) + keyType = "RSA PRIVATE KEY" + default: + return "", fmt.Errorf("unsupported key type") + } + + if err != nil { + return "", err + } + + block := &pem.Block{ + Type: keyType, + Bytes: keyBytes, + } + + return string(pem.EncodeToMemory(block)), nil +} + +func (c *ManualClient) RequestCertificate(ctx context.Context, websiteSSL *model.WebsiteSSL) (certificate.Resource, error) { + var res certificate.Resource + domains := []string{websiteSSL.PrimaryDomain} + if websiteSSL.Domains != "" { + domains = append(domains, strings.Split(websiteSSL.Domains, ",")...) + } + + c.logger.Printf("[INFO] Requesting certificate for domains: %v\n", domains) + csr, certKey, err := c.createCSR(websiteSSL.KeyType, websiteSSL.PrivateKey, domains) + if err != nil { + return res, err + } + + order, ok := Orders[websiteSSL.ID] + if !ok { + return res, fmt.Errorf("order not found") + } + defer delete(Orders, websiteSSL.ID) + + for _, authzURL := range order.AuthzURLs { + if err := c.handleAuthorization(ctx, authzURL); err != nil { + return res, err + } + } + + c.logger.Printf("[INFO] acme: Validations succeeded; requesting certificates") + order, err = c.client.WaitOrder(ctx, order.URI) + if err != nil { + return res, err + } + + if order.Status != acme.StatusReady { + return res, fmt.Errorf("order not ready: %s", order.Status) + } + + certBytes, certURL, err := c.client.CreateOrderCert(ctx, order.FinalizeURL, csr, true) + if err != nil { + return res, fmt.Errorf("failed to finalize order: %v", err) + } + + privateKeyPEM, err := c.encodePrivateKey(certKey) + if err != nil { + return res, err + } + + var certPEM []byte + for _, cert := range certBytes { + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert, + } + certPEM = append(certPEM, pem.EncodeToMemory(block)...) + } + c.logger.Printf("[INFO] acme: Server responded with a certificate.") + resource := certificate.Resource{ + Domain: domains[0], + CertURL: certURL, + CertStableURL: certURL, + PrivateKey: []byte(privateKeyPEM), + Certificate: certPEM, + CSR: csr, + } + return resource, nil +} diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index ee14e9ebd..42eb0a393 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -281,8 +281,8 @@ export namespace Website { } export interface DNSResolveReq { - domains: string[]; acmeAccountId: number; + websiteSSLId: number; } export interface DNSResolve { diff --git a/frontend/src/views/website/ssl/apply/index.vue b/frontend/src/views/website/ssl/apply/index.vue index 85d97308c..f40587813 100644 --- a/frontend/src/views/website/ssl/apply/index.vue +++ b/frontend/src/views/website/ssl/apply/index.vue @@ -65,14 +65,8 @@ const acceptParams = async (props: RenewProps) => { const getDnsResolveRes = async (row: Website.SSL) => { loading.value = true; - - let domains = [row.primaryDomain]; - if (row.domains != '') { - let otherDomains = row.domains.split(','); - domains = domains.concat(otherDomains); - } try { - const res = await getDnsResolve({ acmeAccountId: row.acmeAccountId, domains: domains }); + const res = await getDnsResolve({ acmeAccountId: row.acmeAccountId, websiteSSLId: row.id }); if (res.data) { dnsResolve.value = res.data; }