--
This commit is contained in:
@@ -10,15 +10,30 @@ from pathlib import Path
|
||||
from bs4 import BeautifulSoup
|
||||
from markitdown import MarkItDown
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Request, Response, WebSocket, WebSocketDisconnect, 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
|
||||
from .tools.tls_test.engine import run_full_scan, parse_target
|
||||
from .tools.tls_test.ratelimit import check as ratelimit_check, client_ip_from_scope
|
||||
|
||||
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
|
||||
tls_test_db = TlsTestDB()
|
||||
tls_test_queue = TlsJobQueue(tls_test_db, run_full_scan)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await tls_test_queue.start()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await tls_test_queue.stop()
|
||||
|
||||
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None, lifespan=lifespan)
|
||||
app.add_middleware(Middleware)
|
||||
templates = Jinja2Templates(directory=Path.cwd().joinpath("public"))
|
||||
markitdown = MarkItDown()
|
||||
@@ -129,6 +144,175 @@ async def thumbnail(request: Request, path: str) -> Response:
|
||||
png = resvg_py.svg_to_bytes(svg, font_files=font_files, width=1200, height=630)
|
||||
return Response(content=png, media_type="image/png")
|
||||
|
||||
def _validate_tls_target(raw: str) -> str | None:
|
||||
s = (raw or "").strip()
|
||||
if not s or not re.compile(r"^[A-Za-z0-9._:\[\]\-]{1,255}$").match(s):
|
||||
return None
|
||||
try:
|
||||
host, port, _ = parse_target(s)
|
||||
except Exception:
|
||||
return None
|
||||
if not host:
|
||||
return None
|
||||
if port <= 0 or port > 65535:
|
||||
return None
|
||||
return s
|
||||
|
||||
@app.api_route("/tools/tls-test/", methods=["GET"])
|
||||
async def tls_test_index(request: Request) -> Response:
|
||||
return templates.TemplateResponse(request=request, name="tools/tls-test/index.html")
|
||||
|
||||
@app.api_route("/tools/tls-test/", methods=["POST"])
|
||||
async def tls_test_submit(request: Request) -> Response:
|
||||
form = await request.form()
|
||||
raw = str(form.get("target", "")).strip()
|
||||
target = _validate_tls_target(raw)
|
||||
if not target:
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="tools/tls-test/index.html",
|
||||
context={"error": "無効なターゲットです。ホスト名/IP(:ポート)を入力してください。", "last_target": raw},
|
||||
status_code=400,
|
||||
)
|
||||
client_ip = client_ip_from_scope(request.scope)
|
||||
decision = ratelimit_check(tls_test_db, client_ip)
|
||||
if not decision.allowed:
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="tools/tls-test/index.html",
|
||||
context={"error": decision.reason, "last_target": raw},
|
||||
status_code=429,
|
||||
)
|
||||
test_id = tls_test_queue.submit(target, client_ip)
|
||||
return RedirectResponse(url=f"/tools/tls-test/status/{test_id}/", status_code=303)
|
||||
|
||||
@app.api_route("/tools/tls-test/status/{test_id}/", methods=["GET"])
|
||||
async def tls_test_status_page(request: Request, test_id: str) -> Response:
|
||||
job = tls_test_db.get_job(test_id)
|
||||
if not job:
|
||||
return error_page(templates, request, 404, "指定されたテストが見つかりません。", "…id間違ってない?")
|
||||
if job.get("status") == "done":
|
||||
return RedirectResponse(url=f"/tools/tls-test/results/{test_id}/", status_code=303)
|
||||
progress = tls_test_db.get_progress(test_id)
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="tools/tls-test/status.html",
|
||||
context={
|
||||
"test_id": test_id,
|
||||
"target": job.get("target", ""),
|
||||
"status": job.get("status", ""),
|
||||
"progress_entries": progress,
|
||||
},
|
||||
)
|
||||
|
||||
@app.api_route("/tools/tls-test/results/{test_id}/", methods=["GET"])
|
||||
async def tls_test_results_page(request: Request, test_id: str) -> Response:
|
||||
job = tls_test_db.get_job(test_id)
|
||||
if not job:
|
||||
return error_page(templates, request, 404, "指定されたテストが見つかりません。", "…id間違ってない?")
|
||||
if job.get("status") != "done":
|
||||
return RedirectResponse(url=f"/tools/tls-test/status/{test_id}/", status_code=303)
|
||||
result = job.get("result") or {}
|
||||
categories: dict[str, list[dict]] = {}
|
||||
for f in result.get("findings", []):
|
||||
categories.setdefault(f.get("category", "other"), []).append(f)
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="tools/tls-test/results.html",
|
||||
context={
|
||||
"test_id": test_id,
|
||||
"job": job,
|
||||
"result": result,
|
||||
"categories": categories,
|
||||
},
|
||||
)
|
||||
|
||||
@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)
|
||||
|
||||
@app.api_route("/api/tools/tls-test/scan", methods=["POST"])
|
||||
async def tls_test_api_scan(request: Request) -> Response:
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "invalid JSON"}, status_code=400)
|
||||
target = _validate_tls_target(str(payload.get("target", "")))
|
||||
if not target:
|
||||
return JSONResponse({"error": "invalid target"}, status_code=400)
|
||||
client_ip = client_ip_from_scope(request.scope)
|
||||
decision = ratelimit_check(tls_test_db, client_ip)
|
||||
if not decision.allowed:
|
||||
return JSONResponse({"error": decision.reason}, status_code=429)
|
||||
test_id = tls_test_queue.submit(target, client_ip)
|
||||
return JSONResponse({
|
||||
"id": test_id,
|
||||
"status_url": f"/tools/tls-test/status/{test_id}/",
|
||||
"results_url": f"/tools/tls-test/results/{test_id}/",
|
||||
"ws_url": f"/tools/tls-test/ws/{test_id}",
|
||||
})
|
||||
|
||||
@app.api_route("/api/tools/tls-test/status/{test_id}", methods=["GET"])
|
||||
async def tls_test_api_status(request: Request, test_id: str) -> Response:
|
||||
job = tls_test_db.get_job(test_id)
|
||||
if not job:
|
||||
return JSONResponse({"error": "not found"}, status_code=404)
|
||||
progress = tls_test_db.get_progress(test_id)
|
||||
return JSONResponse({
|
||||
"id": test_id,
|
||||
"target": job.get("target"),
|
||||
"status": job.get("status"),
|
||||
"rank": job.get("rank"),
|
||||
"score": job.get("score"),
|
||||
"created_at": job.get("created_at"),
|
||||
"started_at": job.get("started_at"),
|
||||
"finished_at": job.get("finished_at"),
|
||||
"progress": progress,
|
||||
"error": job.get("error_message"),
|
||||
})
|
||||
|
||||
@app.api_route("/api/tools/tls-test/results/{test_id}", methods=["GET"])
|
||||
async def tls_test_api_results(request: Request, test_id: str) -> Response:
|
||||
job = tls_test_db.get_job(test_id)
|
||||
if not job:
|
||||
return JSONResponse({"error": "not found"}, status_code=404)
|
||||
if job.get("status") != "done":
|
||||
return JSONResponse({"error": "not ready", "status": job.get("status")}, status_code=409)
|
||||
return JSONResponse(job.get("result") or {})
|
||||
|
||||
@app.api_route("/{full_path:path}", methods=["GET", "POST", "HEAD"])
|
||||
async def default_response(request: Request, full_path: str) -> Response:
|
||||
if not full_path.endswith(".html") and not full_path.endswith(".md"):
|
||||
|
||||
Reference in New Issue
Block a user