from loguru import logger
from starlette.types import ASGIApp, Receive, Scope, Send
class AuthMiddleware:
"""ASGI-compliant middleware to extract Jenkins auth from X-Jenkins-* headers."""
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
# Pass through non-HTTP requests directly per ASGI spec
if scope['type'] != 'http':
await self.app(scope, receive, send)
return
# According to ASGI spec, middleware should copy scope when modifying it
scope_copy: Scope = dict(scope)
# Ensure state exists in scope - this is where Starlette stores request state
if 'state' not in scope_copy:
scope_copy['state'] = {}
# Parse headers from scope (headers are byte tuples per ASGI spec)
headers = dict(scope_copy.get('headers', []))
jenkins_url_bytes = headers.get(b'x-jenkins-url')
jenkins_username_bytes = headers.get(b'x-jenkins-username')
jenkins_password_bytes = headers.get(b'x-jenkins-password')
# Convert bytes to strings (ASGI headers are always bytes)
jenkins_url = jenkins_url_bytes.decode('latin-1') if jenkins_url_bytes else None
jenkins_username = jenkins_username_bytes.decode('latin-1') if jenkins_username_bytes else None
jenkins_password = jenkins_password_bytes.decode('latin-1') if jenkins_password_bytes else None
# Store in scope state (modify in place so Starlette Request can access it)
scope_copy['state']['jenkins_url'] = jenkins_url
scope_copy['state']['jenkins_username'] = jenkins_username
scope_copy['state']['jenkins_password'] = jenkins_password
logger.debug(f'[JENKINS-AUTH-MIDDLEWARE] Captured headers - url: {jenkins_url}, username: {jenkins_username}')
# Call the next application with modified scope and safe send wrapper
await self.app(scope_copy, receive, send)