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()
104
105 if scheme == "basic":
106 try:
107 username, _, password = base64.b64decode(rest).decode().partition(":")
108 except (binascii.Error, UnicodeError):
109 return None
110
111 return cls(scheme, {"username": username, "password": password})
112
113 if "=" in rest.rstrip("="):
114 # = that is not trailing, this is parameters.
115 return cls(scheme, parse_dict_header(rest), None)
116
117 # No = or only trailing =, this is a token.
118 return cls(scheme, None, rest)
119
120 def to_header(self) -> str:
121 """Produce an ``Authorization`` header value representing this data.
122
123 .. versionadded:: 2.0
124 """
125 if self.type == "basic":
126 value = base64.b64encode(
127 f"{self.username}:{self.password}".encode()
128 ).decode("ascii")
129 return f"Basic {value}"
130
131 if self.token is not None:
132 return f"{self.type.title()} {self.token}"
133
134 return f"{self.type.title()} {dump_header(self.parameters)}"
135
136 def __str__(self) -> str:
137 return self.to_header()
138
139 def __repr__(self) -> str:
140 return f"<{type(self).__name__} {self.to_header()}>"
141
142
143class WWWAuthenticate:
144 """Represents the parts of a ``WWW-Authenticate`` response header.
145
146 Set :attr:`.Response.www_authenticate` to an instance of list of instances to set
147 values for this header in the response. Modifying this instance will modify the
148 header value.
149
150 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` should be
151 set. The ``Basic`` scheme will encode ``username`` and ``password`` parameters to a
152 token.
153
154 For convenience, ``auth["key"]`` and ``auth.key`` both act on the :attr:`parameters`
155 dict, and can be used to get, set, or delete parameters. ``auth.get("key")`` and
156 ``"key" in auth`` are also provided.
157
158 .. versionchanged:: 2.3
159 The ``token`` parameter and attribute was added to support auth schemes that use
160 a token instead of parameters, such as ``Bearer``.
161
162 .. versionchanged:: 2.3
163 The object is no longer a ``dict``.
164
165 .. versionchanged:: 2.3
166 The ``on_update`` parameter was removed.
167 """
168
169 def __init__(
170 self,
171 auth_type: str,
172 values: dict[str, str | None] | None = None,
173 token: str | None = None,
174 ):
175 self._type = auth_type.lower()
176 self._parameters: dict[str, str | None] = CallbackDict(
177 values, lambda _: self._trigger_on_update()
178 )
179 self._token = token
180 self._on_update: cabc.Callable[[WWWAuthenticate], None] | None = None
181
182 def _trigger_on_update(self) -> None:
183 if self._on_update is not None:
184 self._on_update(self)
185
186 @property
187 def type(self) -> str:
188 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""
189 return self._type
190
191 @type.setter
192 def type(self, value: str) -> None:
193 self._type = value
194 self._trigger_on_update()
195
196 @property
197 def parameters(self) -> dict[str, str | None]:
198 """A dict of parameters for the header. Only one of this or :attr:`token` should
199 have a value for a given scheme.
200 """
201 return self._parameters
202
203 @parameters.setter
204 def parameters(self, value: dict[str, str]) -> None:
205 self._parameters = CallbackDict(value, lambda _: self._trigger_on_update())
206 self._trigger_on_update()
207
208 @property
209 def token(self) -> str | None:
210 """A dict of parameters for the header. Only one of this or :attr:`token` should
211 have a value for a given scheme.
212 """
213 return self._token
214
215 @token.setter
216 def token(self, value: str | None) -> None:
217 """A token for the header. Only one of this or :attr:`parameters` should have a
218 value for a given scheme.
219
220 .. versionadded:: 2.3
221 """
222 self._token = value
223 self._trigger_on_update()
224
225 def __getitem__(self, key: str) -> str | None:
226 return self.parameters.get(key)
227
228 def __setitem__(self, key: str, value: str | None) -> None:
229 if value is None:
230 if key in self.parameters:
231 del self.parameters[key]
232 else:
233 self.parameters[key] = value
234
235 self._trigger_on_update()
236
237 def __delitem__(self, key: str) -> None:
238 if key in self.parameters:
239 del self.parameters[key]
240 self._trigger_on_update()
241
242 def __getattr__(self, name: str) -> str | None:
243 return self[name]
244
245 def __setattr__(self, name: str, value: str | None) -> None:
246 if name in {"_type", "_parameters", "_token", "_on_update"}:
247 super().__setattr__(name, value)
248 else:
249 self[name] = value
250
251 def __delattr__(self, name: str) -> None:
252 del self[name]
253
254 def __contains__(self, key: str) -> bool:
255 return key in self.parameters
256
257 def __eq__(self, other: object) -> bool:
258 if not isinstance(other, WWWAuthenticate):
259 return NotImplemented
260
261 return (
262 other.type == self.type
263 and other.token == self.token
264 and other.parameters == self.parameters
265 )
266
267 def get(self, key: str, default: str | None = None) -> str | None:
268 return self.parameters.get(key, default)
269
270 @classmethod
271 def from_header(cls, value: str | None) -> te.Self | None:
272 """Parse a ``WWW-Authenticate`` header value and return an instance, or ``None``
273 if the value is empty.
274
275 :param value: The header value to parse.
276
277 .. versionadded:: 2.3
278 """
279 if not value:
280 return None
281
282 scheme, _, rest = value.partition(" ")
283 scheme = scheme.lower()
284 rest = rest.strip()
285
286 if "=" in rest.rstrip("="):
287 # = that is not trailing, this is parameters.
288 return cls(scheme, parse_dict_header(rest), None)
289
290 # No = or only trailing =, this is a token.
291 return cls(scheme, None, rest)
292
293 def to_header(self) -> str:
294 """Produce a ``WWW-Authenticate`` header value representing this data."""
295 if self.token is not None:
296 return f"{self.type.title()} {self.token}"
297
298 if self.type == "digest":
299 items = []
300
301 for key, value in self.parameters.items():
302 if key in {"realm", "domain", "nonce", "opaque", "qop"}:
303 value = quote_header_value(value, allow_token=False)
304 else:
305 value = quote_header_value(value)
306
307 items.append(f"{key}={value}")
308
309 return f"Digest {', '.join(items)}"
310
311 return f"{self.type.title()} {dump_header(self.parameters)}"
312
313 def __str__(self) -> str:
314 return self.to_header()
315
316 def __repr__(self) -> str:
317 return f"<{type(self).__name__} {self.to_header()}>"