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-02-04 20:24:44
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-smbprotocol (Old)
 and      /work/SRC/openSUSE:Factory/.python-smbprotocol.new.28504 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-smbprotocol"

Thu Feb  4 20:24:44 2021 rev:9 rq:869527 version:1.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-smbprotocol/python-smbprotocol.changes    
2020-10-29 14:52:51.929256519 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-smbprotocol.new.28504/python-smbprotocol.changes
 2021-02-04 20:25:13.918932027 +0100
@@ -1,0 +2,42 @@
+Wed Feb  3 22:22:15 UTC 2021 - Martin Hauke <mar...@gmx.de>
+
+- Update to version 1.4.0
+  * Fixed up secure negotiation logic when connecting to older
+    SMB dialects.
+  * Will attempt to perform secure negotiation even on older
+    dialects that may not implement it properly.
+  * Added `ClientConfig` option `require_secure_negotiate` to
+    globally turn off secure negotiation if the client wishes.
+  * Fix explicit `ntlm` or `kerberos` authentication when the
+    server response with the initial SPNEGO mech list token.
+
+-------------------------------------------------------------------
+Thu Jan 28 21:52:39 UTC 2021 - Martin Hauke <mar...@gmx.de>
+
+- Update to version 1.3.0
+  * Changed initial credit request from 256 to 64 when creating
+    the SMB session.
+    + This is done to avoid overloading the SMB server.
+    + If smbclient requires more credits to perform an operation
+      it will request it automatically.
+  * Improve credit handling when reading and writing large amounts
+    of data to reduce the number of requests being made.
+  * Fixed up write() in smbclient.open_file() to be able to write
+    bytes greater than the max_write_size.
+  * Fixed issue when receiving an unknown NtStatus error code from
+    the server.
+  * Added PipeBusy exception for STATUS_PIPE_NOT_AVAILABLE
+    0xC00000AD error responses.
+  * Fix credit granting calculation when receiving a compound
+    response.
+    + Original logic granted len(responses) - 1 credits than what
+      the server actually given causing errors when the client ran
+      out of credits without it knowing.
+  * Added auth_protocol to Session, ClientConfig, and
+    register_session() to control what authentication protocol is
+    used.
+    + This can be negotiate (default), kerberos, or ntlm where
+      negotiate selects kerberos or ntlm depending on what's
+      available.
+
+-------------------------------------------------------------------

Old:
----
  python-smbprotocol-1.2.0.tar.gz

New:
----
  python-smbprotocol-1.4.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-smbprotocol.spec ++++++
--- /var/tmp/diff_new_pack.0U5spy/_old  2021-02-04 20:25:14.526932953 +0100
+++ /var/tmp/diff_new_pack.0U5spy/_new  2021-02-04 20:25:14.526932953 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-smbprotocol
 #
-# Copyright (c) 2020 SUSE LLC
+# Copyright (c) 2021 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-smbprotocol
-Version:        1.2.0
+Version:        1.4.0
 Release:        0
 Summary:        SMBv2/v3 client for Python 2 and 3
 License:        MIT

++++++ python-smbprotocol-1.2.0.tar.gz -> python-smbprotocol-1.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/CHANGELOG.md 
new/smbprotocol-1.4.0/CHANGELOG.md
--- old/smbprotocol-1.2.0/CHANGELOG.md  2020-09-22 07:03:27.000000000 +0200
+++ new/smbprotocol-1.4.0/CHANGELOG.md  2021-02-01 23:54:01.000000000 +0100
@@ -1,5 +1,28 @@
 # Changelog
 
+## 1.4.0 - 2021-02-02
+
+* Fixed up secure negotiation logic when connecting to older SMB dialects
+* Will attempt to perform secure negotiation even on older dialects that may 
not implement it properly
+* Added `ClientConfig` option `require_secure_negotiate` to globally turn off 
secure negotiation if the client wishes
+* Fix explicit `ntlm` or `kerberos` authentication when the server response 
with the initial SPNEGO mech list token
+
+
+## 1.3.0 - 2021-01-23
+
+* Changed initial credit request from `256` to `64` when creating the SMB 
session
+    * This is done to avoid overloading the SMB server
+    * If `smbclient` requires more credits to perform an operation it will 
request it automatically
+* Improve credit handling when reading and writing large amounts of data to 
reduce the number of requests being made
+* Fixed up `write()` in `smbclient.open_file()` to be able to write bytes 
greater than the `max_write_size`
+* Fixed issue when receiving an unknown NtStatus error code from the server
+* Added `PipeBusy` exception for `STATUS_PIPE_NOT_AVAILABLE 0xC00000AD` error 
responses
+* Fix credit granting calculation when receiving a compound response
+    * Original logic granted `len(responses) - 1` credits than what the server 
actually given causing errors when the client ran out of credits without it 
knowing
+* Added `auth_protocol` to `Session`, `ClientConfig`, and `register_session()` 
to control what authentication protocol is used
+    * This can be `negotiate` (default), `kerberos`, or `ntlm` where 
`negotiate` selects `kerberos` or `ntlm` depending on what's available
+
+
 ## 1.2.0 - 2020-09-22
 
 * Added experimental support for DFS shares when using `smbclient` function
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/README.md 
new/smbprotocol-1.4.0/README.md
--- old/smbprotocol-1.2.0/README.md     2020-09-22 07:03:27.000000000 +0200
+++ new/smbprotocol-1.4.0/README.md     2021-02-01 23:54:01.000000000 +0100
@@ -158,7 +158,10 @@
 * `username`: The default username to use when creating a new SMB session if 
