# -*- coding: utf-8 -*- # Copyright: (c) 2019, Jordan Borean (@jborean93) <jborean93@gmail.com> # MIT License (see LICENSE or https://opensource.org/licenses/MIT) import pytest import re import threading import uuid from smbprotocol._text import ( to_native, ) from smbprotocol.connection import ( Connection, ) from smbprotocol.exceptions import ( SMBResponseException, ) from smbprotocol.session import Session from smbprotocol.tree import TreeConnect from smbprotocol.change_notify import ( CompletionFilter, FileAction, FileNotifyInformation, FileSystemWatcher, SMB2ChangeNotifyRequest, SMB2ChangeNotifyResponse, ) from smbprotocol.open import ( CreateDisposition, CreateOptions, DirectoryAccessMask, FileAttributes, FilePipePrinterAccessMask, ImpersonationLevel, ShareAccess, Open ) class TestFileNotifyInformation(object): DATA = b"\x00\x00\x00\x00" \ b"\x01\x00\x00\x00" \ b"\x08\x00\x00\x00" \ b"\x63\x00\x61\x00\x66\x00\xe9\x00" def test_create_message(self): message = FileNotifyInformation() message['action'] = 1 message['file_name'] = u"café" actual = message.pack() assert len(message) == 20 assert actual == self.DATA assert str(message['file_name']) == to_native(u"café") def test_parse_message(self): actual = FileNotifyInformation() assert actual.unpack(self.DATA) == b"" assert len(actual) == 20 assert actual['next_entry_offset'].get_value() == 0 assert actual['action'].get_value() == 1 assert actual['file_name_length'].get_value() == 8 assert actual['file_name'].get_value() == u"café" class TestSMB2ChangeNotifyRequest(object): DATA = b"\x20\x00" \ b"\x00\x00" \ b"\x08\x00\x00\x00" \ b"\xff\xff\xff\xff\xff\xff\xff\xff" \ b"\xff\xff\xff\xff\xff\xff\xff\xff" \ b"\x01\x00\x00\x00" \ b"\x00\x00\x00\x00" def test_create_message(self): message = SMB2ChangeNotifyRequest() message['output_buffer_length'] = 8 message['file_id'] = b"\xff" * 16 message['completion_filter'] = 1 actual = message.pack() assert len(message) == 32 assert actual == self.DATA def test_parse_message(self): actual = SMB2ChangeNotifyRequest() assert actual.unpack(self.DATA) == b"" assert len(actual) == 32 assert actual['structure_size'].get_value() == 32 assert actual['flags'].get_value() == 0 assert actual['output_buffer_length'].get_value() == 8 assert actual['file_id'].get_value() == b"\xff" * 16 assert actual['completion_filter'].get_value() == 1 assert actual['reserved'].get_value() == 0 class TestSMB2ChangeNotifyResponse(object): DATA = b"\x09\x00" \ b"\x48\x00" \ b"\x04\x00\x00\x00" \ b"\x01\x02\x03\x04" def test_create_message(self): message = SMB2ChangeNotifyResponse() message['buffer'] = b"\x01\x02\x03\x04" actual = message.pack() assert len(message) == 12 assert actual == self.DATA def test_parse_message(self): actual = SMB2ChangeNotifyResponse() assert actual.unpack(self.DATA) == b"" assert len(actual) == 12 assert actual['structure_size'].get_value() == 9 assert actual['output_buffer_offset'].get_value() == 72 assert actual['output_buffer_length'].get_value() == 4 assert actual['buffer'].get_value() == b"\x01\x02\x03\x04" class TestChangeNotify(object): def _remove_file(self, tree, name): file_open = Open(tree, name) file_open.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.DELETE, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_DELETE_ON_CLOSE ), file_open.close() def test_change_notify_on_dir(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1]) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "directory-watch") try: session.connect() tree.connect() open.create(ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) self._remove_file(tree, "directory-watch\\created file") watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME) assert watcher.result is None assert watcher.response_event.is_set() is False # Run the wait in a separate thread so we can create the dir def watcher_wait(): watcher.wait() watcher_wait_thread = threading.Thread(target=watcher_wait) watcher_wait_thread.daemon = True watcher_wait_thread.start() def watcher_event(): watcher.response_event.wait() watcher_event_thread = threading.Thread(target=watcher_event) watcher_event_thread.daemon = True watcher_event_thread.start() # Create the new file file_open = Open(tree, "directory-watch\\created file") file_open.create(ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) file_open.close() watcher_wait_thread.join(timeout=2) watcher_event_thread.join(timeout=2) assert watcher_wait_thread.is_alive() is False assert watcher_event_thread.is_alive() is False assert watcher.response_event.is_set() assert len(watcher.result) == 1 assert watcher.result[0]['file_name'].get_value() == u"created file" assert watcher.result[0]['action'].get_value() == FileAction.FILE_ACTION_ADDED open.close() finally: connection.disconnect(True) def test_change_notify_on_dir_compound(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() # Cannot use encryption as Samba has a bug where the transform response has the wrong Session Id. Also there's # a special edge case of testing the Session Id of the plaintext response with signatures so don't use # encryption. # https://bugzilla.samba.org/show_bug.cgi?id=14189 session = Session(connection, smb_real[0], smb_real[1], require_encryption=False) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "directory-watch") try: session.connect() tree.connect() # Ensure the dir is clean of files. open.create(ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) self._remove_file(tree, "directory-watch\\created file") open.close() watcher = FileSystemWatcher(open) messages = [ open.create(ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE, send=False), watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME, send=False) ] assert watcher.result is None assert watcher.response_event.is_set() is False requests = connection.send_compound([m[0] for m in messages], sid=session.session_id, tid=tree.tree_connect_id, related=True) [messages[i][1](req) for i, req in enumerate(requests)] # Run the wait in a separate thread so we can create the dir def watcher_wait(): watcher.wait() watcher_wait_thread = threading.Thread(target=watcher_wait) watcher_wait_thread.daemon = True watcher_wait_thread.start() def watcher_event(): watcher.response_event.wait() watcher_event_thread = threading.Thread(target=watcher_event) watcher_event_thread.daemon = True watcher_event_thread.start() # Create the new file file_open = Open(tree, "directory-watch\\created file") file_open.create(ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) file_open.close() watcher_wait_thread.join(timeout=2) watcher_event_thread.join(timeout=2) assert watcher_wait_thread.is_alive() is False assert watcher_event_thread.is_alive() is False assert watcher.response_event.is_set() assert len(watcher.result) == 1 assert watcher.result[0]['file_name'].get_value() == u"created file" assert watcher.result[0]['action'].get_value() == FileAction.FILE_ACTION_ADDED open.close() finally: connection.disconnect(True) def test_change_notify_no_data(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1]) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "directory-watch") try: session.connect() tree.connect() open.create(ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) self._remove_file(tree, "directory-watch\\created file") watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME, output_buffer_length=0) assert watcher.result is None assert watcher.response_event.is_set() is False # Run the wait in a separate thread so we can create the dir def watcher_wait(): watcher.wait() watcher_wait_thread = threading.Thread(target=watcher_wait) watcher_wait_thread.daemon = True watcher_wait_thread.start() def watcher_event(): watcher.response_event.wait() watcher_event_thread = threading.Thread(target=watcher_event) watcher_event_thread.daemon = True watcher_event_thread.start() # Create the new file file_open = Open(tree, "directory-watch\\created file") file_open.create(ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) file_open.close() watcher_wait_thread.join(timeout=2) watcher_event_thread.join(timeout=2) assert watcher_wait_thread.is_alive() is False assert watcher_event_thread.is_alive() is False assert watcher.response_event.is_set() assert watcher.result == [] open.close() finally: connection.disconnect(True) def test_change_notify_underlying_close(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1]) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "directory-watch") try: session.connect() tree.connect() open.create(ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME) assert watcher.result is None assert watcher.response_event.is_set() is False open.close() expected = "Received unexpected status from the server: (267) STATUS_NOTIFY_CLEANUP" with pytest.raises(SMBResponseException, match=re.escape(expected)): watcher.wait() finally: connection.disconnect(True) def test_change_notify_cancel(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1], require_encryption=False) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "directory-watch") try: session.connect() tree.connect() open.create(ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME) assert watcher.result is None assert watcher.response_event.is_set() is False # Makes sure that we cancel after the async response has been returned from the server. while watcher._request.async_id is None: pass assert watcher.result is None watcher.cancel() watcher.wait() assert watcher.cancelled is True assert watcher.result is None # Make sure it doesn't cause any weird errors when calling it again watcher.cancel() finally: connection.disconnect(True) def test_change_notify_on_a_file(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1]) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "file-watch.txt") try: session.connect() tree.connect() open.create(ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME) expected = "Received unexpected status from the server: (3221225485) STATUS_INVALID_PARAMETER" with pytest.raises(SMBResponseException, match=re.escape(expected)): watcher.wait() finally: connection.disconnect(True)