mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-07-11 09:25:03 +00:00
fix(reports): secure PDF export URL and authentication
This commit is contained in:
@@ -5,8 +5,11 @@ from io import BytesIO
|
||||
from urllib.parse import parse_qsl, urlencode, urlparse, urlsplit, urlunsplit
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.http import FileResponse, HttpResponseBadRequest, JsonResponse
|
||||
from django.http import (
|
||||
FileResponse, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
||||
)
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -58,30 +61,51 @@ charts_map = {
|
||||
}
|
||||
|
||||
|
||||
def export_chart_to_pdf(chart_name, sessionid, request=None):
|
||||
def get_trusted_site_url():
|
||||
# Playwright navigation and cookie scope must never depend on request Host.
|
||||
site_url = getattr(settings, 'SITE_URL', '')
|
||||
if not isinstance(site_url, str):
|
||||
raise ValueError('Invalid SITE_URL')
|
||||
site_url = site_url.rstrip('/')
|
||||
parsed_site = urlparse(site_url)
|
||||
if (
|
||||
not site_url or parsed_site.scheme not in ('http', 'https') or
|
||||
not parsed_site.hostname or parsed_site.username or
|
||||
parsed_site.password or parsed_site.query or parsed_site.fragment
|
||||
):
|
||||
raise ValueError('Invalid SITE_URL')
|
||||
return site_url, parsed_site
|
||||
|
||||
|
||||
def build_report_url(chart_path):
|
||||
path = urllib.parse.unquote(chart_path)
|
||||
if not path.startswith('/ui/#/reports/'):
|
||||
raise ValueError('Invalid report path')
|
||||
site_url, _ = get_trusted_site_url()
|
||||
return site_url + path
|
||||
|
||||
|
||||
def export_chart_to_pdf(chart_name, sessionid, request):
|
||||
chart_info = charts_map.get(chart_name)
|
||||
if not chart_info:
|
||||
return None, None
|
||||
|
||||
if request:
|
||||
url = request.build_absolute_uri(urllib.parse.unquote(chart_info['path']))
|
||||
else:
|
||||
url = urllib.parse.unquote(chart_info['path'])
|
||||
url = build_report_url(chart_info['path'])
|
||||
_, parsed_site = get_trusted_site_url()
|
||||
if settings.DEBUG_DEV:
|
||||
url = url.replace(":8080", ":9528")
|
||||
if request:
|
||||
oid = request.COOKIES.get("X-JMS-ORG")
|
||||
request_data = request.POST if request.method == 'POST' else request.GET
|
||||
query = dict(parse_qsl(urlsplit(url).query, keep_blank_values=True))
|
||||
for key, value in request_data.items():
|
||||
if key in {'chart', 'csrfmiddlewaretoken'}:
|
||||
continue
|
||||
query[key] = value
|
||||
query.setdefault('days', 7)
|
||||
query['oid'] = oid
|
||||
parts = list(urlsplit(url))
|
||||
parts[3] = urlencode(query, doseq=True)
|
||||
url = urlunsplit(parts)
|
||||
oid = request.COOKIES.get("X-JMS-ORG")
|
||||
request_data = request.POST if request.method == 'POST' else request.GET
|
||||
query = dict(parse_qsl(urlsplit(url).query, keep_blank_values=True))
|
||||
for key, value in request_data.items():
|
||||
if key in {'chart', 'csrfmiddlewaretoken'}:
|
||||
continue
|
||||
query[key] = value
|
||||
query.setdefault('days', 7)
|
||||
query['oid'] = oid
|
||||
parts = list(urlsplit(url))
|
||||
parts[3] = urlencode(query, doseq=True)
|
||||
url = urlunsplit(parts)
|
||||
|
||||
with sync_playwright() as p:
|
||||
lang = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
|
||||
@@ -92,12 +116,12 @@ def export_chart_to_pdf(chart_name, sessionid, request=None):
|
||||
ignore_https_errors=True
|
||||
)
|
||||
# 设置 sessionid cookie
|
||||
parsed_url = urlparse(url)
|
||||
context.add_cookies([
|
||||
{
|
||||
'name': settings.SESSION_COOKIE_NAME,
|
||||
'value': sessionid,
|
||||
'domain': parsed_url.hostname,
|
||||
# hostname is derived from SITE_URL and never includes a port.
|
||||
'domain': parsed_site.hostname,
|
||||
'path': '/',
|
||||
'httpOnly': True,
|
||||
'secure': False, # 如有 https 可改 True
|
||||
@@ -122,7 +146,7 @@ def export_chart_to_pdf(chart_name, sessionid, request=None):
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class ExportPdfView(View):
|
||||
class ExportPdfView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
chart_name = request.GET.get('chart')
|
||||
return self._handle_export(request, chart_name)
|
||||
@@ -132,13 +156,20 @@ class ExportPdfView(View):
|
||||
return self._handle_export(request, chart_name)
|
||||
|
||||
def _handle_export(self, request, chart_name):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponse(status=401)
|
||||
if not chart_name:
|
||||
return HttpResponseBadRequest('Missing chart parameter')
|
||||
sessionid = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
||||
if not sessionid:
|
||||
return HttpResponseBadRequest('No sessionid found in cookies')
|
||||
|
||||
pdf_bytes, title = export_chart_to_pdf(chart_name, sessionid, request=request)
|
||||
try:
|
||||
pdf_bytes, title = export_chart_to_pdf(
|
||||
chart_name, sessionid, request=request
|
||||
)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest('Invalid report configuration')
|
||||
if not pdf_bytes:
|
||||
return HttpResponseBadRequest('Failed to generate PDF')
|
||||
filename = f"{title}-{timezone.now().strftime('%Y%m%d%H%M%S')}.pdf"
|
||||
@@ -147,9 +178,11 @@ class ExportPdfView(View):
|
||||
return response
|
||||
|
||||
|
||||
class SendMailView(View):
|
||||
class SendMailView(LoginRequiredMixin, View):
|
||||
|
||||
def post(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponse(status=401)
|
||||
chart_name = request.GET.get('chart') or request.POST.get('chart')
|
||||
if not chart_name:
|
||||
return HttpResponseBadRequest('Missing chart parameter')
|
||||
@@ -161,7 +194,12 @@ class SendMailView(View):
|
||||
return HttpResponseBadRequest('No sessionid found in cookies')
|
||||
|
||||
# 1. 生成 PDF
|
||||
pdf_bytes, title = export_chart_to_pdf(chart_name, sessionid, request=request)
|
||||
try:
|
||||
pdf_bytes, title = export_chart_to_pdf(
|
||||
chart_name, sessionid, request=request
|
||||
)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest('Invalid report configuration')
|
||||
if not pdf_bytes:
|
||||
return HttpResponseBadRequest('Failed to generate PDF')
|
||||
|
||||
@@ -188,4 +226,4 @@ class SendMailView(View):
|
||||
msg.send()
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": _('Failed to send email: ') + str(e)})
|
||||
return JsonResponse({"message": _('Email sent successfully to ') + email})
|
||||
return JsonResponse({"message": _('Email sent successfully to ') + email})
|
||||
|
||||
Reference in New Issue
Block a user