explicit credentials weren't set
 * `password`: The default password to use for authentication
 * `domain_controller`: The domain controller hostname. This is useful for 
environments with DFS servers as it is used to identify the DFS domain 
information automatically
-* `skip_dfs`: Whether to skip doing any DFS resolution, useful if there is a 
bug or you don't want to waste any roundtrip requesting referrals 
+* `skip_dfs`: Whether to skip doing any DFS resolution, useful if there is a 
bug or you don't want to waste any roundtrip requesting referrals
+* `auth_protocol`: The authentication protocol to use; `negotiate` (default), 
`kerberos`, or `ntlm`
+* `require_secure_negotiate`: Control whether the client validates the 
negotiation info when connecting to a share (default: `True`).
+    * More information can be found on [SMB3 Secure Dialect 
Negotiation](https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb3-secure-dialect-negotiation)
 
 As well as setting the default credentials on the `ClientConfig` you can also
 specify the credentials and other connection parameters on each `smbclient`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/setup.py 
new/smbprotocol-1.4.0/setup.py
--- old/smbprotocol-1.2.0/setup.py      2020-09-22 07:03:27.000000000 +0200
+++ new/smbprotocol-1.4.0/setup.py      2021-02-01 23:54:01.000000000 +0100
@@ -18,7 +18,7 @@
 
 setup(
     name='smbprotocol',
-    version='1.2.0',
+    version='1.4.0',
     packages=['smbclient', 'smbprotocol'],
     install_requires=[
         'cryptography>=2.0',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/smbclient/_io.py 
new/smbprotocol-1.4.0/smbclient/_io.py
--- old/smbprotocol-1.2.0/smbclient/_io.py      2020-09-22 07:03:27.000000000 
+0200
+++ new/smbprotocol-1.4.0/smbclient/_io.py      2021-02-01 23:54:01.000000000 
+0100
@@ -4,6 +4,7 @@
 
 import io
 import logging
+import math
 
 from smbclient._pool import (
     ClientConfig,
@@ -34,7 +35,6 @@
 from smbprotocol.file_info import (
     FileAttributes,
     FileEndOfFileInformation,
-    FileStandardInformation,
 )
 
 from smbprotocol.open import (
@@ -110,6 +110,32 @@
     return create_disposition
 
 
+def _chunk_size(connection, length, operation):
+    """
+    Get the maximum size of data we can read/write. Also gets the number of 
credits to request to optimize subsequent
+    read/write operations for the remaining length.
+
+    :param connection: The SMB connection.
+    :param length: The length of the data we are working with.
+    :param operation: The operation the chunk is for: 'read', 'write', 
'transact'
+    :return: The size of the chunk we can use and the number of credits to 
request for the next operation.
+    """
+    max_size = getattr(connection, 'max_%s_size' % operation)
+
+    # Determine the maximum data length we can send for the operation. We do 
this by checking the available credits and
+    # calculating whatever is the smallest; length, negotiated operation size, 
available credit size).
+    available_credits = connection.sequence_window['high'] - 
connection.sequence_window['low']
+    chunk_size = min(length, max_size, available_credits * MAX_PAYLOAD_SIZE)
+
+    # Determine how many credits we need to fully optimize subsequent calls 
for the remaining amount of data. Basically
+    # how many credits we need to send either the remaining data or the max 
operation size.
+    remaining_length = min(max(0, length - chunk_size), max_size)
+    consumed_credits = math.ceil(chunk_size / MAX_PAYLOAD_SIZE)
+    credit_request = consumed_credits + math.ceil(remaining_length / 
MAX_PAYLOAD_SIZE)
+
+    return chunk_size, int(credit_request)
+
+
 def ioctl_request(transaction, ctl_code, output_size=0, 
flags=IOCTLFlags.SMB2_0_IOCTL_IS_IOCTL, input_buffer=b""):
     """
     Sends an IOCTL request to the server.
@@ -261,7 +287,7 @@
     _INVALID_MODE = ''
 
     def __init__(self, path, mode='r', share_access=None, desired_access=None, 
file_attributes=None,
-                 create_options=0, buffer_size=MAX_PAYLOAD_SIZE, **kwargs):
+                 create_options=0, **kwargs):
         tree, fd_path = get_smb_tree(path, **kwargs)
         self.share_access = share_access
         self.fd = Open(tree, fd_path)
@@ -269,7 +295,6 @@
         self._name = path
         self._offset = 0
         self._flush = False
-        self._buffer_size = buffer_size
         self.__kwargs = kwargs  # Used in open for DFS referrals
 
         if desired_access is None:
@@ -454,24 +479,29 @@
 
         :return: The byte string read from the SMB file.
         """
-        data = b""
-        remaining_bytes = self.fd.end_of_file - self._offset
-        while len(data) < remaining_bytes or self.FILE_TYPE == 'pipe':
-            try:
-                data_part = self.fd.read(self._offset, self._buffer_size)
-            except PipeBroken:
+        data = bytearray()
+        while True:
+            read_length = min(
+                # We always want to be reading a minimum of 64KiB.
+                max(self.fd.end_of_file - self._offset, MAX_PAYLOAD_SIZE),
+                self.fd.connection.max_read_size  # We can never read more 
than this.
+            )
+
+            buffer = bytearray(b'\x00' * read_length)
+            bytes_read = self.readinto(buffer)
+            if not bytes_read:
                 break
 
-            data += data_part
-            if self.FILE_TYPE != 'pipe':
-                self._offset += len(data_part)
+            data += buffer[:bytes_read]
 
-        return data
+        return bytes(data)
 
     def readinto(self, b):
         """
         Read bytes into a pre-allocated, writable bytes-like object b, and
-        return the number of bytes read.
+        return the number of bytes read. This may read less bytes than
+        requested as it depends on the negotiated read size and SMB credits
+        available.
 
         :param b: bytes-like object to read the data into.
         :return: The number of bytes read.
@@ -479,9 +509,20 @@
         if self._offset >= self.fd.end_of_file and self.FILE_TYPE != 'pipe':
             return 0
 
+        chunk_size, credit_request = _chunk_size(self.fd.connection, len(b), 
'read')
+
+        read_msg, recv_func = self.fd.read(self._offset, chunk_size, 
send=False)
+        request = self.fd.connection.send(
+            read_msg,
+            sid=self.fd.tree_connect.session.session_id,
+            tid=self.fd.tree_connect.tree_connect_id,
+            credit_request=credit_request
+        )
+
         try:
-            file_bytes = self.fd.read(self._offset, len(b))
+            file_bytes = recv_func(request)
         except PipeBroken:
+            # A pipe will block until it returns the data available or was 
closed/broken.
             file_bytes = b""
 
         b[:len(file_bytes)] = file_bytes
@@ -496,23 +537,29 @@
         Write buffer b to file, return number of bytes written.
 
         Only makes one system call, so not all of the data may be written.
-        The number of bytes actually written is returned.
+        The number of bytes actually written is returned. This can be less than
+        the length of b as it depends on the underlying connection.
         """
-        if isinstance(b, memoryview):
-            b = b.tobytes()
+        chunk_size, credit_request = _chunk_size(self.fd.connection, len(b), 
'write')
 
-        with SMBFileTransaction(self) as transaction:
-            transaction += self.fd.write(b, offset=self._offset, send=False)
+        # Python 2 compat, can be removed and just use the else statement.
+        if isinstance(b, memoryview):
+            data = b[:chunk_size].tobytes()
+        else:
+            data = bytes(b[:chunk_size])
 
-            # Send the request with an SMB2QueryInfoRequest for 
FileStandardInformation so we can update the end of
-            # file stored internally.
-            if self.FILE_TYPE != 'pipe':
-                query_info(transaction, FileStandardInformation)
+        write_msg, recv_func = self.fd.write(data, offset=self._offset, 
send=False)
+        request = self.fd.connection.send(
+            write_msg,
+            sid=self.fd.tree_connect.session.session_id,
+            tid=self.fd.tree_connect.tree_connect_id,
+            credit_request=credit_request
+        )
+        bytes_written = recv_func(request)
 
-        bytes_written = transaction.results[0]
         if self.FILE_TYPE != 'pipe':
             self._offset += bytes_written
-            self.fd.end_of_file = 
transaction.results[1]['end_of_file'].get_value()
+            self.fd.end_of_file = max(self.fd.end_of_file, self._offset)
             self._flush = True
 
         return bytes_written
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/smbclient/_os.py 
new/smbprotocol-1.4.0/smbclient/_os.py
--- old/smbprotocol-1.2.0/smbclient/_os.py      2020-09-22 07:03:27.000000000 
+0200
+++ new/smbprotocol-1.4.0/smbclient/_os.py      2021-02-01 23:54:01.000000000 
+0100
@@ -359,11 +359,8 @@
         'pipe': SMBPipeIO,
     }[file_type]
 
-    # buffer_size for this is not the same as the buffering value. We choose 
the max between the input and
-    # MAX_PAYLOAD_SIZE (SMB2 payload size) to ensure a user can set a higher 
size but not limit single payload
-    # requests. This is only used readall() requests to the underlying open.
     raw_fd = file_class(path, mode=mode, share_access=share_access, 
desired_access=desired_access,
-                        file_attributes=file_attributes, 
buffer_size=max(buffering, MAX_PAYLOAD_SIZE), **kwargs)
+                        file_attributes=file_attributes, **kwargs)
     try:
         raw_fd.open()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/smbclient/_pool.py 
new/smbprotocol-1.4.0/smbclient/_pool.py
--- old/smbprotocol-1.2.0/smbclient/_pool.py    2020-09-22 07:03:27.000000000 
+0200
+++ new/smbprotocol-1.4.0/smbclient/_pool.py    2021-02-01 23:54:01.000000000 
+0100
@@ -83,14 +83,20 @@
         skip_dfs (bool): Whether to skip using any DFS referral checks and 
treat any path as a normal path. This is
             only useful if there are problems with the DFS resolver or you 
wish to avoid the extra round trip(s) the
             resolver requires.
+        auth_protocol (str): The protocol to use for authentication. Possible 
values are 'negotiate', 'ntlm' or
+            'kerberos'. Defaults to 'negotiate'.
+        require_secure_negotiate (bool): Whether to verify the negotiated 
dialects and capabilities on the connection
+            to a share to protect against MitM downgrade attacks..
     """
 
     def __init__(self, client_guid=None, username=None, password=None, 
domain_controller=None, skip_dfs=False,
-                 **kwargs):
+                 auth_protocol='negotiate', require_secure_negotiate=True, 
**kwargs):
         self.client_guid = client_guid or uuid.uuid4()
         self.username = username
         self.password = password
         self.skip_dfs = skip_dfs
+        self.auth_protocol = auth_protocol
+        self.require_secure_negotiate = require_secure_negotiate
         self._domain_controller = None  # type: Optional[str]
         self._domain_cache = []  # type: List[DomainEntry]
         self._referral_cache = []  # type: List[ReferralEntry]
@@ -206,7 +212,7 @@
     :param port: The port used for the server.
     :param connection_cache: Connection cache to be used with
     """
-    connection_key = "%s:%s" % (server, port)
+    connection_key = "%s:%s" % (server.lower(), port)
 
     if connection_cache is None:
         connection_cache = _SMB_CONNECTIONS
@@ -238,6 +244,7 @@
     client_config = ClientConfig()
     username = username or client_config.username
     password = password or client_config.password
+    auth_protocol = client_config.auth_protocol
 
     # In case we need to nest a call to get_smb_tree, preserve the kwargs here 
so it's easier to update them in case
     # new kwargs are added.
@@ -284,14 +291,15 @@
 
     server = path_split[0]
     session = register_session(server, username=username, password=password, 
port=port, encrypt=encrypt,
-                               connection_timeout=connection_timeout, 
connection_cache=connection_cache)
+                               connection_timeout=connection_timeout, 
connection_cache=connection_cache,
+                               auth_protocol=auth_protocol)
 
     share_path = "\\\\%s\\%s" % (server, path_split[1])
     tree = next((t for t in session.tree_connect_table.values() if 
t.share_name == share_path), None)
     if not tree:
         tree = TreeConnect(session, share_path)
         try:
