def _on_entering(self): """Acquire network resources and try to connect. The connection is authenticated by the mean of the server certificate, so it is trusted. """ self.logger.info(u"Connecting to server...") self._context._internal_facade.set_global_status( GStatuses.NC_CONNECTING) if self._context.acquire_network_resources(): self.logger.info(u"Server has successfully authenticated itself") self._context.num_connection_attempts = 0 self._context._internal_facade.reset_pause_timer() self._set_next_state(StateRegister.get('ConnectedState')) self._context._input_queue.put(Command('HANDSHAKE'), 'sessioncommand') else: self._context._internal_facade.set_global_status( GStatuses.NC_NOSERVER) if self._context.num_connection_attempts > self._context.max_connection_attempts: self.logger.error( u'Server has been unreachable for too long, giving up.') return self._context._internal_facade.pause_and_restart() self._set_next_state(StateRegister.get('DisconnectedState')) self._context._input_queue.put(Command('CONNECT'), 'sessioncommand')
def setup_fixtures(adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock): from filerockclient.serversession.states.register import StateRegister components = setup_server_session(__file__, adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock) # Note: the StateRegister singleton has been initialized by # ServerSession's constructor syncstart_state = StateRegister.get('SyncStartState') # DisconnectedState is the default initial state, but we want to start # from SyncStartState disconnected_state_mock = disconnectedstate_cls_mock() disconnected_state_mock.do_execute.return_value = syncstart_state def fail(): msg = "ServerSession has attempted to start downloading although an" \ " integrity error was expected" assert_true(False, msg) # If we get into SyncDownloadingLeavesState, it means we have # passed the integrity check. But we shouldn't had to! download_state_mock = downloadstate_cls_mock() download_state_mock.do_execute.side_effect = fail def ask_user(what, content, client_basis, server_basis): if what == 'accept_sync': msg = "The user has been asked to accept the synchronization" \ " although an integrity error was expected" assert_true(False, msg) return 'ok' # If the sync dialog is shown to the user, it means we have # passed the integrity check. But we shouldn't had to! components['mock'][ 'ui_controller'].ask_for_user_input.side_effect = ask_user def terminate(): components['real']['server_session'].terminate() return integrity_failure_state # If the integrity check fails, it's fine integrity_failure_state = StateRegister.get('BasisMismatchState') integrity_failure_state.do_execute = MagicMock(side_effect=terminate) components['mock']['integrity_failure_state'] = integrity_failure_state components['mock']['downloading_state'] = download_state_mock return components
def _handle_message_SYNC_ENCRYPTED_FILES_IVS(self, message): """We have asked the server to send the IVs for some of the encrypted files, here they are. Now it's possible to proceed with the synchronization as usual. ServerSession needs the etag for both the cleartext and the encrypted versions of an encrypted file to correctly detect changes. If the storage cache is not available then we need to encrypt the files again in order to read their etag. The same IVs used the encrypt the first time must be used. Basically ServerSession receives this message only if there are some encrypted files but there is no storage cache available for them. """ self.logger.debug(u"Recomputing the etag for encrypted conflicted pathnames...") server_ivs = message.getParameter('ivs') server_ivs_notNone = { key: server_ivs[key] for key in filter( lambda key: server_ivs[key] is not None, server_ivs ) } try: self.logger.debug("I'm going to recalculate all encrypted etags") enc_etags = CryptoHelpers.recalc_encrypted_etag( server_ivs_notNone, self._context.warebox, self._context.cfg, self._context.must_die) self.logger.debug("Encrypted etags recalculated") except ExecutionInterrupted: self.logger.debug(u'ExecutionInterrupted, terminating...') self._set_next_state(StateRegister.get('DisconnectedState')) return #self.logger.debug(u"Finished recomputing the etag for encrypted conflicted pathnames.") #self.logger.debug('IVs received from server\n %r' % server_ivs) #self.logger.debug('IVs NotNone\n %r' % server_ivs_notNone) #self.logger.debug('encrypted Etag\n %r' % enc_etags) #self.logger.debug('Remote Etag\n %r' % self._context.startup_synchronization.remote_etag) self._context.startup_synchronization.update_conflicts_of_encrypted_pathnames(enc_etags) try: if self._check_hash_mismatch(): self._start_syncing() else: # self._internal_facade.terminate() self._context._internal_facade.pause() except HashMismatchException as excp: self.logger.critical('Integrity Error %s' % excp) self._set_next_state(StateRegister.get('IntegrityErrorState'))
def _handle_message_SYNC_ENCRYPTED_FILES_IVS(self, message): """We have asked the server to send the IVs for some of the encrypted files, here they are. Now it's possible to proceed with the synchronization as usual. ServerSession needs the etag for both the cleartext and the encrypted versions of an encrypted file to correctly detect changes. If the storage cache is not available then we need to encrypt the files again in order to read their etag. The same IVs used the encrypt the first time must be used. Basically ServerSession receives this message only if there are some encrypted files but there is no storage cache available for them. """ self.logger.debug( u"Recomputing the etag for encrypted conflicted pathnames...") server_ivs = message.getParameter('ivs') server_ivs_notNone = { key: server_ivs[key] for key in filter(lambda key: server_ivs[key] is not None, server_ivs) } try: self.logger.debug("I'm going to recalculate all encrypted etags") enc_etags = CryptoHelpers.recalc_encrypted_etag( server_ivs_notNone, self._context.warebox, self._context.cfg, self._context.must_die) self.logger.debug("Encrypted etags recalculated") except ExecutionInterrupted: self.logger.debug(u'ExecutionInterrupted, terminating...') self._set_next_state(StateRegister.get('DisconnectedState')) return #self.logger.debug(u"Finished recomputing the etag for encrypted conflicted pathnames.") #self.logger.debug('IVs received from server\n %r' % server_ivs) #self.logger.debug('IVs NotNone\n %r' % server_ivs_notNone) #self.logger.debug('encrypted Etag\n %r' % enc_etags) #self.logger.debug('Remote Etag\n %r' % self._context.startup_synchronization.remote_etag) self._context.startup_synchronization.update_conflicts_of_encrypted_pathnames( enc_etags) try: if self._check_hash_mismatch(): self._start_syncing() else: # self._internal_facade.terminate() self._context._internal_facade.pause() except HashMismatchException as excp: self.logger.critical('Integrity Error %s' % excp) self._set_next_state(StateRegister.get('IntegrityErrorState'))
def setup_fixtures(adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock): from filerockclient.serversession.states.register import StateRegister components = setup_server_session( __file__, adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock) # Note: the StateRegister singleton has been initialized by # ServerSession's constructor syncstart_state = StateRegister.get('SyncStartState') # DisconnectedState is the default initial state, but we want to start # from SyncStartState disconnected_state_mock = disconnectedstate_cls_mock() disconnected_state_mock.do_execute.return_value = syncstart_state def fail(): msg = "ServerSession has attempted to start downloading although an" \ " integrity error was expected" assert_true(False, msg) # If we get into SyncDownloadingLeavesState, it means we have # passed the integrity check. But we shouldn't had to! download_state_mock = downloadstate_cls_mock() download_state_mock.do_execute.side_effect = fail def ask_user(what, content, client_basis, server_basis): if what == 'accept_sync': msg = "The user has been asked to accept the synchronization" \ " although an integrity error was expected" assert_true(False, msg) return 'ok' # If the sync dialog is shown to the user, it means we have # passed the integrity check. But we shouldn't had to! components['mock']['ui_controller'].ask_for_user_input.side_effect = ask_user def terminate(): components['real']['server_session'].terminate() return integrity_failure_state # If the integrity check fails, it's fine integrity_failure_state = StateRegister.get('BasisMismatchState') integrity_failure_state.do_execute = MagicMock(side_effect=terminate) components['mock']['integrity_failure_state'] = integrity_failure_state components['mock']['downloading_state'] = download_state_mock return components
def _handle_command_HANDSHAKE(self, message): """Positive reply from the server, tell him the protocol version that we can support. """ self._context.output_message_queue.put( PROTOCOL_VERSION('PROTOCOL_VERSION', {'version': 1})) self._set_next_state(StateRegister.get('ProtocolVersionState'))
def _handle_message_COMMIT_DONE(self, message): """Everything went well, the server has completed the commit. Check the resulting basis declared by the server and, if it is valid, then finalize the commit by updating all internal data structures. Finally go back to the Replication & Transfer state, we start again. """ server_basis = message.getParameter('new_basis') self.logger.info(u'Commit done') self.logger.debug(u"Server basis: %s" % (server_basis)) previous_basis = self._context.integrity_manager.getCurrentBasis() self.logger.debug(u"Previous basis: %s" % previous_basis) # Check the server basis against the one we have computed self._check_integrity(server_basis) # Everything OK, persist the new trusted basis and related metadata new_basis = self._context.integrity_manager.getCurrentBasis() completed_ops = self._context.transaction_manager.get_completed_operations() self._persist_integrity_metadata(previous_basis, new_basis, completed_ops) self.logger.info(u"Updated basis: %s" % new_basis) self._context.transaction_manager.clear() self._context.operation_responses.clear() self._context.refused_declare_count = 0 self.logger.debug( u"Current transaction has been committed successfully.") for (_, operation) in completed_ops: operation.notify_pathname_status_change(PStatuses.ALIGNED) self._update_user_interfaces(message) self._try_set_global_status_aligned() self._set_next_state(StateRegister.get('ReplicationAndTransferState'))
def _handle_message_COMMIT_DONE(self, message): """Everything went well, the server has completed the commit. Check the resulting basis sent by the server against our candidate basis, if it's valid then finalize the commit by updating all internal data structures. Finally go into the sync phase, the session can begin at last. """ self.logger.info(u'Commit done') server_basis = message.getParameter('new_basis') candidate_basis = self._load_candidate_basis() previous_basis = self._load_trusted_basis() self.logger.debug(u"Server basis: %s" % server_basis) self.logger.debug(u"Candidate basis: %s" % candidate_basis) self.logger.debug(u"Last trusted basis: %s" % previous_basis) # Check the server basis against the candidate basis self._context.integrity_manager.setCurrentBasis(candidate_basis) self._check_integrity(server_basis) # Everything OK, persist the new trusted basis and related metadata operations = self._context.transaction_cache.get_all_records() operations = [(op_id, op) for (op_id, op, _) in operations] self._persist_integrity_metadata(previous_basis, server_basis, operations) self._context.integrity_manager.setCurrentBasis(server_basis) self.logger.info(u"Pending commit successfully recovered.") self.logger.info(u"Updated basis: %s" % server_basis) self._update_user_interfaces(message) self._set_next_state(StateRegister.get('ReadyForServiceState')) self._context._input_queue.put( Command('STARTSYNCPHASE'), 'sessioncommand')
def _on_complete_operation(self, operation): with self._lock: self._num_finished_operations += 1 num_finished = self._num_finished_operations num_received = self._num_received_operations if self._received_all_operations and num_received == num_finished: self._set_next_state(StateRegister.get("SyncDoneState"))
def _handle_message_COMMIT_DONE(self, message): """Everything went well, the server has completed the commit. Check the resulting basis sent by the server against our candidate basis, if it's valid then finalize the commit by updating all internal data structures. Finally go into the sync phase, the session can begin at last. """ self.logger.info(u'Commit done') server_basis = message.getParameter('new_basis') candidate_basis = self._load_candidate_basis() previous_basis = self._load_trusted_basis() self.logger.debug(u"Server basis: %s" % server_basis) self.logger.debug(u"Candidate basis: %s" % candidate_basis) self.logger.debug(u"Last trusted basis: %s" % previous_basis) # Check the server basis against the candidate basis self._context.integrity_manager.setCurrentBasis(candidate_basis) self._check_integrity(server_basis) # Everything OK, persist the new trusted basis and related metadata operations = self._context.transaction_cache.get_all_records() operations = [(op_id, op) for (op_id, op, _) in operations] self._persist_integrity_metadata(previous_basis, server_basis, operations) self._context.integrity_manager.setCurrentBasis(server_basis) self.logger.info(u"Pending commit successfully recovered.") self.logger.info(u"Updated basis: %s" % server_basis) self._update_user_interfaces(message) self._set_next_state(StateRegister.get('ReadyForServiceState')) self._context._input_queue.put(Command('STARTSYNCPHASE'), 'sessioncommand')
def _on_entering(self): """Tell the server that we are ready. """ self._context.output_message_queue.put( COMMIT_START("COMMIT_START", {'achieved_operations': 'RECOVER_FROM_CRASH'})) self._set_next_state(StateRegister.get('PendingCommitStartState'))
def _handle_message_COMMIT_DONE(self, message): """Everything went well, the server has completed the commit. Check the resulting basis declared by the server and, if it is valid, then finalize the commit by updating all internal data structures. Finally go back to the Replication & Transfer state, we start again. """ server_basis = message.getParameter('new_basis') self.logger.info(u'Commit done') self.logger.debug(u"Server basis: %s" % (server_basis)) previous_basis = self._context.integrity_manager.getCurrentBasis() self.logger.debug(u"Previous basis: %s" % previous_basis) # Check the server basis against the one we have computed self._check_integrity(server_basis) # Everything OK, persist the new trusted basis and related metadata new_basis = self._context.integrity_manager.getCurrentBasis() completed_ops = self._context.transaction_manager.get_completed_operations( ) self._persist_integrity_metadata(previous_basis, new_basis, completed_ops) self.logger.info(u"Updated basis: %s" % new_basis) self._context.transaction_manager.clear() self._context.operation_responses.clear() self._context.refused_declare_count = 0 self.logger.debug( u"Current transaction has been committed successfully.") for (_, operation) in completed_ops: operation.notify_pathname_status_change(PStatuses.ALIGNED) self._update_user_interfaces(message) self._try_set_global_status_aligned() self._set_next_state(StateRegister.get('ReplicationAndTransferState'))
def _on_entering(self): """ServerSession waits for all operations currently handled by workers to be completed, afterwards it begins the commit. Integrity of the transaction is checked and the "candidate basis" (the expected basis after our modifications to the storage) is computed. """ self.logger.debug(u"Committing the current transaction...") self.logger.debug(u"Waiting for the transaction to be finished...") self._context.transaction_manager.wait_until_finished() self.logger.debug(u"Transaction is finished!") self.logger.info(u"Committing the following pathnames:") operations = self._context.transaction_manager.get_completed_operations() for (op_id, op) in operations: self.logger.info(u' %s "%s"' % (op.verb, op.pathname)) self.logger.debug(u" id=%s %s" % (op_id, op)) self._check_transaction_integrity(operations) candidate_basis = self._context.integrity_manager.getCandidateBasis() self._context._persist_candidate_basis(candidate_basis) self.logger.info("Candidate basis: %s" % candidate_basis) self._update_transaction_cache(operations) completed_operations_id = [op_id for (op_id, _) in operations] self._context.output_message_queue.put(COMMIT_START( "COMMIT_START", {'achieved_operations': completed_operations_id})) self._set_next_state(StateRegister.get('CommitStartState'))
def _on_complete_operation(self, operation): with self._lock: self._num_finished_operations += 1 num_finished = self._num_finished_operations num_received = self._num_received_operations if self._received_all_operations and num_received == num_finished: self._set_next_state(StateRegister.get('SyncDoneState'))
def _handle_command_OPERATIONSFINISHED(self, command): """Receiving this command means that all the internal operations have been served. Wait until all of them are complete. """ self._set_next_state(StateRegister.get('SyncWaitingForCompletionState')) cmd = Command('GOTOONCOMPLETION') cmd.next_state = 'SyncDoneState' self._context._input_queue.put(cmd, 'sessioncommand')
def _handle_message_REPLICATION_START(self, message): """The server is ready to leave the sync phase, too. Let's start the Replication & Transfer phase. """ self._set_next_state( StateRegister.get('EnteringReplicationAndTransferState')) self._context._input_queue.put( Command('UPDATEBEFOREREPLICATION'), 'sessioncommand')
def _start_syncing(self): """The list of modification to perform has been accepted, make the synchronization phase start. """ self._context.integrity_manager.setCurrentBasis(self.server_basis.encode()) self._context._internal_facade.set_global_status(GStatuses.C_NOTALIGNED) self._context.startup_synchronization.execute() self._context.startup_synchronization.generate_downlink_events() self._set_next_state(StateRegister.get('SyncDownloadingLeavesState'))
def _handle_message_PROTOCOL_VERSION_AGREEMENT(self, message): """The server replied to our greeting message and then closed the connection. Is our client version obsolete? Note: this is just post-disconnection handling, the natural handler would be ProtocolVersionState. """ state = StateRegister.get('ProtocolVersionState') state._check_protocol_mismatch(message)
def test_nothing_to_sync_from_clean_state(adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock): from filerockclient.serversession.states.register import StateRegister from FileRockSharedLibraries.Communication.Messages import SYNC_FILES_LIST components = setup_server_session(__file__, adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock) # Note: the StateRegister singleton has been initialized by # ServerSession's constructor syncstart_state = StateRegister.get('SyncStartState') def terminate(): components['real']['server_session'].terminate() return syncstart_state # DisconnectedState is the default initial state, but we want to start # from SyncStartState disconnected_state_mock = disconnectedstate_cls_mock() disconnected_state_mock.do_execute.return_value = syncstart_state # Stop the test when we get to SyncDownloadingLeavesState download_state_mock = downloadstate_cls_mock() download_state_mock.do_execute.side_effect = terminate # There is nothing in the warebox components['mock']['warebox'].get_content.return_value = [] # Send ServerSession a scenario with no data on the storage. components['real']['metadata'].set('trusted_basis', 'TRUSTEDBASIS') msg = SYNC_FILES_LIST( 'SYNC_FILES_LIST', { 'basis': 'TRUSTEDBASIS', 'dataset': [], 'last_commit_client_id': '0', 'last_commit_client_hostname': 'myhostname', 'last_commit_client_platform': 'myplatform', 'last_commit_timestamp': 'mytimestamp', 'user_quota': '0', 'used_space': '100' }) components['real']['input_queue'].put(msg, 'servermessage') components['real']['server_session'].start() components['real']['server_session'].join() # No data on the storage, in the warebox or in the storage cache. # We expect that nothing has happened. assert_false(components['mock']['ui_controller'].ask_for_user_input.called) assert_equal(components['real']['storage_cache'].get_all_records(), []) assert_true(components['real']['input_queue'].empty(['operation']))
def on_commit_necessary_to_proceed(self): """Session can decide to commit the current transaction. It happens if the transaction has got too large, if it's passed too much time from the last commit or if a commit is needed to honor some session constraint. """ self.logger.info( u"The application has decided that a commit is necessary") self._set_next_state( StateRegister.get('WaitingForUnauthorizedOperationsState'))
def _handle_message_CHALLENGE_VERIFY_RESPONSE(self, message): """Received a reply to our challenge. Did the server authenticate us? If not, maybe another client is already connected to this account. """ auth_result = message.getParameter('result') if auth_result: self.logger.info(u"Client has successfully authenticated itself.") self._context.output_message_queue.put( READY_FOR_SERVICE('READY_FOR_SERVICE')) self._context.session_id = message.get_session_id() self.logger.debug(u"Received Session id %s from server" % self._context.session_id) self._context.keepalive_timer.resume_execution() self._set_next_state(StateRegister.get('ReadyForServiceState')) else: self.logger.error(u"Server rejected client authentication.") self._context._internal_facade.set_global_status( GStatuses.NC_NOTAUTHORIZED) if message.is_other_client_connected(): client = message.get_other_connected_client() if client["client_id"] == 0: other_client_message = u"Your web client is already connected" else: other_client_message = \ u"Client number %s from computer %s already connected" \ % (client["client_id"], client["hostname"]) self.logger.warning(other_client_message) self._context.disconnect_other_client = True force_disconnection = self._context._ui_controller.ask_for_user_input( "other_client_connected", client["client_id"], client["hostname"]) if not force_disconnection == 'ok': self._context._internal_facade.pause() return else: self._context.current_state._set_next_state( StateRegister.get('DisconnectedState')) self._context._input_queue.put(Command('CONNECT'), 'sessioncommand') return else: relink_user(self)
def test_nothing_to_sync_from_clean_state( adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock): from filerockclient.serversession.states.register import StateRegister from FileRockSharedLibraries.Communication.Messages import SYNC_FILES_LIST components = setup_server_session( __file__, adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock) # Note: the StateRegister singleton has been initialized by # ServerSession's constructor syncstart_state = StateRegister.get('SyncStartState') def terminate(): components['real']['server_session'].terminate() return syncstart_state # DisconnectedState is the default initial state, but we want to start # from SyncStartState disconnected_state_mock = disconnectedstate_cls_mock() disconnected_state_mock.do_execute.return_value = syncstart_state # Stop the test when we get to SyncDownloadingLeavesState download_state_mock = downloadstate_cls_mock() download_state_mock.do_execute.side_effect = terminate # There is nothing in the warebox components['mock']['warebox'].get_content.return_value = [] # Send ServerSession a scenario with no data on the storage. components['real']['metadata'].set('trusted_basis', 'TRUSTEDBASIS') msg = SYNC_FILES_LIST('SYNC_FILES_LIST', { 'basis': 'TRUSTEDBASIS', 'dataset': [], 'last_commit_client_id': '0', 'last_commit_client_hostname': 'myhostname', 'last_commit_client_platform': 'myplatform', 'last_commit_timestamp': 'mytimestamp', 'user_quota': '0', 'used_space': '100' }) components['real']['input_queue'].put(msg, 'servermessage') components['real']['server_session'].start() components['real']['server_session'].join() # No data on the storage, in the warebox or in the storage cache. # We expect that nothing has happened. assert_false(components['mock']['ui_controller'].ask_for_user_input.called) assert_equal(components['real']['storage_cache'].get_all(), []) assert_true(components['real']['input_queue'].empty(['operation']))
def _handle_message_PROTOCOL_VERSION_AGREEMENT(self, message): """Received the reply from the server, check if he agreed our protocol version. If so, ask the server to authenticate. """ self._check_protocol_mismatch(message) # Protocol agreed, starting authentication challenge_request_msg = CHALLENGE_REQUEST( "CHALLENGE_REQUEST", {'client_id': self._context.client_id, 'username': self._context.username}) self._context.output_message_queue.put(challenge_request_msg) self._set_next_state(StateRegister.get('ChallengeRequestState'))
def _handle_command_UPDATEBEFOREREPLICATION(self, command): """Initialize all data structures and components. """ storage_content = self._context.storage_cache.get_all_records() self._update_client_status(storage_content) self._update_filesystem_watcher(storage_content) self._context.filesystem_watcher.resume_execution() self.logger.info(u'Started filesystem monitoring.') self._context.refused_declare_count = 0 self._context.id = 0 self._try_set_global_status_aligned() self._set_next_state(StateRegister.get('ReplicationAndTransferState'))
def _on_entering(self): """Acquire network resources and try to connect. The connection is authenticated by the mean of the server certificate, so it is trusted. """ self.logger.info(u"Connecting to server...") self._context._internal_facade.set_global_status(GStatuses.NC_CONNECTING) if self._context.acquire_network_resources(): self.logger.info(u"Server has successfully authenticated itself") self._context.num_connection_attempts = 0 self._context._internal_facade.reset_pause_timer() self._set_next_state(StateRegister.get('ConnectedState')) self._context._input_queue.put(Command('HANDSHAKE'), 'sessioncommand') else: self._context._internal_facade.set_global_status(GStatuses.NC_NOSERVER) if self._context.num_connection_attempts > self._context.max_connection_attempts: self.logger.error(u'Server has been unreachable for too long, giving up.') return self._context._internal_facade.pause_and_restart() self._set_next_state(StateRegister.get('DisconnectedState')) self._context._input_queue.put(Command('CONNECT'), 'sessioncommand')
def _start_syncing(self): """The list of modification to perform has been accepted, make the synchronization phase start. """ self._context.integrity_manager.setCurrentBasis( self.server_basis.encode()) self._context._internal_facade.set_global_status( GStatuses.C_NOTALIGNED) self._context.startup_synchronization.execute() self._context.startup_synchronization.generate_downlink_events() self._set_next_state( StateRegister.get('ResolvingDeletionConflictsState'))
def _handle_message_CHALLENGE_VERIFY_RESPONSE(self, message): """Received a reply to our challenge. Did the server authenticate us? If not, maybe another client is already connected to this account. """ auth_result = message.getParameter('result') if auth_result: self.logger.info(u"Client has successfully authenticated itself.") self._context.output_message_queue.put(READY_FOR_SERVICE('READY_FOR_SERVICE')) self._context.session_id = message.get_session_id() self.logger.debug( u"Received Session id %s from server" % self._context.session_id) self._context.keepalive_timer.resume_execution() self._set_next_state(StateRegister.get('ReadyForServiceState')) else: self.logger.error(u"Server rejected client authentication.") self._context._internal_facade.set_global_status(GStatuses.NC_NOTAUTHORIZED) if message.is_other_client_connected(): client = message.get_other_connected_client() if client["client_id"]==0: other_client_message = u"Your web client is already connected" else: other_client_message = \ u"Client number %s from computer %s already connected" \ % (client["client_id"], client["hostname"]) self.logger.warning(other_client_message) self._context.disconnect_other_client = True force_disconnection = self._context._ui_controller.ask_for_user_input( "other_client_connected", client["client_id"], client["hostname"]) if not force_disconnection == 'ok': self._context._internal_facade.pause() return else: self._context.current_state._set_next_state( StateRegister.get('DisconnectedState')) self._context._input_queue.put(Command('CONNECT'), 'sessioncommand') return else: relink_user(self)
def _immediate_commit(self): """Pre-emptive style commit. Usually ServerSession wait for authorization from the server for those operations that were already declared, before actually start a commit. But sometimes you just can't wait, so all still unauthorized operations are postponed. For example this happens when the user asked to commit - hey, it's better not to contradict the boss. """ unauthorized = self._context.transaction_manager.flush_unauthorized_operations() map(lambda x: self.postpone_operation(x), unauthorized) self._set_next_state(StateRegister.get('CommitState'))
def _handle_command_INTEGRITYERRORONDELETELOCAL(self, command): """ Attributes available in the command object: - pathname - proof - reason - expected_basis - computed_basis """ self.logger.critical(u"Detected an integrity error while deleting " "locally pathname %s. Reason: %s" % (command.pathname, command.reason)) self._set_next_state(StateRegister.get('IntegrityErrorState'))
def _handle_message_PROTOCOL_VERSION_AGREEMENT(self, message): """Received the reply from the server, check if he agreed our protocol version. If so, ask the server to authenticate. """ self._check_protocol_mismatch(message) # Protocol agreed, starting authentication challenge_request_msg = CHALLENGE_REQUEST( "CHALLENGE_REQUEST", { 'client_id': self._context.client_id, 'username': self._context.username }) self._context.output_message_queue.put(challenge_request_msg) self._set_next_state(StateRegister.get('ChallengeRequestState'))
def on_download_integrity_error(state, command): """ Attributes available in the command object: - operation - proof - reason - expected_etag - expected_basis - actual_etag - computed_basis """ operation = command.operation state.logger.critical(u"Detected an integrity error while downloading " "pathname %s. Reason: %s" % (operation.pathname, command.reason)) state._set_next_state(StateRegister.get('IntegrityErrorState'))
def run(self): """Implementation of the threading.Thread.run() method.""" self._started = True try: self.worker_pool.start_workers() self.keepalive_timer.start() self.current_state = StateRegister.get('DisconnectedState') curr_basis = self.current_state._load_trusted_basis() self.integrity_manager.setCurrentBasis(curr_basis) self.logger.info(u'Current basis is: %s' % curr_basis) self.current_state._on_entering() self._internal_facade.set_global_status(GStatuses.NC_STOPPED) if self.auto_start: self._input_queue.put(Command('CONNECT'), 'sessioncommand') self.cryptoAdapter.start() self._scheduler.schedule_action(self.check_encrypted_folder, name='check_encrypted_folder', seconds=5, repeating=True) # The event loop self._main_loop() except UnexpectedMessageException as e: self.logger.critical( u"Received an unexpected message from the Server while in " u"state '%s': %s. Forcing termination." % (self.current_state.__class__, str(e))) raise except ProtocolException as e: self.logger.critical( u"Detected an unrecoverable error, forcing termination: %s" % str(e)) # Pre-emptive release, just stop before messing up the server self.release_network_resources() raise except Exception as e: self.logger.critical( u"Forcing termination due to uncaught exception '%s': %s" % (e.__class__, e)) self.logger.debug(u"Last error stacktrace:\n%s" % traceback.format_exc()) # Pre-emptive release, just stop before messing up the server self.release_network_resources() raise
def run(self): """Implementation of the threading.Thread.run() method.""" self._started = True try: self.worker_pool.start_workers() self.keepalive_timer.start() self.current_state = StateRegister.get('DisconnectedState') curr_basis = self.current_state._load_trusted_basis() self.integrity_manager.setCurrentBasis(curr_basis) self.logger.info(u'Current basis is: %s' % curr_basis) self.current_state._on_entering() self._internal_facade.set_global_status(GStatuses.NC_STOPPED) if self.auto_start: self._input_queue.put(Command('CONNECT'), 'sessioncommand') self.cryptoAdapter.start() self._scheduler.schedule_action( self.check_encrypted_folder, name='check_encrypted_folder', seconds=5, repeating=True) # The event loop self._main_loop() except UnexpectedMessageException as e: self.logger.critical( u"Received an unexpected message from the Server while in " u"state '%s': %s. Forcing termination." % (self.current_state.__class__, str(e))) raise except ProtocolException as e: self.logger.critical( u"Detected an unrecoverable error, forcing termination: %s" % str(e)) # Pre-emptive release, just stop before messing up the server self.release_network_resources() raise except Exception as e: self.logger.critical( u"Forcing termination due to uncaught exception '%s': %s" % (e.__class__, e)) self.logger.debug( u"Last error stacktrace:\n%s" % traceback.format_exc()) # Pre-emptive release, just stop before messing up the server self.release_network_resources() raise
def _immediate_commit(self): """Pre-emptive style commit. Usually ServerSession wait for authorization from the server for those operations that were already declared, before actually start a commit. But sometimes you just can't wait, so all still unauthorized operations are postponed. For example this happens when the user asked to commit - hey, it's better not to contradict the boss. """ unauthorized = self._context.transaction_manager.flush_unauthorized_operations() for operation in unauthorized: self.postpone_operation(operation) self._context.worker_pool.release_worker() if __debug__: self._context.worker_pool.track_release_unassigned_worker(operation) self._set_next_state(StateRegister.get('CommitState'))
def _handle_operation(self, operation): if operation == 'NO_MORE_OPERATIONS': self._received_all_operations = True with self._lock: num_finished = self._num_finished_operations num_received = self._num_received_operations if num_received == num_finished: self._set_next_state(StateRegister.get('SyncDoneState')) return self._num_received_operations += 1 if not self._context.worker_pool.acquire_worker(): raise FileRockException( u"Concurrency trouble in %s: could not acquire a worker" " although some should have been available" % (self.__class__.__name__ + "._handle_file_operation")) if __debug__: self._context.worker_pool.track_acquire_anonymous_worker( operation.pathname) if not self._context.worker_pool.exist_free_workers(): self._listening_operations = False self.logger.info(u'Synchronizing pathname: %s "%s"' % (operation.verb, operation.pathname)) with operation.lock: if operation.is_working(): operation.register_complete_handler(self._on_complete_operation) operation.register_abort_handler(self._on_complete_operation) operation.register_reject_handler(sync_on_operation_rejected) else: self.logger.debug(u"Ignoring aborted operation:%s" % operation) self._num_received_operations -= 1 self._context.worker_pool.release_worker() return CryptoUtils.prepare_operation(operation, self._context.temp_dir) self._pathname2operation[operation.pathname] = operation request = SYNC_GET_REQUEST("SYNC_GET_REQUEST", {'pathname': operation.pathname}) #self.logger.debug(u"Produced Request message: %s", request) self._context.output_message_queue.put(request)
def _handle_message_CHALLENGE_REQUEST_RESPONSE(self, message): """The server has agreed to start the authentication and has sent us a challange to sign. """ challenge = str(message.getParameter("challenge")) # Sign challenge with private key signed = self.crypto.challenge_sign(challenge, open(self._context.priv_key, "r").read()) # Create CHALLENGE_RESPONSE with signed challenge challenge_response_msg = CHALLENGE_RESPONSE( "CHALLENGE_RESPONSE", {"client_id": self._context.client_id, "response": signed} ) if self._context.disconnect_other_client: key = "force_other_client_disconnection" challenge_response_msg.parameters[key] = True # challenge_response_msg.set_force_other_client_disconnection() self._context.disconnect_other_client = False self._context.output_message_queue.put(challenge_response_msg) self._set_next_state(StateRegister.get("ChallengeResponseState"))
def _on_entering(self): """Persist the new basis, the storage cache and tell the UI about the completion of the sync phase. """ temp = [op.is_completed() for op in self._context._sync_operations] all_done = reduce(lambda x, y: x and y, temp, True) if all_done: # All expected operations have been completed. self.logger.info( u"Startup Synchronization phase has completed successfully") self._context.output_message_queue.put(SYNC_DONE('SYNC_DONE')) completed_operations = self._context._sync_operations self._update_storage_cache(completed_operations) self._context.transaction.clear() # In case the server had sent a fresher basis # TODO: save into the basis history as well, if the basis has # changed (it happens, for example, if the client had crashed # on the last COMMIT_DONE message) self._persist_trusted_basis(self._context.integrity_manager.getCurrentBasis()) self._context.metadataDB.delete_key(metadata.LASTACCEPTEDSTATEKEY) self._clear_candidate_basis() self._context.transaction_cache.clear() self._context._internal_facade.first_startup_end() self._context._ui_controller.update_session_info( {'basis': self._context.integrity_manager.getCurrentBasis()}) else: # Some operations have been aborted. The basis can't be persisted, # because it doesn't match what is on disk. We must stop here and # repeat the synchronization. self.logger.info(u"Startup Synchronization interrupted due to" " the abort of some operations.") self._context._internal_facade.pause() state = StateRegister.get('WaitingForTerminationState') self._set_next_state(state) self._context.worker_pool.clean_download_dir() self._context._sync_operations = []
def _handle_message_COMMIT_DONE(self, message): """Everything went well, the server has completed the commit. Check the resulting basis declared by the server and, if it is valid, then finalize the commit by updating all internal data structures. Finally go back to the Replication & Transfer state, we start again. """ server_basis = message.getParameter('new_basis') self.logger.info(u'Commit done') self.logger.debug(u"Server basis: %s" % (server_basis)) previous_basis = self._context.integrity_manager.getCurrentBasis() self.logger.debug(u"Previous basis: %s" % previous_basis) try: self._context.integrity_manager.checkCommitResult(server_basis) except WrongBasisAfterUpdatingException as e: state = GStatuses.C_HASHMISMATCHONCOMMIT self._context._internal_facade.set_global_status(state) self.logger.critical( u"Detected an integrity problem with data on the storage:" " server basis %s doesn't match our computed basis %s" % (server_basis, e.computed_basis)) raise ProtocolException('WrongBasisAfterUpdatingException') new_basis = self._context.integrity_manager.getCurrentBasis() self._context._persist_basis(new_basis) self._context._save_basis_in_history(previous_basis, new_basis) self.logger.info(u"Updated basis: %s" % new_basis) completed_ops = self._context.transaction_manager.get_completed_operations() self._update_storage_cache(completed_ops) for (_, op) in self._context.transaction_manager.get_completed_operations(): op.notify_pathname_status_change(PStatuses.ALIGNED) self._context.transaction_manager.clear() self._context.transaction_cache.clear() self._context.operation_responses.clear() self._context._clear_candidate_basis() self._context.refused_declare_count = 0 self.logger.debug( u"Current transaction has been committed successfully.") self._update_user_interfaces(message) self._try_set_global_status_aligned() self._set_next_state(StateRegister.get('ReplicationAndTransferState'))
def _on_entering(self): """ServerSession waits for all operations currently handled by workers to be completed, afterwards it begins the commit. Integrity of the transaction is checked and the "candidate basis" (the expected basis after our modifications to the storage) is computed. """ self.logger.debug(u"Committing the current transaction...") # Block until all pending uploads are finished self.logger.debug(u"Waiting for the transaction to be finished...") self._context.transaction_manager.wait_until_finished() self.logger.debug(u"Transaction is finished!") self.logger.info(u"Committing the following pathnames:") operations = self._context.transaction_manager.get_completed_operations( ) for (op_id, op) in operations: self.logger.info(u' %s "%s"' % (op.verb, op.pathname)) self.logger.debug(u" id=%s %s" % (op_id, op)) # Use the received proofs to compute the next expected basis self._check_transaction_integrity(operations) candidate_basis = self._context.integrity_manager.getCandidateBasis() self.logger.info("Candidate basis: %s" % candidate_basis) # Persist the expected state metadata = self._context.metadataDB transaction_cache = self._context.transaction_cache with metadata.transaction(transaction_cache) as (meta, trans): self._persist_candidate_basis(candidate_basis, metadata_db=meta) self._update_transaction_cache(trans, operations) # Ready to go, tell the server to start the commit! completed_operations_id = [op_id for (op_id, _) in operations] self._context.output_message_queue.put( COMMIT_START("COMMIT_START", {'achieved_operations': completed_operations_id})) self._set_next_state(StateRegister.get('CommitStartState'))
def _on_entering(self): """ServerSession waits for all operations currently handled by workers to be completed, afterwards it begins the commit. Integrity of the transaction is checked and the "candidate basis" (the expected basis after our modifications to the storage) is computed. """ self.logger.debug(u"Committing the current transaction...") # Block until all pending uploads are finished self.logger.debug(u"Waiting for the transaction to be finished...") self._context.transaction_manager.wait_until_finished() self.logger.debug(u"Transaction is finished!") self.logger.info(u"Committing the following pathnames:") operations = self._context.transaction_manager.get_completed_operations() for (op_id, op) in operations: self.logger.info(u' %s "%s"' % (op.verb, op.pathname)) self.logger.debug(u" id=%s %s" % (op_id, op)) # Use the received proofs to compute the next expected basis self._check_transaction_integrity(operations) candidate_basis = self._context.integrity_manager.getCandidateBasis() self.logger.info("Candidate basis: %s" % candidate_basis) # Persist the expected state metadata = self._context.metadataDB transaction_cache = self._context.transaction_cache with metadata.transaction(transaction_cache) as (meta, trans): self._persist_candidate_basis(candidate_basis, metadata_db=meta) self._update_transaction_cache(trans, operations) # Ready to go, tell the server to start the commit! completed_operations_id = [op_id for (op_id, _) in operations] self._context.output_message_queue.put(COMMIT_START( "COMMIT_START", {'achieved_operations': completed_operations_id})) self._set_next_state(StateRegister.get('CommitStartState'))
def _handle_message_COMMIT_DONE(self, message): """Everything went well, the server has completed the commit. Check the resulting basis sent by the server against our candidate basis, if it's valid then finalize the commit by updating all internal data structures. Finally go into the sync phase, the session can begin at last. """ self.logger.info(u'Commit done') server_basis = message.getParameter('new_basis') # candidate_basis = self._context._load_candidate_basis() self.logger.debug(u"Server basis: %s" % server_basis) # self.logger.debug(u"Candidate basis: %s" % candidate_basis) # self._context.integrity_manager.setCurrentBasis(candidate_basis) # try: # self._context.integrity_manager.checkCommitResult(server_basis) # except WrongBasisAfterUpdatingException: # state = GStatus.C_HASHMISMATCHONCOMMIT # self._context._internal_facade.set_global_status(state) # self.logger.critical( # u"Detected an integrity problem with data on the storage:" # " server basis %s doesn't match our expected basis %s" # % (server_basis, candidate_basis)) # raise ProtocolException('WrongBasisAfterUpdatingException') previous_basis = self._context._load_trusted_basis() self.logger.debug(u"Last trusted basis: %s" % previous_basis) self._context._persist_basis(server_basis) self._context._save_basis_in_history(previous_basis, server_basis) self._context.integrity_manager.setCurrentBasis(server_basis) self._context._clear_candidate_basis() self._context.transaction_cache.clear() self.logger.info(u"Pending commit successfully recovered.") self.logger.info(u"Updated basis: %s" % server_basis) self._update_user_interfaces(message) self._set_next_state(StateRegister.get('ReadyForServiceState')) self._context._input_queue.put(Command('STARTSYNCPHASE'), 'sessioncommand')
def _handle_message_CHALLENGE_REQUEST_RESPONSE(self, message): """The server has agreed to start the authentication and has sent us a challange to sign. """ challenge = str(message.getParameter('challenge')) # Sign challenge with private key signed = self.crypto.challenge_sign( challenge, open(self._context.priv_key, 'r').read()) # Create CHALLENGE_RESPONSE with signed challenge challenge_response_msg = CHALLENGE_RESPONSE( 'CHALLENGE_RESPONSE', { 'client_id': self._context.client_id, 'response': signed }) if self._context.disconnect_other_client: key = "force_other_client_disconnection" challenge_response_msg.parameters[key] = True # challenge_response_msg.set_force_other_client_disconnection() self._context.disconnect_other_client = False self._context.output_message_queue.put(challenge_response_msg) self._set_next_state(StateRegister.get('ChallengeResponseState'))
def _handle_command_USERCOMMIT(self, message): """The user has asked to commit, the operation will be recovered on next R&T loop. """ self._set_next_state(StateRegister.get('ReplicationAndTransferState')) self._context._input_queue.put(Command('USERCOMMIT'), 'sessioncommand')
def _pass_to_next_state(self): self._context.transaction.wait_until_finished() self._set_next_state(StateRegister.get(self._next_state))
def _handle_message_SYNC_FILES_LIST(self, message): """Received the remote filelist from the server. Compute differences, perform integrity checks on it and ask the user confirmation to proceed with the synchronization. Message: SYNC_FILES_LIST Parameters: last_commit_client_id: String or None last_commit_client_hostname: String or None last_commit_client_platform: String or None last_commit_timestamp: Number used_space: Number basis: String user_quota: Number """ storage_content = message.getParameter('dataset') self.storage_content = storage_content self._validate_storage_content(storage_content) self.server_basis = message.getParameter('basis') self.candidate_basis = self._context._try_load_candidate_basis() self.client_basis = self._context._load_trusted_basis() fields = [ 'last_commit_client_id', 'last_commit_client_hostname', 'last_commit_client_platform', 'last_commit_timestamp', 'used_space', 'user_quota' ] info = dict(map(lambda f: (f, message.getParameter(f)), fields)) self._context._ui_controller.update_session_info(info) # No blacklisted pathname should be found on the storage. If any, tell # the user and then shut down the application. remote_pathnames = [entry['key'] for entry in storage_content] blacklisted = filter( lambda x: self._context.warebox.is_blacklisted(x), remote_pathnames) if len(blacklisted) > 0: self.logger.critical( 'The following blacklisted pathnames have been found on the ' 'storage %s' % format_to_log(blacklisted)) self._context._ui_controller.ask_for_user_input( 'blacklisted_pathname_on_storage', blacklisted) # self._internal_facade.terminate() self._context._internal_facade.pause() return # Detect actions to be taken for synchronization as well as conflicts self.logger.debug(u"Starting computing the three-way diff...") self._context.startup_synchronization.prepare(storage_content) self.logger.debug(u"Finished computing the three-way diff.") # Conflicts on encrypted files need extra data from the server to be # solved. If there are any, handle them. encrypted_pathnames = CryptoUtils.filter_encrypted_pathname( self._context.startup_synchronization.edit_conflicts) if len(encrypted_pathnames) > 0: self.logger.debug( u'Encrypted file in conflict: %r' % encrypted_pathnames) message = SYNC_GET_ENCRYPTED_FILES_IVS( 'SYNC_GET_ENCRYPTED_FILES_IVS', {'requested_files_list': encrypted_pathnames}) self._context.output_message_queue.put(message) return # If there are no conflicts on encrypted files, just proceed normally. try: if self._check_hash_mismatch(): self._start_syncing() else: # self._internal_facade.terminate() self._context._internal_facade.pause() except HashMismatchException as excp: self.logger.critical('BASISMISMATCH %s' % excp) self._set_next_state(StateRegister.get('BasisMismatchState'))
def _handle_command_COMMIT(self, command): """Go to the commit state to start a commit. """ self.logger.info(u"Committing the current transaction") self._set_next_state( StateRegister.get('WaitingForUnauthorizedOperationsState'))
def _handle_command_REDECLAREOPERATION(self, command): """It's time, go back to R&T and let's declare the operation one more time. """ self._set_next_state(StateRegister.get('ReplicationAndTransferState'))
def test_download_operations_from_clean_state( adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock): from filerockclient.serversession.states.register import StateRegister from FileRockSharedLibraries.Communication.Messages import SYNC_FILES_LIST from filerockclient.interfaces import PStatuses components = setup_server_session( __file__, adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock) # Note: the StateRegister singleton has been initialized by # ServerSession's constructor syncstart_state = StateRegister.get('SyncStartState') def terminate(): components['real']['server_session'].terminate() return syncstart_state # DisconnectedState is the default initial state, but we want to start # from SyncStartState disconnected_state_mock = disconnectedstate_cls_mock() disconnected_state_mock.do_execute.return_value = syncstart_state # Stop the test when we get to SyncDownloadingLeavesState download_state_mock = downloadstate_cls_mock() download_state_mock.do_execute.side_effect = terminate # There is nothing in the warebox components['mock']['warebox'].get_content.return_value = [] components['real']['metadata'].set('trusted_basis', 'TRUSTEDBASIS') # Send ServerSession a scenario with one file to download. storage_content = [ { u'key': u'File.txt', u'etag': u'"d41d8cd98f00b204e9800998ecf8427e"', u'lmtime': u'1970-01-01T10:00:00.000Z', u'size': u'1' } ] msg = SYNC_FILES_LIST('SYNC_FILES_LIST', { 'basis': 'NEWBASIS', 'dataset': storage_content, 'last_commit_client_id': '0', 'last_commit_client_hostname': 'myhostname', 'last_commit_client_platform': 'myplatform', 'last_commit_timestamp': 'mytimestamp', 'user_quota': '0', 'used_space': '100' }) components['real']['input_queue'].put(msg, 'servermessage') def user_accepts_sync(what, content, client_basis, server_basis): assert_equal(client_basis, 'TRUSTEDBASIS') assert_equal(server_basis, 'NEWBASIS') assert_equal(len(content), 1) assert_equal(content[0]['pathname'], 'File.txt') assert_equal(content[0]['status'], PStatuses.DOWNLOADNEEDED) assert_equal(content[0]['size'], 1) return 'ok' components['mock']['ui_controller'].ask_for_user_input.side_effect = user_accepts_sync components['real']['server_session'].start() components['real']['server_session'].join() # We expect to download a file assert_true(components['mock']['ui_controller'].ask_for_user_input.called) assert_equal(components['real']['storage_cache'].get_all(), []) assert_false(components['real']['input_queue'].empty(['operation'])) operation, _ = components['real']['input_queue'].get(['operation']) assert_equal(operation.pathname, 'File.txt') assert_equal(operation.verb, 'DOWNLOAD')
def test_download_operations_from_clean_state(adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock): from filerockclient.serversession.states.register import StateRegister from FileRockSharedLibraries.Communication.Messages import SYNC_FILES_LIST from filerockclient.interfaces import PStatuses components = setup_server_session(__file__, adapter_mock, workerpool_mock, connection_lifekeeper, disconnectedstate_cls_mock, downloadstate_cls_mock) # Note: the StateRegister singleton has been initialized by # ServerSession's constructor syncstart_state = StateRegister.get('SyncStartState') def terminate(): components['real']['server_session'].terminate() return syncstart_state # DisconnectedState is the default initial state, but we want to start # from SyncStartState disconnected_state_mock = disconnectedstate_cls_mock() disconnected_state_mock.do_execute.return_value = syncstart_state # Stop the test when we get to SyncDownloadingLeavesState download_state_mock = downloadstate_cls_mock() download_state_mock.do_execute.side_effect = terminate # There is nothing in the warebox components['mock']['warebox'].get_content.return_value = [] components['real']['metadata'].set('trusted_basis', 'TRUSTEDBASIS') # Send ServerSession a scenario with one file to download. storage_content = [{ u'key': u'File.txt', u'etag': u'"d41d8cd98f00b204e9800998ecf8427e"', u'lmtime': u'1970-01-01T10:00:00.000Z', u'size': u'1' }] msg = SYNC_FILES_LIST( 'SYNC_FILES_LIST', { 'basis': 'NEWBASIS', 'dataset': storage_content, 'last_commit_client_id': '0', 'last_commit_client_hostname': 'myhostname', 'last_commit_client_platform': 'myplatform', 'last_commit_timestamp': 'mytimestamp', 'user_quota': '0', 'used_space': '100' }) components['real']['input_queue'].put(msg, 'servermessage') def user_accepts_sync(what, content, client_basis, server_basis): assert_equal(client_basis, 'TRUSTEDBASIS') assert_equal(server_basis, 'NEWBASIS') assert_equal(len(content), 1) assert_equal(content[0]['pathname'], 'File.txt') assert_equal(content[0]['status'], PStatuses.DOWNLOADNEEDED) assert_equal(content[0]['size'], 1) return 'ok' components['mock'][ 'ui_controller'].ask_for_user_input.side_effect = user_accepts_sync components['real']['server_session'].start() components['real']['server_session'].join() # We expect to download a file assert_true(components['mock']['ui_controller'].ask_for_user_input.called) assert_equal(components['real']['storage_cache'].get_all_records(), []) assert_false(components['real']['input_queue'].empty(['operation'])) operation, _ = components['real']['input_queue'].get(['operation']) assert_equal(operation.pathname, 'File.txt') assert_equal(operation.verb, 'DOWNLOAD')
def _pass_to_next_state(self): """Go to the commit state. """ self.logger.debug( u"All operations have been authorized, let's commit.") self._set_next_state(StateRegister.get('CommitState'))
def _handle_message_SYNC_FILES_LIST(self, message): """Received the remote filelist from the server. Compute differences, perform integrity checks on it and ask the user confirmation to proceed with the synchronization. Message: SYNC_FILES_LIST Parameters: last_commit_client_id: String or None last_commit_client_hostname: String or None last_commit_client_platform: String or None last_commit_timestamp: Number used_space: Number basis: String user_quota: Number optional parameters: plan: a dictionary as follows (mandatory) { id: <plan_id>, # a number space: <plan_space_in_GB>, # a number (within a plan this is mandatory and 'not None') price: <price_in_$>, # a number (if absent or ==None it means "free") payment_type: <(SINGLE|SUBSCRIPTION)>, # unicode (present if price is not None) payment_recurrence: <(MONTHLY|YEARLY)> # unicode (present if price is not None) } expires_on: <GMT-Date-or-None> # a number representing a unix timestamp UTC (mandatory) (it might None if plan is "forever", this is the expiration date of the subscription, it does not change when in grace time). status: <(TRIAL|ACTIVE|GRACE|SUSPENDED|MAINTAINANCE)> # unicode (mandatory) """ storage_content = message.getParameter('dataset') self.storage_content = storage_content self._validate_storage_content(storage_content) self.server_basis = message.getParameter('basis') self.candidate_basis = self._try_load_candidate_basis() self.client_basis = self._load_trusted_basis() fields = [ 'last_commit_client_id', 'last_commit_client_hostname', 'last_commit_client_platform', 'last_commit_timestamp', 'used_space', 'user_quota', 'plan', 'status', 'expires_on' ] info = dict(map(lambda f: (f, message.getParameter(f)), fields)) # trial # info.update(status='ACTIVE_TRIAL', # expires_on=1366814411, # plan=dict( # space=1 # ) # ) # beta # info.update(status='ACTIVE_BETA', # expires_on=None, # plan=dict( # space=3 # ) # ) # expired # info.update(status='ACTIVE_GRACE', # expires_on=1366810000, # plan=dict( # space=1, # payment_type='SUBSCRIPTION', # payment_recurrence='MONTHLY' # ) # ) # good yearly # info.update(status='ACTIVE_PAID', # expires_on=1366810000, # plan=dict( # space=1, # payment_type='SUBSCRIPTION', # payment_recurrence='YEARLY' # ) # ) self._context._ui_controller.update_session_info(info) # No blacklisted pathname should be found on the storage. If any, tell # the user and then shut down the application. remote_pathnames = [entry['key'] for entry in storage_content] blacklisted = filter(lambda x: self._context.warebox.is_blacklisted(x), remote_pathnames) if len(blacklisted) > 0: self.logger.critical( 'The following blacklisted pathnames have been found on the ' 'storage %s' % format_to_log(blacklisted)) self._context._ui_controller.ask_for_user_input( 'blacklisted_pathname_on_storage', blacklisted) # self._internal_facade.terminate() self._context._internal_facade.pause() return # Detect actions to be taken for synchronization as well as conflicts self.logger.debug(u"Starting computing the three-way diff...") try: self._context.startup_synchronization.prepare( storage_content, self._context.must_die) except ExecutionInterrupted: self.logger.debug(u'ExecutionInterrupted, terminating...') self._set_next_state(StateRegister.get('DisconnectedState')) return self.logger.debug(u"Finished computing the three-way diff.") # Conflicts on encrypted files need extra data from the server to be # solved. If there are any, handle them. encrypted_pathnames = CryptoUtils.filter_encrypted_pathname( self._context.startup_synchronization.edit_conflicts) if len(encrypted_pathnames) > 0: self.logger.debug(u'Encrypted file in conflict: %r' % encrypted_pathnames) message = SYNC_GET_ENCRYPTED_FILES_IVS( 'SYNC_GET_ENCRYPTED_FILES_IVS', {'requested_files_list': encrypted_pathnames}) self._context.output_message_queue.put(message) return # If there are no conflicts on encrypted files, just proceed normally. try: if self._check_hash_mismatch(): self._start_syncing() else: # self._internal_facade.terminate() self._context._internal_facade.pause() except HashMismatchException as excp: self.logger.critical('Integrity error %s' % excp) self._set_next_state(StateRegister.get('IntegrityErrorState'))