Template
Fix pg_dump backup auth and servers API bearer timeouts.
This commit is contained in:
@@ -103,7 +103,7 @@ else:
|
||||
application_path = os.path.dirname(__file__)
|
||||
|
||||
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
||||
CURRENT_VERSION = "v2.6.5"
|
||||
CURRENT_VERSION = "v2.6.7"
|
||||
RELEASES_REPO_URL = repo_url()
|
||||
RELEASES_API_LATEST = api_latest_url()
|
||||
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
||||
@@ -1354,6 +1354,30 @@ def _touch_api_token(token_entry: dict) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _schedule_api_token_touch(token_id: str) -> None:
|
||||
"""Persist API token last_used_at without blocking the request handler."""
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return
|
||||
loop.create_task(_persist_api_token_touch(token_id))
|
||||
|
||||
|
||||
async def _persist_api_token_touch(token_id: str) -> None:
|
||||
try:
|
||||
async with DATA_LOCK:
|
||||
data = await load_data_async()
|
||||
entry = next(
|
||||
(t for t in data.get('api_tokens', []) if t.get('id') == token_id),
|
||||
None,
|
||||
)
|
||||
if not entry or not _touch_api_token(entry):
|
||||
return
|
||||
await asyncio.to_thread(save_data, data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to touch API token last_used_at: {e}")
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
salt = secrets.token_hex(16)
|
||||
h = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
|
||||
@@ -2944,18 +2968,83 @@ def _check_admin(request):
|
||||
resolved = _resolve_api_token(data, raw_token)
|
||||
if resolved:
|
||||
entry, token_user = resolved
|
||||
# Best-effort last-used tracking; swallow write errors so a flaky
|
||||
# disk never blocks an API call from succeeding.
|
||||
try:
|
||||
if _touch_api_token(entry):
|
||||
save_data(data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to touch API token last_used_at: {e}")
|
||||
if _touch_api_token(entry):
|
||||
token_id = entry.get('id')
|
||||
if token_id:
|
||||
_schedule_api_token_touch(token_id)
|
||||
return token_user
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def _check_admin_async(request):
|
||||
"""Async variant for hot paths (server list/ping) — avoids blocking the loop."""
|
||||
user = get_current_user(request)
|
||||
if user and user['role'] in ('admin', 'support'):
|
||||
return user
|
||||
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if auth_header.lower().startswith('bearer '):
|
||||
raw_token = auth_header[7:].strip()
|
||||
data = await load_data_async()
|
||||
resolved = _resolve_api_token(data, raw_token)
|
||||
if resolved:
|
||||
entry, token_user = resolved
|
||||
if _touch_api_token(entry):
|
||||
token_id = entry.get('id')
|
||||
if token_id:
|
||||
_schedule_api_token_touch(token_id)
|
||||
return token_user
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _public_vpn_server_view(server: dict, server_id: int) -> dict:
|
||||
"""Safe server list view for API consumers (no SSH secrets)."""
|
||||
server_info = dict(server.get('server_info') or {})
|
||||
for key in list(server_info.keys()):
|
||||
if key not in ('uname', 'ssl_domain', 'ssl_email', 'connect_domain'):
|
||||
server_info.pop(key, None)
|
||||
|
||||
protocols = {}
|
||||
for key, info in (server.get('protocols') or {}).items():
|
||||
if not isinstance(info, dict):
|
||||
continue
|
||||
protocols[key] = {
|
||||
'installed': bool(info.get('installed')),
|
||||
'port': info.get('port'),
|
||||
'running': info.get('running'),
|
||||
'container_exists': info.get('container_exists'),
|
||||
}
|
||||
|
||||
return {
|
||||
'id': server_id,
|
||||
'name': server.get('name') or server.get('host') or '',
|
||||
'host': server.get('host') or '',
|
||||
'ssh_port': int(server.get('ssh_port') or 22),
|
||||
'username': server.get('username') or '',
|
||||
'auth': 'key' if server.get('private_key') else 'password',
|
||||
'has_password': bool(server.get('password')),
|
||||
'has_private_key': bool(server.get('private_key')),
|
||||
'server_info': server_info,
|
||||
'protocols': protocols,
|
||||
}
|
||||
|
||||
|
||||
@app.get('/api/servers', tags=["Servers"])
|
||||
async def api_list_servers(request: Request):
|
||||
"""List SSH servers in the panel inventory (credentials are never returned)."""
|
||||
if not await _check_admin_async(request):
|
||||
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||
data = await load_data_async()
|
||||
servers = [
|
||||
_public_vpn_server_view(s, idx)
|
||||
for idx, s in enumerate(data.get('servers', []))
|
||||
if isinstance(s, dict)
|
||||
]
|
||||
return {'servers': servers, 'total': len(servers)}
|
||||
|
||||
|
||||
@app.post('/api/servers/add', tags=["Servers"])
|
||||
async def api_add_server(request: Request, req: AddServerRequest):
|
||||
if not _check_admin(request):
|
||||
@@ -3166,9 +3255,9 @@ async def api_server_ping(request: Request, server_id: int):
|
||||
measures RTT, immediately closes. Runs on the asyncio loop so the page
|
||||
can issue many pings in parallel without blocking each other.
|
||||
"""
|
||||
if not _check_admin(request):
|
||||
if not await _check_admin_async(request):
|
||||
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||
data = load_data()
|
||||
data = await load_data_async()
|
||||
if server_id >= len(data['servers']):
|
||||
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
||||
server = data['servers'][server_id]
|
||||
|
||||
Reference in New Issue
Block a user