Безопасность панели была усилена
Проверка пароля переведена на PAM (python-pam) с fallback на crypt/spwd для совместимости, все POST-формы защищены CSRF-токеном в сессии, после пяти неудачных попыток входа включается пауза 60 секунд, установщик генерирует самоподписанный TLS-сертификат и панель по умолчанию работает по HTTPS (отключается VIRTUALITY_TLS=0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKUoZwHMCHnytfswFVRUHw
Этот коммит содержится в:
+193
-32
@@ -1,16 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import crypt
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import spwd
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
# PAM — основной способ проверки пароля; crypt/spwd удалены в Python 3.13
|
||||
# и остаются только как fallback для старых систем без python-pam.
|
||||
try:
|
||||
import pam as pam_module
|
||||
except ImportError:
|
||||
pam_module = None
|
||||
try:
|
||||
import crypt
|
||||
import spwd
|
||||
except ImportError:
|
||||
crypt = None
|
||||
spwd = None
|
||||
import lzma
|
||||
import tarfile
|
||||
import zipfile
|
||||
@@ -42,7 +54,13 @@ DEFAULT_BRIDGE = "br0"
|
||||
NOVNC_DIR = next((p for p in [Path("/usr/share/novnc"), Path("/usr/share/novnc/app")] if p.exists()), None)
|
||||
|
||||
app = FastAPI(title="Virtuality Panel")
|
||||
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
||||
|
||||
|
||||
def template_globals(request: Request) -> dict[str, Any]:
|
||||
return {"csrf_token": get_csrf_token(request)}
|
||||
|
||||
|
||||
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"), context_processors=[template_globals])
|
||||
static_dir = BASE_DIR / "static"
|
||||
static_dir.mkdir(exist_ok=True)
|
||||
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
||||
@@ -76,6 +94,13 @@ def is_configured() -> bool:
|
||||
def verify_linux_password(username: str, password: str) -> bool:
|
||||
if not username or not password or username != AUTH_USER:
|
||||
return False
|
||||
if pam_module is not None:
|
||||
try:
|
||||
return bool(pam_module.pam().authenticate(username, password, service="login"))
|
||||
except Exception:
|
||||
return False
|
||||
if crypt is None or spwd is None:
|
||||
return False
|
||||
try:
|
||||
shadow = spwd.getspnam(username)
|
||||
except (PermissionError, KeyError):
|
||||
@@ -86,6 +111,36 @@ def verify_linux_password(username: str, password: str) -> bool:
|
||||
return secrets.compare_digest(crypt.crypt(password, stored_hash), stored_hash)
|
||||
|
||||
|
||||
LOGIN_FAILURES: dict[str, dict[str, float]] = {}
|
||||
LOGIN_FAILURES_LOCK = threading.Lock()
|
||||
LOGIN_MAX_FAILURES = 5
|
||||
LOGIN_BLOCK_SECONDS = 60
|
||||
|
||||
|
||||
def login_block_remaining(client_ip: str) -> int:
|
||||
with LOGIN_FAILURES_LOCK:
|
||||
entry = LOGIN_FAILURES.get(client_ip)
|
||||
if not entry or entry["count"] < LOGIN_MAX_FAILURES:
|
||||
return 0
|
||||
remaining = int(entry["last"] + LOGIN_BLOCK_SECONDS - time.time())
|
||||
if remaining <= 0:
|
||||
LOGIN_FAILURES.pop(client_ip, None)
|
||||
return 0
|
||||
return remaining
|
||||
|
||||
|
||||
def register_login_failure(client_ip: str) -> None:
|
||||
with LOGIN_FAILURES_LOCK:
|
||||
entry = LOGIN_FAILURES.setdefault(client_ip, {"count": 0, "last": 0.0})
|
||||
entry["count"] += 1
|
||||
entry["last"] = time.time()
|
||||
|
||||
|
||||
def reset_login_failures(client_ip: str) -> None:
|
||||
with LOGIN_FAILURES_LOCK:
|
||||
LOGIN_FAILURES.pop(client_ip, None)
|
||||
|
||||
|
||||
def user_from_session_token(token: str | None) -> str | None:
|
||||
if not token:
|
||||
return None
|
||||
@@ -106,6 +161,25 @@ def require_auth(request: Request):
|
||||
return None
|
||||
|
||||
|
||||
def get_csrf_token(request: Request) -> str:
|
||||
token = request.cookies.get("virtuality_session")
|
||||
if not token:
|
||||
return ""
|
||||
try:
|
||||
data = serializer.loads(token)
|
||||
except BadSignature:
|
||||
return ""
|
||||
return str(data.get("csrf", ""))
|
||||
|
||||
|
||||
def require_csrf(request: Request, csrf_token: str):
|
||||
"""Проверка CSRF-токена для POST-форм. Возвращает 403-ответ или None."""
|
||||
expected = get_csrf_token(request)
|
||||
if expected and csrf_token and secrets.compare_digest(expected, csrf_token):
|
||||
return None
|
||||
return HTMLResponse("<h1>Сессия устарела</h1><p>Открой страницу заново и повтори действие.</p>", status_code=403)
|
||||
|
||||
|
||||
def run_cmd(cmd: list[str], timeout: int = 12) -> dict[str, Any]:
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False)
|
||||
@@ -1310,16 +1384,25 @@ def login_page(request: Request):
|
||||
def login_submit(request: Request, username: str = Form(...), password: str = Form(...)):
|
||||
if not is_configured():
|
||||
return templates.TemplateResponse("login.html", {"request": request, "app_name": APP_NAME, "error": "Панель ещё не настроена. Запусти установщик веб-панели повторно.", "configured": False, "auth_user": AUTH_USER}, status_code=500)
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
blocked_for = login_block_remaining(client_ip)
|
||||
if blocked_for:
|
||||
return templates.TemplateResponse("login.html", {"request": request, "app_name": APP_NAME, "error": f"Слишком много неудачных попыток входа. Подожди {blocked_for} сек.", "configured": True, "auth_user": AUTH_USER}, status_code=429)
|
||||
if not verify_linux_password(username, password):
|
||||
register_login_failure(client_ip)
|
||||
return templates.TemplateResponse("login.html", {"request": request, "app_name": APP_NAME, "error": "Неверный логин или пароль Linux-пользователя", "configured": True, "auth_user": AUTH_USER}, status_code=401)
|
||||
token = serializer.dumps({"user": AUTH_USER})
|
||||
reset_login_failures(client_ip)
|
||||
token = serializer.dumps({"user": AUTH_USER, "csrf": secrets.token_urlsafe(16)})
|
||||
response = RedirectResponse(url="/", status_code=303)
|
||||
response.set_cookie("virtuality_session", token, httponly=True, samesite="lax", max_age=60 * 60 * 12)
|
||||
response.set_cookie("virtuality_session", token, httponly=True, samesite="lax", max_age=60 * 60 * 12, secure=request.url.scheme == "https")
|
||||
return response
|
||||
|
||||
|
||||
@app.post("/logout")
|
||||
def logout():
|
||||
def logout(request: Request, csrf_token: str = Form("")):
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
response = RedirectResponse(url="/login", status_code=303)
|
||||
response.delete_cookie("virtuality_session")
|
||||
return response
|
||||
@@ -1344,10 +1427,13 @@ def host_page(request: Request):
|
||||
|
||||
|
||||
@app.post("/host/refresh")
|
||||
def host_refresh(request: Request):
|
||||
def host_refresh(request: Request, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
profile = host_profile.detect_host_profile()
|
||||
host_profile.save_host_profile(profile)
|
||||
return RedirectResponse(url="/host", status_code=303)
|
||||
@@ -1362,10 +1448,13 @@ def iso_page(request: Request, error: str | None = None):
|
||||
|
||||
|
||||
@app.post("/iso/upload", response_class=HTMLResponse)
|
||||
def iso_upload(request: Request, iso_file: UploadFile = File(...)):
|
||||
def iso_upload(request: Request, iso_file: UploadFile = File(...), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
safe_name = safe_iso_filename(iso_file.filename or "")
|
||||
if not safe_name:
|
||||
return templates.TemplateResponse("iso.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "error": "Можно загружать только .iso файлы с безопасным именем."}, status_code=400)
|
||||
@@ -1386,10 +1475,13 @@ def iso_upload(request: Request, iso_file: UploadFile = File(...)):
|
||||
|
||||
|
||||
@app.post("/iso/{name}/delete")
|
||||
def iso_delete(request: Request, name: str):
|
||||
def iso_delete(request: Request, name: str, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
path = iso_path_by_name(name)
|
||||
if path and path.exists() and path.is_file():
|
||||
path.unlink()
|
||||
@@ -1398,10 +1490,13 @@ def iso_delete(request: Request, name: str):
|
||||
|
||||
|
||||
@app.post("/iso/refresh")
|
||||
def iso_refresh(request: Request):
|
||||
def iso_refresh(request: Request, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
refresh_iso_pool()
|
||||
return RedirectResponse(url="/iso", status_code=303)
|
||||
|
||||
@@ -1415,10 +1510,13 @@ def disk_images_page(request: Request, error: str | None = None):
|
||||
|
||||
|
||||
@app.post("/disk-images/upload", response_class=HTMLResponse)
|
||||
def disk_image_upload(request: Request, image_file: UploadFile = File(...)):
|
||||
def disk_image_upload(request: Request, image_file: UploadFile = File(...), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
safe_name = safe_disk_upload_filename(image_file.filename or "")
|
||||
if not safe_name:
|
||||
return templates.TemplateResponse("disk_images.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "images": list_disk_image_files(), "error": "Можно загружать только .img, .raw, .qcow2, .img.xz, .zip, .tar.gz или .tgz файлы."}, status_code=400)
|
||||
@@ -1462,10 +1560,13 @@ def disk_image_upload(request: Request, image_file: UploadFile = File(...)):
|
||||
|
||||
|
||||
@app.post("/disk-images/{name}/delete")
|
||||
def disk_image_delete(request: Request, name: str):
|
||||
def disk_image_delete(request: Request, name: str, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
path = disk_image_path_by_name(name)
|
||||
if path and path.exists() and path.is_file():
|
||||
path.unlink()
|
||||
@@ -1483,10 +1584,13 @@ def cloud_images_page(request: Request, error: str | None = None):
|
||||
|
||||
|
||||
@app.post("/cloud-images/download")
|
||||
def cloud_image_download(request: Request, key: str = Form(...)):
|
||||
def cloud_image_download(request: Request, key: str = Form(...), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
entry = cloud_images.catalog_entry(key)
|
||||
if not entry:
|
||||
return cloud_images_page(request, error="Неизвестный образ каталога.")
|
||||
@@ -1501,10 +1605,13 @@ def cloud_image_download(request: Request, key: str = Form(...)):
|
||||
|
||||
|
||||
@app.post("/cloud-images/{name}/delete")
|
||||
def cloud_image_delete(request: Request, name: str):
|
||||
def cloud_image_delete(request: Request, name: str, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
path = cloud_images.image_path_by_name(name)
|
||||
if path and path.exists() and path.is_file():
|
||||
path.unlink()
|
||||
@@ -1526,10 +1633,13 @@ def vm_create_cloud_page(request: Request):
|
||||
|
||||
|
||||
@app.post("/vm/create-cloud", response_class=HTMLResponse)
|
||||
def vm_create_cloud_submit(request: Request, name: str = Form(...), image_name: str = Form(...), memory: int = Form(...), vcpus: int = Form(...), disk_size: int = Form(...), username: str = Form("admin"), password: str = Form(""), ssh_key: str = Form(""), network_mode: str = Form("nat"), bridge: str = Form(DEFAULT_BRIDGE)):
|
||||
def vm_create_cloud_submit(request: Request, name: str = Form(...), image_name: str = Form(...), memory: int = Form(...), vcpus: int = Form(...), disk_size: int = Form(...), username: str = Form("admin"), password: str = Form(""), ssh_key: str = Form(""), network_mode: str = Form("nat"), bridge: str = Form(DEFAULT_BRIDGE), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
form = {"name": name, "image_name": image_name, "memory": memory, "vcpus": vcpus, "disk_size": disk_size, "username": username, "ssh_key": ssh_key, "network_mode": network_mode, "bridge": bridge}
|
||||
image_path = cloud_images.image_path_by_name(image_name)
|
||||
error = None
|
||||
@@ -1648,10 +1758,13 @@ def network_page(request: Request, error: str | None = None):
|
||||
|
||||
|
||||
@app.post("/network/vm-ip/save")
|
||||
def network_vm_ip_save(request: Request, vm_name: str = Form(...), manual_ip: str = Form("")):
|
||||
def network_vm_ip_save(request: Request, vm_name: str = Form(...), manual_ip: str = Form(""), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
if not valid_vm_name(vm_name):
|
||||
return RedirectResponse(url="/network", status_code=303)
|
||||
manual_ip = (manual_ip or "").strip()
|
||||
@@ -1676,10 +1789,13 @@ def network_vm_ip_save(request: Request, vm_name: str = Form(...), manual_ip: st
|
||||
|
||||
|
||||
@app.post("/network/nat/setup")
|
||||
def network_nat_setup(request: Request):
|
||||
def network_nat_setup(request: Request, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
try:
|
||||
network_core.create_nat_network()
|
||||
network_core.apply_port_forwards()
|
||||
@@ -1691,10 +1807,13 @@ def network_nat_setup(request: Request):
|
||||
|
||||
|
||||
@app.post("/network/forward/add")
|
||||
def network_forward_add(request: Request, vm_name: str = Form(...), guest_ip: str = Form(...), external_port: str = Form(...), guest_port: str = Form(...), protocol: str = Form("tcp"), note: str = Form("")):
|
||||
def network_forward_add(request: Request, vm_name: str = Form(...), guest_ip: str = Form(...), external_port: str = Form(...), guest_port: str = Form(...), protocol: str = Form("tcp"), note: str = Form(""), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
try:
|
||||
network_core.add_port_forward(vm_name, guest_ip, external_port, guest_port, protocol, note)
|
||||
except NetworkError as exc:
|
||||
@@ -1703,10 +1822,13 @@ def network_forward_add(request: Request, vm_name: str = Form(...), guest_ip: st
|
||||
|
||||
|
||||
@app.post("/network/forward/{forward_id}/delete")
|
||||
def network_forward_delete(request: Request, forward_id: str):
|
||||
def network_forward_delete(request: Request, forward_id: str, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
try:
|
||||
network_core.delete_port_forward(forward_id)
|
||||
except NetworkError:
|
||||
@@ -1715,10 +1837,13 @@ def network_forward_delete(request: Request, forward_id: str):
|
||||
|
||||
|
||||
@app.post("/network/apply")
|
||||
def network_apply(request: Request):
|
||||
def network_apply(request: Request, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
try:
|
||||
network_core.apply_port_forwards()
|
||||
except NetworkError as exc:
|
||||
@@ -1727,10 +1852,13 @@ def network_apply(request: Request):
|
||||
|
||||
|
||||
@app.post("/network/diagnose", response_class=HTMLResponse)
|
||||
def network_diagnose(request: Request, vm_name: str = Form(...), external_port: int = Form(...), guest_port: int = Form(...), protocol: str = Form("tcp")):
|
||||
def network_diagnose(request: Request, vm_name: str = Form(...), external_port: int = Form(...), guest_port: int = Form(...), protocol: str = Form("tcp"), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
diagnostics = None
|
||||
error = None
|
||||
try:
|
||||
@@ -1793,10 +1921,13 @@ def update_status(request: Request):
|
||||
|
||||
|
||||
@app.post("/update/check")
|
||||
def update_check(request: Request):
|
||||
def update_check(request: Request, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
try:
|
||||
update_core.check_updates(fetch=True)
|
||||
except Exception:
|
||||
@@ -1805,10 +1936,13 @@ def update_check(request: Request):
|
||||
|
||||
|
||||
@app.post("/update/apply")
|
||||
def update_apply(request: Request):
|
||||
def update_apply(request: Request, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
try:
|
||||
update_core.start_update()
|
||||
except Exception:
|
||||
@@ -1883,10 +2017,13 @@ def vm_create_page(request: Request):
|
||||
|
||||
|
||||
@app.post("/vm/create", response_class=HTMLResponse)
|
||||
def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form(...), vcpus: int = Form(...), disk_size: int = Form(...), iso_path: str = Form(""), disk_image_path: str = Form(""), source_type: str = Form("iso"), guest_arch: str = Form("auto"), boot_order: str = Form("auto"), network_mode: str = Form("nat"), bridge: str = Form(DEFAULT_BRIDGE)):
|
||||
def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form(...), vcpus: int = Form(...), disk_size: int = Form(...), iso_path: str = Form(""), disk_image_path: str = Form(""), source_type: str = Form("iso"), guest_arch: str = Form("auto"), boot_order: str = Form("auto"), network_mode: str = Form("nat"), bridge: str = Form(DEFAULT_BRIDGE), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
form = {"name": name, "memory": memory, "vcpus": vcpus, "disk_size": disk_size, "iso_path": iso_path, "disk_image_path": disk_image_path, "source_type": source_type, "guest_arch": guest_arch, "boot_order": boot_order, "network_mode": network_mode, "bridge": bridge}
|
||||
error = None
|
||||
if not valid_vm_name(name):
|
||||
@@ -2025,10 +2162,13 @@ def vm_detail_page(request: Request, name: str):
|
||||
|
||||
|
||||
@app.post("/vm/{name}/resources")
|
||||
def vm_resources_apply(request: Request, name: str, memory_mb: int = Form(...), vcpus: int = Form(...), guest_arch: str = Form("keep")):
|
||||
def vm_resources_apply(request: Request, name: str, memory_mb: int = Form(...), vcpus: int = Form(...), guest_arch: str = Form("keep"), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
ok, message = apply_vm_resources(name, memory_mb, vcpus, guest_arch)
|
||||
if ok:
|
||||
return RedirectResponse(url=f"/vm/{name}?resource_message={message}", status_code=303)
|
||||
@@ -2036,10 +2176,13 @@ def vm_resources_apply(request: Request, name: str, memory_mb: int = Form(...),
|
||||
|
||||
|
||||
@app.post("/vm/{name}/boot-order")
|
||||
def vm_boot_order_apply(request: Request, name: str, boot_order: str = Form("auto")):
|
||||
def vm_boot_order_apply(request: Request, name: str, boot_order: str = Form("auto"), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
ok, message = apply_vm_boot_order(name, boot_order)
|
||||
if ok:
|
||||
return RedirectResponse(url=f"/vm/{name}?boot_message={message}", status_code=303)
|
||||
@@ -2047,10 +2190,13 @@ def vm_boot_order_apply(request: Request, name: str, boot_order: str = Form("aut
|
||||
|
||||
|
||||
@app.post("/vm/{name}/iso/mount")
|
||||
def vm_iso_mount_apply(request: Request, name: str, iso_path: str = Form(...)):
|
||||
def vm_iso_mount_apply(request: Request, name: str, iso_path: str = Form(...), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
ok, message = mount_vm_iso(name, iso_path)
|
||||
if ok:
|
||||
return RedirectResponse(url=f"/vm/{name}?iso_message={message}", status_code=303)
|
||||
@@ -2058,10 +2204,13 @@ def vm_iso_mount_apply(request: Request, name: str, iso_path: str = Form(...)):
|
||||
|
||||
|
||||
@app.post("/vm/{name}/iso/unmount")
|
||||
def vm_iso_unmount_apply(request: Request, name: str):
|
||||
def vm_iso_unmount_apply(request: Request, name: str, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
ok, message = detach_vm_iso(name)
|
||||
if ok:
|
||||
return RedirectResponse(url=f"/vm/{name}?iso_message={message}", status_code=303)
|
||||
@@ -2069,10 +2218,13 @@ def vm_iso_unmount_apply(request: Request, name: str):
|
||||
|
||||
|
||||
@app.post("/vm/{name}/snapshot/{snap_action}")
|
||||
def vm_snapshot_apply(request: Request, name: str, snap_action: str, snapshot_name: str = Form(...), description: str = Form("")):
|
||||
def vm_snapshot_apply(request: Request, name: str, snap_action: str, snapshot_name: str = Form(...), description: str = Form(""), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
if not valid_vm_name(name) or not vm_exists(name):
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
if snap_action not in ("create", "revert", "delete"):
|
||||
@@ -2083,10 +2235,13 @@ def vm_snapshot_apply(request: Request, name: str, snap_action: str, snapshot_na
|
||||
|
||||
|
||||
@app.post("/vm/{name}/clone")
|
||||
def vm_clone(request: Request, name: str, new_name: str = Form(...)):
|
||||
def vm_clone(request: Request, name: str, new_name: str = Form(...), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
if not valid_vm_name(name) or not vm_exists(name):
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
if not valid_vm_name(new_name):
|
||||
@@ -2103,10 +2258,13 @@ def vm_clone(request: Request, name: str, new_name: str = Form(...)):
|
||||
|
||||
|
||||
@app.post("/vm/{name}/template")
|
||||
def vm_template_toggle(request: Request, name: str, enabled: str = Form("0")):
|
||||
def vm_template_toggle(request: Request, name: str, enabled: str = Form("0"), csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
if not valid_vm_name(name) or not vm_exists(name):
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
set_vm_template(name, enabled == "1")
|
||||
@@ -2115,10 +2273,13 @@ def vm_template_toggle(request: Request, name: str, enabled: str = Form("0")):
|
||||
|
||||
|
||||
@app.post("/vm/{name}/{action}")
|
||||
def vm_action(request: Request, name: str, action: str):
|
||||
def vm_action(request: Request, name: str, action: str, csrf_token: str = Form("")):
|
||||
auth_redirect = require_auth(request)
|
||||
if auth_redirect:
|
||||
return auth_redirect
|
||||
csrf_fail = require_csrf(request, csrf_token)
|
||||
if csrf_fail:
|
||||
return csrf_fail
|
||||
allowed = {"start": ["virsh", "start", name], "shutdown": ["virsh", "shutdown", name], "reboot": ["virsh", "reboot", name], "destroy": ["virsh", "destroy", name], "autostart": ["virsh", "autostart", name], "autostart-disable": ["virsh", "autostart", "--disable", name]}
|
||||
if action == "delete":
|
||||
run_cmd(["virsh", "destroy", name], timeout=20)
|
||||
|
||||
Ссылка в новой задаче
Block a user