138 lines
4.9 KiB
Python
138 lines
4.9 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)
|