mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-10 07:26:35 +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 {
|
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 {
|
type WebsiteSSLRenew struct {
|
||||||
|
@ -45,6 +45,11 @@ type WebsiteSSLApply struct {
|
||||||
DisableLog bool `json:"disableLog"`
|
DisableLog bool `json:"disableLog"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebsiteSSLObtain struct {
|
||||||
|
ID uint `json:"ID" validate:"required"`
|
||||||
|
TXTRecords map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
type WebsiteAcmeAccountCreate struct {
|
type WebsiteAcmeAccountCreate struct {
|
||||||
Email string `json:"email" validate:"required"`
|
Email string `json:"email" validate:"required"`
|
||||||
Type string `json:"type" validate:"required,oneof=letsencrypt zerossl buypass google custom"`
|
Type string `json:"type" validate:"required,oneof=letsencrypt zerossl buypass google custom"`
|
||||||
|
|
|
@ -2,10 +2,10 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/go-acme/lego/v4/certificate"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -26,7 +26,6 @@ import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ssl"
|
"github.com/1Panel-dev/1Panel/agent/utils/ssl"
|
||||||
"github.com/go-acme/lego/v4/certcrypto"
|
|
||||||
legoLogger "github.com/go-acme/lego/v4/log"
|
legoLogger "github.com/go-acme/lego/v4/log"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
)
|
)
|
||||||
|
@ -230,10 +229,13 @@ func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
|
||||||
|
|
||||||
func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
websiteSSL *model.WebsiteSSL
|
websiteSSL *model.WebsiteSSL
|
||||||
acmeAccount *model.WebsiteAcmeAccount
|
acmeAccount *model.WebsiteAcmeAccount
|
||||||
dnsAccount *model.WebsiteDnsAccount
|
dnsAccount *model.WebsiteDnsAccount
|
||||||
|
client *ssl.AcmeClient
|
||||||
|
manualClient *ssl.ManualClient
|
||||||
|
resource certificate.Resource
|
||||||
)
|
)
|
||||||
|
|
||||||
websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(apply.ID))
|
websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(apply.ID))
|
||||||
|
@ -244,75 +246,43 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
client, err := ssl.NewAcmeClient(acmeAccount, getSystemProxy(acmeAccount.UseProxy))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
domains := []string{websiteSSL.PrimaryDomain}
|
domains := []string{websiteSSL.PrimaryDomain}
|
||||||
if websiteSSL.Domains != "" {
|
if websiteSSL.Domains != "" {
|
||||||
domains = append(domains, strings.Split(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 {
|
switch websiteSSL.Provider {
|
||||||
case constant.DNSAccount:
|
case constant.DNSAccount:
|
||||||
dnsAccount, err = websiteDnsRepo.GetFirst(repo.WithByID(websiteSSL.DnsAccountID))
|
dnsAccount, err = websiteDnsRepo.GetFirst(repo.WithByID(websiteSSL.DnsAccountID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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")
|
|
||||||
}
|
}
|
||||||
return err
|
if err = client.UseDns(ssl.DnsType(dnsAccount.Type), dnsAccount.Authorization, *websiteSSL); err != nil {
|
||||||
}
|
return err
|
||||||
for _, domain := range domains {
|
}
|
||||||
if strings.Contains(domain, "*") {
|
case constant.Http:
|
||||||
return buserr.New("ErrWildcardDomain")
|
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
|
websiteSSL.Status = constant.SSLApply
|
||||||
err = websiteSSLRepo.Save(websiteSSL)
|
err = websiteSSLRepo.Save(websiteSSL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -329,13 +299,32 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||||
if websiteSSL.Provider == constant.DNSAccount {
|
if websiteSSL.Provider == constant.DNSAccount {
|
||||||
startMsg = startMsg + i18n.GetMsgWithMap("DNSAccountName", map[string]interface{}{"name": dnsAccount.Name, "type": dnsAccount.Type})
|
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 websiteSSL.Provider != constant.DnsManual {
|
||||||
if err != nil {
|
privateKey, err := ssl.GetPrivateKeyByType(websiteSSL.KeyType, websiteSSL.PrivateKey)
|
||||||
handleError(websiteSSL, err)
|
if err != nil {
|
||||||
return
|
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.PrivateKey = string(resource.PrivateKey)
|
||||||
websiteSSL.Pem = string(resource.Certificate)
|
websiteSSL.Pem = string(resource.Certificate)
|
||||||
websiteSSL.CertURL = resource.CertURL
|
websiteSSL.CertURL = resource.CertURL
|
||||||
|
@ -414,12 +403,15 @@ func (w WebsiteSSLService) GetDNSResolve(req request.WebsiteDNSReq) ([]response.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
client, err := ssl.NewCustomAcmeClient(acmeAccount, nil)
|
||||||
client, err := ssl.NewAcmeClient(acmeAccount, getSystemProxy(acmeAccount.UseProxy))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"golang.org/x/crypto/acme"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -25,6 +27,8 @@ import (
|
||||||
"github.com/go-acme/lego/v4/registration"
|
"github.com/go-acme/lego/v4/registration"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Orders = make(map[uint]*acme.Order)
|
||||||
|
|
||||||
type domainError struct {
|
type domainError struct {
|
||||||
Domain string
|
Domain string
|
||||||
Error error
|
Error error
|
||||||
|
@ -173,13 +177,8 @@ func NewRegisterClient(acmeAccount *model.WebsiteAcmeAccount, proxy *dto.SystemP
|
||||||
return acmeClient, nil
|
return acmeClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigWithProxy(user registration.User, accountType, customCaURL string, systemProxy *dto.SystemProxy) *lego.Config {
|
func getCaDirURL(accountType, customCaURL string) string {
|
||||||
var (
|
var caDirURL string
|
||||||
caDirURL string
|
|
||||||
proxyURL string
|
|
||||||
proxyUser string
|
|
||||||
proxyPassword string
|
|
||||||
)
|
|
||||||
switch accountType {
|
switch accountType {
|
||||||
case "letsencrypt":
|
case "letsencrypt":
|
||||||
caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
|
caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
|
@ -194,6 +193,17 @@ func NewConfigWithProxy(user registration.User, accountType, customCaURL string,
|
||||||
case "custom":
|
case "custom":
|
||||||
caDirURL = customCaURL
|
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 {
|
if systemProxy != nil {
|
||||||
proxyURL = fmt.Sprintf("%s://%s:%s", systemProxy.Type, systemProxy.URL, systemProxy.Port)
|
proxyURL = fmt.Sprintf("%s://%s:%s", systemProxy.Type, systemProxy.URL, systemProxy.Port)
|
||||||
proxyUser = systemProxy.User
|
proxyUser = systemProxy.User
|
||||||
|
@ -305,3 +315,45 @@ func getZeroSSLEabCredentials(email string) (*zeroSSLRes, error) {
|
||||||
|
|
||||||
return &result, nil
|
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 (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"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/certificate"
|
||||||
"github.com/go-acme/lego/v4/challenge"
|
|
||||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||||
"github.com/go-acme/lego/v4/lego"
|
"github.com/go-acme/lego/v4/lego"
|
||||||
"github.com/go-acme/lego/v4/providers/http/webroot"
|
"github.com/go-acme/lego/v4/providers/http/webroot"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AcmeClientOption func(*AcmeClientOptions)
|
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 {
|
func (c *AcmeClient) UseHTTP(path string) error {
|
||||||
httpProvider, err := webroot.NewHTTPProvider(path)
|
httpProvider, err := webroot.NewHTTPProvider(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -133,70 +95,3 @@ func (c *AcmeClient) ObtainSSL(domains []string, privateKey crypto.PrivateKey) (
|
||||||
func (c *AcmeClient) RevokeSSL(pemSSL []byte) error {
|
func (c *AcmeClient) RevokeSSL(pemSSL []byte) error {
|
||||||
return c.Client.Certificate.Revoke(pemSSL)
|
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 {
|
export interface DNSResolveReq {
|
||||||
domains: string[];
|
|
||||||
acmeAccountId: number;
|
acmeAccountId: number;
|
||||||
|
websiteSSLId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DNSResolve {
|
export interface DNSResolve {
|
||||||
|
|
|
@ -65,14 +65,8 @@ const acceptParams = async (props: RenewProps) => {
|
||||||
|
|
||||||
const getDnsResolveRes = async (row: Website.SSL) => {
|
const getDnsResolveRes = async (row: Website.SSL) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
let domains = [row.primaryDomain];
|
|
||||||
if (row.domains != '') {
|
|
||||||
let otherDomains = row.domains.split(',');
|
|
||||||
domains = domains.concat(otherDomains);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const res = await getDnsResolve({ acmeAccountId: row.acmeAccountId, domains: domains });
|
const res = await getDnsResolve({ acmeAccountId: row.acmeAccountId, websiteSSLId: row.id });
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
dnsResolve.value = res.data;
|
dnsResolve.value = res.data;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue