Files
1Panel/agent/utils/ssl/client.go
2026-07-20 14:06:59 +08:00

266 lines
7.0 KiB
Go

package ssl
import (
"context"
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"net"
"net/http"
"os"
"strings"
"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"
legoacme "github.com/go-acme/lego/v5/acme"
"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
var ErrAcmeAccountURLMissing = errors.New("acme account url is empty")
type AcmeClientOption func(*AcmeClientOptions)
type AcmeClientOptions struct {
SystemProxy *dto.SystemProxy
}
type AcmeClient struct {
Config *lego.Config
Client *lego.Client
User *AcmeUser
ProxyURL string
dnsChallengeConfig *dnsChallengeConfig
}
type dnsChallengeConfig struct {
recursiveNameservers []string
disableCNAME bool
}
func NewAcmeClient(acmeAccount *model.WebsiteAcmeAccount, systemProxy *dto.SystemProxy) (*AcmeClient, error) {
return NewAcmeClientWithContext(context.Background(), acmeAccount, systemProxy)
}
func NewAcmeClientWithContext(ctx context.Context, acmeAccount *model.WebsiteAcmeAccount, systemProxy *dto.SystemProxy) (*AcmeClient, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if acmeAccount.Email == "" {
return nil, errors.New("email can not blank")
}
accountURL := strings.TrimSpace(acmeAccount.URL)
if accountURL == "" {
return nil, ErrAcmeAccountURLMissing
}
if strings.TrimSpace(acmeAccount.PrivateKey) == "" {
return nil, errors.New("private key can not blank")
}
client, err := newAcmeClient(acmeAccount, systemProxy, &legoacme.ExtendedAccount{Location: accountURL})
if err != nil {
return nil, err
}
return client, nil
}
func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.WebsiteSSL) error {
var httpClient *http.Client
if c.Config != nil {
httpClient = c.Config.HTTPClient
}
p, err := getDNSProviderConfig(dnsType, params, httpClient)
if err != nil {
return err
}
var nameservers []string
if websiteSSL.Nameserver1 != "" {
nameservers = append(nameservers, websiteSSL.Nameserver1)
}
if websiteSSL.Nameserver2 != "" {
nameservers = append(nameservers, websiteSSL.Nameserver2)
}
var opts []dns01.ChallengeOption
if websiteSSL.SkipDNS {
opts = append(opts, dns01.DisableAuthoritativeNssPropagationRequirement())
}
if err := c.Client.Challenge.SetDNS01Provider(p, opts...); err != nil {
return err
}
c.dnsChallengeConfig = &dnsChallengeConfig{
recursiveNameservers: append([]string(nil), nameservers...),
disableCNAME: websiteSSL.DisableCNAME,
}
return nil
}
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
}
c.dnsChallengeConfig = nil
return nil
}
func (c *AcmeClient) ObtainSSL(ctx context.Context, domains []string, privateKey crypto.Signer) (certificate.Resource, error) {
unlockDNSChallenge := c.lockDNSChallenge()
defer unlockDNSChallenge()
// 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,
}
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)
if err := waitForRetry(ctx, retryDelayOn503); err != nil {
return certificate.Resource{}, err
}
continue
}
// Non-503 error or final attempt, return error
return certificate.Resource{}, err
}
return certificate.Resource{}, err
}
func (c *AcmeClient) ObtainIPSSL(ctx context.Context, ipAddress string, privKey crypto.Signer) (certificate.Resource, error) {
unlockDNSChallenge := c.lockDNSChallenge()
defer unlockDNSChallenge()
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,
}
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)
if err := waitForRetry(ctx, retryDelayOn503); err != nil {
return certificate.Resource{}, err
}
continue
}
return certificate.Resource{}, err
}
return certificate.Resource{}, err
}
func (c *AcmeClient) RevokeSSL(pemSSL []byte) error {
return c.Client.Certificate.Revoke(context.Background(), pemSSL)
}
func (c *AcmeClient) lockDNSChallenge() func() {
if c.dnsChallengeConfig == nil {
return func() {}
}
dnsChallengeMu.Lock()
oldCNAME, hadCNAME := os.LookupEnv("LEGO_DISABLE_CNAME_SUPPORT")
if c.dnsChallengeConfig.disableCNAME {
_ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
} else {
_ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "false")
}
previousClient := dns01.DefaultClient()
// lego v5 configures custom recursive nameservers through a client instance.
dns01.SetDefaultClient(dns01.NewClient(&dns01.Options{
RecursiveNameservers: c.dnsChallengeConfig.recursiveNameservers,
}))
return func() {
dns01.SetDefaultClient(previousClient)
if hadCNAME {
_ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", oldCNAME)
} else {
_ = os.Unsetenv("LEGO_DISABLE_CNAME_SUPPORT")
}
dnsChallengeMu.Unlock()
}
}
func waitForRetry(ctx context.Context, delay time.Duration) error {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}