import io import logging import pickle from .connection import FinTSConnectionError from .exceptions import * from .formals import CUSTOMER_ID_ANONYMOUS, Language2, SystemIDStatus from .message import FinTSCustomerMessage, MessageDirection from .segments.auth import HKIDN2, HKVVB3 from .segments.dialog import HKEND1 from .segments.message import HNHBK3, HNHBS1 from .utils import compress_datablob, decompress_datablob logger = logging.getLogger(__name__) DIALOG_ID_UNASSIGNED = '0' DATA_BLOB_MAGIC = b'python-fints_DIALOG_DATABLOB' class FinTSDialog: def __init__(self, client=None, lazy_init=False, enc_mechanism=None, auth_mechanisms=None): self.client = client self.next_message_number = dict((v, 1) for v in MessageDirection) self.messages = dict((v, {}) for v in MessageDirection) self.auth_mechanisms = auth_mechanisms or [] self.enc_mechanism = enc_mechanism self.open = False self.need_init = True self.lazy_init = lazy_init self.dialog_id = DIALOG_ID_UNASSIGNED self.paused = False self._context_count = 0 def __enter__(self): if self._context_count == 0: if not self.lazy_init: self.init() self._context_count += 1 return self def __exit__(self, exc_type, exc_value, traceback): self._context_count -= 1 if not self.paused: if self._context_count == 0: self.end() def init(self, *extra_segments): if self.paused: raise FinTSDialogStateError("Cannot init() a paused dialog") from fints.client import FinTSClientMode, NeedTANResponse if self.client.mode == FinTSClientMode.OFFLINE: raise FinTSDialogOfflineError("Cannot open a dialog with mode=FinTSClientMode.OFFLINE. " "This is a control flow error, no online functionality " "should have been attempted with this FinTSClient object.") if self.need_init and not self.open: segments = [ HKIDN2( self.client.bank_identifier, self.client.customer_id, self.client.system_id, SystemIDStatus.ID_NECESSARY if self.client.customer_id != CUSTOMER_ID_ANONYMOUS else SystemIDStatus.ID_UNNECESSARY ), HKVVB3( self.client.bpd_version, self.client.upd_version, Language2.DE, self.client.product_name, self.client.product_version ), ] if self.client.mode == FinTSClientMode.INTERACTIVE and self.client.get_tan_mechanisms(): tan_seg = self.client._get_tan_segment(segments[0], '4') segments.append(tan_seg) else: tan_seg = None for s in extra_segments: segments.append(s) try: self.open = True retval = self.send(*segments, internal_send=True) if tan_seg: for resp in retval.responses(tan_seg): if resp.code == '0030': self.client.init_tan_response = NeedTANResponse( None, retval.find_segment_first('HITAN'), '_continue_dialog_initialization', self.client.is_challenge_structured() ) self.need_init = False return retval except Exception as e: self.open = False if isinstance(e, (FinTSConnectionError, FinTSClientError)): raise else: raise FinTSDialogInitError("Couldn't establish dialog with bank, Authentication data wrong?") from e finally: self.lazy_init = False def end(self): if self.paused: raise FinTSDialogStateError("Cannot end() on a paused dialog") if self.open: response = self.send(HKEND1(self.dialog_id), internal_send=True) self.open = False def send(self, *segments, **kwargs): internal_send = kwargs.pop('internal_send', False) if self.paused: raise FinTSDialogStateError("Cannot send() on a paused dialog") if not self.open: if self.lazy_init and self.need_init: self.init() if not self.open: raise FinTSDialogStateError("Cannot send on dialog that is not open") message = self.new_customer_message() for s in segments: message += s self.finish_message(message) assert message.segments[0].message_number == self.next_message_number[message.DIRECTION] self.messages[message.DIRECTION][message.segments[0].message_number] = message self.next_message_number[message.DIRECTION] += 1 response = self.client.connection.send(message) # assert response.segments[0].message_number == self.next_message_number[response.DIRECTION] # FIXME Better handling of HKEND in exception case self.messages[response.DIRECTION][response.segments[0].message_number] = response self.next_message_number[response.DIRECTION] += 1 if self.enc_mechanism: self.enc_mechanism.decrypt(message) for auth_mech in self.auth_mechanisms: auth_mech.verify(message) if self.dialog_id == DIALOG_ID_UNASSIGNED: seg = response.find_segment_first(HNHBK3) if not seg: raise FinTSDialogError('Could not find dialog_id') self.dialog_id = seg.dialog_id self.client.process_response_message(self, response, internal_send=internal_send) return response def new_customer_message(self): if self.paused: raise FinTSDialogStateError("Cannot call new_customer_message() on a paused dialog") message = FinTSCustomerMessage(self) message += HNHBK3(0, 300, self.dialog_id, self.next_message_number[message.DIRECTION]) for auth_mech in self.auth_mechanisms: auth_mech.sign_prepare(message) return message def finish_message(self, message): if self.paused: raise FinTSDialogStateError("Cannot call finish_message() on a paused dialog") # Create signature(s) in reverse order: from inner to outer for auth_mech in reversed(self.auth_mechanisms): auth_mech.sign_commit(message) message += HNHBS1(message.segments[0].message_number) if self.enc_mechanism: self.enc_mechanism.encrypt(message) message.segments[0].message_size = len(message.render_bytes()) def pause(self): # FIXME Document, test if self.paused: raise FinTSDialogStateError("Cannot pause a paused dialog") external_dialog = self external_client = self.client class SmartPickler(pickle.Pickler): def persistent_id(self, obj): if obj is external_dialog: return "dialog" if obj is external_client: return "client" return None pickle_out = io.BytesIO() SmartPickler(pickle_out, protocol=4).dump({ k: getattr(self, k) for k in [ 'next_message_number', 'messages', 'auth_mechanisms', 'enc_mechanism', 'open', 'need_init', 'lazy_init', 'dialog_id', ] }) data_pickled = pickle_out.getvalue() self.paused = True return compress_datablob(DATA_BLOB_MAGIC, 1, {'data_bin': data_pickled}) @classmethod def create_resume(cls, client, blob): retval = cls(client=client) decompress_datablob(DATA_BLOB_MAGIC, blob, retval) return retval def _set_data_v1(self, data): external_dialog = self external_client = self.client class SmartUnpickler(pickle.Unpickler): def persistent_load(self, pid): if pid == 'dialog': return external_dialog if pid == 'client': return external_client raise pickle.UnpicklingError("unsupported persistent object") pickle_in = io.BytesIO(data['data_bin']) data_unpickled = SmartUnpickler(pickle_in).load() for k, v in data_unpickled.items(): setattr(self, k, v)