-            tree.connect()
+            
tree.connect(require_secure_negotiate=client_config.require_secure_negotiate)
         except BadNetworkName:
             ipc_path = u"\\\\%s\\IPC$" % server
             if path == ipc_path:  # In case we already tried connecting to 
IPC$ but that failed.
@@ -311,7 +319,7 @@
 
 
 def register_session(server, username=None, password=None, port=445, 
encrypt=None, connection_timeout=60,
-                     connection_cache=None):
+                     connection_cache=None, auth_protocol='negotiate'):
     """
     Creates an active connection and session to the server specified. This can 
be manually called to register the
     credentials of a specific server instead of defining it on the first 
function connecting to the server. The opened
@@ -327,6 +335,8 @@
         back to False.
     :param connection_timeout: Override the timeout used for the initial 
connection.
     :param connection_cache: Connection cache to be used with
+    :param auth_protocol: The protocol to use for authentication. Possible 
values are 'negotiate', 'ntlm' or
+        'kerberos'. Defaults to 'negotiate'.
     :return: The Session that was registered or already existed in the pool.
     """
     connection_key = "%s:%s" % (server.lower(), port)
@@ -344,7 +354,8 @@
     # just use the first session found or fall back to creating a new one with 
implicit auth/kerberos.
     session = next((s for s in connection.session_table.values() if username 
is None or s.username == username), None)
     if not session:
