diff --git a/pyftpdlib-1.5.7.tar.gz b/pyftpdlib-1.5.7.tar.gz deleted file mode 100644 index a42f826..0000000 --- a/pyftpdlib-1.5.7.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7ea3ce4137db8209af1f6b9ea020590f462c63ed7c7a1240bd596e4d3a7b656e -size 196076 diff --git a/pyftpdlib-1.5.9.tar.gz b/pyftpdlib-1.5.9.tar.gz new file mode 100644 index 0000000..b61ac45 --- /dev/null +++ b/pyftpdlib-1.5.9.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:323d4c42f1406aedb4df18faf680f64f32c080ff66f6c26090ba592f5bfc4a0f +size 204755 diff --git a/python-pyftpdlib.changes b/python-pyftpdlib.changes index e2a7be7..30fd106 100644 --- a/python-pyftpdlib.changes +++ b/python-pyftpdlib.changes @@ -1,3 +1,64 @@ +------------------------------------------------------------------- +Mon Mar 25 14:21:26 UTC 2024 - Dirk Müller + +- update to 1.5.9: + * Enhancements + * #611: use ruff code style checker instead of flake8 + isort + (much faster + makes many more code quality checks). + * Bug fixes + * #604: client connection may be reset in PASV/EPSV mode during + TLS handshake. (patch by Benedikt McMullin) + * #607: possible infinite wait in Epoll (patch by + @stat1c-void) + * #607: possible infinite traceback printing in DTPHandler + (patch by @stat1c-void) + * #613: (CRITICAL) bugfix for TLS disconnect causing 100% CPU + usage. (patch by @hakai) + * #614: close connection on SSL EOF error, instead of + erroneously replying with "226 Transfer completed." + * Enhancements + * #586: removed Python 2.6 support. + * #591: speedup logging by 28% by using logging._srcfile = None + trick. This avoids calling calling sys._getframe() for each + log record. + * #605: added support for Python 3.12. + * Enhancements + * #544: replace Travis with Github Actions for CI testing. + * Bug fixes + * #481: fix [WinError 10038] an operation was attempted on + something that is not a socket. (patch by Tailing Yuan) + * #578, [critical]: FTPS broke with PyOpenSSL version 22.1.0. + * Enhancements + * #467: added pre-fork concurrency model, spawn()ing worker + processes to split load. + * #520: directory LISTing is now 3.7x times faster. + * Enhancements + * #495: colored test output. + * Bug fixes + * #492: CRLF line endings are replaced with CRCRLF in ASCII + mode downloads. + * #496: import error due to multiprocessing.Lock() bug. + * Enhancements + * #463: FTPServer class can now be used as a context manager. + * Bug fixes + * #431: Ctrl-C doesn't exit python -m pyftpdlib on Windows. + * #436: ThreadedFTPServer.max_cons is evaluated + threading.activeCount(). If the user uses threads of its own + it will consume the number of max_cons. + * #447: ThreadedFTPServer and MultiprocessFTPServer do not + join() tasks which are no longer consuming resources. + * Enhancements + * #201: implemented SITE MFMT command which changes file + modification time. (patch by Tahir Ijaz) + * #327: add username and password command line options + * #433: documentation moved to readthedocs: + http://pyftpdlib.readthedocs.io + * Bug fixes + * #403: fix duplicated output log. (path by PonyPC) + * #414: Respond successfully to STOR only after closing file + handle. +- drop support-python-312.patch (upstream) + ------------------------------------------------------------------- Tue Sep 19 05:14:22 UTC 2023 - Steve Kowalik @@ -30,7 +91,7 @@ Fri Oct 28 20:09:29 UTC 2022 - Yogalakshmi Arunachalam - Update to Version: 1.5.6 - 2020-02-16 Enhancements * #467: added pre-fork concurrency model, spawn()ing worker processes to split load. - * #520: directory LISTing is now 3.7x times faster. + * #520: directory LISTing is now 3.7x times faster. ------------------------------------------------------------------- Wed Jan 19 08:00:32 UTC 2022 - Matej Cepl @@ -42,7 +103,7 @@ Wed Jan 19 08:00:32 UTC 2022 - Matej Cepl Tue Oct 26 04:12:28 UTC 2021 - Steve Kowalik - Regenerate new private key/x509 certificate for the test suite. - (thanks, Jason!) + (thanks, Jason!) ------------------------------------------------------------------- Fri May 7 23:13:10 UTC 2021 - Ben Greiner diff --git a/python-pyftpdlib.spec b/python-pyftpdlib.spec index 6e266e6..9a48880 100644 --- a/python-pyftpdlib.spec +++ b/python-pyftpdlib.spec @@ -1,7 +1,7 @@ # # spec file for package python-pyftpdlib # -# Copyright (c) 2023 SUSE LLC +# Copyright (c) 2024 SUSE LLC # Copyright (c) 2016 LISA GmbH, Bingen, Germany. # # All modifications and additions to the file contributed by third parties @@ -19,15 +19,13 @@ %{?sle15_python_module_pythons} Name: python-pyftpdlib -Version: 1.5.7 +Version: 1.5.9 Release: 0 Summary: Asynchronous FTP server library for Python License: MIT URL: https://github.com/giampaolo/pyftpdlib/ Source: https://files.pythonhosted.org/packages/source/p/pyftpdlib/pyftpdlib-%{version}.tar.gz Source1: keycert.pem -# PATCH-FIX-UPSTREAM gh#giampaolo/pyftpdlib#605 -Patch0: support-python-312.patch BuildRequires: %{python_module pip} BuildRequires: %{python_module psutil} BuildRequires: %{python_module pyOpenSSL} @@ -39,7 +37,7 @@ BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-pyOpenSSL Requires(post): update-alternatives -Requires(postun):update-alternatives +Requires(postun): update-alternatives Recommends: python-pysendfile BuildArch: noarch %python_subpackages diff --git a/support-python-312.patch b/support-python-312.patch deleted file mode 100644 index f3e220e..0000000 --- a/support-python-312.patch +++ /dev/null @@ -1,896 +0,0 @@ -From a0784dc320042f7f3d8aa0cf2bde5a06a1d771a3 Mon Sep 17 00:00:00 2001 -From: Giampaolo Rodola -Date: Thu, 3 Aug 2023 12:04:08 +0800 -Subject: [PATCH 1/4] copy asyncore.py and asynchat.py from python 3.11 - ---- - MANIFEST.in | 2 + - Makefile | 2 +- - pyftpdlib/_asynchat.py | 217 +++++++++++++++ - pyftpdlib/_asyncore.py | 587 +++++++++++++++++++++++++++++++++++++++++ - pyftpdlib/handlers.py | 7 +- - pyftpdlib/ioloop.py | 10 +- - 6 files changed, 821 insertions(+), 4 deletions(-) - create mode 100644 pyftpdlib/_asynchat.py - create mode 100644 pyftpdlib/_asyncore.py - -Index: pyftpdlib-1.5.7/MANIFEST.in -=================================================================== ---- pyftpdlib-1.5.7.orig/MANIFEST.in -+++ pyftpdlib-1.5.7/MANIFEST.in -@@ -35,6 +35,8 @@ include docs/tutorial.rst - include make.bat - include pyftpdlib/__init__.py - include pyftpdlib/__main__.py -+include pyftpdlib/_asynchat.py -+include pyftpdlib/_asyncore.py - include pyftpdlib/_compat.py - include pyftpdlib/authorizers.py - include pyftpdlib/filesystems.py -Index: pyftpdlib-1.5.7/pyftpdlib/_asynchat.py -=================================================================== ---- /dev/null -+++ pyftpdlib-1.5.7/pyftpdlib/_asynchat.py -@@ -0,0 +1,217 @@ -+# Id: asynchat.py,v 2.26 2000/09/07 22:29:26 rushing Exp -+# Author: Sam Rushing -+ -+# ====================================================================== -+# Copyright 1996 by Sam Rushing -+# -+# All Rights Reserved -+# -+# Permission to use, copy, modify, and distribute this software and -+# its documentation for any purpose and without fee is hereby -+# granted, provided that the above copyright notice appear in all -+# copies and that both that copyright notice and this permission -+# notice appear in supporting documentation, and that the name of Sam -+# Rushing not be used in advertising or publicity pertaining to -+# distribution of the software without specific, written prior -+# permission. -+# -+# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, -+# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN -+# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR -+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -+# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -+# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -+# ====================================================================== -+ -+from collections import deque -+ -+from . import _asyncore as asyncore -+ -+ -+class async_chat(asyncore.dispatcher): -+ ac_in_buffer_size = 65536 -+ ac_out_buffer_size = 65536 -+ use_encoding = 0 -+ encoding = 'latin-1' -+ -+ def __init__(self, sock=None, map=None): -+ self.ac_in_buffer = b'' -+ self.incoming = [] -+ self.producer_fifo = deque() -+ asyncore.dispatcher.__init__(self, sock, map) -+ -+ def collect_incoming_data(self, data): -+ raise NotImplementedError("must be implemented in subclass") -+ -+ def _collect_incoming_data(self, data): -+ self.incoming.append(data) -+ -+ def _get_data(self): -+ d = b''.join(self.incoming) -+ del self.incoming[:] -+ return d -+ -+ def found_terminator(self): -+ raise NotImplementedError("must be implemented in subclass") -+ -+ def set_terminator(self, term): -+ if isinstance(term, str) and self.use_encoding: -+ term = bytes(term, self.encoding) -+ elif isinstance(term, int) and term < 0: -+ raise ValueError('the number of received bytes must be positive') -+ self.terminator = term -+ -+ def get_terminator(self): -+ return self.terminator -+ -+ def handle_read(self): -+ -+ try: -+ data = self.recv(self.ac_in_buffer_size) -+ except BlockingIOError: -+ return -+ except OSError: -+ self.handle_error() -+ return -+ -+ if isinstance(data, str) and self.use_encoding: -+ data = bytes(str, self.encoding) -+ self.ac_in_buffer = self.ac_in_buffer + data -+ -+ while self.ac_in_buffer: -+ lb = len(self.ac_in_buffer) -+ terminator = self.get_terminator() -+ if not terminator: -+ # no terminator, collect it all -+ self.collect_incoming_data(self.ac_in_buffer) -+ self.ac_in_buffer = b'' -+ elif isinstance(terminator, int): -+ # numeric terminator -+ n = terminator -+ if lb < n: -+ self.collect_incoming_data(self.ac_in_buffer) -+ self.ac_in_buffer = b'' -+ self.terminator = self.terminator - lb -+ else: -+ self.collect_incoming_data(self.ac_in_buffer[:n]) -+ self.ac_in_buffer = self.ac_in_buffer[n:] -+ self.terminator = 0 -+ self.found_terminator() -+ else: -+ terminator_len = len(terminator) -+ index = self.ac_in_buffer.find(terminator) -+ if index != -1: -+ if index > 0: -+ self.collect_incoming_data(self.ac_in_buffer[:index]) -+ self.ac_in_buffer = self.ac_in_buffer[index + -+ terminator_len:] -+ self.found_terminator() -+ else: -+ index = find_prefix_at_end(self.ac_in_buffer, terminator) -+ if index: -+ if index != lb: -+ self.collect_incoming_data( -+ self.ac_in_buffer[:-index]) -+ self.ac_in_buffer = self.ac_in_buffer[-index:] -+ break -+ else: -+ self.collect_incoming_data(self.ac_in_buffer) -+ self.ac_in_buffer = b'' -+ -+ def handle_write(self): -+ self.initiate_send() -+ -+ def handle_close(self): -+ self.close() -+ -+ def push(self, data): -+ if not isinstance(data, (bytes, bytearray, memoryview)): -+ raise TypeError('data argument must be byte-ish (%r)', -+ type(data)) -+ sabs = self.ac_out_buffer_size -+ if len(data) > sabs: -+ for i in range(0, len(data), sabs): -+ self.producer_fifo.append(data[i:i + sabs]) -+ else: -+ self.producer_fifo.append(data) -+ self.initiate_send() -+ -+ def push_with_producer(self, producer): -+ self.producer_fifo.append(producer) -+ self.initiate_send() -+ -+ def readable(self): -+ return 1 -+ -+ def writable(self): -+ return self.producer_fifo or (not self.connected) -+ -+ def close_when_done(self): -+ self.producer_fifo.append(None) -+ -+ def initiate_send(self): -+ while self.producer_fifo and self.connected: -+ first = self.producer_fifo[0] -+ if not first: -+ del self.producer_fifo[0] -+ if first is None: -+ self.handle_close() -+ return -+ -+ obs = self.ac_out_buffer_size -+ try: -+ data = first[:obs] -+ except TypeError: -+ data = first.more() -+ if data: -+ self.producer_fifo.appendleft(data) -+ else: -+ del self.producer_fifo[0] -+ continue -+ -+ if isinstance(data, str) and self.use_encoding: -+ data = bytes(data, self.encoding) -+ -+ try: -+ num_sent = self.send(data) -+ except OSError: -+ self.handle_error() -+ return -+ -+ if num_sent: -+ if num_sent < len(data) or obs < len(first): -+ self.producer_fifo[0] = first[num_sent:] -+ else: -+ del self.producer_fifo[0] -+ return -+ -+ def discard_buffers(self): -+ # Emergencies only! -+ self.ac_in_buffer = b'' -+ del self.incoming[:] -+ self.producer_fifo.clear() -+ -+ -+class simple_producer: -+ -+ def __init__(self, data, buffer_size=512): -+ self.data = data -+ self.buffer_size = buffer_size -+ -+ def more(self): -+ if len(self.data) > self.buffer_size: -+ result = self.data[:self.buffer_size] -+ self.data = self.data[self.buffer_size:] -+ return result -+ else: -+ result = self.data -+ self.data = b'' -+ return result -+ -+ -+def find_prefix_at_end(haystack, needle): -+ ll = len(needle) - 1 -+ while ll and not haystack.endswith(needle[:ll]): -+ ll -= 1 -+ return ll -Index: pyftpdlib-1.5.7/pyftpdlib/_asyncore.py -=================================================================== ---- /dev/null -+++ pyftpdlib-1.5.7/pyftpdlib/_asyncore.py -@@ -0,0 +1,587 @@ -+# Id: asyncore.py,v 2.51 2000/09/07 22:29:26 rushing Exp -+# Author: Sam Rushing -+ -+# ====================================================================== -+# Copyright 1996 by Sam Rushing -+# -+# All Rights Reserved -+# -+# Permission to use, copy, modify, and distribute this software and -+# its documentation for any purpose and without fee is hereby -+# granted, provided that the above copyright notice appear in all -+# copies and that both that copyright notice and this permission -+# notice appear in supporting documentation, and that the name of Sam -+# Rushing not be used in advertising or publicity pertaining to -+# distribution of the software without specific, written prior -+# permission. -+# -+# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, -+# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN -+# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR -+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -+# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -+# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -+# ====================================================================== -+ -+ -+import os -+import select -+import socket -+import sys -+import time -+import warnings -+from errno import EAGAIN -+from errno import EALREADY -+from errno import EBADF -+from errno import ECONNABORTED -+from errno import ECONNRESET -+from errno import EINPROGRESS -+from errno import EINVAL -+from errno import EISCONN -+from errno import ENOTCONN -+from errno import EPIPE -+from errno import ESHUTDOWN -+from errno import EWOULDBLOCK -+from errno import errorcode -+ -+ -+_DISCONNECTED = frozenset( -+ {ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED, EPIPE, EBADF}) -+ -+try: -+ socket_map -+except NameError: -+ socket_map = {} -+ -+ -+def _strerror(err): -+ try: -+ return os.strerror(err) -+ except (ValueError, OverflowError, NameError): -+ if err in errorcode: -+ return errorcode[err] -+ return "Unknown error %s" % err -+ -+ -+class ExitNow(Exception): -+ pass -+ -+ -+_reraised_exceptions = (ExitNow, KeyboardInterrupt, SystemExit) -+ -+ -+def read(obj): -+ try: -+ obj.handle_read_event() -+ except _reraised_exceptions: -+ raise -+ except Exception: -+ obj.handle_error() -+ -+ -+def write(obj): -+ try: -+ obj.handle_write_event() -+ except _reraised_exceptions: -+ raise -+ except Exception: -+ obj.handle_error() -+ -+ -+def _exception(obj): -+ try: -+ obj.handle_expt_event() -+ except _reraised_exceptions: -+ raise -+ except Exception: -+ obj.handle_error() -+ -+ -+def readwrite(obj, flags): -+ try: -+ if flags & select.POLLIN: -+ obj.handle_read_event() -+ if flags & select.POLLOUT: -+ obj.handle_write_event() -+ if flags & select.POLLPRI: -+ obj.handle_expt_event() -+ if flags & (select.POLLHUP | select.POLLERR | select.POLLNVAL): -+ obj.handle_close() -+ except OSError as e: -+ if e.errno not in _DISCONNECTED: -+ obj.handle_error() -+ else: -+ obj.handle_close() -+ except _reraised_exceptions: -+ raise -+ except Exception: -+ obj.handle_error() -+ -+ -+def poll(timeout=0.0, map=None): -+ if map is None: -+ map = socket_map -+ if map: -+ r = [] -+ w = [] -+ e = [] -+ for fd, obj in list(map.items()): -+ is_r = obj.readable() -+ is_w = obj.writable() -+ if is_r: -+ r.append(fd) -+ if is_w and not obj.accepting: -+ w.append(fd) -+ if is_r or is_w: -+ e.append(fd) -+ if [] == r == w == e: -+ time.sleep(timeout) -+ return -+ -+ r, w, e = select.select(r, w, e, timeout) -+ -+ for fd in r: -+ obj = map.get(fd) -+ if obj is None: -+ continue -+ read(obj) -+ -+ for fd in w: -+ obj = map.get(fd) -+ if obj is None: -+ continue -+ write(obj) -+ -+ for fd in e: -+ obj = map.get(fd) -+ if obj is None: -+ continue -+ _exception(obj) -+ -+ -+def poll2(timeout=0.0, map=None): -+ if map is None: -+ map = socket_map -+ if timeout is not None: -+ timeout = int(timeout * 1000) -+ pollster = select.poll() -+ if map: -+ for fd, obj in list(map.items()): -+ flags = 0 -+ if obj.readable(): -+ flags |= select.POLLIN | select.POLLPRI -+ if obj.writable() and not obj.accepting: -+ flags |= select.POLLOUT -+ if flags: -+ pollster.register(fd, flags) -+ -+ r = pollster.poll(timeout) -+ for fd, flags in r: -+ obj = map.get(fd) -+ if obj is None: -+ continue -+ readwrite(obj, flags) -+ -+ -+poll3 = poll2 # Alias for backward compatibility -+ -+ -+def loop(timeout=30.0, use_poll=False, map=None, count=None): -+ if map is None: -+ map = socket_map -+ -+ if use_poll and hasattr(select, 'poll'): -+ poll_fun = poll2 -+ else: -+ poll_fun = poll -+ -+ if count is None: -+ while map: -+ poll_fun(timeout, map) -+ -+ else: -+ while map and count > 0: -+ poll_fun(timeout, map) -+ count = count - 1 -+ -+ -+class dispatcher: -+ -+ debug = False -+ connected = False -+ accepting = False -+ connecting = False -+ closing = False -+ addr = None -+ ignore_log_types = frozenset({'warning'}) -+ -+ def __init__(self, sock=None, map=None): -+ if map is None: -+ self._map = socket_map -+ else: -+ self._map = map -+ -+ self._fileno = None -+ -+ if sock: -+ sock.setblocking(False) -+ self.set_socket(sock, map) -+ self.connected = True -+ try: -+ self.addr = sock.getpeername() -+ except OSError as err: -+ if err.errno in (ENOTCONN, EINVAL): -+ self.connected = False -+ else: -+ self.del_channel(map) -+ raise -+ else: -+ self.socket = None -+ -+ def __repr__(self): -+ status = [self.__class__.__module__ + -+ "." + self.__class__.__qualname__] -+ if self.accepting and self.addr: -+ status.append('listening') -+ elif self.connected: -+ status.append('connected') -+ if self.addr is not None: -+ try: -+ status.append('%s:%d' % self.addr) -+ except TypeError: -+ status.append(repr(self.addr)) -+ return '<%s at %#x>' % (' '.join(status), id(self)) -+ -+ def add_channel(self, map=None): -+ # self.log_info('adding channel %s' % self) -+ if map is None: -+ map = self._map -+ map[self._fileno] = self -+ -+ def del_channel(self, map=None): -+ fd = self._fileno -+ if map is None: -+ map = self._map -+ if fd in map: -+ del map[fd] -+ self._fileno = None -+ -+ def create_socket(self, family=socket.AF_INET, type=socket.SOCK_STREAM): -+ self.family_and_type = family, type -+ sock = socket.socket(family, type) -+ sock.setblocking(False) -+ self.set_socket(sock) -+ -+ def set_socket(self, sock, map=None): -+ self.socket = sock -+ self._fileno = sock.fileno() -+ self.add_channel(map) -+ -+ def set_reuse_addr(self): -+ try: -+ self.socket.setsockopt( -+ socket.SOL_SOCKET, socket.SO_REUSEADDR, -+ self.socket.getsockopt(socket.SOL_SOCKET, -+ socket.SO_REUSEADDR) | 1 -+ ) -+ except OSError: -+ pass -+ -+ def readable(self): -+ return True -+ -+ def writable(self): -+ return True -+ -+ def listen(self, num): -+ self.accepting = True -+ if os.name == 'nt' and num > 5: -+ num = 5 -+ return self.socket.listen(num) -+ -+ def bind(self, addr): -+ self.addr = addr -+ return self.socket.bind(addr) -+ -+ def connect(self, address): -+ self.connected = False -+ self.connecting = True -+ err = self.socket.connect_ex(address) -+ if err in (EINPROGRESS, EALREADY, EWOULDBLOCK) \ -+ or err == EINVAL and os.name == 'nt': -+ self.addr = address -+ return -+ if err in (0, EISCONN): -+ self.addr = address -+ self.handle_connect_event() -+ else: -+ raise OSError(err, errorcode[err]) -+ -+ def accept(self): -+ try: -+ conn, addr = self.socket.accept() -+ except TypeError: -+ return None -+ except OSError as why: -+ if why.errno in (EWOULDBLOCK, ECONNABORTED, EAGAIN): -+ return None -+ else: -+ raise -+ else: -+ return conn, addr -+ -+ def send(self, data): -+ try: -+ result = self.socket.send(data) -+ return result -+ except OSError as why: -+ if why.errno == EWOULDBLOCK: -+ return 0 -+ elif why.errno in _DISCONNECTED: -+ self.handle_close() -+ return 0 -+ else: -+ raise -+ -+ def recv(self, buffer_size): -+ try: -+ data = self.socket.recv(buffer_size) -+ if not data: -+ self.handle_close() -+ return b'' -+ else: -+ return data -+ except OSError as why: -+ if why.errno in _DISCONNECTED: -+ self.handle_close() -+ return b'' -+ else: -+ raise -+ -+ def close(self): -+ self.connected = False -+ self.accepting = False -+ self.connecting = False -+ self.del_channel() -+ if self.socket is not None: -+ try: -+ self.socket.close() -+ except OSError as why: -+ if why.errno not in (ENOTCONN, EBADF): -+ raise -+ -+ def log(self, message): -+ sys.stderr.write('log: %s\n' % str(message)) -+ -+ def log_info(self, message, type='info'): -+ if type not in self.ignore_log_types: -+ print('%s: %s' % (type, message)) # noqa -+ -+ def handle_read_event(self): -+ if self.accepting: -+ # accepting sockets are never connected, they "spawn" new -+ # sockets that are connected -+ self.handle_accept() -+ elif not self.connected: -+ if self.connecting: -+ self.handle_connect_event() -+ self.handle_read() -+ else: -+ self.handle_read() -+ -+ def handle_connect_event(self): -+ err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) -+ if err != 0: -+ raise OSError(err, _strerror(err)) -+ self.handle_connect() -+ self.connected = True -+ self.connecting = False -+ -+ def handle_write_event(self): -+ if self.accepting: -+ return -+ -+ if not self.connected: -+ if self.connecting: -+ self.handle_connect_event() -+ self.handle_write() -+ -+ def handle_expt_event(self): -+ err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) -+ if err != 0: -+ self.handle_close() -+ else: -+ self.handle_expt() -+ -+ def handle_error(self): -+ nil, t, v, tbinfo = compact_traceback() -+ try: -+ self_repr = repr(self) -+ except Exception: -+ self_repr = '<__repr__(self) failed for object at %0x>' % id(self) -+ -+ self.log_info( -+ 'uncaptured python exception, closing channel %s (%s:%s %s)' % ( -+ self_repr, -+ t, -+ v, -+ tbinfo -+ ), -+ 'error' -+ ) -+ self.handle_close() -+ -+ def handle_expt(self): -+ self.log_info('unhandled incoming priority event', 'warning') -+ -+ def handle_read(self): -+ self.log_info('unhandled read event', 'warning') -+ -+ def handle_write(self): -+ self.log_info('unhandled write event', 'warning') -+ -+ def handle_connect(self): -+ self.log_info('unhandled connect event', 'warning') -+ -+ def handle_accept(self): -+ pair = self.accept() -+ if pair is not None: -+ self.handle_accepted(*pair) -+ -+ def handle_accepted(self, sock, addr): -+ sock.close() -+ self.log_info('unhandled accepted event', 'warning') -+ -+ def handle_close(self): -+ self.log_info('unhandled close event', 'warning') -+ self.close() -+ -+ -+class dispatcher_with_send(dispatcher): -+ -+ def __init__(self, sock=None, map=None): -+ dispatcher.__init__(self, sock, map) -+ self.out_buffer = b'' -+ -+ def initiate_send(self): -+ num_sent = 0 -+ num_sent = dispatcher.send(self, self.out_buffer[:65536]) -+ self.out_buffer = self.out_buffer[num_sent:] -+ -+ def handle_write(self): -+ self.initiate_send() -+ -+ def writable(self): -+ return (not self.connected) or len(self.out_buffer) -+ -+ def send(self, data): -+ if self.debug: -+ self.log_info('sending %s' % repr(data)) -+ self.out_buffer = self.out_buffer + data -+ self.initiate_send() -+ -+# --------------------------------------------------------------------------- -+# used for debugging. -+# --------------------------------------------------------------------------- -+ -+ -+def compact_traceback(): -+ t, v, tb = sys.exc_info() -+ tbinfo = [] -+ if not tb: # Must have a traceback -+ raise AssertionError("traceback does not exist") -+ while tb: -+ tbinfo.append(( -+ tb.tb_frame.f_code.co_filename, -+ tb.tb_frame.f_code.co_name, -+ str(tb.tb_lineno) -+ )) -+ tb = tb.tb_next -+ -+ # just to be safe -+ del tb -+ -+ file, function, line = tbinfo[-1] -+ info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo]) -+ return (file, function, line), t, v, info -+ -+ -+def close_all(map=None, ignore_all=False): -+ if map is None: -+ map = socket_map -+ for x in list(map.values()): -+ try: -+ x.close() -+ except OSError as x: -+ if x.errno == EBADF: -+ pass -+ elif not ignore_all: -+ raise -+ except _reraised_exceptions: -+ raise -+ except Exception: -+ if not ignore_all: -+ raise -+ map.clear() -+ -+ -+if os.name == 'posix': -+ class file_wrapper: -+ # Here we override just enough to make a file -+ # look like a socket for the purposes of asyncore. -+ # The passed fd is automatically os.dup()'d -+ -+ def __init__(self, fd): -+ self.fd = os.dup(fd) -+ -+ def __del__(self): -+ if self.fd >= 0: -+ warnings.warn("unclosed file %r" % self, ResourceWarning, -+ source=self, stacklevel=2) -+ self.close() -+ -+ def recv(self, *args): -+ return os.read(self.fd, *args) -+ -+ def send(self, *args): -+ return os.write(self.fd, *args) -+ -+ def getsockopt(self, level, optname, buflen=None): -+ if (level == socket.SOL_SOCKET and -+ optname == socket.SO_ERROR and -+ not buflen): -+ return 0 -+ raise NotImplementedError("Only asyncore specific behaviour " -+ "implemented.") -+ -+ read = recv -+ write = send -+ -+ def close(self): -+ if self.fd < 0: -+ return -+ fd = self.fd -+ self.fd = -1 -+ os.close(fd) -+ -+ def fileno(self): -+ return self.fd -+ -+ class file_dispatcher(dispatcher): -+ -+ def __init__(self, fd, map=None): -+ dispatcher.__init__(self, None, map) -+ self.connected = True -+ try: -+ fd = fd.fileno() -+ except AttributeError: -+ pass -+ self.set_file(fd) -+ # set it to non-blocking mode -+ os.set_blocking(fd, False) -+ -+ def set_file(self, fd): -+ self.socket = file_wrapper(fd) -+ self._fileno = self.socket.fileno() -+ self.add_channel() -Index: pyftpdlib-1.5.7/pyftpdlib/handlers.py -=================================================================== ---- pyftpdlib-1.5.7.orig/pyftpdlib/handlers.py -+++ pyftpdlib-1.5.7/pyftpdlib/handlers.py -@@ -2,7 +2,6 @@ - # Use of this source code is governed by MIT license that can be - # found in the LICENSE file. - --import asynchat - import contextlib - import errno - import glob -@@ -57,6 +56,12 @@ from .log import debug - from .log import logger - - -+if sys.version_info[:2] >= (3, 12): -+ from . import _asynchat as asynchat -+else: -+ import asynchat -+ -+ - CR_BYTE = ord('\r') - - -Index: pyftpdlib-1.5.7/pyftpdlib/ioloop.py -=================================================================== ---- pyftpdlib-1.5.7.orig/pyftpdlib/ioloop.py -+++ pyftpdlib-1.5.7/pyftpdlib/ioloop.py -@@ -56,8 +56,6 @@ server = Server('localhost', 8021) - IOLoop.instance().loop() - """ - --import asynchat --import asyncore - import errno - import heapq - import os -@@ -80,6 +78,14 @@ from .log import is_logging_configured - from .log import logger - - -+if sys.version_info[:2] >= (3, 12): -+ from . import _asynchat as asynchat -+ from . import _asyncore as asyncore -+else: -+ import asynchat -+ import asyncore -+ -+ - timer = getattr(time, 'monotonic', time.time) - _read = asyncore.read - _write = asyncore.write