zoraxy/src/web/components/cert.html
2024-02-14 22:52:56 +08:00

413 lines
16 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>
<h4>Default Certificates</h4>
<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">Exists</th>
</tr></thead>
<tbody>
<tr>
<td><i class="globe icon"></i> Default Public Key</td>
<td id="pubkeyExists"></td>
</tr>
<tr>
<td><i class="lock icon"></i> Default 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>
<h4>Sub-domain Certificates</h4>
<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">
</div>
<div class="field">
<label>Public Key (.pem)</label>
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
</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>
<br>
<div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui sortable unstackable celled table">
<thead>
<tr><th>Domain</th>
<th>Last Update</th>
<th>Expire At</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 message">
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
If you have 3rd or even 4th level subdomains like <code>blog.example.com</code> or <code>en.blog.example.com</code> ,
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
</div>
<div class="ui divider"></div>
<h4>Certificate Authority (CA) and Auto Renew (ACME)</h4>
<p>Management features regarding CA and ACME</p>
<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">
<h4 class="ui header" id="acmeAutoRenewer">
<i class="red circle icon"></i>
<div class="content">
<span id="acmeAutoRenewerStatus">Disabled</span>
<div class="sub header">Auto-Renewer Status</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();
//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);
});
$.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");
$("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle 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);
}
}
});
$.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><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 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>