mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-09 15:06:37 +08:00
feat: Refactor manual mode certificate application to increase succes rate (#8799)
This commit is contained in:
parent
9d692b76bf
commit
61ef754a1d
8 changed files with 443 additions and 230 deletions
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
308
agent/utils/ssl/manual_client.go
Normal file
308
agent/utils/ssl/manual_client.go
Normal file
|
@ -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
|
||||
}
|
|
@ -281,8 +281,8 @@ export namespace Website {
|
|||
}
|
||||
|
||||
export interface DNSResolveReq {
|
||||
domains: string[];
|
||||
acmeAccountId: number;
|
||||
websiteSSLId: number;
|
||||
}
|
||||
|
||||
export interface DNSResolve {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue