This commit is contained in:
Timothy Jaeryang Baek 2026-11-05 12:01:25 +09:00
parent 99f4541e3b
commit 69189cbaad
4 changed files with 28 additions and 25 deletions

View file

@ -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

View file

@ -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,

View file

@ -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())

View file

@ -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,