app.js 11 KB

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