diff --git a/src/mod/acme/acme.go b/src/mod/acme/acme.go index ec8bc1c..2e87cd1 100644 --- a/src/mod/acme/acme.go +++ b/src/mod/acme/acme.go @@ -8,7 +8,6 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" - "errors" "fmt" "io/ioutil" "log" @@ -28,6 +27,11 @@ import ( "imuslab.com/zoraxy/mod/utils" ) +type CertificateInfoJSON struct { + AcmeName string `json:"acme_name"` + AcmeUrl string `json:"acme_url"` +} + // ACMEUser represents a user in the ACME system. type ACMEUser struct { Email string @@ -65,7 +69,7 @@ func NewACME(acmeServer string, port string) *ACMEHandler { } // ObtainCert obtains a certificate for the specified domains. -func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, ca string) (bool, error) { +func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string) (bool, error) { log.Println("[ACME] Obtaining certificate...") // generate private key @@ -84,17 +88,23 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email // create config config := lego.NewConfig(&adminUser) - // setup who is the issuer and the key type - config.CADirURL = a.DefaultAcmeServer + // setup the custom ACME url endpoint. + if caUrl != "" { + config.CADirURL = caUrl + } - //Overwrite the CADir URL if set - if ca != "" { - caLinkOverwrite, err := loadCAApiServerFromName(ca) + // if not custom ACME url, load it from ca.json + if caName == "custom" { + log.Println("[INFO] Using Custom ACME" + caUrl + " for CA Directory URL") + } else { + caLinkOverwrite, err := loadCAApiServerFromName(caName) if err == nil { config.CADirURL = caLinkOverwrite log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL") } else { - return false, errors.New("CA " + ca + " is not supported. Please contribute to the source code and add this CA's directory link.") + // (caName == "" || caUrl == "") will use default acme + config.CADirURL = a.DefaultAcmeServer + log.Println("[INFO] Using Default ACME" + a.DefaultAcmeServer + " for CA Directory URL") } } @@ -145,6 +155,24 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email return false, err } + // Save certificate's ACME info for renew usage + certInfo := &CertificateInfoJSON{ + AcmeName: caName, + AcmeUrl: caUrl, + } + + certInfoBytes, err := json.Marshal(certInfo) + if err != nil { + log.Println(err) + return false, err + } + + err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777) + if err != nil { + log.Println(err) + return false, err + } + return true, nil } @@ -250,14 +278,24 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ return } + var caUrl string + ca, err := utils.PostPara(r, "ca") if err != nil { - log.Println("CA not set. Using default (Let's Encrypt)") - ca = "Let's Encrypt" + log.Println("CA not set. Using default") + ca, caUrl = "", "" + } + + if ca == "custom" { + caUrl, err = utils.PostPara(r, "ca_url") + if err != nil { + log.Println("Custom CA set but no URL provide, Using default") + ca, caUrl = "", "" + } } domains := strings.Split(domainPara, ",") - result, err := a.ObtainCert(domains, filename, email, ca) + result, err := a.ObtainCert(domains, filename, email, ca, caUrl) if err != nil { utils.SendErrorResponse(w, jsonEscape(err.Error())) return @@ -285,4 +323,20 @@ func IsPortInUse(port int) bool { } defer listener.Close() return false // Port is not in use + +} + +func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) { + + certInfoBytes, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + certInfo := &CertificateInfoJSON{} + if err = json.Unmarshal(certInfoBytes, certInfo); err != nil { + return nil, err + } + + return certInfo, nil } diff --git a/src/mod/acme/autorenew.go b/src/mod/acme/autorenew.go index 211e168..6ed7433 100644 --- a/src/mod/acme/autorenew.go +++ b/src/mod/acme/autorenew.go @@ -3,6 +3,7 @@ package acme import ( "encoding/json" "errors" + "fmt" "log" "net/http" "net/mail" @@ -355,7 +356,16 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)") fileName := filepath.Base(expiredCert.Filepath) certName := fileName[:len(fileName)-len(filepath.Ext(fileName))] - _, err := a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, expiredCert.CA) + + // Load certificate info for ACME detail + certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName) + certInfo, err := loadCertInfoJSON(certInfoFilename) + if err != nil { + log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, using default ACME", certName, err) + certInfo = &CertificateInfoJSON{} + } + + _, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl) if err != nil { log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error()) } else {