mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-24 00:00:52 +00:00
220 lines
6.0 KiB
Go
220 lines
6.0 KiB
Go
package ssl
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"net"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/go-acme/lego/v5/certificate"
|
|
"github.com/go-acme/lego/v5/challenge/dns01"
|
|
"github.com/go-acme/lego/v5/lego"
|
|
"github.com/go-acme/lego/v5/providers/http/webroot"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// dnsChallengeMu serializes DNS-01 issuance flows because lego v5 keeps the
|
|
// recursive-nameserver Client and the LEGO_DISABLE_CNAME_SUPPORT switch in
|
|
// process-wide globals. Without this lock, two concurrent SSL applications
|
|
// with different nameserver/CNAME settings would clobber each other and
|
|
// occasionally fail propagation checks against the wrong resolver.
|
|
var dnsChallengeMu sync.Mutex
|
|
|
|
type AcmeClientOption func(*AcmeClientOptions)
|
|
|
|
type AcmeClientOptions struct {
|
|
SystemProxy *dto.SystemProxy
|
|
}
|
|
|
|
type AcmeClient struct {
|
|
Config *lego.Config
|
|
Client *lego.Client
|
|
User *AcmeUser
|
|
ProxyURL string
|
|
|
|
// dnsChallengeLocked records whether this client currently holds
|
|
// dnsChallengeMu. It is set by UseDns and cleared by ObtainSSL/
|
|
// ObtainIPSSL once the DNS-01 flow finishes. UseHTTP does not touch it.
|
|
dnsChallengeLocked bool
|
|
}
|
|
|
|
func NewAcmeClient(acmeAccount *model.WebsiteAcmeAccount, systemProxy *dto.SystemProxy) (*AcmeClient, error) {
|
|
if acmeAccount.Email == "" {
|
|
return nil, errors.New("email can not blank")
|
|
}
|
|
|
|
client, err := NewRegisterClient(acmeAccount, systemProxy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.WebsiteSSL) error {
|
|
p, err := getDNSProviderConfig(dnsType, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var nameservers []string
|
|
if websiteSSL.Nameserver1 != "" {
|
|
nameservers = append(nameservers, websiteSSL.Nameserver1)
|
|
}
|
|
if websiteSSL.Nameserver2 != "" {
|
|
nameservers = append(nameservers, websiteSSL.Nameserver2)
|
|
}
|
|
|
|
// Hold the global DNS-01 lock for the entire flow that follows, including
|
|
// the Obtain call. lego v5 reads dns01.DefaultClient() inside its own
|
|
// propagation-precheck loop, so the lock cannot be released right after
|
|
// SetDefaultClient/SetDNS01Provider; it has to span the whole DNS-01
|
|
// challenge. ObtainSSL / ObtainIPSSL release it once the request returns.
|
|
dnsChallengeMu.Lock()
|
|
c.dnsChallengeLocked = true
|
|
|
|
if websiteSSL.DisableCNAME {
|
|
_ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
|
|
} else {
|
|
_ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "false")
|
|
}
|
|
|
|
// lego v5 removed dns01.AddRecursiveNameservers and dns01.AddDNSTimeout;
|
|
// configure them via dns01.NewClient(&dns01.Options{...}) + SetDefaultClient.
|
|
dns01.SetDefaultClient(dns01.NewClient(&dns01.Options{
|
|
RecursiveNameservers: nameservers,
|
|
Timeout: dnsTimeOut,
|
|
}))
|
|
|
|
var opts []dns01.ChallengeOption
|
|
if websiteSSL.SkipDNS {
|
|
opts = append(opts, dns01.DisableAuthoritativeNssPropagationRequirement())
|
|
}
|
|
|
|
if err := c.Client.Challenge.SetDNS01Provider(p, opts...); err != nil {
|
|
c.releaseDNSLock()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// releaseDNSLock releases dnsChallengeMu if it was acquired by UseDns.
|
|
// Safe to call multiple times; calls after the first one are no-ops.
|
|
func (c *AcmeClient) releaseDNSLock() {
|
|
if c.dnsChallengeLocked {
|
|
c.dnsChallengeLocked = false
|
|
dnsChallengeMu.Unlock()
|
|
}
|
|
}
|
|
|
|
func (c *AcmeClient) UseHTTP(path string) error {
|
|
httpProvider, err := webroot.NewHTTPProvider(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.Client.Challenge.SetHTTP01Provider(httpProvider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *AcmeClient) ObtainSSL(domains []string, privateKey crypto.Signer) (certificate.Resource, error) {
|
|
defer c.releaseDNSLock()
|
|
// lego v5 disables Common Name by default; explicitly enable it to keep
|
|
// the v4 behaviour, so legacy Java/router clients that still rely on the
|
|
// CommonName field do not fail TLS handshake.
|
|
request := certificate.ObtainRequest{
|
|
Domains: domains,
|
|
Bundle: true,
|
|
PrivateKey: privateKey,
|
|
EnableCommonName: true,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
var certificates *certificate.Resource
|
|
var err error
|
|
|
|
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
|
|
certificates, err = c.Client.Certificate.Obtain(ctx, request)
|
|
if err == nil {
|
|
return *certificates, nil
|
|
}
|
|
|
|
if isHTTP503Error(err) && attempt < maxRetryAttempts {
|
|
global.LOG.Warnf("ACME server returned 503, retrying in %v (attempt %d/%d)",
|
|
retryDelayOn503, attempt, maxRetryAttempts)
|
|
time.Sleep(retryDelayOn503)
|
|
continue
|
|
}
|
|
|
|
// Non-503 error or final attempt, return error
|
|
return certificate.Resource{}, err
|
|
}
|
|
|
|
return certificate.Resource{}, err
|
|
}
|
|
|
|
func (c *AcmeClient) ObtainIPSSL(ipAddress string, privKey crypto.Signer) (certificate.Resource, error) {
|
|
defer c.releaseDNSLock()
|
|
csrTemplate := &x509.CertificateRequest{
|
|
Subject: pkix.Name{
|
|
CommonName: "",
|
|
},
|
|
IPAddresses: []net.IP{
|
|
net.ParseIP(ipAddress),
|
|
},
|
|
}
|
|
csrDER, err := x509.CreateCertificateRequest(
|
|
rand.Reader,
|
|
csrTemplate,
|
|
privKey,
|
|
)
|
|
if err != nil {
|
|
return certificate.Resource{}, err
|
|
}
|
|
csr, err := x509.ParseCertificateRequest(csrDER)
|
|
if err != nil {
|
|
return certificate.Resource{}, err
|
|
}
|
|
req := certificate.ObtainForCSRRequest{
|
|
CSR: csr,
|
|
PrivateKey: privKey,
|
|
Profile: "shortlived",
|
|
Bundle: true,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
var certificates *certificate.Resource
|
|
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
|
|
certificates, err = c.Client.Certificate.ObtainForCSR(ctx, req)
|
|
if err == nil {
|
|
return *certificates, nil
|
|
}
|
|
|
|
if isHTTP503Error(err) && attempt < maxRetryAttempts {
|
|
global.LOG.Warnf("ACME server returned 503 for IP SSL, retrying in %v (attempt %d/%d)",
|
|
retryDelayOn503, attempt, maxRetryAttempts)
|
|
time.Sleep(retryDelayOn503)
|
|
continue
|
|
}
|
|
|
|
return certificate.Resource{}, err
|
|
}
|
|
|
|
return certificate.Resource{}, err
|
|
}
|
|
|
|
func (c *AcmeClient) RevokeSSL(pemSSL []byte) error {
|
|
return c.Client.Certificate.Revoke(context.Background(), pemSSL)
|
|
}
|