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,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);
}
});
}
})();
+25 -50
View File
@@ -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;
+12 -21
View File
@@ -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(" "));
}
+6 -40
View File
@@ -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:
+10 -2
View File
@@ -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",
]
+38 -1
View File
@@ -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)