File size: 4,895 Bytes
b4f2238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""
中间件模块

提供请求指标、安全头、权限策略、ServiceLocator 作用域等中间件。
"""

import logging
import os
import time
from collections.abc import Callable, Iterable, Mapping

from django.conf import settings
from django.http import HttpRequest, HttpResponse


class RequestMetricsMiddleware:
    """请求指标记录中间件(WSGI callable 风格)"""

    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
        self.get_response = get_response

    def __call__(self, request: HttpRequest) -> HttpResponse:
        from apps.core.telemetry import metrics as metrics_module

        enabled = not settings.DEBUG or os.environ.get("DJANGO_REQUEST_METRICS") == "1"

        start = time.monotonic()
        status_code = 500
        try:
            response = self.get_response(request)
            status_code = response.status_code
            return response
        finally:
            if enabled:
                duration_ms = int((time.monotonic() - start) * 1000)
                metrics_module.record_request(
                    method=request.method or "GET",
                    path=request.path,
                    status_code=status_code,
                    duration_ms=duration_ms,
                )


class SecurityHeadersMiddleware:
    """按路径设置 Content-Security-Policy 响应头的中间件"""

    # /api/v1/docs 等文档路径不使用 API 策略
    _DOCS_SUFFIXES = ("/docs", "/schema", "/redoc", "/swagger")

    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
        self.get_response = get_response

    def __call__(self, request: HttpRequest) -> HttpResponse:
        response = self.get_response(request)
        self._apply_csp(request, response)
        return response

    def _apply_csp(self, request: HttpRequest, response: HttpResponse) -> None:
        path = request.path

        if path.startswith("/admin"):
            csp = getattr(settings, "CONTENT_SECURITY_POLICY_ADMIN", "")
            csp_ro = getattr(settings, "CONTENT_SECURITY_POLICY_ADMIN_REPORT_ONLY", "")
        elif path.startswith("/api/") and not any(path.endswith(s) for s in self._DOCS_SUFFIXES):
            csp = getattr(settings, "CONTENT_SECURITY_POLICY_API", "")
            csp_ro = getattr(settings, "CONTENT_SECURITY_POLICY_API_REPORT_ONLY", "")
        else:
            csp = getattr(settings, "CONTENT_SECURITY_POLICY", "")
            csp_ro = getattr(settings, "CONTENT_SECURITY_POLICY_REPORT_ONLY", "")

        if csp:
            response["Content-Security-Policy"] = csp
        if csp_ro:
            response["Content-Security-Policy-Report-Only"] = csp_ro


class PermissionsPolicyMiddleware:
    """设置 Permissions-Policy 响应头的中间件"""

    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
        self.get_response = get_response

    def __call__(self, request: HttpRequest) -> HttpResponse:
        response = self.get_response(request)
        policy = getattr(settings, "PERMISSIONS_POLICY", "")
        if policy:
            response["Permissions-Policy"] = self._serialize_policy(policy)
        return response

    def _serialize_policy(self, policy: str | Mapping[str, object]) -> str:
        if isinstance(policy, str):
            return policy
        if not isinstance(policy, Mapping):
            return str(policy)

        directives: list[str] = []
        for feature, allowlist in policy.items():
            directives.append(f"{feature}={self._serialize_allowlist(allowlist)}")
        return ", ".join(directives)

    def _serialize_allowlist(self, allowlist: object) -> str:
        if allowlist in (None, [], (), set()):
            return "()"
        if allowlist == "*":
            return "*"
        if isinstance(allowlist, str):
            return f"({self._serialize_source(allowlist)})"
        if isinstance(allowlist, Iterable):
            values = " ".join(self._serialize_source(value) for value in allowlist)
            return f"({values})" if values else "()"
        return f"({self._serialize_source(allowlist)})"

    def _serialize_source(self, value: object) -> str:
        if value in {"self", "src", "*"}:
            return str(value)
        return f'"{value}"'


class ServiceLocatorScopeMiddleware:
    """
    ServiceLocator 请求级作用域中间件

    每个 HTTP 请求在独立的 ServiceLocator scope 中执行,
    确保请求间服务实例不互相污染(基于 ContextVar 实现)。
    """

    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
        self.get_response = get_response

    def __call__(self, request: HttpRequest) -> HttpResponse:
        from apps.core.interfaces import ServiceLocator

        with ServiceLocator.scope():
            return self.get_response(request)