Repository navigation
Add full FinTS parser. - #34
Conversation
|
Sounds great! I will probably not have time to look at this in detail this week, but at a quick glance it looks like a good direction for this project. |
|
Ok, so I did a little work, and, uhhm, this might need to drop the word "minimal" in the project name. ;) I'm still fiddling with code structure and what should go into which package, so it's not pushed here yet, experimenting in https://github.com/henryk/python-fints/tree/parser-ng. Also: tests, with ~80% coverage of the new code. In [1]: from fints.formals import *
In [2]: import fints.segments
In [3]: from fints.parser import FinTS3Parser
In [4]: data = \
...: (b'HNHBK:1:3+000000000428+300+430711670077=043999659571CN9D=+2+430711670077=043'
...: b"999659571CN9D=:2'HNVSK:998:3+PIN:1+998+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+"
...: b"2:2:13:@8@00000000:5:1+280:15050500:hermes:S:0:0+0'HNVSD:999:1+@195@HNSHK:2:"
...: b'4+PIN:1+999+9166926+1+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+1+1:999:1+6:10:16'
...: b"+280:15050500:hermes:S:0:0'HIRMG:3:2+0010::Nachricht entgegengenommen.+0100:"
...: b":Dialog beendet.'HNSHA:4:2+9166926''HNHBS:5:1+2'")
...:
In [5]: m = FinTS3Parser().parse_message(data)
In [6]: m.print_nested()
SegmentSequence([
fints.segments.HNHBK3(
header = fints.formals.SegmentHeader(
type = 'HNHBK',
number = 1,
version = 3,
reference = None,
),
message_size = '000000000428',
hbci_version = 300,
dialogue_id = '430711670077=043999659571CN9D=',
message_number = 2,
reference_message = fints.formals.ReferenceMessage(
dialogue_id = '430711670077=043999659571CN9D=',
message_number = 2,
),
),
fints.segments.HNVSK3(
header = fints.formals.SegmentHeader(
type = 'HNVSK',
number = 998,
version = 3,
reference = None,
),
security_profile = fints.formals.SecurityProfile(
security_method = 'PIN',
security_method_version = 1,
),
security_function = '998',
security_role = '1',
security_identification_details = fints.formals.SecurityIdentificationDetails(
name_party = '2',
cid = None,
identifier_party = 'oIm3BlHv6mQBAADYgbPpp+kWrAQA',
),
security_datetime = fints.formals.SecurityDateTime(
datetime_type = '1',
date = None,
time = None,
),
encryption_algorithm = fints.formals.EncryptionAlgorithm(
usage_encryption = '2',
operation_mode = '2',
encryption_algorithm = '13',
algorithm_parameter_value = b'00000000',
algorithm_parameter_name = '5',
algorithm_parameter_iv_name = '1',
algorithm_parameter_iv_value = None,
),
key_name = fints.formals.KeyName(
bank_identifier = fints.formals.BankIdentifier(
country_identifier = '280',
bank_code = '15050500',
),
user_id = 'hermes',
key_type = 'S',
key_number = 0,
key_version = 0,
),
compression_function = '0',
certificate = fints.formals.Certificate(
certificate_type = None,
certificate_content = None,
),
),
fints.segments.HNVSD1(
header = fints.formals.SegmentHeader(
type = 'HNVSD',
number = 999,
version = 1,
reference = None,
),
data = SegmentSequence([
fints.segments.HNSHK4(
header = fints.formals.SegmentHeader(
type = 'HNSHK',
number = 2,
version = 4,
reference = None,
),
security_profile = fints.formals.SecurityProfile(
security_method = 'PIN',
security_method_version = 1,
),
security_function = '999',
security_reference = '9166926',
security_application_area = '1',
security_role = '1',
security_identification_details = fints.formals.SecurityIdentificationDetails(
name_party = '2',
cid = None,
identifier_party = 'oIm3BlHv6mQBAADYgbPpp+kWrAQA',
),
security_reference_number = 1,
security_datetime = fints.formals.SecurityDateTime(
datetime_type = '1',
date = None,
time = None,
),
hash_algorithm = fints.formals.HashAlgorithm(
usage_hash = '1',
hash_algorithm = '999',
algorithm_parameter_name = '1',
algorithm_parameter_value = None,
),
signature_algorithm = fints.formals.SignatureAlgorithm(
usage_signature = '6',
signature_algorithm = '10',
operation_mode = '16',
),
key_name = fints.formals.KeyName(
bank_identifier = fints.formals.BankIdentifier(
country_identifier = '280',
bank_code = '15050500',
),
user_id = 'hermes',
key_type = 'S',
key_number = 0,
key_version = 0,
),
certificate = fints.formals.Certificate(
certificate_type = None,
certificate_content = None,
),
),
fints.segments.FinTS3Segment(
header = fints.formals.SegmentHeader(
type = 'HIRMG',
number = 3,
version = 2,
reference = None,
),
_additional_data=
[['0010', None, 'Nachricht entgegengenommen.'], ['0100', None, 'Dialog beendet.']],
),
fints.segments.FinTS3Segment(
header = fints.formals.SegmentHeader(
type = 'HNSHA',
number = 4,
version = 2,
reference = None,
),
_additional_data=
['9166926'],
),
]),
),
fints.segments.HNHBS1(
header = fints.formals.SegmentHeader(
type = 'HNHBS',
number = 5,
version = 1,
reference = None,
),
message_number = 2,
),
])
In [7]: m2 = SegmentSequence([ ..... Everything that was output after In [6] .... ])
In [8]: m2
Out[8]: SegmentSequence([fints.segments.HNHBK3(header=fints.formals.SegmentHeader(type='HNHBK', number=1, version=3, reference=None), message_size='000000000428', hbci_version=300, dialogue_id='430711670077=043999659571CN9D=', message_number=2, reference_message=fints.formals.ReferenceMessage(dialogue_id='430711670077=043999659571CN9D=', message_number=2)), fints.segments.HNVSK3(header=fints.formals.SegmentHeader(type='HNVSK', number=998, version=3, reference=None), security_profile=fints.formals.SecurityProfile(security_method='PIN', security_method_version=1), security_function='998', security_role='1', security_identification_details=fints.formals.SecurityIdentificationDetails(name_party='2', cid=None, identifier_party='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_datetime=fints.formals.SecurityDateTime(datetime_type='1', date=None, time=None), encryption_algorithm=fints.formals.EncryptionAlgorithm(usage_encryption='2', operation_mode='2', encryption_algorithm='13', algorithm_parameter_value=b'00000000', algorithm_parameter_name='5', algorithm_parameter_iv_name='1', algorithm_parameter_iv_value=None), key_name=fints.formals.KeyName(bank_identifier=fints.formals.BankIdentifier(country_identifier='280', bank_code='15050500'), user_id='hermes', key_type='S', key_number=0, key_version=0), compression_function='0', certificate=fints.formals.Certificate(certificate_type=None, certificate_content=None)), fints.segments.HNVSD1(header=fints.formals.SegmentHeader(type='HNVSD', number=999, version=1, reference=None), data=SegmentSequence([fints.segments.HNSHK4(header=fints.formals.SegmentHeader(type='HNSHK', number=2, version=4, reference=None), security_profile=fints.formals.SecurityProfile(security_method='PIN', security_method_version=1), security_function='999', security_reference='9166926', security_application_area='1', security_role='1', security_identification_details=fints.formals.SecurityIdentificationDetails(name_party='2', cid=None, identifier_party='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_reference_number=1, security_datetime=fints.formals.SecurityDateTime(datetime_type='1', date=None, time=None), hash_algorithm=fints.formals.HashAlgorithm(usage_hash='1', hash_algorithm='999', algorithm_parameter_name='1', algorithm_parameter_value=None), signature_algorithm=fints.formals.SignatureAlgorithm(usage_signature='6', signature_algorithm='10', operation_mode='16'), key_name=fints.formals.KeyName(bank_identifier=fints.formals.BankIdentifier(country_identifier='280', bank_code='15050500'), user_id='hermes', key_type='S', key_number=0, key_version=0), certificate=fints.formals.Certificate(certificate_type=None, certificate_content=None)), fints.segments.FinTS3Segment(header=fints.formals.SegmentHeader(type='HIRMG', number=3, version=2, reference=None), _additional_data=[['0010', None, 'Nachricht entgegengenommen.'], ['0100', None, 'Dialog beendet.']]), fints.segments.FinTS3Segment(header=fints.formals.SegmentHeader(type='HNSHA', number=4, version=2, reference=None), _additional_data=['9166926'])])), fints.segments.HNHBS1(header=fints.formals.SegmentHeader(type='HNHBS', number=5, version=1, reference=None), message_number=2)])
In [9]: m2.segments[2].data.segments[0].security_profile
Out[9]: fints.formals.SecurityProfile(security_method='PIN', security_method_version=1) |
|
Hi! I haven't looked at the implementation in detail yet, just at the output and at the formals module, and I love it ❤️ Please let me know if there are any specific design decisions you'd like to have feedback on! |
|
Ok, all the core work is done. I've successfully retrieved an account list (not quite a statement yet ;) with the new code in the parser path. Currently I haven't touched the generating/sending bit (and renamed FinTS3Segment to FinTS3SegmentOLD to keep the code running), I'm still slightly unclear as to how best do the "encryption" envelope thing, and segment numbers. (The current code hardcodes segment numbers at segment constructor call time, that should ideally be automatically done by the message class.) I'm also somewhat unhappy with the property names. I've loosely translated most of them just because I needed them to be there to go on. Once this API is in use, it's hard to change the property names, so they should be reviewed and improved before that. What needs to be done (and I don't want to do alone):
What would be nice:
|
|
This is amazing! Unfortunately, I lack the time to help at the moment since I'm to deeply involved with other problems and will be on vacation August 22–30th, but I'll be happy to help or take over at some point if I have more time on my hands. |
|
Getting closer, first working transfer sent. I have two new secondary directives when designing the API:
ad 1) Looks like this: client = FinTS3PinTanClient(..., set_data=None)
with client:
response = client.start_sepa_transfer(...)
dialog_data = client.pause_dialog()
challenge_data = response.get_data()
client_data = client.get_data()
# Store challenge_data, dialog_data and client_data out-of-band somewhere
# Ask the user to respond to response.hitan.challenge
# ... Some time passes ...
# Later, possibly in a different process, restore the state
client = FinTS3PinTanClient(..., set_data=client_data)
challenge = NeedRetryResponse.from_data(challenge_data)
with client.resume_dialog(dialog_data):
client.send_tan(challenge, tan)ad 2) Methods like The developer point of view is: def start_sepa_transfer(...):
return self._send_with_possible_retry(dialog, seg, self._continue_start_sepa_transfer)
def _continue_start_sepa_transfer(self, command_seg, response):
# FIXME Properly find return code
return Truewhere As of now I've not done HKTAN#6 on purpose since that implies Strong Customer Authentication (SCA) which comes with its own set of cans of worms (including TANs for dialogue initiation/"login", TAN exemptions for certain commands based on bank decisions). |
|
Side note: I've established an anonymous dialogue with most banks in Germany, 2045 unique BLZ/URL combinations, to test the parser. For future reference here is the statistics of supported HITANS versions: (e.g. 1172 banks support only HITAN#5, 399 banks support HITAN#1 and HITAN#3, etc.) |
|
I'm mostly content with functionality now (and, in parallel, building a byro-fints plugin to use it). Still not everything cleaned up, not all FIXMEs removed. Goal: Test coverage also for the client (with mocked server). I've broken the API on purpose in most places, so this should get a new major version number.
Question for @raphaelm: I'm now working on the TAN part, formatting of the challenge. I'd like to pull in https://pypi.org/project/bleach/ as a dependency to clean-up "challenge_structured = True" challenges. (I can do this in byro-fints, but maybe it's better in the library for everyone to use.) |
|
+1 for bleach as a dependency. I'm still (now back from vacation, but with an event coming up in a few days) struggling to find an afternoon to invest in this, please bear with me for another while. |
|
(also +1 for breaking the API if it gets better that way) |
raphaelm
left a comment
There was a problem hiding this comment.
Hi! :) I did a first rough review of this, mostly of the documentation to get an idea of the design. So far, I love it a lot. I attached 2 small design questions that I'd like to hear your opinion about.
I plan to dive deeper into the code next week, but will probably merge it more or less as-is, test around and then go from there. Among the FIXMEs, is there any one that you think we should absolutely tacke before that? I could try to resolve them myself.
|
As for the FIXMEs: The one in The one in In The names for |
|
FYI, I just tried using this branch for our actual monthly SEPA batch debit. I committed a few small fixes, but now it works.
I wouldn't worry too much about extending that tuple (since it's unlikely anyone creates it manually right now), but do we need to? The IBAN does contain country information. |
|
TODO for myself to get this done:
|
1f0a328 to
99e551f
Compare
…ments are now lists of lists. Fully supports all escaping and binary content. Decodes 'normal' data from ISO-8859-1 to Python strings, leaves binary data as binary. Still broken: get_holdings()/HIWPD
Otherwise, this breaks with Nationale Kontoverbindung nicht erlaubt. (TRP)
99e551f to
40465ed
Compare
Codecov Report
@@ Coverage Diff @@
## master #34 +/- ##
=========================================
Coverage ? 89.43%
=========================================
Files ? 23
Lines ? 2933
Branches ? 0
=========================================
Hits ? 2623
Misses ? 310
Partials ? 0
Continue to review full report at Codecov.
|
Do you have that script still around? Would probably be useful to have it somewhere to re-try this later, and if it's just to use it as a test for parser changes. |
|
Okay, before this stays around forever, let's merge this. My todo is over, my tests are passing. Holdings probably won't work, but maybe @gonium wants to test that and report back? I still don't have access to any. |
Significantly changes the internal API, as segments are now lists of lists.
Fully supports all escaping and binary content. Decodes 'normal' data from ISO-8859-1 to Python strings, leaves binary data as binary.
Still broken:
get_holdings()/HIWPDIn the end I want a proper object based internal representation that can be parsed from and serialized into the network, with classes and fields like Django models. For the time being this only touches the receiving code and simplifies all the instances of
split_for_data_groups()andsplit_for_data_elements().The handling of FinTS security (segment HNVSD) is a bit hacky: The contents of HNVSD are separately parsed and stored in
FinTSResponse::payload, and_find_segmentswill first search the outer message and then the inner message, to satisfy the existing code.