diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index e206ea32d..e738090a1 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -269,6 +269,51 @@ async def set_terminal_servers_config( } +@router.post('/terminal_servers/verify') +async def verify_terminal_server_connection( + request: Request, form_data: TerminalServerConnection, user=Depends(get_admin_user) +): + """ + Verify the connection to a terminal server by detecting its type. + + Tries GET {url}/api/v1/policies (orchestrator) then GET {url}/api/config + (plain terminal). Returns ``{status: true, type: "orchestrator"|"terminal"}``. + """ + base_url = (form_data.url or '').rstrip('/') + if not base_url: + raise HTTPException(status_code=400, detail='Terminal server URL is required') + + headers = {} + if form_data.auth_type == 'bearer' and form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + + try: + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + # Orchestrators expose a policies API; plain terminals don't. + try: + async with session.get(f'{base_url}/api/v1/policies', headers=headers) as resp: + if resp.ok: + return {'status': True, 'type': 'orchestrator'} + except Exception: + pass + + # Fall back to open-terminal config endpoint. + try: + async with session.get(f'{base_url}/api/config', headers=headers) as resp: + if resp.ok: + return {'status': True, 'type': 'terminal'} + except Exception: + pass + + except Exception as e: + log.debug(f'Failed to connect to the terminal server: {e}') + + raise HTTPException(status_code=400, detail='Failed to connect to the terminal server') + + @router.post('/tool_servers/verify') async def verify_tool_servers_config(request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)): """ diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index d2abfe186..2f26d711e 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -308,6 +308,40 @@ export const putOrchestratorPolicy = async ( return res; }; +/** + * Verify a terminal server connection via the backend proxy. + * Used for system/admin connections to avoid CORS issues and API key exposure. + */ +export const verifyTerminalServerConnection = async (token: string, connection: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers/verify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...connection + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const verifyToolServerConnection = async (token: string, connection: object) => { let error = null; diff --git a/src/lib/components/AddTerminalServerModal.svelte b/src/lib/components/AddTerminalServerModal.svelte index b70a8b823..466884b3e 100644 --- a/src/lib/components/AddTerminalServerModal.svelte +++ b/src/lib/components/AddTerminalServerModal.svelte @@ -12,12 +12,12 @@ import LockClosed from '$lib/components/icons/LockClosed.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; - import { detectTerminalServerType, putOrchestratorPolicy } from '$lib/apis/configs'; + import { detectTerminalServerType, verifyTerminalServerConnection, putOrchestratorPolicy } from '$lib/apis/configs'; import { getTerminalConfig } from '$lib/apis/terminal'; export let show = false; export let edit = false; - export let admin = false; + export let direct = false; export let connection = null; export let onSubmit: Function = () => {}; @@ -110,9 +110,13 @@ verifying = true; try { - if (admin) { - // Admin: detect orchestrator vs terminal - const type = await detectTerminalServerType(_url, key); + if (!direct) { + // System connection: proxy through backend to avoid CORS / key exposure + const result = await verifyTerminalServerConnection( + localStorage.token, + { url: _url, key, auth_type } + ); + const type = result?.type ?? null; if (type) { serverType = type; @@ -137,7 +141,7 @@ toast.error($i18n.t('Server connection failed')); } } else { - // Non-admin: simple terminal verification + // Direct connection: verify from browser const res = await getTerminalConfig(_url, key); if (res) { toast.success($i18n.t('Server connection verified')); @@ -192,7 +196,7 @@ url = url.replace(/\/$/, ''); // Save policy to orchestrator if applicable - if (serverType === 'orchestrator' && admin && policyId) { + if (serverType === 'orchestrator' && !direct && policyId) { try { await putOrchestratorPolicy(url, key, policyId, buildPolicyData()); } catch (err) { @@ -202,7 +206,7 @@ } const result = { - ...(admin && id.trim() ? { id: id.trim() } : {}), + ...(!direct && id.trim() ? { id: id.trim() } : {}), url, key, name, @@ -210,7 +214,7 @@ auth_type, enabled: enabled, config: { - ...(admin ? { access_grants: accessGrants } : {}) + ...(!direct ? { access_grants: accessGrants } : {}) }, // Policy fields ...(serverType ? { server_type: serverType } : {}), @@ -270,7 +274,7 @@ /> - {#if admin} + {#if !direct}