Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 08f5f7a

Browse files
committed
Issue #10883: Fix socket leaks in urllib.request.
* ftpwrapper now uses reference counting to ensure that the underlying socket is closed when the ftpwrapper object is no longer in use * ftplib.FTP.ntransfercmd() now closes the socket if an error occurs Initial patch by Victor Stinner.
1 parent de02a71 commit 08f5f7a

4 files changed

Lines changed: 59 additions & 29 deletions

File tree

Lib/ftplib.py

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -336,33 +336,39 @@ def ntransfercmd(self, cmd, rest=None):
336336
if self.passiveserver:
337337
host, port = self.makepasv()
338338
conn = socket.create_connection((host, port), self.timeout)
339-
if rest is not None:
340-
self.sendcmd("REST %s" % rest)
341-
resp = self.sendcmd(cmd)
342-
# Some servers apparently send a 200 reply to
343-
# a LIST or STOR command, before the 150 reply
344-
# (and way before the 226 reply). This seems to
345-
# be in violation of the protocol (which only allows
346-
# 1xx or error messages for LIST), so we just discard
347-
# this response.
348-
if resp[0] == '2':
349-
resp = self.getresp()
350-
if resp[0] != '1':
351-
raise error_reply(resp)
339+
try:
340+
if rest is not None:
341+
self.sendcmd("REST %s" % rest)
342+
resp = self.sendcmd(cmd)
343+
# Some servers apparently send a 200 reply to
344+
# a LIST or STOR command, before the 150 reply
345+
# (and way before the 226 reply). This seems to
346+
# be in violation of the protocol (which only allows
347+
# 1xx or error messages for LIST), so we just discard
348+
# this response.
349+
if resp[0] == '2':
350+
resp = self.getresp()
351+
if resp[0] != '1':
352+
raise error_reply(resp)
353+
except:
354+
conn.close()
355+
raise
352356
else:
353357
sock = self.makeport()
354-
if rest is not None:
355-
self.sendcmd("REST %s" % rest)
356-
resp = self.sendcmd(cmd)
357-
# See above.
358-
if resp[0] == '2':
359-
resp = self.getresp()
360-
if resp[0] != '1':
361-
raise error_reply(resp)
362-
conn, sockaddr = sock.accept()
363-
if self.timeout is not _GLOBAL_DEFAULT_TIMEOUT:
364-
conn.settimeout(self.timeout)
365-
sock.close()
358+
try:
359+
if rest is not None:
360+
self.sendcmd("REST %s" % rest)
361+
resp = self.sendcmd(cmd)
362+
# See above.
363+
if resp[0] == '2':
364+
resp = self.getresp()
365+
if resp[0] != '1':
366+
raise error_reply(resp)
367+
conn, sockaddr = sock.accept()
368+
if self.timeout is not _GLOBAL_DEFAULT_TIMEOUT:
369+
conn.settimeout(self.timeout)
370+
finally:
371+
sock.close()
366372
if resp[:3] == '150':
367373
# this is conditional in case we received a 125
368374
size = parse150(resp)

Lib/test/test_urllib2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@ def __init__(self, data): self.data = data
622622
def retrfile(self, filename, filetype):
623623
self.filename, self.filetype = filename, filetype
624624
return io.StringIO(self.data), len(self.data)
625+
def close(self): pass
625626

626627
class NullFTPHandler(urllib.request.FTPHandler):
627628
def __init__(self, data): self.data = data

Lib/test/test_urllib2net.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def _extra_handlers(self):
222222
handlers = []
223223

224224
cfh = urllib.request.CacheFTPHandler()
225+
self.addCleanup(cfh.clear_cache)
225226
cfh.setTimeout(1)
226227
handlers.append(cfh)
227228

Lib/urllib/request.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,8 +1362,8 @@ def ftp_open(self, req):
13621362
raise exc.with_traceback(sys.exc_info()[2])
13631363

13641364
def connect_ftp(self, user, passwd, host, port, dirs, timeout):
1365-
fw = ftpwrapper(user, passwd, host, port, dirs, timeout)
1366-
return fw
1365+
return ftpwrapper(user, passwd, host, port, dirs, timeout,
1366+
persistent=False)
13671367

13681368
class CacheFTPHandler(FTPHandler):
13691369
# XXX would be nice to have pluggable cache strategies
@@ -1412,6 +1412,13 @@ def check_cache(self):
14121412
break
14131413
self.soonest = min(list(self.timeout.values()))
14141414

1415+
def clear_cache(self):
1416+
for conn in self.cache.values():
1417+
conn.close()
1418+
self.cache.clear()
1419+
self.timeout.clear()
1420+
1421+
14151422
# Code move from the old urllib module
14161423

14171424
MAXFTPCACHE = 10 # Trim the ftp cache beyond this size
@@ -2135,13 +2142,16 @@ def noheaders():
21352142
class ftpwrapper:
21362143
"""Class used by open_ftp() for cache of open FTP connections."""
21372144

2138-
def __init__(self, user, passwd, host, port, dirs, timeout=None):
2145+
def __init__(self, user, passwd, host, port, dirs, timeout=None,
2146+
persistent=True):
21392147
self.user = user
21402148
self.passwd = passwd
21412149
self.host = host
21422150
self.port = port
21432151
self.dirs = dirs
21442152
self.timeout = timeout
2153+
self.refcount = 0
2154+
self.keepalive = persistent
21452155
self.init()
21462156

21472157
def init(self):
@@ -2192,7 +2202,8 @@ def retrfile(self, file, type):
21922202
conn, retrlen = self.ftp.ntransfercmd(cmd)
21932203
self.busy = 1
21942204

2195-
ftpobj = addclosehook(conn.makefile('rb'), self.endtransfer)
2205+
ftpobj = addclosehook(conn.makefile('rb'), self.file_close)
2206+
self.refcount += 1
21962207
conn.close()
21972208
# Pass back both a suitably decorated object and a retrieval length
21982209
return (ftpobj, retrlen)
@@ -2207,6 +2218,17 @@ def endtransfer(self):
22072218
pass
22082219

22092220
def close(self):
2221+
self.keepalive = False
2222+
if self.refcount <= 0:
2223+
self.real_close()
2224+
2225+
def file_close(self):
2226+
self.endtransfer()
2227+
self.refcount -= 1
2228+
if self.refcount <= 0 and not self.keepalive:
2229+
self.real_close()
2230+
2231+
def real_close(self):
22102232
self.endtransfer()
22112233
try:
22122234
self.ftp.close()

0 commit comments

Comments
 (0)