This commit is contained in:
2026-04-22 14:58:18 +09:00
parent 4d618f3b22
commit ba04eaf573
6 changed files with 131 additions and 176 deletions
@@ -30,17 +30,13 @@
} }
} }
// ---- JSON-copy button (inside the JSON tab) ---- // ---- Shared copy helper (clipboard API + textarea fallback) ----
const copyJsonBtn = document.getElementById("tls-copy-json"); async function copyText(text) {
const rawJsonEl = document.getElementById("tls-raw-json");
if (copyJsonBtn && rawJsonEl) {
const origLabel = copyJsonBtn.textContent;
copyJsonBtn.addEventListener("click", async () => {
const text = rawJsonEl.textContent || "";
try { try {
if (navigator.clipboard && navigator.clipboard.writeText) { if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
} else { return true;
}
const ta = document.createElement("textarea"); const ta = document.createElement("textarea");
ta.value = text; ta.value = text;
ta.style.position = "fixed"; ta.style.position = "fixed";
@@ -49,20 +45,35 @@
ta.select(); ta.select();
try { document.execCommand("copy"); } catch (_) {} try { document.execCommand("copy"); } catch (_) {}
document.body.removeChild(ta); document.body.removeChild(ta);
} return true;
copyJsonBtn.textContent = "コピーしました";
copyJsonBtn.classList.add("is-done");
setTimeout(() => {
copyJsonBtn.textContent = origLabel;
copyJsonBtn.classList.remove("is-done");
}, 1500);
} catch (_) { } catch (_) {
copyJsonBtn.textContent = "コピー失敗"; return false;
setTimeout(() => { copyJsonBtn.textContent = origLabel; }, 1500);
} }
}
// 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 ---- // ---- PDF / print button ----
const printBtn = document.getElementById("tls-print-pdf"); const printBtn = document.getElementById("tls-print-pdf");
if (printBtn) { 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);
}
});
}
})(); })();
+25 -50
View File
@@ -1,11 +1,4 @@
/* ===================================================== /* Nercone TLS Test — shared styles. Palette uses CSS vars from /assets/css/style.css. */
* 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
* ===================================================== */
/* ------ Landing page ------ */ /* ------ Landing page ------ */
.tls-landing { .tls-landing {
@@ -22,7 +15,6 @@
margin: 0 0 8px 0; margin: 0 0 8px 0;
font-size: clamp(40pt, 6vw, 64pt); font-size: clamp(40pt, 6vw, 64pt);
line-height: 1.1; line-height: 1.1;
color: #FFFFFF;
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
.tls-landing-subtitle { .tls-landing-subtitle {
@@ -40,21 +32,21 @@
flex: 1; flex: 1;
min-width: 0; min-width: 0;
background-color: #3a3a3a; background-color: #3a3a3a;
color: #E0E0E0; color: var(--color-light-grey);
border: 1px solid #4a4a4a; border: 1px solid #4a4a4a;
border-radius: 6px; border-radius: 6px;
padding: 12px 16px; padding: 12px 16px;
font-family: inherit; font-family: inherit;
font-size: 13pt; font-size: 13pt;
} }
.tls-landing-input::placeholder { color: #939393; } .tls-landing-input::placeholder { color: var(--color-light-grey-alt); }
.tls-landing-input:focus { .tls-landing-input:focus {
outline: none; outline: none;
border-color: #00C0FF; border-color: var(--color-bright-blue);
} }
.tls-landing-submit { .tls-landing-submit {
background-color: #0096D0; background-color: #0096D0;
color: #FFFFFF; color: var(--color-white);
border: none; border: none;
border-radius: 6px; border-radius: 6px;
padding: 12px 28px; padding: 12px 28px;
@@ -75,10 +67,10 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.tls-landing-links a { .tls-landing-links a {
color: #939393; color: var(--color-light-grey-alt);
text-decoration: underline; text-decoration: underline;
} }
.tls-landing-links a:hover { color: #E0E0E0; } .tls-landing-links a:hover { color: var(--color-light-grey); }
.tls-aux-section { .tls-aux-section {
max-width: 900px; max-width: 900px;
@@ -110,9 +102,6 @@
.tls-status-testid { .tls-status-testid {
margin: 10px 0 0 0; margin: 10px 0 0 0;
} }
.tls-status-testid code {
font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace;
}
.tls-progress-track { .tls-progress-track {
width: 100%; width: 100%;
height: 6px; height: 6px;
@@ -122,7 +111,7 @@
} }
.tls-progress-bar { .tls-progress-bar {
height: 100%; height: 100%;
background-color: #00C878; background-color: var(--color-bright-green);
transition: width 0.25s ease; transition: width 0.25s ease;
} }
@@ -130,7 +119,7 @@
.tls-log-wrap { .tls-log-wrap {
width: min(1100px, 100%); width: min(1100px, 100%);
margin: 0 auto; margin: 0 auto;
background-color: #272727; background-color: var(--color-dark-grey-alt);
border-radius: 8px; border-radius: 8px;
padding: 18px 22px; padding: 18px 22px;
text-align: left; text-align: left;
@@ -183,10 +172,10 @@
.tls-results-header { .tls-results-header {
position: static; position: static;
display: block; display: block;
background-color: #272727; background-color: var(--color-dark-grey-alt);
padding: 28px clamp(16px, 4vw, 40px) 0; padding: 28px clamp(16px, 4vw, 40px) 0;
margin-bottom: 0; margin-bottom: 0;
border-bottom: 1px solid #1a1a1a; border-bottom: 1px solid var(--color-dark-grey);
backdrop-filter: none; backdrop-filter: none;
} }
.tls-results-header::before { .tls-results-header::before {
@@ -234,7 +223,6 @@
margin: 0 0 4px 0; margin: 0 0 4px 0;
font-size: 22pt; font-size: 22pt;
font-weight: 700; font-weight: 700;
color: #FFFFFF;
word-break: break-all; word-break: break-all;
} }
.tls-results-metaline { .tls-results-metaline {
@@ -249,7 +237,6 @@
font-size: 10pt; font-size: 10pt;
} }
.tls-results-testid code { .tls-results-testid code {
font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace;
padding: 0; padding: 0;
} }
.tls-results-actions { .tls-results-actions {
@@ -271,15 +258,15 @@
} }
.tls-btn-secondary { .tls-btn-secondary {
background-color: #3a3a3a; background-color: #3a3a3a;
color: #E0E0E0; color: var(--color-light-grey);
} }
.tls-btn-secondary:hover { background-color: #4a4a4a; } .tls-btn-secondary:hover { background-color: #4a4a4a; }
.tls-btn-primary { .tls-btn-primary {
background-color: #0096D0; background-color: #0096D0;
color: #FFFFFF; color: var(--color-white);
} }
.tls-btn-primary:hover { background-color: #00B4F0; } .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. */ /* Tabs — sit at the bottom of the results header banner. */
.tls-results-header .tls-tabs { .tls-results-header .tls-tabs {
@@ -291,11 +278,11 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 0; gap: 0;
margin: 12px 0 16px 0; margin: 12px 0 16px 0;
border-bottom: 1px solid #1a1a1a; border-bottom: 1px solid var(--color-dark-grey);
} }
.tls-tab { .tls-tab {
background: transparent; background: transparent;
color: #939393; color: var(--color-light-grey-alt);
border: none; border: none;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
padding: 10px 18px 10px 18px; padding: 10px 18px 10px 18px;
@@ -305,12 +292,12 @@
margin-bottom: -1px; margin-bottom: -1px;
} }
.tls-tab:hover { .tls-tab:hover {
color: #E0E0E0; color: var(--color-light-grey);
} }
.tls-tab.is-active { .tls-tab.is-active {
color: #FFFFFF; color: var(--color-white);
font-weight: 600; font-weight: 600;
border-bottom-color: #E0E0E0; border-bottom-color: var(--color-light-grey);
} }
.tls-tab-panels { .tls-tab-panels {
padding: 0 clamp(16px, 4vw, 40px) 40px; padding: 0 clamp(16px, 4vw, 40px) 40px;
@@ -332,7 +319,6 @@
.tls-section-title { .tls-section-title {
font-size: 14pt; font-size: 14pt;
font-weight: 600; font-weight: 600;
color: #FFFFFF;
margin: 0 0 10px 0; margin: 0 0 10px 0;
padding-bottom: 6px; padding-bottom: 6px;
border-bottom: 1px solid #3a3a3a; border-bottom: 1px solid #3a3a3a;
@@ -346,7 +332,7 @@
background-color: #202020; background-color: #202020;
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
color: #E0E0E0; color: var(--color-light-grey);
} }
.tls-table th, .tls-table th,
.tls-table td { .tls-table td {
@@ -357,7 +343,7 @@
} }
.tls-table thead th { .tls-table thead th {
background-color: #2a2a2a; background-color: #2a2a2a;
color: #939393; color: var(--color-light-grey-alt);
font-weight: 500; font-weight: 500;
font-size: 10.5pt; font-size: 10.5pt;
text-transform: uppercase; text-transform: uppercase;
@@ -368,7 +354,6 @@
border-bottom: none; border-bottom: none;
} }
.tls-table code { .tls-table code {
font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace;
font-size: 10.5pt; font-size: 10.5pt;
background-color: transparent; background-color: transparent;
padding: 0; padding: 0;
@@ -381,7 +366,7 @@
.tls-cipher-version { .tls-cipher-version {
margin: 0 0 6px 0; margin: 0 0 6px 0;
font-size: 11.5pt; font-size: 11.5pt;
color: #E0E0E0; color: var(--color-light-grey);
font-weight: 600; font-weight: 600;
} }
.tls-cipher-list { .tls-cipher-list {
@@ -395,9 +380,8 @@
gap: 4px 16px; gap: 4px 16px;
} }
.tls-cipher-list code { .tls-cipher-list code {
font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace;
font-size: 10pt; font-size: 10pt;
color: #E0E0E0; color: var(--color-light-grey);
background: transparent; background: transparent;
padding: 0; padding: 0;
} }
@@ -421,36 +405,28 @@
margin: 0; margin: 0;
} }
.tls-raw code { .tls-raw code {
font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace;
white-space: pre; white-space: pre;
background: transparent; background: transparent;
padding: 0; padding: 0;
} }
/* ------------------------------------------------------------------ /* Print stylesheet — "PDFをダウンロード" uses window.print(). Hide chrome and
* Print stylesheet — "PDFをダウンロード" uses window.print() which * expand all tab panels so the resulting PDF contains every finding. */
* 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.
* ------------------------------------------------------------------ */
@media print { @media print {
html, body { html, body {
background: #ffffff !important; background: #ffffff !important;
color: #101010 !important; color: #101010 !important;
} }
/* Hide site-wide chrome and interactive controls. */
footer, .tls-tabs, .tls-results-actions, footer, .tls-tabs, .tls-results-actions,
#tls-copy-json, script, .tls-landing-links { #tls-copy-json, script, .tls-landing-links {
display: none !important; display: none !important;
} }
/* Keep the results-header banner but make it paper-friendly. */
.tls-results-header { .tls-results-header {
background: transparent !important; background: transparent !important;
border: 0 !important; border: 0 !important;
padding: 0 0 16px 0 !important; padding: 0 0 16px 0 !important;
border-bottom: 1px solid #cccccc !important; border-bottom: 1px solid #cccccc !important;
} }
/* Expand every tab panel so the full report prints. */
.tls-tab-panels { .tls-tab-panels {
padding: 0 !important; padding: 0 !important;
} }
@@ -481,7 +457,6 @@
border-bottom: 1px dotted #cccccc; border-bottom: 1px dotted #cccccc;
padding: 4px 0; padding: 4px 0;
} }
/* Rank badge keeps its colour via currentColor. */
.tls-rank-badge { .tls-rank-badge {
print-color-adjust: exact; print-color-adjust: exact;
-webkit-print-color-adjust: exact; -webkit-print-color-adjust: exact;
+12 -21
View File
@@ -7,24 +7,15 @@
const logEl = document.getElementById("tls-log"); const logEl = document.getElementById("tls-log");
const verbEl = document.getElementById("tls-status-verb"); const verbEl = document.getElementById("tls-status-verb");
// Colors for the severity label tag. Order Good→Bad maps to // Severity label tag. Matches SEVERITY_COLORS in schemas.py so the live
// bright-{green,blue,yellow,red,purple}. Matches SEVERITY_COLORS in // log and results page render identically.
// schemas.py so the live log and results page render identically. const SEV = {
const SEV_COLOR = { good: { color: "bright-green", label: "GOOD" },
good: "bright-green", normal: { color: "bright-blue", label: "NORMAL" },
normal: "bright-blue", notgood: { color: "bright-yellow", label: "NOT GOOD" },
notgood: "bright-yellow", bad: { color: "bright-red", label: "BAD" },
bad: "bright-red", serious: { color: "bright-purple", label: "SERIOUS" },
serious: "bright-purple", info: { color: "light-grey-alt", label: "INFO" },
info: "light-grey-alt",
};
const SEV_LABEL = {
good: "GOOD",
normal: "NORMAL",
notgood: "NOT GOOD",
bad: "BAD",
serious: "SERIOUS",
info: "INFO",
}; };
let reconnectAttempts = 0; let reconnectAttempts = 0;
@@ -50,10 +41,10 @@
const msg = document.createElement("span"); const msg = document.createElement("span");
msg.className = "tls-log-msg"; msg.className = "tls-log-msg";
if (severity && SEV_LABEL[severity] && severity !== "info") { if (severity && SEV[severity] && severity !== "info") {
const sev = document.createElement("span"); const sev = document.createElement("span");
sev.className = "text-" + (SEV_COLOR[severity] || "light-grey") + " font-bold"; sev.className = "text-" + SEV[severity].color + " font-bold";
sev.textContent = SEV_LABEL[severity]; sev.textContent = SEV[severity].label;
msg.appendChild(sev); msg.appendChild(sev);
msg.appendChild(document.createTextNode(" ")); msg.appendChild(document.createTextNode(" "));
} }
+6 -40
View File
@@ -11,18 +11,17 @@ from bs4 import BeautifulSoup
from markitdown import MarkItDown from markitdown import MarkItDown
from datetime import datetime, timezone from datetime import datetime, timezone
from contextlib import asynccontextmanager 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.templating import Jinja2Templates
from fastapi.responses import PlainTextResponse, JSONResponse, FileResponse, RedirectResponse from fastapi.responses import PlainTextResponse, JSONResponse, FileResponse, RedirectResponse
from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplateNotFound
from .error import error_page from .error import error_page
from .database import AccessCounter from .database import AccessCounter
from .middleware import Middleware, server_version, onion_hostname 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 import (
from .tools.tls_test.engine import run_full_scan tls_test_db, tls_test_queue,
tls_submit, tls_api_submit, tls_results_context, tls_websocket_handler,
tls_test_db = TlsTestDB() )
tls_test_queue = TlsJobQueue(tls_test_db, run_full_scan)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): 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}") @app.websocket("/tools/tls-test/ws/{test_id}")
async def tls_test_ws(websocket: WebSocket, test_id: str): async def tls_test_ws(websocket: WebSocket, test_id: str):
job = tls_test_db.get_job(test_id) await tls_websocket_handler(websocket, test_id, tls_test_db, tls_test_queue)
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)
@app.api_route("/api/tools/tls-test/scan", methods=["POST"]) @app.api_route("/api/tools/tls-test/scan", methods=["POST"])
async def tls_test_api_scan(request: Request) -> Response: async def tls_test_api_scan(request: Request) -> Response:
+10 -2
View File
@@ -1,5 +1,13 @@
from .runner import TlsJobQueue from .runner import TlsJobQueue
from .db import TlsTestDB 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",
]
+38 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import json
import datetime import datetime
from fastapi import Request from fastapi import Request, WebSocket, WebSocketDisconnect
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.responses import Response from fastapi.responses import Response
from .engine import validate_tls_target 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, "log_entries": log_entries,
"history": history, "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)