Merge pull request #48 from daluntw/dev-custom-acme

Add custom ACME server feature in backend
This commit is contained in:
Toby Chui 2023-08-22 14:05:42 +08:00 committed by GitHub
commit dce58343db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 97 additions and 12 deletions

View File

@ -8,7 +8,6 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -28,6 +27,11 @@ import (
"imuslab.com/zoraxy/mod/utils" "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. // ACMEUser represents a user in the ACME system.
type ACMEUser struct { type ACMEUser struct {
Email string Email string
@ -65,7 +69,7 @@ func NewACME(acmeServer string, port string) *ACMEHandler {
} }
// ObtainCert obtains a certificate for the specified domains. // 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...") log.Println("[ACME] Obtaining certificate...")
// generate private key // generate private key
@ -84,17 +88,23 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// create config // create config
config := lego.NewConfig(&adminUser) config := lego.NewConfig(&adminUser)
// setup who is the issuer and the key type // setup the custom ACME url endpoint.
config.CADirURL = a.DefaultAcmeServer if caUrl != "" {
config.CADirURL = caUrl
}
//Overwrite the CADir URL if set // if not custom ACME url, load it from ca.json
if ca != "" { if caName == "custom" {
caLinkOverwrite, err := loadCAApiServerFromName(ca) log.Println("[INFO] Using Custom ACME " + caUrl + " for CA Directory URL")
} else {
caLinkOverwrite, err := loadCAApiServerFromName(caName)
if err == nil { if err == nil {
config.CADirURL = caLinkOverwrite config.CADirURL = caLinkOverwrite
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL") log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
} else { } 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 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 return true, nil
} }
@ -250,14 +278,24 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
return return
} }
var caUrl string
ca, err := utils.PostPara(r, "ca") ca, err := utils.PostPara(r, "ca")
if err != nil { if err != nil {
log.Println("CA not set. Using default (Let's Encrypt)") log.Println("CA not set. Using default")
ca = "Let's Encrypt" 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, ",") domains := strings.Split(domainPara, ",")
result, err := a.ObtainCert(domains, filename, email, ca) result, err := a.ObtainCert(domains, filename, email, ca, caUrl)
if err != nil { if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error())) utils.SendErrorResponse(w, jsonEscape(err.Error()))
return return
@ -285,4 +323,20 @@ func IsPortInUse(port int) bool {
} }
defer listener.Close() defer listener.Close()
return false // Port is not in use 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
} }

View File

@ -3,6 +3,7 @@ package acme
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"log" "log"
"net/http" "net/http"
"net/mail" "net/mail"
@ -355,7 +356,16 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)") log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
fileName := filepath.Base(expiredCert.Filepath) fileName := filepath.Base(expiredCert.Filepath)
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))] 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 { if err != nil {
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error()) log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
} else { } else {

View File

@ -109,10 +109,15 @@
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div> <div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div> <div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div> <div class="item" data-value="ZeroSSL">ZeroSSL</div>
<div class="item" data-value="Custom ACME Server">Custom ACME Server</div>
<!-- <div class="item" data-value="Google">Google</div> --> <!-- <div class="item" data-value="Google">Google</div> -->
</div> </div>
</div> </div>
</div> </div>
<div class="field" id="customca" style="display:none;">
<label>ACME Server URL</label>
<input id="caurl" type="text" placeholder="https://example.com/acme/dictionary">
</div>
<button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Renew Certificate</button> <button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Renew Certificate</button>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
@ -295,6 +300,14 @@
obtainCertificate(); obtainCertificate();
}); });
$("input[name=ca]").on('change', function() {
if(this.value == "Custom ACME Server") {
$("#customca").show();
} else {
$("#customca").hide();
}
})
// Obtain certificate from API // Obtain certificate from API
function obtainCertificate() { function obtainCertificate() {
var domains = $("#domainsInput").val(); var domains = $("#domainsInput").val();
@ -316,7 +329,14 @@
parent.msgbox("Filename cannot be empty for certs containing multiple domains.") parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
return; return;
} }
var ca = $("#ca").dropdown("get value"); var ca = $("#ca").dropdown("get value");
var ca_url = "";
if (ca == "Custom ACME Server") {
ca = "custom";
ca_url = $("#caurl").val();
}
$.ajax({ $.ajax({
url: "/api/acme/obtainCert", url: "/api/acme/obtainCert",
method: "GET", method: "GET",
@ -325,6 +345,7 @@
filename: filename, filename: filename,
email: email, email: email,
ca: ca, ca: ca,
ca_url: ca_url,
}, },
success: function(response) { success: function(response) {
$("#obtainButton").removeClass("loading").removeClass("disabled"); $("#obtainButton").removeClass("loading").removeClass("disabled");