async def _tree_connect( self, share_name: str, server_address: str = '*') -> Tuple[int, ShareType]: if self.connection.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError tree_connect_response: TreeConnectResponse = await self.connection._obtain_response( request_message=TreeConnectRequest210( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_TREE_CONNECT, session_id=self.session_id, ), path=f'\\\\{server_address}\\{share_name}'), sign_key=self.session_key) tree_connect_object = TreeConnectObject( tree_connect_id=tree_connect_response.header.tree_id, session=self, is_dfs_share=tree_connect_response.share_capabilities.dfs, is_ca_share=tree_connect_response.share_capabilities. continuous_availability, share_name=share_name) self.tree_connect_id_to_tree_connect_object[ tree_connect_response.header.tree_id] = tree_connect_object self.share_name_to_tree_connect_object[ share_name] = tree_connect_object return tree_connect_response.header.tree_id, tree_connect_response.share_type
async def read_chunks(): num_bytes_remaining = file_size offset = 0 while num_bytes_remaining != 0: num_bytes_to_read = min( num_bytes_remaining, self.connection.negotiated_details.max_read_size) read_response: ReadResponse = await self.connection._obtain_response( request_message=ReadRequest210( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_READ, session_id=self.session_id, tree_id=tree_id, credit_charge=calculate_credit_charge( variable_payload_size=0, expected_maximum_response_size=file_size)), padding=Header.STRUCTURE_SIZE + (ReadResponse.STRUCTURE_SIZE - 1), length=num_bytes_to_read, offset=offset, file_id=file_id, minimum_count=0, remaining_bytes=0), sign_key=self.session_key) yield read_response.buffer num_bytes_remaining -= num_bytes_to_read offset += num_bytes_to_read
async def read_chunks(): data_remains = True offset = 0 while data_remains: read_response: ReadResponse = await self._obtain_response( request_message=ReadRequest210( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_READ, session_id=session.session_id, tree_id=tree_id, credit_charge=calculate_credit_charge( variable_payload_size=0, expected_maximum_response_size=file_size)), padding=Header.STRUCTURE_SIZE + (ReadResponse.STRUCTURE_SIZE - 1), length=file_size, offset=offset, file_id=file_id, minimum_count=0, remaining_bytes=0)) yield read_response.buffer data_remains = read_response.data_remaining_length != 0 offset += read_response.data_length
async def _tree_connect( self, share_name: str, session: SMBv2Session, server_address: Optional[Union[str, IPv4Address, IPv6Address]] = None ) -> Tuple[int, ShareType]: # TODO: "If ServerName is an empty string, the server MUST set it as "*" to indicate that the local server name # used." -- Does this mean that I don't need a server address!? tree_connect_response: TreeConnectResponse = await self._obtain_response( request_message=TreeConnectRequest210( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_TREE_CONNECT, session_id=session.session_id, ), path=f'\\\\{server_address or self._host_address}\\{share_name}' )) tree_connect_object = TreeConnectObject( tree_connect_id=tree_connect_response.header.tree_id, session=session, is_dfs_share=tree_connect_response.share_capabilities.dfs, is_ca_share=tree_connect_response.share_capabilities. continuous_availability, share_name=share_name) session.tree_connect_id_to_tree_connect_object[ tree_connect_response.header.tree_id] = tree_connect_object session.share_name_to_tree_connect_object[ share_name] = tree_connect_object return tree_connect_response.header.tree_id, tree_connect_response.share_type
async def query_directory( self, file_id: FileId, file_information_class: FileInformationClass, query_directory_flag: QueryDirectoryFlag, session: SMBv2Session, tree_id: int, file_name_pattern: str = '', file_index: int = 0, output_buffer_length: int = 256_000 ) -> List[Union[FileDirectoryInformation, FileIdFullDirectoryInformation]]: """ Obtain information about a directory in an SMB share. :param file_id: An identifier for the directory about which to obtain information. :param file_information_class: A specification of the type of information to obtain. :param query_directory_flag: A flag indicating how the operation is to be processed. :param session: An SMB session with access to the directory. :param tree_id: The tree id of the SMB share that stores the directory. :param file_name_pattern: A search pattern specifying which entries in the the directory to retrieve information about. :param file_index: The byte offset within the directory, indicating the position at which to resume the enumeration. :param output_buffer_length: The maximum number of bytes the server is allowed to return in the response. :return: A collection of information entries about the content of the directory. """ if self.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError query_directory_response: QueryDirectoryResponse = await self._obtain_response( request_message=QueryDirectoryRequest( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_QUERY_DIRECTORY, session_id=session.session_id, tree_id=tree_id, # TODO: Consider this value. credit_charge=64), file_information_class=file_information_class, flags=query_directory_flag, file_id=file_id, file_name=file_name_pattern, file_index=file_index, output_buffer_length=output_buffer_length)) if file_information_class is FileInformationClass.FileDirectoryInformation: return query_directory_response.file_directory_information() elif file_information_class is FileInformationClass.FileIdFullDirectoryInformation: return query_directory_response.file_id_full_directory_information( ) else: raise NotImplementedError
async def logoff(self) -> LogoffResponse: """ Terminate a session. :return: A `LOGOFF` response. """ if self.connection.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError return await self.connection._obtain_response( request_message=LogoffRequest(header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_LOGOFF, session_id=self.session_id)), sign_key=self.session_key)
async def logoff(self, session: SMBv2Session) -> None: """ Terminate a session. :param session: The session to be terminated. :return: None """ if self.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError await self._obtain_response(request_message=LogoffRequest( header=SMB210SyncRequestHeader(command=SMBv2Command.SMB2_LOGOFF, session_id=session.session_id)))
async def _create( self, path: Union[str, PureWindowsPath], tree_id: int, requested_oplock_level: OplockLevel = OplockLevel. SMB2_OPLOCK_LEVEL_BATCH, impersonation_level: ImpersonationLevel = ImpersonationLevel. IMPERSONATION, desired_access: Union[ FilePipePrinterAccessMask, DirectoryAccessMask] = FilePipePrinterAccessMask( file_read_data=True, file_read_ea=True, file_read_attributes=True), file_attributes: FileAttributes = FileAttributes(normal=True), share_access: ShareAccess = ShareAccess(read=True), create_disposition: CreateDisposition = CreateDisposition. FILE_OPEN, create_options: CreateOptions = CreateOptions( non_directory_file=True), create_context_list: CreateContextList = None) -> CreateResponse: create_context_list = create_context_list if create_context_list is not None else CreateContextList( ) if self.connection.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError create_response: CreateResponse = await self.connection._obtain_response( request_message=CreateRequest( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_CREATE, session_id=self.session_id, tree_id=tree_id, ), requested_oplock_level=requested_oplock_level, impersonation_level=impersonation_level, desired_access=desired_access, file_attributes=file_attributes, share_access=share_access, create_disposition=create_disposition, create_options=create_options, name=str(path), create_context_list=create_context_list), sign_key=self.session_key) # TODO: I need to add stuff to some connection table, don't I? # TODO: Consider what to return from this function. There is a lot of information in the response. return create_response
async def tree_disconnect(self, session: SMBv2Session, tree_id: int): """ Request that a tree connect is disconnected. :param session: An SMB session that has access to the tree connect. :param tree_id: The ID of the tree connect to be disconnected. :return: None """ if self.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError await self._obtain_response(request_message=TreeDisconnectRequest( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_TREE_DISCONNECT, session_id=session.session_id, tree_id=tree_id)))
async def tree_disconnect(self, tree_id: int) -> TreeDisconnectResponse: """ Request that a tree connect is disconnected. :param tree_id: The ID of the tree connect to be disconnected. :return: A `TREE_DISCONNECT` resposne. """ if self.connection.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError return await self.connection._obtain_response( request_message=TreeDisconnectRequest( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_TREE_DISCONNECT, session_id=self.session_id, tree_id=tree_id)), sign_key=self.session_key)
async def write( self, write_data: bytes, file_id: FileId, session: SMBv2Session, tree_id: int, offset: int = 0, remaining_bytes: int = 0, flags: WriteFlag = WriteFlag()) -> int: """ Write data to a file in an SMB share. :param write_data: The data to be written. :param file_id: An identifier of the file whose data is to be written. :param session: An SMB session with access to the file. :param tree_id: The tree id of the SMB share that stores the file. :param offset: The offset, in bytes, of where to write the data in the destination file. :param remaining_bytes: The number of subsequent bytes the client intends to write to the file after this operation completes. Not binding. :param flags: Flags indicating how to process the operation. :return: The number of bytes written. """ # TODO: Support more dialects. if self.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError write_response: WriteResponse = await self._obtain_response( request_message=WriteRequest210(header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_WRITE, session_id=session.session_id, tree_id=tree_id, credit_charge=calculate_credit_charge( variable_payload_size=0, expected_maximum_response_size=Header.STRUCTURE_SIZE + WriteResponse.STRUCTURE_SIZE)), write_data=write_data, offset=offset, file_id=file_id, remaining_bytes=remaining_bytes, flags=flags)) return write_response.count
async def close(self, tree_id: int, file_id: FileId) -> CloseResponse: """ Close an instance of a file opened with a CREATE request. :param tree_id: The tree id of the share in which the opened file resides. :param file_id: The file id of the file instance to be closed. :return: A `CLOSE` response. """ if self.connection.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError return await self.connection._obtain_response( request_message=CloseRequest(header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_CLOSE, session_id=self.session_id, tree_id=tree_id), flags=CloseFlag(), file_id=file_id), sign_key=self.session_key)
async def change_notify( self, file_id: FileId, session: SMBV2Session, tree_id: int, completion_filter_flag: Optional[CompletionFilterFlag] = None, watch_tree: bool = False) -> Awaitable[ChangeNotifyResponse]: """ Monitor a directory in an SMB share for changes and notify. Only one notification is sent per change notify request. The notification is sent asynchronously. :param file_id: An identifier for the directory to be monitored for changes. :param session: An SMB session with access to the directory to be monitored. :param tree_id: The tree ID of the share that stores the directory to be monitored. :param completion_filter_flag: A flag that specifies which type of changes to notify about. :param watch_tree: Whether to monitor the subdirectories of the directory. :return: A `Future` object that resolves to a `ChangeNotifyResponse` containing a notification. """ if completion_filter_flag is None: completion_filter_flag = CompletionFilterFlag() completion_filter_flag.set_all() if self.negotiated_details.dialect is not Dialect.SMB_2_1: raise NotImplementedError return await self._obtain_response( request_message=ChangeNotifyRequest( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_CHANGE_NOTIFY, session_id=session.session_id, tree_id=tree_id, # TODO: Arbitrary number. Reconsider. credit_charge=64), flags=ChangeNotifyFlag(watch_tree=watch_tree), file_id=file_id, completion_filter=completion_filter_flag), await_async_response=False)
async def negotiate(self) -> None: """ Negotiate the SMB dialect to be used. :return: None """ # TODO: In future, I want to support more dialects. negotiate_response: NegotiateResponse = await self._obtain_response( request_message=NegotiateRequest(header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_NEGOTIATE), dialects=(PREFERRED_DIALECT, ), client_guid=CLIENT_GUID, security_mode=SECURITY_MODE)) negotiated_details_base_kwargs = dict( dialect=negotiate_response.dialect_revision, require_signing=(bool(negotiate_response.security_mode is SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED) or REQUIRE_MESSAGE_SIGNING), server_guid=negotiate_response.server_guid, max_transact_size=negotiate_response.max_transact_size, max_read_size=negotiate_response.max_read_size, max_write_size=negotiate_response.max_write_size) negotiated_details_base_kwargs_2 = dict( supports_file_leasing=negotiate_response.capabilities.leasing, supports_multi_credit=negotiate_response.capabilities.large_mtu) negotiated_details_base_kwargs_3 = dict( supports_directory_leasing=negotiate_response.capabilities. directory_leasing, supports_multi_channel=negotiate_response.capabilities. multi_channel, supports_persistent_handles=negotiate_response.capabilities. persistent_handles, supports_encryption=negotiate_response.capabilities.encryption) if isinstance(negotiate_response, SMB202NegotiateResponse): self.negotiated_details = SMB202NegotiatedDetails( **negotiated_details_base_kwargs) elif isinstance(negotiate_response, SMB210NegotiateResponse): self.negotiated_details = SMB210NegotiatedDetails( **negotiated_details_base_kwargs, **negotiated_details_base_kwargs_2) elif isinstance(negotiate_response, SMB300NegotiateResponse): self.negotiated_details = SMB300NegotiatedDetails( **negotiated_details_base_kwargs, **negotiated_details_base_kwargs_2, **negotiated_details_base_kwargs_3) elif isinstance(negotiate_response, SMB302NegotiateResponse): self.negotiated_details = SMB302NegotiatedDetails( **negotiated_details_base_kwargs, **negotiated_details_base_kwargs_2, **negotiated_details_base_kwargs_3) elif isinstance(negotiate_response, SMB311NegotiateResponse): negotiated_context_map = dict( preauth_integrity_hash_id=None, # TODO: Not sure from where I would get this. preauth_integrity_hash_value=None, cipher_id=None, compression_ids=None) for negotiate_context in negotiate_response.negotiate_context_list: if isinstance(negotiate_context, PreauthIntegrityCapabilitiesContext): negotiated_context_map['preauth_integrity_hash_id'] = next( negotiate_context.hash_algorithms) elif isinstance(negotiate_context, EncryptionCapabilitiesContext): negotiated_context_map['cipher_id'] = next( negotiate_context.ciphers) elif isinstance(negotiate_context, CompressionCapabilitiesContext): negotiated_context_map['compression_ids'] = set( negotiate_context.compression_algorithms) elif isinstance(negotiate_context, NetnameNegotiateContextIdContext): # TODO: Do something with this. ... else: # TODO: Use a proper exception. raise ValueError self.negotiated_details = SMB311NegotiateDetails( **negotiated_details_base_kwargs, **negotiated_details_base_kwargs_2, **negotiated_details_base_kwargs_3, **negotiated_context_map) else: # TODO: Use proper exception. raise ValueError
async def negotiate( self, preferred_dialect: Dialect = Dialect.SMB_2_1, security_mode: SecurityMode = SecurityMode(signing_required=True) ) -> None: """ Negotiate the SMB configuration to be used. :return: None """ if preferred_dialect is not Dialect.SMB_2_1: raise NotImplementedError # TODO: In future, I want to support more dialects. negotiate_response: NegotiateResponse = await self._obtain_response( request_message=NegotiateRequest( header=SMB210SyncRequestHeader( command=SMBv2Command.SMB2_NEGOTIATE ), dialects=(preferred_dialect,), client_guid=self.client_guid, security_mode=security_mode ) ) # TODO: Check if the server is also accepting signing? negotiated_details_base_kwargs = dict( dialect=negotiate_response.dialect_revision, require_signing=negotiate_response.security_mode.signing_required, server_guid=negotiate_response.server_guid, max_transact_size=negotiate_response.max_transact_size, max_read_size=negotiate_response.max_read_size, max_write_size=negotiate_response.max_write_size ) negotiated_details_base_kwargs_2 = dict( supports_file_leasing=negotiate_response.capabilities.leasing, supports_multi_credit=negotiate_response.capabilities.large_mtu ) negotiated_details_base_kwargs_3 = dict( supports_directory_leasing=negotiate_response.capabilities.directory_leasing, supports_multi_channel=negotiate_response.capabilities.multi_channel, supports_persistent_handles=negotiate_response.capabilities.persistent_handles, supports_encryption=negotiate_response.capabilities.encryption ) if isinstance(negotiate_response, SMB202NegotiateResponse): self.negotiated_details = SMB202NegotiatedDetails(**negotiated_details_base_kwargs) elif isinstance(negotiate_response, SMB210NegotiateResponse): self.negotiated_details = SMB210NegotiatedDetails( **negotiated_details_base_kwargs, **negotiated_details_base_kwargs_2 ) elif isinstance(negotiate_response, SMB300NegotiateResponse): self.negotiated_details = SMB300NegotiatedDetails( **negotiated_details_base_kwargs, **negotiated_details_base_kwargs_2, **negotiated_details_base_kwargs_3 ) elif isinstance(negotiate_response, SMB302NegotiateResponse): self.negotiated_details = SMB302NegotiatedDetails( **negotiated_details_base_kwargs, **negotiated_details_base_kwargs_2, **negotiated_details_base_kwargs_3 ) elif isinstance(negotiate_response, SMB311NegotiateResponse): negotiated_context_map = dict( preauth_integrity_hash_id=None, # TODO: Not sure from where I would get this. preauth_integrity_hash_value=None, cipher_id=None, compression_ids=None ) for negotiate_context in negotiate_response.negotiate_context_list: if isinstance(negotiate_context, PreauthIntegrityCapabilitiesContext): negotiated_context_map['preauth_integrity_hash_id'] = next(negotiate_context.hash_algorithms) elif isinstance(negotiate_context, EncryptionCapabilitiesContext): negotiated_context_map['cipher_id'] = next(negotiate_context.ciphers) elif isinstance(negotiate_context, CompressionCapabilitiesContext): negotiated_context_map['compression_ids'] = set(negotiate_context.compression_algorithms) elif isinstance(negotiate_context, NetnameNegotiateContextIdContext): # TODO: Do something with this. ... else: # TODO: Use a proper exception. raise ValueError self.negotiated_details = SMB311NegotiateDetails( **negotiated_details_base_kwargs, **negotiated_details_base_kwargs_2, **negotiated_details_base_kwargs_3, **negotiated_context_map ) else: # TODO: Use proper exception. raise ValueError