From 66572981b388ab5e36735c56f3476a07b472ddce Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Mon, 13 Oct 2025 21:09:29 +0800 Subject: [PATCH] Optimized TLS list - Optimized TLS certificate list - Added common name auto fill for certificate upload tool - Updated version number - Removed unused code in helper --- src/api.go | 2 + src/def.go | 2 +- src/mod/tlscert/handler.go | 99 +++++++++++++++++++++++++++++-- src/mod/tlscert/helper.go | 19 ++---- src/web/components/cert.html | 110 +++++++++++++++++++++++++++++------ 5 files changed, 195 insertions(+), 37 deletions(-) diff --git a/src/api.go b/src/api.go index 27fc773..d4af088 100644 --- a/src/api.go +++ b/src/api.go @@ -79,6 +79,8 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate) //Certificate store functions + authRouter.HandleFunc("/api/cert/setDefault", tlsCertManager.SetCertAsDefault) + authRouter.HandleFunc("/api/cert/getCommonName", tlsCertManager.HandleGetCertCommonName) authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload) authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload) authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate) diff --git a/src/def.go b/src/def.go index 8f7e0cb..01a5909 100644 --- a/src/def.go +++ b/src/def.go @@ -44,7 +44,7 @@ import ( const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" - SYSTEM_VERSION = "3.2.7" + SYSTEM_VERSION = "3.2.8" DEVELOPMENT_BUILD = false /* System Constants */ diff --git a/src/mod/tlscert/handler.go b/src/mod/tlscert/handler.go index 5e9cc7b..099c6a3 100644 --- a/src/mod/tlscert/handler.go +++ b/src/mod/tlscert/handler.go @@ -75,6 +75,50 @@ func (m *Manager) HandleCertDownload(w http.ResponseWriter, r *http.Request) { } } +// Set the selected certificate as the default / fallback certificate +func (m *Manager) SetCertAsDefault(w http.ResponseWriter, r *http.Request) { + certname, err := utils.PostPara(r, "certname") + if err != nil { + utils.SendErrorResponse(w, "invalid certname given") + return + } + + //Check if the previous default cert exists. If yes, get its hostname from cert contents + defaultPubKey := filepath.Join(m.CertStore, "default.key") + defaultPriKey := filepath.Join(m.CertStore, "default.pem") + if utils.FileExists(defaultPubKey) && utils.FileExists(defaultPriKey) { + //Move the existing default cert to its original name + certBytes, err := os.ReadFile(defaultPriKey) + if err == nil { + block, _ := pem.Decode(certBytes) + if block != nil { + cert, err := x509.ParseCertificate(block.Bytes) + if err == nil { + os.Rename(defaultPubKey, filepath.Join(m.CertStore, domainToFilename(cert.Subject.CommonName, "key"))) + os.Rename(defaultPriKey, filepath.Join(m.CertStore, domainToFilename(cert.Subject.CommonName, "pem"))) + } + } + } + } + + //Check if the cert exists + certname = filepath.Base(certname) //prevent path escape + pubKey := filepath.Join(filepath.Join(m.CertStore), certname+".key") + priKey := filepath.Join(filepath.Join(m.CertStore), certname+".pem") + if utils.FileExists(pubKey) && utils.FileExists(priKey) { + os.Rename(pubKey, filepath.Join(m.CertStore, "default.key")) + os.Rename(priKey, filepath.Join(m.CertStore, "default.pem")) + utils.SendOK(w) + + //Update cert list + m.UpdateLoadedCertList() + + } else { + utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store") + return + } +} + // Handle upload of the certificate func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) { // check if request method is POST @@ -124,6 +168,13 @@ func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) { defer file.Close() // create file in upload directory + // Read file contents for validation + fileBytes, err := io.ReadAll(file) + if err != nil { + http.Error(w, "Failed to read file", http.StatusBadRequest) + return + } + os.MkdirAll(m.CertStore, 0775) f, err := os.Create(filepath.Join(m.CertStore, overWriteFilename)) if err != nil { @@ -138,6 +189,11 @@ func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to save file", http.StatusInternalServerError) return } + _, err = f.Write(fileBytes) + if err != nil { + http.Error(w, "Failed to save file", http.StatusInternalServerError) + return + } //Update cert list m.UpdateLoadedCertList() @@ -215,11 +271,13 @@ func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request) showDate, _ := utils.GetBool(r, "date") if showDate { type CertInfo struct { - Domain string + Domain string // Domain name of the certificate + Filename string // Filename that stores the certificate LastModifiedDate string ExpireDate string RemainingDays int - UseDNS bool + UseDNS bool // Whether this cert is obtained via DNS challenge + IsFallback bool // Whether this cert is the fallback/default cert } results := []*CertInfo{} @@ -248,7 +306,7 @@ func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request) if err == nil { certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05") - duration := cert.NotAfter.Sub(time.Now()) + duration := time.Until(cert.NotAfter) // Convert the duration to days expiredIn = int(duration.Hours() / 24) @@ -262,12 +320,23 @@ func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request) useDNSValidation = certInfo.UseDNS } + certDomain := "" + block, _ := pem.Decode(certBtyes) + if block != nil { + cert, err := x509.ParseCertificate(block.Bytes) + if err == nil { + certDomain = cert.Subject.CommonName + } + } + thisCertInfo := CertInfo{ - Domain: filename, + Domain: certDomain, + Filename: filename, LastModifiedDate: modifiedTime, ExpireDate: certExpireTime, RemainingDays: expiredIn, UseDNS: useDNSValidation, + IsFallback: (filename == "default"), //TODO: figure out a better implementation } results = append(results, &thisCertInfo) @@ -350,3 +419,25 @@ func (m *Manager) HandleSelfSignCertGenerate(w http.ResponseWriter, r *http.Requ } utils.SendOK(w) } + +// Extract the common name from a PEM encoded certificate +func (m *Manager) HandleGetCertCommonName(w http.ResponseWriter, r *http.Request) { + certContents, err := utils.PostPara(r, "cert") + if err != nil { + utils.SendErrorResponse(w, "Certificate content not provided") + return + } + block, _ := pem.Decode([]byte(certContents)) + if block == nil { + utils.SendErrorResponse(w, "Failed to decode PEM block") + return + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + utils.SendErrorResponse(w, "Failed to parse certificate: "+err.Error()) + return + } + + js, _ := json.Marshal(cert.Subject.CommonName) + utils.SendJSONResponse(w, string(js)) +} diff --git a/src/mod/tlscert/helper.go b/src/mod/tlscert/helper.go index 0704723..31edfa6 100644 --- a/src/mod/tlscert/helper.go +++ b/src/mod/tlscert/helper.go @@ -29,21 +29,6 @@ func getCertPairs(certFiles []string) []string { return result } -// Get the cloest subdomain certificate from a list of domains -func matchClosestDomainCertificate(subdomain string, domains []string) string { - var matchingDomain string = "" - maxLength := 0 - - for _, domain := range domains { - if strings.HasSuffix(subdomain, "."+domain) && len(domain) > maxLength { - matchingDomain = domain - maxLength = len(domain) - } - } - - return matchingDomain -} - // Convert a domain name to a filename format func domainToFilename(domain string, ext string) string { // Replace wildcard '*' with '_' @@ -52,6 +37,10 @@ func domainToFilename(domain string, ext string) string { domain = "_" + strings.TrimPrefix(domain, "*") } + if strings.HasPrefix(".", ext) { + ext = strings.TrimPrefix(ext, ".") + } + // Add .pem extension ext = strings.TrimPrefix(ext, ".") // Ensure ext does not start with a dot return domain + "." + ext diff --git a/src/web/components/cert.html b/src/web/components/cert.html index bb35c34..7063b29 100644 --- a/src/web/components/cert.html +++ b/src/web/components/cert.html @@ -7,7 +7,12 @@ .valid.certdate{ color: #31c071; } + + #certifiedDomainList .ui.basic.button{ + margin-top: 0.1em; + } +

