aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.py
diff options
context:
space:
mode:
authorkj_sh6042026-05-31 11:29:58 -0400
committerkj_sh6042026-05-31 11:29:58 -0400
commit2b2e511a15414f6c28f15990b75442424f9cc3b9 (patch)
tree99a6cc4ad40dd2309bb4a0b2c875e1719dbfe449 /src/app.py
parent4626e9f40c4678622ef53cc0d9d3fa89768c08f0 (diff)
refactor: initial hardening tweaks
store images in localStorage
Diffstat (limited to 'src/app.py')
-rw-r--r--src/app.py111
1 files changed, 36 insertions, 75 deletions
diff --git a/src/app.py b/src/app.py
index 52484ef..72fb78d 100644
--- a/src/app.py
+++ b/src/app.py
@@ -25,16 +25,15 @@ from werkzeug.middleware.proxy_fix import ProxyFix
APP_NAME = "likha-pdf"
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 5001
+DEFAULT_MAX_CONTENT_LENGTH = 512 * 1024 * 1024
+DEFAULT_MAX_FORM_MEMORY_SIZE = DEFAULT_MAX_CONTENT_LENGTH
BASE_DIR = Path(__file__).resolve().parent
GENERATED_DIR = BASE_DIR / "generated"
-UPLOADS_DIR = BASE_DIR / "uploads"
TEMPLATES_DIR = BASE_DIR / "templates"
PARTIALS_DIR = TEMPLATES_DIR / "partials"
STATIC_DIR = BASE_DIR / "static"
-ALLOWED_IMAGE_EXTS = {"png", "jpg", "jpeg", "gif", "webp", "svg"}
-
VALID_PAPER_SIZES = {
"a0paper",
"a1paper",
@@ -133,7 +132,6 @@ def env_bool(name, default=False):
def ensure_runtime_dirs():
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
- UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
def random_hex(length=32):
@@ -144,26 +142,6 @@ def pick_option(value, fallback, valid):
return value if value in valid else fallback
-def sanitize_filename(name):
- """keep only safe characters in a filename"""
- name = os.path.basename(name.replace("\\", "/"))
- out = []
- for ch in name:
- if ch.isalnum() or ch in "-_.":
- out.append(ch)
- elif ch == " ":
- out.append("_")
- return "".join(out)
-
-
-def is_allowed_image(filename):
- dot = filename.rfind(".")
- if dot < 1 or dot == len(filename) - 1:
- return False
- ext = filename[dot + 1 :].lower()
- return ext in ALLOWED_IMAGE_EXTS
-
-
def is_safe_relative_path(path_part):
if not path_part or "\\" in path_part:
return False
@@ -186,6 +164,20 @@ def tail_text(value, max_len=1200):
return value[-max_len:]
+def format_bytes(num_bytes):
+ if num_bytes < 1024:
+ return f"{num_bytes} B"
+
+ units = ["KB", "MB", "GB", "TB"]
+ value = float(num_bytes)
+ for unit in units:
+ value /= 1024.0
+ if value < 1024.0:
+ return f"{value:.2f} {unit}"
+
+ return f"{value:.2f} PB"
+
+
# pdf stylesheet generator
def build_pdf_css(
paper_size,
@@ -612,9 +604,15 @@ def create_app():
static_url_path="/static",
)
- app.config["MAX_CONTENT_LENGTH"] = int(
- os.getenv("MAX_CONTENT_LENGTH", str(64 * 1024 * 1024))
+ max_content_length = int(
+ os.getenv("MAX_CONTENT_LENGTH", str(DEFAULT_MAX_CONTENT_LENGTH))
)
+ max_form_memory_size = int(
+ os.getenv("MAX_FORM_MEMORY_SIZE", str(DEFAULT_MAX_FORM_MEMORY_SIZE))
+ )
+
+ app.config["MAX_CONTENT_LENGTH"] = max_content_length
+ app.config["MAX_FORM_MEMORY_SIZE"] = max_form_memory_size
if env_bool("TRUST_PROXY", default=True):
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
@@ -631,11 +629,21 @@ def create_app():
@app.errorhandler(413)
def payload_too_large(_err):
+ content_limit = int(app.config.get("MAX_CONTENT_LENGTH") or 0)
+ form_limit = int(app.config.get("MAX_FORM_MEMORY_SIZE") or 0)
+ content_limit_text = (
+ format_bytes(content_limit) if content_limit else "configured limit"
+ )
+ form_limit_text = format_bytes(form_limit) if form_limit else "unlimited"
return (
read_partial(
- "upload_error.html",
+ "error.html",
{
- "{{ message }}": "request body too large.",
+ "{{ message }}": (
+ "request body too large. "
+ f"max request size is {content_limit_text}; "
+ f"max form field memory is {form_limit_text}."
+ ),
},
),
413,
@@ -728,53 +736,6 @@ def create_app():
},
)
- @app.route("/upload-image", methods=["POST"])
- def upload_image():
- uploaded = request.files.get("image")
- if not uploaded or not uploaded.filename or not uploaded.filename.strip():
- return (
- read_partial(
- "upload_error.html",
- {
- "{{ message }}": "image file is required.",
- },
- ),
- 400,
- )
-
- original = sanitize_filename(uploaded.filename)
- if not original or not is_allowed_image(original):
- return (
- read_partial(
- "upload_error.html",
- {
- "{{ message }}": "unsupported image type.",
- },
- ),
- 400,
- )
-
- ext = original.rsplit(".", 1)[-1].lower()
- stored_name = f"img_{int(time.time())}_{random_hex()}.{ext}"
- image_path = UPLOADS_DIR / stored_name
- uploaded.save(str(image_path))
-
- snippet = f"![](uploads/{stored_name})"
- return read_partial(
- "upload_result.html",
- {
- "{{ filename }}": str(escape(stored_name)),
- "{{ markdown_snippet }}": str(escape(snippet)),
- "{{ preview_url }}": f"/uploads/{stored_name}",
- },
- )
-
- @app.route("/uploads/<path:filename>")
- def serve_upload(filename):
- if not is_safe_relative_path(filename):
- abort(400)
- return send_from_directory(str(UPLOADS_DIR), filename, conditional=True)
-
@app.route("/download/<path:filename>")
def download(filename):
if not is_safe_relative_path(filename):