1
0

cert.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  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 tourstep="certTable">
  61. <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
  62. <table class="ui unstackable basic celled table">
  63. <thead>
  64. <tr><th>Domain</th>
  65. <th>Last Update</th>
  66. <th>Expire At</th>
  67. <th>DNS Challenge</th>
  68. <th class="no-sort">Renew</th>
  69. <th class="no-sort">Remove</th>
  70. </tr></thead>
  71. <tbody id="certifiedDomainList">
  72. </tbody>
  73. </table>
  74. </div>
  75. <button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
  76. </div>
  77. <div class="ui divider"></div>
  78. <div tourstep="defaultCertificate">
  79. <h3>Fallback Certificate</h3>
  80. <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>
  81. <table class="ui very basic unstackable celled table">
  82. <thead>
  83. <tr><th class="no-sort">Key Type</th>
  84. <th class="no-sort">Found</th>
  85. </tr></thead>
  86. <tbody>
  87. <tr>
  88. <td><i class="globe icon"></i> Fallback Public Key</td>
  89. <td id="pubkeyExists"></td>
  90. </tr>
  91. <tr>
  92. <td><i class="lock icon"></i> Fallback Private Key</td>
  93. <td id="prikeyExists"></td>
  94. </tr>
  95. </tbody>
  96. </table>
  97. <p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
  98. <div class="ui buttons">
  99. <button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
  100. <button class="ui basic button" onclick="uploadPrivateKey();"><i class="grey lock icon"></i> Private Key</button>
  101. </div>
  102. </div>
  103. <div class="ui divider"></div>
  104. <div tourstep="acmeSettings">
  105. <h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
  106. <p>Management features regarding CA and ACME</p>
  107. <h4>Prefered Certificate Authority</h4>
  108. <p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
  109. <div class="ui fluid form">
  110. <div class="field">
  111. <label>Preferred CA</label>
  112. <div class="ui selection dropdown" id="defaultCA">
  113. <input type="hidden" name="defaultCA">
  114. <i class="dropdown icon"></i>
  115. <div class="default text">Let's Encrypt</div>
  116. <div class="menu">
  117. <div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
  118. <div class="item" data-value="Buypass">Buypass</div>
  119. <div class="item" data-value="ZeroSSL">ZeroSSL</div>
  120. </div>
  121. </div>
  122. </div>
  123. <div class="field">
  124. <label>ACME Email</label>
  125. <input id="prefACMEEmail" type="text" placeholder="ACME Email">
  126. </div>
  127. <button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
  128. </div><br>
  129. <h5>Certificate Renew / Generation (ACME) Settings</h5>
  130. <div class="ui basic segment acmeRenewStateWrapper">
  131. <h4 class="ui header" id="acmeAutoRenewer">
  132. <i class="white remove icon"></i>
  133. <div class="content">
  134. <span id="acmeAutoRenewerStatus">Disabled</span>
  135. <div class="sub header">ACME Auto-Renewer</div>
  136. </div>
  137. </h4>
  138. </div>
  139. <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>
  140. <button class="ui basic button" tourstep="openACMEManager" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
  141. </div>
  142. </div>
  143. <script>
  144. var uploadPendingPublicKey = undefined;
  145. var uploadPendingPrivateKey = undefined;
  146. $("#defaultCA").dropdown();
  147. //Renew certificate by button press
  148. function renewCertificate(domain, dns, btn=undefined){
  149. let defaultCA = $("#defaultCA").dropdown("get value");
  150. if (defaultCA.trim() == ""){
  151. defaultCA = "Let's Encrypt";
  152. }
  153. //Get a new cert using ACME
  154. msgbox("Requesting certificate via " + defaultCA +"...");
  155. //Request ACME for certificate
  156. let buttonOriginalHTML = "";
  157. if (btn != undefined){
  158. buttonOriginalHTML = $(btn).html();
  159. $(btn).addClass('disabled');
  160. $(btn).html(`<i class="ui loading spinner icon"></i>`);
  161. }
  162. obtainCertificate(domain, dns, defaultCA.trim(), function(succ){
  163. if (btn != undefined){
  164. $(btn).removeClass('disabled');
  165. if ($(btn).hasClass("icon")){
  166. //Only change the button icon
  167. if (succ){
  168. $(btn).html(`<i class="ui green check icon"></i>`);
  169. }else{
  170. $(btn).html(`<i class="ui red times icon"></i>`);
  171. }
  172. }else{
  173. //Show error or success icon with text
  174. if (succ){
  175. $(btn).html(`<i class="ui green check icon"></i> Requested`);
  176. }else{
  177. $(btn).html(`<i class="ui red times icon"></i> Error`);
  178. }
  179. }
  180. //Restore the button after 3 seconds
  181. setTimeout(function(){
  182. $(btn).html(buttonOriginalHTML);
  183. }, 3000);
  184. setTimeout(function(){
  185. initManagedDomainCertificateList();
  186. }, 3000);
  187. }
  188. });
  189. }
  190. /*
  191. Obtain Certificate via ACME
  192. */
  193. // Obtain certificate from API, only support one domain
  194. function obtainCertificate(domains, dns, usingCa = "Let's Encrypt", callback=undefined) {
  195. //Load the ACME email from server side
  196. let acmeEmail = "";
  197. $.get("/api/acme/autoRenew/email", function(data){
  198. if (data != "" && data != undefined && data != null){
  199. acmeEmail = data;
  200. }
  201. let filename = "";
  202. let email = acmeEmail;
  203. if (acmeEmail == ""){
  204. msgbox("Unable to obtain certificate: ACME email not set", false, 8000);
  205. if (callback != undefined){
  206. callback(false);
  207. }
  208. return;
  209. }
  210. if (filename.trim() == "" && !domains.includes(",")){
  211. //Zoraxy filename are the matching name for domains.
  212. //Use the same as domains
  213. filename = domains;
  214. }else if (filename != "" && !domains.includes(",")){
  215. //Invalid settings. Force the filename to be same as domain
  216. //if there are only 1 domain
  217. filename = domains;
  218. }else{
  219. msgbox("Filename cannot be empty for certs containing multiple domains.")
  220. if (callback != undefined){
  221. callback(false);
  222. }
  223. return;
  224. }
  225. //Filename cannot contain wildcards, and wildcards are possible with DNS challenges
  226. filename = filename.replace("*", "_");
  227. $.ajax({
  228. url: "/api/acme/obtainCert",
  229. method: "GET",
  230. data: {
  231. domains: domains,
  232. filename: filename,
  233. email: email,
  234. ca: usingCa,
  235. dns: dns
  236. },
  237. success: function(response) {
  238. if (response.error) {
  239. console.log("Error:", response.error);
  240. // Show error message
  241. msgbox(response.error, false, 12000);
  242. if (callback != undefined){
  243. callback(false);
  244. }
  245. } else {
  246. console.log("Certificate installed successfully");
  247. // Show success message
  248. msgbox("Certificate installed successfully");
  249. if (callback != undefined){
  250. callback(true);
  251. }
  252. }
  253. },
  254. error: function(error) {
  255. console.log("Failed to install certificate:", error);
  256. }
  257. });
  258. });
  259. }
  260. //Delete the certificate by its domain
  261. function deleteCertificate(domain){
  262. if (confirm("Confirm delete certificate for " + domain + " ?")){
  263. $.cjax({
  264. url: "/api/cert/delete",
  265. method: "POST",
  266. data: {domain: domain},
  267. success: function(data){
  268. if (data.error != undefined){
  269. msgbox(data.error, false, 5000);
  270. }else{
  271. initManagedDomainCertificateList();
  272. initDefaultKeypairCheck();
  273. }
  274. }
  275. });
  276. }
  277. }
  278. function initAcmeStatus(){
  279. //Initialize the current default CA options
  280. $.get("/api/acme/autoRenew/email", function(data){
  281. $("#prefACMEEmail").val(data);
  282. if (data.trim() == ""){
  283. //acme email is not yet set
  284. $(".renewButton").addClass('disabled');
  285. }else{
  286. $(".renewButton").removeClass('disabled');
  287. }
  288. });
  289. $.get("/api/acme/autoRenew/ca", function(data){
  290. $("#defaultCA").dropdown("set value", data);
  291. });
  292. $.get("/api/acme/autoRenew/enable", function(data){
  293. setACMEEnableStates(data);
  294. })
  295. }
  296. //Set the status of the acme enable icon
  297. function setACMEEnableStates(enabled){
  298. $("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
  299. if (enabled){
  300. $(".acmeRenewStateWrapper").addClass("enabled");
  301. }else{
  302. $(".acmeRenewStateWrapper").removeClass("enabled");
  303. }
  304. $("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
  305. }
  306. initAcmeStatus();
  307. function saveDefaultCA(){
  308. let newDefaultEmail = $("#prefACMEEmail").val().trim();
  309. let newDefaultCA = $("#defaultCA").dropdown("get value");
  310. if (newDefaultEmail == ""){
  311. msgbox("Invalid acme email given", false);
  312. return;
  313. }
  314. $.cjax({
  315. url: "/api/acme/autoRenew/email",
  316. method: "POST",
  317. data: {"set": newDefaultEmail},
  318. success: function(data){
  319. if (data.error != undefined){
  320. msgbox(data.error, false);
  321. }else{
  322. //Update the renew button states
  323. $(".renewButton").removeClass('disabled');
  324. }
  325. }
  326. });
  327. $.cjax({
  328. url: "/api/acme/autoRenew/ca",
  329. data: {"set": newDefaultCA},
  330. method: "POST",
  331. success: function(data){
  332. if (data.error != undefined){
  333. msgbox(data.error, false);
  334. }
  335. }
  336. });
  337. msgbox("Settings updated");
  338. }
  339. //List the stored certificates
  340. function initManagedDomainCertificateList(){
  341. $.get("/api/cert/list?date=true", function(data){
  342. if (data.error != undefined){
  343. msgbox(data.error, false, 5000);
  344. }else{
  345. $("#certifiedDomainList").html("");
  346. data.sort((a,b) => {
  347. return a.Domain > b.Domain
  348. });
  349. data.forEach(entry => {
  350. let isExpired = entry.RemainingDays <= 0;
  351. let entryDomainRenewKey = entry.Domain;
  352. if (entryDomainRenewKey.includes("_.")){
  353. entryDomainRenewKey = entryDomainRenewKey.replace("_.","*.");
  354. }
  355. $("#certifiedDomainList").append(`<tr>
  356. <td><a style="cursor: pointer;" title="Download certificate" onclick="handleCertDownload('${entry.Domain}');">${entry.Domain}</a></td>
  357. <td>${entry.LastModifiedDate}</td>
  358. <td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
  359. <td><i class="${entry.UseDNS?"green check": "red times"} icon"></i></td>
  360. <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>
  361. <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>
  362. </tr>`);
  363. });
  364. if (data.length == 0){
  365. $("#certifiedDomainList").append(`<tr>
  366. <td colspan="4"><i class="ui times red circle icon"></i> No valid keypairs found</td>
  367. </tr>`);
  368. }
  369. }
  370. })
  371. }
  372. initManagedDomainCertificateList();
  373. function openACMEManager(){
  374. showSideWrapper('snippet/acme.html');
  375. }
  376. function handleDomainUploadByKeypress(){
  377. handleDomainKeysUpload(function(){
  378. $("#certUploadingDomain").text($("#certdomain").val().trim());
  379. //After uploaded, reset the file selector
  380. document.getElementById('pubkeySelector').value = '';
  381. document.getElementById('prikeySelector').value = '';
  382. document.getElementById('certdomain').value = '';
  383. uploadPendingPublicKey = undefined;
  384. uploadPendingPrivateKey = undefined;
  385. //Show succ
  386. $("#certUploadSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
  387. initManagedDomainCertificateList();
  388. });
  389. }
  390. function handleCertDownload(certName){
  391. $.get("/api/cert/download?seek=true&certname=" + certName, function(data){
  392. if (data.error != undefined){
  393. //Error resolving certificate
  394. msgbox(data.error, false);
  395. }else{
  396. //Continue to download
  397. window.open("/api/cert/download?certname=" + certName);
  398. }
  399. });
  400. }
  401. //Handle domain keys upload
  402. function handleDomainKeysUpload(callback=undefined){
  403. let domain = $("#certdomain").val();
  404. if (domain.trim() == ""){
  405. msgbox("Missing domain", false, 5000);
  406. return;
  407. }
  408. if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
  409. const publicKeyForm = new FormData();
  410. const csrfToken = document.querySelector('meta[name="zoraxy.csrf.Token"]').getAttribute("content");
  411. publicKeyForm.append('file', uploadPendingPublicKey, 'publicKey');
  412. const privateKeyForm = new FormData();
  413. privateKeyForm.append('file', uploadPendingPrivateKey, 'privateKey');
  414. const publicKeyRequest = new XMLHttpRequest();
  415. publicKeyRequest.open('POST', '/api/cert/upload?ktype=pub&domain=' + domain);
  416. publicKeyRequest.setRequestHeader('X-CSRF-Token', csrfToken);
  417. publicKeyRequest.onreadystatechange = function() {
  418. if (publicKeyRequest.readyState === XMLHttpRequest.DONE) {
  419. if (publicKeyRequest.status !== 200) {
  420. msgbox('Error uploading public key: ' + publicKeyRequest.statusText, false, 5000);
  421. }
  422. if (callback != undefined){
  423. callback();
  424. }
  425. }
  426. };
  427. publicKeyRequest.send(publicKeyForm);
  428. const privateKeyRequest = new XMLHttpRequest();
  429. privateKeyRequest.open('POST', '/api/cert/upload?ktype=pri&domain=' + domain);
  430. privateKeyRequest.setRequestHeader('X-CSRF-Token', csrfToken);
  431. privateKeyRequest.onreadystatechange = function() {
  432. if (privateKeyRequest.readyState === XMLHttpRequest.DONE) {
  433. if (privateKeyRequest.status !== 200) {
  434. msgbox('Error uploading private key: ' + privateKeyRequest.statusText, false, 5000);
  435. }
  436. if (callback != undefined){
  437. callback();
  438. }
  439. }
  440. };
  441. privateKeyRequest.send(privateKeyForm);
  442. } else {
  443. msgbox('One or both of the files is missing or not a file object');
  444. }
  445. }
  446. //Handlers for selecting domain based key pairs
  447. //ktype = {"pub" / "pri"}
  448. function handleFileSelect(event, ktype="pub") {
  449. const file = event.target.files[0];
  450. if (ktype == "pub"){
  451. uploadPendingPublicKey = file;
  452. }else if (ktype == "pri"){
  453. uploadPendingPrivateKey = file;
  454. }
  455. }
  456. //Check if the default keypairs exists
  457. function initDefaultKeypairCheck(){
  458. $.get("/api/cert/checkDefault", function(data){
  459. let tick = `<i class="ui green checkmark icon"></i>`;
  460. let cross = `<i class="ui red times icon"></i>`;
  461. $("#pubkeyExists").html(data.DefaultPubExists?tick:cross);
  462. $("#prikeyExists").html(data.DefaultPriExists?tick:cross);
  463. });
  464. }
  465. initDefaultKeypairCheck();
  466. function uploadPrivateKey(){
  467. // create file input element
  468. const input = document.createElement('input');
  469. input.type = 'file';
  470. // add change listener to file input
  471. input.addEventListener('change', () => {
  472. // create form data object
  473. const formData = new FormData();
  474. const csrfToken = document.querySelector('meta[name="zoraxy.csrf.Token"]').getAttribute("content");
  475. // add selected file to form data
  476. formData.append('file', input.files[0]);
  477. // send form data to server
  478. fetch('/api/cert/upload?ktype=pri', {
  479. method: 'POST',
  480. body: formData,
  481. headers: {
  482. 'X-CSRF-Token': csrfToken
  483. }
  484. })
  485. .then(response => {
  486. initDefaultKeypairCheck();
  487. if (response.ok) {
  488. msgbox('File upload successful!');
  489. } else {
  490. response.text().then(text => {
  491. msgbox(text, false, 5000);
  492. });
  493. //console.log(response.text());
  494. //alert('File upload failed!');
  495. }
  496. })
  497. .catch(error => {
  498. msgbox('An error occurred while uploading the file.', false, 5000);
  499. console.error(error);
  500. });
  501. });
  502. // click file input to open file selector
  503. input.click();
  504. }
  505. function uploadPublicKey() {
  506. // create file input element
  507. const input = document.createElement('input');
  508. const csrfToken = document.querySelector('meta[name="zoraxy.csrf.Token"]').getAttribute("content");
  509. input.type = 'file';
  510. // add change listener to file input
  511. input.addEventListener('change', () => {
  512. // create form data object
  513. const formData = new FormData();
  514. // add selected file to form data
  515. formData.append('file', input.files[0]);
  516. // send form data to server
  517. fetch('/api/cert/upload?ktype=pub', {
  518. method: 'POST',
  519. body: formData,
  520. headers: {
  521. 'X-CSRF-Token': csrfToken
  522. }
  523. })
  524. .then(response => {
  525. if (response.ok) {
  526. msgbox('File upload successful!');
  527. initDefaultKeypairCheck();
  528. } else {
  529. response.text().then(text => {
  530. msgbox(text, false, 5000);
  531. });
  532. //console.log(response.text());
  533. //alert('File upload failed!');
  534. }
  535. })
  536. .catch(error => {
  537. msgbox('An error occurred while uploading the file.', false, 5000);
  538. console.error(error);
  539. });
  540. });
  541. // click file input to open file selector
  542. input.click();
  543. }
  544. </script>