refactor: split serve.py and index.html into single-responsibility modules
Backend:
- path_validator.py: PathValidator-Klasse für Pfad-Validierung
- file_ops.py: read_file, write_file, directory_exists, file_exists
- content_types.py: get_content_type mit EXTENSION_MAP
- handler.py: Handler-Klasse mit do_GET/do_PUT, nutzt above modules
- serve.py: Entry-Point (main, find_free_port), setzt Handler.validator/directory
Frontend:
- css/variables.css: CSS-Variablen (--bg-*, --text-*, --accent, etc.)
- css/styles.css: Alle CSS-Regeln (modal, card, template-grid, etc.)
- js/utils.js: esc, showToast, copyContentToClipboard
- js/modal.js: showModal, closeModal, closeEditModal, wasViewModalOpen
- js/editor.js: editModalContent, createJsonEditUI, extractJsonFromForm
- js/api.js: viewTemplate, copyContent, loadTemplates, saveEditedContent
- js/templates.js: renderTemplates, applyFilters, parseTypeFromHash
- js/main.js: Event-Listener, Hash-Filter, Initialisierung
- index.html: Inline-CSS/JS entfernt, <link>/<script src>-Tags hinzugefügt
Smoke test: SO_REUSEADDR für schnelle Port-Wiederverwendung
2026-05-03 14:40:44 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
Pfad-Validierung für Template-Dateien.
|
|
|
|
|
|
|
|
|
|
Verhindert Path-Traversal-Angriffe und validiert, dass angeforderte
|
|
|
|
|
Pfade innerhalb des erlaubten Verzeichnisses liegen.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
from typing import Optional
|
|
|
|
|
from urllib.parse import unquote, urlparse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PathValidator:
|
|
|
|
|
"""Validiert und auflöst URL-Pfade zu Dateisystem-Pfaden."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, root_dir: str, web_dir: str):
|
|
|
|
|
"""
|
|
|
|
|
Args:
|
|
|
|
|
root_dir: Projekt-Root-Verzeichnis (enthält templates/)
|
|
|
|
|
web_dir: web/-Verzeichnis (enthält templates.json)
|
|
|
|
|
"""
|
|
|
|
|
self.root_dir = root_dir
|
|
|
|
|
self.web_dir = web_dir
|
|
|
|
|
|
|
|
|
|
def resolve_template_path(self, path: str) -> Optional[str]:
|
|
|
|
|
"""
|
|
|
|
|
Resolve einen Request-Pfad zu einem Dateisystem-Pfad.
|
|
|
|
|
|
|
|
|
|
Gibt None zurück, falls der Pfad ungültig oder nicht autorisiert ist.
|
|
|
|
|
|
|
|
|
|
Beispiele:
|
|
|
|
|
/templates.json -> <web_dir>/templates.json
|
|
|
|
|
/templates/system/foo.json -> <root_dir>/templates/system/foo.json
|
|
|
|
|
"""
|
|
|
|
|
parsed = urlparse(path).path
|
|
|
|
|
path = parsed
|
|
|
|
|
|
|
|
|
|
# /templates.json ist ein Sonderfall - exakter Pfad im web/-Verzeichnis
|
|
|
|
|
if path == "/templates.json":
|
|
|
|
|
return os.path.join(self.web_dir, "templates.json")
|
|
|
|
|
|
|
|
|
|
# Nur /templates/* Pfade erlauben
|
|
|
|
|
if not path.startswith("/templates/"):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# Extrahiere den relativen Pfad nach /templates/
|
|
|
|
|
rel_path = path[len("/templates/") :]
|
|
|
|
|
rel_path = unquote(rel_path)
|
|
|
|
|
|
|
|
|
|
# Ablehnen bei leeren Pfaden oder absoluten Pfaden
|
|
|
|
|
if not rel_path or os.path.isabs(rel_path):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# Ablehnen bei '..' Sequenzen (vor der Normalisierung!)
|
|
|
|
|
if ".." in rel_path.split(os.sep):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# Pfad normalisieren
|
|
|
|
|
normalized_rel = os.path.normpath(rel_path)
|
|
|
|
|
|
|
|
|
|
# Nach Normalisierung nochmal prüfen
|
|
|
|
|
if normalized_rel.startswith("..") or os.path.isabs(normalized_rel):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# Vollständigen Pfad konstruieren
|
|
|
|
|
full_path = os.path.join(self.root_dir, "templates", normalized_rel)
|
|
|
|
|
|
2026-05-03 20:09:36 +02:00
|
|
|
templates_base = os.path.realpath(os.path.join(self.root_dir, "templates"))
|
|
|
|
|
full_path_resolved = os.path.realpath(full_path)
|
refactor: split serve.py and index.html into single-responsibility modules
Backend:
- path_validator.py: PathValidator-Klasse für Pfad-Validierung
- file_ops.py: read_file, write_file, directory_exists, file_exists
- content_types.py: get_content_type mit EXTENSION_MAP
- handler.py: Handler-Klasse mit do_GET/do_PUT, nutzt above modules
- serve.py: Entry-Point (main, find_free_port), setzt Handler.validator/directory
Frontend:
- css/variables.css: CSS-Variablen (--bg-*, --text-*, --accent, etc.)
- css/styles.css: Alle CSS-Regeln (modal, card, template-grid, etc.)
- js/utils.js: esc, showToast, copyContentToClipboard
- js/modal.js: showModal, closeModal, closeEditModal, wasViewModalOpen
- js/editor.js: editModalContent, createJsonEditUI, extractJsonFromForm
- js/api.js: viewTemplate, copyContent, loadTemplates, saveEditedContent
- js/templates.js: renderTemplates, applyFilters, parseTypeFromHash
- js/main.js: Event-Listener, Hash-Filter, Initialisierung
- index.html: Inline-CSS/JS entfernt, <link>/<script src>-Tags hinzugefügt
Smoke test: SO_REUSEADDR für schnelle Port-Wiederverwendung
2026-05-03 14:40:44 +02:00
|
|
|
|
|
|
|
|
if (
|
2026-05-03 20:09:36 +02:00
|
|
|
not full_path_resolved.startswith(templates_base + os.sep)
|
|
|
|
|
and full_path_resolved != templates_base
|
refactor: split serve.py and index.html into single-responsibility modules
Backend:
- path_validator.py: PathValidator-Klasse für Pfad-Validierung
- file_ops.py: read_file, write_file, directory_exists, file_exists
- content_types.py: get_content_type mit EXTENSION_MAP
- handler.py: Handler-Klasse mit do_GET/do_PUT, nutzt above modules
- serve.py: Entry-Point (main, find_free_port), setzt Handler.validator/directory
Frontend:
- css/variables.css: CSS-Variablen (--bg-*, --text-*, --accent, etc.)
- css/styles.css: Alle CSS-Regeln (modal, card, template-grid, etc.)
- js/utils.js: esc, showToast, copyContentToClipboard
- js/modal.js: showModal, closeModal, closeEditModal, wasViewModalOpen
- js/editor.js: editModalContent, createJsonEditUI, extractJsonFromForm
- js/api.js: viewTemplate, copyContent, loadTemplates, saveEditedContent
- js/templates.js: renderTemplates, applyFilters, parseTypeFromHash
- js/main.js: Event-Listener, Hash-Filter, Initialisierung
- index.html: Inline-CSS/JS entfernt, <link>/<script src>-Tags hinzugefügt
Smoke test: SO_REUSEADDR für schnelle Port-Wiederverwendung
2026-05-03 14:40:44 +02:00
|
|
|
):
|
|
|
|
|
return None
|
|
|
|
|
|
2026-05-03 20:09:36 +02:00
|
|
|
if os.path.islink(full_path_resolved):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
return full_path_resolved
|