-        session = Session(connection, username=username, password=password, 
require_encryption=(encrypt is True))
+        session = Session(connection, username=username, password=password, 
require_encryption=(encrypt is True),
+                          auth_protocol=auth_protocol)
         session.connect()
     elif encrypt is not None:
         # We cannot go from encryption to no encryption on an existing session 
but we can do the opposite.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/smbprotocol/connection.py 
new/smbprotocol-1.4.0/smbprotocol/connection.py
--- old/smbprotocol-1.2.0/smbprotocol/connection.py     2020-09-22 
07:03:27.000000000 +0200
+++ new/smbprotocol-1.4.0/smbprotocol/connection.py     2021-02-01 
23:54:01.000000000 +0100
@@ -635,7 +635,7 @@
         :param require_signing: Whether signing is required on SMB messages
             sent over this connection
         """
-        log.info("Initialising connection, guid: %s, require_singing: %s, "
+        log.info("Initialising connection, guid: %s, require_signing: %s, "
                  "server_name: %s, port: %d"
                  % (guid, require_signing, server_name, port))
         self.server_name = server_name
@@ -747,7 +747,10 @@
                     SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED):
             self.require_signing = True
         log.info("Connection require signing: %s" % self.require_signing)
+
         capabilities = smb_response['capabilities']
+        self.server_capabilities = capabilities
+        self.server_security_mode = smb_response['security_mode'].get_value()
 
         # SMB 2.1
         if self.dialect >= Dialects.SMB_2_1_0:
@@ -768,9 +771,6 @@
             self.supports_encryption = capabilities.has_flag(
                 Capabilities.SMB2_GLOBAL_CAP_ENCRYPTION) \
                 and self.dialect < Dialects.SMB_3_1_1
-            self.server_capabilities = capabilities
-            self.server_security_mode = \
-                smb_response['security_mode'].get_value()
 
             # TODO: Check/add server to server_list in Client Page 203
 
@@ -803,7 +803,8 @@
         log.info("Disconnecting transport connection")
         self.transport.close()
 
-    def send(self, message, sid=None, tid=None, credit_request=None, 
message_id=None, async_id=None):
+    def send(self, message, sid=None, tid=None, credit_request=None, 
message_id=None, async_id=None,
+             force_signature=False):
         """
         Will send a message to the server that is passed in. The final 
