1from __future__ import annotations
2
3import base64
4import binascii
5import collections.abc as cabc
6import typing as t
7
8from ..http import dump_header
9from ..http import parse_dict_header
10from ..http import quote_header_value
11from .structures import CallbackDict
12
13if t.TYPE_CHECKING:
14 import typing_extensions as te
15
16
17class Authorization:
18 """Represents the parts of an ``Authorization`` request header.
19
20 :attr:`.Request.authorization` returns an instance if the header is set.
21
22 An instance can be used with the test :class:`.Client` request methods' ``auth``
23 parameter to send the header in test requests.
24
25 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` will be
26 set. The ``Basic`` scheme's token is decoded into the ``username`` and ``password``
27 parameters.
28
29 For convenience, ``auth["key"]`` and ``auth.key`` both access the key in the
30 :attr:`parameters` dict, along with ``auth.get("key")`` and ``"key" in auth``.
31
32 .. versionchanged:: 2.3
33 The ``token`` parameter and attribute was added to support auth schemes that use
34 a token instead of parameters, such as ``Bearer``.
35
36 .. versionchanged:: 2.3
37 The object is no longer a ``dict``.
38
39 .. versionchanged:: 0.5
40 The object is an immutable dict.
41 """
42
43 def __init__(
44 self,
45 auth_type: str,
46 data: dict[str, str | None] | None = None,
47 token: str | None = None,
48 ) -> None:
49 self.type = auth_type
50 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""
51
52 if data is None:
53 data = {}
54
55 self.parameters = data
56 """A dict of parameters parsed from the header. Either this or :attr:`token`
57 will have a value for a given scheme.
58 """
59
60 self.token = token
61 """A token parsed from the header. Either this or :attr:`parameters` will have a
62 value for a given scheme.
63
64 .. versionadded:: 2.3
65 """
66
67 def __getattr__(self, name: str) -> str | None:
68 return self.parameters.get(name)
69
70 def __getitem__(self, name: str) -> str | None:
71 return self.parameters.get(name)
72
73 def get(self, key: str, default: str | None = None) -> str | None:
74 return self.parameters.get(key, default)
75
76 def __contains__(self, key: str) -> bool:
77 return key in self.parameters
78
79 def __eq__(self, other: object) -> bool:
80 if not isinstance(other, Authorization):
81 return NotImplemented
82
83 return (
84 other.type == self.type
85 and other.token == self.token
86 and other.parameters == self.parameters
87 )
88
89 @classmethod
90 def from_header(cls, value: str | None) -> te.Self | None:
91 """Parse an ``Authorization`` header value and return an instance, or ``None``
92 if the value is empty.
93
94 :param value: The header value to parse.
95
96 .. versionadded:: 2.3
97 """
98 if not value:
99 return None
100
101 scheme, _, rest = value.partition(" ")
102 scheme = scheme.lower()
103 rest = rest.strip(" \t")
104
105 if scheme == "basic":
106 try:
107 username, _, password = (
108 base64.b64decode(rest, validate=True).decode().partition(":")
109 )
110 except (binascii.Error, UnicodeError):
111 return None
112
113 return cls(scheme, {"username": username, "password": password})
114
115 if "=" in rest.rstrip("="):
116 # = that is not trailing, this is parameters.
117 return cls(scheme, parse_dict_header(rest), None)
118
119 # No = or only trailing =, this is a token.
120 return cls(scheme, None, rest)
121
122 def to_header(self) -> str:
123 """Produce an ``Authorization`` header value representing this data.
124
125 .. versionadded:: 2.0
126 """
127 if self.type == "basic":
128 value = base64.b64encode(
129 f"{self.username}:{self.password}".encode()
130 ).decode("ascii")
131 return f"Basic {value}"
132
133 if self.token is not None:
134 return f"{self.type.title()} {self.token}"
135
136 return f"{self.type.title()} {dump_header(self.parameters)}"
137
138 def __str__(self) -> str:
139 return self.to_header()
140
141 def __repr__(self) -> str:
142 return f"<{type(self).__name__} {self.to_header()}>"
143
144
145class WWWAuthenticate:
146 """Represents the parts of a ``WWW-Authenticate`` response header.
147
148 Set :attr:`.Response.www_authenticate` to an instance of list of instances to set
149 values for this header in the response. Modifying this instance will modify the
150 header value.
151
152 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` should be
153 set. The ``Basic`` scheme will encode ``username`` and ``password`` parameters to a
154 token.
155
156 For convenience, ``auth["key"]`` and ``auth.key`` both act on the :attr:`parameters`
157 dict, and can be used to get, set, or delete parameters. ``auth.get("key")`` and
158 ``"key" in auth`` are also provided.
159
160 .. versionchanged:: 2.3
161 The ``token`` parameter and attribute was added to support auth schemes that use
162 a token instead of parameters, such as ``Bearer``.
163
164 .. versionchanged:: 2.3
165 The object is no longer a ``dict``.
166
167 .. versionchanged:: 2.3
168 The ``on_update`` parameter was removed.
169 """
170
171 def __init__(
172 self,
173 auth_type: str,
174 values: dict[str, str | None] | None = None,
175 token: str | None = None,
176 ):
177 self._type = auth_type.lower()
178 self._parameters: dict[str, str | None] = CallbackDict(
179 values, lambda _: self._trigger_on_update()
180 )
181 self._token = token
182 self._on_update: cabc.Callable[[WWWAuthenticate], None] | None = None
183
184 def _trigger_on_update(self) -> None:
185 if self._on_update is not None:
186 self._on_update(self)
187
188 @property
189 def type(self) -> str:
190 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""
191 return self._type
192
193 @type.setter
194 def type(self, value: str) -> None:
195 self._type = value
196 self._trigger_on_update()
197
198 @property
199 def parameters(self) -> dict[str, str | None]:
200 """A dict of parameters for the header. Only one of this or :attr:`token` should
201 have a value for a given scheme.
202 """
203 return self._parameters
204
205 @parameters.setter
206 def parameters(self, value: dict[str, str]) -> None:
207 self._parameters = CallbackDict(value, lambda _: self._trigger_on_update())
208 self._trigger_on_update()
209
210 @property
211 def token(self) -> str | None:
212 """A dict of parameters for the header. Only one of this or :attr:`token` should
213 have a value for a given scheme.
214 """
215 return self._token
216
217 @token.setter
218 def token(self, value: str | None) -> None:
219 """A token for the header. Only one of this or :attr:`parameters` should have a
220 value for a given scheme.
221
222 .. versionadded:: 2.3
223 """
224 self._token = value
225 self._trigger_on_update()
226
227 def __getitem__(self, key: str) -> str | None:
228 return self.parameters.get(key)
229
230 def __setitem__(self, key: str, value: str | None) -> None:
231 if value is None:
232 if key in self.parameters:
233 del self.parameters[key]
234 else:
235 self.parameters[key] = value
236
237 self._trigger_on_update()
238
239 def __delitem__(self, key: str) -> None:
240 if key in self.parameters:
241 del self.parameters[key]
242 self._trigger_on_update()
243
244 def __getattr__(self, name: str) -> str | None:
245 return self[name]
246
247 def __setattr__(self, name: str, value: str | None) -> None:
248 if name in {"_type", "_parameters", "_token", "_on_update"}:
249 super().__setattr__(name, value)
250 else:
251 self[name] = value
252
253 def __delattr__(self, name: str) -> None:
254 del self[name]
255
256 def __contains__(self, key: str) -> bool:
257 return key in self.parameters
258
259 def __eq__(self, other: object) -> bool:
260 if not isinstance(other, WWWAuthenticate):
261 return NotImplemented
262
263 return (
264 other.type == self.type
265 and other.token == self.token
266 and other.parameters == self.parameters
267 )
268
269 def get(self, key: str, default: str | None = None) -> str | None:
270 return self.parameters.get(key, default)
271
272 @classmethod
273 def from_header(cls, value: str | None) -> te.Self | None:
274 """Parse a ``WWW-Authenticate`` header value and return an instance, or ``None``
275 if the value is empty.
276
277 :param value: The header value to parse.
278
279 .. versionadded:: 2.3
280 """
281 if not value:
282 return None
283
284 scheme, _, rest = value.partition(" ")
285 scheme = scheme.lower()
286 rest = rest.strip(" \t")
287
288 if "=" in rest.rstrip("="):
289 # = that is not trailing, this is parameters.
290 return cls(scheme, parse_dict_header(rest), None)
291
292 # No = or only trailing =, this is a token.
293 return cls(scheme, None, rest)
294
295 def to_header(self) -> str:
296 """Produce a ``WWW-Authenticate`` header value representing this data."""
297 if self.token is not None:
298 return f"{self.type.title()} {self.token}"
299
300 if not self.parameters:
301 return self.type.title()
302
303 if self.type == "digest":
304 items = []
305
306 for key, value in self.parameters.items():
307 if key in {"realm", "domain", "nonce", "opaque", "qop"}:
308 value = quote_header_value(value, allow_token=False)
309 else:
310 value = quote_header_value(value)
311
312 items.append(f"{key}={value}")
313
314 return f"Digest {', '.join(items)}"
315
316 return f"{self.type.title()} {dump_header(self.parameters)}"
317
318 def __str__(self) -> str:
319 return self.to_header()
320
321 def __repr__(self) -> str:
322 return f"<{type(self).__name__} {self.to_header()}>"