Merge pull request #829 from jimmyGALLAND/fix-acme-renew

fix acme renew
This commit is contained in:
Toby Chui
2025-10-03 07:16:20 +08:00
committed by GitHub
3 changed files with 110 additions and 5 deletions

View File

@@ -432,6 +432,21 @@ func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Req
// to renew the certificate, and sends a JSON response indicating the result of the renewal process.
func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
domainPara, err := utils.PostPara(r, "domains")
//Clean each domain
cleanedDomains := []string{}
if (domainPara != "") {
for _, d := range strings.Split(domainPara, ",") {
// Apply normalization on each domain
nd, err := NormalizeDomain(d)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
cleanedDomains = append(cleanedDomains, nd)
}
}
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
@@ -492,7 +507,6 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
dns = true
}
domains := strings.Split(domainPara, ",")
// Default propagation timeout is 300 seconds
propagationTimeout := 300
@@ -511,12 +525,31 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
}
}
//Clean spaces in front or behind each domain
cleanedDomains := []string{}
for _, domain := range domains {
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
// Extract SANs from existing PEM to ensure all domains are included
pemPath := fmt.Sprintf("./conf/certs/%s.pem", filename)
sanDomains, err := ExtractDomainsFromPEM(pemPath)
if err == nil {
// Merge domainPara + SANs
domainSet := map[string]struct{}{}
for _, d := range cleanedDomains {
domainSet[d] = struct{}{}
}
for _, d := range sanDomains {
domainSet[d] = struct{}{}
}
// Rebuild cleanedDomains with all unique domains
cleanedDomains = []string{}
for d := range domainSet {
cleanedDomains = append(cleanedDomains, d)
}
a.Logf("Renewal domains including SANs from PEM: "+strings.Join(cleanedDomains, ","), nil)
} else {
a.Logf("Could not extract SANs from PEM, using domainPara only", err)
}
// Extract DNS servers from the request
var dnsServers []string
dnsServersPara, err := utils.PostPara(r, "dnsServers")

View File

@@ -397,6 +397,15 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
dnsServers = strings.Join(certInfo.DNSServers, ",")
}
// Extract SANs from the existing PEM to ensure all domains are included
sanDomains, errSan := ExtractDomainsFromPEM(expiredCert.Filepath)
if errSan == nil && len(sanDomains) > 0 {
expiredCert.Domains = sanDomains
a.Logf("Using SANs from PEM for renewal: "+strings.Join(sanDomains, ","), nil)
} else {
a.Logf("Could not extract SANs from PEM for "+fileName+", using original domains", errSan)
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout, dnsServers)
if err != nil {
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)

View File

@@ -7,6 +7,8 @@ import (
"fmt"
"os"
"time"
"strings"
"unicode"
)
// Get the issuer name from pem file
@@ -40,6 +42,8 @@ func ExtractDomains(certBytes []byte) ([]string, error) {
return []string{}, errors.New("decode cert bytes failed")
}
func ExtractIssuerName(certBytes []byte) (string, error) {
// Parse the PEM block
block, _ := pem.Decode(certBytes)
@@ -64,6 +68,20 @@ func ExtractIssuerName(certBytes []byte) (string, error) {
return issuer, nil
}
// ExtractDomainsFromPEM reads a PEM certificate file and returns all SANs
func ExtractDomainsFromPEM(pemFilePath string) ([]string, error) {
certBytes, err := os.ReadFile(pemFilePath)
if err != nil {
return nil, err
}
domains,err := ExtractDomains(certBytes)
if err != nil {
return nil, err
}
return domains, nil
}
// Check if a cert is expired by public key
func CertIsExpired(certBytes []byte) bool {
block, _ := pem.Decode(certBytes)
@@ -98,3 +116,48 @@ func CertExpireSoon(certBytes []byte, numberOfDays int) bool {
}
return false
}
// NormalizeDomain cleans and validates a domain string.
// - Trims spaces around the domain
// - Converts to lowercase
// - Removes trailing dot (FQDN canonicalization)
// - Checks that the domain conforms to standard rules:
// * Each label ≤ 63 characters
// * Only letters, digits, and hyphens
// * Labels do not start or end with a hyphen
// * Full domain ≤ 253 characters
// Returns an empty string if the domain is invalid.
func NormalizeDomain(d string) (string, error) {
d = strings.TrimSpace(d)
d = strings.ToLower(d)
d = strings.TrimSuffix(d, ".")
if len(d) == 0 {
return "", errors.New("domain is empty")
}
if len(d) > 253 {
return "", errors.New("domain exceeds 253 characters")
}
labels := strings.Split(d, ".")
for _, label := range labels {
if len(label) == 0 {
return "", errors.New("Domain '" + d + "' not valid: Empty label")
}
if len(label) > 63 {
return "", errors.New("Domain not valid: label exceeds 63 characters")
}
for i, r := range label {
if !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-') {
return "", errors.New("Domain '" + d + "' not valid: Invalid character '" + string(r) + "' in label")
}
if (i == 0 || i == len(label)-1) && r == '-' {
return "", errors.New("Domain '" + d + "' not valid: label '"+ label +"' starts or ends with hyphen")
}
}
}
return d, nil
}