1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/console/tokens.py
catborise 2e4e9c78f8 fix(console): a VNC token works only in its user's session; refusals reach the Logs page
The VNC page's token travels in the websocket URL, so nginx's access log
and anyone who saw the URL had it for 120 s, and it opened the console as
a bearer token. novncd now also wants the session cookie of the token's
user. Its refusals were a warning without a user or an address: they now
name the client address (nginx sends X-Real-IP to novncd too) and leave
a Logs row "VNC console refused: reason".
2026-11-05 18:08:03 +03:00

62 lines
2.8 KiB
Python

"""The console token: the console view signs "host:uuid:user_id:console type"
for the VM whose console it shows; the console daemons (novncd for vnc,
socketiod for pty) accept it for 120 s if that user may still open that VM's
console. novncd, whose token travels in the websocket URL (and so in logs),
also wants the session cookie of the token's user (session_user_id).
socketiod's token travels in the Socket.IO handshake, not the URL."""
from importlib import import_module
from django import db
from django.conf import settings
from django.contrib.auth import SESSION_KEY
from django.contrib.auth import get_user_model
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
from django.http import parse_cookie
from instances.models import Instance
from instances.utils import can_open_console
SIGNER = TimestampSigner(salt="console.novnc")
MAX_AGE = 120
ACCESS_CHECK = 10 # seconds between checks that an open console's user may still use it
def sign(compute_id, uuid, user_id, console_type):
return SIGNER.sign(f"{compute_id}:{uuid}:{user_id}:{console_type}")
def authorize(token, console_type):
"""The Instance the token opens and the user it was given to;
PermissionError otherwise, also for a token of another console type."""
try:
host, uuid, user_id, signed_type = SIGNER.unsign(token, max_age=MAX_AGE).split(":")
host, user_id = int(host), int(user_id)
except (BadSignature, SignatureExpired, ValueError, TypeError):
raise PermissionError("Invalid or expired console token")
if signed_type != console_type:
raise PermissionError(f"The token is for a {signed_type} console")
instance = Instance.objects.filter(compute_id=host, uuid=uuid).select_related("compute").first()
user = get_user_model().objects.filter(id=user_id, is_active=True).first()
if instance is None or user is None or not can_open_console(user, instance):
raise PermissionError("No access to this console")
return instance, user
def may_still_open(user_id, instance):
"""The user still may open the console: an owner may have been removed or
the user deactivated since the session started."""
try:
user = get_user_model().objects.filter(pk=user_id).first()
return user is not None and can_open_console(user, instance)
finally:
db.connection.close() # a session may last for days: no idle connection meanwhile
def session_user_id(cookie_header):
"""The id (a string) of the user signed in with the session cookie of a
request's Cookie header, or None."""
session_key = parse_cookie(cookie_header or "").get(settings.SESSION_COOKIE_NAME)
if not session_key:
return None
return import_module(settings.SESSION_ENGINE).SessionStore(session_key).get(SESSION_KEY)