userver: /data/code/userver/testsuite/pytest_plugins/pytest_userver/utils/httpdigest.py Source File
Loading...
Searching...
No Matches
httpdigest.py
1# type: ignore
2"""Helpers to act as a minimal MD5 Digest client or server from Python."""
3
4import hashlib
5import re
6
7from requests.auth import HTTPDigestAuth
8
9# Parses both `key="value"` and `key=value` forms of digest directives.
10_DIRECTIVE_RE = re.compile(r'(\w+)=("[^"]*"|[^,\s]+)')
11
12
13def _md5(data: str) -> str:
14 return hashlib.md5(data.encode('utf-8')).hexdigest()
15
16
17def parse_directives(header: str) -> dict:
18 """Parses a WWW-Authenticate or an Authorization header value."""
19 prefix, _, params = header.partition(' ')
20 assert prefix == 'Digest', header
21 return {key: value.strip('"') for key, value in _DIRECTIVE_RE.findall(params)}
22
23
24def construct_challenge(*, auth_directives: dict, nonce: str | None = None) -> dict:
25 return {
26 'realm': auth_directives['realm'],
27 'nonce': nonce or auth_directives['nonce'],
28 'algorithm': auth_directives['algorithm'],
29 'qop': 'auth',
30 }
31
32
34 """Minimal stateful Digest client for functional tests."""
35
36 def __init__(self, *, username: str, password: str):
37 self._digest_auth = HTTPDigestAuth(username, password)
38 self._digest_auth.init_per_thread_state()
39
40 def construct_header(self, *, challenge: dict, uri: str, method: str) -> str:
41 """Builds an `Authorization: Digest ...` header value for the challenge."""
42 # pylint: disable=protected-access
43 self._digest_auth._thread_local.chal = challenge
44 return self._digest_auth.build_digest_header(method, uri)
45
46
47def construct_header(*, username: str, password: str, challenge: dict, uri: str, method: str) -> str:
48 """Builds a single `Authorization: Digest ...` header value."""
49 return DigestAuthClient(username=username, password=password).construct_header(
50 challenge=challenge,
51 uri=uri,
52 method=method,
53 )
54
55
57 *,
58 username: str,
59 password: str,
60 realm: str,
61 nonce: str,
62 nonce_count: str,
63 cnonce: str,
64 qop: str,
65 method: str,
66 uri: str,
67) -> str:
68 """Server side counterpart: the expected value of the `response` directive."""
69 ha1 = _md5(f'{username}:{realm}:{password}')
70 ha2 = _md5(f'{method}:{uri}')
71 return _md5(f'{ha1}:{nonce}:{nonce_count}:{cnonce}:{qop}:{ha2}')