mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-06 07:37:21 +02:00
553 lines
22 KiB
HTML
553 lines
22 KiB
HTML
<style>
|
|
.expired.certdate{
|
|
font-weight: bolder;
|
|
color: #bd001c;
|
|
}
|
|
|
|
.valid.certdate{
|
|
color: #31c071;
|
|
}
|
|
</style>
|
|
<div class="standardContainer">
|
|
<div class="ui basic segment">
|
|
<h2>TLS / SSL Certificates</h2>
|
|
<p>Setup TLS cert for different domains of your reverse proxy server names</p>
|
|
</div>
|
|
|
|
<div class="ui divider"></div>
|
|
<h3>Hosts Certificates</h3>
|
|
<p>Provide certificates for multiple domains reverse proxy</p>
|
|
<div class="ui fluid form">
|
|
<div class="three fields">
|
|
<div class="field">
|
|
<label>Server Name (Domain)</label>
|
|
<input type="text" id="certdomain" placeholder="example.com / blog.example.com">
|
|
<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">
|
|
<label>Public Key (.pem)</label>
|
|
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
|
|
<small>or .crt files in order systems</small>
|
|
</div>
|
|
<div class="field">
|
|
<label>Private Key (.key)</label>
|
|
<input type="file" id="prikeySelector" onchange="handleFileSelect(event, 'pri')">
|
|
</div>
|
|
</div>
|
|
<button class="ui basic button" onclick="handleDomainUploadByKeypress();"><i class="ui teal upload icon"></i> Upload</button><br>
|
|
<small>You have intermediate certificate? <a style="cursor:pointer;" onclick="showSideWrapper('snippet/intermediateCertConv.html');">Open Conversion Tool</a></small>
|
|
</div>
|
|
<div id="certUploadSuccMsg" class="ui green message" style="display:none;">
|
|
<i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
|
|
</div>
|
|
<div class="ui message">
|
|
<h4>Tips about Server Names & SNI</h4>
|
|
<div class="ui bulleted list">
|
|
<div class="item">
|
|
If you have two subdomains like <code>a.example.com</code> and <code>b.example.com</code> ,
|
|
for faster response speed, you might want to setup them one by one (i.e. having two seperate certificate for
|
|
<code>a.example.com</code> and <code>b.example.com</code>).
|
|
</div>
|
|
<div class="item">
|
|
If you have a wildcard certificate that covers <code>*.example.com</code>,
|
|
you can just enter <code>example.com</code> as server name to add a certificate.
|
|
</div>
|
|
<div class="item">
|
|
If you have a certificate contain multiple host, you can enter the first domain in your certificate
|
|
and Zoraxy will try to match the remaining CN/DNS for you.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p>Current list of loaded certificates</p>
|
|
<div>
|
|
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
|
<table class="ui sortable 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></thead>
|
|
<tbody id="certifiedDomainList">
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
|
</div>
|
|
<div class="ui divider"></div>
|
|
<h3>Fallback Certificate</h3>
|
|
<p>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</p>
|
|
<table class="ui very basic unstackable celled table">
|
|
<thead>
|
|
<tr><th class="no-sort">Key Type</th>
|
|
<th class="no-sort">Found</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><i class="globe icon"></i> Fallback Public Key</td>
|
|
<td id="pubkeyExists"></td>
|
|
</tr>
|
|
<tr>
|
|
<td><i class="lock icon"></i> Fallback Private Key</td>
|
|
<td id="prikeyExists"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
|
|
<div class="ui buttons">
|
|
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
|
|
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
|
|
</div>
|
|
<div class="ui divider"></div>
|
|
<h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
|
|
<p>Management features regarding CA and ACME</p>
|
|
<h4>Prefered Certificate Authority</h4>
|
|
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
|
|
<div class="ui fluid form">
|
|
<div class="field">
|
|
<label>Preferred CA</label>
|
|
<div class="ui selection dropdown" id="defaultCA">
|
|
<input type="hidden" name="defaultCA">
|
|
<i class="dropdown icon"></i>
|
|
<div class="default text">Let's Encrypt</div>
|
|
<div class="menu">
|
|
<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="ZeroSSL">ZeroSSL</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label>ACME Email</label>
|
|
<input id="prefACMEEmail" type="text" placeholder="ACME Email">
|
|
</div>
|
|
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
|
|
</div><br>
|
|
<h5>Certificate Renew / Generation (ACME) Settings</h5>
|
|
<div class="ui basic segment acmeRenewStateWrapper">
|
|
<h4 class="ui header" id="acmeAutoRenewer">
|
|
<i class="white remove icon"></i>
|
|
<div class="content">
|
|
<span id="acmeAutoRenewerStatus">Disabled</span>
|
|
<div class="sub header">ACME Auto-Renewer</div>
|
|
</div>
|
|
</h4>
|
|
</div>
|
|
<p>This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.</p>
|
|
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
|
|
</div>
|
|
<script>
|
|
var uploadPendingPublicKey = undefined;
|
|
var uploadPendingPrivateKey = undefined;
|
|
|
|
$("#defaultCA").dropdown();
|
|
|
|
|
|
//Renew certificate by button press
|
|
function renewCertificate(domain, dns, btn=undefined){
|
|
let defaultCA = $("#defaultCA").dropdown("get value");
|
|
if (defaultCA.trim() == ""){
|
|
defaultCA = "Let's Encrypt";
|
|
}
|
|
//Get a new cert using ACME
|
|
msgbox("Requesting certificate via " + defaultCA +"...");
|
|
|
|
//Request ACME for certificate
|
|
if (btn != undefined){
|
|
$(btn).addClass('disabled');
|
|
$(btn).html(`<i class="ui loading spinner icon"></i>`);
|
|
}
|
|
obtainCertificate(domain, dns, defaultCA.trim(), function(succ){
|
|
if (btn != undefined){
|
|
$(btn).removeClass('disabled');
|
|
if (succ){
|
|
$(btn).html(`<i class="ui green check icon"></i>`);
|
|
}else{
|
|
$(btn).html(`<i class="ui red times icon"></i>`);
|
|
}
|
|
|
|
setTimeout(function(){
|
|
initManagedDomainCertificateList();
|
|
}, 3000);
|
|
}
|
|
});
|
|
}
|
|
|
|
/*
|
|
Obtain Certificate via ACME
|
|
*/
|
|
|
|
// Obtain certificate from API, only support one domain
|
|
function obtainCertificate(domains, dns, usingCa = "Let's Encrypt", callback=undefined) {
|
|
//Load the ACME email from server side
|
|
let acmeEmail = "";
|
|
$.get("/api/acme/autoRenew/email", function(data){
|
|
if (data != "" && data != undefined && data != null){
|
|
acmeEmail = data;
|
|
}
|
|
|
|
let filename = "";
|
|
let email = acmeEmail;
|
|
if (acmeEmail == ""){
|
|
msgbox("Unable to obtain certificate: ACME email not set", false, 8000);
|
|
if (callback != undefined){
|
|
callback(false);
|
|
}
|
|
return;
|
|
}
|
|
if (filename.trim() == "" && !domains.includes(",")){
|
|
//Zoraxy filename are the matching name for domains.
|
|
//Use the same as domains
|
|
filename = domains;
|
|
}else if (filename != "" && !domains.includes(",")){
|
|
//Invalid settings. Force the filename to be same as domain
|
|
//if there are only 1 domain
|
|
filename = domains;
|
|
}else{
|
|
msgbox("Filename cannot be empty for certs containing multiple domains.")
|
|
if (callback != undefined){
|
|
callback(false);
|
|
}
|
|
return;
|
|
}
|
|
//Filename cannot contain wildcards, and wildcards are possible with DNS challenges
|
|
filename = filename.replace("*", "_");
|
|
|
|
$.ajax({
|
|
url: "/api/acme/obtainCert",
|
|
method: "GET",
|
|
data: {
|
|
domains: domains,
|
|
filename: filename,
|
|
email: email,
|
|
ca: usingCa,
|
|
dns: dns
|
|
},
|
|
success: function(response) {
|
|
if (response.error) {
|
|
console.log("Error:", response.error);
|
|
// Show error message
|
|
msgbox(response.error, false, 12000);
|
|
if (callback != undefined){
|
|
callback(false);
|
|
}
|
|
} else {
|
|
console.log("Certificate installed successfully");
|
|
// Show success message
|
|
msgbox("Certificate installed successfully");
|
|
|
|
if (callback != undefined){
|
|
callback(true);
|
|
}
|
|
}
|
|
},
|
|
error: function(error) {
|
|
console.log("Failed to install certificate:", error);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
//Delete the certificate by its domain
|
|
function deleteCertificate(domain){
|
|
if (confirm("Confirm delete certificate for " + domain + " ?")){
|
|
$.ajax({
|
|
url: "/api/cert/delete",
|
|
method: "POST",
|
|
data: {domain: domain},
|
|
success: function(data){
|
|
if (data.error != undefined){
|
|
msgbox(data.error, false, 5000);
|
|
}else{
|
|
initManagedDomainCertificateList();
|
|
initDefaultKeypairCheck();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
function initAcmeStatus(){
|
|
//Initialize the current default CA options
|
|
$.get("/api/acme/autoRenew/email", function(data){
|
|
$("#prefACMEEmail").val(data);
|
|
if (data.trim() == ""){
|
|
//acme email is not yet set
|
|
$(".renewButton").addClass('disabled');
|
|
}else{
|
|
$(".renewButton").removeClass('disabled');
|
|
}
|
|
});
|
|
|
|
$.get("/api/acme/autoRenew/ca", function(data){
|
|
$("#defaultCA").dropdown("set value", data);
|
|
});
|
|
|
|
$.get("/api/acme/autoRenew/enable", function(data){
|
|
setACMEEnableStates(data);
|
|
})
|
|
}
|
|
//Set the status of the acme enable icon
|
|
function setACMEEnableStates(enabled){
|
|
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
|
|
if (enabled){
|
|
$(".acmeRenewStateWrapper").addClass("enabled");
|
|
}else{
|
|
$(".acmeRenewStateWrapper").removeClass("enabled");
|
|
}
|
|
|
|
$("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
|
|
}
|
|
initAcmeStatus();
|
|
|
|
function saveDefaultCA(){
|
|
let newDefaultEmail = $("#prefACMEEmail").val().trim();
|
|
let newDefaultCA = $("#defaultCA").dropdown("get value");
|
|
|
|
if (newDefaultEmail == ""){
|
|
msgbox("Invalid acme email given", false);
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: "/api/acme/autoRenew/email",
|
|
method: "POST",
|
|
data: {"set": newDefaultEmail},
|
|
success: function(data){
|
|
if (data.error != undefined){
|
|
msgbox(data.error, false);
|
|
}else{
|
|
//Update the renew button states
|
|
$(".renewButton").removeClass('disabled');
|
|
}
|
|
}
|
|
});
|
|
|
|
$.ajax({
|
|
url: "/api/acme/autoRenew/ca",
|
|
data: {"set": newDefaultCA},
|
|
method: "POST",
|
|
success: function(data){
|
|
if (data.error != undefined){
|
|
msgbox(data.error, false);
|
|
}
|
|
}
|
|
});
|
|
|
|
msgbox("Settings updated");
|
|
|
|
}
|
|
|
|
//List the stored certificates
|
|
function initManagedDomainCertificateList(){
|
|
$.get("/api/cert/list?date=true", function(data){
|
|
if (data.error != undefined){
|
|
msgbox(data.error, false, 5000);
|
|
}else{
|
|
$("#certifiedDomainList").html("");
|
|
data.sort((a,b) => {
|
|
return a.Domain > b.Domain
|
|
});
|
|
data.forEach(entry => {
|
|
let isExpired = entry.RemainingDays <= 0;
|
|
|
|
$("#certifiedDomainList").append(`<tr>
|
|
<td>${entry.Domain}</td>
|
|
<td>${entry.LastModifiedDate}</td>
|
|
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
|
|
<td><i class="${entry.DNS?"green check": "red times"} circle outline icon"></i></td>
|
|
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entry.Domain}', '${entry.DNS}', 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>
|
|
</tr>`);
|
|
});
|
|
|
|
if (data.length == 0){
|
|
$("#certifiedDomainList").append(`<tr>
|
|
<td colspan="4"><i class="ui times red circle icon"></i> No valid keypairs found</td>
|
|
</tr>`);
|
|
}
|
|
}
|
|
})
|
|
}
|
|
initManagedDomainCertificateList();
|
|
|
|
function openACMEManager(){
|
|
showSideWrapper('snippet/acme.html');
|
|
}
|
|
|
|
function handleDomainUploadByKeypress(){
|
|
handleDomainKeysUpload(function(){
|
|
$("#certUploadingDomain").text($("#certdomain").val().trim());
|
|
//After uploaded, reset the file selector
|
|
document.getElementById('pubkeySelector').value = '';
|
|
document.getElementById('prikeySelector').value = '';
|
|
document.getElementById('certdomain').value = '';
|
|
|
|
uploadPendingPublicKey = undefined;
|
|
uploadPendingPrivateKey = undefined;
|
|
|
|
//Show succ
|
|
$("#certUploadSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
|
initManagedDomainCertificateList();
|
|
});
|
|
}
|
|
//Handle domain keys upload
|
|
function handleDomainKeysUpload(callback=undefined){
|
|
let domain = $("#certdomain").val();
|
|
if (domain.trim() == ""){
|
|
msgbox("Missing domain", false, 5000);
|
|
return;
|
|
}
|
|
if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
|
|
const publicKeyForm = new FormData();
|
|
publicKeyForm.append('file', uploadPendingPublicKey, 'publicKey');
|
|
|
|
const privateKeyForm = new FormData();
|
|
privateKeyForm.append('file', uploadPendingPrivateKey, 'privateKey');
|
|
|
|
const publicKeyRequest = new XMLHttpRequest();
|
|
publicKeyRequest.open('POST', '/api/cert/upload?ktype=pub&domain=' + domain);
|
|
publicKeyRequest.onreadystatechange = function() {
|
|
if (publicKeyRequest.readyState === XMLHttpRequest.DONE) {
|
|
if (publicKeyRequest.status !== 200) {
|
|
msgbox('Error uploading public key: ' + publicKeyRequest.statusText, false, 5000);
|
|
}
|
|
|
|
if (callback != undefined){
|
|
callback();
|
|
}
|
|
|
|
}
|
|
};
|
|
publicKeyRequest.send(publicKeyForm);
|
|
|
|
const privateKeyRequest = new XMLHttpRequest();
|
|
privateKeyRequest.open('POST', '/api/cert/upload?ktype=pri&domain=' + domain);
|
|
privateKeyRequest.onreadystatechange = function() {
|
|
if (privateKeyRequest.readyState === XMLHttpRequest.DONE) {
|
|
if (privateKeyRequest.status !== 200) {
|
|
msgbox('Error uploading private key: ' + privateKeyRequest.statusText, false, 5000);
|
|
}
|
|
if (callback != undefined){
|
|
callback();
|
|
}
|
|
}
|
|
};
|
|
privateKeyRequest.send(privateKeyForm);
|
|
} else {
|
|
msgbox('One or both of the files is missing or not a file object');
|
|
}
|
|
}
|
|
|
|
//Handlers for selecting domain based key pairs
|
|
//ktype = {"pub" / "pri"}
|
|
function handleFileSelect(event, ktype="pub") {
|
|
const file = event.target.files[0];
|
|
//const fileNameInput = document.getElementById('selected-file-name');
|
|
if (ktype == "pub"){
|
|
uploadPendingPublicKey = file;
|
|
}else if (ktype == "pri"){
|
|
uploadPendingPrivateKey = file;
|
|
}
|
|
|
|
|
|
//fileNameInput.value = file.name;
|
|
}
|
|
|
|
//Check if the default keypairs exists
|
|
function initDefaultKeypairCheck(){
|
|
$.get("/api/cert/checkDefault", function(data){
|
|
let tick = `<i class="ui green checkmark icon"></i>`;
|
|
let cross = `<i class="ui red times icon"></i>`;
|
|
$("#pubkeyExists").html(data.DefaultPubExists?tick:cross);
|
|
$("#prikeyExists").html(data.DefaultPriExists?tick:cross);
|
|
});
|
|
}
|
|
initDefaultKeypairCheck();
|
|
|
|
function uploadPrivateKey(){
|
|
// create file input element
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
|
|
// add change listener to file input
|
|
input.addEventListener('change', () => {
|
|
// create form data object
|
|
const formData = new FormData();
|
|
|
|
// add selected file to form data
|
|
formData.append('file', input.files[0]);
|
|
|
|
// send form data to server
|
|
fetch('/api/cert/upload?ktype=pri', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => {
|
|
initDefaultKeypairCheck();
|
|
if (response.ok) {
|
|
msgbox('File upload successful!');
|
|
} else {
|
|
response.text().then(text => {
|
|
msgbox(text, false, 5000);
|
|
});
|
|
//console.log(response.text());
|
|
//alert('File upload failed!');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
msgbox('An error occurred while uploading the file.', false, 5000);
|
|
console.error(error);
|
|
});
|
|
});
|
|
|
|
// click file input to open file selector
|
|
input.click();
|
|
}
|
|
|
|
function uploadPublicKey() {
|
|
// create file input element
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
|
|
// add change listener to file input
|
|
input.addEventListener('change', () => {
|
|
// create form data object
|
|
const formData = new FormData();
|
|
|
|
// add selected file to form data
|
|
formData.append('file', input.files[0]);
|
|
|
|
// send form data to server
|
|
fetch('/api/cert/upload?ktype=pub', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
msgbox('File upload successful!');
|
|
initDefaultKeypairCheck();
|
|
} else {
|
|
response.text().then(text => {
|
|
msgbox(text, false, 5000);
|
|
});
|
|
//console.log(response.text());
|
|
//alert('File upload failed!');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
msgbox('An error occurred while uploading the file.', false, 5000);
|
|
console.error(error);
|
|
});
|
|
});
|
|
|
|
// click file input to open file selector
|
|
input.click();
|
|
}
|
|
</script> |