From 87b44b39a4f0813000aea1bec33b1aef579e7b82 Mon Sep 17 00:00:00 2001 From: Antoine A <> Date: Thu, 28 Mar 2024 11:48:15 +0100 Subject: Run HEV and HKD in ebics-setup --- .../main/kotlin/tech/libeufin/nexus/EbicsSetup.kt | 30 +++++++- .../main/kotlin/tech/libeufin/nexus/Iso20022.kt | 6 +- .../kotlin/tech/libeufin/nexus/XmlCombinators.kt | 3 - .../libeufin/nexus/ebics/EbicsAdministrative.kt | 89 ++++++++++++++++++++++ .../tech/libeufin/nexus/ebics/EbicsCommon.kt | 10 +++ testbench/src/main/kotlin/Main.kt | 2 + 6 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt index dd13eb30..a2b0934a 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt @@ -198,14 +198,21 @@ class EbicsSetup: CliktCommand("Set up the EBICS subscriber") { */ override fun run() = cliCmd(logger, common.log) { val cfg = extractEbicsConfig(common.config) - // Config is sane. Go (maybe) making the private keys. - val clientKeys = loadOrGenerateClientKeys(cfg.clientPrivateKeysPath) val client = HttpClient { install(HttpTimeout) { // It can take a lot of time for the bank to generate documents socketTimeoutMillis = 5 * 60 * 1000 } } + + // Check EBICS 3 support + val versions = HEV(client, cfg) + logger.debug("HEV: $versions") + if (!versions.contains(VersionNumber(3.0f, "H005")) && !versions.contains(VersionNumber(3.02f, "H005"))) { + throw Exception("EBICS 3 is not supported by your bank") + } + + val clientKeys = loadOrGenerateClientKeys(cfg.clientPrivateKeysPath) // Privs exist. Upload their pubs val keysNotSub = !clientKeys.submitted_ini if ((!clientKeys.submitted_ini) || forceKeysResubmission) @@ -215,7 +222,7 @@ class EbicsSetup: CliktCommand("Set up the EBICS subscriber") { if ((!clientKeys.submitted_hia) || forceKeysResubmission) doKeysRequestAndUpdateState(cfg, clientKeys, client, HIA) - // Checking if the bank keys exist on disk. + // Checking if the bank keys exist on disk var bankKeys = loadBankKeys(cfg.bankPublicKeysPath) if (bankKeys == null) { doKeysRequestAndUpdateState(cfg, clientKeys, client, HPB) @@ -237,6 +244,23 @@ class EbicsSetup: CliktCommand("Set up the EBICS subscriber") { throw Exception("Could not set bank keys as accepted on disk", e) } } + + // Check account information + logger.info("Doing administrative request HKD") + try { + ebicsDownload(client, cfg, clientKeys, bankKeys, EbicsOrder.V3("HKD"), null, null) { stream -> + val account = EbicsAdministrative.parseHKD(stream) + // TODO parse and check more information + if (account.currency != null && account.currency != cfg.currency) + logger.warn("Expected CURRENCY '${cfg.currency}' from config got '${account.currency}' from bank") + if (account.iban != null && account.iban != cfg.account.iban) + logger.warn("Expected IBAN '${cfg.account.iban}' from config got '${account.iban}' from bank") + if (account.name != null && account.name != cfg.account.name) + logger.warn("Expected NAME '${cfg.account.name}' from config got '${account.name}' from bank") + } + } catch (e: Exception) { + logger.warn("HKD failed: ${e.fmt()}") + } println("setup ready") } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt index 6269377a..103795d6 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -145,7 +145,7 @@ data class CustomerAck( /** Parse HAC pain.002 XML file */ fun parseCustomerAck(xml: InputStream): List { - return destructXml(xml, "Document") { + return XmlDestructor.fromStream(xml, "Document") { one("CstmrPmtStsRpt").map("OrgnlPmtInfAndSts") { val actionType = one("OrgnlPmtInfId").enum() one("StsRsnInf") { @@ -219,7 +219,7 @@ fun parseCustomerPaymentStatusReport(xml: InputStream): PaymentStatus { Reason(code, "") } } - return destructXml(xml, "Document") { + return XmlDestructor.fromStream(xml, "Document") { // TODO handle batch status one("CstmrPmtStsRpt") { val (msgId, msgCode, msgReasons) = one("OrgnlGrpInfAndSts") { @@ -288,7 +288,7 @@ fun parseTxNotif( fun notificationForEachTx( directionLambda: XmlDestructor.(Instant, Boolean, String?) -> Unit ) { - destructXml(notifXml, "Document") { + XmlDestructor.fromStream(notifXml, "Document") { opt("BkToCstmrDbtCdtNtfctn")?.each("Ntfctn") { each("Ntry") { val reversal = opt("RvslInd")?.bool() ?: false diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt index fbed0b7b..1ad16f6c 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt @@ -199,6 +199,3 @@ class XmlDestructor internal constructor(private val el: Element) { } } } - -fun destructXml(xml: InputStream, root: String, f: XmlDestructor.() -> T): T - = XmlDestructor.fromStream(xml, root, f) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt new file mode 100644 index 00000000..464db37f --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt @@ -0,0 +1,89 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * + */ + +package tech.libeufin.nexus.ebics + +import org.w3c.dom.Document +import tech.libeufin.common.crypto.CryptoUtil +import tech.libeufin.common.* +import tech.libeufin.nexus.* +import tech.libeufin.nexus.BankPublicKeysFile +import tech.libeufin.nexus.ClientPrivateKeysFile +import java.io.InputStream +import java.time.Instant +import java.time.ZoneId +import java.util.* +import javax.xml.datatype.DatatypeFactory +import java.security.interfaces.* +import tech.libeufin.nexus.ebics.EbicsKeyMng.Order.* + +data class VersionNumber(val number: Float, val schema: String) { + override fun toString(): String = "$number:$schema" +} + +data class AccountInfo( + val currency: String?, + val iban: String?, + val name: String? +) + +object EbicsAdministrative { + fun HEV(cfg: NexusConfig): ByteArray { + return XmlBuilder.toBytes("ebicsHEVRequest") { + attr("xmlns", "http://www.ebics.org/H000") + el("HostID", cfg.ebicsHostId) + } + } + + fun parseHEV(doc: Document): EbicsResponse> { + return XmlDestructor.fromDoc(doc, "ebicsHEVResponse") { + val technicalCode = one("SystemReturnCode") { + EbicsReturnCode.lookup(one("ReturnCode").text()) + } + val versions = map("VersionNumber") { + VersionNumber(text().toFloat(), attr("ProtocolVersion")) + } + EbicsResponse( + technicalCode = technicalCode, + bankCode = EbicsReturnCode.EBICS_OK, + content = versions + ) + } + } + + fun parseHKD(stream: InputStream): AccountInfo { + return XmlDestructor.fromStream(stream, "HKDResponseOrderData") { + var currency: String? = null + var iban: String? = null + var name: String? = null + one("PartnerInfo") { + name = opt("AddressInfo")?.one("Name")?.text() + opt("AccountInfo") { + currency = attr("Currency") + each("AccountNumber") { + if (attr("international") == "true") { + iban = text() + } + } + } + } + AccountInfo(currency, iban, name) + } + } +} diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt index f87c55b3..aa7d40e3 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -258,6 +258,16 @@ suspend fun ebicsDownload( Unit } +suspend fun HEV( + client: HttpClient, + cfg: NexusConfig +): List { + logger.info("Doing administrative request HEV") + val req = EbicsAdministrative.HEV(cfg) + val xml = client.postToBank(cfg.hostBaseUrl, req, "HEV") + return EbicsAdministrative.parseHEV(xml).okOrFail("HEV") +} + /** * Signs and the encrypts the data to send via EBICS. * diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt index d6d9c3e9..7a3216bb 100644 --- a/testbench/src/main/kotlin/Main.kt +++ b/testbench/src/main/kotlin/Main.kt @@ -53,6 +53,7 @@ fun ask(question: String): String? { fun CliktCommand.run(arg: String): Boolean { val res = this.test(arg) + print(res.output) if (res.statusCode != 0) { println("\u001b[;31mERROR ${res.statusCode}\u001b[0m") } else { @@ -142,6 +143,7 @@ class Cli : CliktCommand("Run integration tests on banks provider") { put("status", "Fetch CustomerPaymentStatusReport", "ebics-fetch $ebicsFlags status") put("notification", "Fetch BankToCustomerDebitCreditNotification", "ebics-fetch $ebicsFlags notification") put("submit", "Submit pending transactions", "ebics-submit $ebicsFlags") + put("setup", "Setup", "ebics-setup $flags") put("reset-keys", suspend { if (kind.test) { clientKeysPath.deleteIfExists() -- cgit v1.2.3