From ba04eaf573df3324b25c96d07ea8cd5e28b9b0cf Mon Sep 17 00:00:00 2001 From: nercone-dev Date: Wed, 22 Apr 2026 14:58:18 +0900 Subject: [PATCH] -- --- .../tools/tls-test/assets/tls-test-results.js | 102 +++++++----------- public/tools/tls-test/assets/tls-test.css | 75 +++++-------- public/tools/tls-test/assets/tls-test.js | 33 +++--- src/nercone_website/server.py | 46 ++------ .../tools/tls_test/__init__.py | 12 ++- src/nercone_website/tools/tls_test/views.py | 39 ++++++- 6 files changed, 131 insertions(+), 176 deletions(-) diff --git a/public/tools/tls-test/assets/tls-test-results.js b/public/tools/tls-test/assets/tls-test-results.js index 91f021f..cdb3646 100644 --- a/public/tools/tls-test/assets/tls-test-results.js +++ b/public/tools/tls-test/assets/tls-test-results.js @@ -30,39 +30,50 @@ } } - // ---- JSON-copy button (inside the JSON tab) ---- - const copyJsonBtn = document.getElementById("tls-copy-json"); - const rawJsonEl = document.getElementById("tls-raw-json"); - if (copyJsonBtn && rawJsonEl) { - const origLabel = copyJsonBtn.textContent; - copyJsonBtn.addEventListener("click", async () => { - const text = rawJsonEl.textContent || ""; - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(text); - } else { - const ta = document.createElement("textarea"); - ta.value = text; - ta.style.position = "fixed"; - ta.style.opacity = "0"; - document.body.appendChild(ta); - ta.select(); - try { document.execCommand("copy"); } catch (_) {} - document.body.removeChild(ta); - } - copyJsonBtn.textContent = "コピーしました"; - copyJsonBtn.classList.add("is-done"); - setTimeout(() => { - copyJsonBtn.textContent = origLabel; - copyJsonBtn.classList.remove("is-done"); - }, 1500); - } catch (_) { - copyJsonBtn.textContent = "コピー失敗"; - setTimeout(() => { copyJsonBtn.textContent = origLabel; }, 1500); + // ---- Shared copy helper (clipboard API + textarea fallback) ---- + async function copyText(text) { + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + return true; } + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand("copy"); } catch (_) {} + document.body.removeChild(ta); + return true; + } catch (_) { + return false; + } + } + + // Swap a button's label and is-done class for 1.5s based on action result. + function bindCopyButton(btn, getText) { + if (!btn) return; + const orig = btn.textContent; + btn.addEventListener("click", async () => { + const ok = await copyText(getText()); + btn.textContent = ok ? "コピーしました" : "コピー失敗"; + if (ok) btn.classList.add("is-done"); + setTimeout(() => { + btn.textContent = orig; + btn.classList.remove("is-done"); + }, 1500); }); } + // ---- JSON-copy button (inside the JSON tab) ---- + const rawJsonEl = document.getElementById("tls-raw-json"); + bindCopyButton(document.getElementById("tls-copy-json"), () => (rawJsonEl && rawJsonEl.textContent) || ""); + + // ---- Copy-link button ---- + const copyLinkBtn = document.getElementById("tls-copy-link"); + bindCopyButton(copyLinkBtn, () => (copyLinkBtn && copyLinkBtn.dataset.link) || location.href); + // ---- PDF / print button ---- const printBtn = document.getElementById("tls-print-pdf"); if (printBtn) { @@ -88,37 +99,4 @@ } }); } - - // ---- Copy-link button ---- - const copyBtn = document.getElementById("tls-copy-link"); - if (copyBtn) { - const origLabel = copyBtn.textContent; - copyBtn.addEventListener("click", async () => { - const link = copyBtn.dataset.link || location.href; - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(link); - } else { - // Fallback: use a hidden textarea + document.execCommand - const ta = document.createElement("textarea"); - ta.value = link; - ta.style.position = "fixed"; - ta.style.opacity = "0"; - document.body.appendChild(ta); - ta.select(); - try { document.execCommand("copy"); } catch (_) {} - document.body.removeChild(ta); - } - copyBtn.textContent = "コピーしました"; - copyBtn.classList.add("is-done"); - setTimeout(() => { - copyBtn.textContent = origLabel; - copyBtn.classList.remove("is-done"); - }, 1500); - } catch (_) { - copyBtn.textContent = "コピー失敗"; - setTimeout(() => { copyBtn.textContent = origLabel; }, 1500); - } - }); - } })(); diff --git a/public/tools/tls-test/assets/tls-test.css b/public/tools/tls-test/assets/tls-test.css index 90fae4a..8497c0b 100644 --- a/public/tools/tls-test/assets/tls-test.css +++ b/public/tools/tls-test/assets/tls-test.css @@ -1,11 +1,4 @@ -/* ===================================================== - * Nercone TLS Test — shared styles - * Palette comes from /public/assets/css/style.css - * #1A1A1A = page bg #272727 = block bg - * #202020 = input bg #3a3a3a = border - * #E0E0E0 = tx #939393 = light-grey-alt - * #00C878 = bright-green #00C0FF = bright-blue - * ===================================================== */ +/* Nercone TLS Test — shared styles. Palette uses CSS vars from /assets/css/style.css. */ /* ------ Landing page ------ */ .tls-landing { @@ -22,7 +15,6 @@ margin: 0 0 8px 0; font-size: clamp(40pt, 6vw, 64pt); line-height: 1.1; - color: #FFFFFF; letter-spacing: -0.01em; } .tls-landing-subtitle { @@ -40,21 +32,21 @@ flex: 1; min-width: 0; background-color: #3a3a3a; - color: #E0E0E0; + color: var(--color-light-grey); border: 1px solid #4a4a4a; border-radius: 6px; padding: 12px 16px; font-family: inherit; font-size: 13pt; } -.tls-landing-input::placeholder { color: #939393; } +.tls-landing-input::placeholder { color: var(--color-light-grey-alt); } .tls-landing-input:focus { outline: none; - border-color: #00C0FF; + border-color: var(--color-bright-blue); } .tls-landing-submit { background-color: #0096D0; - color: #FFFFFF; + color: var(--color-white); border: none; border-radius: 6px; padding: 12px 28px; @@ -75,10 +67,10 @@ flex-wrap: wrap; } .tls-landing-links a { - color: #939393; + color: var(--color-light-grey-alt); text-decoration: underline; } -.tls-landing-links a:hover { color: #E0E0E0; } +.tls-landing-links a:hover { color: var(--color-light-grey); } .tls-aux-section { max-width: 900px; @@ -110,9 +102,6 @@ .tls-status-testid { margin: 10px 0 0 0; } -.tls-status-testid code { - font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; -} .tls-progress-track { width: 100%; height: 6px; @@ -122,7 +111,7 @@ } .tls-progress-bar { height: 100%; - background-color: #00C878; + background-color: var(--color-bright-green); transition: width 0.25s ease; } @@ -130,7 +119,7 @@ .tls-log-wrap { width: min(1100px, 100%); margin: 0 auto; - background-color: #272727; + background-color: var(--color-dark-grey-alt); border-radius: 8px; padding: 18px 22px; text-align: left; @@ -183,10 +172,10 @@ .tls-results-header { position: static; display: block; - background-color: #272727; + background-color: var(--color-dark-grey-alt); padding: 28px clamp(16px, 4vw, 40px) 0; margin-bottom: 0; - border-bottom: 1px solid #1a1a1a; + border-bottom: 1px solid var(--color-dark-grey); backdrop-filter: none; } .tls-results-header::before { @@ -234,7 +223,6 @@ margin: 0 0 4px 0; font-size: 22pt; font-weight: 700; - color: #FFFFFF; word-break: break-all; } .tls-results-metaline { @@ -249,7 +237,6 @@ font-size: 10pt; } .tls-results-testid code { - font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; padding: 0; } .tls-results-actions { @@ -271,15 +258,15 @@ } .tls-btn-secondary { background-color: #3a3a3a; - color: #E0E0E0; + color: var(--color-light-grey); } .tls-btn-secondary:hover { background-color: #4a4a4a; } .tls-btn-primary { background-color: #0096D0; - color: #FFFFFF; + color: var(--color-white); } .tls-btn-primary:hover { background-color: #00B4F0; } -.tls-btn-primary.is-done { background-color: #00A050; } +.tls-btn-primary.is-done { background-color: var(--color-green); } /* Tabs — sit at the bottom of the results header banner. */ .tls-results-header .tls-tabs { @@ -291,11 +278,11 @@ flex-wrap: wrap; gap: 0; margin: 12px 0 16px 0; - border-bottom: 1px solid #1a1a1a; + border-bottom: 1px solid var(--color-dark-grey); } .tls-tab { background: transparent; - color: #939393; + color: var(--color-light-grey-alt); border: none; border-bottom: 2px solid transparent; padding: 10px 18px 10px 18px; @@ -305,12 +292,12 @@ margin-bottom: -1px; } .tls-tab:hover { - color: #E0E0E0; + color: var(--color-light-grey); } .tls-tab.is-active { - color: #FFFFFF; + color: var(--color-white); font-weight: 600; - border-bottom-color: #E0E0E0; + border-bottom-color: var(--color-light-grey); } .tls-tab-panels { padding: 0 clamp(16px, 4vw, 40px) 40px; @@ -332,7 +319,6 @@ .tls-section-title { font-size: 14pt; font-weight: 600; - color: #FFFFFF; margin: 0 0 10px 0; padding-bottom: 6px; border-bottom: 1px solid #3a3a3a; @@ -346,7 +332,7 @@ background-color: #202020; border-radius: 6px; overflow: hidden; - color: #E0E0E0; + color: var(--color-light-grey); } .tls-table th, .tls-table td { @@ -357,7 +343,7 @@ } .tls-table thead th { background-color: #2a2a2a; - color: #939393; + color: var(--color-light-grey-alt); font-weight: 500; font-size: 10.5pt; text-transform: uppercase; @@ -368,7 +354,6 @@ border-bottom: none; } .tls-table code { - font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; font-size: 10.5pt; background-color: transparent; padding: 0; @@ -381,7 +366,7 @@ .tls-cipher-version { margin: 0 0 6px 0; font-size: 11.5pt; - color: #E0E0E0; + color: var(--color-light-grey); font-weight: 600; } .tls-cipher-list { @@ -395,9 +380,8 @@ gap: 4px 16px; } .tls-cipher-list code { - font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; font-size: 10pt; - color: #E0E0E0; + color: var(--color-light-grey); background: transparent; padding: 0; } @@ -421,36 +405,28 @@ margin: 0; } .tls-raw code { - font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; white-space: pre; background: transparent; padding: 0; } -/* ------------------------------------------------------------------ - * Print stylesheet — "PDFをダウンロード" uses window.print() which - * lets the user save the page as a PDF from the browser print dialog. - * Hide chrome (tabs, buttons, site header/footer) and expand all tab - * panels so the resulting PDF contains every finding. - * ------------------------------------------------------------------ */ +/* Print stylesheet — "PDFをダウンロード" uses window.print(). Hide chrome and + * expand all tab panels so the resulting PDF contains every finding. */ @media print { html, body { background: #ffffff !important; color: #101010 !important; } - /* Hide site-wide chrome and interactive controls. */ footer, .tls-tabs, .tls-results-actions, #tls-copy-json, script, .tls-landing-links { display: none !important; } - /* Keep the results-header banner but make it paper-friendly. */ .tls-results-header { background: transparent !important; border: 0 !important; padding: 0 0 16px 0 !important; border-bottom: 1px solid #cccccc !important; } - /* Expand every tab panel so the full report prints. */ .tls-tab-panels { padding: 0 !important; } @@ -481,7 +457,6 @@ border-bottom: 1px dotted #cccccc; padding: 4px 0; } - /* Rank badge keeps its colour via currentColor. */ .tls-rank-badge { print-color-adjust: exact; -webkit-print-color-adjust: exact; diff --git a/public/tools/tls-test/assets/tls-test.js b/public/tools/tls-test/assets/tls-test.js index 9725ee4..a7eb5ec 100644 --- a/public/tools/tls-test/assets/tls-test.js +++ b/public/tools/tls-test/assets/tls-test.js @@ -7,24 +7,15 @@ const logEl = document.getElementById("tls-log"); const verbEl = document.getElementById("tls-status-verb"); - // Colors for the severity label tag. Order Good→Bad maps to - // bright-{green,blue,yellow,red,purple}. Matches SEVERITY_COLORS in - // schemas.py so the live log and results page render identically. - const SEV_COLOR = { - good: "bright-green", - normal: "bright-blue", - notgood: "bright-yellow", - bad: "bright-red", - serious: "bright-purple", - info: "light-grey-alt", - }; - const SEV_LABEL = { - good: "GOOD", - normal: "NORMAL", - notgood: "NOT GOOD", - bad: "BAD", - serious: "SERIOUS", - info: "INFO", + // Severity label tag. Matches SEVERITY_COLORS in schemas.py so the live + // log and results page render identically. + const SEV = { + good: { color: "bright-green", label: "GOOD" }, + normal: { color: "bright-blue", label: "NORMAL" }, + notgood: { color: "bright-yellow", label: "NOT GOOD" }, + bad: { color: "bright-red", label: "BAD" }, + serious: { color: "bright-purple", label: "SERIOUS" }, + info: { color: "light-grey-alt", label: "INFO" }, }; let reconnectAttempts = 0; @@ -50,10 +41,10 @@ const msg = document.createElement("span"); msg.className = "tls-log-msg"; - if (severity && SEV_LABEL[severity] && severity !== "info") { + if (severity && SEV[severity] && severity !== "info") { const sev = document.createElement("span"); - sev.className = "text-" + (SEV_COLOR[severity] || "light-grey") + " font-bold"; - sev.textContent = SEV_LABEL[severity]; + sev.className = "text-" + SEV[severity].color + " font-bold"; + sev.textContent = SEV[severity].label; msg.appendChild(sev); msg.appendChild(document.createTextNode(" ")); } diff --git a/src/nercone_website/server.py b/src/nercone_website/server.py index a94d306..6dfadf4 100644 --- a/src/nercone_website/server.py +++ b/src/nercone_website/server.py @@ -11,18 +11,17 @@ from bs4 import BeautifulSoup from markitdown import MarkItDown from datetime import datetime, timezone from contextlib import asynccontextmanager -from fastapi import FastAPI, Request, Response, WebSocket, WebSocketDisconnect, HTTPException +from fastapi import FastAPI, Request, Response, WebSocket, HTTPException from fastapi.templating import Jinja2Templates from fastapi.responses import PlainTextResponse, JSONResponse, FileResponse, RedirectResponse from jinja2.exceptions import TemplateNotFound from .error import error_page from .database import AccessCounter from .middleware import Middleware, server_version, onion_hostname -from .tools.tls_test import TlsJobQueue, TlsTestDB, tls_submit, tls_api_submit, tls_results_context -from .tools.tls_test.engine import run_full_scan - -tls_test_db = TlsTestDB() -tls_test_queue = TlsJobQueue(tls_test_db, run_full_scan) +from .tools.tls_test import ( + tls_test_db, tls_test_queue, + tls_submit, tls_api_submit, tls_results_context, tls_websocket_handler, +) @asynccontextmanager async def lifespan(app: FastAPI): @@ -179,40 +178,7 @@ async def tls_test_results_page(request: Request, test_id: str) -> Response: @app.websocket("/tools/tls-test/ws/{test_id}") async def tls_test_ws(websocket: WebSocket, test_id: str): - job = tls_test_db.get_job(test_id) - if not job: - await websocket.close(code=4404) - return - await websocket.accept() - tls_test_queue.add_subscriber(test_id, websocket) - try: - history = tls_test_db.get_progress(test_id) - await websocket.send_text(json.dumps({ - "type": "history", - "status": job.get("status"), - "target": job.get("target"), - "entries": history, - })) - if job.get("status") == "done": - await websocket.send_text(json.dumps({ - "type": "done", - "redirect": f"/tools/tls-test/results/{test_id}/", - "rank": job.get("rank"), - "score": job.get("score"), - })) - await websocket.close() - return - while True: - try: - await websocket.receive_text() - except WebSocketDisconnect: - break - except WebSocketDisconnect: - pass - except Exception: - pass - finally: - tls_test_queue.remove_subscriber(test_id, websocket) + await tls_websocket_handler(websocket, test_id, tls_test_db, tls_test_queue) @app.api_route("/api/tools/tls-test/scan", methods=["POST"]) async def tls_test_api_scan(request: Request) -> Response: diff --git a/src/nercone_website/tools/tls_test/__init__.py b/src/nercone_website/tools/tls_test/__init__.py index 1fde8e8..2360bc9 100644 --- a/src/nercone_website/tools/tls_test/__init__.py +++ b/src/nercone_website/tools/tls_test/__init__.py @@ -1,5 +1,13 @@ from .runner import TlsJobQueue from .db import TlsTestDB -from .views import tls_submit, tls_api_submit, tls_results_context +from .engine import run_full_scan +from .views import tls_submit, tls_api_submit, tls_results_context, tls_websocket_handler -__all__ = ["TlsJobQueue", "TlsTestDB", "tls_submit", "tls_api_submit", "tls_results_context"] +tls_test_db = TlsTestDB() +tls_test_queue = TlsJobQueue(tls_test_db, run_full_scan) + +__all__ = [ + "TlsJobQueue", "TlsTestDB", + "tls_submit", "tls_api_submit", "tls_results_context", "tls_websocket_handler", + "tls_test_db", "tls_test_queue", +] diff --git a/src/nercone_website/tools/tls_test/views.py b/src/nercone_website/tools/tls_test/views.py index 4dbd680..8f0340a 100644 --- a/src/nercone_website/tools/tls_test/views.py +++ b/src/nercone_website/tools/tls_test/views.py @@ -1,6 +1,7 @@ from __future__ import annotations +import json import datetime -from fastapi import Request +from fastapi import Request, WebSocket, WebSocketDisconnect from fastapi.templating import Jinja2Templates from fastapi.responses import Response from .engine import validate_tls_target @@ -101,3 +102,39 @@ def tls_results_context(job: dict, test_id: str, request: Request, tls_test_db) "log_entries": log_entries, "history": history, } + + +async def tls_websocket_handler(websocket: WebSocket, test_id: str, tls_test_db, tls_test_queue) -> None: + job = tls_test_db.get_job(test_id) + if not job: + await websocket.close(code=4404) + return + await websocket.accept() + tls_test_queue.add_subscriber(test_id, websocket) + try: + await websocket.send_text(json.dumps({ + "type": "history", + "status": job.get("status"), + "target": job.get("target"), + "entries": tls_test_db.get_progress(test_id), + })) + if job.get("status") == "done": + await websocket.send_text(json.dumps({ + "type": "done", + "redirect": f"/tools/tls-test/results/{test_id}/", + "rank": job.get("rank"), + "score": job.get("score"), + })) + await websocket.close() + return + while True: + try: + await websocket.receive_text() + except WebSocketDisconnect: + break + except WebSocketDisconnect: + pass + except Exception: + pass + finally: + tls_test_queue.remove_subscriber(test_id, websocket)