chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Authentication handlers.
|
||||
Auth objects are callables that modify a request before it is sent.
|
||||
DigestAuth is the most interesting: it participates in a full request/response cycle,
|
||||
reading the 401 response to build the challenge before re-sending.
|
||||
"""
|
||||
import hashlib
|
||||
import time
|
||||
from models import Request, Response
|
||||
|
||||
|
||||
class Auth:
|
||||
"""Base class for all authentication handlers."""
|
||||
|
||||
def auth_flow(self, request: Request):
|
||||
"""Modify the request. May yield to inspect the response."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BasicAuth(Auth):
|
||||
"""HTTP Basic Authentication."""
|
||||
|
||||
def __init__(self, username: str, password: str):
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def auth_flow(self, request: Request):
|
||||
import base64
|
||||
credentials = f"{self.username}:{self.password}".encode()
|
||||
encoded = base64.b64encode(credentials).decode()
|
||||
request.headers["Authorization"] = f"Basic {encoded}"
|
||||
yield request
|
||||
|
||||
|
||||
class BearerAuth(Auth):
|
||||
"""Bearer token authentication."""
|
||||
|
||||
def __init__(self, token: str):
|
||||
self.token = token
|
||||
|
||||
def auth_flow(self, request: Request):
|
||||
request.headers["Authorization"] = f"Bearer {self.token}"
|
||||
yield request
|
||||
|
||||
|
||||
class DigestAuth(Auth):
|
||||
"""
|
||||
HTTP Digest Authentication.
|
||||
Requires a full request/response cycle: sends the initial request,
|
||||
reads the 401 WWW-Authenticate header, then re-sends with credentials.
|
||||
This is the only auth handler that reads from Response.
|
||||
"""
|
||||
|
||||
def __init__(self, username: str, password: str):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self._nonce_count = 0
|
||||
|
||||
def auth_flow(self, request: Request):
|
||||
yield request # first attempt, no credentials
|
||||
|
||||
# This handler must inspect the Response to continue
|
||||
response = yield
|
||||
|
||||
if response.status_code == 401:
|
||||
challenge = self._parse_challenge(response)
|
||||
credentials = self._build_credentials(request, challenge)
|
||||
request.headers["Authorization"] = credentials
|
||||
yield request
|
||||
|
||||
def _parse_challenge(self, response: Response) -> dict:
|
||||
"""Extract digest parameters from the WWW-Authenticate header."""
|
||||
header = response.headers.get("www-authenticate", "")
|
||||
params = {}
|
||||
for part in header.replace("Digest ", "").split(","):
|
||||
if "=" in part:
|
||||
key, _, value = part.strip().partition("=")
|
||||
params[key.strip()] = value.strip().strip('"')
|
||||
return params
|
||||
|
||||
def _build_credentials(self, request: Request, challenge: dict) -> str:
|
||||
"""Compute the Authorization header value for a digest challenge."""
|
||||
self._nonce_count += 1
|
||||
nc = f"{self._nonce_count:08x}"
|
||||
cnonce = hashlib.md5(str(time.time()).encode()).hexdigest()[:8]
|
||||
realm = challenge.get("realm", "")
|
||||
nonce = challenge.get("nonce", "")
|
||||
|
||||
ha1 = hashlib.md5(f"{self.username}:{realm}:{self.password}".encode()).hexdigest()
|
||||
ha2 = hashlib.md5(f"{request.method}:{request.url.path}".encode()).hexdigest()
|
||||
response_hash = hashlib.md5(f"{ha1}:{nonce}:{nc}:{cnonce}:auth:{ha2}".encode()).hexdigest()
|
||||
|
||||
return (
|
||||
f'Digest username="{self.username}", realm="{realm}", '
|
||||
f'nonce="{nonce}", uri="{request.url.path}", '
|
||||
f'nc={nc}, cnonce="{cnonce}", response="{response_hash}"'
|
||||
)
|
||||
|
||||
|
||||
class NetRCAuth(Auth):
|
||||
"""Load credentials from ~/.netrc based on the request host."""
|
||||
|
||||
def auth_flow(self, request: Request):
|
||||
import netrc
|
||||
try:
|
||||
credentials = netrc.netrc().authenticators(request.url.host)
|
||||
if credentials:
|
||||
username, _, password = credentials
|
||||
basic = BasicAuth(username, password)
|
||||
yield from basic.auth_flow(request)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
yield request
|
||||
Reference in New Issue
Block a user