mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-01 11:02:56 +02:00
Experimental dynamic manifest
This commit is contained in:
@@ -1,13 +1,27 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolve_request_origin(request: Request) -> str:
|
||||
"""Resolve the external origin, honoring common reverse-proxy headers."""
|
||||
forwarded_proto = request.headers.get("x-forwarded-proto")
|
||||
forwarded_host = request.headers.get("x-forwarded-host")
|
||||
|
||||
if forwarded_proto and forwarded_host:
|
||||
proto = forwarded_proto.split(",")[0].strip()
|
||||
host = forwarded_host.split(",")[0].strip()
|
||||
if proto and host:
|
||||
return f"{proto}://{host}"
|
||||
|
||||
return str(request.base_url).rstrip("/")
|
||||
|
||||
|
||||
def register_frontend_static_routes(app: FastAPI, frontend_dir: Path) -> bool:
|
||||
"""Register frontend static file routes if a built frontend is available.
|
||||
|
||||
@@ -55,6 +69,41 @@ def register_frontend_static_routes(app: FastAPI, frontend_dir: Path) -> bool:
|
||||
"""Serve the frontend index.html."""
|
||||
return FileResponse(index_file)
|
||||
|
||||
@app.get("/site.webmanifest")
|
||||
async def serve_webmanifest(request: Request):
|
||||
"""Serve a dynamic web manifest using the active request origin."""
|
||||
origin = _resolve_request_origin(request)
|
||||
manifest = {
|
||||
"name": "RemoteTerm for MeshCore",
|
||||
"short_name": "RemoteTerm",
|
||||
"id": f"{origin}/",
|
||||
"start_url": f"{origin}/",
|
||||
"scope": f"{origin}/",
|
||||
"display": "standalone",
|
||||
"display_override": ["window-controls-overlay", "standalone", "fullscreen"],
|
||||
"theme_color": "#111419",
|
||||
"background_color": "#111419",
|
||||
"icons": [
|
||||
{
|
||||
"src": f"{origin}/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": f"{origin}/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable",
|
||||
},
|
||||
],
|
||||
}
|
||||
return JSONResponse(
|
||||
manifest,
|
||||
media_type="application/manifest+json",
|
||||
headers={"Cache-Control": "no-store"},
|
||||
)
|
||||
|
||||
@app.get("/{path:path}")
|
||||
async def serve_frontend(path: str):
|
||||
"""Serve frontend files, falling back to index.html for SPA routing."""
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "RemoteTerm for MeshCore",
|
||||
"short_name": "RemoteTerm",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -53,6 +53,16 @@ def test_valid_dist_serves_static_and_spa_fallback(tmp_path):
|
||||
assert root_response.status_code == 200
|
||||
assert "index page" in root_response.text
|
||||
|
||||
manifest_response = client.get("/site.webmanifest")
|
||||
assert manifest_response.status_code == 200
|
||||
assert manifest_response.headers["content-type"].startswith("application/manifest+json")
|
||||
manifest = manifest_response.json()
|
||||
assert manifest["start_url"] == "http://testserver/"
|
||||
assert manifest["scope"] == "http://testserver/"
|
||||
assert manifest["id"] == "http://testserver/"
|
||||
assert manifest["display"] == "standalone"
|
||||
assert manifest["icons"][0]["src"] == "http://testserver/web-app-manifest-192x192.png"
|
||||
|
||||
file_response = client.get("/robots.txt")
|
||||
assert file_response.status_code == 200
|
||||
assert file_response.text == "User-agent: *"
|
||||
@@ -64,3 +74,28 @@ def test_valid_dist_serves_static_and_spa_fallback(tmp_path):
|
||||
asset_response = client.get("/assets/app.js")
|
||||
assert asset_response.status_code == 200
|
||||
assert "console.log('ok');" in asset_response.text
|
||||
|
||||
|
||||
def test_webmanifest_uses_forwarded_origin_headers(tmp_path):
|
||||
app = FastAPI()
|
||||
dist_dir = tmp_path / "frontend" / "dist"
|
||||
dist_dir.mkdir(parents=True)
|
||||
(dist_dir / "index.html").write_text("<html><body>index page</body></html>")
|
||||
|
||||
registered = register_frontend_static_routes(app, dist_dir)
|
||||
assert registered is True
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.get(
|
||||
"/site.webmanifest",
|
||||
headers={
|
||||
"x-forwarded-proto": "https",
|
||||
"x-forwarded-host": "mesh.example.com:8443",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["start_url"] == "https://mesh.example.com:8443/"
|
||||
assert data["scope"] == "https://mesh.example.com:8443/"
|
||||
assert data["id"] == "https://mesh.example.com:8443/"
|
||||
|
||||
Reference in New Issue
Block a user