unencrypted header is returned to the function
         that called this.
@@ -814,10 +815,11 @@
         :param credit_request: Specifies extra credits to be requested with 
the SMB header.
         :param message_id: The message_id for the header, only useful for a 
cancel request.
         :param async_id: The async_id for the header, only useful for a cancel 
request.
+        :param force_signature: Force signing the SMB request even if not 
requested by the client/server.
         :return: Request of the message that was sent.
         """
         return self._send([message], session_id=sid, tree_id=tid, 
message_id=message_id, credit_request=credit_request,
-                          async_id=async_id)[0]
+                          async_id=async_id, 
force_signature=force_signature)[0]
 
     def send_compound(self, messages, sid, tid, related=False):
         """
@@ -1007,7 +1009,7 @@
 
     @_worker_running
     def _send(self, messages, session_id=None, tree_id=None, message_id=None, 
credit_request=None, related=False,
-              async_id=None):
+              async_id=None, force_signature=False):
         send_data = b""
         requests = []
         session = self.session_table.get(session_id, None)
@@ -1067,7 +1069,7 @@
                 header['tree_id'] = b"\xff" * 4
                 
header['flags'].set_flag(Smb2Flags.SMB2_FLAGS_RELATED_OPERATIONS)
 
-            if session and session.signing_required and session.signing_key:
+            if force_signature or (session and session.signing_required and 
session.signing_key):
                 header['flags'].set_flag(Smb2Flags.SMB2_FLAGS_SIGNED)
                 b_header = header.pack() + padding
                 signature = self._generate_signature(b_header, 
session.signing_key)
@@ -1160,8 +1162,14 @@
                         self.verify_signature(header, session_id)
 
                     credit_response = header['credit_response'].get_value()
+                    if credit_response == 0 and not self.supports_multi_credit:
+                        # If the dialect does not support credits we still 
need to adjust our sequence window.
+                        # Otherwise the credit response may be 0 in the case 
of compound responses and the last
+                        # response contains the credits that were granted.
+                        credit_response += 1
+
                     with self.sequence_lock:
-                        self.sequence_window['high'] += credit_response if 
credit_response > 0 else 1
+                        self.sequence_window['high'] += credit_response
 
                     command = header['command'].get_value()
                     status = header['status'].get_value()
@@ -1295,11 +1303,19 @@
                       % self.client_guid)
             neg_req['client_guid'] = self.client_guid
 
+        else:
+            # Must be None, this value is used to verify the negotiation info.
+            self.client_guid = None
+
         if highest_dialect >= Dialects.SMB_3_0_0:
             log.debug("Adding client capabilities %d to negotiate request"
                       % self.client_capabilities)
             neg_req['capabilities'] = self.client_capabilities
 
+        else:
+            # Must be 0, this value is used to verify the negotiation info.
+            self.client_capabilities = 0
+
         if highest_dialect >= Dialects.SMB_3_1_1:
             int_cap = SMB2NegotiateContextRequest()
             int_cap['context_type'] = \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/smbprotocol/exceptions.py 
new/smbprotocol-1.4.0/smbprotocol/exceptions.py
--- old/smbprotocol-1.2.0/smbprotocol/exceptions.py     2020-09-22 
07:03:27.000000000 +0200
+++ new/smbprotocol-1.4.0/smbprotocol/exceptions.py     2021-02-01 
23:54:01.000000000 +0100
@@ -186,7 +186,7 @@
 
     def __call__(cls, header=None):
         if header:
-            new_cls = cls.__registry[header['status'].get_value()]
+            new_cls = cls.__registry.get(header['status'].get_value(), cls)
 
         else:
             header = SMB2HeaderResponse()
@@ -347,6 +347,11 @@
     _STATUS_CODE = NtStatus.STATUS_NO_SUCH_FILE
 
 
+class InvalidDeviceRequest(SMBResponseException):
+    _BASE_MESSAGE = "The specified request is not a valid operation for the 
target device."
+    _STATUS_CODE = NtStatus.STATUS_INVALID_DEVICE_REQUEST
+
+
 class MoreProcessingRequired(SMBResponseException):
     _BASE_MESSAGE = "The specified I/O request packet (IRP) cannot be disposed 
of because the I/O operation is not " \
                     "complete."
@@ -444,6 +449,11 @@
     _STATUS_CODE = NtStatus.STATUS_INSUFFICIENT_RESOURCES
 
 
