Skip to content

Commit edf93de

Browse files
miss-islingtonorsenthilclaude
authored
[3.14] gh-60299: Fix ProxyDigestAuthHandler credential lookup to use proxy authority (GH-160528) (#160915)
gh-60299: Fix ProxyDigestAuthHandler credential lookup to use proxy authority (GH-160528) AbstractDigestAuthHandler.get_authorization() always looked up credentials using the target request URL (req.full_url), even when invoked via ProxyDigestAuthHandler, whose credentials are registered against the proxy's host:port. The lookup never matched, no Proxy-Authorization header was produced, and proxy digest auth failed with an unhandled 407. (cherry picked from commit 9b3e095) Co-authored-by: Senthil Kumaran <senthil@python.org> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent d90b952 commit edf93de

4 files changed

Lines changed: 76 additions & 4 deletions

File tree

‎Lib/test/test_urllib2.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,6 +1734,66 @@ def test_proxy_basic_auth(self):
17341734
"proxy.example.com:3128",
17351735
)
17361736

1737+
def test_proxy_digest_auth(self):
1738+
# gh-60299: ProxyDigestAuthHandler must look up credentials using
1739+
# the proxy's authority
1740+
opener = OpenerDirector()
1741+
ph = urllib.request.ProxyHandler(dict(http="proxy.example.com:3128"))
1742+
opener.add_handler(ph)
1743+
password_manager = MockPasswordManager()
1744+
digest_handler = urllib.request.ProxyDigestAuthHandler(password_manager)
1745+
realm = "ACME Networks"
1746+
http_handler = MockHTTPHandlerRedirect(
1747+
407,
1748+
'Proxy-Authenticate: Digest realm="%s", nonce="abcdef0123456789", '
1749+
'qop="auth"\r\n\r\n' % realm)
1750+
opener.add_handler(digest_handler)
1751+
opener.add_handler(http_handler)
1752+
1753+
user, password = "wile", "coyote"
1754+
request_url = "http://acme.example.com/protected"
1755+
proxy_authority = "proxy.example.com:3128"
1756+
1757+
digest_handler.add_password(realm, proxy_authority, user, password)
1758+
self.assertEqual(realm, password_manager.realm)
1759+
self.assertEqual(proxy_authority, password_manager.url)
1760+
1761+
opener.open(request_url)
1762+
1763+
self.assertEqual(password_manager.target_realm, realm)
1764+
self.assertEqual(password_manager.target_url, proxy_authority)
1765+
1766+
self.assertEqual(len(http_handler.requests), 2)
1767+
self.assertFalse(
1768+
http_handler.requests[0].has_header("Proxy-authorization"))
1769+
self.assertTrue(
1770+
http_handler.requests[1].has_header("Proxy-authorization"))
1771+
self.assertTrue(
1772+
http_handler.requests[1]
1773+
.get_header("Proxy-authorization").startswith("Digest "))
1774+
1775+
def test_digest_auth_uses_full_url(self):
1776+
# The non-proxy case must keep using the full request URL (not
1777+
# just the host) for the password lookup, so that sub-URI realm
1778+
# matching (HTTPPasswordMgr.is_suburi) keeps working
1779+
opener = OpenerDirector()
1780+
password_manager = MockPasswordManager()
1781+
digest_handler = urllib.request.HTTPDigestAuthHandler(password_manager)
1782+
realm = "ACME Networks"
1783+
request_url = "http://acme.example.com/protected"
1784+
http_handler = MockHTTPHandlerRedirect(
1785+
401,
1786+
'WWW-Authenticate: Digest realm="%s", nonce="abcdef0123456789", '
1787+
'qop="auth"\r\n\r\n' % realm)
1788+
opener.add_handler(digest_handler)
1789+
opener.add_handler(http_handler)
1790+
1791+
digest_handler.add_password(realm, request_url, "wile", "coyote")
1792+
opener.open(request_url)
1793+
1794+
self.assertEqual(password_manager.target_realm, realm)
1795+
self.assertEqual(password_manager.target_url, request_url)
1796+
17371797
def test_basic_and_digest_auth_handlers(self):
17381798
# HTTPDigestAuthHandler raised an exception if it couldn't handle a 40*
17391799
# response (https://bugs.python.org/issue1479302), where it should instead

‎Lib/test/test_urllib2_localnet.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ def create_fake_proxy_handler(*args, **kwargs):
351351
self.server.start()
352352
self.server.ready.wait()
353353
proxy_url = "http://127.0.0.1:%d" % self.server.port
354+
# Credentials must be registered against proxy authority
355+
self.proxy_authority = "127.0.0.1:%d" % self.server.port
354356
handler = urllib.request.ProxyHandler({"http" : proxy_url})
355357
self.proxy_digest_handler = urllib.request.ProxyDigestAuthHandler()
356358
self.opener = urllib.request.build_opener(
@@ -361,7 +363,7 @@ def stop_server(self):
361363
self.server = None
362364

363365
def test_proxy_with_bad_password_raises_httperror(self):
364-
self.proxy_digest_handler.add_password(self.REALM, self.URL,
366+
self.proxy_digest_handler.add_password(self.REALM, self.proxy_authority,
365367
self.USER, self.PASSWD+"bad")
366368
self.digest_auth_handler.set_qop("auth")
367369
with self.assertRaises(urllib.error.HTTPError) as cm:
@@ -375,15 +377,15 @@ def test_proxy_with_no_password_raises_httperror(self):
375377
cm.exception.close()
376378

377379
def test_proxy_qop_auth_works(self):
378-
self.proxy_digest_handler.add_password(self.REALM, self.URL,
380+
self.proxy_digest_handler.add_password(self.REALM, self.proxy_authority,
379381
self.USER, self.PASSWD)
380382
self.digest_auth_handler.set_qop("auth")
381383
with self.opener.open(self.URL) as result:
382384
while result.read():
383385
pass
384386

385387
def test_proxy_qop_auth_int_works_or_throws_urlerror(self):
386-
self.proxy_digest_handler.add_password(self.REALM, self.URL,
388+
self.proxy_digest_handler.add_password(self.REALM, self.proxy_authority,
387389
self.USER, self.PASSWD)
388390
self.digest_auth_handler.set_qop("auth-int")
389391
try:

‎Lib/urllib/request.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,13 @@ def get_authorization(self, req, chal):
11421142
if H is None:
11431143
return None
11441144

1145-
user, pw = self.passwd.find_user_password(realm, req.full_url)
1145+
# proxy auth is looked up by the proxy's authority
1146+
# req.host, set by Request.set_proxy()
1147+
if self.auth_header == 'Proxy-Authorization':
1148+
authority = req.host
1149+
else:
1150+
authority = req.full_url
1151+
user, pw = self.passwd.find_user_password(realm, authority)
11461152
if user is None:
11471153
return None
11481154

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :class:`urllib.request.ProxyDigestAuthHandler` failing to authenticate. It
2+
looked up credentials using the target request URL instead of the proxy's
3+
authority, so passwords registered via ``add_password()`` against the proxy
4+
never matched. Patch by Senthil Kumaran.

0 commit comments

Comments
 (0)