Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-aioftp for openSUSE:Factory checked in at 2025-06-03 19:10:44 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-aioftp (Old) and /work/SRC/openSUSE:Factory/.python-aioftp.new.16005 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-aioftp" Tue Jun 3 19:10:44 2025 rev:12 rq:1282358 version:0.25.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-aioftp/python-aioftp.changes 2025-01-22 16:39:11.241023231 +0100 +++ /work/SRC/openSUSE:Factory/.python-aioftp.new.16005/python-aioftp.changes 2025-06-03 19:10:46.041988060 +0200 @@ -1,0 +2,9 @@ +Tue Jun 3 06:47:47 UTC 2025 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to 0.25.1 + * do not start explicit tls if implicit mode used (fixes #184) (#185) +- from version 0.25.0 + * client: add partial client support for explicit tls (#183) +- Update BuildRequires from pyproject.toml + +------------------------------------------------------------------- Old: ---- aioftp-0.24.1.tar.gz New: ---- aioftp-0.25.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-aioftp.spec ++++++ --- /var/tmp/diff_new_pack.eB2K5R/_old 2025-06-03 19:10:46.626012319 +0200 +++ /var/tmp/diff_new_pack.eB2K5R/_new 2025-06-03 19:10:46.630012485 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-aioftp -Version: 0.24.1 +Version: 0.25.1 Release: 0 Summary: FTP client/server for asyncio License: Apache-2.0 @@ -34,6 +34,7 @@ BuildRequires: %{python_module async_timeout >= 4.0.0} BuildRequires: %{python_module pytest-asyncio} BuildRequires: %{python_module pytest-cov} +BuildRequires: %{python_module pytest-mock} BuildRequires: %{python_module pytest} BuildRequires: %{python_module siosocks >= 0.2.0} BuildRequires: %{python_module trustme} ++++++ aioftp-0.24.1.tar.gz -> aioftp-0.25.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/PKG-INFO new/aioftp-0.25.1/PKG-INFO --- old/aioftp-0.24.1/PKG-INFO 2024-12-13 13:22:30.911728900 +0100 +++ new/aioftp-0.25.1/PKG-INFO 2025-04-11 20:53:50.560357600 +0200 @@ -1,9 +1,9 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: aioftp -Version: 0.24.1 +Version: 0.25.1 Summary: ftp client/server for asyncio Author: yieyu, rsichnyi, jw4js, oleksandr-kuzmenko, ndhansen, modelmat, greut, PonyPC, jacobtomlinson, bachya, CrafterKolyan, jkr78 -Author-email: pohmelie <multisosnoo...@gmail.com>, asvetlov <andrew.svet...@gmail.com>, decaz <deca...@gmail.com>, janneronkko <janne.ron...@iki.fi>, thirtyseven <t...@shlashdot.org>, ported-pw <cont...@ported.pw>, Olegt0rr <t...@mail.ru>, michalc <mic...@charemza.name>, ch3pjw <p...@concertdaw.co.uk>, puddly <pudd...@gmail.com>, AMDmi3 <amd...@amdmi3.ru>, webknjaz <webknjaz+github/prof...@redhat.com>, rcfox <r...@rcfox.ca> +Author-email: pohmelie <multisosnoo...@gmail.com>, asvetlov <andrew.svet...@gmail.com>, decaz <deca...@gmail.com>, janneronkko <janne.ron...@iki.fi>, thirtyseven <t...@shlashdot.org>, ported-pw <cont...@ported.pw>, Olegt0rr <t...@mail.ru>, michalc <mic...@charemza.name>, ch3pjw <p...@concertdaw.co.uk>, puddly <pudd...@gmail.com>, AMDmi3 <amd...@amdmi3.ru>, webknjaz <webknjaz+github/prof...@redhat.com>, rcfox <r...@rcfox.ca>, bellini666 <thi...@bellini.dev> License: Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -220,6 +220,7 @@ Requires-Dist: async_timeout>=4.0.0; extra == "dev" Requires-Dist: pytest-asyncio; extra == "dev" Requires-Dist: pytest-cov; extra == "dev" +Requires-Dist: pytest-mock; extra == "dev" Requires-Dist: pytest; extra == "dev" Requires-Dist: siosocks; extra == "dev" Requires-Dist: trustme; extra == "dev" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/history.rst new/aioftp-0.25.1/history.rst --- old/aioftp-0.24.1/history.rst 2024-12-13 13:22:27.000000000 +0100 +++ new/aioftp-0.25.1/history.rst 2025-04-11 20:53:44.000000000 +0200 @@ -1,130 +1,131 @@ -x.x.x (xx-xx-xxxx) +x.x.x (xxxx-xx-xx) -0.24.1 (13-12-2024) +0.25.1 (2025-04-11) +------------------- +- client: do not start explicit tls if implicit mode used (#184) + +0.25.0 (2025-04-08) +------------------- +- client: add partial client support for explicit tls (#183) +Thanks to `bellini666 <https://github.com/bellini666>`_ + +0.24.1 (2024-12-13) +------------------- - server: use single line pasv response (fix #142) -0.24.0 (11-12-2024) +0.24.0 (2024-12-11) ------------------- - remove documentation dependencies from pyproject.toml (moved to docs/requirements.txt) - include symlink destination in path info for unix legacy mode (#169) - update documentation links (#180) Thanks to `webknjaz <https://github.com/webknjaz>`_, `rcfox <https://github.com/rcfox>`_ -0.23.1 (14-10-2024) +0.23.1 (2024-10-14) ------------------- - update ci -0.23.0 (14-10-2024) +0.23.0 (2024-10-14) ------------------- - server: fix pathlib `relative_to` issue (#179) - minimal python version upgraded to 3.9 -0.22.3 (05-01-2024) +0.22.3 (2024-01-05) ------------------- - minimal python version downgraded to 3.8 -0.22.2 (29-12-2023) +0.22.2 (2023-12-29) ------------------- - ci: separate build and publish jobs -0.22.1 (29-12-2023) +0.22.1 (2023-12-29) ------------------- - docs: update/fix readthedocs configuration - ci: fix workflow file extension from `yaml` to `yml` -0.22.0 (29-12-2023) +0.22.0 (2023-12-29) ------------------- - client.list: fix infinite symlink loop for `.` and `..` on FTP servers with UNIX-like filesystem for `client.list(path, recursive=True)` - project file structure: refactor to use `pyproject.toml` - minimal python version bumped to 3.11 - ci: update publish/deploy job (#171) -0.21.4 (13-10-2022) +0.21.4 (2022-10-13) ------------------- - tests: use `pytest_asyncio` `strict` mode and proper decorations (#155) - setup/tests: set low bound for version of `async-timeout` (#159) -0.21.3 (15-07-2022) +0.21.3 (2022-07-15) ------------------- - server/`LIST`: prevent broken links are listed, but can't be used with `stat` - server: make `User.get_permissions` async -0.21.2 (22-04-2022) +0.21.2 (2022-04-22) ------------------- - tests: remove exception representation check -0.21.1 (20-04-2022) +0.21.1 (2022-04-20) ------------------- - tests: replace more specific `ConnectionRefusedError` with `OSError` for compatibility with FreeBSD (#152) Thanks to `AMDmi3 https://github.com/AMDmi3`_ -0.21.0 (18-03-2022) ------------------- +0.21.0 (2022-03-18) +------------------- - server: support PASV response with custom address (#150) Thanks to `janneronkko https://github.com/janneronkko`_ -0.20.1 (15-02-2022) ------------------- +0.20.1 (2022-02-15) +------------------- - server: fix real directory resolve for windows (#147) Thanks to `ported-pw https://github.com/ported-pw`_ -0.20.0 (27-12-2021) ------------------- - +0.20.0 (2021-12-27) +------------------- - add client argument to set priority of custom list parser (`parse_list_line_custom_first`) (#145) - do not ignore failed parsing of list response (#144) Thanks to `spolloni https://github.com/spolloni`_ -0.19.0 (08-10-2021) ------------------- - +0.19.0 (2021-10-08) +------------------- - add client connection timeout (#140) - remove explicit coroutine passing to `asyncio.wait` (#134) Thanks to `decaz <https://github.com/decaz>`_ -0.18.1 (03-10-2020) ------------------- - +0.18.1 (2020-10-03) +------------------- - sync tests with new `siosocks` (#127) - some docs fixes - log level changes -0.18.0 (03-09-2020) ------------------- - +0.18.0 (2020-09-03) +------------------- - server: fix `MLSX` time format (#125) - server: resolve server address from connection (#125) Thanks to `PonyPC <https://github.com/PonyPC>`_ -0.17.2 (21-08-2020) ------------------- - +0.17.2 (2020-08-21) +------------------- - server: fix broken `python -m aioftp` after 3.7 migration -0.17.1 (14-08-2020) ------------------- - +0.17.1 (2020-08-14) +------------------- - common/stream: add `readexactly` proxy method -0.17.0 (11-08-2020) ------------------- - +0.17.0 (2020-08-11) +------------------- - tests: fix test_unlink_on_dir on POSIX compatible systems (#118) - docs: fix extra parentheses (#122) - client: replace `ClientSession` with `Client.context` Thanks to `AMDmi3 <https://github.com/AMDmi3>`_, `Olegt0rr <https://github.com/Olegt0rr>`_ -0.16.1 (09-07-2020) ------------------- - +0.16.1 (2020-07-09) +------------------- - client: strip date before parsing (#113) - client: logger no longer prints out plaintext password (#114) - client: add custom passive commands to client (#116) Thanks to `ndhansen <https://github.com/ndhansen>`_ -0.16.0 (11-03-2020) ------------------- - +0.16.0 (2020-03-11) +------------------- - server: remove obsolete `pass` to `pass_` command renaming Thanks to `Puddly <https://github.com/puddly>`_ @@ -132,58 +133,49 @@ - all: add base exception class Thanks to `decaz <https://github.com/decaz>`_ -0.15.0 (07-01-2020) +0.15.0 (2020-01-07) ------------------- - - server: use explicit mapping of available commands for security reasons Thanks to `Puddly` for report -0.14.0 (30-12-2019) +0.14.0 (2019-12-30) ------------------- - - client: add socks proxy support via `siosocks <https://github.com/pohmelie/siosocks>`_ (#94) - client: add custom `list` parser (#95) Thanks to `purpleskyfall <https://github.com/purpleskyfall>`_, `VyachAp <https://github.com/VyachAp>`_ -0.13.0 (24-03-2019) +0.13.0 (2019-03-24) ------------------- - - client: add windows list parser (#82) - client/server: fix implicit ssl mode (#89) - tests: move to pytest - all: small fixes Thanks to `jw4js <https://github.com/jw4js>`_, `PonyPC <https://github.com/PonyPC>`_ -0.12.0 (15-10-2018) +0.12.0 (2018-10-15) ------------------- - - all: add implicit ftps mode support (#81) Thanks to `alxpy <https://github.com/alxpy>`_, `webknjaz <https://github.com/webknjaz>`_ -0.11.1 (30-08-2018) +0.11.1 (2018-08-30) ------------------- - - server: fix memory pathio is not shared between connections - client: add argument to `list` to allow manually specifying raw command (#78) Thanks to `thirtyseven <https://github.com/thirtyseven>`_ - -0.11.0 (04-07-2018) +0.11.0 (2018-07-04) ------------------- - - client: fix parsing `ls` modify time (#60) - all: add python3.7 support (`__aiter__` must be regular function since now) (#76, #77) Thanks to `saulcruz <https://github.com/saulcruz>`_, `NickG123 <https://github.com/NickG123>`_, `rsichny <https://github.com/rsichny>`_, `Modelmat <https://github.com/Modelmat>`_, `webknjaz <https://github.com/webknjaz>`_ -0.10.1 (01-03-2018) +0.10.1 (2018-03-01) ------------------- - - client: more flexible `EPSV` response parsing Thanks to `p4l1ly <https://github.com/p4l1ly>`_ -0.10.0 (03-02-2018) +0.10.0 (2018-02-03) ------------------- - - server: fix ipv6 peername unpack - server: `connection` object is accessible from path-io layer since now - main: add command line argument to set version of IP protocol @@ -193,29 +185,25 @@ - client: change `PASV` to `EPSV` with fallback to `PASV` Thanks to `jacobtomlinson <https://github.com/jacobtomlinson>`_, `mbkr1992 <https://github.com/mbkr1992>`_ -0.9.0 (04-01-2018) +0.9.0 (2018-01-04) ------------------ - - server: fix server address in passive mode - server: do not reraise dispatcher exceptions - server: remove `wait_closed`, `close` is coroutine since now Thanks to `yieyu <https://github.com/yieyu>`_, `jkr78 <https://github.com/jkr78>`_ -0.8.1 (08-10-2017) +0.8.1 (2017-10-08) ------------------ - - client: ignore LIST lines, which can't be parsed Thanks to `bachya <https://github.com/bachya>`_ -0.8.0 (06-08-2017) +0.8.0 (2017-08-06) ------------------ - - client/server: add explicit encoding Thanks to `anan-lee <https://github.com/anan-lee>`_ -0.7.0 (17-04-2017) +0.7.0 (2017-04-17) ------------------ - - client: add base `LIST` parsing - client: add `client.list` fallback on `MLSD` «not implemented» status code to `LIST` - client: add `client.stat` fallback on `MLST` «not implemented» status code to `LIST` @@ -224,51 +212,44 @@ - server: fix `PASV` sequencies before data transfer (latest `PASV` win) Thanks to `jw4js <https://github.com/jw4js>`_, `rsichny <https://github.com/rsichny>`_ -0.6.3 (02-03-2017) +0.6.3 (2017-03-02) ------------------ - - `stream.read` will read whole data by default (as `asyncio.StreamReader.read`) Thanks to `sametmax <https://github.com/sametmax>`_ -0.6.2 (27-02-2017) +0.6.2 (2017-02-27) ------------------ - - replace `docopt` with `argparse` - add `syst` server command - improve client `list` documentation Thanks to `thelostt <https://github.com/thelostt>`_, `yieyu <https://github.com/yieyu>`_ -0.6.1 (16-04-2016) +0.6.1 (2016-04-16) ------------------ - - fix documentation main page client example -0.6.0 (16-04-2016) +0.6.0 (2016-04-16) ------------------ - - fix `modifed time` field for `list` command result - add `ClientSession` context - add `REST` command to server and client Thanks to `rsichny <https://github.com/rsichny>`_ -0.5.0 (12-02-2016) +0.5.0 (2016-02-12) ------------------ - - change development status to production/stable - add configuration to restrict port range for passive server - build LIST string with stat.filemode Thanks to `rsichny <https://github.com/rsichny>`_ -0.4.1 (21-12-2015) +0.4.1 (2015-12-21) ------------------ - - improved performance on non-throttled streams - default path io layer for client and server is PathIO since now - added benchmark result -0.4.0 (17-12-2015) +0.4.0 (2015-12-17) ------------------ - - `async for` for pathio list function - async context manager for streams and pathio files io - python 3.5 only @@ -276,14 +257,12 @@ - all path errors are now reraised as PathIOError - server does not drop connection on path io errors since now, but return "451" code -0.3.1 (09-11-2015) +0.3.1 (2015-11-09) ------------------ - - fixed setup.py long-description -0.3.0 (09-11-2015) +0.3.0 (2015-11-09) ------------------ - - added handling of OSError in dispatcher - fixed client/server close not opened file in finally - handling PASS after login @@ -300,9 +279,8 @@ - all socket streams are ThrottleStreamIO since now Thanks to `rsichny <https://github.com/rsichny>`_, `tier2003 <https://github.com/tier2003>`_ -0.2.0 (22-09-2015) +0.2.0 (2015-09-22) ------------------ - - client throttle - new server dispatcher (can wait for connections) - maximum connections per user/server @@ -313,54 +291,45 @@ - "sh" module removed from test requirements Thanks to `rsichny <https://github.com/rsichny>`_, `jettify <https://github.com/jettify>`_ -0.1.7 (03-09-2015) +0.1.7 (2015-09-03) ------------------ - - bugfix on windows (can't make passive connection to 0.0.0.0:port) - default host is "127.0.0.1" since now - silently ignoring ipv6 sockets in server binding list -0.1.6 (03-09-2015) +0.1.6 (2015-09-03) ------------------ - - bugfix on windows (ipv6 address come first in list of binded sockets) -0.1.5 (01-09-2015) +0.1.5 (2015-09-01) ------------------ - - bugfix server on windows (PurePosixPath for virtual path) -0.1.4 (31-08-2015) +0.1.4 (2015-08-31) ------------------ - - close data connection after client disconnects Thanks to `rsichny <https://github.com/rsichny>`_ -0.1.3 (28-08-2015) +0.1.3 (2015-08-28) ------------------ - - pep8 "Method definitions inside a class are surrounded by a single blank line" - MemoryPathIO.Stats should include st_mode Thanks to `rsichny <https://github.com/rsichny>`_ -0.1.2 (11-06-2015) +0.1.2 (2015-06-11) ------------------ - - aioftp now executes like script ("python -m aioftp") -0.1.1 (10-06-2015) +0.1.1 (2015-06-10) ------------------ - - typos in server strings - docstrings for path abstraction layer -0.1.0 (05-06-2015) +0.1.0 (2015-06-05) ------------------ - - server functionality - path abstraction layer -0.0.1 (24-04-2015) +0.0.1 (2015-04-24) ------------------ - - first release (client only) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/pyproject.toml new/aioftp-0.25.1/pyproject.toml --- old/aioftp-0.24.1/pyproject.toml 2024-12-13 13:22:27.000000000 +0100 +++ new/aioftp-0.25.1/pyproject.toml 2025-04-11 20:53:44.000000000 +0200 @@ -1,6 +1,6 @@ [project] name = "aioftp" -version = "0.24.1" +version = "0.25.1" description = "ftp client/server for asyncio" readme = "README.rst" requires-python = ">= 3.9" @@ -31,6 +31,7 @@ {name = "AMDmi3", email="amd...@amdmi3.ru"}, {name = "webknjaz", email="webknjaz+github/prof...@redhat.com"}, {name = "rcfox", email="r...@rcfox.ca"}, + {name = "bellini666", email="thi...@bellini.dev"}, ] classifiers = [ "Programming Language :: Python", @@ -52,6 +53,7 @@ "async_timeout >= 4.0.0", "pytest-asyncio", "pytest-cov", + "pytest-mock", "pytest", "siosocks", "trustme", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/client.py new/aioftp-0.25.1/src/aioftp/client.py --- old/aioftp-0.24.1/src/aioftp/client.py 2024-12-13 13:22:27.000000000 +0100 +++ new/aioftp-0.25.1/src/aioftp/client.py 2025-04-11 20:53:44.000000000 +0200 @@ -6,6 +6,7 @@ import logging import pathlib import re +import ssl from functools import partial from . import errors, pathio @@ -19,6 +20,7 @@ HALF_OF_YEAR_IN_SECONDS, TWO_YEARS_IN_SECONDS, AsyncListerMixin, + SSLSessionBoundContext, StreamThrottle, ThrottleStreamIO, async_enterable, @@ -138,6 +140,8 @@ self.parse_list_line_custom_first = parse_list_line_custom_first self._passive_commands = passive_commands self._open_connection = partial(open_connection, ssl=self.ssl, **siosocks_asyncio_kwargs) + self._upgraded_to_tls = False + self._logged_in = False async def connect(self, host, port=DEFAULT_PORT): self.server_host = host @@ -636,6 +640,46 @@ code, info = await self.command(None, "220", "120") return info + async def _send_tls_protection_commands(self) -> None: + """ + :py:func:`asyncio.coroutine` + + Sends the PBSZ and PROT commands as required for TLS connection. + """ + await self.command("PBSZ 0", "200") + await self.command("PROT P", "200") + + async def upgrade_to_tls(self, sslcontext=None): + """ + :py:func:`asyncio.coroutine` + + Attempts to upgrade the connection to TLS (explicit TLS). + Downgrading via the CCC or REIN commands is not supported. Both the command + and data channels will be encrypted after using this command. You may + call this command at any point during the connection, but not every FTP server + will allow a connection upgrade after logging in. + + :param sslcontext: custom ssl context + :type sslcontext: :py:class:`ssl.SSLContext` + """ + if self.ssl: + raise RuntimeError("SSL context is already set in implicit mode, can't use explicit mode") + + if self._upgraded_to_tls: + return + + await self.command("AUTH TLS", "234") + + if sslcontext: + self.ssl = sslcontext + elif not isinstance(self.ssl, ssl.SSLContext): + self.ssl = ssl.create_default_context() + + await self.stream.start_tls(sslcontext=self.ssl, server_hostname=self.server_host) + + if self._logged_in: + await self._send_tls_protection_commands() + async def login( self, user=DEFAULT_USER, @@ -674,6 +718,11 @@ censor_after=censor_after, ) + self._logged_in = True + + if self._upgraded_to_tls: + await self._send_tls_protection_commands() + async def get_current_directory(self): """ :py:func:`asyncio.coroutine` @@ -779,7 +828,7 @@ cls.parse_line = self.parse_mlsx_line if raw_command not in [None, "MLSD", "LIST"]: raise ValueError( - "raw_command must be one of MLSD or " f"LIST, but got {raw_command}", + f"raw_command must be one of MLSD or LIST, but got {raw_command}", ) if raw_command in [None, "MLSD"]: try: @@ -1190,6 +1239,18 @@ throttles={"_": self.throttle}, timeout=self.socket_timeout, ) + + ssl_object = self.stream.writer.transport.get_extra_info("ssl_object") + if ssl_object and not self.ssl: + await writer.start_tls( + sslcontext=SSLSessionBoundContext( + ssl.PROTOCOL_TLS_CLIENT, + context=ssl_object.context, + session=ssl_object.session, + ), + server_hostname=self.server_host, + ) + return stream async def abort(self, *, wait=True): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/common.py new/aioftp-0.25.1/src/aioftp/common.py --- old/aioftp-0.24.1/src/aioftp/common.py 2024-12-13 13:22:27.000000000 +0100 +++ new/aioftp-0.25.1/src/aioftp/common.py 2025-04-11 20:53:44.000000000 +0200 @@ -3,6 +3,8 @@ import collections import functools import locale +import socket +import ssl import threading from contextlib import contextmanager @@ -24,9 +26,9 @@ "DEFAULT_PASSWORD", "DEFAULT_ACCOUNT", "setlocale", + "SSLSessionBoundContext", ) - END_OF_LINE = "\r\n" DEFAULT_BLOCK_SIZE = 8192 @@ -319,6 +321,16 @@ """ self.writer.close() + async def start_tls(self, sslcontext, server_hostname): + """ + Upgrades the connection to TLS + """ + await self.writer.start_tls( + sslcontext=sslcontext, + server_hostname=server_hostname, + ssl_handshake_timeout=self.write_timeout, + ) + class Throttle: """ @@ -394,7 +406,7 @@ return Throttle(limit=self._limit, reset_rate=self.reset_rate) def __repr__(self): - return f"{self.__class__.__name__}(limit={self._limit!r}, " f"reset_rate={self.reset_rate!r})" + return f"{self.__class__.__name__}(limit={self._limit!r}, reset_rate={self.reset_rate!r})" class StreamThrottle(collections.namedtuple("StreamThrottle", "read write")): @@ -589,3 +601,67 @@ yield locale.setlocale(locale.LC_ALL, name) finally: locale.setlocale(locale.LC_ALL, old_locale) + + +# class from https://github.com/python/cpython/issues/79152 (with some changes) +class SSLSessionBoundContext(ssl.SSLContext): + """ssl.SSLContext bound to an existing SSL session. + + Actually asyncio doesn't support TLS session resumption, the loop.create_connection() API + does not take any TLS session related argument. There is ongoing work to add support for this + at https://github.com/python/cpython/issues/79152. + + The loop.create_connection() API takes a SSL context argument though, the SSLSessionBoundContext + is used to wrap a SSL context and inject a SSL session on calls to + - SSLSessionBoundContext.wrap_socket() + - SSLSessionBoundContext.wrap_bio() + + This wrapper is compatible with any TLS application which calls only the methods above when + making new TLS connections. This class is NOT a subclass of ssl.SSLContext, so it will be + rejected by applications which ensure the SSL context is an instance of ssl.SSLContext. Not being + a subclass of ssl.SSLContext makes this wrapper lightweight. + """ + + __slots__ = ("context", "session") + + def __init__(self, protocol: int, context: ssl.SSLContext, session: ssl.SSLSession): + self.context = context + self.session = session + + def wrap_socket( + self, + sock: socket.socket, + server_side: bool = False, + do_handshake_on_connect: bool = True, + suppress_ragged_eofs: bool = True, + server_hostname: bool = None, + session: ssl.SSLSession = None, + ) -> ssl.SSLSocket: + if session is not None: + raise ValueError("expected session to be None") + return self.context.wrap_socket( + sock=sock, + server_hostname=server_hostname, + server_side=server_side, + do_handshake_on_connect=do_handshake_on_connect, + suppress_ragged_eofs=suppress_ragged_eofs, + session=self.session, + ) + + def wrap_bio( + self, + incoming: ssl.MemoryBIO, + outgoing: ssl.MemoryBIO, + server_side: bool = False, + server_hostname: bool = None, + session: ssl.SSLSession = None, + ) -> ssl.SSLObject: + if session is not None: + raise ValueError("expected session to be None") + return self.context.wrap_bio( + incoming=incoming, + outgoing=outgoing, + server_hostname=server_hostname, + server_side=server_side, + session=self.session, + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/errors.py new/aioftp-0.25.1/src/aioftp/errors.py --- old/aioftp-0.24.1/src/aioftp/errors.py 2024-12-13 13:22:27.000000000 +0100 +++ new/aioftp-0.25.1/src/aioftp/errors.py 2025-04-11 20:53:44.000000000 +0200 @@ -43,7 +43,7 @@ def __init__(self, expected_codes, received_codes, info): super().__init__( - f"Waiting for {expected_codes} but got " f"{received_codes} {info!r}", + f"Waiting for {expected_codes} but got {received_codes} {info!r}", ) self.expected_codes = common.wrap_with_container(expected_codes) self.received_codes = common.wrap_with_container(received_codes) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/pathio.py new/aioftp-0.25.1/src/aioftp/pathio.py --- old/aioftp-0.24.1/src/aioftp/pathio.py 2024-12-13 13:22:27.000000000 +0100 +++ new/aioftp-0.25.1/src/aioftp/pathio.py 2025-04-11 20:53:44.000000000 +0200 @@ -117,7 +117,7 @@ async def wrapper(self, file, *args, **kwargs): if isinstance(file, AsyncPathIOContext): raise ValueError( - "Native path io file methods can not be used " "with wrapped file object", + "Native path io file methods can not be used with wrapped file object", ) return await coro(self, file, *args, **kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/server.py new/aioftp-0.25.1/src/aioftp/server.py --- old/aioftp-0.24.1/src/aioftp/server.py 2024-12-13 13:22:27.000000000 +0100 +++ new/aioftp-0.25.1/src/aioftp/server.py 2025-04-11 20:53:44.000000000 +0200 @@ -72,7 +72,7 @@ return False def __repr__(self): - return f"{self.__class__.__name__}({self.path!r}, " f"readable={self.readable!r}, writable={self.writable!r})" + return f"{self.__class__.__name__}({self.path!r}, readable={self.readable!r}, writable={self.writable!r})" class User: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp.egg-info/PKG-INFO new/aioftp-0.25.1/src/aioftp.egg-info/PKG-INFO --- old/aioftp-0.24.1/src/aioftp.egg-info/PKG-INFO 2024-12-13 13:22:30.000000000 +0100 +++ new/aioftp-0.25.1/src/aioftp.egg-info/PKG-INFO 2025-04-11 20:53:50.000000000 +0200 @@ -1,9 +1,9 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: aioftp -Version: 0.24.1 +Version: 0.25.1 Summary: ftp client/server for asyncio Author: yieyu, rsichnyi, jw4js, oleksandr-kuzmenko, ndhansen, modelmat, greut, PonyPC, jacobtomlinson, bachya, CrafterKolyan, jkr78 -Author-email: pohmelie <multisosnoo...@gmail.com>, asvetlov <andrew.svet...@gmail.com>, decaz <deca...@gmail.com>, janneronkko <janne.ron...@iki.fi>, thirtyseven <t...@shlashdot.org>, ported-pw <cont...@ported.pw>, Olegt0rr <t...@mail.ru>, michalc <mic...@charemza.name>, ch3pjw <p...@concertdaw.co.uk>, puddly <pudd...@gmail.com>, AMDmi3 <amd...@amdmi3.ru>, webknjaz <webknjaz+github/prof...@redhat.com>, rcfox <r...@rcfox.ca> +Author-email: pohmelie <multisosnoo...@gmail.com>, asvetlov <andrew.svet...@gmail.com>, decaz <deca...@gmail.com>, janneronkko <janne.ron...@iki.fi>, thirtyseven <t...@shlashdot.org>, ported-pw <cont...@ported.pw>, Olegt0rr <t...@mail.ru>, michalc <mic...@charemza.name>, ch3pjw <p...@concertdaw.co.uk>, puddly <pudd...@gmail.com>, AMDmi3 <amd...@amdmi3.ru>, webknjaz <webknjaz+github/prof...@redhat.com>, rcfox <r...@rcfox.ca>, bellini666 <thi...@bellini.dev> License: Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -220,6 +220,7 @@ Requires-Dist: async_timeout>=4.0.0; extra == "dev" Requires-Dist: pytest-asyncio; extra == "dev" Requires-Dist: pytest-cov; extra == "dev" +Requires-Dist: pytest-mock; extra == "dev" Requires-Dist: pytest; extra == "dev" Requires-Dist: siosocks; extra == "dev" Requires-Dist: trustme; extra == "dev" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp.egg-info/SOURCES.txt new/aioftp-0.25.1/src/aioftp.egg-info/SOURCES.txt --- old/aioftp-0.24.1/src/aioftp.egg-info/SOURCES.txt 2024-12-13 13:22:30.000000000 +0100 +++ new/aioftp-0.25.1/src/aioftp.egg-info/SOURCES.txt 2025-04-11 20:53:50.000000000 +0200 @@ -33,4 +33,5 @@ tests/test_restart.py tests/test_simple_functions.py tests/test_throttle.py +tests/test_tls.py tests/test_user.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp.egg-info/requires.txt new/aioftp-0.25.1/src/aioftp.egg-info/requires.txt --- old/aioftp-0.24.1/src/aioftp.egg-info/requires.txt 2024-12-13 13:22:30.000000000 +0100 +++ new/aioftp-0.25.1/src/aioftp.egg-info/requires.txt 2025-04-11 20:53:50.000000000 +0200 @@ -3,6 +3,7 @@ async_timeout>=4.0.0 pytest-asyncio pytest-cov +pytest-mock pytest siosocks trustme diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/tests/conftest.py new/aioftp-0.25.1/tests/conftest.py --- old/aioftp-0.24.1/tests/conftest.py 2024-12-13 13:22:27.000000000 +0100 +++ new/aioftp-0.25.1/tests/conftest.py 2025-04-11 20:53:44.000000000 +0200 @@ -192,7 +192,7 @@ ok = math.isclose(self.delay, delay, rel_tol=rel_tol, abs_tol=abs_tol) if not ok: print( - f"latest sleep: {self.delay}; expected delay: " f"{delay}; rel: {rel_tol}", + f"latest sleep: {self.delay}; expected delay: {delay}; rel: {rel_tol}", ) return ok diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aioftp-0.24.1/tests/test_tls.py new/aioftp-0.25.1/tests/test_tls.py --- old/aioftp-0.24.1/tests/test_tls.py 1970-01-01 01:00:00.000000000 +0100 +++ new/aioftp-0.25.1/tests/test_tls.py 2025-04-11 20:53:44.000000000 +0200 @@ -0,0 +1,113 @@ +import sys + +import pytest + +if sys.version_info < (3, 11): + pytest.skip(reason="required python 3.11+", allow_module_level=True) + + +async def _auth_response(connection, rest): + connection.response("234", ":P") + return True + + +async def _ok_response(connection, rest): + connection.response("200", ":P") + return True + + +@pytest.mark.asyncio +async def test_upgrade_to_tls(mocker, pair_factory): + ssl_context = object() + create_default_context = mocker.patch("aioftp.client.ssl.create_default_context", return_value=ssl_context) + + async with pair_factory(logged=False, do_quit=False) as pair: + pair.server.commands_mapping["auth"] = _auth_response + + start_tls = mocker.patch.object(pair.client.stream, "start_tls") + command_spy = mocker.spy(pair.client, "command") + + await pair.client.upgrade_to_tls() + + create_default_context.assert_called_once_with() + start_tls.assert_called_once_with(sslcontext=ssl_context, server_hostname=pair.client.server_host) + command_spy.assert_called_once_with("AUTH TLS", "234") + + +@pytest.mark.asyncio +async def test_upgrade_to_tls_custom_ssl_context(mocker, pair_factory): + ssl_context = object() + create_default_context = mocker.patch("aioftp.client.ssl.create_default_context") + + async with pair_factory(logged=False, do_quit=False) as pair: + pair.server.commands_mapping["auth"] = _auth_response + + start_tls = mocker.patch.object(pair.client.stream, "start_tls") + command_spy = mocker.spy(pair.client, "command") + + await pair.client.upgrade_to_tls(sslcontext=ssl_context) + + create_default_context.assert_not_called() + start_tls.assert_called_once_with(sslcontext=ssl_context, server_hostname=pair.client.server_host) + command_spy.assert_called_once_with("AUTH TLS", "234") + + +@pytest.mark.asyncio +async def test_upgrade_to_tls_does_nothing_when_already_updated(mocker, pair_factory): + mocker.patch("aioftp.client.ssl.create_default_context") + + async with pair_factory(logged=False, do_quit=False) as pair: + pair.client._upgraded_to_tls = True + + start_tls = mocker.patch.object(pair.client.stream, "start_tls") + command_spy = mocker.spy(pair.client, "command") + + await pair.client.upgrade_to_tls() + + start_tls.assert_not_called() + command_spy.assert_not_called() + + +@pytest.mark.asyncio +async def test_upgrade_to_tls_when_logged_in(mocker, pair_factory): + mocker.patch("aioftp.client.ssl.create_default_context") + + async with pair_factory(logged=False, do_quit=False) as pair: + pair.server.commands_mapping["auth"] = _auth_response + pair.server.commands_mapping["pbsz"] = _ok_response + pair.server.commands_mapping["prot"] = _ok_response + pair.client._logged_in = True + + mocker.patch.object(pair.client.stream, "start_tls") + command_spy = mocker.spy(pair.client, "command") + + await pair.client.upgrade_to_tls() + + command_spy.assert_has_calls( + [ + mocker.call("AUTH TLS", "234"), + mocker.call("PBSZ 0", "200"), + mocker.call("PROT P", "200"), + ], + ) + + +@pytest.mark.asyncio +async def test_login_should_send_tls_protection_when_upgraded(mocker, pair_factory): + mocker.patch("aioftp.client.ssl.create_default_context") + + async with pair_factory(logged=False, do_quit=False) as pair: + pair.server.commands_mapping["pbsz"] = _ok_response + pair.server.commands_mapping["prot"] = _ok_response + pair.client._upgraded_to_tls = True + + command_spy = mocker.spy(pair.client, "command") + + await pair.client.login("foo", "bar") + + command_spy.assert_has_calls( + [ + mocker.call("PBSZ 0", "200"), + mocker.call("PROT P", "200"), + ], + )