libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit e5e2ba20a612aaebeb3365f6d07640abb22f8d45
parent 55a63c220d9af923aa986138f27bdcea7be36768
Author: Marcello Stanisci <ms@taler.net>
Date:   Tue, 28 Apr 2020 18:12:54 +0200

More separation for transport types.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 6++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 29+++++++++++++++++++++++++----
Mnexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt | 104++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 1172+++++++++++++++++++++++++++++--------------------------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/taler.kt | 7+++----
Mnexus/src/test/kotlin/authentication.kt | 18+++++-------------
6 files changed, 526 insertions(+), 810 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -210,13 +210,15 @@ class EbicsSubscriberEntity(id: EntityID<Int>) : Entity<Int>(id) { object NexusUsersTable : IdTable<String>() { override val id = varchar("id", ID_MAX_LENGTH).entityId().primaryKey() - val ebicsSubscriber = reference("ebicsSubscriber", EbicsSubscribersTable) + val ebicsSubscriber = reference("ebicsSubscriber", EbicsSubscribersTable).nullable() + val testSubscriber = reference("testSubscriber", EbicsSubscribersTable).nullable() val password = EbicsSubscribersTable.blob("password").nullable() } class NexusUserEntity(id: EntityID<String>) : Entity<String>(id) { companion object : EntityClass<String, NexusUserEntity>(NexusUsersTable) - var ebicsSubscriber by EbicsSubscriberEntity referencedOn NexusUsersTable.ebicsSubscriber + var ebicsSubscriber by EbicsSubscriberEntity optionalReferencedOn NexusUsersTable.ebicsSubscriber + var testSubscriber by EbicsSubscriberEntity optionalReferencedOn NexusUsersTable.testSubscriber var password by NexusUsersTable.password } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -20,10 +20,10 @@ import java.time.ZonedDateTime import java.time.Instant import java.time.ZoneId -fun getSubscriberEntityFromNexusUserId(nexusUserId: String): EbicsSubscriberEntity { +fun getSubscriberEntityFromNexusUserId(nexusUserId: String?): EbicsSubscriberEntity { return transaction { - val nexusUser = expectNexusIdTransaction(nexusUserId) - nexusUser.ebicsSubscriber + val nexusUser = expectNexusIdTransaction(expectId(nexusUserId)) + getEbicsSubscriberFromUser(nexusUser) } } @@ -128,10 +128,21 @@ fun getSubscriberDetailsInternal(subscriber: EbicsSubscriberEntity): EbicsClient ) } +/** Return non null Ebics subscriber, or throw error otherwise. */ +fun getEbicsSubscriberFromUser(nexusUser: NexusUserEntity): EbicsSubscriberEntity { + return nexusUser.ebicsSubscriber ?: throw NexusError( + HttpStatusCode.NotFound, + "Ebics subscriber was never activated" + ) +} + fun getSubscriberDetailsFromNexusUserId(id: String): EbicsClientSubscriberDetails { return transaction { val nexusUser = expectNexusIdTransaction(id) - getSubscriberDetailsInternal(nexusUser.ebicsSubscriber) + getSubscriberDetailsInternal(nexusUser.ebicsSubscriber ?: throw NexusError( + HttpStatusCode.NotFound, + "Cannot get details for non-activated subscriber!" + )) } } @@ -404,6 +415,16 @@ fun subscriberHasRights(subscriber: EbicsSubscriberEntity, bankAccount: BankAcco return row != null } +/** Check if the nexus user is allowed to use the claimed bank account. */ +fun userHasRights(subscriber: NexusUserEntity, bankAccount: BankAccountEntity): Boolean { + val row = transaction { + UserToBankAccountEntity.find { + UserToBankAccountsTable.bankAccount eq bankAccount.id and + (UserToBankAccountsTable.nexusUser eq subscriber.id) + }.firstOrNull() + } + return row != null +} fun parseDate(date: String): DateTime { return DateTime.parse(date, DateTimeFormat.forPattern("YYYY-MM-DD")) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt @@ -4,6 +4,7 @@ import tech.libeufin.util.Amount import tech.libeufin.util.EbicsDateRange import tech.libeufin.util.EbicsOrderParams import tech.libeufin.util.EbicsStandardOrderParams +import java.lang.NullPointerException import java.time.LocalDate data class EbicsBackupRequestJson( @@ -54,54 +55,12 @@ data class EbicsKeysBackupJson( val passphrase: String? = null ) - data class EbicsPubKeyInfo( val authPub: String, val encPub: String, val sigPub: String ) -/** - * This object is POSTed by clients _after_ having created - * a EBICS subscriber at the sandbox. - */ -data class EbicsSubscriberInfoRequestJson( - val ebicsURL: String, - val hostID: String, - val partnerID: String, - val userID: String, - val systemID: String? = null, - val password: String? = null -) - -/** - * Contain the ID that identifies the new user in the Nexus system. - */ -data class EbicsSubscriberInfoResponseJson( - val nexusUserID: String, - val ebicsURL: String, - val hostID: String, - val partnerID: String, - val userID: String, - val systemID: String? = null -) - -data class Pain001Data( - val creditorIban: String, - val creditorBic: String, - val creditorName: String, - val sum: Amount, - val currency: String = "EUR", - val subject: String -) - -/** - * Admin call that tells all the subscribers managed by Nexus. - */ -data class EbicsSubscribersResponseJson( - val ebicsSubscribers: MutableList<EbicsSubscriberInfoResponseJson> = mutableListOf() -) - data class ProtocolAndVersionJson( val protocol: String, val version: String @@ -131,6 +90,56 @@ data class BankAccountsInfoResponse( var accounts: MutableList<BankAccountInfoElement> = mutableListOf() ) +/** THE NEXUS USER */ + +/** SHOWS details about one user */ +data class NexusUser( + val userID: String, + val transports: MutableList<Any> = mutableListOf() +) + +/** Instructs the nexus to CREATE a new user */ +data class NexusUserRequest( + val userID: String, + val password: String? +) + +/** Collection of all the nexus users existing in the system */ +data class NexusUsers( + val users: MutableList<NexusUser> = mutableListOf() +) + +/************************************/ + +/** TRANSPORT TYPES */ + +/** Instructs the nexus to CREATE a new Ebics subscriber. + * Note that the nexus user to which the subscriber must be + * associated is extracted from other HTTP details. + * + * This same structure can be user to SHOW one Ebics subscriber + * existing at the nexus. + */ +data class EbicsSubscriber( + val ebicsURL: String, + val hostID: String, + val partnerID: String, + val userID: String, + val systemID: String? = null +) + +/** Type representing the "test" transport. Test transport + * does not cooperate with the bank/sandbox in order to obtain + * data about one user. All the data is just mocked internally + * at the NEXUS. + */ +class TestSubscriber() + + +/** PAYMENT INSTRUCTIONS TYPES */ + +/** Represents a prepared payment at the nexus. This structure is + * used to SHOW a prepared payment to the called. */ data class PaymentInfoElement( val debtorAccount: String, val creditorIban: String, @@ -140,7 +149,16 @@ data class PaymentInfoElement( val sum: Amount, val submitted: Boolean ) - data class PaymentsInfo( var payments: MutableList<PaymentInfoElement> = mutableListOf() +) + +/** This structure is used to INSTRUCT the nexus to prepare such payment. */ +data class Pain001Data( + val creditorIban: String, + val creditorBic: String, + val creditorName: String, + val sum: Amount, + val currency: String = "EUR", + val subject: String ) \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -133,6 +133,9 @@ fun main() { } routing { + + /** General / debug endpoints */ + get("/") { call.respondText("Hello by Nexus!\n") return@get @@ -144,54 +147,71 @@ fun main() { return@get } - post("/ebics/subscribers/{id}/sendPTK") { - val id = expectId(call.parameters["id"]) - val paramsJson = call.receive<EbicsStandardOrderParamsJson>() - val orderParams = paramsJson.toOrderParams() - println("PTK order params: $orderParams") - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "PTK", 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)) - ) + /** USER endpoints (no EBICS) */ + + /** Lists the users known to this system */ + get("/users") { + val ret = NexusUsers() + transaction { + NexusUserEntity.all().forEach { + val nexusUser = NexusUser(userID = it.id.value) + val ebicsSubscriber = it.ebicsSubscriber + if (ebicsSubscriber != null) { + nexusUser.transports.add( + EbicsSubscriber( + userID = ebicsSubscriber.userID, + ebicsURL = ebicsSubscriber.ebicsURL, + hostID = ebicsSubscriber.hostID, + partnerID = ebicsSubscriber.partnerID, + systemID = ebicsSubscriber.systemID + ) + ) + } + if (it.testSubscriber != null) { + nexusUser.transports.add(TestSubscriber()) + } } } - return@post + call.respond(ret) + return@get } - post("/ebics/subscribers/{id}/sendHAC") { - val id = expectId(call.parameters["id"]) - val paramsJson = call.receive<EbicsStandardOrderParamsJson>() - val orderParams = paramsJson.toOrderParams() - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HAC", 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)) - ) + /** Get all the details associated with a NEXUS user */ + get("/user/{id}") { + val response = transaction { + val nexusUser = expectNexusIdTransaction(call.parameters["id"]) + NexusUser( + userID = nexusUser.id.value + ) + } + call.respond(HttpStatusCode.OK, response) + return@get + } + + /** Make a new NEXUS user in the system */ + post("/users/{id}") { + val newUserId = expectId(call.parameters["id"]) + val body = call.receive<NexusUserRequest>() + transaction { + NexusUserEntity.new(id = newUserId) { + password = if (body.password != null) { + SerialBlob(CryptoUtil.hashStringSHA256(body.password)) + } else { + logger.debug("No password set for $newUserId") + null + } } } + call.respondText( + "New NEXUS user registered. ID: $newUserId", + ContentType.Text.Plain, + HttpStatusCode.OK + ) + return@post } - get("/ebics/subscribers/{id}/accounts") { + + /** List all the bank accounts associated with a given NEXUS user. */ + get("/users/{id}/accounts") { // this information is only avaiable *after* HTD or HKD has been called val id = expectId(call.parameters["id"]) val ret = BankAccountsInfoResponse() @@ -215,40 +235,14 @@ fun main() { ) return@get } - /** - * This endpoint gathers all the data needed to create a payment and persists it - * into the database. However, it does NOT perform the payment itself! - */ - post("/ebics/subscribers/{id}/accounts/{acctid}/prepare-payment") { - val acctid = transaction { - val accountInfo = expectAcctidTransaction(call.parameters["acctid"]) - val nexusUser = expectNexusIdTransaction(call.parameters["id"]) - if (!subscriberHasRights(nexusUser.ebicsSubscriber, accountInfo)) { - throw NexusError( - HttpStatusCode.BadRequest, - "Claimed bank account '${accountInfo.id}' doesn't belong to user '${nexusUser.id.value}'!" - ) - } - accountInfo.id.value - } - val pain001data = call.receive<Pain001Data>() - createPain001entity(pain001data, acctid) - call.respondText( - "Payment instructions persisted in DB", - ContentType.Text.Plain, HttpStatusCode.OK - ) - return@post - } - /** - * list all the prepared payments related to customer {id} - */ - get("/ebics/subscribers/{id}/payments") { + /** Show list of payments prepared by calling user. */ + get("/users/{id}/payments") { val nexusUserId = expectId(call.parameters["id"]) val ret = PaymentsInfo() transaction { val nexusUser = expectNexusIdTransaction(nexusUserId) - val bankAccountsMap = EbicsToBankAccountEntity.find { - EbicsToBankAccountsTable.ebicsSubscriber eq nexusUser.ebicsSubscriber.id + val bankAccountsMap = UserToBankAccountEntity.find { + UserToBankAccountsTable.nexusUser eq nexusUser.id } bankAccountsMap.forEach { Pain001Entity.find { @@ -271,441 +265,131 @@ fun main() { call.respond(ret) return@get } - /** - * This function triggers the Nexus to perform all those un-submitted payments. - * Ideally, this logic will be moved into some more automatic mechanism. - * NOTE: payments are not yet marked as "done" after this function returns. This - * should be done AFTER the PAIN.002 data corresponding to a payment witnesses it. - */ - post("/ebics/admin/execute-payments") { - val (paymentRowId, painDoc: String, debtorAccount) = transaction { - val entity = Pain001Entity.find { - (Pain001Table.submitted eq false) and (Pain001Table.invalid eq false) - }.firstOrNull() ?: throw NexusError(HttpStatusCode.Accepted, reason = "No ready payments found") - Triple(entity.id, createPain001document(entity), entity.debtorAccount) - } - logger.debug("Uploading PAIN.001: ${painDoc}") - val subscriberDetails = getSubscriberDetailsFromBankAccount(debtorAccount) - doEbicsUploadTransaction( - client, - subscriberDetails, - "CCT", - painDoc.toByteArray(Charsets.UTF_8), - EbicsStandardOrderParams() - ) - /* flow here == no errors occurred */ - transaction { - val payment = Pain001Entity.findById(paymentRowId) ?: throw NexusError( - HttpStatusCode.InternalServerError, - "Severe internal error: could not find payment in DB after having submitted it to the bank" - ) - payment.submitted = true + post("/users/{id}/accounts/{acctid}/prepare-payment") { + val acctid = transaction { + val accountInfo = expectAcctidTransaction(call.parameters["acctid"]) + val nexusUser = expectNexusIdTransaction(call.parameters["id"]) + if (!userHasRights(nexusUser, accountInfo)) { + throw NexusError( + HttpStatusCode.BadRequest, + "Claimed bank account '${accountInfo.id}' doesn't belong to user '${nexusUser.id.value}'!" + ) + } + accountInfo.id.value } + val pain001data = call.receive<Pain001Data>() + createPain001entity(pain001data, acctid) call.respondText( - "CCT message submitted to the bank", - ContentType.Text.Plain, - HttpStatusCode.OK + "Payment instructions persisted in DB", + ContentType.Text.Plain, HttpStatusCode.OK ) return@post } - /** - * This function triggers the Nexus to perform all those un-submitted payments. - * Ideally, this logic will be moved into some more automatic mechanism. - * NOTE: payments are not yet marked as "done" after this function returns. This - * should be done AFTER the PAIN.002 data corresponding to a payment witnesses it. - */ - post("/ebics/admin/execute-payments-ccc") { - val (paymentRowId, painDoc: String, debtorAccount) = transaction { - val entity = Pain001Entity.find { - (Pain001Table.submitted eq false) and (Pain001Table.invalid eq false) - }.firstOrNull() ?: throw NexusError(HttpStatusCode.Accepted, reason = "No ready payments found") - Triple(entity.id, createPain001document(entity), entity.debtorAccount) - } - logger.debug("Uploading PAIN.001 via CCC: ${painDoc}") - val subscriberDetails = getSubscriberDetailsFromBankAccount(debtorAccount) - doEbicsUploadTransaction( - client, - subscriberDetails, - "CCC", - painDoc.toByteArray(Charsets.UTF_8).zip(), - EbicsStandardOrderParams() - ) - /* flow here == no errors occurred */ + + /** Associate a EBICS subscriber to the existing user */ + post("/ebics/{id}/subscriber") { + val body = call.receive<EbicsSubscriber>() + val pairA = CryptoUtil.generateRsaKeyPair(2048) + val pairB = CryptoUtil.generateRsaKeyPair(2048) + val pairC = CryptoUtil.generateRsaKeyPair(2048) + transaction { - val payment = Pain001Entity.findById(paymentRowId) ?: throw NexusError( - HttpStatusCode.InternalServerError, - "Severe internal error: could not find payment in DB after having submitted it to the bank" + val newEbicsSubscriber = EbicsSubscriberEntity.new { + ebicsURL = body.ebicsURL + hostID = body.hostID + partnerID = body.partnerID + userID = body.userID + systemID = body.systemID + signaturePrivateKey = SerialBlob(pairA.private.encoded) + encryptionPrivateKey = SerialBlob(pairB.private.encoded) + authenticationPrivateKey = SerialBlob(pairC.private.encoded) + } + val nexusUser = expectNexusIdTransaction(call.parameters["id"]) + nexusUser.ebicsSubscriber = newEbicsSubscriber + } + + } + post("/ebics/subscribers/{id}/restoreBackup") { + val body = call.receive<EbicsKeysBackupJson>() + val nexusId = expectId(call.parameters["id"]) + val subscriber = transaction { + NexusUserEntity.findById(nexusId) + } + if (subscriber != null) { + call.respond( + HttpStatusCode.Conflict, + NexusErrorJson("ID exists, please choose a new one") ) - payment.submitted = true + return@post + } + val (authKey, encKey, sigKey) = try { + Triple( + CryptoUtil.decryptKey( + EncryptedPrivateKeyInfo(base64ToBytes(body.authBlob)), body.passphrase!! + ), + CryptoUtil.decryptKey( + EncryptedPrivateKeyInfo(base64ToBytes(body.encBlob)), body.passphrase + ), + CryptoUtil.decryptKey( + EncryptedPrivateKeyInfo(base64ToBytes(body.sigBlob)), body.passphrase + ) + ) + } catch (e: Exception) { + e.printStackTrace() + logger.info("Restoring keys failed, probably due to wrong passphrase") + throw NexusError(HttpStatusCode.BadRequest, reason = "Bad backup given") + } + logger.info("Restoring keys, creating new user: $nexusId") + try { + transaction { + NexusUserEntity.new(id = nexusId) { + ebicsSubscriber = EbicsSubscriberEntity.new { + ebicsURL = body.ebicsURL + hostID = body.hostID + partnerID = body.partnerID + userID = body.userID + signaturePrivateKey = SerialBlob(sigKey.encoded) + encryptionPrivateKey = SerialBlob(encKey.encoded) + authenticationPrivateKey = SerialBlob(authKey.encoded) + } + } + } + } catch (e: Exception) { + print(e) + call.respond(NexusErrorJson("Could not store the new account into database")) + return@post } call.respondText( - "CCC message submitted to the bank", + "Keys successfully restored", ContentType.Text.Plain, HttpStatusCode.OK ) return@post } - post("/ebics/subscribers/{id}/fetch-payment-status") { - val id = expectId(call.parameters["id"]) - val paramsJson = call.receive<EbicsStandardOrderParamsJson>() - val orderParams = paramsJson.toOrderParams() - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction( - client, - subscriberData, - "CRZ", - orderParams - ) - when (response) { - is EbicsDownloadSuccessResult -> - call.respondText( - response.orderData.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) - /** - * NOTE: flow gets here when the bank-technical return code is - * different from 000000. This happens also for 090005 (no data available) - */ - else -> call.respond(NexusErrorJson("Could not download any PAIN.002")) - } - return@post - } - post("/ebics/subscribers/{id}/collect-transactions-c52") { - // FIXME(florian): Download C52 and store the result in the right database table + /** EBICS CONVENIENCE */ - } - get("/ebics/subscribers/{id}/show-collected-transactions-c53") { - val id = expectId(call.parameters["id"]) - var ret = "" - transaction { - val subscriber: EbicsSubscriberEntity = getSubscriberEntityFromNexusUserId(id) - RawBankTransactionEntity.find { - RawBankTransactionsTable.nexusSubscriber eq subscriber.id.value - }.forEach { - ret += "###\nDebitor: ${it.debitorIban}\nCreditor: ${it.creditorIban}\nAmount: ${it.currency}:${it.amount}\nDate: ${it.bookingDate}\n" - } - } - call.respondText( - ret, - ContentType.Text.Plain, - HttpStatusCode.OK - ) - - return@get - } - - /* Taler class will initialize all the relevant handlers. */ - Taler(this) - - post("/ebics/subscribers/{id}/collect-transactions-c53") { - val id = expectId(call.parameters["id"]) - val paramsJson = call.receive<EbicsStandardOrderParamsJson>() - val orderParams = paramsJson.toOrderParams() - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - when (val response = doEbicsDownloadTransaction(client, subscriberData, "C53", orderParams)) { - is EbicsDownloadSuccessResult -> { - /** - * The current code is _heavily_ dependent on the way GLS returns - * data. For example, GLS makes one ZIP entry for each "Ntry" element - * (a bank transfer), but per the specifications one bank can choose to - * return all the "Ntry" elements into one single ZIP entry, or even unzipped - * at all. - */ - response.orderData.unzipWithLoop { - val fileName = it.first - val camt53doc = XMLUtil.parseStringIntoDom(it.second) - transaction { - RawBankTransactionEntity.new { - sourceFileName = fileName - unstructuredRemittanceInformation = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy") - transactionType = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='CdtDbtInd']") - currency = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy") - amount = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']") - status = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Sts']") - bookingDate = parseDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis - nexusSubscriber = getSubscriberEntityFromNexusUserId(id) - creditorName = camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='Dbtr']//*[local-name()='Nm']") - creditorIban = camt53doc.pickString("//*[local-name()='CdtrAcct']//*[local-name()='IBAN']") - debitorName = camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='Dbtr']//*[local-name()='Nm']") - debitorIban = camt53doc.pickString("//*[local-name()='DbtrAcct']//*[local-name()='IBAN']") - counterpartBic = camt53doc.pickString("//*[local-name()='RltdAgts']//*[local-name()='BIC']") - } - } - } - call.respondText( - "C53 data persisted into the database (WIP).", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - } - is EbicsDownloadBankErrorResult -> { - call.respond( - HttpStatusCode.BadGateway, - EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) - ) - } - } - return@post - } - post("/ebics/subscribers/{id}/collect-transactions-c54") { - // FIXME(florian): Download C54 and store the result in the right database table - } - post("/ebics/subscribers/{id}/sendC52") { - val id = expectId(call.parameters["id"]) - val paramsJson = call.receive<EbicsStandardOrderParamsJson>() - val orderParams = paramsJson.toOrderParams() - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "C52", orderParams) - when (response) { - is EbicsDownloadSuccessResult -> { - call.respondText( - response.orderData.prettyPrintUnzip(), - ContentType.Text.Plain, - HttpStatusCode.OK - ) - } - is EbicsDownloadBankErrorResult -> { - call.respond( - HttpStatusCode.BadGateway, - EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) - ) - } - } - } - post("/ebics/subscribers/{id}/sendCRZ") { - val id = expectId(call.parameters["id"]) - val paramsJson = call.receive<EbicsStandardOrderParamsJson>() - val orderParams = paramsJson.toOrderParams() - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "CRZ", orderParams) - when (response) { - is EbicsDownloadSuccessResult -> { - call.respondText( - response.orderData.prettyPrintUnzip(), - ContentType.Text.Plain, - HttpStatusCode.OK - ) - } - is EbicsDownloadBankErrorResult -> { - call.respond( - HttpStatusCode.BadGateway, - EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) - ) - } - } - } - post("/ebics/subscribers/{id}/sendC53") { - val id = expectId(call.parameters["id"]) - val paramsJson = call.receive<EbicsStandardOrderParamsJson>() - val orderParams = paramsJson.toOrderParams() - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "C53", orderParams) - when (response) { - is EbicsDownloadSuccessResult -> { - call.respondText( - response.orderData.prettyPrintUnzip(), - ContentType.Text.Plain, - HttpStatusCode.OK - ) - } - is EbicsDownloadBankErrorResult -> { - call.respond( - HttpStatusCode.BadGateway, - EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) - ) - } - } - } - post("/ebics/subscribers/{id}/sendC54") { - val id = expectId(call.parameters["id"]) - val paramsJson = call.receive<EbicsStandardOrderParamsJson>() - val orderParams = paramsJson.toOrderParams() - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "C54", 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)) - ) - } - } - return@post - } - get("/ebics/subscribers/{id}/sendHTD") { - val customerIdAtNexus = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(customerIdAtNexus) - val response = doEbicsDownloadTransaction( - client, - subscriberData, - "HTD", - EbicsStandardOrderParams() - ) - 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)) - ) - } - } - return@get - } - post("/ebics/subscribers/{id}/sendHAA") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HAA", EbicsStandardOrderParams()) - 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)) - ) - } - } - return@post - } - - post("/ebics/subscribers/{id}/sendHVZ") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - // FIXME: order params are wrong - val response = doEbicsDownloadTransaction(client, subscriberData, "HVZ", EbicsStandardOrderParams()) - 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)) - ) - } - } - return@post - } - - post("/ebics/subscribers/{id}/sendHVU") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - // FIXME: order params are wrong - val response = doEbicsDownloadTransaction(client, subscriberData, "HVU", EbicsStandardOrderParams()) - 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)) - ) - } - } - return@post - } - - post("/ebics/subscribers/{id}/sendHPD") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HPD", EbicsStandardOrderParams()) - 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)) - ) - } + get("/ebics/subscribers/{id}/pubkeys") { + val nexusUser = expectNexusIdTransaction(call.parameters["id"]) + val response = transaction { + val subscriber = getEbicsSubscriberFromUser(nexusUser) + val authPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()) + val authPub = CryptoUtil.getRsaPublicFromPrivate(authPriv) + val encPriv = CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.toByteArray()) + val encPub = CryptoUtil.getRsaPublicFromPrivate(encPriv) + val sigPriv = CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray()) + val sigPub = CryptoUtil.getRsaPublicFromPrivate(sigPriv) + EbicsPubKeyInfo( + bytesToBase64(authPub.encoded), + bytesToBase64(encPub.encoded), + bytesToBase64(sigPub.encoded) + ) } - return@post - } - - get("/ebics/subscribers/{id}/sendHKD") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction( - client, - subscriberData, - "HKD", - EbicsStandardOrderParams() + call.respond( + HttpStatusCode.OK, + response ) - 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)) - ) - } - } - return@get - } - - post("/ebics/subscribers/{id}/sendTSD") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "TSD", EbicsGenericOrderParams()) - 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)) - ) - } - } - return@post } - get("/ebics/subscribers/{id}/keyletter") { val nexusUserId = expectId(call.parameters["id"]) var usernameLine = "TODO" @@ -732,7 +416,7 @@ fun main() { var hostID = "" transaction { val nexusUser = expectNexusIdTransaction(nexusUserId) - val subscriber = nexusUser.ebicsSubscriber + val subscriber = getEbicsSubscriberFromUser(nexusUser) val signPubTmp = CryptoUtil.getRsaPublicFromPrivate( CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray()) ) @@ -834,231 +518,223 @@ fun main() { HttpStatusCode.OK ) } - /** - * Lists the EBICS subscribers known to this service. - */ - get("/ebics/subscribers") { - val ret = EbicsSubscribersResponseJson() - transaction { - NexusUserEntity.all().forEach { - ret.ebicsSubscribers.add( - EbicsSubscriberInfoResponseJson( - hostID = it.ebicsSubscriber.hostID, - partnerID = it.ebicsSubscriber.partnerID, - systemID = it.ebicsSubscriber.systemID, - ebicsURL = it.ebicsSubscriber.ebicsURL, - userID = it.ebicsSubscriber.userID, - nexusUserID = it.id.value - ) - ) - } - } - call.respond(ret) - return@get - } - /** - * Get all the details associated with a NEXUS user. - */ - get("/ebics/subscribers/{id}") { - val nexusUserId = expectId(call.parameters["id"]) - val response = transaction { - val nexusUser = expectNexusIdTransaction(nexusUserId) - EbicsSubscriberInfoResponseJson( - nexusUserID = nexusUser.id.value, - hostID = nexusUser.ebicsSubscriber.hostID, - partnerID = nexusUser.ebicsSubscriber.partnerID, - systemID = nexusUser.ebicsSubscriber.systemID, - ebicsURL = nexusUser.ebicsSubscriber.ebicsURL, - userID = nexusUser.ebicsSubscriber.userID - ) - } - call.respond(HttpStatusCode.OK, response) - return@get - } - get("/ebics/{id}/sendHev") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val request = makeEbicsHEVRequest(subscriberData) - val response = client.postToBank(subscriberData.ebicsUrl, request) - val versionDetails = parseEbicsHEVResponse(response) - call.respond( - HttpStatusCode.OK, - EbicsHevResponseJson(versionDetails.versions.map { ebicsVersionSpec -> - ProtocolAndVersionJson( - ebicsVersionSpec.protocol, - ebicsVersionSpec.version - ) - }) - ) - return@get - } + /** STATE CHANGES VIA EBICS */ - /** - * Make a new NEXUS user in the system. This user gets (also) a new EBICS - * user associated. - */ - post("/{id}/subscribers") { - val newUserId = call.parameters["id"] - val body = call.receive<EbicsSubscriberInfoRequestJson>() - val pairA = CryptoUtil.generateRsaKeyPair(2048) - val pairB = CryptoUtil.generateRsaKeyPair(2048) - val pairC = CryptoUtil.generateRsaKeyPair(2048) - transaction { - val newEbicsSubscriber = EbicsSubscriberEntity.new { - ebicsURL = body.ebicsURL - hostID = body.hostID - partnerID = body.partnerID - userID = body.userID - systemID = body.systemID - signaturePrivateKey = SerialBlob(pairA.private.encoded) - encryptionPrivateKey = SerialBlob(pairB.private.encoded) - authenticationPrivateKey = SerialBlob(pairC.private.encoded) - } - NexusUserEntity.new(id = newUserId) { - ebicsSubscriber = newEbicsSubscriber - password = if (body.password != null) { - SerialBlob(CryptoUtil.hashStringSHA256(body.password)) - } else { - logger.debug("No password set for $newUserId") - null - } - } - } - call.respondText( - "New NEXUS user registered. ID: $newUserId", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - return@post - } - - post("/ebics/subscribers/{id}/sendIni") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val iniRequest = makeEbicsIniRequest(subscriberData) - val responseStr = client.postToBank( - subscriberData.ebicsUrl, - iniRequest - ) - val resp = parseAndDecryptEbicsKeyManagementResponse(subscriberData, responseStr) - if (resp.technicalReturnCode != EbicsReturnCode.EBICS_OK) { - throw NexusError(HttpStatusCode.InternalServerError,"Unexpected INI response code: ${resp.technicalReturnCode}") + post("/ebics/admin/execute-payments") { + val (paymentRowId, painDoc: String, debtorAccount) = transaction { + val entity = Pain001Entity.find { + (Pain001Table.submitted eq false) and (Pain001Table.invalid eq false) + }.firstOrNull() ?: throw NexusError(HttpStatusCode.Accepted, reason = "No ready payments found") + Triple(entity.id, createPain001document(entity), entity.debtorAccount) } - call.respondText("Bank accepted signature key\n", ContentType.Text.Plain, HttpStatusCode.OK) - return@post - } - - post("/ebics/subscribers/{id}/sendHia") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val hiaRequest = makeEbicsHiaRequest(subscriberData) - val responseStr = client.postToBank( - subscriberData.ebicsUrl, - hiaRequest + logger.debug("Uploading PAIN.001: ${painDoc}") + val subscriberDetails = getSubscriberDetailsFromBankAccount(debtorAccount) + doEbicsUploadTransaction( + client, + subscriberDetails, + "CCT", + painDoc.toByteArray(Charsets.UTF_8), + EbicsStandardOrderParams() ) - val resp = parseAndDecryptEbicsKeyManagementResponse(subscriberData, responseStr) - if (resp.technicalReturnCode != EbicsReturnCode.EBICS_OK) { - throw NexusError(HttpStatusCode.InternalServerError,"Unexpected HIA response code: ${resp.technicalReturnCode}") + /* flow here == no errors occurred */ + transaction { + val payment = Pain001Entity.findById(paymentRowId) ?: throw NexusError( + HttpStatusCode.InternalServerError, + "Severe internal error: could not find payment in DB after having submitted it to the bank" + ) + payment.submitted = true } call.respondText( - "Bank accepted authentication and encryption keys\n", + "CCT message submitted to the bank", ContentType.Text.Plain, HttpStatusCode.OK ) return@post } - - post("/ebics/subscribers/{id}/restoreBackup") { - val body = call.receive<EbicsKeysBackupJson>() - val nexusId = expectId(call.parameters["id"]) - val subscriber = transaction { - NexusUserEntity.findById(nexusId) - } - if (subscriber != null) { - call.respond( - HttpStatusCode.Conflict, - NexusErrorJson("ID exists, please choose a new one") - ) - return@post - } - val (authKey, encKey, sigKey) = try { - Triple( - CryptoUtil.decryptKey( - EncryptedPrivateKeyInfo(base64ToBytes(body.authBlob)), body.passphrase!! - ), - CryptoUtil.decryptKey( - EncryptedPrivateKeyInfo(base64ToBytes(body.encBlob)), body.passphrase - ), - CryptoUtil.decryptKey( - EncryptedPrivateKeyInfo(base64ToBytes(body.sigBlob)), body.passphrase - ) - ) - } catch (e: Exception) { - e.printStackTrace() - logger.info("Restoring keys failed, probably due to wrong passphrase") - throw NexusError(HttpStatusCode.BadRequest, reason = "Bad backup given") + post("/ebics/admin/execute-payments-ccc") { + val (paymentRowId, painDoc: String, debtorAccount) = transaction { + val entity = Pain001Entity.find { + (Pain001Table.submitted eq false) and (Pain001Table.invalid eq false) + }.firstOrNull() ?: throw NexusError(HttpStatusCode.Accepted, reason = "No ready payments found") + Triple(entity.id, createPain001document(entity), entity.debtorAccount) } - logger.info("Restoring keys, creating new user: $nexusId") - try { - transaction { - val newNexusUser = NexusUserEntity.new(id = nexusId) { - ebicsSubscriber = EbicsSubscriberEntity.new { - ebicsURL = body.ebicsURL - hostID = body.hostID - partnerID = body.partnerID - userID = body.userID - signaturePrivateKey = SerialBlob(sigKey.encoded) - encryptionPrivateKey = SerialBlob(encKey.encoded) - authenticationPrivateKey = SerialBlob(authKey.encoded) - } - } - } - } catch (e: Exception) { - print(e) - call.respond(NexusErrorJson("Could not store the new account into database")) - return@post + logger.debug("Uploading PAIN.001 via CCC: ${painDoc}") + val subscriberDetails = getSubscriberDetailsFromBankAccount(debtorAccount) + doEbicsUploadTransaction( + client, + subscriberDetails, + "CCC", + painDoc.toByteArray(Charsets.UTF_8).zip(), + EbicsStandardOrderParams() + ) + /* flow here == no errors occurred */ + transaction { + val payment = Pain001Entity.findById(paymentRowId) ?: throw NexusError( + HttpStatusCode.InternalServerError, + "Severe internal error: could not find payment in DB after having submitted it to the bank" + ) + payment.submitted = true } call.respondText( - "Keys successfully restored", + "CCC message submitted to the bank", ContentType.Text.Plain, HttpStatusCode.OK ) return@post } + post("/ebics/subscribers/{id}/collect-transactions-c52") { + // FIXME(florian): Download C52 and store the result in the right database table - get("/ebics/subscribers/{id}/pubkeys") { - val nexusId = expectId(call.parameters["id"]) + } + /** exports keys backup copy */ + post("/ebics/subscribers/{id}/backup") { + val body = call.receive<EbicsBackupRequestJson>() val response = transaction { - val nexusUser = expectNexusIdTransaction(nexusId) - val subscriber = nexusUser.ebicsSubscriber - val authPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()) - val authPub = CryptoUtil.getRsaPublicFromPrivate(authPriv) - val encPriv = CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.toByteArray()) - val encPub = CryptoUtil.getRsaPublicFromPrivate(encPriv) - val sigPriv = CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray()) - val sigPub = CryptoUtil.getRsaPublicFromPrivate(sigPriv) - EbicsPubKeyInfo( - bytesToBase64(authPub.encoded), - bytesToBase64(encPub.encoded), - bytesToBase64(sigPub.encoded) + val nexusUser = expectNexusIdTransaction(call.parameters["id"]) + val subscriber = getEbicsSubscriberFromUser(nexusUser) + EbicsKeysBackupJson( + userID = subscriber.userID, + hostID = subscriber.hostID, + partnerID = subscriber.partnerID, + ebicsURL = subscriber.ebicsURL, + authBlob = bytesToBase64( + CryptoUtil.encryptKey( + subscriber.authenticationPrivateKey.toByteArray(), + body.passphrase + ) + ), + encBlob = bytesToBase64( + CryptoUtil.encryptKey( + subscriber.encryptionPrivateKey.toByteArray(), + body.passphrase + ) + ), + sigBlob = bytesToBase64( + CryptoUtil.encryptKey( + subscriber.signaturePrivateKey.toByteArray(), + body.passphrase + ) + ) ) } + call.response.headers.append("Content-Disposition", "attachment") call.respond( HttpStatusCode.OK, response ) } + + /** Download keys from bank */ + post("/ebics/subscribers/{id}/sync") { + val nexusUser = expectNexusIdTransaction(call.parameters["id"]) + val subscriberDetails = getSubscriberDetailsFromNexusUserId(nexusUser.id.value) + val hpbRequest = makeEbicsHpbRequest(subscriberDetails) + val responseStr = client.postToBank(subscriberDetails.ebicsUrl, hpbRequest) + val response = parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, responseStr) + val orderData = response.orderData ?: throw NexusError( + HttpStatusCode.InternalServerError, + "HPB response has no order data" + ) + val hpbData = parseEbicsHpbOrder(orderData) + // put bank's keys into database. + transaction { + val ebicsSubscriber = getEbicsSubscriberFromUser(nexusUser) + ebicsSubscriber.bankAuthenticationPublicKey = SerialBlob(hpbData.authenticationPubKey.encoded) + ebicsSubscriber.bankEncryptionPublicKey = SerialBlob(hpbData.encryptionPubKey.encoded) + } + call.respondText("Bank keys stored in database\n", ContentType.Text.Plain, HttpStatusCode.OK) + return@post + } + post("/ebics/subscribers/{id}/fetch-payment-status") { + val id = expectId(call.parameters["id"]) + val paramsJson = call.receive<EbicsStandardOrderParamsJson>() + val orderParams = paramsJson.toOrderParams() + val subscriberData = getSubscriberDetailsFromNexusUserId(id) + val response = doEbicsDownloadTransaction( + client, + subscriberData, + "CRZ", + orderParams + ) + when (response) { + is EbicsDownloadSuccessResult -> + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + /** + * NOTE: flow gets here when the bank-technical return code is + * different from 000000. This happens also for 090005 (no data available) + */ + else -> call.respond(NexusErrorJson("Could not download any PAIN.002")) + } + return@post + } + post("/ebics/subscribers/{id}/collect-transactions-c53") { + val id = expectId(call.parameters["id"]) + val paramsJson = call.receive<EbicsStandardOrderParamsJson>() + val orderParams = paramsJson.toOrderParams() + val subscriberData = getSubscriberDetailsFromNexusUserId(id) + when (val response = doEbicsDownloadTransaction(client, subscriberData, "C53", orderParams)) { + is EbicsDownloadSuccessResult -> { + /** + * The current code is _heavily_ dependent on the way GLS returns + * data. For example, GLS makes one ZIP entry for each "Ntry" element + * (a bank transfer), but per the specifications one bank can choose to + * return all the "Ntry" elements into one single ZIP entry, or even unzipped + * at all. + */ + response.orderData.unzipWithLoop { + val fileName = it.first + val camt53doc = XMLUtil.parseStringIntoDom(it.second) + transaction { + RawBankTransactionEntity.new { + sourceFileName = fileName + unstructuredRemittanceInformation = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy") + transactionType = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='CdtDbtInd']") + currency = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy") + amount = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']") + status = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Sts']") + bookingDate = parseDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis + nexusSubscriber = getSubscriberEntityFromNexusUserId(id) + creditorName = camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='Dbtr']//*[local-name()='Nm']") + creditorIban = camt53doc.pickString("//*[local-name()='CdtrAcct']//*[local-name()='IBAN']") + debitorName = camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='Dbtr']//*[local-name()='Nm']") + debitorIban = camt53doc.pickString("//*[local-name()='DbtrAcct']//*[local-name()='IBAN']") + counterpartBic = camt53doc.pickString("//*[local-name()='RltdAgts']//*[local-name()='BIC']") + } + } + } + call.respondText( + "C53 data persisted into the database (WIP).", + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } + return@post + } + post("/ebics/subscribers/{id}/collect-transactions-c54") { + // FIXME(florian): Download C54 and store the result in the right database table + } /** * This endpoint downloads bank account information associated with the * calling EBICS subscriber. */ post("/ebics/subscribers/{id}/fetch-accounts") { - val nexusUserId = expectId(call.parameters["id"]) + val nexusUser = expectNexusIdTransaction((call.parameters["id"])) val paramsJson = call.receive<EbicsStandardOrderParamsJson>() val orderParams = paramsJson.toOrderParams() - val subscriberData = getSubscriberDetailsFromNexusUserId(nexusUserId) + val subscriberData = getSubscriberDetailsFromNexusUserId(nexusUser.id.value) val response = doEbicsDownloadTransaction(client, subscriberData, "HTD", orderParams) when (response) { is EbicsDownloadSuccessResult -> { @@ -1070,9 +746,8 @@ fun main() { 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") } - val nexusUser = expectNexusIdTransaction(nexusUserId) EbicsToBankAccountEntity.new { - ebicsSubscriber = nexusUser.ebicsSubscriber + ebicsSubscriber = getEbicsSubscriberFromUser(nexusUser) this.bankAccount = bankAccount } } @@ -1094,89 +769,98 @@ fun main() { return@post } - /* performs a keys backup */ - post("/ebics/subscribers/{id}/backup") { - val nexusId = expectId(call.parameters["id"]) - val body = call.receive<EbicsBackupRequestJson>() - val response = transaction { - val nexusUser = expectNexusIdTransaction(nexusId) - val subscriber = nexusUser.ebicsSubscriber - EbicsKeysBackupJson( - userID = subscriber.userID, - hostID = subscriber.hostID, - partnerID = subscriber.partnerID, - ebicsURL = subscriber.ebicsURL, - authBlob = bytesToBase64( - CryptoUtil.encryptKey( - subscriber.authenticationPrivateKey.toByteArray(), - body.passphrase - ) - ), - encBlob = bytesToBase64( - CryptoUtil.encryptKey( - subscriber.encryptionPrivateKey.toByteArray(), - body.passphrase - ) - ), - sigBlob = bytesToBase64( - CryptoUtil.encryptKey( - subscriber.signaturePrivateKey.toByteArray(), - body.passphrase - ) + /** EBICS MESSAGES / DEBUG */ + + // FIXME: some messages include a ZIPped payload. + post("/ebics/subscribers/{id}/send{MSG}") { + val id = expectId(call.parameters["id"]) + val MSG = expectId(call.parameters["MSG"]) + val paramsJson = call.receive<EbicsStandardOrderParamsJson>() + val orderParams = paramsJson.toOrderParams() + println("$MSG order params: $orderParams") + val subscriberData = getSubscriberDetailsFromNexusUserId(id) + val response = doEbicsDownloadTransaction( + client, + subscriberData, + MSG, + 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)) + ) + } } - call.response.headers.append("Content-Disposition", "attachment") + return@post + } + get("/ebics/{id}/sendHEV") { + val id = expectId(call.parameters["id"]) + val subscriberData = getSubscriberDetailsFromNexusUserId(id) + val request = makeEbicsHEVRequest(subscriberData) + val response = client.postToBank(subscriberData.ebicsUrl, request) + val versionDetails = parseEbicsHEVResponse(response) call.respond( HttpStatusCode.OK, - response + EbicsHevResponseJson(versionDetails.versions.map { ebicsVersionSpec -> + ProtocolAndVersionJson( + ebicsVersionSpec.protocol, + ebicsVersionSpec.version + ) + }) + ) + return@get + } + post("/ebics/subscribers/{id}/sendINI") { + val id = expectId(call.parameters["id"]) + val subscriberData = getSubscriberDetailsFromNexusUserId(id) + val iniRequest = makeEbicsIniRequest(subscriberData) + val responseStr = client.postToBank( + subscriberData.ebicsUrl, + iniRequest ) + val resp = parseAndDecryptEbicsKeyManagementResponse(subscriberData, responseStr) + if (resp.technicalReturnCode != EbicsReturnCode.EBICS_OK) { + throw NexusError(HttpStatusCode.InternalServerError,"Unexpected INI response code: ${resp.technicalReturnCode}") + } + call.respondText("Bank accepted signature key\n", ContentType.Text.Plain, HttpStatusCode.OK) + return@post } - post("/ebics/subscribers/{id}/sendTSU") { + post("/ebics/subscribers/{id}/sendHIA") { val id = expectId(call.parameters["id"]) val subscriberData = getSubscriberDetailsFromNexusUserId(id) - val payload = "PAYLOAD" - doEbicsUploadTransaction( - client, - subscriberData, - "TSU", - payload.toByteArray(Charsets.UTF_8), - EbicsGenericOrderParams() + val hiaRequest = makeEbicsHiaRequest(subscriberData) + val responseStr = client.postToBank( + subscriberData.ebicsUrl, + hiaRequest ) + val resp = parseAndDecryptEbicsKeyManagementResponse(subscriberData, responseStr) + if (resp.technicalReturnCode != EbicsReturnCode.EBICS_OK) { + throw NexusError(HttpStatusCode.InternalServerError,"Unexpected HIA response code: ${resp.technicalReturnCode}") + } call.respondText( - "TST INITIALIZATION & TRANSACTION phases succeeded\n", + "Bank accepted authentication and encryption keys\n", ContentType.Text.Plain, HttpStatusCode.OK ) - } - - post("/ebics/subscribers/{id}/sync") { - val nexusId = expectId(call.parameters["id"]) - val subscriberDetails = getSubscriberDetailsFromNexusUserId(nexusId) - val hpbRequest = makeEbicsHpbRequest(subscriberDetails) - val responseStr = client.postToBank(subscriberDetails.ebicsUrl, hpbRequest) - - val response = parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, responseStr) - val orderData = - response.orderData ?: throw NexusError(HttpStatusCode.InternalServerError, "HPB response has no order data") - val hpbData = parseEbicsHpbOrder(orderData) - - // put bank's keys into database. - transaction { - val nexusUser = expectNexusIdTransaction(nexusId) - nexusUser.ebicsSubscriber.bankAuthenticationPublicKey = SerialBlob(hpbData.authenticationPubKey.encoded) - nexusUser.ebicsSubscriber.bankEncryptionPublicKey = SerialBlob(hpbData.encryptionPubKey.encoded) - } - call.respondText("Bank keys stored in database\n", ContentType.Text.Plain, HttpStatusCode.OK) - return@post - } - post("/test/intercept") { - call.respondText(call.receive<String>() + "\n") return@post } + + /** PLUGINS */ + /* Taler class will initialize all the relevant handlers. */ + Taler(this) } } + logger.info("Up and running") server.start(wait = true) } \ 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 @@ -284,7 +284,7 @@ class Taler(app: Route) { creditorIban = creditorObj.iban counterpartBic = creditorObj.bic bookingDate = DateTime.now().millis - nexusSubscriber = nexusUser.ebicsSubscriber + nexusSubscriber = getEbicsSubscriberFromUser(nexusUser) status = "BOOK" } } else null @@ -370,8 +370,7 @@ class Taler(app: Route) { * all the prepared payments. */ app.post("/ebics/taler/{id}/accounts/{acctid}/refund-invalid-payments") { transaction { - val nexusUser = expectNexusIdTransaction(call.parameters["id"]) - val subscriber = nexusUser.ebicsSubscriber + val subscriber = getSubscriberEntityFromNexusUserId(call.parameters["id"]) val acctid = expectAcctidTransaction(call.parameters["acctid"]) if (!subscriberHasRights(subscriber, acctid)) { throw NexusError( @@ -549,7 +548,7 @@ class Taler(app: Route) { val nexusUser = expectNexusIdTransaction(exchangeId) EbicsToBankAccountEntity.new { bankAccount = newBankAccount - ebicsSubscriber = nexusUser.ebicsSubscriber + ebicsSubscriber = getEbicsSubscriberFromUser(nexusUser) } } } diff --git a/nexus/src/test/kotlin/authentication.kt b/nexus/src/test/kotlin/authentication.kt @@ -17,21 +17,11 @@ class AuthenticationTest { SchemaUtils.create(NexusUsersTable) NexusUserEntity.new(id = "username") { password = SerialBlob(CryptoUtil.hashStringSHA256("password")) - ebicsSubscriber = EbicsSubscriberEntity.new { - ebicsURL = "ebics url" - hostID = "host" - partnerID = "partner" - userID = "user" - systemID = "system" - signaturePrivateKey = SerialBlob("signturePrivateKey".toByteArray()) - authenticationPrivateKey = SerialBlob("authenticationPrivateKey".toByteArray()) - encryptionPrivateKey = SerialBlob("encryptionPrivateKey".toByteArray()) - } } // base64 of "username:password" == "dXNlcm5hbWU6cGFzc3dvcmQ=" - val (username: String, hashedPass: ByteArray) = extractUserAndHashedPassword( + val hashedPass= extractUserAndHashedPassword( "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" - ) + ).second val row = NexusUserEntity.findById("username") assert(row?.password == SerialBlob(hashedPass)) } @@ -39,7 +29,9 @@ class AuthenticationTest { @Test fun basicAuthHeaderTest() { - val (username: String, hashedPass: ByteArray) = extractUserAndHashedPassword("Basic dXNlcm5hbWU6cGFzc3dvcmQ=") + val hashedPass = extractUserAndHashedPassword( + "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ).second assert(CryptoUtil.hashStringSHA256("password").contentEquals(hashedPass)) } } \ No newline at end of file