mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-10-14 06:39:35 +02:00
Optimized TLS list
- Optimized TLS certificate list - Added common name auto fill for certificate upload tool - Updated version number - Removed unused code in helper
This commit is contained in:
@@ -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)
|
||||
|
@@ -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 */
|
||||
|
@@ -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))
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -7,7 +7,12 @@
|
||||
.valid.certdate{
|
||||
color: #31c071;
|
||||
}
|
||||
|
||||
#certifiedDomainList .ui.basic.button{
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
</style>
|
||||
<script src="script/jsrsasign-all-min.js"></script>
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>TLS / SSL Certificates</h2>
|
||||
@@ -21,7 +26,7 @@
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>Server Name (Domain)</label>
|
||||
<input type="text" id="certdomain" placeholder="example.com / blog.example.com">
|
||||
<input type="text" id="certdomain" placeholder="">
|
||||
<small><i class="exclamation circle yellow icon"></i> Match the server name with your CN/DNS entry in certificate for faster resolve time</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -59,23 +64,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<p>Current list of loaded certificates</p>
|
||||
<div tourstep="certTable">
|
||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||
<div tourstep="certTable" style="width: 100%; overflow-x: auto; padding-bottom: 1em;">
|
||||
<div style="min-width: 960px; max-width: 100%; ">
|
||||
<table class="ui unstackable basic celled table">
|
||||
<thead>
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
<th>Expire At</th>
|
||||
<th>DNS Challenge</th>
|
||||
<th class="no-sort">Renew</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Filename</th>
|
||||
<th>Last Update</th>
|
||||
<th>Expire At</th>
|
||||
<th>Fallback</th>
|
||||
<th>DNS Challenge</th>
|
||||
<th class="no-sort">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody id="certifiedDomainList">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
@@ -103,6 +110,8 @@
|
||||
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
|
||||
<button class="ui basic button" onclick="uploadPrivateKey();"><i class="grey lock icon"></i> Private Key</button>
|
||||
</div>
|
||||
<div class="ui yellow message"><i class="exclamation triangle icon"></i> We will be removing the fallback certificate section soon. <br>
|
||||
Please use "<i class="ui blue home icon"></i>Set Fallback" button in the certificate list above to set the fallback certificate.</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div tourstep="acmeSettings">
|
||||
@@ -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(`<tr>
|
||||
<td><a style="cursor: pointer;" title="Download certificate" onclick="handleCertDownload('${entry.Domain}');">${entry.Domain}</a></td>
|
||||
<td><a style="cursor: pointer;" title="Download certificate" onclick="handleCertDownload('${entry.Filename}');">${entry.Domain}</a></td>
|
||||
<td>${entry.Filename}</td>
|
||||
<td>${entry.LastModifiedDate}</td>
|
||||
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
|
||||
<td>${entry.IsFallback?"<i class='green check icon'></i>":""}</td>
|
||||
<td><i class="${entry.UseDNS?"green check": "red times"} icon"></i></td>
|
||||
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entryDomainRenewKey}', '${entry.UseDNS}', this);"><i class="ui green refresh icon"></i></button></td>
|
||||
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
|
||||
<td>
|
||||
<button title="Set as Default / Fallback Certificate" class="ui mini basic button ${(entry.IsFallback?"disabled":"")} " onclick="setSelectedCertAsFallbackCertificate('${entry.Filename}');"><i class="ui blue home icon"></i> Set Fallback</button>
|
||||
<button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entryDomainRenewKey}', '${entry.UseDNS}', this);"><i class="ui green refresh icon"></i></button>
|
||||
<button title="Delete key-pair" class="ui mini basic icon red button" onclick="deleteCertificate('${entry.Filename}');"><i class="ui red trash icon"></i></button>
|
||||
</td>
|
||||
</tr>`);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user