Загрузить файлы в «venv/Lib/site-packages/starlette/middleware»
This commit is contained in:
37
venv/Lib/site-packages/starlette/middleware/__init__.py
Normal file
37
venv/Lib/site-packages/starlette/middleware/__init__.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable, Iterator
|
||||||
|
from typing import Any, ParamSpec, Protocol
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
|
||||||
|
|
||||||
|
_Scope = Any
|
||||||
|
_Receive = Callable[[], Awaitable[Any]]
|
||||||
|
_Send = Callable[[Any], Awaitable[None]]
|
||||||
|
# Since `starlette.types.ASGIApp` type differs from `ASGIApplication` from `asgiref`
|
||||||
|
# we need to define a more permissive version of ASGIApp that doesn't cause type errors.
|
||||||
|
_ASGIApp = Callable[[_Scope, _Receive, _Send], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
|
class _MiddlewareFactory(Protocol[P]):
|
||||||
|
def __call__(self, app: _ASGIApp, /, *args: P.args, **kwargs: P.kwargs) -> _ASGIApp: ... # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
class Middleware:
|
||||||
|
def __init__(self, cls: _MiddlewareFactory[P], *args: P.args, **kwargs: P.kwargs) -> None:
|
||||||
|
self.cls = cls
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[Any]:
|
||||||
|
as_tuple = (self.cls, self.args, self.kwargs)
|
||||||
|
return iter(as_tuple)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
class_name = self.__class__.__name__
|
||||||
|
args_strings = [f"{value!r}" for value in self.args]
|
||||||
|
option_strings = [f"{key}={value!r}" for key, value in self.kwargs.items()]
|
||||||
|
name = getattr(self.cls, "__name__", "")
|
||||||
|
args_repr = ", ".join([name] + args_strings + option_strings)
|
||||||
|
return f"{class_name}({args_repr})"
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from starlette.authentication import (
|
||||||
|
AuthCredentials,
|
||||||
|
AuthenticationBackend,
|
||||||
|
AuthenticationError,
|
||||||
|
UnauthenticatedUser,
|
||||||
|
)
|
||||||
|
from starlette.requests import HTTPConnection
|
||||||
|
from starlette.responses import PlainTextResponse, Response
|
||||||
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationMiddleware:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
app: ASGIApp,
|
||||||
|
backend: AuthenticationBackend,
|
||||||
|
on_error: Callable[[HTTPConnection, AuthenticationError], Response] | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.app = app
|
||||||
|
self.backend = backend
|
||||||
|
self.on_error: Callable[[HTTPConnection, AuthenticationError], Response] = (
|
||||||
|
on_error if on_error is not None else self.default_on_error
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||||
|
if scope["type"] not in ["http", "websocket"]:
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
return
|
||||||
|
|
||||||
|
conn = HTTPConnection(scope)
|
||||||
|
try:
|
||||||
|
auth_result = await self.backend.authenticate(conn)
|
||||||
|
except AuthenticationError as exc:
|
||||||
|
response = self.on_error(conn, exc)
|
||||||
|
if scope["type"] == "websocket":
|
||||||
|
await send({"type": "websocket.close", "code": 1000})
|
||||||
|
else:
|
||||||
|
await response(scope, receive, send)
|
||||||
|
return
|
||||||
|
|
||||||
|
if auth_result is None:
|
||||||
|
auth_result = AuthCredentials(), UnauthenticatedUser()
|
||||||
|
scope["auth"], scope["user"] = auth_result
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def default_on_error(conn: HTTPConnection, exc: Exception) -> Response:
|
||||||
|
return PlainTextResponse(str(exc), status_code=400)
|
||||||
244
venv/Lib/site-packages/starlette/middleware/base.py
Normal file
244
venv/Lib/site-packages/starlette/middleware/base.py
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Callable, Mapping, MutableMapping
|
||||||
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
|
||||||
|
from starlette._utils import create_collapsing_task_group
|
||||||
|
from starlette.requests import ClientDisconnect, Request
|
||||||
|
from starlette.responses import Response
|
||||||
|
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
||||||
|
|
||||||
|
RequestResponseEndpoint = Callable[[Request], Awaitable[Response]]
|
||||||
|
DispatchFunction = Callable[[Request, RequestResponseEndpoint], Awaitable[Response]]
|
||||||
|
BodyStreamGenerator = AsyncGenerator[bytes | MutableMapping[str, Any], None]
|
||||||
|
AsyncContentStream = AsyncIterable[str | bytes | memoryview | MutableMapping[str, Any]]
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class _CachedRequest(Request):
|
||||||
|
"""
|
||||||
|
If the user calls Request.body() from their dispatch function
|
||||||
|
we cache the entire request body in memory and pass that to downstream middlewares,
|
||||||
|
but if they call Request.stream() then all we do is send an
|
||||||
|
empty body so that downstream things don't hang forever.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, scope: Scope, receive: Receive):
|
||||||
|
super().__init__(scope, receive)
|
||||||
|
self._wrapped_rcv_disconnected = False
|
||||||
|
self._wrapped_rcv_consumed = False
|
||||||
|
self._wrapped_rc_stream = self.stream()
|
||||||
|
|
||||||
|
async def wrapped_receive(self) -> Message:
|
||||||
|
# wrapped_rcv state 1: disconnected
|
||||||
|
if self._wrapped_rcv_disconnected:
|
||||||
|
# we've already sent a disconnect to the downstream app
|
||||||
|
# we don't need to wait to get another one
|
||||||
|
# (although most ASGI servers will just keep sending it)
|
||||||
|
return {"type": "http.disconnect"}
|
||||||
|
# wrapped_rcv state 1: consumed but not yet disconnected
|
||||||
|
if self._wrapped_rcv_consumed:
|
||||||
|
# since the downstream app has consumed us all that is left
|
||||||
|
# is to send it a disconnect
|
||||||
|
if self._is_disconnected:
|
||||||
|
# the middleware has already seen the disconnect
|
||||||
|
# since we know the client is disconnected no need to wait
|
||||||
|
# for the message
|
||||||
|
self._wrapped_rcv_disconnected = True
|
||||||
|
return {"type": "http.disconnect"}
|
||||||
|
# we don't know yet if the client is disconnected or not
|
||||||
|
# so we'll wait until we get that message
|
||||||
|
msg = await self.receive()
|
||||||
|
if msg["type"] != "http.disconnect": # pragma: no cover
|
||||||
|
# at this point a disconnect is all that we should be receiving
|
||||||
|
# if we get something else, things went wrong somewhere
|
||||||
|
raise RuntimeError(f"Unexpected message received: {msg['type']}")
|
||||||
|
self._wrapped_rcv_disconnected = True
|
||||||
|
return msg
|
||||||
|
|
||||||
|
# wrapped_rcv state 3: not yet consumed
|
||||||
|
if getattr(self, "_body", None) is not None:
|
||||||
|
# body() was called, we return it even if the client disconnected
|
||||||
|
self._wrapped_rcv_consumed = True
|
||||||
|
return {
|
||||||
|
"type": "http.request",
|
||||||
|
"body": self._body,
|
||||||
|
"more_body": False,
|
||||||
|
}
|
||||||
|
elif self._stream_consumed:
|
||||||
|
# stream() was called to completion
|
||||||
|
# return an empty body so that downstream apps don't hang
|
||||||
|
# waiting for a disconnect
|
||||||
|
self._wrapped_rcv_consumed = True
|
||||||
|
return {
|
||||||
|
"type": "http.request",
|
||||||
|
"body": b"",
|
||||||
|
"more_body": False,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# body() was never called and stream() wasn't consumed
|
||||||
|
try:
|
||||||
|
stream = self.stream()
|
||||||
|
chunk = await stream.__anext__()
|
||||||
|
self._wrapped_rcv_consumed = self._stream_consumed
|
||||||
|
return {
|
||||||
|
"type": "http.request",
|
||||||
|
"body": chunk,
|
||||||
|
"more_body": not self._stream_consumed,
|
||||||
|
}
|
||||||
|
except ClientDisconnect:
|
||||||
|
self._wrapped_rcv_disconnected = True
|
||||||
|
return {"type": "http.disconnect"}
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHTTPMiddleware:
|
||||||
|
def __init__(self, app: ASGIApp, dispatch: DispatchFunction | None = None) -> None:
|
||||||
|
self.app = app
|
||||||
|
self.dispatch_func = self.dispatch if dispatch is None else dispatch
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||||
|
if scope["type"] != "http":
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
return
|
||||||
|
|
||||||
|
request = _CachedRequest(scope, receive)
|
||||||
|
wrapped_receive = request.wrapped_receive
|
||||||
|
response_sent = anyio.Event()
|
||||||
|
app_exc: Exception | None = None
|
||||||
|
exception_already_raised = False
|
||||||
|
|
||||||
|
async def call_next(request: Request) -> Response:
|
||||||
|
async def receive_or_disconnect() -> Message:
|
||||||
|
if response_sent.is_set():
|
||||||
|
return {"type": "http.disconnect"}
|
||||||
|
|
||||||
|
async with anyio.create_task_group() as task_group:
|
||||||
|
|
||||||
|
async def wrap(func: Callable[[], Awaitable[T]]) -> T:
|
||||||
|
result = await func()
|
||||||
|
task_group.cancel_scope.cancel()
|
||||||
|
return result
|
||||||
|
|
||||||
|
task_group.start_soon(wrap, response_sent.wait)
|
||||||
|
message = await wrap(wrapped_receive)
|
||||||
|
|
||||||
|
if response_sent.is_set():
|
||||||
|
return {"type": "http.disconnect"}
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
async def send_no_error(message: Message) -> None:
|
||||||
|
try:
|
||||||
|
await send_stream.send(message)
|
||||||
|
except anyio.BrokenResourceError:
|
||||||
|
# recv_stream has been closed, i.e. response_sent has been set.
|
||||||
|
return
|
||||||
|
|
||||||
|
async def coro() -> None:
|
||||||
|
nonlocal app_exc
|
||||||
|
|
||||||
|
with send_stream:
|
||||||
|
try:
|
||||||
|
await self.app(scope, receive_or_disconnect, send_no_error)
|
||||||
|
except Exception as exc:
|
||||||
|
app_exc = exc
|
||||||
|
|
||||||
|
task_group.start_soon(coro)
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = await recv_stream.receive()
|
||||||
|
info = message.get("info", None)
|
||||||
|
if message["type"] == "http.response.debug" and info is not None:
|
||||||
|
message = await recv_stream.receive()
|
||||||
|
except anyio.EndOfStream:
|
||||||
|
if app_exc is not None:
|
||||||
|
nonlocal exception_already_raised
|
||||||
|
exception_already_raised = True
|
||||||
|
# Prevent `anyio.EndOfStream` from polluting app exception context.
|
||||||
|
# If both cause and context are None then the context is suppressed
|
||||||
|
# and `anyio.EndOfStream` is not present in the exception traceback.
|
||||||
|
# If exception cause is not None then it is propagated with
|
||||||
|
# reraising here.
|
||||||
|
# If exception has no cause but has context set then the context is
|
||||||
|
# propagated as a cause with the reraise. This is necessary in order
|
||||||
|
# to prevent `anyio.EndOfStream` from polluting the exception
|
||||||
|
# context.
|
||||||
|
raise app_exc from app_exc.__cause__ or app_exc.__context__
|
||||||
|
raise RuntimeError("No response returned.")
|
||||||
|
|
||||||
|
assert message["type"] == "http.response.start"
|
||||||
|
|
||||||
|
async def body_stream() -> BodyStreamGenerator:
|
||||||
|
async for message in recv_stream:
|
||||||
|
if message["type"] == "http.response.pathsend":
|
||||||
|
yield message
|
||||||
|
break
|
||||||
|
assert message["type"] == "http.response.body", f"Unexpected message: {message}"
|
||||||
|
body = message.get("body", b"")
|
||||||
|
if body:
|
||||||
|
yield body
|
||||||
|
if not message.get("more_body", False):
|
||||||
|
break
|
||||||
|
|
||||||
|
response = _StreamingResponse(status_code=message["status"], content=body_stream(), info=info)
|
||||||
|
response.raw_headers = message["headers"]
|
||||||
|
return response
|
||||||
|
|
||||||
|
streams: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream()
|
||||||
|
send_stream, recv_stream = streams
|
||||||
|
with recv_stream, send_stream:
|
||||||
|
async with create_collapsing_task_group() as task_group:
|
||||||
|
response = await self.dispatch_func(request, call_next)
|
||||||
|
await response(scope, wrapped_receive, send)
|
||||||
|
response_sent.set()
|
||||||
|
recv_stream.close()
|
||||||
|
if app_exc is not None and not exception_already_raised:
|
||||||
|
raise app_exc
|
||||||
|
|
||||||
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
||||||
|
raise NotImplementedError() # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
class _StreamingResponse(Response):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
content: AsyncContentStream,
|
||||||
|
status_code: int = 200,
|
||||||
|
headers: Mapping[str, str] | None = None,
|
||||||
|
media_type: str | None = None,
|
||||||
|
info: Mapping[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.info = info
|
||||||
|
self.body_iterator = content
|
||||||
|
self.status_code = status_code
|
||||||
|
self.media_type = media_type
|
||||||
|
self.init_headers(headers)
|
||||||
|
self.background = None
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||||
|
if self.info is not None:
|
||||||
|
await send({"type": "http.response.debug", "info": self.info})
|
||||||
|
await send(
|
||||||
|
{
|
||||||
|
"type": "http.response.start",
|
||||||
|
"status": self.status_code,
|
||||||
|
"headers": self.raw_headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
should_close_body = True
|
||||||
|
async for chunk in self.body_iterator:
|
||||||
|
if isinstance(chunk, dict):
|
||||||
|
# We got an ASGI message which is not response body (eg: pathsend)
|
||||||
|
should_close_body = False
|
||||||
|
await send(chunk)
|
||||||
|
continue
|
||||||
|
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
||||||
|
|
||||||
|
if should_close_body:
|
||||||
|
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
||||||
|
|
||||||
|
if self.background:
|
||||||
|
await self.background()
|
||||||
179
venv/Lib/site-packages/starlette/middleware/cors.py
Normal file
179
venv/Lib/site-packages/starlette/middleware/cors.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import re
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from starlette.datastructures import Headers, MutableHeaders
|
||||||
|
from starlette.responses import PlainTextResponse, Response
|
||||||
|
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
||||||
|
|
||||||
|
ALL_METHODS = ("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT")
|
||||||
|
SAFELISTED_HEADERS = {"Accept", "Accept-Language", "Content-Language", "Content-Type"}
|
||||||
|
|
||||||
|
|
||||||
|
class CORSMiddleware:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
app: ASGIApp,
|
||||||
|
allow_origins: Sequence[str] = (),
|
||||||
|
allow_methods: Sequence[str] = ("GET",),
|
||||||
|
allow_headers: Sequence[str] = (),
|
||||||
|
allow_credentials: bool = False,
|
||||||
|
allow_origin_regex: str | None = None,
|
||||||
|
allow_private_network: bool = False,
|
||||||
|
expose_headers: Sequence[str] = (),
|
||||||
|
max_age: int = 600,
|
||||||
|
) -> None:
|
||||||
|
if "*" in allow_methods:
|
||||||
|
allow_methods = ALL_METHODS
|
||||||
|
|
||||||
|
compiled_allow_origin_regex = None
|
||||||
|
if allow_origin_regex is not None:
|
||||||
|
compiled_allow_origin_regex = re.compile(allow_origin_regex)
|
||||||
|
|
||||||
|
allow_all_origins = "*" in allow_origins
|
||||||
|
allow_all_headers = "*" in allow_headers
|
||||||
|
preflight_explicit_allow_origin = not allow_all_origins or allow_credentials
|
||||||
|
|
||||||
|
simple_headers: dict[str, str] = {}
|
||||||
|
if allow_all_origins:
|
||||||
|
simple_headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
if allow_credentials:
|
||||||
|
simple_headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
|
if expose_headers:
|
||||||
|
simple_headers["Access-Control-Expose-Headers"] = ", ".join(expose_headers)
|
||||||
|
|
||||||
|
preflight_headers: dict[str, str] = {}
|
||||||
|
if preflight_explicit_allow_origin:
|
||||||
|
# The origin value will be set in preflight_response() if it is allowed.
|
||||||
|
preflight_headers["Vary"] = "Origin"
|
||||||
|
else:
|
||||||
|
preflight_headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
preflight_headers.update(
|
||||||
|
{
|
||||||
|
"Access-Control-Allow-Methods": ", ".join(allow_methods),
|
||||||
|
"Access-Control-Max-Age": str(max_age),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
allow_headers = sorted(SAFELISTED_HEADERS | set(allow_headers))
|
||||||
|
if allow_headers and not allow_all_headers:
|
||||||
|
preflight_headers["Access-Control-Allow-Headers"] = ", ".join(allow_headers)
|
||||||
|
if allow_credentials:
|
||||||
|
preflight_headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
self.allow_origins = allow_origins
|
||||||
|
self.allow_methods = allow_methods
|
||||||
|
self.allow_headers = [h.lower() for h in allow_headers]
|
||||||
|
self.allow_all_origins = allow_all_origins
|
||||||
|
self.allow_all_headers = allow_all_headers
|
||||||
|
self.allow_credentials = allow_credentials
|
||||||
|
self.preflight_explicit_allow_origin = preflight_explicit_allow_origin
|
||||||
|
self.allow_origin_regex = compiled_allow_origin_regex
|
||||||
|
self.allow_private_network = allow_private_network
|
||||||
|
self.simple_headers = simple_headers
|
||||||
|
self.preflight_headers = preflight_headers
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||||
|
if scope["type"] != "http": # pragma: no cover
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
return
|
||||||
|
|
||||||
|
method = scope["method"]
|
||||||
|
headers = Headers(scope=scope)
|
||||||
|
origin = headers.get("origin")
|
||||||
|
|
||||||
|
if origin is None:
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
return
|
||||||
|
|
||||||
|
if method == "OPTIONS" and "access-control-request-method" in headers:
|
||||||
|
response = self.preflight_response(request_headers=headers)
|
||||||
|
await response(scope, receive, send)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.simple_response(scope, receive, send, request_headers=headers)
|
||||||
|
|
||||||
|
def is_allowed_origin(self, origin: str) -> bool:
|
||||||
|
if self.allow_all_origins:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.allow_origin_regex is not None and self.allow_origin_regex.fullmatch(origin):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return origin in self.allow_origins
|
||||||
|
|
||||||
|
def preflight_response(self, request_headers: Headers) -> Response:
|
||||||
|
requested_origin = request_headers["origin"]
|
||||||
|
requested_method = request_headers["access-control-request-method"]
|
||||||
|
requested_headers = request_headers.get("access-control-request-headers")
|
||||||
|
requested_private_network = request_headers.get("access-control-request-private-network")
|
||||||
|
|
||||||
|
headers = dict(self.preflight_headers)
|
||||||
|
failures: list[str] = []
|
||||||
|
|
||||||
|
if self.is_allowed_origin(origin=requested_origin):
|
||||||
|
if self.preflight_explicit_allow_origin:
|
||||||
|
# The "else" case is already accounted for in self.preflight_headers
|
||||||
|
# and the value would be "*".
|
||||||
|
headers["Access-Control-Allow-Origin"] = requested_origin
|
||||||
|
else:
|
||||||
|
failures.append("origin")
|
||||||
|
|
||||||
|
if requested_method not in self.allow_methods:
|
||||||
|
failures.append("method")
|
||||||
|
|
||||||
|
# If we allow all headers, then we have to mirror back any requested
|
||||||
|
# headers in the response.
|
||||||
|
if self.allow_all_headers and requested_headers is not None:
|
||||||
|
headers["Access-Control-Allow-Headers"] = requested_headers
|
||||||
|
elif requested_headers is not None:
|
||||||
|
for header in [h.lower() for h in requested_headers.split(",")]:
|
||||||
|
if header.strip() not in self.allow_headers:
|
||||||
|
failures.append("headers")
|
||||||
|
break
|
||||||
|
|
||||||
|
if requested_private_network is not None:
|
||||||
|
if self.allow_private_network:
|
||||||
|
headers["Access-Control-Allow-Private-Network"] = "true"
|
||||||
|
else:
|
||||||
|
failures.append("private-network")
|
||||||
|
|
||||||
|
# We don't strictly need to use 400 responses here, since its up to
|
||||||
|
# the browser to enforce the CORS policy, but its more informative
|
||||||
|
# if we do.
|
||||||
|
if failures:
|
||||||
|
failure_text = "Disallowed CORS " + ", ".join(failures)
|
||||||
|
return PlainTextResponse(failure_text, status_code=400, headers=headers)
|
||||||
|
|
||||||
|
return PlainTextResponse("OK", status_code=200, headers=headers)
|
||||||
|
|
||||||
|
async def simple_response(self, scope: Scope, receive: Receive, send: Send, request_headers: Headers) -> None:
|
||||||
|
send = functools.partial(self.send, send=send, request_headers=request_headers)
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
async def send(self, message: Message, send: Send, request_headers: Headers) -> None:
|
||||||
|
if message["type"] != "http.response.start":
|
||||||
|
await send(message)
|
||||||
|
return
|
||||||
|
|
||||||
|
message.setdefault("headers", [])
|
||||||
|
headers = MutableHeaders(scope=message)
|
||||||
|
headers.update(self.simple_headers)
|
||||||
|
origin = request_headers["Origin"]
|
||||||
|
|
||||||
|
# If credentials are allowed, then we must respond with the specific origin instead of '*'.
|
||||||
|
if self.allow_all_origins and self.allow_credentials:
|
||||||
|
self.allow_explicit_origin(headers, origin)
|
||||||
|
|
||||||
|
# If we only allow specific origins, then we have to mirror back the Origin header in the response.
|
||||||
|
elif not self.allow_all_origins and self.is_allowed_origin(origin=origin):
|
||||||
|
self.allow_explicit_origin(headers, origin)
|
||||||
|
|
||||||
|
await send(message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def allow_explicit_origin(headers: MutableHeaders, origin: str) -> None:
|
||||||
|
headers["Access-Control-Allow-Origin"] = origin
|
||||||
|
headers.add_vary_header("Origin")
|
||||||
259
venv/Lib/site-packages/starlette/middleware/errors.py
Normal file
259
venv/Lib/site-packages/starlette/middleware/errors.py
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import html
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from starlette._utils import is_async_callable
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import HTMLResponse, PlainTextResponse, Response
|
||||||
|
from starlette.types import ASGIApp, ExceptionHandler, Message, Receive, Scope, Send
|
||||||
|
|
||||||
|
STYLES = """
|
||||||
|
p {
|
||||||
|
color: #211c1c;
|
||||||
|
}
|
||||||
|
.traceback-container {
|
||||||
|
border: 1px solid #038BB8;
|
||||||
|
}
|
||||||
|
.traceback-title {
|
||||||
|
background-color: #038BB8;
|
||||||
|
color: lemonchiffon;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
.frame-line {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.frame-filename {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.center-line {
|
||||||
|
background-color: #038BB8;
|
||||||
|
color: #f9f6e1;
|
||||||
|
padding: 5px 0px 5px 5px;
|
||||||
|
}
|
||||||
|
.lineno {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.frame-title {
|
||||||
|
font-weight: unset;
|
||||||
|
padding: 10px 10px 10px 10px;
|
||||||
|
background-color: #E4F4FD;
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #191f21;
|
||||||
|
font-size: 17px;
|
||||||
|
border: 1px solid #c7dce8;
|
||||||
|
}
|
||||||
|
.collapse-btn {
|
||||||
|
float: right;
|
||||||
|
padding: 0px 5px 1px 5px;
|
||||||
|
border: solid 1px #96aebb;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.source-code {
|
||||||
|
font-family: courier;
|
||||||
|
font-size: small;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
JS = """
|
||||||
|
<script type="text/javascript">
|
||||||
|
function collapse(element){
|
||||||
|
const frameId = element.getAttribute("data-frame-id");
|
||||||
|
const frame = document.getElementById(frameId);
|
||||||
|
|
||||||
|
if (frame.classList.contains("collapsed")){
|
||||||
|
element.innerHTML = "‒";
|
||||||
|
frame.classList.remove("collapsed");
|
||||||
|
} else {
|
||||||
|
element.innerHTML = "+";
|
||||||
|
frame.classList.add("collapsed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
TEMPLATE = """
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style type='text/css'>
|
||||||
|
{styles}
|
||||||
|
</style>
|
||||||
|
<title>Starlette Debugger</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>500 Server Error</h1>
|
||||||
|
<h2>{error}</h2>
|
||||||
|
<div class="traceback-container">
|
||||||
|
<p class="traceback-title">Traceback</p>
|
||||||
|
<div>{exc_html}</div>
|
||||||
|
</div>
|
||||||
|
{js}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
FRAME_TEMPLATE = """
|
||||||
|
<div>
|
||||||
|
<p class="frame-title">File <span class="frame-filename">{frame_filename}</span>,
|
||||||
|
line <i>{frame_lineno}</i>,
|
||||||
|
in <b>{frame_name}</b>
|
||||||
|
<span class="collapse-btn" data-frame-id="{frame_filename}-{frame_lineno}" onclick="collapse(this)">{collapse_button}</span>
|
||||||
|
</p>
|
||||||
|
<div id="{frame_filename}-{frame_lineno}" class="source-code {collapsed}">{code_context}</div>
|
||||||
|
</div>
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
LINE = """
|
||||||
|
<p><span class="frame-line">
|
||||||
|
<span class="lineno">{lineno}.</span> {line}</span></p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
CENTER_LINE = """
|
||||||
|
<p class="center-line"><span class="frame-line center-line">
|
||||||
|
<span class="lineno">{lineno}.</span> {line}</span></p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ServerErrorMiddleware:
|
||||||
|
"""
|
||||||
|
Handles returning 500 responses when a server error occurs.
|
||||||
|
|
||||||
|
If 'debug' is set, then traceback responses will be returned,
|
||||||
|
otherwise the designated 'handler' will be called.
|
||||||
|
|
||||||
|
This middleware class should generally be used to wrap *everything*
|
||||||
|
else up, so that unhandled exceptions anywhere in the stack
|
||||||
|
always result in an appropriate 500 response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
app: ASGIApp,
|
||||||
|
handler: ExceptionHandler | None = None,
|
||||||
|
debug: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self.app = app
|
||||||
|
self.handler = handler
|
||||||
|
self.debug = debug
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||||
|
if scope["type"] != "http":
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
return
|
||||||
|
|
||||||
|
response_started = False
|
||||||
|
|
||||||
|
async def _send(message: Message) -> None:
|
||||||
|
nonlocal response_started, send
|
||||||
|
|
||||||
|
if message["type"] == "http.response.start":
|
||||||
|
response_started = True
|
||||||
|
await send(message)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.app(scope, receive, _send)
|
||||||
|
except Exception as exc:
|
||||||
|
request = Request(scope)
|
||||||
|
if self.debug:
|
||||||
|
# In debug mode, return traceback responses.
|
||||||
|
response = self.debug_response(request, exc)
|
||||||
|
elif self.handler is None:
|
||||||
|
# Use our default 500 error handler.
|
||||||
|
response = self.error_response(request, exc)
|
||||||
|
else:
|
||||||
|
# Use an installed 500 error handler.
|
||||||
|
if is_async_callable(self.handler):
|
||||||
|
response = await self.handler(request, exc) # type: ignore[assignment, arg-type]
|
||||||
|
else:
|
||||||
|
response = await run_in_threadpool(self.handler, request, exc) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
if not response_started:
|
||||||
|
await response(scope, receive, send)
|
||||||
|
|
||||||
|
# We always continue to raise the exception.
|
||||||
|
# This allows servers to log the error, or allows test clients
|
||||||
|
# to optionally raise the error within the test case.
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
def format_line(self, index: int, line: str, frame_lineno: int, frame_index: int) -> str:
|
||||||
|
values = {
|
||||||
|
# HTML escape - line could contain < or >
|
||||||
|
"line": html.escape(line).replace(" ", " "),
|
||||||
|
"lineno": (frame_lineno - frame_index) + index,
|
||||||
|
}
|
||||||
|
|
||||||
|
if index != frame_index:
|
||||||
|
return LINE.format(**values)
|
||||||
|
return CENTER_LINE.format(**values)
|
||||||
|
|
||||||
|
def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str:
|
||||||
|
code_context = "".join(
|
||||||
|
self.format_line(
|
||||||
|
index,
|
||||||
|
line,
|
||||||
|
frame.lineno,
|
||||||
|
frame.index, # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
for index, line in enumerate(frame.code_context or [])
|
||||||
|
)
|
||||||
|
|
||||||
|
values = {
|
||||||
|
# HTML escape - filename could contain < or >, especially if it's a virtual
|
||||||
|
# file e.g. <stdin> in the REPL
|
||||||
|
"frame_filename": html.escape(frame.filename),
|
||||||
|
"frame_lineno": frame.lineno,
|
||||||
|
# HTML escape - if you try very hard it's possible to name a function with <
|
||||||
|
# or >
|
||||||
|
"frame_name": html.escape(frame.function),
|
||||||
|
"code_context": code_context,
|
||||||
|
"collapsed": "collapsed" if is_collapsed else "",
|
||||||
|
"collapse_button": "+" if is_collapsed else "‒",
|
||||||
|
}
|
||||||
|
return FRAME_TEMPLATE.format(**values)
|
||||||
|
|
||||||
|
def generate_html(self, exc: Exception, limit: int = 7) -> str:
|
||||||
|
traceback_obj = traceback.TracebackException.from_exception(exc, capture_locals=True)
|
||||||
|
|
||||||
|
exc_html = ""
|
||||||
|
is_collapsed = False
|
||||||
|
exc_traceback = exc.__traceback__
|
||||||
|
if exc_traceback is not None:
|
||||||
|
frames = inspect.getinnerframes(exc_traceback, limit)
|
||||||
|
for frame in reversed(frames):
|
||||||
|
exc_html += self.generate_frame_html(frame, is_collapsed)
|
||||||
|
is_collapsed = True
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 13): # pragma: no cover
|
||||||
|
exc_type_str = traceback_obj.exc_type_str
|
||||||
|
else: # pragma: no cover
|
||||||
|
exc_type_str = traceback_obj.exc_type.__name__
|
||||||
|
|
||||||
|
# escape error class and text
|
||||||
|
error = f"{html.escape(exc_type_str)}: {html.escape(str(traceback_obj))}"
|
||||||
|
|
||||||
|
return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html)
|
||||||
|
|
||||||
|
def generate_plain_text(self, exc: Exception) -> str:
|
||||||
|
return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
||||||
|
|
||||||
|
def debug_response(self, request: Request, exc: Exception) -> Response:
|
||||||
|
accept = request.headers.get("accept", "")
|
||||||
|
|
||||||
|
if "text/html" in accept:
|
||||||
|
content = self.generate_html(exc)
|
||||||
|
return HTMLResponse(content, status_code=500)
|
||||||
|
content = self.generate_plain_text(exc)
|
||||||
|
return PlainTextResponse(content, status_code=500)
|
||||||
|
|
||||||
|
def error_response(self, request: Request, exc: Exception) -> Response:
|
||||||
|
return PlainTextResponse("Internal Server Error", status_code=500)
|
||||||
Reference in New Issue
Block a user