cert.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  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. <h4>Default Certificates</h4>
  17. <small>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</small></p>
  18. <table class="ui very basic unstackable celled table">
  19. <thead>
  20. <tr><th class="no-sort">Key Type</th>
  21. <th class="no-sort">Exists</th>
  22. </tr></thead>
  23. <tbody>
  24. <tr>
  25. <td><i class="globe icon"></i> Default Public Key</td>
  26. <td id="pubkeyExists"></td>
  27. </tr>
  28. <tr>
  29. <td><i class="lock icon"></i> Default Private Key</td>
  30. <td id="prikeyExists"></td>
  31. </tr>
  32. </tbody>
  33. </table>
  34. <p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
  35. <div class="ui buttons">
  36. <button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
  37. <button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
  38. </div>
  39. <div class="ui divider"></div>
  40. <h4>Sub-domain Certificates</h4>
  41. <p>Provide certificates for multiple domains reverse proxy</p>
  42. <div class="ui fluid form">
  43. <div class="three fields">
  44. <div class="field">
  45. <label>Server Name (Domain)</label>
  46. <input type="text" id="certdomain" placeholder="example.com / blog.example.com">
  47. </div>
  48. <div class="field">
  49. <label>Public Key (.pem)</label>
  50. <input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
  51. </div>
  52. <div class="field">
  53. <label>Private Key (.key)</label>
  54. <input type="file" id="prikeySelector" onchange="handleFileSelect(event, 'pri')">
  55. </div>
  56. </div>
  57. <button class="ui basic button" onclick="handleDomainUploadByKeypress();"><i class="ui teal upload icon"></i> Upload</button><br>
  58. <small>You have intermediate certificate? <a style="cursor:pointer;" onclick="showSideWrapper('snippet/intermediateCertConv.html');">Open Conversion Tool</a></small>
  59. </div>
  60. <div id="certUploadSuccMsg" class="ui green message" style="display:none;">
  61. <i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
  62. </div>
  63. <br>
  64. <div>
  65. <table class="ui sortable unstackable celled table">
  66. <thead>
  67. <tr><th>Domain</th>
  68. <th>Last Update</th>
  69. <th>Expire At</th>
  70. <th class="no-sort">Remove</th>
  71. </tr></thead>
  72. <tbody id="certifiedDomainList">
  73. </tbody>
  74. </table>
  75. <button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
  76. </div>
  77. <div class="ui message">
  78. <h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
  79. If you have 3rd or even 4th level subdomains like <code>blog.example.com</code> or <code>en.blog.example.com</code> ,
  80. 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>
  81. 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.
  82. </div>
  83. <div class="ui divider"></div>
  84. <h4>Certificate Authority (CA) and Auto Renew (ACME)</h4>
  85. <p>Management features regarding CA and ACME</p>
  86. <p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
  87. <div class="ui fluid form">
  88. <div class="field">
  89. <div class="ui selection dropdown" id="defaultCA">
  90. <input type="hidden" name="defaultCA">
  91. <i class="dropdown icon"></i>
  92. <div class="default text">Let's Encrypt</div>
  93. <div class="menu">
  94. <div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
  95. <div class="item" data-value="Buypass">Buypass</div>
  96. <div class="item" data-value="ZeroSSL">ZeroSSL</div>
  97. </div>
  98. </div>
  99. </div>
  100. <button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
  101. </div><br>
  102. <h5>Certificate Renew / Generation (ACME) Settings</h5>
  103. <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>
  104. <button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
  105. </div>
  106. <script>
  107. var uploadPendingPublicKey = undefined;
  108. var uploadPendingPrivateKey = undefined;
  109. $("#defaultCA").dropdown();
  110. //Delete the certificate by its domain
  111. function deleteCertificate(domain){
  112. if (confirm("Confirm delete certificate for " + domain + " ?")){
  113. $.ajax({
  114. url: "/api/cert/delete",
  115. method: "POST",
  116. data: {domain: domain},
  117. success: function(data){
  118. if (data.error != undefined){
  119. msgbox(data.error, false, 5000);
  120. }else{
  121. initManagedDomainCertificateList();
  122. initDefaultKeypairCheck();
  123. }
  124. }
  125. });
  126. }
  127. }
  128. function saveDefaultCA(){
  129. //TODO: Add an endpoint to handle default CA set and load
  130. alert("WIP");
  131. }
  132. //List the stored certificates
  133. function initManagedDomainCertificateList(){
  134. $.get("/api/cert/list?date=true", function(data){
  135. if (data.error != undefined){
  136. msgbox(data.error, false, 5000);
  137. }else{
  138. $("#certifiedDomainList").html("");
  139. data.sort((a,b) => {
  140. return a.Domain > b.Domain
  141. });
  142. data.forEach(entry => {
  143. let isExpired = entry.RemainingDays <= 0;
  144. $("#certifiedDomainList").append(`<tr>
  145. <td>${entry.Domain}</td>
  146. <td>${entry.LastModifiedDate}</td>
  147. <td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
  148. <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>
  149. </tr>`);
  150. });
  151. if (data.length == 0){
  152. $("#certifiedDomainList").append(`<tr>
  153. <td colspan="4"><i class="ui times circle icon"></i> No valid keypairs found</td>
  154. </tr>`);
  155. }
  156. }
  157. })
  158. }
  159. initManagedDomainCertificateList();
  160. function openACMEManager(){
  161. showSideWrapper('snippet/acme.html');
  162. }
  163. function handleDomainUploadByKeypress(){
  164. handleDomainKeysUpload(function(){
  165. $("#certUploadingDomain").text($("#certdomain").val().trim());
  166. //After uploaded, reset the file selector
  167. document.getElementById('pubkeySelector').value = '';
  168. document.getElementById('prikeySelector').value = '';
  169. document.getElementById('certdomain').value = '';
  170. uploadPendingPublicKey = undefined;
  171. uploadPendingPrivateKey = undefined;
  172. //Show succ
  173. $("#certUploadSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
  174. initManagedDomainCertificateList();
  175. });
  176. }
  177. //Handle domain keys upload
  178. function handleDomainKeysUpload(callback=undefined){
  179. let domain = $("#certdomain").val();
  180. if (domain.trim() == ""){
  181. msgbox("Missing domain", false, 5000);
  182. return;
  183. }
  184. if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
  185. const publicKeyForm = new FormData();
  186. publicKeyForm.append('file', uploadPendingPublicKey, 'publicKey');
  187. const privateKeyForm = new FormData();
  188. privateKeyForm.append('file', uploadPendingPrivateKey, 'privateKey');
  189. const publicKeyRequest = new XMLHttpRequest();
  190. publicKeyRequest.open('POST', '/api/cert/upload?ktype=pub&domain=' + domain);
  191. publicKeyRequest.onreadystatechange = function() {
  192. if (publicKeyRequest.readyState === XMLHttpRequest.DONE) {
  193. if (publicKeyRequest.status !== 200) {
  194. msgbox('Error uploading public key: ' + publicKeyRequest.statusText, false, 5000);
  195. }
  196. if (callback != undefined){
  197. callback();
  198. }
  199. }
  200. };
  201. publicKeyRequest.send(publicKeyForm);
  202. const privateKeyRequest = new XMLHttpRequest();
  203. privateKeyRequest.open('POST', '/api/cert/upload?ktype=pri&domain=' + domain);
  204. privateKeyRequest.onreadystatechange = function() {
  205. if (privateKeyRequest.readyState === XMLHttpRequest.DONE) {
  206. if (privateKeyRequest.status !== 200) {
  207. msgbox('Error uploading private key: ' + privateKeyRequest.statusText, false, 5000);
  208. }
  209. if (callback != undefined){
  210. callback();
  211. }
  212. }
  213. };
  214. privateKeyRequest.send(privateKeyForm);
  215. } else {
  216. msgbox('One or both of the files is missing or not a file object');
  217. }
  218. }
  219. //Handlers for selecting domain based key pairs
  220. //ktype = {"pub" / "pri"}
  221. function handleFileSelect(event, ktype="pub") {
  222. const file = event.target.files[0];
  223. //const fileNameInput = document.getElementById('selected-file-name');
  224. if (ktype == "pub"){
  225. uploadPendingPublicKey = file;
  226. }else if (ktype == "pri"){
  227. uploadPendingPrivateKey = file;
  228. }
  229. //fileNameInput.value = file.name;
  230. }
  231. //Check if the default keypairs exists
  232. function initDefaultKeypairCheck(){
  233. $.get("/api/cert/checkDefault", function(data){
  234. let tick = `<i class="ui green checkmark icon"></i>`;
  235. let cross = `<i class="ui red times icon"></i>`;
  236. $("#pubkeyExists").html(data.DefaultPubExists?tick:cross);
  237. $("#prikeyExists").html(data.DefaultPriExists?tick:cross);
  238. });
  239. }
  240. initDefaultKeypairCheck();
  241. function uploadPrivateKey(){
  242. // create file input element
  243. const input = document.createElement('input');
  244. input.type = 'file';
  245. // add change listener to file input
  246. input.addEventListener('change', () => {
  247. // create form data object
  248. const formData = new FormData();
  249. // add selected file to form data
  250. formData.append('file', input.files[0]);
  251. // send form data to server
  252. fetch('/api/cert/upload?ktype=pri', {
  253. method: 'POST',
  254. body: formData
  255. })
  256. .then(response => {
  257. initDefaultKeypairCheck();
  258. if (response.ok) {
  259. msgbox('File upload successful!');
  260. } else {
  261. response.text().then(text => {
  262. msgbox(text, false, 5000);
  263. });
  264. //console.log(response.text());
  265. //alert('File upload failed!');
  266. }
  267. })
  268. .catch(error => {
  269. msgbox('An error occurred while uploading the file.', false, 5000);
  270. console.error(error);
  271. });
  272. });
  273. // click file input to open file selector
  274. input.click();
  275. }
  276. function uploadPublicKey() {
  277. // create file input element
  278. const input = document.createElement('input');
  279. input.type = 'file';
  280. // add change listener to file input
  281. input.addEventListener('change', () => {
  282. // create form data object
  283. const formData = new FormData();
  284. // add selected file to form data
  285. formData.append('file', input.files[0]);
  286. // send form data to server
  287. fetch('/api/cert/upload?ktype=pub', {
  288. method: 'POST',
  289. body: formData
  290. })
  291. .then(response => {
  292. if (response.ok) {
  293. msgbox('File upload successful!');
  294. initDefaultKeypairCheck();
  295. } else {
  296. response.text().then(text => {
  297. msgbox(text, false, 5000);
  298. });
  299. //console.log(response.text());
  300. //alert('File upload failed!');
  301. }
  302. })
  303. .catch(error => {
  304. msgbox('An error occurred while uploading the file.', false, 5000);
  305. console.error(error);
  306. });
  307. });
  308. // click file input to open file selector
  309. input.click();
  310. }
  311. </script>