123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599 |
- <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 tourstep="certTable">
- <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
- <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></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>
- <div tourstep="defaultCertificate">
- <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 button" onclick="uploadPrivateKey();"><i class="grey lock icon"></i> Private Key</button>
- </div>
- </div>
- <div class="ui divider"></div>
- <div tourstep="acmeSettings">
- <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" tourstep="openACMEManager" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
- </div>
- </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
- let buttonOriginalHTML = "";
- if (btn != undefined){
- buttonOriginalHTML = $(btn).html();
- $(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 ($(btn).hasClass("icon")){
- //Only change the button icon
- if (succ){
- $(btn).html(`<i class="ui green check icon"></i>`);
- }else{
- $(btn).html(`<i class="ui red times icon"></i>`);
- }
- }else{
- //Show error or success icon with text
- if (succ){
- $(btn).html(`<i class="ui green check icon"></i> Requested`);
- }else{
- $(btn).html(`<i class="ui red times icon"></i> Error`);
- }
- }
- //Restore the button after 3 seconds
- setTimeout(function(){
- $(btn).html(buttonOriginalHTML);
- }, 3000);
-
- 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 + " ?")){
- $.cjax({
- 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;
- }
- $.cjax({
- 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');
- }
- }
- });
- $.cjax({
- 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;
- let entryDomainRenewKey = entry.Domain;
- 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>${entry.LastModifiedDate}</td>
- <td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</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>
- </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();
- });
- }
- function handleCertDownload(certName){
- $.get("/api/cert/download?seek=true&certname=" + certName, function(data){
- if (data.error != undefined){
- //Error resolving certificate
- msgbox(data.error, false);
- }else{
- //Continue to download
- window.open("/api/cert/download?certname=" + certName);
- }
- });
- }
- //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();
- const csrfToken = document.querySelector('meta[name="zoraxy.csrf.Token"]').getAttribute("content");
- 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.setRequestHeader('X-CSRF-Token', csrfToken);
- 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.setRequestHeader('X-CSRF-Token', csrfToken);
- 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];
- if (ktype == "pub"){
- uploadPendingPublicKey = file;
- }else if (ktype == "pri"){
- uploadPendingPrivateKey = file;
- }
- }
- //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();
- const csrfToken = document.querySelector('meta[name="zoraxy.csrf.Token"]').getAttribute("content");
- // 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,
- headers: {
- 'X-CSRF-Token': csrfToken
- }
- })
- .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');
- const csrfToken = document.querySelector('meta[name="zoraxy.csrf.Token"]').getAttribute("content");
- 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,
- headers: {
- 'X-CSRF-Token': csrfToken
- }
- })
- .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>
|