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