site: dawn.gg-style landing at root + port /skin/* for TG bot

- serve new launcher landing site (zern.cc root -> launcher server 1582)
- static assets mounted at /css /js /img
- port GET/HEAD /skin/{filename} from old site with identical headers
- exempt /skin/* + site assets from rate-limit and cache middleware
- Caddy: root domains -> 1582, old site preserved at legacy.zernmc.*
This commit is contained in:
SashegDev
2026-08-20 08:26:00 +00:00
parent 943211314e
commit b4bd76ed35
6 changed files with 1040 additions and 4 deletions
+58 -4
View File
@@ -12,7 +12,8 @@ import json
import structlog
from cachetools import TTLCache
from fastapi import Depends, FastAPI, HTTPException, Request, Response
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
# Disable httpx debug logging
@@ -46,6 +47,12 @@ BUILDS_DIR = Path("builds")
VERSIONS_DIR = BUILDS_DIR / "versions"
WHITELIST_DIR = Path(__file__).parent / "whitelist"
# Website (landing) static files
SITE_DIR = Path(__file__).parent / "site"
# SkinRestorer skins (shared with TG bot)
SKINS_DIR = Path("/root/python/site/skins")
# Mirror configuration
LAUNCHER_MIRRORS = {
"main": "https://api.zern.cc", # primary
@@ -838,7 +845,7 @@ class CacheControlMiddleware:
path = scope.get("path", "")
# Skip caching for dynamic endpoints
skip_cache = any(p in path for p in ["/api/", "/auth/", "/login", "/launch", "/install"])
skip_cache = any(p in path for p in ["/api/", "/auth/", "/login", "/launch", "/install", "/skin/"])
if skip_cache:
await self.app(scope, receive, send)
return
@@ -953,6 +960,12 @@ app.include_router(admin_router)
app.include_router(friends_router)
app.include_router(playtime_router)
# Static assets for the landing site
for sub in ("css", "js", "img"):
mount_dir = SITE_DIR / sub
mount_dir.mkdir(parents=True, exist_ok=True)
app.mount(f"/{sub}", StaticFiles(directory=mount_dir), name=f"site-{sub}")
# Monkey patch to catch invalid HTTP requests
original_data_received = HttpToolsProtocol.data_received
@@ -990,8 +1003,10 @@ HttpToolsProtocol.data_received = patched_data_received
@app.get("/")
async def root():
"""Root endpoint"""
logger.info("Root endpoint accessed")
"""Root endpoint - serves the launcher landing site"""
index_path = SITE_DIR / "index.html"
if index_path.exists():
return HTMLResponse(index_path.read_text(encoding="utf-8"))
return {
"status": "ok",
"message": "ZernMC Launcher Server is running",
@@ -1000,6 +1015,45 @@ async def root():
}
@app.get("/skin/{filename}")
@app.head("/skin/{filename}")
async def get_skin(filename: str, request: Request):
"""Serve SkinRestorer skins (compatibility with TG bot direct links)"""
if not filename or ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=404, detail="Скин не найден")
filepath = SKINS_DIR / filename
if not filepath.is_file():
raise HTTPException(status_code=404, detail="Скин не найден")
try:
content = filepath.read_bytes()
size = len(content)
headers = {
"Content-Length": str(size),
"Content-Type": "image/png",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
"Access-Control-Allow-Origin": "*",
"Content-Encoding": "identity",
}
if request.method == "HEAD":
return Response(status_code=200, headers=headers)
return Response(
content=content,
media_type="image/png",
headers=headers
)
except Exception as e:
logger.error("Skin read error", error=str(e))
raise HTTPException(status_code=500, detail="Ошибка чтения скина")
@app.get("/health")
async def health():
"""Health check endpoint"""