+class PipeNotAvailable(SMBResponseException):
+    _BASE_MESSAGE = "An instance of a named pipe cannot be found in the 
listening state."
+    _STATUS_CODE = NtStatus.STATUS_PIPE_NOT_AVAILABLE
+
+
 class PipeBusy(SMBResponseException):
     _BASE_MESSAGE = "The specified pipe is set to complete operations and 
there are current I/O operations queued " \
                     "so that it cannot be changed to queue operations."
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/smbprotocol/header.py 
new/smbprotocol-1.4.0/smbprotocol/header.py
--- old/smbprotocol-1.2.0/smbprotocol/header.py 2020-09-22 07:03:27.000000000 
+0200
+++ new/smbprotocol-1.4.0/smbprotocol/header.py 2021-02-01 23:54:01.000000000 
+0100
@@ -70,6 +70,7 @@
     STATUS_INFO_LENGTH_MISMATCH = 0xC0000004
     STATUS_INVALID_PARAMETER = 0xC000000D
     STATUS_NO_SUCH_FILE = 0xC000000F
+    STATUS_INVALID_DEVICE_REQUEST = 0xC0000010
     STATUS_MORE_PROCESSING_REQUIRED = 0xC0000016
     STATUS_ACCESS_DENIED = 0xC0000022
     STATUS_BUFFER_TOO_SMALL = 0xC0000023
@@ -89,6 +90,7 @@
     STATUS_LOGON_FAILURE = 0xC000006D
     STATUS_PASSWORD_EXPIRED = 0xC0000071
     STATUS_INSUFFICIENT_RESOURCES = 0xC000009A
+    STATUS_PIPE_NOT_AVAILABLE = 0xC00000AC
     STATUS_PIPE_BUSY = 0xC00000AE
     STATUS_PIPE_DISCONNECTED = 0xC00000B0
     STATUS_PIPE_CLOSING = 0xC00000B1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/smbprotocol/session.py 
new/smbprotocol-1.4.0/smbprotocol/session.py
--- old/smbprotocol-1.2.0/smbprotocol/session.py        2020-09-22 
07:03:27.000000000 +0200
+++ new/smbprotocol-1.4.0/smbprotocol/session.py        2021-02-01 
23:54:01.000000000 +0100
@@ -168,7 +168,7 @@
 class Session(object):
 
     def __init__(self, connection, username=None, password=None,
-                 require_encryption=True):
+                 require_encryption=True, auth_protocol='negotiate'):
         """
         [MS-SMB2] v53.0 2017-09-15
 
@@ -209,6 +209,9 @@
         :param require_encryption: Whether any messages sent over the session
             require encryption regardless of the server settings (Dialects 3+),
             needs to be set to False for older dialects.
+        :param auth_protocol: The protocol to use for authentication. Possible
+            values are 'negotiate', 'ntlm' or 'kerberos'. Defaults to
+            'negotiate'.
         """
         log.info("Initialising session with username: %s" % username)
         self._connected = False
@@ -229,6 +232,9 @@
         self.username = username
         self.password = password
 
+        # No need to validate this as the spnego library will raise a 
ValueError
+        self.auth_protocol = auth_protocol
+
         # Table of OpenFile, lookup by OpenFile.file_id
         self.open_table = {}
 
@@ -255,14 +261,16 @@
         log.debug("Decoding SPNEGO token containing supported auth mechanisms")
         try:
             context = spnego.client(self.username, self.password, 
service='cifs', hostname=self.connection.server_name,
-                                    
options=spnego.NegotiateOptions.session_key)
+                                    
options=spnego.NegotiateOptions.session_key, protocol=self.auth_protocol)
         except spnego.exceptions.SpnegoError as err:
             raise SMBAuthenticationError("Failed to authenticate with server: 
%s" % str(err.message))
 
         self.connection.preauth_session_table[self.session_id] = self
         in_token = self.connection.gss_negotiate_token
+        if self.auth_protocol != 'negotiate':
+            in_token = None  # The GSS Negotiate Token can only be used for 
Negotiate auth.
 
-        while not context.complete or not in_token:
+        while not context.complete or in_token:
             try:
                 out_token = context.step(in_token)
             except spnego.exceptions.SpnegoError as err:
@@ -277,7 +285,7 @@
             session_setup['buffer'] = out_token
 
             log.info("Sending SMB2_SESSION_SETUP request message")
-            request = self.connection.send(session_setup, sid=self.session_id, 
credit_request=256)
+            request = self.connection.send(session_setup, sid=self.session_id, 
credit_request=64)
 
             log.info("Receiving SMB2_SESSION_SETUP response message")
             try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/smbprotocol/tree.py 
new/smbprotocol-1.4.0/smbprotocol/tree.py
--- old/smbprotocol-1.2.0/smbprotocol/tree.py   2020-09-22 07:03:27.000000000 
+0200
+++ new/smbprotocol-1.4.0/smbprotocol/tree.py   2021-02-01 23:54:01.000000000 
+0100
@@ -13,6 +13,9 @@
 )
 
 from smbprotocol.exceptions import (
+    FileClosed,
+    InvalidDeviceRequest,
+    NotSupported,
     SMBException,
 )
 
@@ -266,9 +269,8 @@
             self.is_scaleout_share = capabilities.has_flag(
                 ShareCapabilities.SMB2_SHARE_CAP_SCALEOUT)
 
