aboutsummaryrefslogtreecommitdiff
path: root/nexus/src/main
diff options
context:
space:
mode:
authorMarcello Stanisci <ms@taler.net>2020-05-08 17:43:11 +0200
committerMarcello Stanisci <ms@taler.net>2020-05-08 17:43:11 +0200
commit606a74d6ee3a811f2d38a995951ff4763fe608d6 (patch)
tree0f0eff3436817d4e54250e849a9075379f055a27 /nexus/src/main
parent6aaa8b6aeac30c428443d68f3e17d5e331d45fa0 (diff)
downloadlibeufin-606a74d6ee3a811f2d38a995951ff4763fe608d6.tar.gz
libeufin-606a74d6ee3a811f2d38a995951ff4763fe608d6.tar.bz2
libeufin-606a74d6ee3a811f2d38a995951ff4763fe608d6.zip
fix compilation
Diffstat (limited to 'nexus/src/main')
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt4
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt12
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt775
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Main2.kt216
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt881
5 files changed, 942 insertions, 946 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
index fd4b2674..610db194 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
@@ -20,6 +20,10 @@ import java.time.ZonedDateTime
import java.time.Instant
import java.time.ZoneId
+fun isProduction(): Boolean {
+ return System.getenv("NEXUS_PRODUCTION") != null
+}
+
fun calculateRefund(amount: String): Amount {
// fixme: must apply refund fees!
return Amount(amount)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
index 63acf4bb..bb0a18ba 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
@@ -76,15 +76,15 @@ data class EbicsErrorJson(
val error: EbicsErrorDetailJson
)
-data class BankAccountInfoElement(
- var accountHolderName: String? = null,
+data class BankAccount(
+ var holder: String,
var iban: String,
- var bankCode: String,
- var accountId: String
+ var bic: String,
+ var account: String
)
-data class BankAccountsInfoResponse(
- var accounts: MutableList<BankAccountInfoElement> = mutableListOf()
+data class BankAccounts(
+ var accounts: MutableList<BankAccount> = mutableListOf()
)
/** THE NEXUS USER */
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index e0ae9289..6ed26161 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -58,15 +58,9 @@ import java.util.zip.InflaterInputStream
import javax.crypto.EncryptedPrivateKeyInfo
import javax.sql.rowset.serial.SerialBlob
-
data class NexusError(val statusCode: HttpStatusCode, val reason: String) : Exception()
-
val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
-fun isProduction(): Boolean {
- return System.getenv("NEXUS_PRODUCTION") != null
-}
-
@ExperimentalIoApi
@KtorExperimentalAPI
fun main() {
@@ -134,749 +128,82 @@ fun main() {
}
routing {
-
- /** GENERAL / DEBUG ENDPOINTS */
-
- get("/") {
- call.respondText("Hello by Nexus!\n")
- return@get
- }
- get("/test-auth") {
- authenticateRequest(call.request.headers["Authorization"])
- call.respondText("Authenticated!", ContentType.Text.Plain, HttpStatusCode.OK)
- return@get
- }
-
- /** USER endpoints (no EBICS) */
-
- get("/users/{id}/history") {
- /** NOTE: behaviour could be augmented by filtering on date range. */
- val nexusUser = extractNexusUser(call.parameters["id"])
- val ret = RawPayments()
- transaction {
- RawBankTransactionEntity.find {
- RawBankTransactionsTable.nexusUser eq nexusUser.id.value
- }.forEach {
- ret.payments.add(
- RawPayment(
- debitorIban = it.debitorIban,
- creditorIban = it.creditorIban,
- subject = it.unstructuredRemittanceInformation,
- date = DateTime(it.bookingDate).toDashedDate(),
- amount = "${it.currency}:${it.amount}"
- )
- )
- }
- }
- call.respond(
- HttpStatusCode.OK,
- ret
- )
- return@get
- }
- /** 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())
- }
- }
- }
- call.respond(ret)
- return@get
- }
- /** Get all the details associated with a NEXUS user */
- get("/users/{id}") {
- val response = transaction {
- val nexusUser = extractNexusUser(call.parameters["id"])
- NexusUser(
- userID = nexusUser.id.value
- )
- }
- call.respond(HttpStatusCode.OK, response)
+ /**
+ * Shows information about the requesting user.
+ */
+ get("/user") {
+ val userId = authenticateRequest(call.request.headers["Authorization"])
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
- )
+ /**
+ * Add a new ordinary user in the system (requires "admin" privileges)
+ */
+ post("/users") {
return@post
}
- /** Show 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()
- transaction {
- BankAccountMapEntity.find {
- BankAccountMapsTable.nexusUser eq id
- }.forEach {
- ret.accounts.add(
- BankAccountInfoElement(
- accountHolderName = it.bankAccount.accountHolder,
- iban = it.bankAccount.iban,
- bankCode = it.bankAccount.bankCode,
- accountId = it.bankAccount.id.value
- )
- )
- }
- }
- call.respond(
- HttpStatusCode.OK,
- ret
- )
- return@get
- }
- /** Show PREPARED payments */
- get("/users/{id}/payments") {
- val nexusUserId = expectId(call.parameters["id"])
- val ret = RawPayments()
- transaction {
- val nexusUser = extractNexusUser(nexusUserId)
- val bankAccountsMap = BankAccountMapEntity.find {
- BankAccountMapsTable.nexusUser eq nexusUser.id
- }
- bankAccountsMap.forEach {
- Pain001Entity.find {
- Pain001Table.debitorIban eq it.bankAccount.iban
- }.forEach {
- ret.payments.add(
- RawPayment(
- creditorIban = it.creditorIban,
- debitorIban = it.debitorIban,
- subject = it.subject,
- amount = "${it.currency}:${it.sum}",
- date = DateTime(it.date).toDashedDate()
- )
- )
- }
- }
- }
- call.respond(ret)
+ /**
+ * Shows the bank accounts belonging to the requesting user.
+ */
+ get("/bank-accounts") {
return@get
}
- post("/users/{id}/prepare-payment") {
- val nexusUser = extractNexusUser(call.parameters["id"])
- val pain001data = call.receive<Pain001Data>()
- transaction {
- if (!userHasRights(nexusUser, pain001data.debitorIban)) {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "User ${nexusUser.id.value} can't access ${pain001data.debitorIban}"
- )
- }
- }
- createPain001entity(pain001data, nexusUser)
- call.respondText(
- "Payment instructions persisted in DB",
- ContentType.Text.Plain, HttpStatusCode.OK
- )
+ /**
+ * Submit one particular payment at the bank.
+ */
+ post("/bank-accounts/{accountid}/prepared-payments/submit") {
return@post
}
- get("/users/{id}/raw-payments") {
- val nexusUser = extractNexusUser(call.parameters["id"])
- var ret = RawPayments()
- transaction {
- RawBankTransactionEntity.find {
- RawBankTransactionsTable.nexusUser eq nexusUser.id.value
- }.forEach {
- ret.payments.add(
- RawPayment(
- creditorIban = it.creditorIban,
- debitorIban = it.debitorIban,
- subject = it.unstructuredRemittanceInformation,
- date = DateTime(it.bookingDate).toDashedDate(),
- amount = it.amount + " " + it.currency
- )
- )
- }
- }
- call.respond(
- HttpStatusCode.OK,
- ret
- )
+ /**
+ * Shows information about one particular prepared payment.
+ */
+ get("/bank-accounts/{accountid}/prepared-payments/{uuid}") {
return@get
}
- /** Associate a EBICS subscriber to the existing user */
- post("/ebics/subscribers/{id}") {
- val nexusUser = extractNexusUser(call.parameters["id"])
- val body = call.receive<EbicsSubscriber>()
- 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)
- }
- nexusUser.ebicsSubscriber = newEbicsSubscriber
- }
- call.respondText(
- "EBICS user successfully created",
- 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")
- }
- 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(
- "Keys successfully restored",
- ContentType.Text.Plain,
- HttpStatusCode.OK
- )
- return@post
- }
-
- /** EBICS CONVENIENCE */
-
- get("/ebics/subscribers/{id}/pubkeys") {
- val nexusUser = extractNexusUser(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)
- )
- }
- call.respond(
- HttpStatusCode.OK,
- response
- )
- }
- get("/ebics/subscribers/{id}/keyletter") {
- val nexusUserId = expectId(call.parameters["id"])
- var usernameLine = "TODO"
- var recipientLine = "TODO"
- val customerIdLine = "TODO"
- var userIdLine = ""
- var esExponentLine = ""
- var esModulusLine = ""
- var authExponentLine = ""
- var authModulusLine = ""
- var encExponentLine = ""
- var encModulusLine = ""
- var esKeyHashLine = ""
- var encKeyHashLine = ""
- var authKeyHashLine = ""
- val esVersionLine = "A006"
- val authVersionLine = "X002"
- val encVersionLine = "E002"
- val now = Date()
- val dateFormat = SimpleDateFormat("DD.MM.YYYY")
- val timeFormat = SimpleDateFormat("HH:mm:ss")
- val dateLine = dateFormat.format(now)
- val timeLine = timeFormat.format(now)
- var hostID = ""
- transaction {
- val nexusUser = extractNexusUser(nexusUserId)
- val subscriber = getEbicsSubscriberFromUser(nexusUser)
- val signPubTmp = CryptoUtil.getRsaPublicFromPrivate(
- CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray())
- )
- val authPubTmp = CryptoUtil.getRsaPublicFromPrivate(
- CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray())
- )
- val encPubTmp = CryptoUtil.getRsaPublicFromPrivate(
- CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.toByteArray())
- )
- hostID = subscriber.hostID
- userIdLine = subscriber.userID
- esExponentLine = signPubTmp.publicExponent.toUnsignedHexString()
- esModulusLine = signPubTmp.modulus.toUnsignedHexString()
- encExponentLine = encPubTmp.publicExponent.toUnsignedHexString()
- encModulusLine = encPubTmp.modulus.toUnsignedHexString()
- authExponentLine = authPubTmp.publicExponent.toUnsignedHexString()
- authModulusLine = authPubTmp.modulus.toUnsignedHexString()
- esKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(signPubTmp).toHexString()
- encKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(encPubTmp).toHexString()
- authKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(authPubTmp).toHexString()
- }
- val iniLetter = """
- |Name: ${usernameLine}
- |Date: ${dateLine}
- |Time: ${timeLine}
- |Recipient: ${recipientLine}
- |Host ID: ${hostID}
- |User ID: ${userIdLine}
- |Partner ID: ${customerIdLine}
- |ES version: ${esVersionLine}
-
- |Public key for the electronic signature:
-
- |Exponent:
- |${chunkString(esExponentLine)}
-
- |Modulus:
- |${chunkString(esModulusLine)}
-
- |SHA-256 hash:
- |${chunkString(esKeyHashLine)}
-
- |I hereby confirm the above public keys for my electronic signature.
-
- |__________
- |Place/date
-
- |__________
- |Signature
- |
- """.trimMargin()
-
- val hiaLetter = """
- |Name: ${usernameLine}
- |Date: ${dateLine}
- |Time: ${timeLine}
- |Recipient: ${recipientLine}
- |Host ID: ${hostID}
- |User ID: ${userIdLine}
- |Partner ID: ${customerIdLine}
- |Identification and authentication signature version: ${authVersionLine}
- |Encryption version: ${encVersionLine}
-
- |Public key for the identification and authentication signature:
-
- |Exponent:
- |${chunkString(authExponentLine)}
-
- |Modulus:
- |${chunkString(authModulusLine)}
-
- |SHA-256 hash:
- |${chunkString(authKeyHashLine)}
-
- |Public encryption key:
-
- |Exponent:
- |${chunkString(encExponentLine)}
-
- |Modulus:
- |${chunkString(encModulusLine)}
-
- |SHA-256 hash:
- |${chunkString(encKeyHashLine)}
-
- |I hereby confirm the above public keys for my electronic signature.
-
- |__________
- |Place/date
-
- |__________
- |Signature
- |
- """.trimMargin()
-
- call.respondText(
- "####INI####:\n${iniLetter}\n\n\n####HIA####:\n${hiaLetter}",
- ContentType.Text.Plain,
- HttpStatusCode.OK
- )
- }
-
-
- /** STATE CHANGES VIA EBICS */
-
- post("/ebics/execute-payments") {
- val (paymentRowId, painDoc, subscriber) = 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.nexusUser.ebicsSubscriber)
- }
- if (subscriber == null) {
- throw NexusError(
- HttpStatusCode.NotFound,
- "Ebics subscriber wasn't found for this prepared payment."
- )
- }
- logger.debug("Uploading PAIN.001: ${painDoc}")
- doEbicsUploadTransaction(
- client,
- getSubscriberDetailsInternal(subscriber),
- "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
- }
- call.respondText(
- "CCT message submitted to the bank",
- ContentType.Text.Plain,
- HttpStatusCode.OK
- )
+ /**
+ * Adds a new prepared payment.
+ */
+ post("/bank-accounts/{accountid}/prepared-payments") {
return@post
}
- /** exports keys backup copy */
- post("/ebics/subscribers/{id}/backup") {
- val body = call.receive<EbicsBackupRequestJson>()
- val response = transaction {
- val nexusUser = extractNexusUser(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 = extractNexusUser(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)
+ /**
+ * Downloads new transactions from the bank.
+ */
+ post("/bank-accounts/{accountid}/collected-transactions") {
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
+ /**
+ * Queries list of transactions ALREADY downloaded from the bank.
+ */
+ get("/bank-accounts/{accountid}/collected-transactions") {
+ return@get
}
- 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.unzipWithLambda {
- logger.debug("C53 entry: ${it.second}")
- val fileName = it.first
- val camt53doc = XMLUtil.parseStringIntoDom(it.second)
- transaction {
- RawBankTransactionEntity.new {
- sourceFileName = fileName
- unstructuredRemittanceInformation = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Ustrd']")
- 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 = parseDashedDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis
- nexusUser = extractNexusUser(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))
- )
- }
- }
+ /**
+ * Adds a new bank transport.
+ */
+ post("/bank-transports") {
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.
+ * Sends to the bank a message "MSG" according to the transport
+ * "transportName". Does not alterate any DB table.
*/
- post("/ebics/subscribers/{id}/fetch-accounts") {
- val nexusUser = extractNexusUser((call.parameters["id"]))
- val paramsJson = call.receive<EbicsStandardOrderParamsJson>()
- val orderParams = paramsJson.toOrderParams()
- val subscriberData = getSubscriberDetailsFromNexusUserId(nexusUser.id.value)
- val response = doEbicsDownloadTransaction(client, subscriberData, "HTD", orderParams)
- when (response) {
- is EbicsDownloadSuccessResult -> {
- val payload = XMLUtil.convertStringToJaxb<HTDResponseOrderData>(response.orderData.toString(Charsets.UTF_8))
- transaction {
- payload.value.partnerInfo.accountInfoList?.forEach {
- val bankAccount = BankAccountEntity.new(id = it.id) {
- accountHolder = it.accountHolder
- 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")
- }
- BankAccountMapEntity.new {
- ebicsSubscriber = getEbicsSubscriberFromUser(nexusUser)
- this.nexusUser = nexusUser
- this.bankAccount = bankAccount
- }
- }
-
- }
- 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("/bank-transports/{transportName}/send{MSG}") {
return@post
}
-
- /** 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))
- )
- }
- }
+ /**
+ * Sends the bank a message "MSG" according to the transport
+ * "transportName". DOES alterate DB tables.
+ */
+ post("/bank-transports/{transportName}/sync{MSG}") {
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,
- EbicsHevResponseJson(versionDetails.versions.map { ebicsVersionSpec ->
- ProtocolAndVersionJson(
- ebicsVersionSpec.protocol,
- ebicsVersionSpec.version
- )
- })
- )
+ /**
+ * Hello endpoint.
+ */
+ get("/") {
+ call.respondText("Hello by nexus!\n")
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}/sendHIA") {
- val id = expectId(call.parameters["id"])
- val subscriberData = getSubscriberDetailsFromNexusUserId(id)
- 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(
- "Bank accepted authentication and encryption keys\n",
- ContentType.Text.Plain,
- HttpStatusCode.OK
- )
- return@post
- }
-
- /** PLUGINS */
- /* Taler class will initialize all the relevant handlers. */
- Taler(this)
}
}
logger.info("Up and running")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main2.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main2.kt
deleted file mode 100644
index 8de652b6..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main2.kt
+++ /dev/null
@@ -1,216 +0,0 @@
-package tech.libeufin.nexus
-
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2019 Stanisci and Dold.
-
- * 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/>
- */
-
-import io.ktor.application.ApplicationCallPipeline
-import io.ktor.application.call
-import io.ktor.application.install
-import io.ktor.client.HttpClient
-import io.ktor.features.*
-import io.ktor.gson.gson
-import io.ktor.http.ContentType
-import io.ktor.http.HttpStatusCode
-import io.ktor.request.ApplicationReceivePipeline
-import io.ktor.request.ApplicationReceiveRequest
-import io.ktor.request.receive
-import io.ktor.request.uri
-import io.ktor.response.respond
-import io.ktor.response.respondText
-import io.ktor.routing.get
-import io.ktor.routing.post
-import io.ktor.routing.routing
-import io.ktor.server.engine.embeddedServer
-import io.ktor.server.netty.Netty
-import io.ktor.util.KtorExperimentalAPI
-import kotlinx.coroutines.io.ByteReadChannel
-import kotlinx.coroutines.io.jvm.javaio.toByteReadChannel
-import kotlinx.coroutines.io.jvm.javaio.toInputStream
-import kotlinx.io.core.ExperimentalIoApi
-import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.joda.time.DateTime
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import org.slf4j.event.Level
-import tech.libeufin.util.*
-import tech.libeufin.util.ebics_h004.HTDResponseOrderData
-import java.text.DateFormat
-import java.text.SimpleDateFormat
-import java.util.*
-import java.util.zip.InflaterInputStream
-import javax.crypto.EncryptedPrivateKeyInfo
-import javax.sql.rowset.serial.SerialBlob
-
-
-/**
- * Conflicts with current Main.kt
-
-data class NexusError(val statusCode: HttpStatusCode, val reason: String) : Exception()
-val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
-fun isProduction(): Boolean {
- return System.getenv("NEXUS_PRODUCTION") != null
-}
- */
-
-@ExperimentalIoApi
-@KtorExperimentalAPI
-fun main() {
- dbCreateTables()
- val client = HttpClient() {
- expectSuccess = false // this way, it does not throw exceptions on != 200 responses.
- }
- val server = embeddedServer(Netty, port = 5001) {
-
- install(CallLogging) {
- this.level = Level.DEBUG
- this.logger = tech.libeufin.nexus.logger
- }
- install(ContentNegotiation) {
- gson {
- setDateFormat(DateFormat.LONG)
- setPrettyPrinting()
- }
- }
- install(StatusPages) {
- exception<NexusError> { cause ->
- logger.error("Exception while handling '${call.request.uri}'", cause)
- call.respondText(
- cause.reason,
- ContentType.Text.Plain,
- cause.statusCode
- )
- }
- exception<UtilError> { cause ->
- logger.error("Exception while handling '${call.request.uri}'", cause)
- call.respondText(
- cause.reason,
- ContentType.Text.Plain,
- cause.statusCode
- )
- }
- exception<Exception> { cause ->
- logger.error("Uncaught exception while handling '${call.request.uri}'", cause)
- logger.error(cause.toString())
- call.respondText(
- "Internal server error",
- ContentType.Text.Plain,
- HttpStatusCode.InternalServerError
- )
- }
- }
-
- intercept(ApplicationCallPipeline.Fallback) {
- if (this.call.response.status() == null) {
- call.respondText("Not found (no route matched).\n", ContentType.Text.Plain, HttpStatusCode.NotFound)
- return@intercept finish()
- }
- }
-
- receivePipeline.intercept(ApplicationReceivePipeline.Before) {
- if (this.context.request.headers["Content-Encoding"] == "deflate") {
- logger.debug("About to inflate received data")
- val deflated = this.subject.value as ByteReadChannel
- val inflated = InflaterInputStream(deflated.toInputStream())
- proceedWith(ApplicationReceiveRequest(this.subject.typeInfo, inflated.toByteReadChannel()))
- return@intercept
- }
- proceed()
- return@intercept
- }
-
- routing {
-
- get("/") {
- call.respondText("Hello by nexus!\n")
- return@get
- }
- /**
- * Shows information about the requesting user.
- */
- get("/user") {
- return@get
- }
- /**
- * Add a new ordinary user in the system (requires "admin" privileges)
- */
- post("/users") {
- return@post
- }
- /**
- * Shows the bank accounts belonging to the requesting user.
- */
- get("/bank-accounts") {
- return@get
- }
- /**
- * Submit one particular payment at the bank.
- */
- post("/bank-accounts/{accountid}/prepared-payments/submit") {
- return@post
- }
- /**
- * Shows information about one particular prepared payment.
- */
- get("/bank-accounts/{accountid}/prepared-payments/{uuid}") {
- return@get
- }
- /**
- * Adds a new prepared payment.
- */
- post("/bank-accounts/{accountid}/prepared-payments") {
- return@post
- }
- /**
- * Downloads new transactions from the bank.
- */
- post("/bank-accounts/{accountid}/collected-transactions") {
- return@post
- }
- /**
- * Queries list of transactions ALREADY downloaded from the bank.
- */
- get("/bank-accounts/{accountid}/collected-transactions") {
- return@get
- }
- /**
- * Adds a new bank transport.
- */
- post("/bank-transports") {
- return@post
- }
- /**
- * Sends to the bank a message "MSG" according to the transport
- * "transportName". Does not alterate any DB table.
- */
- post("/bank-transports/{transportName}/send{MSG}") {
- return@post
- }
- /**
- * Sends the bank a message "MSG" according to the transport
- * "transportName". DOES alterate DB tables.
- */
- post("/bank-transports/{transportName}/sync{MSG}") {
- return@post
- }
- }
- }
- 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/MainDeprecated.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt
new file mode 100644
index 00000000..eb9215a5
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt
@@ -0,0 +1,881 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2019 Stanisci and Dold.
+
+ * 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/>
+ */
+
+package tech.libeufin.nexus
+
+import io.ktor.application.ApplicationCallPipeline
+import io.ktor.application.call
+import io.ktor.application.install
+import io.ktor.client.HttpClient
+import io.ktor.features.*
+import io.ktor.gson.gson
+import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import io.ktor.request.ApplicationReceivePipeline
+import io.ktor.request.ApplicationReceiveRequest
+import io.ktor.request.receive
+import io.ktor.request.uri
+import io.ktor.response.respond
+import io.ktor.response.respondText
+import io.ktor.routing.get
+import io.ktor.routing.post
+import io.ktor.routing.routing
+import io.ktor.server.engine.embeddedServer
+import io.ktor.server.netty.Netty
+import io.ktor.util.KtorExperimentalAPI
+import kotlinx.coroutines.io.ByteReadChannel
+import kotlinx.coroutines.io.jvm.javaio.toByteReadChannel
+import kotlinx.coroutines.io.jvm.javaio.toInputStream
+import kotlinx.io.core.ExperimentalIoApi
+import org.jetbrains.exposed.sql.and
+import org.jetbrains.exposed.sql.transactions.transaction
+import org.joda.time.DateTime
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.slf4j.event.Level
+import tech.libeufin.util.*
+import tech.libeufin.util.ebics_h004.HTDResponseOrderData
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.zip.InflaterInputStream
+import javax.crypto.EncryptedPrivateKeyInfo
+import javax.sql.rowset.serial.SerialBlob
+
+
+data class NexusError(val statusCode: HttpStatusCode, val reason: String) : Exception()
+
+val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
+
+
+@ExperimentalIoApi
+@KtorExperimentalAPI
+fun main() {
+ dbCreateTables()
+ val client = HttpClient() {
+ expectSuccess = false // this way, it does not throw exceptions on != 200 responses.
+ }
+ val server = embeddedServer(Netty, port = 5001) {
+
+ install(CallLogging) {
+ this.level = Level.DEBUG
+ this.logger = tech.libeufin.nexus.logger
+ }
+ install(ContentNegotiation) {
+ gson {
+ setDateFormat(DateFormat.LONG)
+ setPrettyPrinting()
+ }
+ }
+ install(StatusPages) {
+ exception<NexusError> { cause ->
+ logger.error("Exception while handling '${call.request.uri}'", cause)
+ call.respondText(
+ cause.reason,
+ ContentType.Text.Plain,
+ cause.statusCode
+ )
+ }
+ exception<UtilError> { cause ->
+ logger.error("Exception while handling '${call.request.uri}'", cause)
+ call.respondText(
+ cause.reason,
+ ContentType.Text.Plain,
+ cause.statusCode
+ )
+ }
+ exception<Exception> { cause ->
+ logger.error("Uncaught exception while handling '${call.request.uri}'", cause)
+ logger.error(cause.toString())
+ call.respondText(
+ "Internal server error",
+ ContentType.Text.Plain,
+ HttpStatusCode.InternalServerError
+ )
+ }
+ }
+
+ intercept(ApplicationCallPipeline.Fallback) {
+ if (this.call.response.status() == null) {
+ call.respondText("Not found (no route matched).\n", ContentType.Text.Plain, HttpStatusCode.NotFound)
+ return@intercept finish()
+ }
+ }
+
+ receivePipeline.intercept(ApplicationReceivePipeline.Before) {
+ if (this.context.request.headers["Content-Encoding"] == "deflate") {
+ logger.debug("About to inflate received data")
+ val deflated = this.subject.value as ByteReadChannel
+ val inflated = InflaterInputStream(deflated.toInputStream())
+ proceedWith(ApplicationReceiveRequest(this.subject.typeInfo, inflated.toByteReadChannel()))
+ return@intercept
+ }
+ proceed()
+ return@intercept
+ }
+
+ routing {
+
+ /** GENERAL / DEBUG ENDPOINTS */
+
+ get("/") {
+ call.respondText("Hello by Nexus!\n")
+ return@get
+ }
+ get("/test-auth") {
+ authenticateRequest(call.request.headers["Authorization"])
+ call.respondText("Authenticated!", ContentType.Text.Plain, HttpStatusCode.OK)
+ return@get
+ }
+
+ /** USER endpoints (no EBICS) */
+
+ get("/users/{id}/history") {
+ /** NOTE: behaviour could be augmented by filtering on date range. */
+ val nexusUser = extractNexusUser(call.parameters["id"])
+ val ret = RawPayments()
+ transaction {
+ RawBankTransactionEntity.find {
+ RawBankTransactionsTable.nexusUser eq nexusUser.id.value
+ }.forEach {
+ ret.payments.add(
+ RawPayment(
+ debitorIban = it.debitorIban,
+ creditorIban = it.creditorIban,
+ subject = it.unstructuredRemittanceInformation,
+ date = DateTime(it.bookingDate).toDashedDate(),
+ amount = "${it.currency}:${it.amount}"
+ )
+ )
+ }
+ }
+ call.respond(
+ HttpStatusCode.OK,
+ ret
+ )
+ return@get
+ }
+ /** 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())
+ }
+ }
+ }
+ call.respond(ret)
+ return@get
+ }
+ /** Get all the details associated with a NEXUS user */
+ get("/users/{id}") {
+ val response = transaction {
+ val nexusUser = extractNexusUser(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
+ }
+ /** Show 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()
+ transaction {
+ BankAccountMapEntity.find {
+ BankAccountMapsTable.nexusUser eq id
+ }.forEach {
+ ret.accounts.add(
+ BankAccountInfoElement(
+ accountHolderName = it.bankAccount.accountHolder,
+ iban = it.bankAccount.iban,
+ bankCode = it.bankAccount.bankCode,
+ accountId = it.bankAccount.id.value
+ )
+ )
+ }
+ }
+ call.respond(
+ HttpStatusCode.OK,
+ ret
+ )
+ return@get
+ }
+ /** Show PREPARED payments */
+ get("/users/{id}/payments") {
+ val nexusUserId = expectId(call.parameters["id"])
+ val ret = RawPayments()
+ transaction {
+ val nexusUser = extractNexusUser(nexusUserId)
+ val bankAccountsMap = BankAccountMapEntity.find {
+ BankAccountMapsTable.nexusUser eq nexusUser.id
+ }
+ bankAccountsMap.forEach {
+ Pain001Entity.find {
+ Pain001Table.debitorIban eq it.bankAccount.iban
+ }.forEach {
+ ret.payments.add(
+ RawPayment(
+ creditorIban = it.creditorIban,
+ debitorIban = it.debitorIban,
+ subject = it.subject,
+ amount = "${it.currency}:${it.sum}",
+ date = DateTime(it.date).toDashedDate()
+ )
+ )
+ }
+ }
+ }
+ call.respond(ret)
+ return@get
+ }
+ post("/users/{id}/prepare-payment") {
+ val nexusUser = extractNexusUser(call.parameters["id"])
+ val pain001data = call.receive<Pain001Data>()
+ transaction {
+ if (!userHasRights(nexusUser, pain001data.debitorIban)) {
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "User ${nexusUser.id.value} can't access ${pain001data.debitorIban}"
+ )
+ }
+ }
+ createPain001entity(pain001data, nexusUser)
+ call.respondText(
+ "Payment instructions persisted in DB",
+ ContentType.Text.Plain, HttpStatusCode.OK
+ )
+ return@post
+ }
+ get("/users/{id}/raw-payments") {
+ val nexusUser = extractNexusUser(call.parameters["id"])
+ var ret = RawPayments()
+ transaction {
+ RawBankTransactionEntity.find {
+ RawBankTransactionsTable.nexusUser eq nexusUser.id.value
+ }.forEach {
+ ret.payments.add(
+ RawPayment(
+ creditorIban = it.creditorIban,
+ debitorIban = it.debitorIban,
+ subject = it.unstructuredRemittanceInformation,
+ date = DateTime(it.bookingDate).toDashedDate(),
+ amount = it.amount + " " + it.currency
+ )
+ )
+ }
+ }
+ call.respond(
+ HttpStatusCode.OK,
+ ret
+ )
+ return@get
+ }
+ /** Associate a EBICS subscriber to the existing user */
+ post("/ebics/subscribers/{id}") {
+ val nexusUser = extractNexusUser(call.parameters["id"])
+ val body = call.receive<EbicsSubscriber>()
+ 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)
+ }
+ nexusUser.ebicsSubscriber = newEbicsSubscriber
+ }
+ call.respondText(
+ "EBICS user successfully created",
+ 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")
+ }
+ 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(
+ "Keys successfully restored",
+ ContentType.Text.Plain,
+ HttpStatusCode.OK
+ )
+ return@post
+ }
+
+ /** EBICS CONVENIENCE */
+
+ get("/ebics/subscribers/{id}/pubkeys") {
+ val nexusUser = extractNexusUser(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)
+ )
+ }
+ call.respond(
+ HttpStatusCode.OK,
+ response
+ )
+ }
+ get("/ebics/subscribers/{id}/keyletter") {
+ val nexusUserId = expectId(call.parameters["id"])
+ var usernameLine = "TODO"
+ var recipientLine = "TODO"
+ val customerIdLine = "TODO"
+ var userIdLine = ""
+ var esExponentLine = ""
+ var esModulusLine = ""
+ var authExponentLine = ""
+ var authModulusLine = ""
+ var encExponentLine = ""
+ var encModulusLine = ""
+ var esKeyHashLine = ""
+ var encKeyHashLine = ""
+ var authKeyHashLine = ""
+ val esVersionLine = "A006"
+ val authVersionLine = "X002"
+ val encVersionLine = "E002"
+ val now = Date()
+ val dateFormat = SimpleDateFormat("DD.MM.YYYY")
+ val timeFormat = SimpleDateFormat("HH:mm:ss")
+ val dateLine = dateFormat.format(now)
+ val timeLine = timeFormat.format(now)
+ var hostID = ""
+ transaction {
+ val nexusUser = extractNexusUser(nexusUserId)
+ val subscriber = getEbicsSubscriberFromUser(nexusUser)
+ val signPubTmp = CryptoUtil.getRsaPublicFromPrivate(
+ CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray())
+ )
+ val authPubTmp = CryptoUtil.getRsaPublicFromPrivate(
+ CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray())
+ )
+ val encPubTmp = CryptoUtil.getRsaPublicFromPrivate(
+ CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.toByteArray())
+ )
+ hostID = subscriber.hostID
+ userIdLine = subscriber.userID
+ esExponentLine = signPubTmp.publicExponent.toUnsignedHexString()
+ esModulusLine = signPubTmp.modulus.toUnsignedHexString()
+ encExponentLine = encPubTmp.publicExponent.toUnsignedHexString()
+ encModulusLine = encPubTmp.modulus.toUnsignedHexString()
+ authExponentLine = authPubTmp.publicExponent.toUnsignedHexString()
+ authModulusLine = authPubTmp.modulus.toUnsignedHexString()
+ esKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(signPubTmp).toHexString()
+ encKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(encPubTmp).toHexString()
+ authKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(authPubTmp).toHexString()
+ }
+ val iniLetter = """
+ |Name: ${usernameLine}
+ |Date: ${dateLine}
+ |Time: ${timeLine}
+ |Recipient: ${recipientLine}
+ |Host ID: ${hostID}
+ |User ID: ${userIdLine}
+ |Partner ID: ${customerIdLine}
+ |ES version: ${esVersionLine}
+
+ |Public key for the electronic signature:
+
+ |Exponent:
+ |${chunkString(esExponentLine)}
+
+ |Modulus:
+ |${chunkString(esModulusLine)}
+
+ |SHA-256 hash:
+ |${chunkString(esKeyHashLine)}
+
+ |I hereby confirm the above public keys for my electronic signature.
+
+ |__________
+ |Place/date
+
+ |__________
+ |Signature
+ |
+ """.trimMargin()
+
+ val hiaLetter = """
+ |Name: ${usernameLine}
+ |Date: ${dateLine}
+ |Time: ${timeLine}
+ |Recipient: ${recipientLine}
+ |Host ID: ${hostID}
+ |User ID: ${userIdLine}
+ |Partner ID: ${customerIdLine}
+ |Identification and authentication signature version: ${authVersionLine}
+ |Encryption version: ${encVersionLine}
+
+ |Public key for the identification and authentication signature:
+
+ |Exponent:
+ |${chunkString(authExponentLine)}
+
+ |Modulus:
+ |${chunkString(authModulusLine)}
+
+ |SHA-256 hash:
+ |${chunkString(authKeyHashLine)}
+
+ |Public encryption key:
+
+ |Exponent:
+ |${chunkString(encExponentLine)}
+
+ |Modulus:
+ |${chunkString(encModulusLine)}
+
+ |SHA-256 hash:
+ |${chunkString(encKeyHashLine)}
+
+ |I hereby confirm the above public keys for my electronic signature.
+
+ |__________
+ |Place/date
+
+ |__________
+ |Signature
+ |
+ """.trimMargin()
+
+ call.respondText(
+ "####INI####:\n${iniLetter}\n\n\n####HIA####:\n${hiaLetter}",
+ ContentType.Text.Plain,
+ HttpStatusCode.OK
+ )
+ }
+
+
+ /** STATE CHANGES VIA EBICS */
+
+ post("/ebics/execute-payments") {
+ val (paymentRowId, painDoc, subscriber) = 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.nexusUser.ebicsSubscriber)
+ }
+ if (subscriber == null) {
+ throw NexusError(
+ HttpStatusCode.NotFound,
+ "Ebics subscriber wasn't found for this prepared payment."
+ )
+ }
+ logger.debug("Uploading PAIN.001: ${painDoc}")
+ doEbicsUploadTransaction(
+ client,
+ getSubscriberDetailsInternal(subscriber),
+ "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
+ }
+ call.respondText(
+ "CCT message submitted to the bank",
+ ContentType.Text.Plain,
+ HttpStatusCode.OK
+ )
+ return@post
+ }
+ /** exports keys backup copy */
+ post("/ebics/subscribers/{id}/backup") {
+ val body = call.receive<EbicsBackupRequestJson>()
+ val response = transaction {
+ val nexusUser = extractNexusUser(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 = extractNexusUser(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.unzipWithLambda {
+ logger.debug("C53 entry: ${it.second}")
+ val fileName = it.first
+ val camt53doc = XMLUtil.parseStringIntoDom(it.second)
+ transaction {
+ RawBankTransactionEntity.new {
+ sourceFileName = fileName
+ unstructuredRemittanceInformation = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Ustrd']")
+ 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 = parseDashedDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis
+ nexusUser = extractNexusUser(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 nexusUser = extractNexusUser((call.parameters["id"]))
+ val paramsJson = call.receive<EbicsStandardOrderParamsJson>()
+ val orderParams = paramsJson.toOrderParams()
+ val subscriberData = getSubscriberDetailsFromNexusUserId(nexusUser.id.value)
+ val response = doEbicsDownloadTransaction(client, subscriberData, "HTD", orderParams)
+ when (response) {
+ is EbicsDownloadSuccessResult -> {
+ val payload = XMLUtil.convertStringToJaxb<HTDResponseOrderData>(response.orderData.toString(Charsets.UTF_8))
+ transaction {
+ payload.value.partnerInfo.accountInfoList?.forEach {
+ val bankAccount = BankAccountEntity.new(id = it.id) {
+ accountHolder = it.accountHolder
+ 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")
+ }
+ BankAccountMapEntity.new {
+ ebicsSubscriber = getEbicsSubscriberFromUser(nexusUser)
+ this.nexusUser = nexusUser
+ this.bankAccount = bankAccount
+ }
+ }
+
+ }
+ 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
+ }
+
+ /** 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))
+ )
+ }
+ }
+ 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,
+ 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}/sendHIA") {
+ val id = expectId(call.parameters["id"])
+ val subscriberData = getSubscriberDetailsFromNexusUserId(id)
+ 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(
+ "Bank accepted authentication and encryption keys\n",
+ ContentType.Text.Plain,
+ HttpStatusCode.OK
+ )
+ 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