cert.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <style>
  2. .expired.certdate{
  3. font-weight: bolder;
  4. color: #bd001c;
  5. }
  6. .valid.certdate{
  7. color: #31c071;
  8. }
  9. </style>
  10. <div class="standardContainer">
  11. <div class="ui basic segment">
  12. <h2>TLS / SSL Certificates</h2>
  13. <p>Setup TLS cert for different domains of your reverse proxy server names</p>
  14. </div>
  15. <div class="ui divider"></div>
  16. <h3>Hosts Certificates</h3>
  17. <p>Provide certificates for multiple domains reverse proxy</p>
  18. <div class="ui fluid form">
  19. <div class="three fields">
  20. <div class="field">
  21. <label>Server Name (Domain)</label>
  22. <input type="text" id="certdomain" placeholder="example.com / blog.example.com">
  23. <small><i class="exclamation circle yellow icon"></i> Match the server name with your CN/DNS entry in certificate for faster resolve time</small>
  24. </div>
  25. <div class="field">
  26. <label>Public Key (.pem)</label>
  27. <input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
  28. <small>or .crt files in order systems</small>
  29. </div>
  30. <div class="field">
  31. <label>Private Key (.key)</label>
  32. <input type="file" id="prikeySelector" onchange="handleFileSelect(event, 'pri')">
  33. </div>
  34. </div>
  35. <button class="ui basic button" onclick="handleDomainUploadByKeypress();"><i class="ui teal upload icon"></i> Upload</button><br>
  36. <small>You have intermediate certificate? <a style="cursor:pointer;" onclick="showSideWrapper('snippet/intermediateCertConv.html');">Open Conversion Tool</a></small>
  37. </div>
  38. <div id="certUploadSuccMsg" class="ui green message" style="display:none;">
  39. <i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
  40. </div>
  41. <div class="ui message">
  42. <h4>Tips about Server Names & SNI</h4>
  43. <div class="ui bulleted list">
  44. <div class="item">
  45. If you have two subdomains like <code>a.example.com</code> and <code>b.example.com</code> ,
  46. for faster response speed, you might want to setup them one by one (i.e. having two seperate certificate for
  47. <code>a.example.com</code> and <code>b.example.com</code>).
  48. </div>
  49. <div class="item">
  50. If you have a wildcard certificate that covers <code>*.example.com</code>,
  51. you can just enter <code>example.com</code> as server name to add a certificate.
  52. </div>
  53. <div class="item">
  54. If you have a certificate contain multiple host, you can enter the first domain in your certificate
  55. and Zoraxy will try to match the remaining CN/DNS for you.
  56. </div>
  57. </div>
  58. </div>
  59. <p>Current list of loaded certificates</p>
  60. <div>
  61. <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
  62. <table class="ui sortable unstackable basic celled table">
  63. <thead>
  64. <tr><th>Domain</th>
  65. <th>Last Update</th>
  66. <th>Expire At</th>
  67. <th class="no-sort">Renew</th>
  68. <th class="no-sort">Remove</th>
  69. </tr></thead>
  70. <tbody id="certifiedDomainList">
  71. </tbody>
  72. </table>
  73. </div>
  74. <button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
  75. </div>
  76. <div class="ui divider"></div>
  77. <h3>Fallback Certificate</h3>
  78. <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>
  79. <table class="ui very basic unstackable celled table">
  80. <thead>
  81. <tr><th class="no-sort">Key Type</th>
  82. <th class="no-sort">Found</th>
  83. </tr></thead>
  84. <tbody>
  85. <tr>
  86. <td><i class="globe icon"></i> Fallback Public Key</td>
  87. <td id="pubkeyExists"></td>
  88. </tr>
  89. <tr>
  90. <td><i class="lock icon"></i> Fallback Private Key</td>
  91. <td id="prikeyExists"></td>
  92. </tr>
  93. </tbody>
  94. </table>
  95. <p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
  96. <div class="ui buttons">
  97. <button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
  98. <button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
  99. </div>
  100. <div class="ui divider"></div>
  101. <h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
  102. <p>Management features regarding CA and ACME</p>
  103. <h4>Prefered Certificate Authority</h4>
  104. <p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
  105. <div class="ui fluid form">
  106. <div class="field">
  107. <label>Preferred CA</label>
  108. <div class="ui selection dropdown" id="defaultCA">
  109. <input type="hidden" name="defaultCA">
  110. <i class="dropdown icon"></i>
  111. <div class="default text">Let's Encrypt</div>
  112. <div class="menu">
  113. <div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
  114. <div class="item" data-value="Buypass">Buypass</div>
  115. <div class="item" data-value="ZeroSSL">ZeroSSL</div>
  116. </div>
  117. </div>
  118. </div>
  119. <div class="field">
  120. <label>ACME Email</label>
  121. <input id="prefACMEEmail" type="text" placeholder="ACME Email">
  122. </div>
  123. <button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
  124. </div><br>
  125. <h5>Certificate Renew / Generation (ACME) Settings</h5>
  126. <div class="ui basic segment acmeRenewStateWrapper">
  127. <h4 class="ui header" id="acmeAutoRenewer">
  128. <i class="white remove icon"></i>
  129. <div class="content">
  130. <span id="acmeAutoRenewerStatus">Disabled</span>
  131. <div class="sub header">ACME Auto-Renewer</div>
  132. </div>
  133. </h4>
  134. </div>
  135. <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>
  136. <button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
  137. </div>
  138. <script>
  139. var uploadPendingPublicKey = undefined;
  140. var uploadPendingPrivateKey = undefined;
  141. $("#defaultCA").dropdown();
  142. //Renew certificate by button press
  143. function renewCertificate(domain, btn=undefined){
  144. let defaultCA = $("#defaultCA").dropdown("get value");
  145. if (defaultCA.trim() == ""){
  146. defaultCA = "Let's Encrypt";
  147. }
  148. //Get a new cert using ACME
  149. msgbox("Requesting certificate via " + defaultCA +"...");
  150. //Request ACME for certificate
  151. if (btn != undefined){
  152. $(btn).addClass('disabled');
  153. $(btn).html(`<i class="ui loading spinner icon"></i>`);
  154. }
  155. obtainCertificate(domain, defaultCA.trim(), function(succ){
  156. if (btn != undefined){
  157. $(btn).removeClass('disabled');
  158. if (succ){
  159. $(btn).html(`<i class="ui green check icon"></i>`);
  160. }else{
  161. $(btn).html(`<i class="ui red times icon"></i>`);
  162. }
  163. setTimeout(function(){
  164. initManagedDomainCertificateList();
  165. }, 3000);
  166. }
  167. });
  168. }
  169. /*
  170. Obtain Certificate via ACME
  171. */
  172. // Obtain certificate from API, only support one domain
  173. function obtainCertificate(domains, usingCa = "Let's Encrypt", callback=undefined) {
  174. //Load the ACME email from server side
  175. let acmeEmail = "";
  176. $.get("/api/acme/autoRenew/email", function(data){
  177. if (data != "" && data != undefined && data != null){
  178. acmeEmail = data;
  179. }
  180. let filename = "";
  181. let email = acmeEmail;
  182. if (acmeEmail == ""){
  183. msgbox("Unable to obtain certificate: ACME email not set", false, 8000);
  184. if (callback != undefined){
  185. callback(false);
  186. }
  187. return;
  188. }
  189. if (filename.trim() == "" && !domains.includes(",")){
  190. //Zoraxy filename are the matching name for domains.
  191. //Use the same as domains
  192. filename = domains;
  193. }else if (filename != "" && !domains.includes(",")){
  194. //Invalid settings. Force the filename to be same as domain
  195. //if there are only 1 domain
  196. filename = domains;
  197. }else{
  198. msgbox("Filename cannot be empty for certs containing multiple domains.")
  199. if (callback != undefined){
  200. callback(false);
  201. }
  202. return;
  203. }
  204. $.ajax({
  205. url: "/api/acme/obtainCert",
  206. method: "GET",
  207. data: {
  208. domains: domains,
  209. filename: filename,
  210. email: email,
  211. ca: usingCa,
  212. },
  213. success: function(response) {
  214. if (response.error) {
  215. console.log("Error:", response.error);
  216. // Show error message
  217. msgbox(response.error, false, 12000);
  218. if (callback != undefined){
  219. callback(false);
  220. }
  221. } else {
  222. console.log("Certificate installed successfully");
  223. // Show success message
  224. msgbox("Certificate installed successfully");
  225. if (callback != undefined){
  226. callback(false);
  227. }
  228. }
  229. },
  230. error: function(error) {
  231. console.log("Failed to install certificate:", error);
  232. }
  233. });
  234. });
  235. }
  236. //Delete the certificate by its domain
  237. function deleteCertificate(domain){
  238. if (confirm("Confirm delete certificate for " + domain + " ?")){
  239. $.ajax({
  240. url: "/api/cert/delete",
  241. method: "POST",
  242. data: {domain: domain},
  243. success: function(data){
  244. if (data.error != undefined){
  245. msgbox(data.error, false, 5000);
  246. }else{
  247. initManagedDomainCertificateList();
  248. initDefaultKeypairCheck();
  249. }
  250. }
  251. });
  252. }
  253. }
  254. function initAcmeStatus(){
  255. //Initialize the current default CA options
  256. $.get("/api/acme/autoRenew/email", function(data){
  257. $("#prefACMEEmail").val(data);
  258. if (data.trim() == ""){
  259. //acme email is not yet set
  260. $(".renewButton").addClass('disabled');
  261. }else{
  262. $(".renewButton").removeClass('disabled');
  263. }
  264. });
  265. $.get("/api/acme/autoRenew/ca", function(data){
  266. $("#defaultCA").dropdown("set value", data);
  267. });
  268. $.get("/api/acme/autoRenew/enable", function(data){
  269. setACMEEnableStates(data);
  270. })
  271. }
  272. //Set the status of the acme enable icon
  273. function setACMEEnableStates(enabled){
  274. $("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
  275. if (enabled){
  276. $(".acmeRenewStateWrapper").addClass("enabled");
  277. }else{
  278. $(".acmeRenewStateWrapper").removeClass("enabled");
  279. }
  280. $("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
  281. }
  282. initAcmeStatus();
  283. function saveDefaultCA(){
  284. let newDefaultEmail = $("#prefACMEEmail").val().trim();
  285. let newDefaultCA = $("#defaultCA").dropdown("get value");
  286. if (newDefaultEmail == ""){
  287. msgbox("Invalid acme email given", false);
  288. return;
  289. }
  290. $.ajax({
  291. url: "/api/acme/autoRenew/email",
  292. method: "POST",
  293. data: {"set": newDefaultEmail},
  294. success: function(data){
  295. if (data.error != undefined){
  296. msgbox(data.error, false);
  297. }else{
  298. //Update the renew button states
  299. $(".renewButton").removeClass('disabled');
  300. }
  301. }
  302. });
  303. $.ajax({
  304. url: "/api/acme/autoRenew/ca",
  305. data: {"set": newDefaultCA},
  306. method: "POST",
  307. success: function(data){
  308. if (data.error != undefined){
  309. msgbox(data.error, false);
  310. }
  311. }
  312. });
  313. msgbox("Settings updated");
  314. }
  315. //List the stored certificates
  316. function initManagedDomainCertificateList(){
  317. $.get("/api/cert/list?date=true", function(data){
  318. if (data.error != undefined){
  319. msgbox(data.error, false, 5000);
  320. }else{
  321. $("#certifiedDomainList").html("");
  322. data.sort((a,b) => {
  323. return a.Domain > b.Domain
  324. });
  325. data.forEach(entry => {
  326. let isExpired = entry.RemainingDays <= 0;
  327. $("#certifiedDomainList").append(`<tr>
  328. <td>${entry.Domain}</td>
  329. <td>${entry.LastModifiedDate}</td>
  330. <td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
  331. <td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entry.Domain}', this);"><i class="ui green refresh icon"></i></button></td>
  332. <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>
  333. </tr>`);
  334. });
  335. if (data.length == 0){
  336. $("#certifiedDomainList").append(`<tr>
  337. <td colspan="4"><i class="ui times red circle icon"></i> No valid keypairs found</td>
  338. </tr>`);
  339. }
  340. }
  341. })
  342. }
  343. initManagedDomainCertificateList();
  344. function openACMEManager(){
  345. showSideWrapper('snippet/acme.html');
  346. }
  347. function handleDomainUploadByKeypress(){
  348. handleDomainKeysUpload(function(){
  349. $("#certUploadingDomain").text($("#certdomain").val().trim());
  350. //After uploaded, reset the file selector
  351. document.getElementById('pubkeySelector').value = '';
  352. document.getElementById('prikeySelector').value = '';
  353. document.getElementById('certdomain').value = '';
  354. uploadPendingPublicKey = undefined;
  355. uploadPendingPrivateKey = undefined;
  356. //Show succ
  357. $("#certUploadSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
  358. initManagedDomainCertificateList();
  359. });
  360. }
  361. //Handle domain keys upload
  362. function handleDomainKeysUpload(callback=undefined){
  363. let domain = $("#certdomain").val();
  364. if (domain.trim() == ""){
  365. msgbox("Missing domain", false, 5000);
  366. return;
  367. }
  368. if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
  369. const publicKeyForm = new FormData();
  370. publicKeyForm.append('file', uploadPendingPublicKey, 'publicKey');
  371. const privateKeyForm = new FormData();
  372. privateKeyForm.append('file', uploadPendingPrivateKey, 'privateKey');
  373. const publicKeyRequest = new XMLHttpRequest();
  374. publicKeyRequest.open('POST', '/api/cert/upload?ktype=pub&domain=' + domain);
  375. publicKeyRequest.onreadystatechange = function() {
  376. if (publicKeyRequest.readyState === XMLHttpRequest.DONE) {
  377. if (publicKeyRequest.status !== 200) {
  378. msgbox('Error uploading public key: ' + publicKeyRequest.statusText, false, 5000);
  379. }
  380. if (callback != undefined){
  381. callback();
  382. }
  383. }
  384. };
  385. publicKeyRequest.send(publicKeyForm);
  386. const privateKeyRequest = new XMLHttpRequest();
  387. privateKeyRequest.open('POST', '/api/cert/upload?ktype=pri&domain=' + domain);
  388. privateKeyRequest.onreadystatechange = function() {
  389. if (privateKeyRequest.readyState === XMLHttpRequest.DONE) {
  390. if (privateKeyRequest.status !== 200) {
  391. msgbox('Error uploading private key: ' + privateKeyRequest.statusText, false, 5000);
  392. }
  393. if (callback != undefined){
  394. callback();
  395. }
  396. }
  397. };
  398. privateKeyRequest.send(privateKeyForm);
  399. } else {
  400. msgbox('One or both of the files is missing or not a file object');
  401. }
  402. }
  403. //Handlers for selecting domain based key pairs
  404. //ktype = {"pub" / "pri"}
  405. function handleFileSelect(event, ktype="pub") {
  406. const file = event.target.files[0];
  407. //const fileNameInput = document.getElementById('selected-file-name');
  408. if (ktype == "pub"){
  409. uploadPendingPublicKey = file;
  410. }else if (ktype == "pri"){
  411. uploadPendingPrivateKey = file;
  412. }
  413. //fileNameInput.value = file.name;
  414. }
  415. //Check if the default keypairs exists
  416. function initDefaultKeypairCheck(){
  417. $.get("/api/cert/checkDefault", function(data){
  418. let tick = `<i class="ui green checkmark icon"></i>`;
  419. let cross = `<i class="ui red times icon"></i>`;
  420. $("#pubkeyExists").html(data.DefaultPubExists?tick:cross);
  421. $("#prikeyExists").html(data.DefaultPriExists?tick:cross);
  422. });
  423. }
  424. initDefaultKeypairCheck();
  425. function uploadPrivateKey(){
  426. // create file input element
  427. const input = document.createElement('input');
  428. input.type = 'file';
  429. // add change listener to file input
  430. input.addEventListener('change', () => {
  431. // create form data object
  432. const formData = new FormData();
  433. // add selected file to form data
  434. formData.append('file', input.files[0]);
  435. // send form data to server
  436. fetch('/api/cert/upload?ktype=pri', {
  437. method: 'POST',
  438. body: formData
  439. })
  440. .then(response => {
  441. initDefaultKeypairCheck();
  442. if (response.ok) {
  443. msgbox('File upload successful!');
  444. } else {
  445. response.text().then(text => {
  446. msgbox(text, false, 5000);
  447. });
  448. //console.log(response.text());
  449. //alert('File upload failed!');
  450. }
  451. })
  452. .catch(error => {
  453. msgbox('An error occurred while uploading the file.', false, 5000);
  454. console.error(error);
  455. });
  456. });
  457. // click file input to open file selector
  458. input.click();
  459. }
  460. function uploadPublicKey() {
  461. // create file input element
  462. const input = document.createElement('input');
  463. input.type = 'file';
  464. // add change listener to file input
  465. input.addEventListener('change', () => {
  466. // create form data object
  467. const formData = new FormData();
  468. // add selected file to form data
  469. formData.append('file', input.files[0]);
  470. // send form data to server
  471. fetch('/api/cert/upload?ktype=pub', {
  472. method: 'POST',
  473. body: formData
  474. })
  475. .then(response => {
  476. if (response.ok) {
  477. msgbox('File upload successful!');
  478. initDefaultKeypairCheck();
  479. } else {
  480. response.text().then(text => {
  481. msgbox(text, false, 5000);
  482. });
  483. //console.log(response.text());
  484. //alert('File upload failed!');
  485. }
  486. })
  487. .catch(error => {
  488. msgbox('An error occurred while uploading the file.', false, 5000);
  489. console.error(error);
  490. });
  491. });
  492. // click file input to open file selector
  493. input.click();
  494. }
  495. </script>