Kaydet (Commit) 02000161 authored tarafından Nadeem Vawda's avatar Nadeem Vawda

Merge: #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.
...@@ -343,33 +343,39 @@ class FTP: ...@@ -343,33 +343,39 @@ class FTP:
host, port = self.makepasv() host, port = self.makepasv()
conn = socket.create_connection((host, port), self.timeout, conn = socket.create_connection((host, port), self.timeout,
source_address=self.source_address) source_address=self.source_address)
if rest is not None: try:
self.sendcmd("REST %s" % rest) if rest is not None:
resp = self.sendcmd(cmd) self.sendcmd("REST %s" % rest)
# Some servers apparently send a 200 reply to resp = self.sendcmd(cmd)
# a LIST or STOR command, before the 150 reply # Some servers apparently send a 200 reply to
# (and way before the 226 reply). This seems to # a LIST or STOR command, before the 150 reply
# be in violation of the protocol (which only allows # (and way before the 226 reply). This seems to
# 1xx or error messages for LIST), so we just discard # be in violation of the protocol (which only allows
# this response. # 1xx or error messages for LIST), so we just discard
if resp[0] == '2': # this response.
resp = self.getresp() if resp[0] == '2':
if resp[0] != '1': resp = self.getresp()
raise error_reply(resp) if resp[0] != '1':
raise error_reply(resp)
except:
conn.close()
raise
else: else:
sock = self.makeport() sock = self.makeport()
if rest is not None: try:
self.sendcmd("REST %s" % rest) if rest is not None:
resp = self.sendcmd(cmd) self.sendcmd("REST %s" % rest)
# See above. resp = self.sendcmd(cmd)
if resp[0] == '2': # See above.
resp = self.getresp() if resp[0] == '2':
if resp[0] != '1': resp = self.getresp()
raise error_reply(resp) if resp[0] != '1':
conn, sockaddr = sock.accept() raise error_reply(resp)
if self.timeout is not _GLOBAL_DEFAULT_TIMEOUT: conn, sockaddr = sock.accept()
conn.settimeout(self.timeout) if self.timeout is not _GLOBAL_DEFAULT_TIMEOUT:
sock.close() conn.settimeout(self.timeout)
finally:
sock.close()
if resp[:3] == '150': if resp[:3] == '150':
# this is conditional in case we received a 125 # this is conditional in case we received a 125
size = parse150(resp) size = parse150(resp)
......
...@@ -623,6 +623,7 @@ class HandlerTests(unittest.TestCase): ...@@ -623,6 +623,7 @@ class HandlerTests(unittest.TestCase):
def retrfile(self, filename, filetype): def retrfile(self, filename, filetype):
self.filename, self.filetype = filename, filetype self.filename, self.filetype = filename, filetype
return io.StringIO(self.data), len(self.data) return io.StringIO(self.data), len(self.data)
def close(self): pass
class NullFTPHandler(urllib.request.FTPHandler): class NullFTPHandler(urllib.request.FTPHandler):
def __init__(self, data): self.data = data def __init__(self, data): self.data = data
......
...@@ -222,6 +222,7 @@ class OtherNetworkTests(unittest.TestCase): ...@@ -222,6 +222,7 @@ class OtherNetworkTests(unittest.TestCase):
handlers = [] handlers = []
cfh = urllib.request.CacheFTPHandler() cfh = urllib.request.CacheFTPHandler()
self.addCleanup(cfh.clear_cache)
cfh.setTimeout(1) cfh.setTimeout(1)
handlers.append(cfh) handlers.append(cfh)
......
...@@ -1371,8 +1371,8 @@ class FTPHandler(BaseHandler): ...@@ -1371,8 +1371,8 @@ class FTPHandler(BaseHandler):
raise exc.with_traceback(sys.exc_info()[2]) raise exc.with_traceback(sys.exc_info()[2])
def connect_ftp(self, user, passwd, host, port, dirs, timeout): def connect_ftp(self, user, passwd, host, port, dirs, timeout):
fw = ftpwrapper(user, passwd, host, port, dirs, timeout) return ftpwrapper(user, passwd, host, port, dirs, timeout,
return fw persistent=False)
class CacheFTPHandler(FTPHandler): class CacheFTPHandler(FTPHandler):
# XXX would be nice to have pluggable cache strategies # XXX would be nice to have pluggable cache strategies
...@@ -1421,6 +1421,13 @@ class CacheFTPHandler(FTPHandler): ...@@ -1421,6 +1421,13 @@ class CacheFTPHandler(FTPHandler):
break break
self.soonest = min(list(self.timeout.values())) self.soonest = min(list(self.timeout.values()))
def clear_cache(self):
for conn in self.cache.values():
conn.close()
self.cache.clear()
self.timeout.clear()
# Code move from the old urllib module # Code move from the old urllib module
MAXFTPCACHE = 10 # Trim the ftp cache beyond this size MAXFTPCACHE = 10 # Trim the ftp cache beyond this size
...@@ -2144,13 +2151,16 @@ def noheaders(): ...@@ -2144,13 +2151,16 @@ def noheaders():
class ftpwrapper: class ftpwrapper:
"""Class used by open_ftp() for cache of open FTP connections.""" """Class used by open_ftp() for cache of open FTP connections."""
def __init__(self, user, passwd, host, port, dirs, timeout=None): def __init__(self, user, passwd, host, port, dirs, timeout=None,
persistent=True):
self.user = user self.user = user
self.passwd = passwd self.passwd = passwd
self.host = host self.host = host
self.port = port self.port = port
self.dirs = dirs self.dirs = dirs
self.timeout = timeout self.timeout = timeout
self.refcount = 0
self.keepalive = persistent
self.init() self.init()
def init(self): def init(self):
...@@ -2201,7 +2211,8 @@ class ftpwrapper: ...@@ -2201,7 +2211,8 @@ class ftpwrapper:
conn, retrlen = self.ftp.ntransfercmd(cmd) conn, retrlen = self.ftp.ntransfercmd(cmd)
self.busy = 1 self.busy = 1
ftpobj = addclosehook(conn.makefile('rb'), self.endtransfer) ftpobj = addclosehook(conn.makefile('rb'), self.file_close)
self.refcount += 1
conn.close() conn.close()
# Pass back both a suitably decorated object and a retrieval length # Pass back both a suitably decorated object and a retrieval length
return (ftpobj, retrlen) return (ftpobj, retrlen)
...@@ -2216,6 +2227,17 @@ class ftpwrapper: ...@@ -2216,6 +2227,17 @@ class ftpwrapper:
pass pass
def close(self): def close(self):
self.keepalive = False
if self.refcount <= 0:
self.real_close()
def file_close(self):
self.endtransfer()
self.refcount -= 1
if self.refcount <= 0 and not self.keepalive:
self.real_close()
def real_close(self):
self.endtransfer() self.endtransfer()
try: try:
self.ftp.close() self.ftp.close()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment