summaryrefslogtreecommitdiff
path: root/nexus/src/main/kotlin/tech
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-06-15 14:36:22 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-06-15 14:36:22 +0530
commitb86b5a2fd7e13adeaa5d49f7ee459a584730b30e (patch)
treefa9ee250075e2ff9024eedd5236f618b6b0561f3 /nexus/src/main/kotlin/tech
parent3d8ad9ef15c56c172e8603eb6aafa73201ad7298 (diff)
downloadlibeufin-b86b5a2fd7e13adeaa5d49f7ee459a584730b30e.tar.gz
libeufin-b86b5a2fd7e13adeaa5d49f7ee459a584730b30e.tar.bz2
libeufin-b86b5a2fd7e13adeaa5d49f7ee459a584730b30e.zip
start separating out EBICS handling from rest of nexus
Diffstat (limited to 'nexus/src/main/kotlin/tech')
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt127
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt2
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt383
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt (renamed from nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt)3
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt482
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt2
6 files changed, 542 insertions, 457 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
index b57dc26a..04942252 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
@@ -101,19 +101,6 @@ fun getEbicsSubscriberDetailsInternal(subscriber: EbicsSubscriberEntity): EbicsC
}
/**
- * Retrieve Ebics subscriber details given a bank connection.
- */
-fun getEbicsSubscriberDetails(bankConnectionId: String): EbicsClientSubscriberDetails {
- val transport = NexusBankConnectionEntity.findById(bankConnectionId)
- if (transport == null) {
- throw NexusError(HttpStatusCode.NotFound, "transport not found")
- }
- val subscriber = EbicsSubscriberEntity.find { EbicsSubscribersTable.nexusBankConnection eq transport.id }.first()
- // transport exists and belongs to caller.
- return getEbicsSubscriberDetailsInternal(subscriber)
-}
-
-/**
* Check if the transaction is already found in the database.
*/
private fun isDuplicate(acctSvcrRef: String): Boolean {
@@ -194,93 +181,6 @@ fun ingestBankMessagesIntoAccount(
}
}
-private data class EbicsFetchSpec(
- val orderType: String,
- val orderParams: EbicsOrderParams
-)
-
-suspend fun fetchEbicsBySpec(fetchSpec: FetchSpecJson, client: HttpClient, bankConnectionId: String) {
- val subscriberDetails = getEbicsSubscriberDetails(bankConnectionId)
- val specs = mutableListOf<EbicsFetchSpec>()
- when (fetchSpec) {
- is FetchSpecLatestJson -> {
- val p = EbicsStandardOrderParams()
- when (fetchSpec.level) {
- FetchLevel.ALL -> {
- specs.add(EbicsFetchSpec("C52", p))
- specs.add(EbicsFetchSpec("C53", p))
- }
- FetchLevel.REPORT -> {
- specs.add(EbicsFetchSpec("C52", p))
- }
- FetchLevel.STATEMENT -> {
- specs.add(EbicsFetchSpec("C53", p))
- }
- }
- }
- }
- for (spec in specs) {
- fetchEbicsC5x(spec.orderType, client, bankConnectionId, spec.orderParams, subscriberDetails)
- }
-}
-
-/**
- * Fetch EBICS C5x and store it locally, but do not update bank accounts.
- */
-private suspend fun fetchEbicsC5x(
- historyType: String,
- client: HttpClient,
- bankConnectionId: String,
- orderParams: EbicsOrderParams,
- subscriberDetails: EbicsClientSubscriberDetails
-) {
- val response = doEbicsDownloadTransaction(
- client,
- subscriberDetails,
- historyType,
- orderParams
- )
- when (historyType) {
- "C52" -> {
- }
- "C53" -> {
- }
- else -> {
- throw NexusError(HttpStatusCode.BadRequest, "history type '$historyType' not supported")
- }
- }
- when (response) {
- is EbicsDownloadSuccessResult -> {
- response.orderData.unzipWithLambda {
- logger.debug("Camt entry: ${it.second}")
- val camt53doc = XMLUtil.parseStringIntoDom(it.second)
- val msgId = camt53doc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId")
- logger.info("msg id $msgId")
- transaction {
- val conn = NexusBankConnectionEntity.findById(bankConnectionId)
- if (conn == null) {
- throw NexusError(HttpStatusCode.InternalServerError, "bank connection missing")
- }
- val oldMsg = NexusBankMessageEntity.find { NexusBankMessagesTable.messageId eq msgId }.firstOrNull()
- if (oldMsg == null) {
- NexusBankMessageEntity.new {
- this.bankConnection = conn
- this.code = historyType
- this.messageId = msgId
- this.message = ExposedBlob(it.second.toByteArray(Charsets.UTF_8))
- }
- }
- }
- }
- }
- is EbicsDownloadBankErrorResult -> {
- throw NexusError(
- HttpStatusCode.BadGateway,
- response.returnCode.errorCode
- )
- }
- }
-}
/**
* Create a PAIN.001 XML document according to the input data.
@@ -472,29 +372,4 @@ fun extractUserAndPassword(authorizationHeader: String): Pair<String, String> {
)
}
return Pair(username, password)
-}
-
-/**
- * Test HTTP basic auth. Throws error if password is wrong,
- * and makes sure that the user exists in the system.
- *
- * @param authorization the Authorization:-header line.
- * @return user id
- */
-fun authenticateRequest(request: ApplicationRequest): NexusUserEntity {
- val authorization = request.headers["Authorization"]
- val headerLine = if (authorization == null) throw NexusError(
- HttpStatusCode.BadRequest, "Authentication:-header line not found"
- ) else authorization
- val (username, password) = extractUserAndPassword(headerLine)
- val user = NexusUserEntity.find {
- NexusUsersTable.id eq username
- }.firstOrNull()
- if (user == null) {
- throw NexusError(HttpStatusCode.Unauthorized, "Unknown user '$username'")
- }
- if (!CryptoUtil.checkpw(password, user.passwordHash)) {
- throw NexusError(HttpStatusCode.Forbidden, "Wrong password")
- }
- return user
-}
+} \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
index 0ec844a6..c5d7c3a3 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
@@ -28,7 +28,7 @@ import tech.libeufin.util.*
import java.time.LocalDate
import java.time.LocalDateTime
-data class EbicsBackupRequestJson(
+data class BackupRequestJson(
val passphrase: String
)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index e270e284..8ca4b9f6 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -67,16 +67,14 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
+import tech.libeufin.nexus.ebics.*
import tech.libeufin.util.*
import tech.libeufin.util.CryptoUtil.hashpw
-import tech.libeufin.util.ebics_h004.HTDResponseOrderData
import java.io.PrintWriter
import java.io.StringWriter
import java.net.URLEncoder
import java.time.Duration
-import java.util.*
import java.util.zip.InflaterInputStream
-import javax.crypto.EncryptedPrivateKeyInfo
data class NexusError(val statusCode: HttpStatusCode, val reason: String) :
Exception("$reason (HTTP status $statusCode)")
@@ -135,65 +133,32 @@ suspend inline fun <reified T : Any> ApplicationCall.receiveJson(): T {
}
}
-fun createEbicsBankConnectionFromBackup(
- bankConnectionName: String,
- user: NexusUserEntity,
- passphrase: String?,
- backup: JsonNode
-) {
- if (passphrase === null) {
- throw NexusError(HttpStatusCode.BadRequest, "EBICS backup needs passphrase")
- }
- val bankConn = NexusBankConnectionEntity.new(bankConnectionName) {
- owner = user
- type = "ebics"
- }
- val ebicsBackup = jacksonObjectMapper().treeToValue(backup, EbicsKeysBackupJson::class.java)
- val (authKey, encKey, sigKey) = try {
- Triple(
- CryptoUtil.decryptKey(
- EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.authBlob)),
- passphrase
- ),
- CryptoUtil.decryptKey(
- EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.encBlob)),
- passphrase
- ),
- CryptoUtil.decryptKey(
- EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.sigBlob)),
- passphrase
- )
- )
- } catch (e: Exception) {
- e.printStackTrace()
- logger.info("Restoring keys failed, probably due to wrong passphrase")
- throw NexusError(
- HttpStatusCode.BadRequest,
- "Bad backup given"
- )
+/**
+ * Test HTTP basic auth. Throws error if password is wrong,
+ * and makes sure that the user exists in the system.
+ *
+ * @param authorization the Authorization:-header line.
+ * @return user id
+ */
+fun authenticateRequest(request: ApplicationRequest): NexusUserEntity {
+ val authorization = request.headers["Authorization"]
+ val headerLine = if (authorization == null) throw NexusError(
+ HttpStatusCode.BadRequest, "Authentication:-header line not found"
+ ) else authorization
+ val (username, password) = extractUserAndPassword(headerLine)
+ val user = NexusUserEntity.find {
+ NexusUsersTable.id eq username
+ }.firstOrNull()
+ if (user == null) {
+ throw NexusError(HttpStatusCode.Unauthorized, "Unknown user '$username'")
}
- try {
- EbicsSubscriberEntity.new {
- ebicsURL = ebicsBackup.ebicsURL
- hostID = ebicsBackup.hostID
- partnerID = ebicsBackup.partnerID
- userID = ebicsBackup.userID
- signaturePrivateKey = ExposedBlob(sigKey.encoded)
- encryptionPrivateKey = ExposedBlob((encKey.encoded))
- authenticationPrivateKey = ExposedBlob((authKey.encoded))
- nexusBankConnection = bankConn
- ebicsIniState = EbicsInitState.UNKNOWN
- ebicsHiaState = EbicsInitState.UNKNOWN
- }
- } catch (e: Exception) {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "exception: $e"
- )
+ if (!CryptoUtil.checkpw(password, user.passwordHash)) {
+ throw NexusError(HttpStatusCode.Forbidden, "Wrong password")
}
- return
+ return user
}
+
fun createLoopbackBankConnection(bankConnectionName: String, user: NexusUserEntity, data: JsonNode) {
val bankConn = NexusBankConnectionEntity.new(bankConnectionName) {
owner = user
@@ -511,12 +476,10 @@ fun serverMain(dbName: String) {
return@get
}
- post("/bank-connection-protocols/ebics/test-host") {
- val r = call.receiveJson<EbicsHostTestRequest>()
- val qr = doEbicsHostVersionQuery(client, r.ebicsBaseUrl, r.ebicsHostId)
- call.respond(HttpStatusCode.OK, qr)
- return@post
+ route("/bank-connection-protocols/ebics") {
+ ebicsBankProtocolRoutes(client)
}
+
/**
* Shows the bank accounts belonging to the requesting user.
*/
@@ -570,24 +533,16 @@ fun serverMain(dbName: String) {
}
val defaultBankConnection = bankAccount.defaultBankConnection
?: throw NexusError(HttpStatusCode.NotFound, "needs a default connection")
- val subscriberDetails = getEbicsSubscriberDetails(defaultBankConnection.id.value)
return@transaction object {
val pain001document = createPain001document(preparedPayment)
val bankConnectionType = defaultBankConnection.type
- val subscriberDetails = subscriberDetails
+ val connId = defaultBankConnection.id.value
}
}
// type and name aren't null
when (res.bankConnectionType) {
"ebics" -> {
- logger.debug("Uploading PAIN.001: ${res.pain001document}")
- doEbicsUploadTransaction(
- client,
- res.subscriberDetails,
- "CCT",
- res.pain001document.toByteArray(Charsets.UTF_8),
- EbicsStandardOrderParams()
- )
+ submitEbicsPaymentInitiation(client, res.connId, res.pain001document)
}
else -> throw NexusError(
HttpStatusCode.NotFound,
@@ -771,61 +726,29 @@ fun serverMain(dbName: String) {
val resp = transaction {
val user = authenticateRequest(call.request)
val conn = requireBankConnection(call, "connid")
- if (conn.type != "ebics") {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "bank connection is not of type 'ebics' (but '${conn.type}')"
- )
+ when (conn.type) {
+ "ebics" -> {
+ getEbicsConnectionDetails(conn)
+ }
+ else -> {
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "bank connection is not of type 'ebics' (but '${conn.type}')"
+ )
+ }
}
- val ebicsSubscriber = getEbicsSubscriberDetails(conn.id.value)
- val mapper = ObjectMapper()
- val details = mapper.createObjectNode()
- details.put("ebicsUrl", ebicsSubscriber.ebicsUrl)
- details.put("ebicsHostId", ebicsSubscriber.hostId)
- details.put("partnerId", ebicsSubscriber.partnerId)
- details.put("userId", ebicsSubscriber.userId)
- val node = mapper.createObjectNode()
- node.put("type", conn.type)
- node.put("owner", conn.owner.id.value)
- node.set<JsonNode>("details", details)
- node
}
call.respond(resp)
}
post("/bank-connections/{connid}/export-backup") {
- val body = call.receive<EbicsBackupRequestJson>()
- val response = transaction {
- val user = authenticateRequest(call.request)
+ transaction { authenticateRequest(call.request) }
+ val body = call.receive<BackupRequestJson>()
+ val response = run {
val conn = requireBankConnection(call, "connid")
when (conn.type) {
"ebics" -> {
- val subscriber = getEbicsSubscriberDetails(conn.id.value)
- EbicsKeysBackupJson(
- type = "ebics",
- userID = subscriber.userId,
- hostID = subscriber.hostId,
- partnerID = subscriber.partnerId,
- ebicsURL = subscriber.ebicsUrl,
- authBlob = bytesToBase64(
- CryptoUtil.encryptKey(
- subscriber.customerAuthPriv.encoded,
- body.passphrase
- )
- ),
- encBlob = bytesToBase64(
- CryptoUtil.encryptKey(
- subscriber.customerEncPriv.encoded,
- body.passphrase
- )
- ),
- sigBlob = bytesToBase64(
- CryptoUtil.encryptKey(
- subscriber.customerSignPriv.encoded,
- body.passphrase
- )
- )
- )
+ exportEbicsKeyBackup(conn.id.value, body.passphrase)
}
else -> {
throw NexusError(
@@ -843,62 +766,13 @@ fun serverMain(dbName: String) {
}
post("/bank-connections/{connid}/connect") {
- val subscriber = transaction {
- val user = authenticateRequest(call.request)
- val conn = requireBankConnection(call, "connid")
- if (conn.type != "ebics") {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "bank connection is not of type 'ebics' (but '${conn.type}')"
- )
- }
- getEbicsSubscriberDetails(conn.id.value)
- }
- if (subscriber.bankAuthPub != null && subscriber.bankEncPub != null) {
- call.respond(object {
- val ready = true
- })
- return@post
- }
-
- val iniDone = when (subscriber.ebicsIniState) {
- EbicsInitState.NOT_SENT, EbicsInitState.UNKNOWN -> {
- val iniResp = doEbicsIniRequest(client, subscriber)
- iniResp.bankReturnCode == EbicsReturnCode.EBICS_OK && iniResp.technicalReturnCode == EbicsReturnCode.EBICS_OK
- }
- else -> {
- false
- }
- }
- val hiaDone = when (subscriber.ebicsHiaState) {
- EbicsInitState.NOT_SENT, EbicsInitState.UNKNOWN -> {
- val hiaResp = doEbicsHiaRequest(client, subscriber)
- hiaResp.bankReturnCode == EbicsReturnCode.EBICS_OK && hiaResp.technicalReturnCode == EbicsReturnCode.EBICS_OK
- }
- else -> {
- false
- }
- }
- val hpbData = try {
- doEbicsHpbRequest(client, subscriber)
- } catch (e: EbicsProtocolError) {
- logger.warn("failed hpb request", e)
- null
+ val conn = transaction {
+ authenticateRequest(call.request)
+ requireBankConnection(call, "connid")
}
- transaction {
- val conn = requireBankConnection(call, "connid")
- val subscriberEntity =
- EbicsSubscriberEntity.find { EbicsSubscribersTable.nexusBankConnection eq conn.id }.first()
- if (iniDone) {
- subscriberEntity.ebicsIniState = EbicsInitState.SENT
- }
- if (hiaDone) {
- subscriberEntity.ebicsHiaState = EbicsInitState.SENT
- }
- if (hpbData != null) {
- subscriberEntity.bankAuthenticationPublicKey =
- ExposedBlob((hpbData.authenticationPubKey.encoded))
- subscriberEntity.bankEncryptionPublicKey = ExposedBlob((hpbData.encryptionPubKey.encoded))
+ when (conn.type) {
+ "ebics" -> {
+ connectEbics(client, conn.id.value)
}
}
call.respond(object {})
@@ -933,157 +807,6 @@ fun serverMain(dbName: String) {
call.respondBytes(ret.msgContent, ContentType("application", "xml"))
}
- post("/bank-connections/{connid}/ebics/send-ini") {
- val subscriber = transaction {
- val user = authenticateRequest(call.request)
- val conn = requireBankConnection(call, "connid")
- if (conn.type != "ebics") {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "bank connection is not of type 'ebics' (but '${conn.type}')"
- )
- }
- getEbicsSubscriberDetails(conn.id.value)
- }
- val resp = doEbicsIniRequest(client, subscriber)
- call.respond(resp)
- }
-
- post("/bank-connections/{connid}/ebics/send-hia") {
- val subscriber = transaction {
- val user = authenticateRequest(call.request)
- val conn = requireBankConnection(call, "connid")
- if (conn.type != "ebics") {
- throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
- }
- getEbicsSubscriberDetails(conn.id.value)
- }
- val resp = doEbicsHiaRequest(client, subscriber)
- call.respond(resp)
- }
-
- post("/bank-connections/{connid}/ebics/send-hev") {
- val subscriber = transaction {
- val user = authenticateRequest(call.request)
- val conn = requireBankConnection(call, "connid")
- if (conn.type != "ebics") {
- throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
- }
- getEbicsSubscriberDetails(conn.id.value)
- }
- val resp = doEbicsHostVersionQuery(client, subscriber.ebicsUrl, subscriber.hostId)
- call.respond(resp)
- }
-
- post("/bank-connections/{connid}/ebics/send-hpb") {
- val subscriberDetails = transaction {
- val user = authenticateRequest(call.request)
- val conn = requireBankConnection(call, "connid")
- if (conn.type != "ebics") {
- throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
- }
- getEbicsSubscriberDetails(conn.id.value)
- }
- val hpbData = doEbicsHpbRequest(client, subscriberDetails)
- transaction {
- val conn = requireBankConnection(call, "connid")
- val subscriber =
- EbicsSubscriberEntity.find { EbicsSubscribersTable.nexusBankConnection eq conn.id }.first()
- subscriber.bankAuthenticationPublicKey = ExposedBlob((hpbData.authenticationPubKey.encoded))
- subscriber.bankEncryptionPublicKey = ExposedBlob((hpbData.encryptionPubKey.encoded))
- }
- call.respond(object {})
- }
-
- /**
- * Directly import accounts. Used for testing.
- */
- post("/bank-connections/{connid}/ebics/import-accounts") {
- val subscriberDetails = transaction {
- val user = authenticateRequest(call.request)
- val conn = requireBankConnection(call, "connid")
- if (conn.type != "ebics") {
- throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
- }
- getEbicsSubscriberDetails(conn.id.value)
- }
- val response = doEbicsDownloadTransaction(
- client, subscriberDetails, "HTD", EbicsStandardOrderParams()
- )
- when (response) {
- is EbicsDownloadBankErrorResult -> {
- throw NexusError(
- HttpStatusCode.BadGateway,
- response.returnCode.errorCode
- )
- }
- is EbicsDownloadSuccessResult -> {
- val payload = XMLUtil.convertStringToJaxb<HTDResponseOrderData>(
- response.orderData.toString(Charsets.UTF_8)
- )
- transaction {
- val conn = requireBankConnection(call, "connid")
- payload.value.partnerInfo.accountInfoList?.forEach {
- val bankAccount = NexusBankAccountEntity.new(id = it.id) {
- accountHolder = it.accountHolder ?: "NOT-GIVEN"
- iban = extractFirstIban(it.accountNumberList)
- ?: throw NexusError(HttpStatusCode.NotFound, reason = "bank gave no IBAN")
- bankCode = extractFirstBic(it.bankCodeList) ?: throw NexusError(
- HttpStatusCode.NotFound,
- reason = "bank gave no BIC"
- )
- defaultBankConnection = conn
- highestSeenBankMessageId = 0
- }
- }
- }
- response.orderData.toString(Charsets.UTF_8)
- }
- }
- call.respond(object {})
- }
-
- post("/bank-connections/{connid}/ebics/download/{msgtype}") {
- val orderType = requireNotNull(call.parameters["msgtype"]).toUpperCase(Locale.ROOT)
- if (orderType.length != 3) {
- throw NexusError(HttpStatusCode.BadRequest, "ebics order type must be three characters")
- }
- val paramsJson = call.receiveOrNull<EbicsStandardOrderParamsDateJson>()
- val orderParams = if (paramsJson == null) {
- EbicsStandardOrderParams()
- } else {
- paramsJson.toOrderParams()
- }
- val subscriberDetails = transaction {
- val user = authenticateRequest(call.request)
- val conn = requireBankConnection(call, "connid")
- if (conn.type != "ebics") {
- throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
- }
- getEbicsSubscriberDetails(conn.id.value)
- }
- val response = doEbicsDownloadTransaction(
- client,
- subscriberDetails,
- orderType,
- orderParams
- )
- when (response) {
- is EbicsDownloadSuccessResult -> {
- call.respondText(
- response.orderData.toString(Charsets.UTF_8),
- ContentType.Text.Plain,
- HttpStatusCode.OK
- )
- }
- is EbicsDownloadBankErrorResult -> {
- call.respond(
- HttpStatusCode.BadGateway,
- EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode))
- )
- }
- }
- }
post("/facades") {
val body = call.receive<FacadeInfo>()
val newFacade = transaction {
@@ -1106,10 +829,12 @@ fun serverMain(dbName: String) {
return@post
}
- route("/facades/{fcid}") {
- route("taler") {
- talerFacadeRoutes(this)
- }
+ route("/bank-connections/{connid}/ebics") {
+ ebicsBankConnectionRoutes(client)
+ }
+
+ route("/facades/{fcid}/taler") {
+ talerFacadeRoutes(this)
}
/**
* Hello endpoint.
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
index f23ad843..1cdabc9a 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
@@ -20,11 +20,12 @@
/**
* High-level interface for the EBICS protocol.
*/
-package tech.libeufin.nexus
+package tech.libeufin.nexus.ebics
import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.http.HttpStatusCode
+import tech.libeufin.nexus.NexusError
import tech.libeufin.util.*
import java.util.*
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
new file mode 100644
index 00000000..5fc83f24
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -0,0 +1,482 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2020 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
+ * <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Handlers for EBICS-related endpoints offered by the nexus for EBICS
+ * connections.
+ */
+package tech.libeufin.nexus.ebics
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import io.ktor.application.Application
+import io.ktor.application.ApplicationCall
+import io.ktor.application.call
+import io.ktor.client.HttpClient
+import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import io.ktor.request.receive
+import io.ktor.request.receiveOrNull
+import io.ktor.response.respond
+import io.ktor.response.respondText
+import io.ktor.routing.Route
+import io.ktor.routing.Routing
+import io.ktor.routing.post
+import io.ktor.util.pipeline.PipelineContext
+import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
+import org.jetbrains.exposed.sql.statements.api.ExposedBlob
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.nexus.*
+import tech.libeufin.nexus.logger
+import tech.libeufin.util.*
+import tech.libeufin.util.ebics_h004.HTDResponseOrderData
+import java.util.*
+import javax.crypto.EncryptedPrivateKeyInfo
+
+
+private data class EbicsFetchSpec(
+ val orderType: String,
+ val orderParams: EbicsOrderParams
+)
+
+suspend fun fetchEbicsBySpec(fetchSpec: FetchSpecJson, client: HttpClient, bankConnectionId: String) {
+ val subscriberDetails = transaction { getEbicsSubscriberDetails(bankConnectionId) }
+ val specs = mutableListOf<EbicsFetchSpec>()
+ when (fetchSpec) {
+ is FetchSpecLatestJson -> {
+ val p = EbicsStandardOrderParams()
+ when (fetchSpec.level) {
+ FetchLevel.ALL -> {
+ specs.add(EbicsFetchSpec("C52", p))
+ specs.add(EbicsFetchSpec("C53", p))
+ }
+ FetchLevel.REPORT -> {
+ specs.add(EbicsFetchSpec("C52", p))
+ }
+ FetchLevel.STATEMENT -> {
+ specs.add(EbicsFetchSpec("C53", p))
+ }
+ }
+ }
+ }
+ for (spec in specs) {
+ fetchEbicsC5x(spec.orderType, client, bankConnectionId, spec.orderParams, subscriberDetails)
+ }
+}
+
+/**
+ * Fetch EBICS C5x and store it locally, but do not update bank accounts.
+ */
+private suspend fun fetchEbicsC5x(
+ historyType: String,
+ client: HttpClient,
+ bankConnectionId: String,
+ orderParams: EbicsOrderParams,
+ subscriberDetails: EbicsClientSubscriberDetails
+) {
+ val response = doEbicsDownloadTransaction(
+ client,
+ subscriberDetails,
+ historyType,
+ orderParams
+ )
+ when (historyType) {
+ "C52" -> {
+ }
+ "C53" -> {
+ }
+ else -> {
+ throw NexusError(HttpStatusCode.BadRequest, "history type '$historyType' not supported")
+ }
+ }
+ when (response) {
+ is EbicsDownloadSuccessResult -> {
+ response.orderData.unzipWithLambda {
+ logger.debug("Camt entry: ${it.second}")
+ val camt53doc = XMLUtil.parseStringIntoDom(it.second)
+ val msgId = camt53doc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId")
+ logger.info("msg id $msgId")
+ transaction {
+ val conn = NexusBankConnectionEntity.findById(bankConnectionId)
+ if (conn == null) {
+ throw NexusError(HttpStatusCode.InternalServerError, "bank connection missing")
+ }
+ val oldMsg = NexusBankMessageEntity.find { NexusBankMessagesTable.messageId eq msgId }.firstOrNull()
+ if (oldMsg == null) {
+ NexusBankMessageEntity.new {
+ this.bankConnection = conn
+ this.code = historyType
+ this.messageId = msgId
+ this.message = ExposedBlob(it.second.toByteArray(Charsets.UTF_8))
+ }
+ }
+ }
+ }
+ }
+ is EbicsDownloadBankErrorResult -> {
+ throw NexusError(
+ HttpStatusCode.BadGateway,
+ response.returnCode.errorCode
+ )
+ }
+ }
+}
+
+
+fun createEbicsBankConnectionFromBackup(
+ bankConnectionName: String,
+ user: NexusUserEntity,
+ passphrase: String?,
+ backup: JsonNode
+) {
+ if (passphrase === null) {
+ throw NexusError(HttpStatusCode.BadRequest, "EBICS backup needs passphrase")
+ }
+ val bankConn = NexusBankConnectionEntity.new(bankConnectionName) {
+ owner = user
+ type = "ebics"
+ }
+ val ebicsBackup = jacksonObjectMapper().treeToValue(backup, EbicsKeysBackupJson::class.java)
+ val (authKey, encKey, sigKey) = try {
+ Triple(
+ CryptoUtil.decryptKey(
+ EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.authBlob)),
+ passphrase
+ ),
+ CryptoUtil.decryptKey(
+ EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.encBlob)),
+ passphrase
+ ),
+ CryptoUtil.decryptKey(
+ EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.sigBlob)),
+ passphrase
+ )
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ logger.info("Restoring keys failed, probably due to wrong passphrase")
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "Bad backup given"
+ )
+ }
+ try {
+ EbicsSubscriberEntity.new {
+ ebicsURL = ebicsBackup.ebicsURL
+ hostID = ebicsBackup.hostID
+ partnerID = ebicsBackup.partnerID
+ userID = ebicsBackup.userID
+ signaturePrivateKey = ExposedBlob(sigKey.encoded)
+ encryptionPrivateKey = ExposedBlob((encKey.encoded))
+ authenticationPrivateKey = ExposedBlob((authKey.encoded))
+ nexusBankConnection = bankConn
+ ebicsIniState = EbicsInitState.UNKNOWN
+ ebicsHiaState = EbicsInitState.UNKNOWN
+ }
+ } catch (e: Exception) {
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "exception: $e"
+ )
+ }
+ return
+}
+
+/**
+ * Retrieve Ebics subscriber details given a bank connection.
+ */
+private fun getEbicsSubscriberDetails(bankConnectionId: String): EbicsClientSubscriberDetails {
+ val transport = NexusBankConnectionEntity.findById(bankConnectionId)
+ if (transport == null) {
+ throw NexusError(HttpStatusCode.NotFound, "transport not found")
+ }
+ val subscriber = EbicsSubscriberEntity.find { EbicsSubscribersTable.nexusBankConnection eq transport.id }.first()
+ // transport exists and belongs to caller.
+ return getEbicsSubscriberDetailsInternal(subscriber)
+}
+
+fun Route.ebicsBankProtocolRoutes(client: HttpClient) {
+ post("test-host") {
+ val r = call.receiveJson<EbicsHostTestRequest>()
+ val qr = doEbicsHostVersionQuery(client, r.ebicsBaseUrl, r.ebicsHostId)
+ call.respond(HttpStatusCode.OK, qr)
+ return@post
+ }
+}
+
+fun Route.ebicsBankConnectionRoutes(client: HttpClient) {
+ post("/send-ini") {
+ val subscriber = transaction {
+ val user = authenticateRequest(call.request)
+ val conn = requireBankConnection(call, "connid")
+ if (conn.type != "ebics") {
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "bank connection is not of type 'ebics' (but '${conn.type}')"
+ )
+ }
+ getEbicsSubscriberDetails(conn.id.value)
+ }
+ val resp = doEbicsIniRequest(client, subscriber)
+ call.respond(resp)
+ }
+
+ post("/send-hia") {
+ val subscriber = transaction {
+ val user = authenticateRequest(call.request)
+ val conn = requireBankConnection(call, "connid")
+ if (conn.type != "ebics") {
+ throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
+ }
+ getEbicsSubscriberDetails(conn.id.value)
+ }
+ val resp = doEbicsHiaRequest(client, subscriber)
+ call.respond(resp)
+ }
+
+ post("/send-hev") {
+ val subscriber = transaction {
+ val user = authenticateRequest(call.request)
+ val conn = requireBankConnection(call, "connid")
+ if (conn.type != "ebics") {
+ throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
+ }
+ getEbicsSubscriberDetails(conn.id.value)
+ }
+ val resp = doEbicsHostVersionQuery(client, subscriber.ebicsUrl, subscriber.hostId)
+ call.respond(resp)
+ }
+
+ post("/send-hpb") {
+ val subscriberDetails = transaction {
+ val user = authenticateRequest(call.request)
+ val conn = requireBankConnection(call, "connid")
+ if (conn.type != "ebics") {
+ throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
+ }
+ getEbicsSubscriberDetails(conn.id.value)
+ }
+ val hpbData = doEbicsHpbRequest(client, subscriberDetails)
+ transaction {
+ val conn = requireBankConnection(call, "connid")
+ val subscriber =
+ EbicsSubscriberEntity.find { EbicsSubscribersTable.nexusBankConnection eq conn.id }.first()
+ subscriber.bankAuthenticationPublicKey = ExposedBlob((hpbData.authenticationPubKey.encoded))
+ subscriber.bankEncryptionPublicKey = ExposedBlob((hpbData.encryptionPubKey.encoded))
+ }
+ call.respond(object {})
+ }
+
+ /**
+ * Directly import accounts. Used for testing.
+ */
+ post("/import-accounts") {
+ val subscriberDetails = transaction {
+ val user = authenticateRequest(call.request)
+ val conn = requireBankConnection(call, "connid")
+ if (conn.type != "ebics") {
+ throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
+ }
+ getEbicsSubscriberDetails(conn.id.value)
+ }
+ val response = doEbicsDownloadTransaction(
+ client, subscriberDetails, "HTD", EbicsStandardOrderParams()
+ )
+ when (response) {
+ is EbicsDownloadBankErrorResult -> {
+ throw NexusError(
+ HttpStatusCode.BadGateway,
+ response.returnCode.errorCode
+ )
+ }
+ is EbicsDownloadSuccessResult -> {
+ val payload = XMLUtil.convertStringToJaxb<HTDResponseOrderData>(
+ response.orderData.toString(Charsets.UTF_8)
+ )
+ transaction {
+ val conn = requireBankConnection(call, "connid")
+ payload.value.partnerInfo.accountInfoList?.forEach {
+ val bankAccount = NexusBankAccountEntity.new(id = it.id) {
+ accountHolder = it.accountHolder ?: "NOT-GIVEN"
+ iban = extractFirstIban(it.accountNumberList)
+ ?: throw NexusError(HttpStatusCode.NotFound, reason = "bank gave no IBAN")
+ bankCode = extractFirstBic(it.bankCodeList) ?: throw NexusError(
+ HttpStatusCode.NotFound,
+ reason = "bank gave no BIC"
+ )
+ defaultBankConnection = conn
+ highestSeenBankMessageId = 0
+ }
+ }
+ }
+ response.orderData.toString(Charsets.UTF_8)
+ }
+ }
+ call.respond(object {})
+ }
+
+ post("/download/{msgtype}") {
+ val orderType = requireNotNull(call.parameters["msgtype"]).toUpperCase(Locale.ROOT)
+ if (orderType.length != 3) {
+ throw NexusError(HttpStatusCode.BadRequest, "ebics order type must be three characters")
+ }
+ val paramsJson = call.receiveOrNull<EbicsStandardOrderParamsDateJson>()
+ val orderParams = if (paramsJson == null) {
+ EbicsStandardOrderParams()
+ } else {
+ paramsJson.toOrderParams()
+ }
+ val subscriberDetails = transaction {
+ val user = authenticateRequest(call.request)
+ val conn = requireBankConnection(call, "connid")
+ if (conn.type != "ebics") {
+ throw NexusError(HttpStatusCode.BadRequest, "bank connection is not of type 'ebics'")
+ }
+ getEbicsSubscriberDetails(conn.id.value)
+ }
+ val response = doEbicsDownloadTransaction(
+ client,
+ subscriberDetails,
+ orderType,
+ orderParams
+ )
+ when (response) {
+ is EbicsDownloadSuccessResult -> {
+ call.respondText(
+ response.orderData.toString(Charsets.UTF_8),
+ ContentType.Text.Plain,
+ HttpStatusCode.OK
+ )
+ }
+ is EbicsDownloadBankErrorResult -> {
+ call.respond(
+ HttpStatusCode.BadGateway,
+ EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode))
+ )
+ }
+ }
+ }
+}
+
+fun exportEbicsKeyBackup(bankConnectionId: String, passphrase: String): Any {
+ val subscriber = transaction { getEbicsSubscriberDetails(bankConnectionId) }
+ return EbicsKeysBackupJson(
+ type = "ebics",
+ userID = subscriber.userId,
+ hostID = subscriber.hostId,
+ partnerID = subscriber.partnerId,
+ ebicsURL = subscriber.ebicsUrl,
+ authBlob = bytesToBase64(
+ CryptoUtil.encryptKey(
+ subscriber.customerAuthPriv.encoded,
+ passphrase
+ )
+ ),
+ encBlob = bytesToBase64(
+ CryptoUtil.encryptKey(
+ subscriber.customerEncPriv.encoded,
+ passphrase
+ )
+ ),
+ sigBlob = bytesToBase64(
+ CryptoUtil.encryptKey(
+ subscriber.customerSignPriv.encoded,
+ passphrase
+ )
+ )
+ )
+}
+
+suspend fun submitEbicsPaymentInitiation(client: HttpClient, connId: String, pain001Document: String) {
+ val ebicsSubscriberDetails = transaction { getEbicsSubscriberDetails(connId) }
+ logger.debug("Uploading PAIN.001: ${pain001Document}")
+ doEbicsUploadTransaction(
+ client,
+ ebicsSubscriberDetails,
+ "CCT",
+ pain001Document.toByteArray(Charsets.UTF_8),
+ EbicsStandardOrderParams()
+ )
+}
+
+fun getEbicsConnectionDetails(conn: NexusBankConnectionEntity): Any {
+ val ebicsSubscriber = transaction { getEbicsSubscriberDetails(conn.id.value) }
+ val mapper = ObjectMapper()
+ val details = mapper.createObjectNode()
+ details.put("ebicsUrl", ebicsSubscriber.ebicsUrl)
+ details.put("ebicsHostId", ebicsSubscriber.hostId)
+ details.put("partnerId", ebicsSubscriber.partnerId)
+ details.put("userId", ebicsSubscriber.userId)
+ val node = mapper.createObjectNode()
+ node.put("type", conn.type)
+ node.put("owner", conn.owner.id.value)
+ node.set<JsonNode>("details", details)
+ return node
+}
+
+suspend fun connectEbics(client: HttpClient, connId: String) {
+ val subscriber = transaction { getEbicsSubscriberDetails(connId) }
+ if (subscriber.bankAuthPub != null && subscriber.bankEncPub != null) {
+ return
+ }
+ val iniDone = when (subscriber.ebicsIniState) {
+ EbicsInitState.NOT_SENT, EbicsInitState.UNKNOWN -> {
+ val iniResp = doEbicsIniRequest(client, subscriber)
+ iniResp.bankReturnCode == EbicsReturnCode.EBICS_OK && iniResp.technicalReturnCode == EbicsReturnCode.EBICS_OK
+ }
+ else -> {
+ false
+ }
+ }
+ val hiaDone = when (subscriber.ebicsHiaState) {
+ EbicsInitState.NOT_SENT, EbicsInitState.UNKNOWN -> {
+ val hiaResp = doEbicsHiaRequest(client, subscriber)
+ hiaResp.bankReturnCode == EbicsReturnCode.EBICS_OK && hiaResp.technicalReturnCode == EbicsReturnCode.EBICS_OK
+ }
+ else -> {
+ false
+ }
+ }
+ val hpbData = try {
+ doEbicsHpbRequest(client, subscriber)
+ } catch (e: EbicsProtocolError) {
+ logger.warn("failed hpb request", e)
+ null
+ }
+ transaction {
+ val conn = NexusBankConnectionEntity.findById(connId)
+ if (conn == null) {
+ throw NexusError(HttpStatusCode.NotFound, "bank connection '$connId' not found")
+ }
+ val subscriberEntity =
+ EbicsSubscriberEntity.find { EbicsSubscribersTable.nexusBankConnection eq conn.id }.first()
+ if (iniDone) {
+ subscriberEntity.ebicsIniState = EbicsInitState.SENT
+ }
+ if (hiaDone) {
+ subscriberEntity.ebicsHiaState = EbicsInitState.SENT
+ }
+ if (hpbData != null) {
+ subscriberEntity.bankAuthenticationPublicKey =
+ ExposedBlob((hpbData.authenticationPubKey.encoded))
+ subscriberEntity.bankEncryptionPublicKey = ExposedBlob((hpbData.encryptionPubKey.encoded))
+ }
+ }
+} \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
index 33a71530..85e9cc81 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
@@ -38,6 +38,7 @@ import org.jetbrains.exposed.dao.Entity
import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.nexus.ebics.doEbicsUploadTransaction
import tech.libeufin.util.*
import kotlin.math.abs
import kotlin.math.min
@@ -378,6 +379,7 @@ private suspend fun talerAddIncoming(call: ApplicationCall): Unit {
}
// submits ALL the prepared payments from ALL the Taler facades.
+// FIXME(dold): This should not be done here.
suspend fun submitPreparedPaymentsViaEbics() {
data class EbicsSubmission(
val subscriberDetails: EbicsClientSubscriberDetails,