-
Notifications
You must be signed in to change notification settings - Fork 654
Expand file tree
/
Copy pathfrontend_server.py
More file actions
115 lines (89 loc) · 3.21 KB
/
frontend_server.py
File metadata and controls
115 lines (89 loc) · 3.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import os
import httpx
import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
# Load environment variables from .env file
load_dotenv()
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# Build paths
BUILD_DIR = os.path.join(os.path.dirname(__file__), "build")
INDEX_HTML = os.path.join(BUILD_DIR, "index.html")
# Proxy configuration for WAF/private networking deployments
PROXY_API_REQUESTS = os.getenv("PROXY_API_REQUESTS", "false").lower() == "true"
BACKEND_API_URL = os.getenv("BACKEND_API_URL", "http://localhost:8000")
# Serve static files from build directory
app.mount(
"/assets", StaticFiles(directory=os.path.join(BUILD_DIR, "assets")), name="assets"
)
@app.get("/")
async def serve_index():
return FileResponse(INDEX_HTML)
@app.get("/config")
async def get_config():
auth_enabled = os.getenv("AUTH_ENABLED", "false")
if PROXY_API_REQUESTS:
# WAF mode: frontend proxies API calls, so tell browser to use same origin
api_url = "/api"
else:
# Non-WAF mode: browser calls backend directly
backend_url = os.getenv("BACKEND_API_URL", "http://localhost:8000")
api_url = backend_url + "/api"
config = {
"API_URL": api_url,
"ENABLE_AUTH": auth_enabled,
}
return config
@app.get("/health")
async def health():
return {"status": "healthy"}
# API proxy routes for WAF/private networking deployments
if PROXY_API_REQUESTS:
@app.api_route("/api/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def proxy_api(request: Request, path: str):
"""Proxy API requests to the private backend over VNet."""
target_url = f"{BACKEND_API_URL}/api/{path}"
query_string = str(request.query_params)
if query_string:
target_url = f"{target_url}?{query_string}"
headers = dict(request.headers)
headers.pop("host", None)
body = await request.body()
async with httpx.AsyncClient(timeout=300.0) as client:
response = await client.request(
method=request.method,
url=target_url,
headers=headers,
content=body,
)
return StreamingResponse(
iter([response.content]),
status_code=response.status_code,
headers=dict(response.headers),
)
@app.get("/{full_path:path}")
async def serve_app(full_path: str):
# Remediation: normalize and check containment before serving
file_path = os.path.normpath(os.path.join(BUILD_DIR, full_path))
# Block traversal and dotfiles
if (
not file_path.startswith(BUILD_DIR)
or ".." in full_path
or "/." in full_path
or "\\." in full_path
):
return FileResponse(INDEX_HTML)
if os.path.isfile(file_path):
return FileResponse(file_path)
return FileResponse(INDEX_HTML)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=3000, access_log=False, log_level="info")