diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 89fffd7c05..b9a2099921 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -27,6 +27,7 @@ from open_webui.utils.oauth import ( resolve_oauth_client_info, ) from open_webui.utils.tools import ( + bearer_auth_header, get_tool_server_data, get_tool_server_url, set_terminal_servers, @@ -351,7 +352,7 @@ async def verify_terminal_server_connection( headers = {} if form_data.auth_type == 'bearer' and form_data.key: - headers['Authorization'] = f'Bearer {form_data.key}' + headers.update(bearer_auth_header(form_data.key)) try: async with aiohttp.ClientSession( @@ -423,7 +424,7 @@ async def put_terminal_server_policy( headers = {'Content-Type': 'application/json'} if form_data.auth_type == 'bearer' and form_data.key: - headers['Authorization'] = f'Bearer {form_data.key}' + headers.update(bearer_auth_header(form_data.key)) try: async with aiohttp.ClientSession( @@ -458,7 +459,7 @@ async def put_terminal_server_lifecycle( headers = {'Content-Type': 'application/json'} if form_data.auth_type == 'bearer' and form_data.key: - headers['Authorization'] = f'Bearer {form_data.key}' + headers.update(bearer_auth_header(form_data.key)) try: async with aiohttp.ClientSession( @@ -496,7 +497,7 @@ async def refresh_terminal_server_terminals( headers = {'Content-Type': 'application/json'} if form_data.auth_type == 'bearer' and form_data.key: - headers['Authorization'] = f'Bearer {form_data.key}' + headers.update(bearer_auth_header(form_data.key)) body = { 'only_idle': form_data.only_idle, diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index 3d637010fd..ce2efa7096 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -20,6 +20,7 @@ from open_webui.models.groups import Groups from open_webui.models.users import Users from open_webui.utils.access_control import has_connection_access from open_webui.utils.auth import get_verified_user +from open_webui.utils.tools import bearer_auth_header, normalize_bearer_token from starlette.background import BackgroundTask log = logging.getLogger(__name__) @@ -126,15 +127,15 @@ async def proxy_terminal( auth_type = connection.get('auth_type', 'bearer') if auth_type == 'bearer': - headers['Authorization'] = f'Bearer {connection.get("key", "")}' + headers.update(bearer_auth_header(connection.get('key', ''))) elif auth_type == 'session': cookies = request.cookies - headers['Authorization'] = f'Bearer {request.state.token.credentials}' + headers.update(bearer_auth_header(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}' + headers.update(bearer_auth_header(oauth_token)) # auth_type == "none": no Authorization header content_type = request.headers.get('content-type') @@ -309,7 +310,7 @@ async def ws_terminal( # First-message auth to upstream terminal server auth_type = connection.get('auth_type', 'bearer') if auth_type == 'bearer': - key = connection.get('key', '') + key = normalize_bearer_token(connection.get('key', '')) await upstream.send_str(_json.dumps({'type': 'auth', 'token': key})) await publish_event( diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 33f0441bfb..411eff49ac 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -103,6 +103,15 @@ from pydantic.fields import FieldInfo log = logging.getLogger(__name__) +def normalize_bearer_token(token: Any) -> str: + return token.strip() if isinstance(token, str) else token or '' + + +def bearer_auth_header(token: Any) -> dict[str, str]: + token = normalize_bearer_token(token) + return {'Authorization': f'Bearer {token}'} if token else {} + + async def build_tool_server_headers( connection: dict, request, @@ -1141,7 +1150,7 @@ async def set_terminal_servers(request: Request): return headers = {} if connection.get('auth_type', 'bearer') == 'bearer': - headers['Authorization'] = f'Bearer {connection.get("key", "")}' + headers.update(bearer_auth_header(connection.get('key', ''))) prompt = await get_terminal_system_prompt(server['url'], headers) if prompt: server['system_prompt'] = prompt @@ -1216,15 +1225,15 @@ async def get_terminal_tools( headers = {'Content-Type': 'application/json', 'X-User-Id': user.id} if auth_type == 'bearer': - headers['Authorization'] = f'Bearer {connection.get("key", "")}' + headers.update(bearer_auth_header(connection.get('key', ''))) elif auth_type == 'session': cookies = request.cookies - headers['Authorization'] = f'Bearer {request.state.token.credentials}' + headers.update(bearer_auth_header(request.state.token.credentials)) elif auth_type == 'system_oauth': cookies = request.cookies oauth_token = extra_params.get('__oauth_token__', None) if oauth_token: - headers['Authorization'] = f'Bearer {oauth_token.get("access_token", "")}' + headers.update(bearer_auth_header(oauth_token.get('access_token', ''))) # auth_type == "none": no Authorization header system_prompt = server_data.get('system_prompt') @@ -1349,7 +1358,7 @@ async def get_tool_servers_data(servers: list[dict[str, Any]]) -> list[dict[str, # Fetch from URL task = get_tool_server_data( spec_url, - {'Authorization': f'Bearer {token}'} if token else None, + bearer_auth_header(token) or None, ) elif spec_type == 'json' and server.get('spec', ''): # Use provided JSON spec diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index da491043ef..ee38a9a8fa 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -28,6 +28,10 @@ export type TerminalCwd = { import { WEBUI_API_BASE_URL } from '$lib/constants'; +const bearerHeaders = (apiKey: string): Record => ({ + Authorization: `Bearer ${apiKey.trim()}` +}); + export type TerminalServer = { id: string; url: string; @@ -50,7 +54,7 @@ export const getTerminalConfig = async ( ): Promise<{ features: TerminalFeatures } | null> => { const url = `${baseUrl.replace(/\/$/, '')}/api/config`; const res = await fetch(url, { - headers: { Authorization: `Bearer ${apiKey}` } + headers: bearerHeaders(apiKey) }).catch(() => null); if (!res || !res.ok) return null; return res.json().catch(() => null); @@ -62,7 +66,7 @@ export const getCwd = async ( sessionId?: string ): Promise => { const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`; - const headers: Record = { Authorization: `Bearer ${apiKey}` }; + const headers: Record = bearerHeaders(apiKey); if (sessionId) headers['X-Session-Id'] = sessionId; const res = await fetch(url, { headers }).catch(() => null); if (!res || !res.ok) return null; @@ -83,7 +87,7 @@ export const listFiles = async ( ): Promise => { // The endpoint uses `directory` as the query param name const url = `${baseUrl.replace(/\/$/, '')}/files/list?directory=${encodeURIComponent(path)}`; - const headers: Record = { Authorization: `Bearer ${apiKey}` }; + const headers: Record = bearerHeaders(apiKey); if (sessionId) headers['X-Session-Id'] = sessionId; const res = await fetch(url, { headers }) .then(async (res) => { @@ -104,7 +108,7 @@ export const readFile = async ( sessionId?: string ): Promise => { const url = `${baseUrl.replace(/\/$/, '')}/files/read?path=${encodeURIComponent(path)}`; - const headers: Record = { Authorization: `Bearer ${apiKey}` }; + const headers: Record = bearerHeaders(apiKey); if (sessionId) headers['X-Session-Id'] = sessionId; const res = await fetch(url, { headers }).catch((err) => { console.error('open-terminal readFile error:', err); @@ -132,7 +136,7 @@ export const downloadFileBlob = async ( sessionId?: string ): Promise<{ blob: Blob; filename: string } | null> => { const url = `${baseUrl.replace(/\/$/, '')}/files/view?path=${encodeURIComponent(path)}`; - const headers: Record = { Authorization: `Bearer ${apiKey}` }; + const headers: Record = bearerHeaders(apiKey); if (sessionId) headers['X-Session-Id'] = sessionId; const res = await fetch(url, { headers }).catch(() => null); @@ -151,7 +155,7 @@ export const archiveFromTerminal = async ( ): Promise<{ blob: Blob; filename: string } | null> => { const url = `${baseUrl.replace(/\/$/, '')}/files/archive`; const headers: Record = { - Authorization: `Bearer ${apiKey}`, + ...bearerHeaders(apiKey), 'Content-Type': 'application/json' }; if (sessionId) headers['X-Session-Id'] = sessionId; @@ -180,7 +184,7 @@ export const uploadToTerminal = async ( const url = `${baseUrl.replace(/\/$/, '')}/files/upload?directory=${encodeURIComponent(directory)}`; const body = new FormData(); body.append('file', file); - const headers: Record = { Authorization: `Bearer ${apiKey}` }; + const headers: Record = bearerHeaders(apiKey); if (sessionId) headers['X-Session-Id'] = sessionId; const res = await fetch(url, { method: 'POST', @@ -206,7 +210,7 @@ export const createDirectory = async ( ): Promise<{ path: string } | null> => { const url = `${baseUrl.replace(/\/$/, '')}/files/mkdir`; const headers: Record = { - Authorization: `Bearer ${apiKey}`, + ...bearerHeaders(apiKey), 'Content-Type': 'application/json' }; if (sessionId) headers['X-Session-Id'] = sessionId; @@ -233,7 +237,7 @@ export const deleteEntry = async ( sessionId?: string ): Promise<{ path: string; type: string } | null> => { const url = `${baseUrl.replace(/\/$/, '')}/files/delete?path=${encodeURIComponent(path)}`; - const headers: Record = { Authorization: `Bearer ${apiKey}` }; + const headers: Record = bearerHeaders(apiKey); if (sessionId) headers['X-Session-Id'] = sessionId; const res = await fetch(url, { method: 'DELETE', @@ -258,7 +262,7 @@ export const setCwd = async ( ): Promise<{ cwd: string } | null> => { const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`; const headers: Record = { - Authorization: `Bearer ${apiKey}`, + ...bearerHeaders(apiKey), 'Content-Type': 'application/json' }; if (sessionId) headers['X-Session-Id'] = sessionId; @@ -287,7 +291,7 @@ export const moveEntry = async ( ): Promise<{ source: string; destination: string } | { error: string }> => { const url = `${baseUrl.replace(/\/$/, '')}/files/move`; const headers: Record = { - Authorization: `Bearer ${apiKey}`, + ...bearerHeaders(apiKey), 'Content-Type': 'application/json' }; if (sessionId) headers['X-Session-Id'] = sessionId; @@ -313,7 +317,7 @@ export const getListeningPorts = async ( ): Promise => { const url = `${baseUrl.replace(/\/$/, '')}/ports`; const res = await fetch(url, { - headers: { Authorization: `Bearer ${apiKey}` } + headers: bearerHeaders(apiKey) }).catch(() => null); if (!res || !res.ok) return []; const json = await res.json().catch(() => null); @@ -337,7 +341,7 @@ export const createNotebookSession = async ( const res = await fetch(url, { method: 'POST', headers: { - Authorization: `Bearer ${apiKey}`, + ...bearerHeaders(apiKey), 'Content-Type': 'application/json' }, body: JSON.stringify({ path }) @@ -370,7 +374,7 @@ export const executeNotebookCell = async ( const res = await fetch(url, { method: 'POST', headers: { - Authorization: `Bearer ${apiKey}`, + ...bearerHeaders(apiKey), 'Content-Type': 'application/json' }, body: JSON.stringify(body) @@ -397,7 +401,7 @@ export const stopNotebookSession = async ( const url = `${baseUrl.replace(/\/$/, '')}/notebooks/${sessionId}`; const res = await fetch(url, { method: 'DELETE', - headers: { Authorization: `Bearer ${apiKey}` } + headers: bearerHeaders(apiKey) }).catch(() => null); return res?.ok ?? false; };