This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/authentik/root/asgi.py

136 lines
4.3 KiB
Python
Raw Normal View History

2020-09-02 22:04:12 +00:00
"""
2020-12-05 21:08:42 +00:00
ASGI config for authentik project.
2020-09-02 22:04:12 +00:00
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
"""
import typing
from time import time
import django
from asgiref.compatibility import guarantee_single_callable
from channels.routing import ProtocolTypeRouter, URLRouter
2020-09-02 22:04:12 +00:00
from defusedxml import defuse_stdlib
from django.core.asgi import get_asgi_application
2020-09-02 22:04:12 +00:00
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from structlog.stdlib import get_logger
2020-09-02 22:04:12 +00:00
from authentik.core.middleware import RESPONSE_HEADER_ID
# DJANGO_SETTINGS_MODULE is set in gunicorn.conf.py
2020-09-02 22:04:12 +00:00
defuse_stdlib()
django.setup()
2020-11-11 21:35:40 +00:00
# pylint: disable=wrong-import-position
2020-12-05 21:08:42 +00:00
from authentik.root import websocket # noqa # isort:skip
2020-11-11 21:35:40 +00:00
2020-09-02 22:04:12 +00:00
# See https://github.com/encode/starlette/blob/master/starlette/types.py
Scope = typing.MutableMapping[str, typing.Any]
Message = typing.MutableMapping[str, typing.Any]
Receive = typing.Callable[[], typing.Awaitable[Message]]
Send = typing.Callable[[Message], typing.Awaitable[None]]
ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
ASGI_IP_HEADERS = (
b"x-forwarded-for",
b"x-real-ip",
)
2020-12-05 21:08:42 +00:00
LOGGER = get_logger("authentik.asgi")
2020-09-02 22:04:12 +00:00
class ASGILogger:
"""ASGI Logger, instantiated for each request"""
app: ASGIApp
status_code: int
start: float
def __init__(self, app: ASGIApp):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
content_length = 0
request_id = ""
2020-09-02 22:04:12 +00:00
async def send_hooked(message: Message) -> None:
"""Hooked send method, which records status code and content-length, and for the final
requests logs it"""
headers = dict(message.get("headers", []))
if "status" in message:
self.status_code = message["status"]
if b"Content-Length" in headers:
nonlocal content_length
content_length += int(headers.get(b"Content-Length", b"0"))
if message["type"] == "http.response.start":
response_headers = dict(message["headers"])
nonlocal request_id
request_id = response_headers.get(
RESPONSE_HEADER_ID.encode(), b""
).decode()
2020-11-24 11:36:54 +00:00
if message["type"] == "http.response.body" and not message.get(
"more_body", True
2020-11-24 11:36:54 +00:00
):
2021-02-09 22:33:25 +00:00
runtime = int((time() - self.start) * 1000)
self.log(scope, runtime, content_length, request_id=request_id)
await send(message)
2020-09-02 22:04:12 +00:00
self.start = time()
if scope["type"] == "lifespan":
# https://code.djangoproject.com/ticket/31508
# https://github.com/encode/uvicorn/issues/266
return
await self.app(scope, receive, send_hooked)
2020-09-02 22:04:12 +00:00
def _get_ip(self, scope: Scope) -> str:
client_ip = None
headers = dict(scope.get("headers", []))
for header in ASGI_IP_HEADERS:
if header in headers:
client_ip = headers[header].decode()
if not client_ip:
client_ip, _ = scope.get("client", ("", 0))
# Check if header has multiple values, and use the first one
return client_ip.split(", ")[0]
2020-09-02 22:04:12 +00:00
def log(self, scope: Scope, content_length: int, runtime: float, **kwargs):
2020-09-02 22:04:12 +00:00
"""Outpot access logs in a structured format"""
host = self._get_ip(scope)
2020-09-02 22:04:12 +00:00
query_string = ""
if scope.get("query_string", b"") != b"":
query_string = f"?{scope.get('query_string').decode()}"
2020-09-02 22:04:12 +00:00
LOGGER.info(
f"{scope.get('path', '')}{query_string}",
2020-09-02 22:04:12 +00:00
host=host,
method=scope.get("method", ""),
scheme=scope.get("scheme", ""),
2020-09-02 22:04:12 +00:00
status=self.status_code,
size=content_length / 1000 if content_length > 0 else 0,
2020-09-02 22:04:12 +00:00
runtime=runtime,
**kwargs,
2020-09-02 22:04:12 +00:00
)
application = ASGILogger(
guarantee_single_callable(
SentryAsgiMiddleware(
ProtocolTypeRouter(
{
"http": get_asgi_application(),
"websocket": URLRouter(websocket.websocket_urlpatterns),
}
)
)
)
2020-09-02 22:04:12 +00:00
)