Skip to content

Commit 16b6719

Browse files
miss-islingtonorsenthilclaude
authored
[3.15] gh-60299: Fix ProxyDigestAuthHandler credential lookup to use proxy authority (GH-160528) (#160914)
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 145abab commit 16b6719

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
@@ -1751,6 +1751,66 @@ def test_proxy_basic_auth(self):
17511751
"proxy.example.com:3128",
17521752
)
17531753

1754+
def test_proxy_digest_auth(self):
1755+
# gh-60299: ProxyDigestAuthHandler must look up credentials using
1756+
# the proxy's authority
1757+
opener = OpenerDirector()
1758+
ph = urllib.request.ProxyHandler(dict(http="proxy.example.com:3128"))
1759+
opener.add_handler(ph)
1760+
password_manager = MockPasswordManager()
1761+
digest_handler = urllib.request.ProxyDigestAuthHandler(password_manager)
1762+
realm = "ACME Networks"
1763+
http_handler = MockHTTPHandlerRedirect(
1764+
407,
1765+
'Proxy-Authenticate: Digest realm="%s", nonce="abcdef0123456789", '
1766+
'qop="auth"\r\n\r\n' % realm)
1767+
opener.add_handler(digest_handler)
1768+
opener.add_handler(http_handler)
1769+
1770+
user, password = "wile", "coyote"
1771+
request_url = "http://acme.example.com/protected"
1772+
proxy_authority = "proxy.example.com:3128"
1773+
1774+
digest_handler.add_password(realm, proxy_authority, user, password)
1775+
self.assertEqual(realm, password_manager.realm)
1776+
self.assertEqual(proxy_authority, password_manager.url)
1777+
1778+
opener.open(request_url)
1779+
1780+
self.assertEqual(password_manager.target_realm, realm)
1781+
self.assertEqual(password_manager.target_url, proxy_authority)
1782+
1783+
self.assertEqual(len(http_handler.requests), 2)
1784+
self.assertFalse(
1785+
http_handler.requests[0].has_header("Proxy-authorization"))
1786+
self.assertTrue(
1787+
http_handler.requests[1].has_header("Proxy-authorization"))
1788+
self.assertTrue(
1789+
http_handler.requests[1]
1790+
.get_header("Proxy-authorization").startswith("Digest "))
1791+
1792+
def test_digest_auth_uses_full_url(self):
1793+
# The non-proxy case must keep using the full request URL (not
1794+
# just the host) for the password lookup, so that sub-URI realm
1795+
# matching (HTTPPasswordMgr.is_suburi) keeps working
1796+
opener = OpenerDirector()
1797+
password_manager = MockPasswordManager()
1798+
digest_handler = urllib.request.HTTPDigestAuthHandler(password_manager)
1799+
realm = "ACME Networks"
1800+
request_url = "http://acme.example.com/protected"
1801+
http_handler = MockHTTPHandlerRedirect(
1802+
401,
1803+
'WWW-Authenticate: Digest realm="%s", nonce="abcdef0123456789", '
1804+
'qop="auth"\r\n\r\n' % realm)
1805+
opener.add_handler(digest_handler)
1806+
opener.add_handler(http_handler)
1807+
1808+
digest_handler.add_password(realm, request_url, "wile", "coyote")
1809+
opener.open(request_url)
1810+
1811+
self.assertEqual(password_manager.target_realm, realm)
1812+
self.assertEqual(password_manager.target_url, request_url)
1813+
17541814
def test_basic_and_digest_auth_handlers(self):
17551815
# HTTPDigestAuthHandler raised an exception if it couldn't handle a 40*
17561816
# 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
@@ -1144,7 +1144,13 @@ def get_authorization(self, req, chal):
11441144
if H is None:
11451145
return None
11461146

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

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)