Spaces:
Running
Running
Hermes Agent commited on
Commit Β·
42fc0fd
1
Parent(s): 20a21f0
security(hardening): enforce api key auth, dynamic secret fallbacks, and error log sanitization
Browse files- entrypoint.sh +11 -11
- gateway/omniroute.py +14 -29
- gateway/utils.py +2 -21
- tg_streamer.py +7 -0
entrypoint.sh
CHANGED
|
@@ -29,22 +29,22 @@ chmod -R 777 /root/.cache /data/cache /data/hermes 2>/dev/null || true
|
|
| 29 |
|
| 30 |
# ββ STEP 1: Master Secret Validation ββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
if [ -z "$STORAGE_ENCRYPTION_KEY" ]; then
|
| 32 |
-
echo "[WARN] STORAGE_ENCRYPTION_KEY is not set in secrets;
|
| 33 |
-
export STORAGE_ENCRYPTION_KEY="omniroute_default_storage_key_32bytes"
|
| 34 |
fi
|
| 35 |
|
| 36 |
if [ -z "$JWT_SECRET" ]; then
|
| 37 |
-
echo "[WARN] JWT_SECRET is not set in secrets;
|
| 38 |
-
export JWT_SECRET="omniroute_default_jwt_secret_key_32bytes"
|
| 39 |
fi
|
| 40 |
|
| 41 |
if [ -z "$API_KEY_SECRET" ]; then
|
| 42 |
-
echo "[WARN] API_KEY_SECRET is not set in secrets;
|
| 43 |
-
export API_KEY_SECRET="omniroute_default_api_key_secret_32bytes"
|
| 44 |
fi
|
| 45 |
|
| 46 |
if [ -z "$INITIAL_PASSWORD" ]; then
|
| 47 |
-
echo "[WARN] INITIAL_PASSWORD is not set in secrets; using
|
| 48 |
export INITIAL_PASSWORD="admin123"
|
| 49 |
fi
|
| 50 |
|
|
@@ -332,10 +332,10 @@ echo "[BOOT] Background services starting asynchronously..."
|
|
| 332 |
export CLI_COMPAT_CURSOR=1
|
| 333 |
export CLI_COMPAT_QWEN=1
|
| 334 |
export OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true
|
| 335 |
-
export OMNIROUTE_ALLOW_UNAUTHENTICATED=
|
| 336 |
-
export REQUIRE_API_KEY=
|
| 337 |
-
export OMNIROUTE_REQUIRE_API_KEY=
|
| 338 |
-
export OMNIROUTE_AUTH_REQUIRED=
|
| 339 |
|
| 340 |
if [ -d "/omniroute" ]; then
|
| 341 |
cd /omniroute
|
|
|
|
| 29 |
|
| 30 |
# ββ STEP 1: Master Secret Validation ββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
if [ -z "$STORAGE_ENCRYPTION_KEY" ]; then
|
| 32 |
+
echo "[WARN] STORAGE_ENCRYPTION_KEY is not set in secrets; generating dynamic 32-hex key."
|
| 33 |
+
export STORAGE_ENCRYPTION_KEY=$(python3 -c "import secrets; print(secrets.token_hex(16))" 2>/dev/null || echo "omniroute_default_storage_key_32bytes")
|
| 34 |
fi
|
| 35 |
|
| 36 |
if [ -z "$JWT_SECRET" ]; then
|
| 37 |
+
echo "[WARN] JWT_SECRET is not set in secrets; generating dynamic 32-hex key."
|
| 38 |
+
export JWT_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(16))" 2>/dev/null || echo "omniroute_default_jwt_secret_key_32bytes")
|
| 39 |
fi
|
| 40 |
|
| 41 |
if [ -z "$API_KEY_SECRET" ]; then
|
| 42 |
+
echo "[WARN] API_KEY_SECRET is not set in secrets; generating dynamic 32-hex key."
|
| 43 |
+
export API_KEY_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(16))" 2>/dev/null || echo "omniroute_default_api_key_secret_32bytes")
|
| 44 |
fi
|
| 45 |
|
| 46 |
if [ -z "$INITIAL_PASSWORD" ]; then
|
| 47 |
+
echo "[WARN] INITIAL_PASSWORD is not set in secrets; using default admin password."
|
| 48 |
export INITIAL_PASSWORD="admin123"
|
| 49 |
fi
|
| 50 |
|
|
|
|
| 332 |
export CLI_COMPAT_CURSOR=1
|
| 333 |
export CLI_COMPAT_QWEN=1
|
| 334 |
export OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true
|
| 335 |
+
export OMNIROUTE_ALLOW_UNAUTHENTICATED=false
|
| 336 |
+
export REQUIRE_API_KEY=true
|
| 337 |
+
export OMNIROUTE_REQUIRE_API_KEY=true
|
| 338 |
+
export OMNIROUTE_AUTH_REQUIRED=true
|
| 339 |
|
| 340 |
if [ -d "/omniroute" ]; then
|
| 341 |
cd /omniroute
|
gateway/omniroute.py
CHANGED
|
@@ -77,43 +77,28 @@ async def handle_omniroute_proxy(target: str, request: Request, default_prefix:
|
|
| 77 |
}
|
| 78 |
res = await proxy_http_request(target, request, default_prefix=default_prefix, extra_headers=extra_auth, html_fixup=html_fixup)
|
| 79 |
if res.status_code in (500, 502, 503) and request.method == "GET" and "html" in request.headers.get("accept", "").lower():
|
| 80 |
-
|
| 81 |
-
for log_file in ["/data/omniroute/omniroute.log", "/root/.omniroute/omniroute.log"]:
|
| 82 |
-
try:
|
| 83 |
-
if os.path.exists(log_file):
|
| 84 |
-
with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
|
| 85 |
-
lines = f.readlines()
|
| 86 |
-
log_content = "".join(lines[-120:])
|
| 87 |
-
if log_content:
|
| 88 |
-
break
|
| 89 |
-
except Exception:
|
| 90 |
-
pass
|
| 91 |
-
|
| 92 |
-
if log_content:
|
| 93 |
-
print(f"[OMNIROUTE SERVER LOGS]\n{log_content}")
|
| 94 |
-
safe_logs = log_content.replace("\\", "\\\\").replace("`", "\\`").replace("$", "\\$")
|
| 95 |
-
diagnostic_html = f"""<!DOCTYPE html>
|
| 96 |
<html>
|
| 97 |
<head>
|
| 98 |
-
<title>OmniRoute Server
|
| 99 |
<style>
|
| 100 |
-
body {{ font-family: system-ui,
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
| 104 |
</style>
|
| 105 |
-
<script>
|
| 106 |
-
console.error("=== OMNIROUTE BACKEND LOGS ===");
|
| 107 |
-
console.error(`{safe_logs}`);
|
| 108 |
-
</script>
|
| 109 |
</head>
|
| 110 |
<body>
|
| 111 |
-
<
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
</body>
|
| 115 |
</html>"""
|
| 116 |
-
|
| 117 |
return res
|
| 118 |
|
| 119 |
# ββ Dashboard & Admin UI Routes (20128) ββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 77 |
}
|
| 78 |
res = await proxy_http_request(target, request, default_prefix=default_prefix, extra_headers=extra_auth, html_fixup=html_fixup)
|
| 79 |
if res.status_code in (500, 502, 503) and request.method == "GET" and "html" in request.headers.get("accept", "").lower():
|
| 80 |
+
diagnostic_html = f"""<!DOCTYPE html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
<html>
|
| 82 |
<head>
|
| 83 |
+
<title>OmniRoute Server Status</title>
|
| 84 |
<style>
|
| 85 |
+
body {{ font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #f8fafc; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 24px; }}
|
| 86 |
+
.card {{ background: #1e293b; border: 1px solid #334155; padding: 32px; border-radius: 12px; max-width: 500px; text-align: center; shadow: 0 10px 25px rgba(0,0,0,0.5); }}
|
| 87 |
+
h1 {{ color: #f87171; font-size: 1.4rem; margin: 0 0 12px; }}
|
| 88 |
+
p {{ color: #94a3b8; font-size: 0.95rem; line-height: 1.5; margin: 0 0 20px; }}
|
| 89 |
+
a {{ color: #38bdf8; text-decoration: none; font-weight: 600; }}
|
| 90 |
+
a:hover {{ text-decoration: underline; }}
|
| 91 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
</head>
|
| 93 |
<body>
|
| 94 |
+
<div class="card">
|
| 95 |
+
<h1>β οΈ OmniRoute Service Temporarily Unavailable</h1>
|
| 96 |
+
<p>OmniRoute backend is currently initializing or completing a background task. Status code: {res.status_code}.</p>
|
| 97 |
+
<p><a href="/dashboard">Reload Dashboard →</a></p>
|
| 98 |
+
</div>
|
| 99 |
</body>
|
| 100 |
</html>"""
|
| 101 |
+
return HTMLResponse(content=diagnostic_html, status_code=res.status_code)
|
| 102 |
return res
|
| 103 |
|
| 104 |
# ββ Dashboard & Admin UI Routes (20128) ββββββββββββββββββββββββββββββββββββββ
|
gateway/utils.py
CHANGED
|
@@ -179,23 +179,6 @@ async def proxy_http_request(
|
|
| 179 |
continue
|
| 180 |
if method in ("GET", "HEAD") and ("html" in request.headers.get("accept", "").lower() or request.url.path in ("/", "/index.html", "/healthz", "/health", "/omniroute", "/dashboard", "/jellyfin") or request.url.path.startswith("/dashboard") or request.url.path.startswith("/omniroute") or request.url.path.startswith("/jellyfin")):
|
| 181 |
service_name = "OmniRoute AI Gateway" if default_prefix == "/omniroute" or "omniroute" in request.url.path or "dashboard" in request.url.path else ("Jellyfin Media Server" if default_prefix == "/jellyfin" or "jellyfin" in request.url.path else "Open WebUI")
|
| 182 |
-
|
| 183 |
-
log_content = ""
|
| 184 |
-
if service_name == "OmniRoute AI Gateway":
|
| 185 |
-
for log_file in ["/data/omniroute/omniroute.log", "/root/.omniroute/omniroute.log"]:
|
| 186 |
-
try:
|
| 187 |
-
if os.path.exists(log_file):
|
| 188 |
-
with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
|
| 189 |
-
lines = f.readlines()
|
| 190 |
-
log_content = "".join(lines[-100:])
|
| 191 |
-
if log_content:
|
| 192 |
-
break
|
| 193 |
-
except Exception:
|
| 194 |
-
pass
|
| 195 |
-
|
| 196 |
-
safe_logs = log_content.replace("\\", "\\\\").replace("`", "\\`").replace("$", "\\$").replace("\n", "\\n") if log_content else ""
|
| 197 |
-
log_box = f'<pre style="text-align:left; background:#0d1117; padding:12px; border-radius:6px; color:#38bdf8; font-size:0.8rem; overflow-x:auto; max-height:200px; margin-top:1rem; border:1px solid #30363d;">{log_content}</pre>' if log_content else ''
|
| 198 |
-
log_script = f'<script>console.error("=== OMNIROUTE LOGS ==="); console.error(`{safe_logs}`);</script>' if safe_logs else ''
|
| 199 |
|
| 200 |
html_retry = f"""<!DOCTYPE html>
|
| 201 |
<html>
|
|
@@ -205,27 +188,25 @@ async def proxy_http_request(
|
|
| 205 |
|
| 206 |
<style>
|
| 207 |
body {{ font-family: system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 20px; box-sizing: border-box; }}
|
| 208 |
-
.card {{ text-align: center; background: #161b22; padding: 2.5rem; border-radius: 12px; border: 1px solid #30363d; max-width:
|
| 209 |
.spinner {{ width: 40px; height: 40px; border: 4px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1.5rem; }}
|
| 210 |
@keyframes spin {{ to {{ transform: rotate(360deg); }} }}
|
| 211 |
h2 {{ margin: 0 0 0.5rem; color: #f0f6fc; font-size: 1.25rem; }}
|
| 212 |
p {{ color: #8b949e; font-size: 0.9rem; margin: 0; }}
|
| 213 |
</style>
|
| 214 |
-
{log_script}
|
| 215 |
</head>
|
| 216 |
<body>
|
| 217 |
<div class="card">
|
| 218 |
<div class="spinner"></div>
|
| 219 |
<h2>Initializing {service_name}...</h2>
|
| 220 |
<p>The service is completing backend startup. This page will refresh automatically in 3 seconds.</p>
|
| 221 |
-
{log_box}
|
| 222 |
</div>
|
| 223 |
</body>
|
| 224 |
</html>"""
|
| 225 |
return Response(content=html_retry, status_code=200, headers={"Retry-After": "3", "Refresh": "3"}, media_type="text/html")
|
| 226 |
|
| 227 |
return Response(
|
| 228 |
-
content=f"<h2>502 Service Unavailable</h2><p>
|
| 229 |
status_code=502,
|
| 230 |
media_type="text/html",
|
| 231 |
)
|
|
|
|
| 179 |
continue
|
| 180 |
if method in ("GET", "HEAD") and ("html" in request.headers.get("accept", "").lower() or request.url.path in ("/", "/index.html", "/healthz", "/health", "/omniroute", "/dashboard", "/jellyfin") or request.url.path.startswith("/dashboard") or request.url.path.startswith("/omniroute") or request.url.path.startswith("/jellyfin")):
|
| 181 |
service_name = "OmniRoute AI Gateway" if default_prefix == "/omniroute" or "omniroute" in request.url.path or "dashboard" in request.url.path else ("Jellyfin Media Server" if default_prefix == "/jellyfin" or "jellyfin" in request.url.path else "Open WebUI")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
html_retry = f"""<!DOCTYPE html>
|
| 184 |
<html>
|
|
|
|
| 188 |
|
| 189 |
<style>
|
| 190 |
body {{ font-family: system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 20px; box-sizing: border-box; }}
|
| 191 |
+
.card {{ text-align: center; background: #161b22; padding: 2.5rem; border-radius: 12px; border: 1px solid #30363d; max-width: 500px; width: 100%; }}
|
| 192 |
.spinner {{ width: 40px; height: 40px; border: 4px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1.5rem; }}
|
| 193 |
@keyframes spin {{ to {{ transform: rotate(360deg); }} }}
|
| 194 |
h2 {{ margin: 0 0 0.5rem; color: #f0f6fc; font-size: 1.25rem; }}
|
| 195 |
p {{ color: #8b949e; font-size: 0.9rem; margin: 0; }}
|
| 196 |
</style>
|
|
|
|
| 197 |
</head>
|
| 198 |
<body>
|
| 199 |
<div class="card">
|
| 200 |
<div class="spinner"></div>
|
| 201 |
<h2>Initializing {service_name}...</h2>
|
| 202 |
<p>The service is completing backend startup. This page will refresh automatically in 3 seconds.</p>
|
|
|
|
| 203 |
</div>
|
| 204 |
</body>
|
| 205 |
</html>"""
|
| 206 |
return Response(content=html_retry, status_code=200, headers={"Retry-After": "3", "Refresh": "3"}, media_type="text/html")
|
| 207 |
|
| 208 |
return Response(
|
| 209 |
+
content=f"<h2>502 Service Unavailable</h2><p>Service initializing. Please retry in a few seconds.</p>",
|
| 210 |
status_code=502,
|
| 211 |
media_type="text/html",
|
| 212 |
)
|
tg_streamer.py
CHANGED
|
@@ -194,11 +194,18 @@ async def trigger_jellyfin_scan():
|
|
| 194 |
continue
|
| 195 |
logger.warning(f"Could not trigger Jellyfin library refresh after retries: {e}")
|
| 196 |
|
|
|
|
|
|
|
| 197 |
@routes.post("/")
|
| 198 |
@routes.post("/telegram-webhook")
|
| 199 |
@routes.post("/webhook")
|
| 200 |
async def telegram_webhook(request):
|
| 201 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
data = await request.json()
|
| 203 |
post = data.get("channel_post") or data.get("message")
|
| 204 |
if not post:
|
|
|
|
| 194 |
continue
|
| 195 |
logger.warning(f"Could not trigger Jellyfin library refresh after retries: {e}")
|
| 196 |
|
| 197 |
+
WEBHOOK_SECRET = os.environ.get("TG_WEBHOOK_SECRET")
|
| 198 |
+
|
| 199 |
@routes.post("/")
|
| 200 |
@routes.post("/telegram-webhook")
|
| 201 |
@routes.post("/webhook")
|
| 202 |
async def telegram_webhook(request):
|
| 203 |
try:
|
| 204 |
+
if WEBHOOK_SECRET:
|
| 205 |
+
token = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
|
| 206 |
+
if token != WEBHOOK_SECRET:
|
| 207 |
+
return web.json_response({"ok": False, "error": "Unauthorized"}, status=401)
|
| 208 |
+
|
| 209 |
data = await request.json()
|
| 210 |
post = data.get("channel_post") or data.get("message")
|
| 211 |
if not post:
|