app.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. const toolButtons = document.querySelectorAll(".tool-button");
  2. const panels = document.querySelectorAll(".panel");
  3. document.addEventListener("click", async (event) => {
  4. const button = event.target.closest("[data-copy-value]");
  5. if (!button) {
  6. return;
  7. }
  8. const value = button.dataset.copyValue || "";
  9. const originalLabel = button.textContent;
  10. try {
  11. await copyText(value);
  12. button.textContent = "Copied";
  13. button.classList.add("copied");
  14. setTimeout(() => {
  15. button.textContent = originalLabel;
  16. button.classList.remove("copied");
  17. }, 1500);
  18. } catch (error) {
  19. button.textContent = "Copy failed";
  20. setTimeout(() => {
  21. button.textContent = originalLabel;
  22. }, 1500);
  23. }
  24. });
  25. toolButtons.forEach((button) => {
  26. button.addEventListener("click", () => {
  27. const target = button.dataset.tool;
  28. toolButtons.forEach((item) => item.classList.remove("active"));
  29. panels.forEach((panel) => panel.classList.remove("active"));
  30. button.classList.add("active");
  31. document.getElementById(target).classList.add("active");
  32. });
  33. });
  34. document.getElementById("tls-form").addEventListener("submit", async (event) => {
  35. event.preventDefault();
  36. const form = new FormData(event.currentTarget);
  37. const action = event.submitter?.value || "generate";
  38. await submitJSON("/api/tls/generate", {
  39. commonName: form.get("commonName"),
  40. organization: form.get("organization"),
  41. organizationalUnit: form.get("organizationalUnit"),
  42. locality: form.get("locality"),
  43. state: form.get("state"),
  44. country: form.get("country"),
  45. dnsNames: form.get("dnsNames"),
  46. validDays: Number(form.get("validDays")),
  47. keySize: Number(form.get("keySize")),
  48. }, renderTLSResult, document.getElementById("tls-result"), (data) => {
  49. if (action === "download") {
  50. downloadBase64File(data.zipBase64, data.zipFilename || "tls-materials.zip", "application/zip");
  51. }
  52. });
  53. });
  54. document.getElementById("dns-form").addEventListener("submit", async (event) => {
  55. event.preventDefault();
  56. const form = new FormData(event.currentTarget);
  57. await submitJSON("/api/dns/lookup", {
  58. host: form.get("host"),
  59. }, renderDNSResult, document.getElementById("dns-result"));
  60. });
  61. document.getElementById("ssl-form").addEventListener("submit", async (event) => {
  62. event.preventDefault();
  63. const form = new FormData(event.currentTarget);
  64. await submitJSON("/api/ssl/check", {
  65. url: form.get("url"),
  66. }, renderSSLResult, document.getElementById("ssl-result"));
  67. });
  68. document.getElementById("pem-form").addEventListener("submit", async (event) => {
  69. event.preventDefault();
  70. const form = new FormData(event.currentTarget);
  71. await submitJSON("/api/pem/check", {
  72. pem: form.get("pem"),
  73. }, renderPEMResult, document.getElementById("pem-result"));
  74. });
  75. async function submitJSON(url, payload, renderer, target, onSuccess) {
  76. setLoading(target);
  77. try {
  78. const response = await fetch(url, {
  79. method: "POST",
  80. headers: {
  81. "Content-Type": "application/json",
  82. },
  83. body: JSON.stringify(payload),
  84. });
  85. const data = await response.json();
  86. if (!response.ok) {
  87. throw new Error(data.error || "Request failed");
  88. }
  89. target.classList.remove("empty", "error");
  90. target.innerHTML = renderer(data);
  91. if (onSuccess) {
  92. onSuccess(data);
  93. }
  94. } catch (error) {
  95. target.classList.remove("empty");
  96. target.classList.add("error");
  97. target.textContent = error.message;
  98. }
  99. }
  100. function setLoading(target) {
  101. target.classList.remove("error");
  102. target.classList.add("empty");
  103. target.textContent = "Working...";
  104. }
  105. function renderTLSResult(data) {
  106. return `
  107. ${renderMetaGrid([
  108. ["Subject", escapeHTML(data.subject)],
  109. ["Issuer", escapeHTML(data.issuer)],
  110. ["Serial Number", escapeHTML(data.serialNumber)],
  111. ["Valid From", escapeHTML(data.notBefore)],
  112. ["Valid Until", escapeHTML(data.notAfter)],
  113. ["Certificate Type", escapeHTML(data.isSelfSigned ? "Self-Signed" : "Signed")],
  114. ["Key Size", escapeHTML(`${data.keySize || ""}-bit RSA`)],
  115. ["Organization", escapeHTML((data.organization || []).join(", "))],
  116. ["OU", escapeHTML((data.ou || []).join(", "))],
  117. ["Locality", escapeHTML((data.locality || []).join(", "))],
  118. ["State / Province", escapeHTML((data.state || []).join(", "))],
  119. ["Country", escapeHTML((data.country || []).join(", "))],
  120. ["DNS Names", escapeHTML((data.dnsNames || []).join(", "))],
  121. ])}
  122. ${renderCopyablePEMBlock("Public Key PEM", data.publicKeyPem)}
  123. ${renderCopyablePEMBlock("Certificate Signing Request", data.csrPem)}
  124. ${renderCopyablePEMBlock("Self-Signed Certificate PEM", data.selfSignedCertificatePem || data.certificatePem)}
  125. ${renderCopyablePEMBlock("Private Key PEM", data.privateKeyPem)}
  126. `;
  127. }
  128. function renderDNSResult(data) {
  129. return `
  130. ${renderMetaGrid([
  131. ["Host", escapeHTML(data.host)],
  132. ["CNAME", escapeHTML(data.cname || "None")],
  133. ])}
  134. ${renderListBlock("A / AAAA", data.ips)}
  135. ${renderListBlock("TXT", data.txt)}
  136. ${renderListBlock("NS", data.ns)}
  137. ${renderListBlock("MX", (data.mx || []).map((record) => `${record.host} (pref ${record.pref})`))}
  138. ${renderErrorBlock(data.errors)}
  139. `;
  140. }
  141. function renderSSLResult(data) {
  142. const leaf = (data.chain || [])[0] || {};
  143. return `
  144. ${renderMetaGrid([
  145. ["Requested URL", escapeHTML(data.url)],
  146. ["Server Name", escapeHTML(data.serverName)],
  147. ["TLS Version", escapeHTML(data.version)],
  148. ["Cipher Suite", escapeHTML(data.cipherSuite)],
  149. ["ALPN", escapeHTML(data.negotiatedProtocol || "None")],
  150. ["Leaf Subject", escapeHTML(leaf.subject || "")],
  151. ["Leaf Issuer", escapeHTML(leaf.issuer || "")],
  152. ["Leaf Valid Until", escapeHTML(leaf.notAfter || "")],
  153. ])}
  154. <div class="record-block">
  155. <h3>Certificate Chain</h3>
  156. <pre>${escapeHTML(JSON.stringify(data.chain, null, 2))}</pre>
  157. </div>
  158. <div class="record-block">
  159. <h3>Leaf Certificate PEM</h3>
  160. <pre>${escapeHTML(data.certificatePem)}</pre>
  161. </div>
  162. `;
  163. }
  164. function renderPEMResult(data) {
  165. return `
  166. ${renderMetaGrid([
  167. ["PEM Blocks", escapeHTML(String(data.count || 0))],
  168. ])}
  169. ${(data.blocks || []).map((block, index) => `
  170. <div class="record-block">
  171. <h3>Block ${index + 1}: ${escapeHTML(block.kind || "unknown")} (${escapeHTML(block.pemType || "")})</h3>
  172. ${renderMetaGrid([
  173. ["Subject", escapeHTML(block.subject || "None")],
  174. ["Issuer", escapeHTML(block.issuer || "None")],
  175. ["Serial Number", escapeHTML(block.serialNumber || "None")],
  176. ["Valid From", escapeHTML(block.notBefore || "None")],
  177. ["Valid Until", escapeHTML(block.notAfter || "None")],
  178. ["Algorithm", escapeHTML(block.algorithm || block.publicKeyAlgorithm || "None")],
  179. ["Signature Algorithm", escapeHTML(block.signatureAlgorithm || "None")],
  180. ["Size", escapeHTML(block.size ? `${block.size}-bit` : "None")],
  181. ["Curve", escapeHTML(block.curve || "None")],
  182. ["DNS Names", escapeHTML((block.dnsNames || []).join(", ") || "None")],
  183. ["Emails", escapeHTML((block.emailAddresses || []).join(", ") || "None")],
  184. ["CA", escapeHTML(block.isCA === true ? "Yes" : block.isCA === false ? "No" : "None")],
  185. ])}
  186. </div>
  187. `).join("")}
  188. `;
  189. }
  190. function renderCopyablePEMBlock(title, value) {
  191. return `
  192. <div class="record-block">
  193. <div class="record-heading">
  194. <h3>${escapeHTML(title)}</h3>
  195. <button type="button" class="copy-button" data-copy-value="${escapeHTML(value || "")}">Copy</button>
  196. </div>
  197. <pre>${escapeHTML(value || "")}</pre>
  198. </div>
  199. `;
  200. }
  201. function renderMetaGrid(items) {
  202. return `
  203. <div class="meta-grid">
  204. ${items.map(([label, value]) => `
  205. <div class="meta-item">
  206. <strong>${label}</strong>
  207. <span>${value || "None"}</span>
  208. </div>
  209. `).join("")}
  210. </div>
  211. `;
  212. }
  213. function renderListBlock(title, values = []) {
  214. return `
  215. <div class="record-block">
  216. <h3>${title}</h3>
  217. ${values.length ? `<ul>${values.map((value) => `<li>${escapeHTML(value)}</li>`).join("")}</ul>` : "<p>No records returned.</p>"}
  218. </div>
  219. `;
  220. }
  221. function renderErrorBlock(errors = {}) {
  222. const entries = Object.entries(errors);
  223. if (!entries.length) {
  224. return "";
  225. }
  226. return `
  227. <div class="record-block">
  228. <h3>Lookup Notes</h3>
  229. <ul>${entries.map(([kind, message]) => `<li>${escapeHTML(kind)}: ${escapeHTML(message)}</li>`).join("")}</ul>
  230. </div>
  231. `;
  232. }
  233. async function copyText(value) {
  234. if (navigator.clipboard && window.isSecureContext) {
  235. await navigator.clipboard.writeText(value);
  236. return;
  237. }
  238. const textarea = document.createElement("textarea");
  239. textarea.value = value;
  240. textarea.setAttribute("readonly", "");
  241. textarea.style.position = "absolute";
  242. textarea.style.left = "-9999px";
  243. document.body.appendChild(textarea);
  244. textarea.select();
  245. const success = document.execCommand("copy");
  246. document.body.removeChild(textarea);
  247. if (!success) {
  248. throw new Error("copy failed");
  249. }
  250. }
  251. function downloadBase64File(base64Value, filename, mimeType) {
  252. const binary = atob(base64Value || "");
  253. const bytes = new Uint8Array(binary.length);
  254. for (let index = 0; index < binary.length; index += 1) {
  255. bytes[index] = binary.charCodeAt(index);
  256. }
  257. const blob = new Blob([bytes], { type: mimeType });
  258. const url = URL.createObjectURL(blob);
  259. const link = document.createElement("a");
  260. link.href = url;
  261. link.download = filename;
  262. document.body.appendChild(link);
  263. link.click();
  264. document.body.removeChild(link);
  265. URL.revokeObjectURL(url);
  266. }
  267. function escapeHTML(value) {
  268. return String(value ?? "")
  269. .replaceAll("&", "&amp;")
  270. .replaceAll("<", "&lt;")
  271. .replaceAll(">", "&gt;")
  272. .replaceAll('"', "&quot;")
  273. .replaceAll("'", "&#39;");
  274. }