diff options
author | Marcello Stanisci <ms@taler.net> | 2020-05-08 17:43:11 +0200 |
---|---|---|
committer | Marcello Stanisci <ms@taler.net> | 2020-05-08 17:43:11 +0200 |
commit | 606a74d6ee3a811f2d38a995951ff4763fe608d6 (patch) | |
tree | 0f0eff3436817d4e54250e849a9075379f055a27 /nexus/src/main | |
parent | 6aaa8b6aeac30c428443d68f3e17d5e331d45fa0 (diff) | |
download | libeufin-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.kt | 4 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt | 12 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 775 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Main2.kt | 216 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt | 881 |
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 |