Zettai_Systems_Website/serve.py
2026-09-18 14:52:33 +05:30

206 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Local static server with extensionless URL support (mirrors Apache .htaccess).
Usage:
python3 serve.py
python3 serve.py 8080
Then open http://127.0.0.1:8080/about (not file://)
"""
from __future__ import annotations
import argparse
import json
import mimetypes
import os
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import unquote, urlparse
ROOT = Path(__file__).resolve().parent
SECURITY_HEADERS = (
("X-Content-Type-Options", "nosniff"),
("Referrer-Policy", "strict-origin-when-cross-origin"),
("X-Frame-Options", "SAMEORIGIN"),
("Permissions-Policy", "camera=(), microphone=(), geolocation=()"),
(
"Content-Security-Policy",
"frame-ancestors 'self'; base-uri 'self'; object-src 'none'; form-action 'self'",
),
)
class CleanURLHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(ROOT), **kwargs)
def log_message(self, format, *args):
return # quieter local dev logs
def _parts(self):
parsed = urlparse(self.path)
path = unquote(parsed.path)
query = f"?{parsed.query}" if parsed.query else ""
return path, query
def _redirect(self, location: str) -> None:
self.send_response(301)
self.send_header("Location", location)
self.end_headers()
def _json(self, status: int, payload: dict) -> None:
body = json.dumps(payload).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
def _serve_404(self) -> None:
page = ROOT / "404.html"
data = page.read_bytes() if page.is_file() else b"Not found"
self.send_response(404)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
if self.command != "HEAD":
self.wfile.write(data)
def _maybe_redirect(self) -> bool:
path, query = self._parts()
if path in ("/services/odoo", "/services/odoo/", "/services/odoo.html"):
self._redirect("/services/erp" + query)
return True
if path in ("/index.html", "/index"):
self._redirect("/" + query)
return True
if path.endswith(".html") and path != "/404.html":
clean = path[:-5] or "/"
self._redirect(clean + query)
return True
# Trailing slash on pages (and /services/) → slashless URL
if path != "/" and path.endswith("/"):
self._redirect(path.rstrip("/") + query)
return True
return False
def _handle_php(self) -> bool:
path, _query = self._parts()
clean = path.rstrip("/") or "/"
if not clean.endswith(".php"):
return False
if clean.endswith("/api/helpers.php") or clean == "/helpers.php":
self.send_error(403, "Forbidden")
return True
if self.command == "POST":
self.send_error(501, "PHP is not available on the local static server")
return True
self._json(405, {"ok": False, "error": "Method not allowed."})
return True
def do_GET(self):
if self._maybe_redirect():
return
if self._handle_php():
return
translated = Path(self.translate_path(self.path))
if not self._is_servable(translated):
self._serve_404()
return
return super().do_GET()
def do_HEAD(self):
if self._maybe_redirect():
return
if self._handle_php():
return
translated = Path(self.translate_path(self.path))
if not self._is_servable(translated):
self._serve_404()
return
return super().do_HEAD()
def do_POST(self):
if self._handle_php():
return
self.send_error(501, "Unsupported method")
def _is_servable(self, translated: Path) -> bool:
if translated.is_file():
return True
# Directory with index.html (site root /)
return translated.is_dir() and (translated / "index.html").is_file()
def list_directory(self, path):
self._serve_404()
return None
def translate_path(self, path: str) -> str:
parsed = urlparse(path)
clean = unquote(parsed.path)
if clean != "/" and clean.endswith("/"):
clean = clean.rstrip("/")
candidate = clean.lstrip("/")
if candidate and candidate != "404.html":
html_path = ROOT / f"{candidate}.html"
file_path = ROOT / candidate
# Prefer .html page over a same-named folder (/services → services.html)
if html_path.is_file() and (not file_path.exists() or file_path.is_dir()):
path = f"/{candidate}.html"
if parsed.query:
path += f"?{parsed.query}"
return super().translate_path(path)
def end_headers(self):
self.send_header("Cache-Control", "no-store")
for name, value in SECURITY_HEADERS:
self.send_header(name, value)
super().end_headers()
class ReusableHTTPServer(ThreadingHTTPServer):
allow_reuse_address = True
def main() -> None:
parser = argparse.ArgumentParser(description="Serve Zettai Systems with clean URLs")
parser.add_argument("port", nargs="?", type=int, default=8080)
args = parser.parse_args()
os.chdir(ROOT)
mimetypes.add_type("text/css", ".css")
mimetypes.add_type("application/javascript", ".js")
try:
server = ReusableHTTPServer(("127.0.0.1", args.port), CleanURLHandler)
except OSError as exc:
if exc.errno == 48:
print(f"Port {args.port} is already in use.")
print(f"Stop the other server: lsof -ti:{args.port} | xargs kill -9")
print(f"Or use another port: python3 serve.py 8081")
raise SystemExit(1) from exc
raise
print(f"Serving {ROOT}", flush=True)
print(f"Open http://127.0.0.1:{args.port}/services", flush=True)
print("Press Ctrl+C to stop", flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopped.")
if __name__ == "__main__":
main()