From 69189cbaad2aa2cf437ffbc988ef397691c4e04a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 5 Nov 2026 12:01:25 +0900 Subject: [PATCH] refac --- backend/open_webui/models/auth_sessions.py | 1 - backend/open_webui/routers/sessions.py | 5 ++- backend/open_webui/routers/tether.py | 10 +++--- backend/open_webui/utils/tether.py | 37 ++++++++++++---------- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/backend/open_webui/models/auth_sessions.py b/backend/open_webui/models/auth_sessions.py index d2e6eec7db..7a3ab0160d 100644 --- a/backend/open_webui/models/auth_sessions.py +++ b/backend/open_webui/models/auth_sessions.py @@ -89,7 +89,6 @@ class AuthSessionsTable: return [ { **{key: getattr(row, key) for key in ('id', 'created_at', 'updated_at', 'expires_at')}, - 'connection_id': row.data.get('connection_id'), 'name': row.data['name'], } for row in rows diff --git a/backend/open_webui/routers/sessions.py b/backend/open_webui/routers/sessions.py index 1de8997c41..00b65c9f27 100644 --- a/backend/open_webui/routers/sessions.py +++ b/backend/open_webui/routers/sessions.py @@ -33,7 +33,6 @@ PROOF_KEYS = ('auth_time', 'auth_method', 'session_stamp', 'mfa_verified') class SessionForm(BaseModel): model_config = ConfigDict(extra='forbid', str_strip_whitespace=True) - connection_id: UUID | None = None name: str = Field(min_length=1, max_length=100) @@ -147,7 +146,7 @@ async def create_session(form: SessionForm, request: Request, user=Depends(get_h proof = await verify_proof(request.state.claims, request) row = await AuthSessions.create( user_id=user.id, - data=form.model_dump(mode='json', exclude_none=True), + data={'name': form.name}, expires_at=await idle_expiry(), refresh_token_id=str(uuid4()), ) @@ -159,7 +158,7 @@ async def authorization_request(form: AuthorizationRequest, request: Request): await throttle(request) row = await AuthSessions.create( data={ - **form.model_dump(mode='json', include={'connection_id', 'name'}, exclude_none=True), + 'name': form.name, 'authorization': { 'redirect_uri': form.redirect_uri, 'state': form.state, diff --git a/backend/open_webui/routers/tether.py b/backend/open_webui/routers/tether.py index 562fcbd100..fb61645e96 100644 --- a/backend/open_webui/routers/tether.py +++ b/backend/open_webui/routers/tether.py @@ -4,7 +4,7 @@ import asyncio import json from contextlib import suppress from types import SimpleNamespace -from uuid import uuid4 +from uuid import UUID, uuid4 from fastapi import APIRouter, WebSocket from open_webui.models.auth_sessions import AuthSessions @@ -31,9 +31,10 @@ async def connect(ws: WebSocket): row = await AuthSessions.get(claims['sid']) if not row or row.user_id != user.id or row.refresh_token_id is None: raise ValueError('Invalid session') - connection_id = row.data.get('connection_id') - if not connection_id: - raise ValueError('A Tether connection ID is required') + connection_id = str(UUID(hello['connection_id'])) + name = hello.get('name') + if not isinstance(name, str) or not 1 <= len(name.strip()) <= 100: + raise ValueError('A connection name of 1–100 characters is required') await hub.start() presence = { 'connection': str(uuid4()), @@ -41,6 +42,7 @@ async def connect(ws: WebSocket): 'session': row.id, 'user': user.id, 'connection_id': connection_id, + 'name': name.strip(), } connection = SimpleNamespace(ws=ws, presence=presence, routes={}, lock=asyncio.Lock()) diff --git a/backend/open_webui/utils/tether.py b/backend/open_webui/utils/tether.py index 6592601c7f..297ce9c0eb 100644 --- a/backend/open_webui/utils/tether.py +++ b/backend/open_webui/utils/tether.py @@ -209,18 +209,25 @@ class Hub: None, ) + async def presences(self, user_id): + await self.start() + if self.redis: + records = [] + async for key in self.redis.scan_iter(match=self.key(user_id, '*'), count=100): + raw = await self.redis.get(key) + if raw: + records.append(json.loads(raw)) + else: + records = [connection.presence for connection in self.connections.values()] + # SCAN may return a key more than once while connections are changing. + return list({record['connection_id']: record for record in records if record['user'] == user_id}.values()) + async def open(self, user_id, connection_id): presence = await self.presence(user_id, connection_id) - if not presence: + if not presence or presence['user'] != user_id or presence['connection_id'] != connection_id: raise HTTPException(503, 'Tether is offline.') row = await AuthSessions.get(presence['session']) - if ( - not row - or row.user_id != user_id - or row.refresh_token_id is None - or row.expires_at <= time.time() - or row.data.get('connection_id') != connection_id - ): + if not row or row.user_id != user_id or row.refresh_token_id is None or row.expires_at <= time.time(): raise HTTPException(403, 'Tether session is no longer available.') if sum(s.presence['connection'] == presence['connection'] for s in self.streams.values()) >= 16: raise HTTPException(429, 'Tether has too many active operations.') @@ -269,19 +276,15 @@ async def get_terminal_connections(user): """Resolve personal targets alongside existing configured servers.""" user_id = user['id'] if isinstance(user, dict) else user.id connections = list(await Config.get('terminal_server.connections', []) or []) - seen = set() - for row in await AuthSessions.list(user_id): - connection_id = row['connection_id'] - if not connection_id or connection_id in seen: - continue - seen.add(connection_id) - presence = await hub.presence(user_id, connection_id) - if not presence: + for presence in await hub.presences(user_id): + row = await AuthSessions.get(presence['session']) + if not row or row.user_id != user_id or row.refresh_token_id is None or row.expires_at <= time.time(): continue + connection_id = presence['connection_id'] connections.append( { 'id': 'tether:' + connection_id, - 'name': row['name'], + 'name': presence['name'], 'url': 'tether://' + connection_id, 'auth_type': 'none', 'enabled': True,