Skip to content

Commit 1bca1b4

Browse files
encukoucodenamenam
andauthored
[3.13] gh-140691: urllib.request: Close FTP control socket if data socket can't connect (GH-140835) (GH-141657)
(cherry picked from commit f2bce51) Co-authored-by: codenamenam <bluetire27@gmail.com>
1 parent 8f71888 commit 1bca1b4

File tree

3 files changed

+67
-19
lines changed

3 files changed

+67
-19
lines changed

Lib/test/test_urllib2net.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import contextlib
12
import errno
3+
import sysconfig
24
import unittest
5+
from unittest import mock
36
from test import support
47
from test.support import os_helper
58
from test.support import socket_helper
69
from test.support import ResourceDenied
710
from test.test_urllib2 import sanepathname2url
11+
from test.support.warnings_helper import check_no_resource_warning
812

913
import os
1014
import socket
@@ -144,6 +148,43 @@ def test_ftp(self):
144148
]
145149
self._test_urls(urls, self._extra_handlers())
146150

151+
@support.requires_resource('walltime')
152+
@unittest.skipIf(sysconfig.get_platform() == 'linux-ppc64le',
153+
'leaks on PPC64LE (gh-140691)')
154+
def test_ftp_no_leak(self):
155+
# gh-140691: When the data connection (but not control connection)
156+
# cannot be made established, we shouldn't leave an open socket object.
157+
158+
class MockError(OSError):
159+
pass
160+
161+
orig_create_connection = socket.create_connection
162+
def patched_create_connection(address, *args, **kwargs):
163+
"""Simulate REJECTing connections to ports other than 21"""
164+
host, port = address
165+
if port != 21:
166+
raise MockError()
167+
return orig_create_connection(address, *args, **kwargs)
168+
169+
url = 'ftp://www.pythontest.net/README'
170+
entry = url, None, urllib.error.URLError
171+
no_cache_handlers = [urllib.request.FTPHandler()]
172+
cache_handlers = self._extra_handlers()
173+
with mock.patch('socket.create_connection', patched_create_connection):
174+
with check_no_resource_warning(self):
175+
# Try without CacheFTPHandler
176+
self._test_urls([entry], handlers=no_cache_handlers,
177+
retry=False)
178+
with check_no_resource_warning(self):
179+
# Try with CacheFTPHandler (uncached)
180+
self._test_urls([entry], cache_handlers, retry=False)
181+
with check_no_resource_warning(self):
182+
# Try with CacheFTPHandler (cached)
183+
self._test_urls([entry], cache_handlers, retry=False)
184+
# Try without the mock: the handler should not use a closed connection
185+
with check_no_resource_warning(self):
186+
self._test_urls([url], cache_handlers, retry=False)
187+
147188
def test_file(self):
148189
TESTFN = os_helper.TESTFN
149190
f = open(TESTFN, 'w')
@@ -256,18 +297,16 @@ def _test_urls(self, urls, handlers, retry=True):
256297
else:
257298
req = expected_err = None
258299

300+
if expected_err:
301+
context = self.assertRaises(expected_err)
302+
else:
303+
context = contextlib.nullcontext()
304+
259305
with socket_helper.transient_internet(url):
260-
try:
306+
f = None
307+
with context:
261308
f = urlopen(url, req, support.INTERNET_TIMEOUT)
262-
# urllib.error.URLError is a subclass of OSError
263-
except OSError as err:
264-
if expected_err:
265-
msg = ("Didn't get expected error(s) %s for %s %s, got %s: %s" %
266-
(expected_err, url, req, type(err), err))
267-
self.assertIsInstance(err, expected_err, msg)
268-
else:
269-
raise
270-
else:
309+
if f is not None:
271310
try:
272311
with time_out, \
273312
socket_peer_reset, \

Lib/urllib/request.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,6 +1537,7 @@ def ftp_open(self, req):
15371537
dirs, file = dirs[:-1], dirs[-1]
15381538
if dirs and not dirs[0]:
15391539
dirs = dirs[1:]
1540+
fw = None
15401541
try:
15411542
fw = self.connect_ftp(user, passwd, host, port, dirs, req.timeout)
15421543
type = file and 'I' or 'D'
@@ -1554,8 +1555,12 @@ def ftp_open(self, req):
15541555
headers += "Content-length: %d\n" % retrlen
15551556
headers = email.message_from_string(headers)
15561557
return addinfourl(fp, headers, req.full_url)
1557-
except ftplib.all_errors as exp:
1558-
raise URLError(exp) from exp
1558+
except Exception as exp:
1559+
if fw is not None and not fw.keepalive:
1560+
fw.close()
1561+
if isinstance(exp, ftplib.all_errors):
1562+
raise URLError(exp) from exp
1563+
raise
15591564

15601565
def connect_ftp(self, user, passwd, host, port, dirs, timeout):
15611566
return ftpwrapper(user, passwd, host, port, dirs, timeout,
@@ -1579,14 +1584,15 @@ def setMaxConns(self, m):
15791584

15801585
def connect_ftp(self, user, passwd, host, port, dirs, timeout):
15811586
key = user, host, port, '/'.join(dirs), timeout
1582-
if key in self.cache:
1583-
self.timeout[key] = time.time() + self.delay
1584-
else:
1585-
self.cache[key] = ftpwrapper(user, passwd, host, port,
1586-
dirs, timeout)
1587-
self.timeout[key] = time.time() + self.delay
1587+
conn = self.cache.get(key)
1588+
if conn is None or not conn.keepalive:
1589+
if conn is not None:
1590+
conn.close()
1591+
conn = self.cache[key] = ftpwrapper(user, passwd, host, port,
1592+
dirs, timeout)
1593+
self.timeout[key] = time.time() + self.delay
15881594
self.check_cache()
1589-
return self.cache[key]
1595+
return conn
15901596

15911597
def check_cache(self):
15921598
# first check for old ones
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In :mod:`urllib.request`, when opening a FTP URL fails because a data
2+
connection cannot be made, the control connection's socket is now closed to
3+
avoid a :exc:`ResourceWarning`.

0 commit comments

Comments
 (0)