cert.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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. </div>
  24. <div class="field">
  25. <label>Public Key (.pem)</label>
  26. <input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
  27. </div>
  28. <div class="field">
  29. <label>Private Key (.key)</label>
  30. <input type="file" id="prikeySelector" onchange="handleFileSelect(event, 'pri')">
  31. </div>
  32. </div>
  33. <button class="ui basic button" onclick="handleDomainUploadByKeypress();"><i class="ui teal upload icon"></i> Upload</button><br>
  34. <small>You have intermediate certificate? <a style="cursor:pointer;" onclick="showSideWrapper('snippet/intermediateCertConv.html');">Open Conversion Tool</a></small>
  35. </div>
  36. <div id="certUploadSuccMsg" class="ui green message" style="display:none;">
  37. <i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
  38. </div>
  39. <div class="ui message">
  40. <h4><i class="info circle icon"></i>Tips about Server Names & SNI</h4>
  41. <div class="ui bulleted list">
  42. <div class="item">
  43. If you have two subdomains like <code>a.example.com</code> and <code>b.example.com</code> ,
  44. for faster response speed, you might want to setup them one by one (i.e. having two seperate certificate for
  45. <code>a.example.com</code> and <code>b.example.com</code>).
  46. </div>
  47. <div class="item">
  48. If you have a wildcard certificate that covers <code>*.example.com</code>,
  49. you can just enter <code>example.com</code> as server name to add a certificate.
  50. </div>
  51. <div class="item">
  52. If you have a certificate contain multiple host, you can enter the first domain in your certificate
  53. and Zoraxy will try to match the remaining CN/DNS for you.
  54. </div>
  55. </div>
  56. </div>
  57. <p>Current list of loaded certificates</p>
  58. <div>
  59. <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
  60. <table class="ui sortable unstackable basic celled table">
  61. <thead>
  62. <tr><th>Domain</th>
  63. <th>Last Update</th>
  64. <th>Expire At</th>
  65. <th class="no-sort">Remove</th>
  66. </tr></thead>
  67. <tbody id="certifiedDomainList">
  68. </tbody>
  69. </table>
  70. </div>
  71. <button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
  72. </div>
  73. <div class="ui divider"></div>
  74. <h3>Fallback Certificate</h3>
  75. <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>
  76. <table class="ui very basic unstackable celled table">
  77. <thead>
  78. <tr><th class="no-sort">Key Type</th>
  79. <th class="no-sort">Found</th>
  80. </tr></thead>
  81. <tbody>
  82. <tr>
  83. <td><i class="globe icon"></i> Fallback Public Key</td>
  84. <td id="pubkeyExists"></td>
  85. </tr>
  86. <tr>
  87. <td><i class="lock icon"></i> Fallback Private Key</td>
  88. <td id="prikeyExists"></td>
  89. </tr>
  90. </tbody>
  91. </table>
  92. <p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
  93. <div class="ui buttons">
  94. <button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
  95. <button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
  96. </div>
  97. <div class="ui divider"></div>
  98. <h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
  99. <p>Management features regarding CA and ACME</p>
  100. <h4>Prefered Certificate Authority</h4>
  101. <p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
  102. <div class="ui fluid form">
  103. <div class="field">
  104. <label>Preferred CA</label>
  105. <div class="ui selection dropdown" id="defaultCA">
  106. <input type="hidden" name="defaultCA">
  107. <i class="dropdown icon"></i>
  108. <div class="default text">Let's Encrypt</div>
  109. <div class="menu">
  110. <div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
  111. <div class="item" data-value="Buypass">Buypass</div>
  112. <div class="item" data-value="ZeroSSL">ZeroSSL</div>
  113. </div>
  114. </div>
  115. </div>
  116. <div class="field">
  117. <label>ACME Email</label>
  118. <input id="prefACMEEmail" type="text" placeholder="ACME Email">
  119. </div>
  120. <button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
  121. </div><br>
  122. <h5>Certificate Renew / Generation (ACME) Settings</h5>
  123. <div class="ui basic segment acmeRenewStateWrapper">
  124. <h4 class="ui header" id="acmeAutoRenewer">
  125. <i class="white remove icon"></i>
  126. <div class="content">
  127. <span id="acmeAutoRenewerStatus">Disabled</span>
  128. <div class="sub header">ACME Auto-Renewer</div>
  129. </div>
  130. </h4>
  131. </div>
  132. <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>
  133. <button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
  134. </div>
  135. <script>
  136. var uploadPendingPublicKey = undefined;
  137. var uploadPendingPrivateKey = undefined;
  138. $("#defaultCA").dropdown();
  139. //Delete the certificate by its domain
  140. function deleteCertificate(domain){
  141. if (confirm("Confirm delete certificate for " + domain + " ?")){
  142. $.ajax({
  143. url: "/api/cert/delete",
  144. method: "POST",
  145. data: {domain: domain},
  146. success: function(data){
  147. if (data.error != undefined){
  148. msgbox(data.error, false, 5000);
  149. }else{
  150. initManagedDomainCertificateList();
  151. initDefaultKeypairCheck();
  152. }
  153. }
  154. });
  155. }
  156. }
  157. function initAcmeStatus(){
  158. //Initialize the current default CA options
  159. $.get("/api/acme/autoRenew/email", function(data){
  160. $("#prefACMEEmail").val(data);
  161. });
  162. $.get("/api/acme/autoRenew/ca", function(data){
  163. $("#defaultCA").dropdown("set value", data);
  164. });
  165. $.get("/api/acme/autoRenew/enable", function(data){
  166. setACMEEnableStates(data);
  167. })
  168. }
  169. //Set the status of the acme enable icon
  170. function setACMEEnableStates(enabled){
  171. $("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
  172. if (enabled){
  173. $(".acmeRenewStateWrapper").addClass("enabled");
  174. }else{
  175. $(".acmeRenewStateWrapper").removeClass("enabled");
  176. }
  177. $("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
  178. }
  179. initAcmeStatus();
  180. function saveDefaultCA(){
  181. let newDefaultEmail = $("#prefACMEEmail").val().trim();
  182. let newDefaultCA = $("#defaultCA").dropdown("get value");
  183. if (newDefaultEmail == ""){
  184. msgbox("Invalid acme email given", false);
  185. return;
  186. }
  187. $.ajax({
  188. url: "/api/acme/autoRenew/email",
  189. method: "POST",
  190. data: {"set": newDefaultEmail},
  191. success: function(data){
  192. if (data.error != undefined){
  193. msgbox(data.error, false);
  194. }
  195. }
  196. });
  197. $.ajax({
  198. url: "/api/acme/autoRenew/ca",
  199. data: {"set": newDefaultCA},
  200. method: "POST",
  201. success: function(data){
  202. if (data.error != undefined){
  203. msgbox(data.error, false);
  204. }
  205. }
  206. });
  207. msgbox("Settings updated");
  208. }
  209. //List the stored certificates
  210. function initManagedDomainCertificateList(){
  211. $.get("/api/cert/list?date=true", function(data){
  212. if (data.error != undefined){
  213. msgbox(data.error, false, 5000);
  214. }else{
  215. $("#certifiedDomainList").html("");
  216. data.sort((a,b) => {
  217. return a.Domain > b.Domain
  218. });
  219. data.forEach(entry => {
  220. let isExpired = entry.RemainingDays <= 0;
  221. $("#certifiedDomainList").append(`<tr>
  222. <td>${entry.Domain}</td>
  223. <td>${entry.LastModifiedDate}</td>
  224. <td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
  225. <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>
  226. </tr>`);
  227. });
  228. if (data.length == 0){
  229. $("#certifiedDomainList").append(`<tr>
  230. <td colspan="4"><i class="ui times red circle icon"></i> No valid keypairs found</td>
  231. </tr>`);
  232. }
  233. }
  234. })
  235. }
  236. initManagedDomainCertificateList();
  237. function openACMEManager(){
  238. showSideWrapper('snippet/acme.html');
  239. }
  240. function handleDomainUploadByKeypress(){
  241. handleDomainKeysUpload(function(){
  242. $("#certUploadingDomain").text($("#certdomain").val().trim());
  243. //After uploaded, reset the file selector
  244. document.getElementById('pubkeySelector').value = '';
  245. document.getElementById('prikeySelector').value = '';
  246. document.getElementById('certdomain').value = '';
  247. uploadPendingPublicKey = undefined;
  248. uploadPendingPrivateKey = undefined;
  249. //Show succ
  250. $("#certUploadSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
  251. initManagedDomainCertificateList();
  252. });
  253. }
  254. //Handle domain keys upload
  255. function handleDomainKeysUpload(callback=undefined){
  256. let domain = $("#certdomain").val();
  257. if (domain.trim() == ""){
  258. msgbox("Missing domain", false, 5000);
  259. return;
  260. }
  261. if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
  262. const publicKeyForm = new FormData();
  263. publicKeyForm.append('file', uploadPendingPublicKey, 'publicKey');
  264. const privateKeyForm = new FormData();
  265. privateKeyForm.append('file', uploadPendingPrivateKey, 'privateKey');
  266. const publicKeyRequest = new XMLHttpRequest();
  267. publicKeyRequest.open('POST', '/api/cert/upload?ktype=pub&domain=' + domain);
  268. publicKeyRequest.onreadystatechange = function() {
  269. if (publicKeyRequest.readyState === XMLHttpRequest.DONE) {
  270. if (publicKeyRequest.status !== 200) {
  271. msgbox('Error uploading public key: ' + publicKeyRequest.statusText, false, 5000);
  272. }
  273. if (callback != undefined){
  274. callback();
  275. }
  276. }
  277. };
  278. publicKeyRequest.send(publicKeyForm);
  279. const privateKeyRequest = new XMLHttpRequest();
  280. privateKeyRequest.open('POST', '/api/cert/upload?ktype=pri&domain=' + domain);
  281. privateKeyRequest.onreadystatechange = function() {
  282. if (privateKeyRequest.readyState === XMLHttpRequest.DONE) {
  283. if (privateKeyRequest.status !== 200) {
  284. msgbox('Error uploading private key: ' + privateKeyRequest.statusText, false, 5000);
  285. }
  286. if (callback != undefined){
  287. callback();
  288. }
  289. }
  290. };
  291. privateKeyRequest.send(privateKeyForm);
  292. } else {
  293. msgbox('One or both of the files is missing or not a file object');
  294. }
  295. }
  296. //Handlers for selecting domain based key pairs
  297. //ktype = {"pub" / "pri"}
  298. function handleFileSelect(event, ktype="pub") {
  299. const file = event.target.files[0];
  300. //const fileNameInput = document.getElementById('selected-file-name');
  301. if (ktype == "pub"){
  302. uploadPendingPublicKey = file;
  303. }else if (ktype == "pri"){
  304. uploadPendingPrivateKey = file;
  305. }
  306. //fileNameInput.value = file.name;
  307. }
  308. //Check if the default keypairs exists
  309. function initDefaultKeypairCheck(){
  310. $.get("/api/cert/checkDefault", function(data){
  311. let tick = `<i class="ui green checkmark icon"></i>`;
  312. let cross = `<i class="ui red times icon"></i>`;
  313. $("#pubkeyExists").html(data.DefaultPubExists?tick:cross);
  314. $("#prikeyExists").html(data.DefaultPriExists?tick:cross);
  315. });
  316. }
  317. initDefaultKeypairCheck();
  318. function uploadPrivateKey(){
  319. // create file input element
  320. const input = document.createElement('input');
  321. input.type = 'file';
  322. // add change listener to file input
  323. input.addEventListener('change', () => {
  324. // create form data object
  325. const formData = new FormData();
  326. // add selected file to form data
  327. formData.append('file', input.files[0]);
  328. // send form data to server
  329. fetch('/api/cert/upload?ktype=pri', {
  330. method: 'POST',
  331. body: formData
  332. })
  333. .then(response => {
  334. initDefaultKeypairCheck();
  335. if (response.ok) {
  336. msgbox('File upload successful!');
  337. } else {
  338. response.text().then(text => {
  339. msgbox(text, false, 5000);
  340. });
  341. //console.log(response.text());
  342. //alert('File upload failed!');
  343. }
  344. })
  345. .catch(error => {
  346. msgbox('An error occurred while uploading the file.', false, 5000);
  347. console.error(error);
  348. });
  349. });
  350. // click file input to open file selector
  351. input.click();
  352. }
  353. function uploadPublicKey() {
  354. // create file input element
  355. const input = document.createElement('input');
  356. input.type = 'file';
  357. // add change listener to file input
  358. input.addEventListener('change', () => {
  359. // create form data object
  360. const formData = new FormData();
  361. // add selected file to form data
  362. formData.append('file', input.files[0]);
  363. // send form data to server
  364. fetch('/api/cert/upload?ktype=pub', {
  365. method: 'POST',
  366. body: formData
  367. })
  368. .then(response => {
  369. if (response.ok) {
  370. msgbox('File upload successful!');
  371. initDefaultKeypairCheck();
  372. } else {
  373. response.text().then(text => {
  374. msgbox(text, false, 5000);
  375. });
  376. //console.log(response.text());
  377. //alert('File upload failed!');
  378. }
  379. })
  380. .catch(error => {
  381. msgbox('An error occurred while uploading the file.', false, 5000);
  382. console.error(error);
  383. });
  384. });
  385. // click file input to open file selector
  386. input.click();
  387. }
  388. </script>