Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-smbprotocol for openSUSE:Factory checked in at 2021-03-25 14:53:02 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-smbprotocol (Old) and /work/SRC/openSUSE:Factory/.python-smbprotocol.new.2401 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-smbprotocol" Thu Mar 25 14:53:02 2021 rev:10 rq:881306 version:1.5.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-smbprotocol/python-smbprotocol.changes 2021-02-04 20:25:13.918932027 +0100 +++ /work/SRC/openSUSE:Factory/.python-smbprotocol.new.2401/python-smbprotocol.changes 2021-03-25 14:53:03.828530529 +0100 @@ -1,0 +2,24 @@ +Thu Mar 25 09:39:12 UTC 2021 - Martin Hauke <mar...@gmx.de> + +- Update to version 1.5.0 + * Added smbprotocol.exceptions.SMBConnectionClosed that is + raised when trying to send or receive data on a connection + that has been closed. + * Added smbprotocol.exceptions.WrongPassword that is raised when + some servers indicate the password is not correct or the + account is locked out. + * Do not attempt to reuse any cached connections that have been + closed in smbclient + * Added a lock when writing to the socket, only 1 thread can + write a message at a single point in time + * Revamped the SMB receiver code to simplify the logic and make + it more durable + + Removed the TCP recv thread for each connection, now each + connection uses just 1 thread instead of 2. + + Be more defensive when reading data from a socket to ensure + we get all the data we require. + + Handled server side FIN packets that close the connection + unexpectedly, any requests waiting for a response will + raise SMBConnectionClosed. + +------------------------------------------------------------------- Old: ---- python-smbprotocol-1.4.0.tar.gz New: ---- python-smbprotocol-1.5.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-smbprotocol.spec ++++++ --- /var/tmp/diff_new_pack.WBvXyN/_old 2021-03-25 14:53:04.280530988 +0100 +++ /var/tmp/diff_new_pack.WBvXyN/_new 2021-03-25 14:53:04.280530988 +0100 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-smbprotocol -Version: 1.4.0 +Version: 1.5.0 Release: 0 Summary: SMBv2/v3 client for Python 2 and 3 License: MIT ++++++ python-smbprotocol-1.4.0.tar.gz -> python-smbprotocol-1.5.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/CHANGELOG.md new/smbprotocol-1.5.0/CHANGELOG.md --- old/smbprotocol-1.4.0/CHANGELOG.md 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/CHANGELOG.md 2021-03-25 06:15:16.000000000 +0100 @@ -1,5 +1,17 @@ # Changelog +## 1.5.0 - 2021-03-25 + +* Added `smbprotocol.exceptions.SMBConnectionClosed` that is raised when trying to send or receive data on a connection that has been closed +* Added `smbprotocol.exceptions.WrongPassword` that is raised when some servers indicate the password is not correct or the account is locked out +* Do not attempt to reuse any cached connections that have been closed in `smbclient` +* Added a lock when writing to the socket, only 1 thread can write a message at a single point in time +* Revamped the SMB receiver code to simplify the logic and make it more durable + * Removed the TCP recv thread for each connection, now each connection uses just 1 thread instead of 2 + * Be more defensive when reading data from a socket to ensure we get all the data we require + * Handled server side FIN packets that close the connection unexpectedly, any requests waiting for a response will raise `SMBConnectionClosed` + + ## 1.4.0 - 2021-02-02 * Fixed up secure negotiation logic when connecting to older SMB dialects diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/azure-pipelines.yml new/smbprotocol-1.5.0/azure-pipelines.yml --- old/smbprotocol-1.4.0/azure-pipelines.yml 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/azure-pipelines.yml 2021-03-25 06:15:16.000000000 +0100 @@ -43,6 +43,8 @@ python.version: 3.7 Python38: python.version: 3.8 + Python39: + python.version: 3.9 steps: - task: UsePythonVersion@0 @@ -146,6 +148,12 @@ Python38-x64: python.version: 3.8 python.arch: x64 + Python39-x86: + python.version: 3.9 + python.arch: x86 + Python39-x64: + python.version: 3.9 + python.arch: x64 steps: - task: UsePythonVersion@0 @@ -231,7 +239,7 @@ steps: - task: UsePythonVersion@0 inputs: - versionSpec: 3.8 + versionSpec: 3.9 - script: | python -m pip install twine wheel -c tests/constraints.txt diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/setup.py new/smbprotocol-1.5.0/setup.py --- old/smbprotocol-1.4.0/setup.py 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/setup.py 2021-03-25 06:15:16.000000000 +0100 @@ -18,7 +18,7 @@ setup( name='smbprotocol', - version='1.4.0', + version='1.5.0', packages=['smbclient', 'smbprotocol'], install_requires=[ 'cryptography>=2.0', @@ -50,5 +50,6 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/smbclient/_pool.py new/smbprotocol-1.5.0/smbclient/_pool.py --- old/smbprotocol-1.4.0/smbclient/_pool.py 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/smbclient/_pool.py 2021-03-25 06:15:16.000000000 +0100 @@ -150,7 +150,7 @@ for referral in self._referral_cache: referral_path_components = [p for p in referral.dfs_path.split("\\") if p] for idx, referral_component in enumerate(referral_path_components): - if idx > len(path_components) or referral_component != path_components[idx]: + if idx >= len(path_components) or referral_component != path_components[idx]: break else: @@ -345,7 +345,8 @@ connection_cache = _SMB_CONNECTIONS connection = connection_cache.get(connection_key, None) - if not connection: + # Make sure we ignore any connections that may have had a closed connection + if not connection or not connection.transport.connected: connection = Connection(ClientConfig().client_guid, server, port) connection.connect(timeout=connection_timeout) connection_cache[connection_key] = connection diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/smbprotocol/_compat.py new/smbprotocol-1.5.0/smbprotocol/_compat.py --- old/smbprotocol-1.4.0/smbprotocol/_compat.py 1970-01-01 01:00:00.000000000 +0100 +++ new/smbprotocol-1.5.0/smbprotocol/_compat.py 2021-03-25 06:15:16.000000000 +0100 @@ -0,0 +1,27 @@ +# Copyright: (c) 2021, Jordan Borean (@jborean93) <jborea...@gmail.com> +# MIT License (see LICENSE or https://opensource.org/licenses/MIT) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type # noqa (fixes E402 for the imports below) + +import sys + + +# TODO: Remove once Python 2.7 is dropped, use 'raise Blah() from err' instead. +# Slightly modified from six.reraise to make calling it simpler and more like raise Excp() from err. +if sys.version_info[0] == 3: + def reraise(exc, inner=None): + exc.__cause__ = inner[1] if inner else sys.exc_info()[1] + raise exc + +else: + def _exec(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + frame = sys._getframe(1) + _globs_ = frame.f_globals + _locs_ = frame.f_locals + del frame + + exec("""exec _code_ in _globs_, _locs_""") + + _exec("def reraise(exc, inner=None):\n raise exc, None, inner[2] if inner else sys.exc_info()[2]") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/smbprotocol/connection.py new/smbprotocol-1.5.0/smbprotocol/connection.py --- old/smbprotocol-1.4.0/smbprotocol/connection.py 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/smbprotocol/connection.py 2021-03-25 06:15:16.000000000 +0100 @@ -54,6 +54,7 @@ ) from smbprotocol.exceptions import ( + SMBConnectionClosed, SMB2SymbolicLinkErrorResponse, SMBException, SMBResponseException, @@ -85,6 +86,7 @@ ) from smbprotocol.transport import ( + _TimeoutError, Tcp, ) @@ -610,14 +612,6 @@ super(SMB2TransformHeader, self).__init__() -def _worker_running(func): - """ Ensures the message worker thread is still running and hasn't failed for any reason. """ - def wrapped(self, *args, **kwargs): - self._check_worker_running() - return func(self, *args, **kwargs) - return wrapped - - class Connection(object): def __init__(self, guid, server_name, port=445, require_signing=True): @@ -724,9 +718,9 @@ negotiation process to complete """ log.info("Setting up transport connection") - message_queue = Queue() - self.transport = Tcp(self.server_name, self.port, message_queue, timeout) - t_worker = threading.Thread(target=self._process_message_thread, args=(message_queue,), + self.transport = Tcp(self.server_name, self.port, timeout) + self.transport.connect() + t_worker = threading.Thread(target=self._process_message_thread, name="msg_worker-%s:%s" % (self.server_name, self.port)) t_worker.daemon = True t_worker.start() @@ -796,7 +790,8 @@ :param close: Will close all sessions in the connection as well as the tree connections of each session. """ - if close: + # We cannot close the session or tree if the socket has been closed. + if close and self.transport.connected: for session in list(self.session_table.values()): session.disconnect(True) @@ -836,7 +831,6 @@ """ return self._send(messages, session_id=sid, tree_id=tid, related=related) - @_worker_running def receive(self, request, wait=True, timeout=None, resolve_symlinks=True): """ Polls the message buffer of the TCP connection and waits until a valid @@ -849,6 +843,9 @@ :param resolve_symlinks: Set to automatically resolve symlinks in the path when opening a file or directory. :return: SMB2HeaderResponse of the received message """ + # Make sure the receiver is still active, if not this raises an exception. + self._check_worker_running() + start_time = time.time() while True: iter_timeout = int(max(timeout - (time.time() - start_time), 1)) if timeout is not None else None @@ -1007,7 +1004,9 @@ self.disconnect(False) raise self._t_exc - @_worker_running + elif not self.transport.connected: + raise SMBConnectionClosed('SMB socket was closed, cannot send or receive any more data') + def _send(self, messages, session_id=None, tree_id=None, message_id=None, credit_request=None, related=False, async_id=None, force_signature=False): send_data = b"" @@ -1104,32 +1103,33 @@ if session and session.encrypt_data or tree and tree.encrypt_data: send_data = self._encrypt(send_data, session) + self._check_worker_running() self.transport.send(send_data) return requests - def _process_message_thread(self, msg_queue): - while True: - # Wait for a max of 10 minutes before sending an echo that tells the SMB server the client is still - # available. This stops the server from closing the connection and the associated sessions on a long lived - # connection. A brief test shows Windows kills a connection at ~16 minutes so 10 minutes is a safe choice. - # https://github.com/jborean93/smbprotocol/issues/31 - try: - b_msg = msg_queue.get(timeout=600) - except Empty: - log.debug("Sending SMB2 Echo to keep connection alive") - for sid in self.session_table.keys(): - req = self.send(SMB2Echo(), sid=sid) - # Set this reserved field to 1 as we use that internally to check whether the outstanding requests - # queue should be cleared in this thread or not. - req.message['reserved'] = 1 - - continue - - # The socket will put None in the queue if it is closed, signalling the end of the connection. - if b_msg is None: - return + def _process_message_thread(self): + try: + while True: + # Wait for a max of 10 minutes before sending an echo that tells the SMB server the client is still + # available. This stops the server from closing the connection and the associated sessions on a long + # lived connection. A brief test shows Windows kills a connection at ~16 minutes so 10 minutes is a + # safe choice. + # https://github.com/jborean93/smbprotocol/issues/31 + try: + b_msg = self.transport.recv(600) + except _TimeoutError: + log.debug("Sending SMB2 Echo to keep connection alive") + for sid in self.session_table.keys(): + req = self.send(SMB2Echo(), sid=sid) + # Set this reserved field to 1 as we use that internally to check whether the outstanding + # requests queue should be cleared in this thread or not. + req.message['reserved'] = 1 + continue + + # If recv didn't return any data then the socket is considered to be closed. + if not b_msg: + return - try: is_encrypted = b_msg[:4] == b"\xfdSMB" if is_encrypted: msg = SMB2TransformHeader() @@ -1190,18 +1190,18 @@ # request queue. if request.message['reserved'].get_value() == 1: del self.outstanding_requests[message_id] - except Exception as exc: - # The exception is raised in _check_worker_running by the main thread when send/receive is called next. - self._t_exc = exc - - # Make sure we fire all the request events to ensure the main thread isn't waiting on a receive. - for request in self.outstanding_requests.values(): - request.response_event.set() - - # While a caller of send/receive could theoretically catch this exception, we consider any failures - # here a fatal errors and the connection should be closed so we exit the worker thread. - self.disconnect(False) - return + except Exception as exc: + # The exception is raised in _check_worker_running by the main thread when send/receive is called next. + self._t_exc = exc + + # While a caller of send/receive could theoretically catch this exception, we consider any failures + # here a fatal errors and the connection should be closed so we exit the worker thread. + self.disconnect(False) + + finally: + # Make sure we fire all the request events to ensure the main thread isn't waiting on a receive. + for request in self.outstanding_requests.values(): + request.response_event.set() def _generate_signature(self, b_header, signing_key): b_header = b_header[:48] + (b"\x00" * 16) + b_header[64:] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/smbprotocol/exceptions.py new/smbprotocol-1.5.0/smbprotocol/exceptions.py --- old/smbprotocol-1.4.0/smbprotocol/exceptions.py 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/smbprotocol/exceptions.py 2021-03-25 06:15:16.000000000 +0100 @@ -52,6 +52,11 @@ pass +class SMBConnectionClosed(SMBException): + # Used to denote the underlying TCP transport has been closed. + pass + + class SMBOSError(OSError, SMBException): """Wrapper for OSError with smbprotocol specific details. @@ -433,6 +438,11 @@ _STATUS_CODE = NtStatus.STATUS_PRIVILEGE_NOT_HELD +class WrongPassword(SMBResponseException): + _BASE_MESSAGE = "The specified password is not correct or the user is locked out." + _STATUS_CODE = NtStatus.STATUS_WRONG_PASSWORD + + class LogonFailure(SMBResponseException): _BASE_MESSAGE = "The attempted logon is invalid. This is either due to a bad username or authentication " \ "information." diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/smbprotocol/header.py new/smbprotocol-1.5.0/smbprotocol/header.py --- old/smbprotocol-1.4.0/smbprotocol/header.py 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/smbprotocol/header.py 2021-03-25 06:15:16.000000000 +0100 @@ -87,6 +87,7 @@ STATUS_NO_EAS_ON_FILE = 0xC0000052 STATUS_EA_CORRUPT_ERROR = 0xC0000053 STATUS_PRIVILEGE_NOT_HELD = 0xC0000061 + STATUS_WRONG_PASSWORD = 0xC000006A STATUS_LOGON_FAILURE = 0xC000006D STATUS_PASSWORD_EXPIRED = 0xC0000071 STATUS_INSUFFICIENT_RESOURCES = 0xC000009A diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/smbprotocol/transport.py new/smbprotocol-1.5.0/smbprotocol/transport.py --- old/smbprotocol-1.4.0/smbprotocol/transport.py 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/smbprotocol/transport.py 2021-03-25 06:15:16.000000000 +0100 @@ -2,30 +2,36 @@ # Copyright: (c) 2019, Jordan Borean (@jborean93) <jborea...@gmail.com> # MIT License (see LICENSE or https://opensource.org/licenses/MIT) +import errno import logging import select import socket import struct import threading +import timeit from collections import ( OrderedDict, ) +from smbprotocol._compat import ( + reraise, +) + from smbprotocol.structure import ( BytesField, IntField, Structure, ) -try: - from queue import Queue -except ImportError: # pragma: no cover - from Queue import Queue - log = logging.getLogger(__name__) +# TODO: Replace with TimeoutError when Python 2.7 is dropped +class _TimeoutError(Exception): + pass + + class DirectTCPPacket(Structure): """ [MS-SMB2] v53.0 2017-09-15 @@ -49,50 +55,48 @@ super(DirectTCPPacket, self).__init__() -def socket_connect(func): - def wrapped(self, *args, **kwargs): - if not self._connected: - log.info("Connecting to DirectTcp socket") - try: - self._sock = socket.create_connection((self.server, self.port), timeout=self.timeout) - except (OSError, socket.gaierror) as err: - raise ValueError("Failed to connect to '%s:%s': %s" % (self.server, self.port, str(err))) - self._sock.settimeout(None) # Make sure the socket is in blocking mode. - - self._t_recv = threading.Thread(target=self.recv_thread, name="recv-%s:%s" % (self.server, self.port)) - self._t_recv.daemon = True - self._t_recv.start() - self._connected = True - - func(self, *args, **kwargs) - - return wrapped - - class Tcp(object): MAX_SIZE = 16777215 - def __init__(self, server, port, recv_queue, timeout=None): + def __init__(self, server, port, timeout=None): self.server = server self.port = port self.timeout = timeout - self._connected = False + self.connected = False self._sock = None - self._recv_queue = recv_queue - self._t_recv = None + self._sock_lock = threading.Lock() + self._close_lock = threading.Lock() + + def connect(self): + with self._sock_lock: + if not self.connected: + log.info("Connecting to DirectTcp socket") + try: + self._sock = socket.create_connection((self.server, self.port), timeout=self.timeout) + except (OSError, socket.gaierror) as err: + reraise(ValueError("Failed to connect to '%s:%s': %s" % (self.server, self.port, str(err)))) + self._sock.settimeout(None) # Make sure the socket is in blocking mode. + self.connected = True def close(self): - if self._connected: - log.info("Disconnecting DirectTcp socket") - # Send a shutdown to the socket so the select returns and wait until the thread is closed before actually - # closing the socket. - self._connected = False - self._sock.shutdown(socket.SHUT_RDWR) - self._t_recv.join() - self._sock.close() + with self._sock_lock: + if self.connected: + log.info("Disconnecting DirectTcp socket") + + # Sending shutdown first will tell the recv thread (for both select and recv) that the socket has data + # which returns b'' meaning it was closed. + self._sock.shutdown(socket.SHUT_RDWR) + + # This is even more special, we cannot close the socket if we are in the middle of a select or recv(). + # Doing so causes either a timeout (bad!) or bad fd descriptor (somewhat bad). By shutting down the + # socket first then waiting until recv() has exited the critical select/recv section we can ensure we + # gracefully handle client side socket closures both here and our recv thread without any extra waits + # or exceptions. + with self._close_lock: + self._sock.close() + self.connected = False - @socket_connect def send(self, header): b_msg = header data_length = len(b_msg) @@ -104,33 +108,58 @@ tcp_packet['smb2_message'] = b_msg data = tcp_packet.pack() - while data: - sent = self._sock.send(data) - data = data[sent:] - - def recv_thread(self): - try: - while True: - select.select([self._sock], [], []) - b_packet_size = self._sock.recv(4) - if b_packet_size == b"": - return - - packet_size = struct.unpack(">L", b_packet_size)[0] - b_data = bytearray() - bytes_read = 0 - while bytes_read < packet_size: - b_fragment = self._sock.recv(packet_size - bytes_read) - b_data.extend(b_fragment) - bytes_read += len(b_fragment) - - self._recv_queue.put(bytes(b_data)) - except Exception as e: - # Log a warning if the exception was raised while we were connected and not just some weird platform-ism - # exception when reading from a closed socket. - if self._connected: - log.warning("Uncaught exception in socket recv thread: %s" % e) - return - finally: - # Make sure we close the message processing thread in connection.py - self._recv_queue.put(None) + + with self._sock_lock: + while data: + sent = self._sock.send(data) + data = data[sent:] + + def recv(self, timeout): + # We don't need a lock for recv as the receiver is called from 1 thread. + b_packet_size, timeout = self._recv(4, timeout) + if not b_packet_size: + return b'' + + packet_size = struct.unpack(">L", b_packet_size)[0] + + return self._recv(packet_size, timeout)[0] + + def _recv(self, length, timeout): + buffer = bytearray(length) + offset = 0 + while offset < length: + read_len = length - offset + log.debug("Socket recv(%s) (total %s)" % (read_len, length)) + + start_time = timeit.default_timer() + + with self._close_lock: + if not self.connected: + return None, timeout # The socket was closed + + read = select.select([self._sock], [], [], max(timeout, 1))[0] + timeout = timeout - (timeit.default_timer() - start_time) + if not read: + log.debug("Socket recv(%s) timed out") + raise _TimeoutError() + + try: + b_data = self._sock.recv(read_len) + except socket.error as e: + # Windows will raise this error if the socket has been shutdown, Linux return returns an empty byte + # string so we just replicate that. + if e.errno not in [errno.ESHUTDOWN, errno.ECONNRESET]: + raise + b_data = b'' + + read_len = len(b_data) + log.debug("Socket recv() returned %s bytes (total %s)" % (read_len, length)) + + if read_len == 0: + self.close() + return None, timeout # The socket has been shutdown + + buffer[offset:offset + read_len] = b_data + offset += read_len + + return bytes(buffer), timeout diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/tests/test_connection.py new/smbprotocol-1.5.0/tests/test_connection.py --- old/smbprotocol-1.4.0/tests/test_connection.py 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/tests/test_connection.py 2021-03-25 06:15:16.000000000 +0100 @@ -47,6 +47,7 @@ ) from smbprotocol.exceptions import ( + SMBConnectionClosed, SMBException, ) @@ -54,6 +55,10 @@ Session, ) +from smbprotocol.transport import ( + _TimeoutError, +) + def test_valid_hash_algorithm(): expected = hashlib.sha512 @@ -886,7 +891,7 @@ finally: connection.disconnect() - def test_broken_message_worker(self, smb_real): + def test_broken_message_worker_closed_socket(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) connection.connect() try: @@ -894,19 +899,50 @@ test_req = Request(test_msg, type(test_msg), connection) connection.outstanding_requests[666] = test_req - # Put a bad message in the incoming queue to break the worker in a bad way - connection.transport._recv_queue.put(b"\x01\x02\x03\x04") - while connection._t_exc is None: - pass + # Close the connection manually + connection.transport.close() - with pytest.raises(Exception): - connection.send(SMB2NegotiateRequest()) + with pytest.raises(SMBConnectionClosed): + connection.receive(test_req) - # Verify that all outstanding request events have been set on a failure - assert test_req.response_event.is_set() + with pytest.raises(SMBConnectionClosed): + connection.send(SMB2NegotiateRequest()) finally: connection.disconnect() + def test_broken_message_worker_exception(self, mocker): + connection = Connection(uuid.uuid4(), 'server', 445, True) + + mock_transport = mocker.MagicMock() + connection.transport = mock_transport + connection.transport.recv.side_effect = Exception('test') + connection._process_message_thread() + + with pytest.raises(Exception, match='test'): + connection.send(SMB2Echo()) + + with pytest.raises(Exception, match='test'): + connection.receive(None) + + def test_message_worker_timeout(self, mocker): + connection = Connection(uuid.uuid4(), 'server', 445, True) + + connection.session_table[1] = mocker.MagicMock() + + mock_send = mocker.MagicMock() + connection.send = mock_send + + mock_transport = mocker.MagicMock() + connection.transport = mock_transport + connection.transport.recv.side_effect = (_TimeoutError, b'') + + # Not the best test but better than waiting 10 minutes for the socket to timeout. + connection._process_message_thread() + + assert mock_send.call_count == 1 + assert isinstance(mock_send.call_args[0][0], SMB2Echo) + assert mock_send.call_args[1] == {'sid': 1} + def test_verify_fail_no_session(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) connection.connect() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/tests/test_smbclient_os.py new/smbprotocol-1.5.0/tests/test_smbclient_os.py --- old/smbprotocol-1.4.0/tests/test_smbclient_os.py 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/tests/test_smbclient_os.py 2021-03-25 06:15:16.000000000 +0100 @@ -1931,7 +1931,7 @@ def test_credit_calculation_with_compound_requests(smb_share): filename = ntpath.join(smb_share, 'file.txt') - + connection = None with smbclient.open_file(filename, mode='wb') as fd: connection = fd.raw.fd.connection diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/smbprotocol-1.4.0/tests/test_transport.py new/smbprotocol-1.5.0/tests/test_transport.py --- old/smbprotocol-1.4.0/tests/test_transport.py 2021-02-01 23:54:01.000000000 +0100 +++ new/smbprotocol-1.5.0/tests/test_transport.py 2021-03-25 06:15:16.000000000 +0100 @@ -3,14 +3,43 @@ # MIT License (see LICENSE or https://opensource.org/licenses/MIT) import pytest + import re +import socket +import struct +import threading +import time from smbprotocol.transport import ( + _TimeoutError, DirectTCPPacket, Tcp, ) +@pytest.fixture() +def server_tcp(request): + server_func_name = 'server_' + request.node.name + server_func = globals().get(server_func_name) + if not server_func: + raise Exception('Test must have defined %s to run the server thread' % server_func_name) + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.bind(('127.0.0.1', 0)) + sock.listen(1) + + server_thread = threading.Thread(target=server_func, args=(sock,)) + server_thread.start() + try: + yield Tcp(*sock.getsockname()) + + finally: + server_thread.join() + finally: + sock.close() + + class TestDirectTcpPacket(object): def test_create_message(self): @@ -41,8 +70,8 @@ class TestTcp(object): def test_normal_fail_message_too_big(self): - tcp = Tcp("0.0.0.0", 0, None) - tcp._connected = True + tcp = Tcp("0.0.0.0", 0) + tcp.connected = True with pytest.raises(ValueError) as exc: tcp.send(b"\x00" * 16777216) assert str(exc.value) == "Data to be sent over Direct TCP size " \ @@ -50,6 +79,98 @@ "16777215" def test_invalid_host(self): - tcp = Tcp("fake-host", 445, None) + tcp = Tcp("fake-host", 445) with pytest.raises(ValueError, match=re.escape("Failed to connect to 'fake-host:445': ")): - tcp.send(b"") + tcp.connect() + + +def test_small_recv(server_tcp): + server_tcp.connect() + server_tcp.send(b'\x00') + + actual = server_tcp.recv(10) + + server_tcp.send(b'\x00') + + assert actual == b"\x01\x02\x03\x04" + + +def server_test_small_recv(server): + # I'm not sure how else to test this but it seems like the small sleeps is enough for the client recv() to read it + # in the chunks we send. + sock = server.accept()[0] + try: + sock.recv(5) + + b_len = struct.pack(">I", 4) + + sock.send(b_len[:2]) + time.sleep(0.1) + sock.send(b_len[2:]) + + sock.send(b'\x01\x02') + time.sleep(0.1) + sock.send(b'\x03\x04') + + sock.recv(5) + + finally: + sock.shutdown(socket.SHUT_RDWR) + sock.close() + + +def test_recv_timeout(server_tcp): + server_tcp.connect() + + with pytest.raises(_TimeoutError): + server_tcp.recv(1) + + server_tcp.send(b'\x00') + + +def server_test_recv_timeout(server): + sock = server.accept()[0] + try: + sock.recv(5) + + finally: + sock.shutdown(socket.SHUT_RDWR) + sock.close() + + +def test_recv_closed(server_tcp): + server_tcp.connect() + actual = server_tcp.recv(10) + assert actual == b'' + assert server_tcp.connected is False + + +def server_test_recv_closed(server): + sock = server.accept()[0] + try: + time.sleep(1) + + finally: + sock.shutdown(socket.SHUT_RDWR) + sock.close() + + +def test_recv_closed_client(server_tcp): + server_tcp.connect() + recv_res = [] + + def recv(): + recv_res.append(server_tcp.recv(5)) + client_recv_t = threading.Thread(target=recv) + client_recv_t.start() + + server_tcp.close() + client_recv_t.join() + assert recv_res == [b''] + + +def server_test_recv_closed_client(server): + sock = server.accept()[0] + sock.recv(1) + sock.shutdown(socket.SHUT_RDWR) + sock.close()