Files
open-webui-ai4me/backend/open_webui/routers/terminals.py
T
2026-02-28 21:28:59 -06:00

152 lines
5.1 KiB
Python

"""Reverse proxy for admin-configured terminal servers.
Routes:
GET / — list terminals the user has access to
* /{server_id}/{path:path} — proxy request to terminal server
"""
import logging
import aiohttp
from fastapi import APIRouter, Depends, Request, Response
from fastapi.responses import JSONResponse, StreamingResponse
from starlette.background import BackgroundTask
from open_webui.utils.auth import get_verified_user
from open_webui.utils.access_control import has_connection_access
from open_webui.models.groups import Groups
log = logging.getLogger(__name__)
router = APIRouter()
STREAMING_CONTENT_TYPES = ("application/octet-stream", "image/", "application/pdf")
STRIPPED_RESPONSE_HEADERS = frozenset(
("transfer-encoding", "connection", "content-encoding", "content-length")
)
@router.get("/")
async def list_terminal_servers(request: Request, user=Depends(get_verified_user)):
"""Return terminal servers the authenticated user has access to."""
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
return [
{
"id": connection.get("id", ""),
"url": connection.get("url", ""),
"name": connection.get("name", ""),
}
for connection in connections
if connection.get("enabled", True)
and has_connection_access(user, connection, user_group_ids)
]
PROXY_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
@router.api_route("/{server_id}/{path:path}", methods=PROXY_METHODS)
async def proxy_terminal(
server_id: str,
path: str,
request: Request,
user=Depends(get_verified_user),
):
"""Proxy a request to the admin terminal server identified by *server_id*."""
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connection = next((c for c in connections if c.get("id") == server_id), None)
if connection is None:
return JSONResponse(
{"error": f"Terminal server '{server_id}' not found"}, status_code=404
)
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
if not has_connection_access(user, connection, user_group_ids):
return JSONResponse({"error": "Access denied"}, status_code=403)
base_url = (connection.get("url") or "").rstrip("/")
if not base_url:
return JSONResponse(
{"error": "Terminal server URL not configured"}, status_code=503
)
target_url = f"{base_url}/{path}"
if request.query_params:
target_url += f"?{request.query_params}"
headers = {"X-User-Id": user.id}
cookies = {}
auth_type = connection.get("auth_type", "bearer")
if auth_type == "bearer":
headers["Authorization"] = f"Bearer {connection.get('key', '')}"
elif auth_type == "session":
cookies = request.cookies
headers["Authorization"] = f"Bearer {request.state.token.credentials}"
elif auth_type == "system_oauth":
cookies = request.cookies
oauth_token = request.headers.get("x-oauth-access-token", "")
if oauth_token:
headers["Authorization"] = f"Bearer {oauth_token}"
# auth_type == "none": no Authorization header
content_type = request.headers.get("content-type")
if content_type:
headers["Content-Type"] = content_type
body = await request.body()
session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=300, connect=10),
trust_env=True,
)
try:
upstream_response = await session.request(
method=request.method,
url=target_url,
headers=headers,
cookies=cookies,
data=body or None,
)
upstream_content_type = upstream_response.headers.get("content-type", "")
filtered_headers = {
key: value
for key, value in upstream_response.headers.items()
if key.lower() not in STRIPPED_RESPONSE_HEADERS
}
# Stream binary responses directly
if any(t in upstream_content_type for t in STREAMING_CONTENT_TYPES):
async def cleanup():
await upstream_response.release()
await session.close()
return StreamingResponse(
content=upstream_response.content.iter_any(),
status_code=upstream_response.status,
headers=filtered_headers,
background=BackgroundTask(cleanup),
)
# Buffer text/JSON responses
response_body = await upstream_response.read()
status_code = upstream_response.status
await upstream_response.release()
await session.close()
return Response(
content=response_body, status_code=status_code, headers=filtered_headers
)
except Exception as error:
await session.close()
log.exception("Terminal proxy error: %s", error)
return JSONResponse(
{"error": f"Terminal proxy error: {error}"}, status_code=502
)