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()

Reply via email to