mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
A request gets nginx's $request_id (or a new one when it comes without a valid one) and sends it back in X-Request-ID. The app log's lines carry the logger, process and thread, the request ID, the signed-in user and the client address; gunicorn's access lines carry the client address from X-Real-IP and the same request ID, so the two logs can be joined.
110 lines
4.7 KiB
Python
110 lines
4.7 KiB
Python
import re
|
|
import uuid
|
|
|
|
from django.contrib import messages
|
|
from django.http import JsonResponse
|
|
from django.shortcuts import redirect, render
|
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
from django.utils.translation import gettext_lazy as _
|
|
from libvirt import libvirtError
|
|
from logs.audit import request_ip
|
|
from operations import claims, jobs
|
|
from operations.utils import busy_message, public_error_text, said, summary, words
|
|
|
|
from webvirtcloud.log_context import current_request
|
|
|
|
API_PREFIX = "/api/"
|
|
# nginx's $request_id; any other value is replaced, so a client cannot write into the log
|
|
REQUEST_ID = re.compile(r"[0-9a-f]{32}")
|
|
# scripts only from this site: no inline script or event handler runs (inline styles still do);
|
|
# the XML editor (Ace) and the API docs (Redoc) start their workers from blob: URLs
|
|
CONTENT_SECURITY_POLICY = (
|
|
"script-src 'self'; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; "
|
|
"frame-ancestors 'none'"
|
|
)
|
|
|
|
|
|
def same_origin_referer(request, default):
|
|
"""The Referer without its fragment if it is on this site, else default."""
|
|
referer = request.META.get("HTTP_REFERER")
|
|
if referer and url_has_allowed_host_and_scheme(
|
|
url=referer, allowed_hosts={request.get_host()}, require_https=request.is_secure()
|
|
):
|
|
return referer.split("#")[0]
|
|
return default
|
|
|
|
|
|
def error_text(request, exception):
|
|
"""
|
|
The libvirt error to show. Administrators (superusers and the global
|
|
view_instances permission, who see the hosts) get the full error; other
|
|
users the public text.
|
|
"""
|
|
user = getattr(request, "user", None)
|
|
if user and (user.is_superuser or user.has_perm("instances.view_instances")):
|
|
return words(said(exception))
|
|
return public_error_text(exception)
|
|
|
|
|
|
class RequestIdMiddleware:
|
|
"""Names the request for its log lines (see log_context) and sends the ID back in X-Request-ID."""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
|
|
def __call__(self, request):
|
|
request_id = request.headers.get("X-Request-ID", "")
|
|
request.request_id = request_id if REQUEST_ID.fullmatch(request_id) else uuid.uuid4().hex
|
|
request.client_ip = request_ip(request)
|
|
# not reset after the response: Django logs a 4xx or 5xx answer after the middleware returns
|
|
current_request.set(request)
|
|
response = self.get_response(request)
|
|
response["X-Request-ID"] = request.request_id
|
|
return response
|
|
|
|
|
|
class ContentSecurityPolicyMiddleware:
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
|
|
def __call__(self, request):
|
|
response = self.get_response(request)
|
|
response.setdefault("Content-Security-Policy", CONTENT_SECURITY_POLICY)
|
|
return response
|
|
|
|
|
|
class ExceptionMiddleware:
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
|
|
def __call__(self, request):
|
|
return self.get_response(request)
|
|
|
|
def process_exception(self, request, exception):
|
|
# API clients get JSON with a failure status, not the HTML error page.
|
|
if request.path_info.startswith(API_PREFIX):
|
|
if isinstance(exception, libvirtError):
|
|
return JsonResponse({"detail": error_text(request, exception)}, status=400)
|
|
if isinstance(exception, claims.Busy):
|
|
return JsonResponse({"detail": busy_message(exception, request.user),
|
|
"operation": summary(exception.blocker, request.user) if exception.blocker else None},
|
|
status=409)
|
|
if isinstance(exception, jobs.SubmissionConflict):
|
|
return JsonResponse({"detail": str(exception)}, status=409)
|
|
if isinstance(exception, TimeoutError):
|
|
return JsonResponse({"detail": "Operation is busy. Please retry."}, status=409)
|
|
return None
|
|
if isinstance(exception, claims.Busy) and request.method not in ("GET", "HEAD"):
|
|
# back to the form with the reason; a GET is never redirected to itself
|
|
messages.error(request, busy_message(exception, request.user))
|
|
return redirect(request.get_full_path())
|
|
if isinstance(exception, libvirtError):
|
|
messages.error(
|
|
request,
|
|
_("libvirt Error - %(exception)s") % {"exception": error_text(request, exception)},
|
|
)
|
|
if request.method not in ("GET", "HEAD"):
|
|
# a refused form: back to the page it was on, with the reason
|
|
return redirect(same_origin_referer(request, "/"))
|
|
return render(request, "500.html", {"libvirt_error": True}, status=500)
|
|
# TODO: check connecting to host via VPN
|