commit 4bc5f38f571a45d427f73813ec3846bf59413afa
parent 06452b9adc4d149bdb1532a3ea3160909eb51c9a
Author: MS <ms@taler.net>
Date: Thu, 6 Jul 2023 11:21:28 +0200
EBICS 3 fixes.
Removing one stale reference to schema H004 in the
new H005 <ebicsResponse>, and avoiding to validate
every response that arrives from the bank.
Diffstat:
7 files changed, 88 insertions(+), 28 deletions(-)
diff --git a/Makefile b/Makefile
@@ -68,6 +68,10 @@ check-cli:
@cd ./cli/tests && ./circuit_test.sh
@cd ./cli/tests && ./debit_test.sh
-.PHONY: pofi
-pofi:
- @./gradlew -q :nexus:pofi
+.PHONY: pofi-get
+pofi-get:
+ @./gradlew -q :nexus:pofi --args="download" # --args="arg1 arg2 .."
+
+.PHONY: pofi-post
+pofi-post:
+ @./gradlew -q :nexus:pofi --args="upload"
diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli
@@ -574,7 +574,6 @@ def new_ebics_connection(
check_response_status(resp)
-
@connections.command(help="Initialize the bank connection.")
@click.argument("connection-name")
@click.pass_obj
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
@@ -169,6 +169,7 @@ suspend fun doEbicsDownloadTransaction(
return EbicsDownloadEmptyResult()
}
else -> {
+ println("Bank raw response: $initResponseStr")
logger.error(
"Bank-technical error at init phase: ${initResponse.bankReturnCode}" +
", for fetching level ${fetchSpec.originalLevel} and transaction ID $transactionID."
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -99,7 +99,8 @@ private fun validateAndStoreCamt(
bankConnectionId: String,
camt: String,
fetchLevel: FetchLevel,
- transactionID: String? = null // the EBICS transaction that carried this camt.
+ transactionID: String? = null, // the EBICS transaction that carried this camt.
+ validateBankContent: Boolean = false
) {
val camtDoc = try {
XMLUtil.parseStringIntoDom(camt)
@@ -107,8 +108,10 @@ private fun validateAndStoreCamt(
catch (e: Exception) {
throw badGateway("Could not parse camt document from EBICS transaction $transactionID")
}
- if (!XMLUtil.validateFromDom(camtDoc))
+ if (validateBankContent && !XMLUtil.validateFromDom(camtDoc)) {
+ logger.error("This document didn't validate: $camt")
throw badGateway("Camt document from EBICS transaction $transactionID is invalid")
+ }
val msgId = camtDoc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId")
logger.info("Camt document '$msgId' received via $fetchLevel.")
@@ -191,7 +194,7 @@ private suspend fun fetchEbicsTransactions(
)
} catch (e: EbicsProtocolError) {
/**
- * Although given a error type, a empty transactions list does
+ * Although given a error type, an empty transactions list does
* not mean anything wrong.
*/
if (e.ebicsTechnicalCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) {
@@ -498,7 +501,17 @@ private fun getStatementSpecAfterDialect(dialect: String? = null, p: EbicsOrderP
"pf" -> EbicsFetchSpec(
orderType = "Z53",
orderParams = p,
- ebics3Service = null,
+ ebics3Service = Ebics3Request.OrderDetails.Service().apply {
+ serviceName = "EOP"
+ messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+ value = "camt.053"
+ version = "04"
+ }
+ scope = "CH"
+ container = Ebics3Request.OrderDetails.Service.Container().apply {
+ containerType = "ZIP"
+ }
+ },
originalLevel = FetchLevel.STATEMENT
)
else -> EbicsFetchSpec(
@@ -519,7 +532,7 @@ private fun getNotificationSpecAfterDialect(dialect: String? = null, p: EbicsOrd
serviceName = "REP"
messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
value = "camt.054"
- version = "04"
+ version = "08"
}
scope = "CH"
container = Ebics3Request.OrderDetails.Service.Container().apply {
@@ -739,6 +752,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol {
)
} catch (e: Exception) {
logger.warn("Fetching transactions (${spec.originalLevel}) excepted: ${e.message}.")
+ e.printStackTrace()
errors.add(e)
}
}
diff --git a/nexus/src/test/kotlin/PostFinance.kt b/nexus/src/test/kotlin/PostFinance.kt
@@ -1,4 +1,7 @@
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.github.ajalt.clikt.core.*
+import com.github.ajalt.clikt.parameters.options.default
+import com.github.ajalt.clikt.parameters.options.option
import io.ktor.client.*
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.transactions.transaction
@@ -11,13 +14,13 @@ import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails
import tech.libeufin.nexus.getConnectionPlugin
import tech.libeufin.nexus.getNexusUser
import tech.libeufin.nexus.server.*
-import tech.libeufin.util.EbicsStandardOrderParams
import tech.libeufin.util.ebics_h005.Ebics3Request
import java.io.BufferedReader
import java.io.File
+import kotlin.system.exitProcess
// Asks a camt.054 to the bank.
-private fun downloadPayment() {
+private fun downloadPayments() {
val httpClient = HttpClient()
runBlocking {
fetchBankAccountTransactions(
@@ -31,14 +34,19 @@ private fun downloadPayment() {
}
}
-/* Simulates one incoming payment for the test platorm's bank account.
+/* Simulates one incoming payment for the 'payee' argument.
+ * It pays the test platform's bank account if none is found.
* The QRR format is NOT used in Taler, it is just convenient.
* */
-private fun uploadQrrPayment() {
+private fun uploadQrrPayment(maybePayee: String? = null) {
+ val payee = if (maybePayee == null) {
+ val localAccount = getBankAccount("foo")
+ localAccount.iban
+ } else maybePayee
val httpClient = HttpClient()
val qrr = """
Product;Channel;Account;Currency;Amount;Reference;Name;Street;Number;Postcode;City;Country;DebtorAddressLine;DebtorAddressLine;DebtorAccount;ReferenceType;UltimateDebtorName;UltimateDebtorStreet;UltimateDebtorNumber;UltimateDebtorPostcode;UltimateDebtorTownName;UltimateDebtorCountry;UltimateDebtorAddressLine;UltimateDebtorAddressLine;RemittanceInformationText
- QRR;PO;CH9789144829733648596;CHF;33;;D009;Musterstrasse;1;1111;Musterstadt;CH;;;;NON;D009;Musterstrasse;1;1111;Musterstadt;CH;;;Taler-Demo
+ QRR;PO;$payee;CHF;33;;D009;Musterstrasse;1;1111;Musterstadt;CH;;;;NON;D009;Musterstrasse;1;1111;Musterstadt;CH;;;Taler-Demo
""".trimIndent()
runBlocking {
doEbicsUploadTransaction(
@@ -69,16 +77,19 @@ private fun uploadQrrPayment() {
* by the sender. Hence, the sender can itself ensure the EndToEndId
* uniqueness.
*/
-private fun uploadPain001Payment() {
+private fun uploadPain001Payment(
+ subject: String,
+ creditorIban: String = "CH9300762011623852957" // random creditor
+) {
transaction {
addPaymentInitiation(
Pain001Data(
- creditorIban = "CH9300762011623852957",
+ creditorIban = creditorIban,
creditorBic = "POFICHBEXXX",
creditorName = "Muster Frau",
sum = "2",
currency = "CHF",
- subject = "Muster Zahlung 0",
+ subject = subject,
endToEndId = "Zufall"
),
getBankAccount("foo")
@@ -89,7 +100,33 @@ private fun uploadPain001Payment() {
runBlocking { ebicsConn.submitPaymentInitiation(httpClient, 1L) }
}
-fun main() {
+class PostFinanceCommand : CliktCommand() {
+ private val myIban by option(
+ help = "IBAN as assigned by the PostFinance test platform."
+ ).default("CH9789144829733648596")
+ override fun run() { prepare(myIban) }
+}
+class Download : CliktCommand("Download the latest camt.054 from the bank") {
+ // Ask 'notification' to the bank.
+ override fun run() {
+ // uploadPain001Payment("auto")
+ downloadPayments()
+ }
+}
+
+class Upload : CliktCommand("Upload a pain.001 to the bank") {
+ private val subject by option(help = "Payment subject").default("Muster Zahlung")
+ override fun run() { uploadPain001Payment(subject) }
+}
+
+class GenIncoming : CliktCommand("Uploads a CSV document to create one incoming payment") {
+ override fun run() {
+ val bankAccount = getBankAccount("foo")
+ uploadQrrPayment(bankAccount.iban)
+ }
+}
+
+private fun prepare(iban: String) {
// Loads EBICS subscriber's keys from disk.
// The keys should be found under libeufin-internal.git/convenience/
val bufferedReader: BufferedReader = File("/tmp/pofi.json").bufferedReader()
@@ -108,11 +145,13 @@ fun main() {
accessDataJson
)
val fooBankAccount = getBankAccount("foo")
+ // Hooks the PoFi details to the local bank account.
+ // No need to run the canonical steps (creating account, downloading bank accounts, ..)
fooBankAccount.defaultBankConnection = getBankConnection("postfinance")
- fooBankAccount.iban = "CH9789144829733648596"
+ fooBankAccount.iban = iban
}
}
- uploadQrrPayment()
- downloadPayment()
- uploadPain001Payment()
+}
+fun main(args: Array<String>) {
+ PostFinanceCommand().subcommands(Download(), Upload(), GenIncoming()).main(args)
}
\ No newline at end of file
diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt
@@ -210,7 +210,11 @@ fun createEbicsRequestForDownloadReceipt(
)
XMLUtil.convertJaxbToDocument(req)
}
- XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv)
+ XMLUtil.signEbicsDocument(
+ doc,
+ subscriberDetails.customerAuthPriv,
+ withEbics3
+ )
return XMLUtil.convertDomToString(doc)
}
@@ -619,6 +623,7 @@ fun parseEbicsHpbOrder(orderDataRaw: ByteArray): HpbResponseData {
}
private fun ebics3toInternalRepr(response: String): EbicsResponseContent {
+ // logger.debug("Converting bank resp to internal repr.: $response")
val resp: JAXBElement<Ebics3Response> = try {
XMLUtil.convertStringToJaxb(response)
} catch (e: Exception) {
diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Response.kt b/util/src/main/kotlin/ebics_h005/Ebics3Response.kt
@@ -108,13 +108,12 @@ class Ebics3Response {
@XmlType(name = "DataTransferResponseType", propOrder = ["dataEncryptionInfo", "orderData"])
class DataTransferResponseType {
@get:XmlElement(name = "DataEncryptionInfo")
- var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null
+ var dataEncryptionInfo: Ebics3Types.DataEncryptionInfo? = null
@get:XmlElement(name = "OrderData", required = true)
lateinit var orderData: OrderData
}
-
@XmlAccessorType(XmlAccessType.NONE)
@XmlType(name = "ResponseStaticHeaderType", propOrder = ["transactionID", "numSegments"])
class StaticHeaderType {
@@ -295,7 +294,6 @@ class Ebics3Response {
}
}
}
-
fun createForDownloadInitializationPhase(
transactionID: String,
numSegments: Int,
@@ -330,9 +328,9 @@ class Ebics3Response {
this.value = "000000"
}
this.dataTransfer = DataTransferResponseType().apply {
- this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply {
+ this.dataEncryptionInfo = Ebics3Types.DataEncryptionInfo().apply {
this.authenticate = true
- this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest()
+ this.encryptionPubKeyDigest = Ebics3Types.PubKeyDigest()
.apply {
this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256"
this.version = "E002"