Skip to content

Commit a102977

Browse files
committed
Added a digest authentication helper (2/2)
This completes the implementation of the digest helper, implementing for additional hashing algorithms sha256 and sha512, as well as auth-int qop. It implements tests and documents the features. The DigestAuth feature was shimmed into the existing API so it could be used in place of BasicAuth in the auth field of ClientSession and ClientRequest.
1 parent 3cd9feb commit a102977

11 files changed

+642
-418
lines changed

aiohttp/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from .connector import AddrInfoType, SocketFactoryType
5151
from .cookiejar import CookieJar, DummyCookieJar
5252
from .formdata import FormData
53-
from .helpers import BasicAuth, ChainMapProxy, ETag
53+
from .helpers import BasicAuth, ChainMapProxy, DigestAuth, ETag
5454
from .http import (
5555
HttpVersion,
5656
HttpVersion10,
@@ -164,6 +164,7 @@
164164
# helpers
165165
"BasicAuth",
166166
"ChainMapProxy",
167+
"DigestAuth",
167168
"ETag",
168169
# http
169170
"HttpVersion",

aiohttp/client.py

+20-14
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
from .helpers import (
9696
_SENTINEL,
9797
EMPTY_BODY_METHODS,
98-
BasicAuth,
98+
AuthBase,
9999
TimeoutHandle,
100100
frozen_dataclass_decorator,
101101
get_env_proxy_for_url,
@@ -174,7 +174,7 @@ class _RequestOptions(TypedDict, total=False):
174174
cookies: Union[LooseCookies, None]
175175
headers: Union[LooseHeaders, None]
176176
skip_auto_headers: Union[Iterable[str], None]
177-
auth: Union[BasicAuth, None]
177+
auth: Union[AuthBase, None]
178178
allow_redirects: bool
179179
max_redirects: int
180180
compress: Union[str, bool]
@@ -183,7 +183,7 @@ class _RequestOptions(TypedDict, total=False):
183183
raise_for_status: Union[None, bool, Callable[[ClientResponse], Awaitable[None]]]
184184
read_until_eof: bool
185185
proxy: Union[StrOrURL, None]
186-
proxy_auth: Union[BasicAuth, None]
186+
proxy_auth: Union[AuthBase, None]
187187
timeout: "Union[ClientTimeout, _SENTINEL, None]"
188188
ssl: Union[SSLContext, bool, Fingerprint]
189189
server_hostname: Union[str, None]
@@ -270,9 +270,9 @@ def __init__(
270270
cookies: Optional[LooseCookies] = None,
271271
headers: Optional[LooseHeaders] = None,
272272
proxy: Optional[StrOrURL] = None,
273-
proxy_auth: Optional[BasicAuth] = None,
273+
proxy_auth: Optional[AuthBase] = None,
274274
skip_auto_headers: Optional[Iterable[str]] = None,
275-
auth: Optional[BasicAuth] = None,
275+
auth: Optional[AuthBase] = None,
276276
json_serialize: JSONEncoder = json.dumps,
277277
request_class: Type[ClientRequest] = ClientRequest,
278278
response_class: Type[ClientResponse] = ClientResponse,
@@ -429,7 +429,7 @@ async def _request(
429429
cookies: Optional[LooseCookies] = None,
430430
headers: Optional[LooseHeaders] = None,
431431
skip_auto_headers: Optional[Iterable[str]] = None,
432-
auth: Optional[BasicAuth] = None,
432+
auth: Optional[AuthBase] = None,
433433
allow_redirects: bool = True,
434434
max_redirects: int = 10,
435435
compress: Union[str, bool] = False,
@@ -440,7 +440,7 @@ async def _request(
440440
] = None,
441441
read_until_eof: bool = True,
442442
proxy: Optional[StrOrURL] = None,
443-
proxy_auth: Optional[BasicAuth] = None,
443+
proxy_auth: Optional[AuthBase] = None,
444444
timeout: Union[ClientTimeout, _SENTINEL, None] = sentinel,
445445
ssl: Union[SSLContext, bool, Fingerprint] = True,
446446
server_hostname: Optional[str] = None,
@@ -672,6 +672,13 @@ async def _request(
672672
resp = await req.send(conn)
673673
try:
674674
await resp.start(conn)
675+
# Try performing digest authentication. It returns
676+
# True if we need to resend the request, as
677+
# DigestAuth is a bit of a handshake. This is
678+
# a no-op for BasicAuth. If it
679+
if auth is not None and auth.authenticate(resp):
680+
resp.close()
681+
continue
675682
except BaseException:
676683
resp.close()
677684
raise
@@ -824,12 +831,12 @@ def ws_connect(
824831
autoclose: bool = True,
825832
autoping: bool = True,
826833
heartbeat: Optional[float] = None,
827-
auth: Optional[BasicAuth] = None,
834+
auth: Optional[AuthBase] = None,
828835
origin: Optional[str] = None,
829836
params: Query = None,
830837
headers: Optional[LooseHeaders] = None,
831838
proxy: Optional[StrOrURL] = None,
832-
proxy_auth: Optional[BasicAuth] = None,
839+
proxy_auth: Optional[AuthBase] = None,
833840
ssl: Union[SSLContext, bool, Fingerprint] = True,
834841
server_hostname: Optional[str] = None,
835842
proxy_headers: Optional[LooseHeaders] = None,
@@ -872,12 +879,12 @@ async def _ws_connect(
872879
autoclose: bool = True,
873880
autoping: bool = True,
874881
heartbeat: Optional[float] = None,
875-
auth: Optional[BasicAuth] = None,
882+
auth: Optional[AuthBase] = None,
876883
origin: Optional[str] = None,
877884
params: Query = None,
878885
headers: Optional[LooseHeaders] = None,
879886
proxy: Optional[StrOrURL] = None,
880-
proxy_auth: Optional[BasicAuth] = None,
887+
proxy_auth: Optional[AuthBase] = None,
881888
ssl: Union[SSLContext, bool, Fingerprint] = True,
882889
server_hostname: Optional[str] = None,
883890
proxy_headers: Optional[LooseHeaders] = None,
@@ -1247,7 +1254,7 @@ def skip_auto_headers(self) -> FrozenSet[istr]:
12471254
return self._skip_auto_headers
12481255

12491256
@property
1250-
def auth(self) -> Optional[BasicAuth]: # type: ignore[misc]
1257+
def auth(self) -> Optional[AuthBase]:
12511258
"""An object that represents HTTP Basic Authorization"""
12521259
return self._default_auth
12531260

@@ -1412,8 +1419,7 @@ def request(
14121419
headers - (optional) Dictionary of HTTP Headers to send with
14131420
the request
14141421
cookies - (optional) Dict object to send with the request
1415-
auth - (optional) BasicAuth named tuple represent HTTP Basic Auth
1416-
auth - aiohttp.helpers.BasicAuth
1422+
auth - (optional) something implementing AuthBase for authentication
14171423
allow_redirects - (optional) If set to False, do not follow
14181424
redirects
14191425
version - Request HTTP version.

aiohttp/client_reqrep.py

+16-12
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@
4343
from .hdrs import CONTENT_TYPE
4444
from .helpers import (
4545
_SENTINEL,
46+
AuthBase,
4647
BaseTimerContext,
4748
BasicAuth,
49+
DigestAuth,
4850
HeadersMixin,
4951
TimerNoop,
5052
basicauth_from_netrc,
@@ -186,7 +188,7 @@ class ConnectionKey(NamedTuple):
186188
is_ssl: bool
187189
ssl: Union[SSLContext, bool, Fingerprint]
188190
proxy: Optional[URL]
189-
proxy_auth: Optional[BasicAuth]
191+
proxy_auth: Optional[AuthBase]
190192
proxy_headers_hash: Optional[int] # hash(CIMultiDict)
191193

192194

@@ -230,15 +232,15 @@ def __init__(
230232
skip_auto_headers: Optional[Iterable[str]] = None,
231233
data: Any = None,
232234
cookies: Optional[LooseCookies] = None,
233-
auth: Optional[BasicAuth] = None,
235+
auth: Optional[AuthBase] = None,
234236
version: http.HttpVersion = http.HttpVersion11,
235237
compress: Union[str, bool] = False,
236238
chunked: Optional[bool] = None,
237239
expect100: bool = False,
238240
loop: asyncio.AbstractEventLoop,
239241
response_class: Optional[Type["ClientResponse"]] = None,
240242
proxy: Optional[URL] = None,
241-
proxy_auth: Optional[BasicAuth] = None,
243+
proxy_auth: Optional[AuthBase] = None,
242244
timer: Optional[BaseTimerContext] = None,
243245
session: Optional["ClientSession"] = None,
244246
ssl: Union[SSLContext, bool, Fingerprint] = True,
@@ -287,12 +289,12 @@ def __init__(
287289
self.update_auto_headers(skip_auto_headers)
288290
self.update_cookies(cookies)
289291
self.update_content_encoding(data, compress)
290-
self.update_auth(auth, trust_env)
291292
self.update_proxy(proxy, proxy_auth, proxy_headers)
292293

293294
self.update_body_from_data(data)
294295
if data is not None or self.method not in self.GET_METHODS:
295296
self.update_transfer_encoding()
297+
self.update_auth(auth, trust_env) # Must be after we set the body
296298
self.update_expect_continue(expect100)
297299
self._traces = [] if traces is None else traces
298300

@@ -322,7 +324,7 @@ def ssl(self) -> Union["SSLContext", bool, Fingerprint]:
322324
return self._ssl
323325

324326
@property
325-
def connection_key(self) -> ConnectionKey: # type: ignore[misc]
327+
def connection_key(self) -> ConnectionKey:
326328
if proxy_headers := self.proxy_headers:
327329
h: Optional[int] = hash(tuple(proxy_headers.items()))
328330
else:
@@ -370,7 +372,7 @@ def update_host(self, url: URL) -> None:
370372

371373
# basic auth info
372374
if url.raw_user or url.raw_password:
373-
self.auth = helpers.BasicAuth(url.user or "", url.password or "")
375+
self.auth = BasicAuth(url.user or "", url.password or "")
374376

375377
def update_version(self, version: Union[http.HttpVersion, str]) -> None:
376378
"""Convert request version to two elements tuple.
@@ -494,7 +496,7 @@ def update_transfer_encoding(self) -> None:
494496
if hdrs.CONTENT_LENGTH not in self.headers:
495497
self.headers[hdrs.CONTENT_LENGTH] = str(len(self.body))
496498

497-
def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> None:
499+
def update_auth(self, auth: Optional[AuthBase], trust_env: bool = False) -> None:
498500
"""Set basic auth."""
499501
if auth is None:
500502
auth = self.auth
@@ -505,10 +507,12 @@ def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> Non
505507
if auth is None:
506508
return
507509

508-
if not isinstance(auth, helpers.BasicAuth):
509-
raise TypeError("BasicAuth() tuple is required instead")
510+
if not isinstance(auth, BasicAuth) and not isinstance(auth, DigestAuth):
511+
raise TypeError("BasicAuth() or DigestAuth() is required instead")
510512

511-
self.headers[hdrs.AUTHORIZATION] = auth.encode()
513+
authorization_hdr = auth.encode(self.method, self.url, self.body)
514+
if authorization_hdr:
515+
self.headers[hdrs.AUTHORIZATION] = authorization_hdr
512516

513517
def update_body_from_data(self, body: Any) -> None:
514518
if body is None:
@@ -561,7 +565,7 @@ def update_expect_continue(self, expect: bool = False) -> None:
561565
def update_proxy(
562566
self,
563567
proxy: Optional[URL],
564-
proxy_auth: Optional[BasicAuth],
568+
proxy_auth: Optional[AuthBase],
565569
proxy_headers: Optional[LooseHeaders],
566570
) -> None:
567571
self.proxy = proxy
@@ -570,7 +574,7 @@ def update_proxy(
570574
self.proxy_headers = None
571575
return
572576

573-
if proxy_auth and not isinstance(proxy_auth, helpers.BasicAuth):
577+
if proxy_auth and not isinstance(proxy_auth, BasicAuth):
574578
raise ValueError("proxy_auth must be None or BasicAuth() tuple")
575579
self.proxy_auth = proxy_auth
576580

0 commit comments

Comments
 (0)