-            # secure negotiate is only valid for SMB 3 dialects before 3.1.1
-            if dialect < Dialects.SMB_3_1_1 and require_secure_negotiate:
-                self._verify_dialect_negotiate()
+        if require_secure_negotiate:
+            self._verify_dialect_negotiate()
 
     def disconnect(self):
         """
@@ -301,6 +303,18 @@
         log_header = "Session: %s, Tree: %s" \
                      % (self.session.username, self.share_name)
         log.info("%s - Running secure negotiate process" % log_header)
+
+        if not self.session.signing_key:
+            # This will only happen if we authenticated with the guest or 
anonymous user.
+            raise SMBException('Cannot verify negotiate information without a 
session signing key. Authenticate with '
+                               'a non-guest or anonymous account or set 
require_secure_negotiate=False to disable the '
+                               'negotiation info verification checks.')
+
+        dialect = self.session.connection.dialect
+        if dialect >= Dialects.SMB_3_1_1:
+            # SMB 3.1.1+ uses the negotiation info to generate the signing key 
so doesn't need this extra exchange.
+            return
+
         ioctl_request = SMB2IOCTLRequest()
         ioctl_request['ctl_code'] = \
             CtlCode.FSCTL_VALIDATE_NEGOTIATE_INFO
@@ -323,10 +337,24 @@
         log.debug(ioctl_request)
         request = self.session.connection.send(ioctl_request,
                                                sid=self.session.session_id,
-                                               tid=self.tree_connect_id)
+                                               tid=self.tree_connect_id,
+                                               force_signature=True)
 
         log.info("%s - Receiving secure negotiation response" % log_header)
-        response = self.session.connection.receive(request)
+        try:
+            response = self.session.connection.receive(request)
+
+        except (FileClosed, InvalidDeviceRequest, NotSupported) as e:
+            # 
https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb3-secure-dialect-negotiation
+            # Older dialects may respond with these exceptions, this is 
expected and we only want to fail if
+            # they are not signed. Check that header signature was signed, 
fail if it wasn't. The signature, if
+            # present, would have been verified when the connection received 
the data.
+            if e.header['signature'].get_value() == b'\x00' * 16:
+                raise
+
+            return
+
+        # If we received an actual response we want to validate the info 
provided matches with what was negotiated.
         ioctl_resp = SMB2IOCTLResponse()
         ioctl_resp.unpack(response['data'].get_value())
         log.debug(ioctl_resp)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/tests/conftest.py 
new/smbprotocol-1.4.0/tests/conftest.py
--- old/smbprotocol-1.2.0/tests/conftest.py     2020-09-22 07:03:27.000000000 
+0200
+++ new/smbprotocol-1.4.0/tests/conftest.py     2021-02-01 23:54:01.000000000 
+0100
@@ -7,6 +7,7 @@
 import time
 
 from smbclient import (
+    delete_session,
     mkdir,
 )
 
@@ -180,6 +181,7 @@
 def smb_share(request, smb_real):
     # Use some non ASCII chars to test out edge cases by default.
     share_path = u"%s\\%s" % (smb_real[request.param[1]], u"P??t??s???-[%s] 
????" % time.time())
+    delete_session(smb_real[2])
 
     # Test out forward slashes also work with the share-encrypted test
     if request.param[0] == 'share-encrypted':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/tests/test_connection.py 
new/smbprotocol-1.4.0/tests/test_connection.py
--- old/smbprotocol-1.2.0/tests/test_connection.py      2020-09-22 
07:03:27.000000000 +0200
+++ new/smbprotocol-1.4.0/tests/test_connection.py      2021-02-01 
23:54:01.000000000 +0100
@@ -745,7 +745,9 @@
                 SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED
 
             # server settings override the require signing
-            assert connection.server_security_mode is None
+            assert connection.server_security_mode & \
+                SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED == \
+                SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED
             assert not connection.supports_encryption
             assert connection.require_signing
         finally:
@@ -766,7 +768,9 @@
                 SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED
 
             # server settings override the require signing
-            assert connection.server_security_mode is None
+            assert connection.server_security_mode & \
+                SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED == \
+                SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED
             assert not connection.supports_encryption
             assert connection.require_signing
         finally:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/tests/test_session.py 
new/smbprotocol-1.4.0/tests/test_session.py
--- old/smbprotocol-1.2.0/tests/test_session.py 2020-09-22 07:03:27.000000000 
+0200
+++ new/smbprotocol-1.4.0/tests/test_session.py 2021-02-01 23:54:01.000000000 
+0100
@@ -327,3 +327,28 @@
             assert session.signing_required
         finally:
             connection.disconnect(True)
+
+    def test_setup_session_with_ntlm_only(self, smb_real):
+        connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3])
+        connection.connect()
+
+        session = Session(connection, smb_real[0], smb_real[1], False, 
auth_protocol='ntlm')
+        try:
+            session.connect()
+            assert len(session.application_key) == 16
+            assert session.application_key != session.session_key
+            assert len(session.decryption_key) == 16
+            assert session.decryption_key != session.session_key
+            assert not session.encrypt_data
+            assert len(session.encryption_key) == 16
+            assert session.encryption_key != session.session_key
+            assert len(session.connection.preauth_integrity_hash_value) == 2
+            assert len(session.preauth_integrity_hash_value) == 3
+            assert not session.require_encryption
+            assert session.session_id is not None
+            assert len(session.session_key) == 16
+            assert len(session.signing_key) == 16
+            assert session.signing_key != session.session_key
+            assert session.signing_required
+        finally:
+            connection.disconnect()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/tests/test_smbclient_os.py 
new/smbprotocol-1.4.0/tests/test_smbclient_os.py
--- old/smbprotocol-1.2.0/tests/test_smbclient_os.py    2020-09-22 
07:03:27.000000000 +0200
+++ new/smbprotocol-1.4.0/tests/test_smbclient_os.py    2021-02-01 
23:54:01.000000000 +0100
@@ -96,8 +96,6 @@
     with smbclient.open_file("%s\\file1" % smb_share, mode='w') as fd:
         fd.write(u"content" * 1024)
 
-    smbclient._os.CHUNK_SIZE = 1024
-
     smbclient.copyfile("%s\\file1" % smb_share, "%s\\dir2\\file1" % smb_share)
 
     src_stat = smbclient.stat("%s\\file1" % smb_share)
@@ -106,6 +104,28 @@
     assert src_stat.st_size == dst_stat.st_size
 
 
+def test_write_large_bytes(smb_share):
+    with smbclient.open_file("%s\\file1" % smb_share, mode='wb') as fd:
+        content = b"a" * (fd.raw.fd.connection.max_write_size + 1024)
+        assert isinstance(fd, io.BufferedWriter)
+        assert fd.write(content) == len(content)
+
+    assert smbclient.stat("%s\\file1" % smb_share).st_size == len(content)
+    with smbclient.open_file("%s\\file1" % smb_share, mode='rb') as fd:
+        assert fd.read() == content
+
+
+def test_write_large_text(smb_share):
+    with smbclient.open_file("%s\\file1" % smb_share, mode='w') as fd:
+        content = u"a" * (fd.buffer.raw.fd.connection.max_write_size + 1024)
+        assert isinstance(fd, io.TextIOWrapper)
+        assert fd.write(content) == len(content)
+
+    assert smbclient.stat("%s\\file1" % smb_share).st_size == len(content)
+    with smbclient.open_file("%s\\file1" % smb_share, mode='r') as fd:
+        assert fd.read() == content
+
+
 def test_server_side_copy_large_file(smb_share):
     src_filename = "%s\\file1" % smb_share
     dst_filename = "%s\\file2" % smb_share
@@ -1909,6 +1929,27 @@
     assert smbclient.listxattr(dst_filename, follow_symlinks=False) == []
 
 
+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
+
+    # Sending a compound message to make our client credit window out of wack. 
A stat calls has 5 requests in 1 which
+    # based on the older faulty logic added 4 extra credits we did not have. 
This should no longer be the case but we
+    # want to test this logic to ensure it doesn't regress in the future.
+    assert smbclient.stat(filename).st_size == 0
+
+    # Write data that should fit in the credits that we have available.
+    available_credits = connection.sequence_window['high'] - 
connection.sequence_window['low']
+    large_length = available_credits * 65536
+    with smbclient.open_file(filename, buffering=0, mode='wb') as fd:
+        fd.write(b'a' * large_length)
+
+    assert smbclient.stat(filename).st_size == large_length
+
+
 def test_dfs_path(smb_dfs_share):
     actual_listdir = smbclient.listdir(smb_dfs_share)
     assert actual_listdir == []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.2.0/tests/test_smbclient_shutil.py 
new/smbprotocol-1.4.0/tests/test_smbclient_shutil.py
--- old/smbprotocol-1.2.0/tests/test_smbclient_shutil.py        2020-09-22 
07:03:27.000000000 +0200
+++ new/smbprotocol-1.4.0/tests/test_smbclient_shutil.py        2021-02-01 
23:54:01.000000000 +0100
@@ -1207,15 +1207,17 @@
         copytree(src_dirname, dst_dirname, dirs_exist_ok=True)
 
     assert len(actual.value.args[0]) == 2
-    err1 = actual.value.args[0][0]
-    assert err1[0] == "%s\\dir1\\file2.txt" % src_dirname
-    assert err1[1] == "%s\\dir1\\file2.txt" % dst_dirname
-    assert "STATUS_ACCESS_DENIED" in err1[2]
+    for err in actual.value.args[0]:
+        # We cannot guarantee the order the SMB server will return the dir 
listing
+        if err[0].endswith('file1.txt'):
+            assert err[0] == "%s\\file1.txt" % src_dirname
+            assert err[1] == "%s\\file1.txt" % dst_dirname
 
-    err2 = actual.value.args[0][1]
-    assert err2[0] == "%s\\file1.txt" % src_dirname
-    assert err2[1] == "%s\\file1.txt" % dst_dirname
-    assert "STATUS_ACCESS_DENIED" in err2[2]
+        else:
+            assert err[0] == "%s\\dir1\\file2.txt" % src_dirname
+            assert err[1] == "%s\\dir1\\file2.txt" % dst_dirname
+
+        assert "STATUS_ACCESS_DENIED" in err[2]
 
 
 @pytest.mark.skipif(os.name != "nt" and not os.environ.get('SMB_FORCE', False),

Reply via email to