소스 검색

Update UI. Add features.

Ben Allen 1 주 전
부모
커밋
871ab9dbf6
6개의 변경된 파일113개의 추가작업 그리고 41개의 파일을 삭제
  1. 2 2
      README.md
  2. 35 4
      main.go
  3. 2 2
      main_test.go
  4. 65 28
      static/app.js
  5. 4 0
      static/styles.css
  6. 5 5
      templates/index.html

+ 2 - 2
README.md

@@ -56,7 +56,7 @@ Look up common DNS records for a hostname:
 - `NS`
 - `TXT`
 
-### SSL Check
+### TSL Check
 
 Enter a URL and the app connects to the remote server over TLS and displays:
 
@@ -83,7 +83,7 @@ The UI uses these JSON endpoints:
 
 - `POST /api/tls/generate`
 - `POST /api/dns/lookup`
-- `POST /api/ssl/check`
+- `POST /api/tsl/check`
 - `POST /api/pem/check`
 
 ## Test and build

+ 35 - 4
main.go

@@ -12,6 +12,7 @@ import (
 	"encoding/pem"
 	"errors"
 	"fmt"
+	"math"
 	"math/big"
 	"net"
 	"net/http"
@@ -40,7 +41,7 @@ type dnsLookupRequest struct {
 	Host string `json:"host"`
 }
 
-type sslCheckRequest struct {
+type tslCheckRequest struct {
 	URL string `json:"url"`
 }
 
@@ -91,7 +92,7 @@ func newRouter() *gin.Engine {
 	{
 		api.POST("/tls/generate", handleTLSGenerate)
 		api.POST("/dns/lookup", handleDNSLookup)
-		api.POST("/ssl/check", handleSSLCheck)
+		api.POST("/tsl/check", handleTSLCheck)
 		api.POST("/pem/check", handlePEMCheck)
 	}
 
@@ -246,8 +247,8 @@ func handleDNSLookup(c *gin.Context) {
 	})
 }
 
-func handleSSLCheck(c *gin.Context) {
-	var req sslCheckRequest
+func handleTSLCheck(c *gin.Context) {
+	var req tslCheckRequest
 	if err := c.ShouldBindJSON(&req); err != nil {
 		c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
 		return
@@ -304,6 +305,16 @@ func handleSSLCheck(c *gin.Context) {
 		})
 	}
 
+	validDays := int(math.Ceil(leaf.NotAfter.Sub(leaf.NotBefore).Hours() / 24))
+	if validDays < 1 {
+		validDays = 1
+	}
+
+	keySize := publicKeySize(leaf.PublicKey)
+	if keySize == 0 {
+		keySize = 2048
+	}
+
 	c.JSON(http.StatusOK, gin.H{
 		"url":                parsedURL.String(),
 		"serverName":         host,
@@ -312,6 +323,17 @@ func handleSSLCheck(c *gin.Context) {
 		"negotiatedProtocol": state.NegotiatedProtocol,
 		"certificatePem":     string(certPEM),
 		"chain":              chain,
+		"leafTemplate": gin.H{
+			"commonName":         leaf.Subject.CommonName,
+			"organization":       strings.Join(leaf.Subject.Organization, ", "),
+			"organizationalUnit": strings.Join(leaf.Subject.OrganizationalUnit, ", "),
+			"locality":           strings.Join(leaf.Subject.Locality, ", "),
+			"state":              strings.Join(leaf.Subject.Province, ", "),
+			"country":            strings.Join(leaf.Subject.Country, ", "),
+			"dnsNames":           strings.Join(leaf.DNSNames, ", "),
+			"validDays":          validDays,
+			"keySize":            keySize,
+		},
 	})
 }
 
@@ -660,6 +682,15 @@ func sanitizeFilename(value string) string {
 	return result
 }
 
+func publicKeySize(key any) int {
+	switch typed := key.(type) {
+	case *rsa.PublicKey:
+		return typed.N.BitLen()
+	default:
+		return 0
+	}
+}
+
 func getenv(key, fallback string) string {
 	value := strings.TrimSpace(os.Getenv(key))
 	if value == "" {

+ 2 - 2
main_test.go

@@ -81,7 +81,7 @@ func TestTLSGenerateRoute(t *testing.T) {
 	}
 }
 
-func TestSSLCheckRejectsEmptyURL(t *testing.T) {
+func TestTSLCheckRejectsEmptyURL(t *testing.T) {
 	router := newRouter()
 	recorder := httptest.NewRecorder()
 
@@ -92,7 +92,7 @@ func TestSSLCheckRejectsEmptyURL(t *testing.T) {
 		t.Fatalf("marshal payload: %v", err)
 	}
 
-	request := httptest.NewRequest(http.MethodPost, "/api/ssl/check", bytes.NewReader(payload))
+	request := httptest.NewRequest(http.MethodPost, "/api/tsl/check", bytes.NewReader(payload))
 	request.Header.Set("Content-Type", "application/json")
 
 	router.ServeHTTP(recorder, request)

+ 65 - 28
static/app.js

@@ -1,40 +1,40 @@
 const toolButtons = document.querySelectorAll(".tool-button");
 const panels = document.querySelectorAll(".panel");
+let currentTSLTemplate = null;
 
 document.addEventListener("click", async (event) => {
   const button = event.target.closest("[data-copy-value]");
-  if (!button) {
+  if (button) {
+    const value = button.dataset.copyValue || "";
+    const originalLabel = button.textContent;
+
+    try {
+      await copyText(value);
+      button.textContent = "Copied";
+      button.classList.add("copied");
+      setTimeout(() => {
+        button.textContent = originalLabel;
+        button.classList.remove("copied");
+      }, 1500);
+    } catch (error) {
+      button.textContent = "Copy failed";
+      setTimeout(() => {
+        button.textContent = originalLabel;
+      }, 1500);
+    }
     return;
   }
 
-  const value = button.dataset.copyValue || "";
-  const originalLabel = button.textContent;
-
-  try {
-    await copyText(value);
-    button.textContent = "Copied";
-    button.classList.add("copied");
-    setTimeout(() => {
-      button.textContent = originalLabel;
-      button.classList.remove("copied");
-    }, 1500);
-  } catch (error) {
-    button.textContent = "Copy failed";
-    setTimeout(() => {
-      button.textContent = originalLabel;
-    }, 1500);
+  const prefillButton = event.target.closest("[data-action='prefill-tls']");
+  if (prefillButton && currentTSLTemplate) {
+    populateTLSForm(currentTSLTemplate);
+    activateTool("tls-generator");
   }
 });
 
 toolButtons.forEach((button) => {
   button.addEventListener("click", () => {
-    const target = button.dataset.tool;
-
-    toolButtons.forEach((item) => item.classList.remove("active"));
-    panels.forEach((panel) => panel.classList.remove("active"));
-
-    button.classList.add("active");
-    document.getElementById(target).classList.add("active");
+    activateTool(button.dataset.tool);
   });
 });
 
@@ -69,13 +69,15 @@ document.getElementById("dns-form").addEventListener("submit", async (event) =>
   }, renderDNSResult, document.getElementById("dns-result"));
 });
 
-document.getElementById("ssl-form").addEventListener("submit", async (event) => {
+document.getElementById("tsl-form").addEventListener("submit", async (event) => {
   event.preventDefault();
   const form = new FormData(event.currentTarget);
 
-  await submitJSON("/api/ssl/check", {
+  await submitJSON("/api/tsl/check", {
     url: form.get("url"),
-  }, renderSSLResult, document.getElementById("ssl-result"));
+  }, renderTSLResult, document.getElementById("tsl-result"), (data) => {
+    currentTSLTemplate = data.leafTemplate || null;
+  });
 });
 
 document.getElementById("pem-form").addEventListener("submit", async (event) => {
@@ -160,7 +162,7 @@ function renderDNSResult(data) {
   `;
 }
 
-function renderSSLResult(data) {
+function renderTSLResult(data) {
   const leaf = (data.chain || [])[0] || {};
 
   return `
@@ -174,6 +176,9 @@ function renderSSLResult(data) {
       ["Leaf Issuer", escapeHTML(leaf.issuer || "")],
       ["Leaf Valid Until", escapeHTML(leaf.notAfter || "")],
     ])}
+    <div class="record-block">
+      <button type="button" class="secondary-button prefill-button" data-action="prefill-tls">Use In TLS Generator</button>
+    </div>
     <div class="record-block">
       <h3>Certificate Chain</h3>
       <pre>${escapeHTML(JSON.stringify(data.chain, null, 2))}</pre>
@@ -185,6 +190,38 @@ function renderSSLResult(data) {
   `;
 }
 
+function activateTool(target) {
+  toolButtons.forEach((item) => item.classList.remove("active"));
+  panels.forEach((panel) => panel.classList.remove("active"));
+
+  document.querySelector(`.tool-button[data-tool="${target}"]`)?.classList.add("active");
+  document.getElementById(target)?.classList.add("active");
+}
+
+function populateTLSForm(template) {
+  const form = document.getElementById("tls-form");
+  if (!form || !template) {
+    return;
+  }
+
+  setFormValue(form, "commonName", template.commonName);
+  setFormValue(form, "organization", template.organization);
+  setFormValue(form, "organizationalUnit", template.organizationalUnit);
+  setFormValue(form, "locality", template.locality);
+  setFormValue(form, "state", template.state);
+  setFormValue(form, "country", template.country);
+  setFormValue(form, "dnsNames", template.dnsNames);
+  setFormValue(form, "validDays", template.validDays);
+  setFormValue(form, "keySize", template.keySize);
+}
+
+function setFormValue(form, name, value) {
+  const field = form.elements.namedItem(name);
+  if (field) {
+    field.value = value ?? "";
+  }
+}
+
 function renderPEMResult(data) {
   return `
     ${renderMetaGrid([

+ 4 - 0
static/styles.css

@@ -186,6 +186,10 @@ h2 {
   border: 1px solid rgba(94, 234, 212, 0.28);
 }
 
+.prefill-button {
+  width: auto;
+}
+
 .result-card {
   background: var(--panel-strong);
   border: 1px solid var(--border);

+ 5 - 5
templates/index.html

@@ -19,7 +19,7 @@
         <nav class="tool-menu" aria-label="Tool menu">
           <button class="tool-button active" data-tool="tls-generator">TLS Generator</button>
           <button class="tool-button" data-tool="dns-lookup">DNS Lookup</button>
-          <button class="tool-button" data-tool="ssl-check">SSL Check</button>
+          <button class="tool-button" data-tool="tsl-check">TSL Check</button>
           <button class="tool-button" data-tool="pem-check">PEM Check</button>
         </nav>
       </aside>
@@ -100,22 +100,22 @@
           <div id="dns-result" class="result-card empty">Lookups will show A/AAAA, CNAME, MX, NS, and TXT records here.</div>
         </section>
 
-        <section class="panel" id="ssl-check">
+        <section class="panel" id="tsl-check">
           <div class="panel-header">
             <div>
               <p class="eyebrow">Inspection Tool</p>
-              <h2>SSL Check</h2>
+              <h2>TSL Check</h2>
             </div>
             <p class="panel-copy">Connect to a TLS endpoint and display the negotiated certificate.</p>
           </div>
-          <form id="ssl-form" class="tool-form">
+          <form id="tsl-form" class="tool-form">
             <label>
               URL
               <input type="text" name="url" placeholder="https://example.com" required />
             </label>
             <button type="submit">Retrieve Certificate</button>
           </form>
-          <div id="ssl-result" class="result-card empty">Certificate details and PEM output will appear after a check.</div>
+          <div id="tsl-result" class="result-card empty">Certificate details and PEM output will appear after a check.</div>
         </section>
 
         <section class="panel" id="pem-check">