- serve.py: explizite if/else Content-Type (GET), PUT antwortet mit passendem CT - index.html: Original-Indent speichern/wiederverwenden, View-Modal-Status nach Edit - smoke_test.sh: $$ statt $PORT für Log-PID, pkill mit TERM+Retry+SIGKILL - agent_verify.sh: try/except FileNotFoundError für node-Check - validate.py: re.fullmatch statt re.match für Pattern-Validierung
188 lines
6.3 KiB
Python
Executable file
188 lines
6.3 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Minimaler Entwicklungs-Server für die Prompt Templates Webansicht.
|
|
Startet auf Port 8081 und dient die statischen Dateien aus.
|
|
"""
|
|
|
|
import http.server
|
|
import socketserver
|
|
import socket
|
|
import os
|
|
import json
|
|
import logging
|
|
from urllib.parse import urlparse, unquote
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
DIRECTORY = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT_DIR = os.path.abspath(os.path.join(DIRECTORY, ".."))
|
|
|
|
|
|
def find_free_port(start_port=9000):
|
|
"""Finde einen freien Port ab start_port
|
|
|
|
Für zukünftige Smoke-Test-Integration.
|
|
"""
|
|
port = start_port
|
|
while port < 10000:
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("", port))
|
|
return port
|
|
except OSError:
|
|
port += 1
|
|
return None
|
|
|
|
|
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, directory=DIRECTORY, **kwargs)
|
|
|
|
def _validate_template_path(self, path):
|
|
"""Validiert, dass der Pfad auf Schritte Beschränkt ist und innerhalb von ROOT_DIR/templates/ oder DIRECTORY liegt."""
|
|
parsed = urlparse(path).path
|
|
path = parsed
|
|
|
|
# /templates.json ist ein Sonderfall - exakter Pfad im web/-Verzeichnis
|
|
if path == "/templates.json":
|
|
return os.path.join(DIRECTORY, "templates.json")
|
|
|
|
# Nur /templates/* Pfade erlauben
|
|
if not path.startswith("/templates/"):
|
|
return None
|
|
|
|
# Extrahiere den relativen Pfad nach /templates/
|
|
rel_path = path[len("/templates/") :] # 'system/test.json' oder '../etc/passwd'
|
|
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(ROOT_DIR, "templates", normalized_rel)
|
|
|
|
# Explizite Prüfung, dass der Pfad innerhalb von ROOT_DIR/templates/ liegt
|
|
templates_base = os.path.abspath(os.path.join(ROOT_DIR, "templates"))
|
|
full_path_abs = os.path.abspath(full_path)
|
|
|
|
if (
|
|
not full_path_abs.startswith(templates_base + os.sep)
|
|
and full_path_abs != templates_base
|
|
):
|
|
return None
|
|
|
|
return full_path_abs
|
|
|
|
def do_PUT(self):
|
|
# Nur PUT auf /templates/* Pfade erlauben
|
|
file_path = self._validate_template_path(urlparse(self.path).path)
|
|
if file_path is None:
|
|
self.send_error(403, "Forbidden: Invalid path")
|
|
return
|
|
|
|
# Verzeichnis prüfen - muss existieren
|
|
file_dir = os.path.dirname(file_path)
|
|
if not os.path.exists(file_dir) or not os.path.isdir(file_dir):
|
|
self.send_error(404, "Directory not found")
|
|
return
|
|
|
|
# Content-Type prüfen
|
|
content_type = self.headers.get("Content-Type", "")
|
|
if "text/plain" not in content_type and not content_type.startswith("text/"):
|
|
self.send_error(400, "Unsupported content type")
|
|
return
|
|
|
|
# Inhalt lesen und speichern
|
|
MAX_BODY = 10 * 1024 * 1024
|
|
content_length = int(self.headers.get("Content-Length", 0))
|
|
if content_length > MAX_BODY:
|
|
self.send_error(413, "Request body too large")
|
|
return
|
|
if content_length <= 0:
|
|
self.send_error(400, "No content provided")
|
|
return
|
|
|
|
try:
|
|
file_content = self.rfile.read(content_length)
|
|
|
|
# Datei schreiben
|
|
with open(file_path, "wb") as f:
|
|
f.write(file_content)
|
|
|
|
# Response Content-Type je nach Dateityp setzen
|
|
if file_path.endswith(".json"):
|
|
response_content_type = "application/json"
|
|
else:
|
|
response_content_type = "text/plain"
|
|
|
|
self.send_response(200)
|
|
self.send_header("Content-type", response_content_type)
|
|
self.end_headers()
|
|
self.wfile.write("File saved successfully".encode())
|
|
except Exception as e:
|
|
self.send_error(500, f"Failed to save file: {e}")
|
|
|
|
def do_GET(self):
|
|
# Für Root-Pfad: index.html servieren
|
|
parsed_path = urlparse(self.path).path
|
|
if parsed_path == "/" or parsed_path == "/index.html":
|
|
self.path = "/index.html"
|
|
return super().do_GET()
|
|
|
|
# Anfragen für /templates.json oder /templates/* umleiten
|
|
file_path = self._validate_template_path(urlparse(self.path).path)
|
|
if file_path is not None:
|
|
# Content-Type je nach Dateiendung setzen
|
|
if file_path.endswith(".md"):
|
|
content_type = "text/plain"
|
|
elif file_path.endswith(".json"):
|
|
content_type = "application/json"
|
|
else:
|
|
content_type = "text/plain"
|
|
|
|
try:
|
|
with open(file_path, "rb") as f:
|
|
self.send_response(200)
|
|
self.send_header("Content-type", content_type)
|
|
self.end_headers()
|
|
self.wfile.write(f.read())
|
|
return
|
|
except FileNotFoundError:
|
|
self.send_error(404, "File not found")
|
|
return
|
|
except Exception as e:
|
|
self.send_error(500, f"Error serving file: {e}")
|
|
return
|
|
|
|
return super().do_GET()
|
|
|
|
|
|
def main():
|
|
PORT = 8081
|
|
logging.info("Serving on http://localhost:%s", PORT)
|
|
|
|
socketserver.TCPServer.allow_reuse_address = True
|
|
with socketserver.TCPServer(("", PORT), Handler) as httpd:
|
|
logging.info("Serving Prompt Templates on http://localhost:%s", PORT)
|
|
logging.info("Press Ctrl+C to stop")
|
|
logging.info("Directory: %s", DIRECTORY)
|
|
try:
|
|
httpd.serve_forever()
|
|
except KeyboardInterrupt:
|
|
logging.info("\nServer stopped")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|