TLS / SSL Certificates

@@ -21,7 +26,7 @@
- + Match the server name with your CN/DNS entry in certificate for faster resolve time
@@ -59,23 +64,25 @@

Current list of loaded certificates

-
-
+
+
- - - - - - + + + + + + + +
DomainLast UpdateExpire AtDNS ChallengeRenewRemove
DomainFilenameLast UpdateExpire AtFallbackDNS ChallengeActions
- +
@@ -103,6 +110,8 @@
+
We will be removing the fallback certificate section soon.
+ Please use "Set Fallback" button in the certificate list above to set the fallback certificate.
@@ -150,6 +159,58 @@ $("#defaultCA").dropdown(); + function getPossibleCommonNameFromSelectedCertificate(){ + const fileInput = document.getElementById('pubkeySelector'); + const file = fileInput.files[0]; + if (!file) { + msgbox("No certificate file selected", false, 4000); + return; + } + const reader = new FileReader(); + reader.onload = function(e) { + const certContent = e.target.result; + $.cjax({ + url: '/api/cert/getCommonName', + method: 'POST', + data: { cert: certContent }, + success: function(data) { + if (data.error !== undefined) { + //Ignore error + $("#certdomain").attr("placeholder", ""); + } else if (data) { + if (typeof data === "string" && data.startsWith("*.")) { + data = data.substring(2); + } + $("#certdomain").attr("placeholder", data); + } + }, + error: function(xhr) { + //Ignore error + } + }); + }; + reader.readAsText(file); + } + + function setSelectedCertAsFallbackCertificate(certDomain){ + $.cjax({ + url: '/api/cert/setDefault', + method: 'POST', + data: { certname: certDomain }, + success: function(data) { + if (data.error !== undefined) { + msgbox(data.error, false, 5000); + } else { + msgbox('Fallback certificate set successfully!'); + initManagedDomainCertificateList(); + initDefaultKeypairCheck(); + } + }, + error: function(xhr) { + msgbox('Failed to set fallback certificate', false, 5000); + } + }); + } //Renew certificate by button press function renewCertificate(domain, dns, btn=undefined){ @@ -378,17 +439,22 @@ }); data.forEach(entry => { let isExpired = entry.RemainingDays <= 0; - let entryDomainRenewKey = entry.Domain; + let entryDomainRenewKey = entry.Filename; if (entryDomainRenewKey.includes("_.")){ entryDomainRenewKey = entryDomainRenewKey.replace("_.","*."); } $("#certifiedDomainList").append(` - ${entry.Domain} + ${entry.Domain} + ${entry.Filename} ${entry.LastModifiedDate} ${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"}) + ${entry.IsFallback?"":""} - - + + + + + `); }); @@ -413,7 +479,7 @@ document.getElementById('pubkeySelector').value = ''; document.getElementById('prikeySelector').value = ''; document.getElementById('certdomain').value = ''; - + $("#certdomain").attr("placeholder", ""); uploadPendingPublicKey = undefined; uploadPendingPrivateKey = undefined; @@ -439,8 +505,17 @@ function handleDomainKeysUpload(callback=undefined){ let domain = $("#certdomain").val(); if (domain.trim() == ""){ - msgbox("Missing domain", false, 5000); - return; + //Check if placeholder has value + if ($("#certdomain").attr("placeholder").trim() != ""){ + domain = $("#certdomain").attr("placeholder").trim(); + }else{ + domain = undefined; + } + + if (domain == undefined || domain.trim() == "") { + msgbox("Missing domain", false, 5000); + return; + } } if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') { const publicKeyForm = new FormData(); @@ -493,6 +568,7 @@ const file = event.target.files[0]; if (ktype == "pub"){ uploadPendingPublicKey = file; + getPossibleCommonNameFromSelectedCertificate(); }else if (ktype == "pri"){ uploadPendingPrivateKey = file; }