feat: self-hosted frontend JS (offline) + docker hardening

- Vendored epub.js/pdf.js+worker/JSZip/DOMPurify in static/vendor/
- reader.html/reader.js servono i JS da /static (niente CDN, offline-ready)
- CSP: rimosse allowlist CDN, aggiunto worker-src 'self' blob:
- Test CSP aggiornati; tutti 16 verdi; verificato smoke test offline in container
This commit is contained in:
truetype committed 2026-08-29 22:32:10 +02:00
1 parent e62c50c616
commit 0f15b7958f
31 files changed
+1363 -215

No files matched your search

+18 -15
View File
@@ -5,7 +5,7 @@ import logging
import threading
from flask import Blueprint, request, jsonify, Response, send_file, render_template, current_app
from werkzeug.utils import secure_filename
from config import SUPPORTED_FORMATS, OUTPUT_FORMATS_BY_INPUT
from config import SUPPORTED_FORMATS, OUTPUT_FORMATS_BY_INPUT, CLEANUP_HOURS
from bookish.services.sse_manager import create_task, generate_sse_events, get_status
from bookish.services.converter import run_conversion, get_format_from_ext
@@ -24,7 +24,7 @@ def _get_output_ext(output_format):
return f".{output_format}"
def _clean_old_files(folder, hours=1):
def _clean_old_files(folder, hours=CLEANUP_HOURS):
import time
now = time.time()
for f in os.listdir(folder):
@@ -50,21 +50,11 @@ def upload():
supported = ", ".join(sorted(SUPPORTED_FORMATS.keys()))
return jsonify({"error": f"Formato '{ext}' non supportato. Supportati: {supported}"}), 400
_clean_old_files(current_app.config["UPLOAD_FOLDER"])
_clean_old_files(current_app.config["CONVERTED_FOLDER"])
filename = secure_filename(file.filename)
if not filename:
return jsonify({"error": "Nome file non valido"}), 400
input_ext = get_format_from_ext(filename)
unique_name = f"{uuid.uuid4().hex}_{filename}"
input_path = os.path.join(current_app.config["UPLOAD_FOLDER"], unique_name)
file.save(input_path)
sha256_hash = hashlib.sha256()
with open(input_path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256_hash.update(chunk)
content_hash = sha256_hash.hexdigest()
output_format = request.form.get("output_format", "").lower()
if output_format not in SUPPORTED_FORMATS:
return jsonify({"error": f"Formato output '{output_format}' non supportato"}), 400
@@ -75,6 +65,19 @@ def upload():
"error": f"Conversione da {input_ext} a {output_format} non supportata"
}), 400
_clean_old_files(current_app.config["UPLOAD_FOLDER"])
_clean_old_files(current_app.config["CONVERTED_FOLDER"])
unique_name = f"{uuid.uuid4().hex}_{filename}"
input_path = os.path.join(current_app.config["UPLOAD_FOLDER"], unique_name)
file.save(input_path)
sha256_hash = hashlib.sha256()
with open(input_path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256_hash.update(chunk)
content_hash = sha256_hash.hexdigest()
output_ext = _get_output_ext(output_format)
output_name = f"{uuid.uuid4().hex}{output_ext}"
output_path = os.path.join(current_app.config["CONVERTED_FOLDER"], output_name)
+34 -14
View File
@@ -1,6 +1,6 @@
import os
import hashlib
from flask import Blueprint, render_template, jsonify, send_file, current_app
from flask import Blueprint, render_template, jsonify, send_file, current_app, url_for
from config import SUPPORTED_FORMATS
reader_bp = Blueprint("reader", __name__)
@@ -8,9 +8,13 @@ reader_bp = Blueprint("reader", __name__)
READABLE_FORMATS = {"epub", "pdf", "html", "md", "txt", "fb2", "docx"}
def _pdf_worker_url():
return url_for("static", filename="vendor/pdf.worker.min.js")
@reader_bp.route("/reader")
def reader():
return render_template("reader.html")
return render_template("reader.html", pdf_worker_url=_pdf_worker_url())
def _file_hash(filepath):
@@ -21,30 +25,46 @@ def _file_hash(filepath):
return h.hexdigest()
def _resolve_file(filename):
"""Trova il file su disco gestendo sia il nome di upload (<uuid>_<name>)
sia il nome convertito (<uuid>.ext). Ritorna (path, ext) o (None, None)."""
safe = os.path.basename(filename)
candidates = []
for folder in (current_app.config["UPLOAD_FOLDER"], current_app.config["CONVERTED_FOLDER"]):
candidates.append(os.path.join(folder, safe))
# Fallback: un file 'libro_test.epub' passato a mano potrebbe essere
# stato salvato come <uuid>_libro_test.epub (upload) → cerchiamo per
# suffisso dentro i folder.
if os.path.isdir(folder):
for f in os.listdir(folder):
if f == safe:
continue
if f.endswith("_" + safe):
candidates.append(os.path.join(folder, f))
for p in candidates:
if os.path.isfile(p):
ext = os.path.splitext(p)[1].lower().lstrip(".")
return p, ext
return None, None
@reader_bp.route("/reader/<filename>")
def reader_file(filename):
safe = os.path.basename(filename)
filepath = os.path.join(current_app.config["UPLOAD_FOLDER"], safe)
if not os.path.exists(filepath):
filepath = os.path.join(current_app.config["CONVERTED_FOLDER"], safe)
if not os.path.exists(filepath):
filepath, ext = _resolve_file(filename)
if not filepath:
return jsonify({"error": "File non trovato"}), 404
ext = os.path.splitext(safe)[1].lower().lstrip(".")
if ext not in READABLE_FORMATS:
return jsonify({"error": f"Formato '{ext}' non supportato dal reader"}), 400
content_hash = _file_hash(filepath)
return render_template("reader.html", filename=safe, file_ext=ext, content_hash=content_hash)
return render_template("reader.html", filename=os.path.basename(filepath), file_ext=ext, content_hash=content_hash, pdf_worker_url=_pdf_worker_url())
@reader_bp.route("/reader/serve/<filename>")
def reader_serve(filename):
safe = os.path.basename(filename)
filepath = os.path.join(current_app.config["UPLOAD_FOLDER"], safe)
if not os.path.exists(filepath):
filepath = os.path.join(current_app.config["CONVERTED_FOLDER"], safe)
if not os.path.exists(filepath):
filepath, ext = _resolve_file(filename)
if not filepath:
return jsonify({"error": "File non trovato"}), 404
return send_file(filepath, mimetype="application/octet-stream")