[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated: refactor, towards common interface for
From: |
gnunet |
Subject: |
[libeufin] branch master updated: refactor, towards common interface for bank protocols |
Date: |
Fri, 19 Jun 2020 08:51:23 +0200 |
This is an automated email from the git hooks/post-receive script.
dold pushed a commit to branch master
in repository libeufin.
The following commit(s) were added to refs/heads/master by this push:
new f214ac0 refactor, towards common interface for bank protocols
f214ac0 is described below
commit f214ac079dae5a93e8716bf0349f8e70b5df0957
Author: Florian Dold <florian.dold@gmail.com>
AuthorDate: Fri Jun 19 12:21:07 2020 +0530
refactor, towards common interface for bank protocols
---
.../tech/libeufin/nexus/BankConnectionProtocol.kt | 20 +
nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 3 +
.../src/main/kotlin/tech/libeufin/nexus/Errors.kt | 25 +
nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 792 +--------------------
.../main/kotlin/tech/libeufin/nexus/Scheduling.kt | 101 +++
.../tech/libeufin/nexus/bankaccount/BankAccount.kt | 47 ++
.../kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 36 +-
.../tech/libeufin/nexus/{ => server}/JSON.kt | 9 +-
.../nexus/{Main.kt => server/NexusServer.kt} | 417 ++++-------
nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt | 18 +-
10 files changed, 384 insertions(+), 1084 deletions(-)
diff --git
a/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
new file mode 100644
index 0000000..cbe5ebb
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
@@ -0,0 +1,20 @@
+package tech.libeufin.nexus/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2020 Taler Systems S.A.
+ *
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+ *
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+interface BankConnectionProtocol
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
index 2b7475a..ab07fde 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -51,6 +51,7 @@ object TalerRequestedPayments : LongIdTable() {
class TalerRequestedPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
companion object :
LongEntityClass<TalerRequestedPaymentEntity>(TalerRequestedPayments)
+
var preparedPayment by PaymentInitiationEntity referencedOn
TalerRequestedPayments.preparedPayment
var requestUId by TalerRequestedPayments.requestUId
var amount by TalerRequestedPayments.amount
@@ -148,6 +149,7 @@ object NexusBankTransactionsTable : LongIdTable() {
class NexusBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) {
companion object :
LongEntityClass<NexusBankTransactionEntity>(NexusBankTransactionsTable)
+
var currency by NexusBankTransactionsTable.currency
var amount by NexusBankTransactionsTable.amount
var status by NexusBankTransactionsTable.status
@@ -316,6 +318,7 @@ object TalerFacadeStateTable : IntIdTable() {
val reserveTransferLevel = text("reserveTransferLevel")
val intervalIncrement = text("intervalIncrement")
val facade = reference("facade", FacadesTable)
+
// highest ID seen in the raw transactions table.
val highestSeenMsgID = long("highestSeenMsgID").default(0)
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt
new file mode 100644
index 0000000..b5a40f3
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt
@@ -0,0 +1,25 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2020 Taler Systems S.A.
+ *
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+ *
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.nexus
+
+import io.ktor.http.HttpStatusCode
+
+data class NexusError(val statusCode: HttpStatusCode, val reason: String) :
+ Exception("$reason (HTTP status $statusCode)")
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index 46ddafa..31e9e7b 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -19,14 +19,6 @@
package tech.libeufin.nexus
-import com.fasterxml.jackson.core.util.DefaultIndenter
-import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.databind.exc.MismatchedInputException
-import com.fasterxml.jackson.module.kotlin.KotlinModule
-import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.core.subcommands
@@ -34,50 +26,11 @@ import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.prompt
-import io.ktor.application.ApplicationCall
-import io.ktor.application.ApplicationCallPipeline
-import io.ktor.application.call
-import io.ktor.application.install
-import io.ktor.client.HttpClient
-import io.ktor.features.CallLogging
-import io.ktor.features.ContentNegotiation
-import io.ktor.features.StatusPages
-import io.ktor.http.ContentType
-import io.ktor.http.HttpStatusCode
-import io.ktor.jackson.jackson
-import io.ktor.request.*
-import io.ktor.response.respond
-import io.ktor.response.respondBytes
-import io.ktor.response.respondText
-import io.ktor.routing.get
-import io.ktor.routing.post
-import io.ktor.routing.route
-import io.ktor.routing.routing
-import io.ktor.server.engine.embeddedServer
-import io.ktor.server.netty.Netty
-import io.ktor.utils.io.ByteReadChannel
-import io.ktor.utils.io.jvm.javaio.toByteReadChannel
-import io.ktor.utils.io.jvm.javaio.toInputStream
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.time.delay
-import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.Logger
import org.slf4j.LoggerFactory
-import org.slf4j.event.Level
-import tech.libeufin.nexus.bankaccount.*
-import tech.libeufin.nexus.ebics.*
-import tech.libeufin.util.*
+import tech.libeufin.nexus.server.serverMain
import tech.libeufin.util.CryptoUtil.hashpw
-import java.io.PrintWriter
-import java.io.StringWriter
-import java.net.URLEncoder
-import java.time.Duration
-import java.util.zip.InflaterInputStream
-
-data class NexusError(val statusCode: HttpStatusCode, val reason: String) :
- Exception("$reason (HTTP status $statusCode)")
val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
@@ -122,746 +75,3 @@ fun main(args: Array<String>) {
.subcommands(Serve(), Superuser())
.main(args)
}
-
-suspend inline fun <reified T : Any> ApplicationCall.receiveJson(): T {
- try {
- return this.receive<T>()
- } catch (e: MissingKotlinParameterException) {
- throw NexusError(HttpStatusCode.BadRequest, "Missing value for
${e.pathReference}")
- } catch (e: MismatchedInputException) {
- throw NexusError(HttpStatusCode.BadRequest, "Invalid value for
${e.pathReference}")
- }
-}
-
-/**
- * Test HTTP basic auth. Throws error if password is wrong,
- * and makes sure that the user exists in the system.
- *
- * @param authorization the Authorization:-header line.
- * @return user id
- */
-fun authenticateRequest(request: ApplicationRequest): NexusUserEntity {
- val authorization = request.headers["Authorization"]
- val headerLine = if (authorization == null) throw NexusError(
- HttpStatusCode.BadRequest, "Authentication:-header line not found"
- ) else authorization
- val (username, password) = extractUserAndPassword(headerLine)
- val user = NexusUserEntity.find {
- NexusUsersTable.id eq username
- }.firstOrNull()
- if (user == null) {
- throw NexusError(HttpStatusCode.Unauthorized, "Unknown user
'$username'")
- }
- if (!CryptoUtil.checkpw(password, user.passwordHash)) {
- throw NexusError(HttpStatusCode.Forbidden, "Wrong password")
- }
- return user
-}
-
-
-fun createLoopbackBankConnection(bankConnectionName: String, user:
NexusUserEntity, data: JsonNode) {
- val bankConn = NexusBankConnectionEntity.new(bankConnectionName) {
- owner = user
- type = "loopback"
- }
- val bankAccount = jacksonObjectMapper().treeToValue(data,
BankAccount::class.java)
- NexusBankAccountEntity.new(bankAccount.account) {
- iban = bankAccount.iban
- bankCode = bankAccount.bic
- accountHolder = bankAccount.holder
- defaultBankConnection = bankConn
- highestSeenBankMessageId = 0
- }
-}
-
-fun createEbicsBankConnection(bankConnectionName: String, user:
NexusUserEntity, data: JsonNode) {
- val bankConn = NexusBankConnectionEntity.new(bankConnectionName) {
- owner = user
- type = "ebics"
- }
- val newTransportData = jacksonObjectMapper().treeToValue(data,
EbicsNewTransport::class.java)
- val pairA = CryptoUtil.generateRsaKeyPair(2048)
- val pairB = CryptoUtil.generateRsaKeyPair(2048)
- val pairC = CryptoUtil.generateRsaKeyPair(2048)
- EbicsSubscriberEntity.new {
- ebicsURL = newTransportData.ebicsURL
- hostID = newTransportData.hostID
- partnerID = newTransportData.partnerID
- userID = newTransportData.userID
- systemID = newTransportData.systemID
- signaturePrivateKey = ExposedBlob((pairA.private.encoded))
- encryptionPrivateKey = ExposedBlob((pairB.private.encoded))
- authenticationPrivateKey = ExposedBlob((pairC.private.encoded))
- nexusBankConnection = bankConn
- ebicsIniState = EbicsInitState.NOT_SENT
- ebicsHiaState = EbicsInitState.NOT_SENT
- }
-}
-
-fun requireBankConnection(call: ApplicationCall, parameterKey: String):
NexusBankConnectionEntity {
- val name = call.parameters[parameterKey]
- if (name == null) {
- throw NexusError(HttpStatusCode.InternalServerError, "no parameter for
bank connection")
- }
- val conn = transaction { NexusBankConnectionEntity.findById(name) }
- if (conn == null) {
- throw NexusError(HttpStatusCode.NotFound, "bank connection '$name' not
found")
- }
- return conn
-}
-
-fun ApplicationRequest.hasBody(): Boolean {
- if (this.isChunked()) {
- return true
- }
- val contentLengthHeaderStr = this.headers["content-length"]
- if (contentLengthHeaderStr != null) {
- try {
- val cl = contentLengthHeaderStr.toInt()
- return cl != 0
- } catch (e: NumberFormatException) {
- return false
- }
- }
- return false
-}
-
-inline fun reportAndIgnoreErrors(f: () -> Unit) {
- try {
- f()
- } catch (e: java.lang.Exception) {
- logger.error("ignoring exception", e)
- }
-}
-
-fun moreFrequentBackgroundTasks(httpClient: HttpClient) {
- GlobalScope.launch {
- while (true) {
- logger.debug("Running more frequent background jobs")
- reportAndIgnoreErrors {
- downloadTalerFacadesTransactions(
- httpClient,
- FetchSpecLatestJson(FetchLevel.ALL, null)
- )
- }
- // FIXME: should be done automatically after raw ingestion
- reportAndIgnoreErrors { ingestTalerTransactions() }
- reportAndIgnoreErrors { submitAllPaymentInitiations(httpClient) }
- logger.debug("More frequent background jobs done")
- delay(Duration.ofSeconds(1))
- }
- }
-}
-
-fun lessFrequentBackgroundTasks(httpClient: HttpClient) {
- GlobalScope.launch {
- while (true) {
- logger.debug("Less frequent background job")
- try {
- //downloadTalerFacadesTransactions(httpClient, "C53")
- } catch (e: Exception) {
- val sw = StringWriter()
- val pw = PrintWriter(sw)
- e.printStackTrace(pw)
- logger.info("==== Less frequent background task exception
====\n${sw}======")
- }
- delay(Duration.ofSeconds(10))
- }
- }
-}
-
-/** Crawls all the facades, and requests history for each of its creators. */
-suspend fun downloadTalerFacadesTransactions(httpClient: HttpClient,
fetchSpec: FetchSpecJson) {
- val work = mutableListOf<Pair<String, String>>()
- transaction {
- TalerFacadeStateEntity.all().forEach {
- logger.debug("Fetching history for facade: ${it.id.value}, bank
account: ${it.bankAccount}")
- work.add(Pair(it.facade.creator.id.value, it.bankAccount))
- }
- }
- work.forEach {
- fetchTransactionsInternal(
- client = httpClient,
- fetchSpec = fetchSpec,
- userId = it.first,
- accountid = it.second
- )
- }
-}
-
-fun <T> expectNonNull(param: T?): T {
- return param ?: throw EbicsProtocolError(
- HttpStatusCode.BadRequest,
- "Non-null value expected."
- )
-}
-
-fun ApplicationCall.expectUrlParameter(name: String): String {
- return this.request.queryParameters[name]
- ?: throw EbicsProtocolError(HttpStatusCode.BadRequest, "Parameter
'$name' not provided in URI")
-}
-
-private suspend fun fetchTransactionsInternal(
- client: HttpClient,
- fetchSpec: FetchSpecJson,
- userId: String,
- accountid: String
-) {
- val res = transaction {
- val acct = NexusBankAccountEntity.findById(accountid)
- if (acct == null) {
- throw NexusError(
- HttpStatusCode.NotFound,
- "Account not found"
- )
- }
- val conn = acct.defaultBankConnection
- if (conn == null) {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "No default bank connection (explicit connection not yet
supported)"
- )
- }
- return@transaction object {
- val connectionType = conn.type
- val connectionName = conn.id.value
- }
- }
- when (res.connectionType) {
- "ebics" -> {
- // FIXME(dold): Support fetching not only the latest transactions.
- // It's not clear what's the nicest way to support this.
- fetchEbicsBySpec(
- fetchSpec,
- client,
- res.connectionName
- )
- ingestBankMessagesIntoAccount(res.connectionName, accountid)
- }
- else -> throw NexusError(
- HttpStatusCode.BadRequest,
- "Connection type '${res.connectionType}' not implemented"
- )
- }
-}
-
-fun ensureNonNull(param: String?): String {
- return param ?: throw NexusError(
- HttpStatusCode.BadRequest, "Bad ID given: ${param}"
- )
-}
-
-fun ensureLong(param: String?): Long {
- val asString = ensureNonNull(param)
- return asString.toLongOrNull() ?: throw NexusError(
- HttpStatusCode.BadRequest, "Parameter is not a number: ${param}"
- )
-}
-
-/**
- * This helper function parses a Authorization:-header line, decode the
credentials
- * and returns a pair made of username and hashed (sha256) password. The
hashed value
- * will then be compared with the one kept into the database.
- */
-fun extractUserAndPassword(authorizationHeader: String): Pair<String, String> {
- logger.debug("Authenticating: $authorizationHeader")
- val (username, password) = try {
- val split = authorizationHeader.split(" ")
- val plainUserAndPass = String(base64ToBytes(split[1]), Charsets.UTF_8)
- plainUserAndPass.split(":")
- } catch (e: java.lang.Exception) {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "invalid Authorization:-header received"
- )
- }
- return Pair(username, password)
-}
-
-fun serverMain(dbName: String) {
- dbCreateTables(dbName)
- 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) {
- jackson {
- enable(SerializationFeature.INDENT_OUTPUT)
- setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
-
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
- indentObjectsWith(DefaultIndenter(" ", "\n"))
- })
- registerModule(KotlinModule(nullisSameAsDefault = true))
- }
- }
-
- install(StatusPages) {
- exception<NexusError> { cause ->
- logger.error("Exception while handling '${call.request.uri}'",
cause)
- call.respondText(
- cause.reason,
- ContentType.Text.Plain,
- cause.statusCode
- )
- }
- exception<EbicsProtocolError> { 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()
- }
- }
-
- /**
- * Allow request body compression. Needed by Taler.
- */
- 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
- }
-
- lessFrequentBackgroundTasks(client)
- moreFrequentBackgroundTasks(client)
-
- routing {
- /**
- * Shows information about the requesting user.
- */
- get("/user") {
- val ret = transaction {
- val currentUser = authenticateRequest(call.request)
- UserResponse(
- username = currentUser.id.value,
- superuser = currentUser.superuser
- )
- }
- call.respond(HttpStatusCode.OK, ret)
- return@get
- }
-
- get("/users") {
- val users = transaction {
- transaction {
- NexusUserEntity.all().map {
- UserInfo(it.id.value, it.superuser)
- }
- }
- }
- val usersResp = UsersResponse(users)
- call.respond(HttpStatusCode.OK, usersResp)
- return@get
- }
-
- /**
- * Add a new ordinary user in the system (requires superuser
privileges)
- */
- post("/users") {
- val body = call.receiveJson<User>()
- transaction {
- val currentUser = authenticateRequest(call.request)
- if (!currentUser.superuser) {
- throw NexusError(HttpStatusCode.Forbidden, "only
superuser can do that")
- }
- NexusUserEntity.new(body.username) {
- passwordHash = hashpw(body.password)
- superuser = false
- }
- }
- call.respondText(
- "New NEXUS user registered. ID: ${body.username}",
- ContentType.Text.Plain,
- HttpStatusCode.OK
- )
- return@post
- }
-
- get("/bank-connection-protocols") {
- call.respond(HttpStatusCode.OK,
BankProtocolsResponse(listOf("ebics", "loopback")))
- return@get
- }
-
- route("/bank-connection-protocols/ebics") {
- ebicsBankProtocolRoutes(client)
- }
-
- /**
- * Shows the bank accounts belonging to the requesting user.
- */
- get("/bank-accounts") {
- val bankAccounts = BankAccounts()
- transaction {
- authenticateRequest(call.request)
- // FIXME(dold): Only return accounts the user has at least
read access to?
- NexusBankAccountEntity.all().forEach {
-
bankAccounts.accounts.add(BankAccount(it.accountHolder, it.iban, it.bankCode,
it.id.value))
- }
- }
- call.respond(bankAccounts)
- return@get
- }
-
- get("/bank-accounts/{accountid}") {
- val accountId = ensureNonNull(call.parameters["accountid"])
- val res = transaction {
- val user = authenticateRequest(call.request)
- val bankAccount =
NexusBankAccountEntity.findById(accountId)
- if (bankAccount == null) {
- throw NexusError(HttpStatusCode.NotFound, "unknown
bank account")
- }
- val holderEnc =
URLEncoder.encode(bankAccount.accountHolder, "UTF-8")
- return@transaction object {
- val defaultBankConnection =
bankAccount.defaultBankConnection?.id?.value
- val accountPaytoUri =
"payto://iban/${bankAccount.iban}?receiver-name=$holderEnc"
- }
- }
- call.respond(res)
- }
- /**
- * Submit one particular payment to the bank.
- */
-
post("/bank-accounts/{accountid}/payment-initiations/{uuid}/submit") {
- val uuid = ensureLong(call.parameters["uuid"])
- val accountId = ensureNonNull(call.parameters["accountid"])
- val res = transaction {
- authenticateRequest(call.request)
- }
- submitPaymentInitiation(client, uuid)
- call.respondText("Payment ${uuid} submitted")
- return@post
- }
-
- /**
- * Shows information about one particular payment initiation.
- */
- get("/bank-accounts/{accountid}/payment-initiations/{uuid}") {
- val res = transaction {
- val user = authenticateRequest(call.request)
- val paymentInitiation =
getPaymentInitiation(ensureLong(call.parameters["uuid"]))
- return@transaction object {
- val paymentInitiation = paymentInitiation
- }
- }
- val sd = res.paymentInitiation.submissionDate
- call.respond(
- PaymentStatus(
- paymentInitiationId =
res.paymentInitiation.id.value.toString(),
- submitted = res.paymentInitiation.submitted,
- creditorName = res.paymentInitiation.creditorName,
- creditorBic = res.paymentInitiation.creditorBic,
- creditorIban = res.paymentInitiation.creditorIban,
- amount =
"${res.paymentInitiation.currency}:${res.paymentInitiation.sum}",
- subject = res.paymentInitiation.subject,
- submissionDate = if (sd != null) {
- importDateFromMillis(sd).toDashedDate()
- } else null,
- preparationDate =
importDateFromMillis(res.paymentInitiation.preparationDate).toDashedDate()
- )
- )
- return@get
- }
-
- /**
- * Adds a new payment initiation.
- */
- post("/bank-accounts/{accountid}/payment-initiations") {
- val body = call.receive<CreatePaymentInitiationRequest>()
- val accountId = ensureNonNull(call.parameters["accountid"])
- val res = transaction {
- authenticateRequest(call.request)
- val bankAccount =
NexusBankAccountEntity.findById(accountId)
- if (bankAccount == null) {
- throw NexusError(HttpStatusCode.NotFound, "unknown
bank account")
- }
- val amount = parseAmount(body.amount)
- val paymentEntity = addPaymentInitiation(
- Pain001Data(
- creditorIban = body.iban,
- creditorBic = body.bic,
- creditorName = body.name,
- sum = amount.amount,
- currency = amount.currency,
- subject = body.subject
- ),
- bankAccount
- )
- return@transaction object {
- val uuid = paymentEntity.id.value
- }
- }
- call.respond(
- HttpStatusCode.OK,
- PaymentInitiationResponse(uuid = res.uuid.toString())
- )
- return@post
- }
-
- /**
- * Downloads new transactions from the bank.
- */
- post("/bank-accounts/{accountid}/fetch-transactions") {
- val accountid = call.parameters["accountid"]
- if (accountid == null) {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "Account id missing"
- )
- }
- val user = transaction { authenticateRequest(call.request) }
- val fetchSpec = if (call.request.hasBody()) {
- call.receive<FetchSpecJson>()
- } else {
- FetchSpecLatestJson(FetchLevel.ALL, null)
- }
- fetchTransactionsInternal(
- client,
- fetchSpec,
- user.id.value,
- accountid
- )
- call.respondText("Collection performed")
- return@post
- }
-
- /**
- * Asks list of transactions ALREADY downloaded from the bank.
- */
- get("/bank-accounts/{accountid}/transactions") {
- val bankAccount = expectNonNull(call.parameters["accountid"])
- val start = call.request.queryParameters["start"]
- val end = call.request.queryParameters["end"]
- val ret = Transactions()
- transaction {
- authenticateRequest(call.request).id.value
- NexusBankTransactionEntity.all().map {
- val tx =
jacksonObjectMapper().readValue(it.transactionJson, BankTransaction::class.java)
- ret.transactions.add(tx)
- }
- }
- call.respond(ret)
- return@get
- }
-
- /**
- * Adds a new bank transport.
- */
- post("/bank-connections") {
- // user exists and is authenticated.
- val body = call.receive<CreateBankConnectionRequestJson>()
- transaction {
- val user = authenticateRequest(call.request)
- when (body) {
- is CreateBankConnectionFromBackupRequestJson -> {
- val type = body.data.get("type")
- if (type == null || !type.isTextual) {
- throw NexusError(HttpStatusCode.BadRequest,
"backup needs type")
- }
- when (type.textValue()) {
- "ebics" -> {
-
createEbicsBankConnectionFromBackup(body.name, user, body.passphrase, body.data)
- }
- else -> {
- throw
NexusError(HttpStatusCode.BadRequest, "backup type not supported")
- }
- }
- }
- is CreateBankConnectionFromNewRequestJson -> {
- when (body.type) {
- "ebics" -> {
- createEbicsBankConnection(body.name, user,
body.data)
- }
- "loopback" -> {
- createLoopbackBankConnection(body.name,
user, body.data)
-
- }
- else -> {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "connection type ${body.type} not
supported"
- )
- }
- }
- }
- }
- }
- call.respond(object {})
- }
-
- get("/bank-connections") {
- val connList = mutableListOf<BankConnectionInfo>()
- transaction {
- NexusBankConnectionEntity.all().forEach {
- connList.add(BankConnectionInfo(it.id.value, it.type))
- }
- }
- call.respond(BankConnectionsList(connList))
- }
-
- get("/bank-connections/{connid}") {
- val resp = transaction {
- val user = authenticateRequest(call.request)
- val conn = requireBankConnection(call, "connid")
- when (conn.type) {
- "ebics" -> {
- getEbicsConnectionDetails(conn)
- }
- else -> {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "bank connection is not of type 'ebics' (but
'${conn.type}')"
- )
- }
- }
- }
- call.respond(resp)
- }
-
- post("/bank-connections/{connid}/export-backup") {
- transaction { authenticateRequest(call.request) }
- val body = call.receive<BackupRequestJson>()
- val response = run {
- val conn = requireBankConnection(call, "connid")
- when (conn.type) {
- "ebics" -> {
- exportEbicsKeyBackup(conn.id.value,
body.passphrase)
- }
- else -> {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "bank connection is not of type 'ebics' (but
'${conn.type}')"
- )
- }
- }
- }
- call.response.headers.append("Content-Disposition",
"attachment")
- call.respond(
- HttpStatusCode.OK,
- response
- )
- }
-
- post("/bank-connections/{connid}/connect") {
- val conn = transaction {
- authenticateRequest(call.request)
- requireBankConnection(call, "connid")
- }
- when (conn.type) {
- "ebics" -> {
- connectEbics(client, conn.id.value)
- }
- }
- call.respond(object {})
- }
-
- get("/bank-connections/{connid}/keyletter") {
- val conn = transaction {
- authenticateRequest(call.request)
- requireBankConnection(call, "connid")
- }
- when (conn.type) {
- "ebics" -> {
- val pdfBytes = getEbicsKeyLetterPdf(conn)
- call.respondBytes(pdfBytes, ContentType("application",
"pdf"))
- }
- else -> throw NexusError(HttpStatusCode.NotImplemented,
"keyletter not supporte dfor ${conn.type}")
- }
- }
-
- get("/bank-connections/{connid}/messages") {
- val ret = transaction {
- val list = BankMessageList()
- val conn = requireBankConnection(call, "connid")
- NexusBankMessageEntity.find {
NexusBankMessagesTable.bankConnection eq conn.id }.map {
- list.bankMessages.add(BankMessageInfo(it.messageId,
it.code, it.message.bytes.size.toLong()))
- }
- list
- }
- call.respond(ret)
- }
-
- get("/bank-connections/{connid}/messages/{msgid}") {
- val ret = transaction {
- val msgid = call.parameters["msgid"]
- if (msgid == null || msgid == "") {
- throw NexusError(HttpStatusCode.BadRequest, "missing
or invalid message ID")
- }
- val msg = NexusBankMessageEntity.find {
NexusBankMessagesTable.messageId eq msgid }.firstOrNull()
- if (msg == null) {
- throw NexusError(HttpStatusCode.NotFound, "bank
message not found")
- }
- return@transaction object {
- val msgContent = msg.message.bytes
- }
- }
- call.respondBytes(ret.msgContent, ContentType("application",
"xml"))
- }
-
- post("/facades") {
- val body = call.receive<FacadeInfo>()
- val newFacade = transaction {
- val user = authenticateRequest(call.request)
- FacadeEntity.new(body.name) {
- type = body.type
- creator = user
- }
- }
- transaction {
- TalerFacadeStateEntity.new {
- bankAccount = body.config.bankAccount
- bankConnection = body.config.bankConnection
- intervalIncrement = body.config.intervalIncremental
- reserveTransferLevel = body.config.reserveTransferLevel
- facade = newFacade
- }
- }
- call.respondText("Facade created")
- return@post
- }
-
- route("/bank-connections/{connid}/ebics") {
- ebicsBankConnectionRoutes(client)
- }
-
- route("/facades/{fcid}/taler") {
- talerFacadeRoutes(this, client)
- }
- /**
- * Hello endpoint.
- */
- get("/") {
- call.respondText("Hello, this is Nexus.\n")
- return@get
- }
- }
- }
- logger.info("Up and running")
- server.start(wait = true)
-}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt
new file mode 100644
index 0000000..c530adf
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt
@@ -0,0 +1,101 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2020 Taler Systems S.A.
+ *
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+ *
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.nexus
+
+import io.ktor.client.HttpClient
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.time.delay
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.nexus.bankaccount.fetchTransactionsInternal
+import tech.libeufin.nexus.bankaccount.submitAllPaymentInitiations
+import tech.libeufin.nexus.server.FetchLevel
+import tech.libeufin.nexus.server.FetchSpecJson
+import tech.libeufin.nexus.server.FetchSpecLatestJson
+import java.io.PrintWriter
+import java.io.StringWriter
+import java.time.Duration
+
+/** Crawls all the facades, and requests history for each of its creators. */
+suspend fun downloadTalerFacadesTransactions(httpClient: HttpClient,
fetchSpec: FetchSpecJson) {
+ val work = mutableListOf<Pair<String, String>>()
+ transaction {
+ TalerFacadeStateEntity.all().forEach {
+ logger.debug("Fetching history for facade: ${it.id.value}, bank
account: ${it.bankAccount}")
+ work.add(Pair(it.facade.creator.id.value, it.bankAccount))
+ }
+ }
+ work.forEach {
+ fetchTransactionsInternal(
+ client = httpClient,
+ fetchSpec = fetchSpec,
+ userId = it.first,
+ accountid = it.second
+ )
+ }
+}
+
+
+private inline fun reportAndIgnoreErrors(f: () -> Unit) {
+ try {
+ f()
+ } catch (e: java.lang.Exception) {
+ logger.error("ignoring exception", e)
+ }
+}
+
+fun moreFrequentBackgroundTasks(httpClient: HttpClient) {
+ GlobalScope.launch {
+ while (true) {
+ logger.debug("Running more frequent background jobs")
+ reportAndIgnoreErrors {
+ downloadTalerFacadesTransactions(
+ httpClient,
+ FetchSpecLatestJson(
+ FetchLevel.ALL,
+ null
+ )
+ )
+ }
+ // FIXME: should be done automatically after raw ingestion
+ reportAndIgnoreErrors { ingestTalerTransactions() }
+ reportAndIgnoreErrors { submitAllPaymentInitiations(httpClient) }
+ logger.debug("More frequent background jobs done")
+ delay(Duration.ofSeconds(1))
+ }
+ }
+}
+
+fun lessFrequentBackgroundTasks(httpClient: HttpClient) {
+ GlobalScope.launch {
+ while (true) {
+ logger.debug("Less frequent background job")
+ try {
+ //downloadTalerFacadesTransactions(httpClient, "C53")
+ } catch (e: Exception) {
+ val sw = StringWriter()
+ val pw = PrintWriter(sw)
+ e.printStackTrace(pw)
+ logger.info("==== Less frequent background task exception
====\n${sw}======")
+ }
+ delay(Duration.ofSeconds(10))
+ }
+ }
+}
diff --git
a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
index d35e9e6..bf672ce 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -27,7 +27,10 @@ import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction
import org.w3c.dom.Document
import tech.libeufin.nexus.*
+import tech.libeufin.nexus.ebics.fetchEbicsBySpec
import tech.libeufin.nexus.ebics.submitEbicsPaymentInitiation
+import tech.libeufin.nexus.server.FetchSpecJson
+import tech.libeufin.nexus.server.Pain001Data
import tech.libeufin.util.XMLUtil
import java.time.Instant
@@ -229,3 +232,47 @@ fun addPaymentInitiation(paymentData: Pain001Data,
debitorAccount: NexusBankAcco
}
}
}
+
+suspend fun fetchTransactionsInternal(
+ client: HttpClient,
+ fetchSpec: FetchSpecJson,
+ userId: String,
+ accountid: String
+) {
+ val res = transaction {
+ val acct = NexusBankAccountEntity.findById(accountid)
+ if (acct == null) {
+ throw NexusError(
+ HttpStatusCode.NotFound,
+ "Account not found"
+ )
+ }
+ val conn = acct.defaultBankConnection
+ if (conn == null) {
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "No default bank connection (explicit connection not yet
supported)"
+ )
+ }
+ return@transaction object {
+ val connectionType = conn.type
+ val connectionName = conn.id.value
+ }
+ }
+ when (res.connectionType) {
+ "ebics" -> {
+ // FIXME(dold): Support fetching not only the latest transactions.
+ // It's not clear what's the nicest way to support this.
+ fetchEbicsBySpec(
+ fetchSpec,
+ client,
+ res.connectionName
+ )
+ ingestBankMessagesIntoAccount(res.connectionName, accountid)
+ }
+ else -> throw NexusError(
+ HttpStatusCode.BadRequest,
+ "Connection type '${res.connectionType}' not implemented"
+ )
+ }
+}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
index 4d4c27f..022aa64 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -44,6 +44,7 @@ import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
import tech.libeufin.nexus.*
import tech.libeufin.nexus.logger
+import tech.libeufin.nexus.server.*
import tech.libeufin.util.*
import tech.libeufin.util.ebics_h004.EbicsTypes
import tech.libeufin.util.ebics_h004.HTDResponseOrderData
@@ -422,7 +423,12 @@ fun Route.ebicsBankConnectionRoutes(client: HttpClient) {
is EbicsDownloadBankErrorResult -> {
call.respond(
HttpStatusCode.BadGateway,
- EbicsErrorJson(EbicsErrorDetailJson("bankError",
response.returnCode.errorCode))
+ EbicsErrorJson(
+ EbicsErrorDetailJson(
+ "bankError",
+ response.returnCode.errorCode
+ )
+ )
)
}
}
@@ -655,7 +661,8 @@ suspend fun submitEbicsPaymentInitiation(httpClient:
HttpClient, paymentInitiati
subject = paymentInitiation.subject,
instructionId = paymentInitiation.instructionId,
endToEndId = paymentInitiation.endToEndId
- ))
+ )
+ )
object {
val subscriberDetails = subscriberDetails
val painMessage = painMessage
@@ -674,3 +681,28 @@ suspend fun submitEbicsPaymentInitiation(httpClient:
HttpClient, paymentInitiati
paymentInitiation.submitted = true
}
}
+
+
+fun createEbicsBankConnection(bankConnectionName: String, user:
NexusUserEntity, data: JsonNode) {
+ val bankConn = NexusBankConnectionEntity.new(bankConnectionName) {
+ owner = user
+ type = "ebics"
+ }
+ val newTransportData = jacksonObjectMapper().treeToValue(data,
EbicsNewTransport::class.java)
+ val pairA = CryptoUtil.generateRsaKeyPair(2048)
+ val pairB = CryptoUtil.generateRsaKeyPair(2048)
+ val pairC = CryptoUtil.generateRsaKeyPair(2048)
+ EbicsSubscriberEntity.new {
+ ebicsURL = newTransportData.ebicsURL
+ hostID = newTransportData.hostID
+ partnerID = newTransportData.partnerID
+ userID = newTransportData.userID
+ systemID = newTransportData.systemID
+ signaturePrivateKey = ExposedBlob((pairA.private.encoded))
+ encryptionPrivateKey = ExposedBlob((pairB.private.encoded))
+ authenticationPrivateKey = ExposedBlob((pairC.private.encoded))
+ nexusBankConnection = bankConn
+ ebicsIniState = EbicsInitState.NOT_SENT
+ ebicsHiaState = EbicsInitState.NOT_SENT
+ }
+}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
similarity index 98%
rename from nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
rename to nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
index d3bf9c0..0301a05 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
@@ -17,16 +17,16 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.nexus
+package tech.libeufin.nexus.server
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonTypeName
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.databind.JsonNode
+import tech.libeufin.nexus.BankTransaction
import tech.libeufin.util.*
import java.time.LocalDate
-import java.time.LocalDateTime
data class BackupRequestJson(
val passphrase: String
@@ -135,7 +135,7 @@ enum class FetchLevel(@get:JsonValue val jsonName: String) {
@JsonSubTypes(
JsonSubTypes.Type(value = FetchSpecLatestJson::class, name = "latest"),
JsonSubTypes.Type(value = FetchSpecAllJson::class, name = "all"),
- JsonSubTypes.Type(value = FetchSpecPreviousDaysJson::class, name =
"previous-days") ,
+ JsonSubTypes.Type(value = FetchSpecPreviousDaysJson::class, name =
"previous-days"),
JsonSubTypes.Type(value = FetchSpecSinceLastJson::class, name =
"since-last")
)
abstract class FetchSpecJson(
@@ -145,10 +145,13 @@ abstract class FetchSpecJson(
@JsonTypeName("latest")
class FetchSpecLatestJson(level: FetchLevel, bankConnection: String?) :
FetchSpecJson(level, bankConnection)
+
@JsonTypeName("all")
class FetchSpecAllJson(level: FetchLevel, bankConnection: String?) :
FetchSpecJson(level, bankConnection)
+
@JsonTypeName("since-last")
class FetchSpecSinceLastJson(level: FetchLevel, bankConnection: String?) :
FetchSpecJson(level, bankConnection)
+
@JsonTypeName("previous-days")
class FetchSpecPreviousDaysJson(level: FetchLevel, bankConnection: String?,
val number: Int) :
FetchSpecJson(level, bankConnection)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
similarity index 76%
copy from nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
copy to nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index 46ddafa..6e4a422 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -1,23 +1,4 @@
-/*
- * 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
+package tech.libeufin.nexus.server
import com.fasterxml.jackson.core.util.DefaultIndenter
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
@@ -27,13 +8,6 @@ import
com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import com.github.ajalt.clikt.core.CliktCommand
-import com.github.ajalt.clikt.core.ProgramResult
-import com.github.ajalt.clikt.core.subcommands
-import com.github.ajalt.clikt.parameters.arguments.argument
-import com.github.ajalt.clikt.parameters.options.default
-import com.github.ajalt.clikt.parameters.options.option
-import com.github.ajalt.clikt.parameters.options.prompt
import io.ktor.application.ApplicationCall
import io.ktor.application.ApplicationCallPipeline
import io.ktor.application.call
@@ -58,81 +32,61 @@ import io.ktor.server.netty.Netty
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.jvm.javaio.toByteReadChannel
import io.ktor.utils.io.jvm.javaio.toInputStream
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.time.delay
-import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
import org.slf4j.event.Level
-import tech.libeufin.nexus.bankaccount.*
+import tech.libeufin.nexus.*
+import tech.libeufin.nexus.bankaccount.addPaymentInitiation
+import tech.libeufin.nexus.bankaccount.fetchTransactionsInternal
+import tech.libeufin.nexus.bankaccount.getPaymentInitiation
+import tech.libeufin.nexus.bankaccount.submitPaymentInitiation
import tech.libeufin.nexus.ebics.*
import tech.libeufin.util.*
-import tech.libeufin.util.CryptoUtil.hashpw
-import java.io.PrintWriter
-import java.io.StringWriter
+import tech.libeufin.util.logger
import java.net.URLEncoder
-import java.time.Duration
import java.util.zip.InflaterInputStream
-data class NexusError(val statusCode: HttpStatusCode, val reason: String) :
- Exception("$reason (HTTP status $statusCode)")
-val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
-
-class NexusCommand : CliktCommand() {
- override fun run() = Unit
-}
-
-class Serve : CliktCommand("Run nexus HTTP server") {
- private val dbName by option().default("libeufin-nexus.sqlite3")
- override fun run() {
- serverMain(dbName)
- }
+fun ensureNonNull(param: String?): String {
+ return param ?: throw NexusError(
+ HttpStatusCode.BadRequest, "Bad ID given: ${param}"
+ )
}
-class Superuser : CliktCommand("Add superuser or change pw") {
- private val dbName by option().default("libeufin-nexus.sqlite3")
- private val username by argument()
- private val password by option().prompt(requireConfirmation = true,
hideInput = true)
- override fun run() {
- dbCreateTables(dbName)
- transaction {
- val hashedPw = hashpw(password)
- val user = NexusUserEntity.findById(username)
- if (user == null) {
- NexusUserEntity.new(username) {
- this.passwordHash = hashedPw
- this.superuser = true
- }
- } else {
- if (!user.superuser) {
- println("Can only change password for superuser with this
command.")
- throw ProgramResult(1)
- }
- user.passwordHash = hashedPw
- }
- }
- }
+fun ensureLong(param: String?): Long {
+ val asString = ensureNonNull(param)
+ return asString.toLongOrNull() ?: throw NexusError(
+ HttpStatusCode.BadRequest, "Parameter is not a number: ${param}"
+ )
}
-fun main(args: Array<String>) {
- NexusCommand()
- .subcommands(Serve(), Superuser())
- .main(args)
+fun <T> expectNonNull(param: T?): T {
+ return param ?: throw EbicsProtocolError(
+ HttpStatusCode.BadRequest,
+ "Non-null value expected."
+ )
}
-suspend inline fun <reified T : Any> ApplicationCall.receiveJson(): T {
- try {
- return this.receive<T>()
- } catch (e: MissingKotlinParameterException) {
- throw NexusError(HttpStatusCode.BadRequest, "Missing value for
${e.pathReference}")
- } catch (e: MismatchedInputException) {
- throw NexusError(HttpStatusCode.BadRequest, "Invalid value for
${e.pathReference}")
+/**
+ * This helper function parses a Authorization:-header line, decode the
credentials
+ * and returns a pair made of username and hashed (sha256) password. The
hashed value
+ * will then be compared with the one kept into the database.
+ */
+fun extractUserAndPassword(authorizationHeader: String): Pair<String, String> {
+ logger.debug("Authenticating: $authorizationHeader")
+ val (username, password) = try {
+ val split = authorizationHeader.split(" ")
+ val plainUserAndPass = String(base64ToBytes(split[1]), Charsets.UTF_8)
+ plainUserAndPass.split(":")
+ } catch (e: java.lang.Exception) {
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "invalid Authorization:-header received"
+ )
}
+ return Pair(username, password)
}
+
/**
* Test HTTP basic auth. Throws error if password is wrong,
* and makes sure that the user exists in the system.
@@ -159,57 +113,6 @@ fun authenticateRequest(request: ApplicationRequest):
NexusUserEntity {
}
-fun createLoopbackBankConnection(bankConnectionName: String, user:
NexusUserEntity, data: JsonNode) {
- val bankConn = NexusBankConnectionEntity.new(bankConnectionName) {
- owner = user
- type = "loopback"
- }
- val bankAccount = jacksonObjectMapper().treeToValue(data,
BankAccount::class.java)
- NexusBankAccountEntity.new(bankAccount.account) {
- iban = bankAccount.iban
- bankCode = bankAccount.bic
- accountHolder = bankAccount.holder
- defaultBankConnection = bankConn
- highestSeenBankMessageId = 0
- }
-}
-
-fun createEbicsBankConnection(bankConnectionName: String, user:
NexusUserEntity, data: JsonNode) {
- val bankConn = NexusBankConnectionEntity.new(bankConnectionName) {
- owner = user
- type = "ebics"
- }
- val newTransportData = jacksonObjectMapper().treeToValue(data,
EbicsNewTransport::class.java)
- val pairA = CryptoUtil.generateRsaKeyPair(2048)
- val pairB = CryptoUtil.generateRsaKeyPair(2048)
- val pairC = CryptoUtil.generateRsaKeyPair(2048)
- EbicsSubscriberEntity.new {
- ebicsURL = newTransportData.ebicsURL
- hostID = newTransportData.hostID
- partnerID = newTransportData.partnerID
- userID = newTransportData.userID
- systemID = newTransportData.systemID
- signaturePrivateKey = ExposedBlob((pairA.private.encoded))
- encryptionPrivateKey = ExposedBlob((pairB.private.encoded))
- authenticationPrivateKey = ExposedBlob((pairC.private.encoded))
- nexusBankConnection = bankConn
- ebicsIniState = EbicsInitState.NOT_SENT
- ebicsHiaState = EbicsInitState.NOT_SENT
- }
-}
-
-fun requireBankConnection(call: ApplicationCall, parameterKey: String):
NexusBankConnectionEntity {
- val name = call.parameters[parameterKey]
- if (name == null) {
- throw NexusError(HttpStatusCode.InternalServerError, "no parameter for
bank connection")
- }
- val conn = transaction { NexusBankConnectionEntity.findById(name) }
- if (conn == null) {
- throw NexusError(HttpStatusCode.NotFound, "bank connection '$name' not
found")
- }
- return conn
-}
-
fun ApplicationRequest.hasBody(): Boolean {
if (this.isChunked()) {
return true
@@ -226,157 +129,50 @@ fun ApplicationRequest.hasBody(): Boolean {
return false
}
-inline fun reportAndIgnoreErrors(f: () -> Unit) {
- try {
- f()
- } catch (e: java.lang.Exception) {
- logger.error("ignoring exception", e)
- }
+fun ApplicationCall.expectUrlParameter(name: String): String {
+ return this.request.queryParameters[name]
+ ?: throw EbicsProtocolError(HttpStatusCode.BadRequest, "Parameter
'$name' not provided in URI")
}
-fun moreFrequentBackgroundTasks(httpClient: HttpClient) {
- GlobalScope.launch {
- while (true) {
- logger.debug("Running more frequent background jobs")
- reportAndIgnoreErrors {
- downloadTalerFacadesTransactions(
- httpClient,
- FetchSpecLatestJson(FetchLevel.ALL, null)
- )
- }
- // FIXME: should be done automatically after raw ingestion
- reportAndIgnoreErrors { ingestTalerTransactions() }
- reportAndIgnoreErrors { submitAllPaymentInitiations(httpClient) }
- logger.debug("More frequent background jobs done")
- delay(Duration.ofSeconds(1))
- }
+suspend inline fun <reified T : Any> ApplicationCall.receiveJson(): T {
+ try {
+ return this.receive<T>()
+ } catch (e: MissingKotlinParameterException) {
+ throw NexusError(HttpStatusCode.BadRequest, "Missing value for
${e.pathReference}")
+ } catch (e: MismatchedInputException) {
+ throw NexusError(HttpStatusCode.BadRequest, "Invalid value for
${e.pathReference}")
}
}
-fun lessFrequentBackgroundTasks(httpClient: HttpClient) {
- GlobalScope.launch {
- while (true) {
- logger.debug("Less frequent background job")
- try {
- //downloadTalerFacadesTransactions(httpClient, "C53")
- } catch (e: Exception) {
- val sw = StringWriter()
- val pw = PrintWriter(sw)
- e.printStackTrace(pw)
- logger.info("==== Less frequent background task exception
====\n${sw}======")
- }
- delay(Duration.ofSeconds(10))
- }
- }
-}
-/** Crawls all the facades, and requests history for each of its creators. */
-suspend fun downloadTalerFacadesTransactions(httpClient: HttpClient,
fetchSpec: FetchSpecJson) {
- val work = mutableListOf<Pair<String, String>>()
- transaction {
- TalerFacadeStateEntity.all().forEach {
- logger.debug("Fetching history for facade: ${it.id.value}, bank
account: ${it.bankAccount}")
- work.add(Pair(it.facade.creator.id.value, it.bankAccount))
- }
+fun createLoopbackBankConnection(bankConnectionName: String, user:
NexusUserEntity, data: JsonNode) {
+ val bankConn = NexusBankConnectionEntity.new(bankConnectionName) {
+ owner = user
+ type = "loopback"
}
- work.forEach {
- fetchTransactionsInternal(
- client = httpClient,
- fetchSpec = fetchSpec,
- userId = it.first,
- accountid = it.second
- )
+ val bankAccount = jacksonObjectMapper().treeToValue(data,
BankAccount::class.java)
+ NexusBankAccountEntity.new(bankAccount.account) {
+ iban = bankAccount.iban
+ bankCode = bankAccount.bic
+ accountHolder = bankAccount.holder
+ defaultBankConnection = bankConn
+ highestSeenBankMessageId = 0
}
}
-fun <T> expectNonNull(param: T?): T {
- return param ?: throw EbicsProtocolError(
- HttpStatusCode.BadRequest,
- "Non-null value expected."
- )
-}
-
-fun ApplicationCall.expectUrlParameter(name: String): String {
- return this.request.queryParameters[name]
- ?: throw EbicsProtocolError(HttpStatusCode.BadRequest, "Parameter
'$name' not provided in URI")
-}
-private suspend fun fetchTransactionsInternal(
- client: HttpClient,
- fetchSpec: FetchSpecJson,
- userId: String,
- accountid: String
-) {
- val res = transaction {
- val acct = NexusBankAccountEntity.findById(accountid)
- if (acct == null) {
- throw NexusError(
- HttpStatusCode.NotFound,
- "Account not found"
- )
- }
- val conn = acct.defaultBankConnection
- if (conn == null) {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "No default bank connection (explicit connection not yet
supported)"
- )
- }
- return@transaction object {
- val connectionType = conn.type
- val connectionName = conn.id.value
- }
+fun requireBankConnection(call: ApplicationCall, parameterKey: String):
NexusBankConnectionEntity {
+ val name = call.parameters[parameterKey]
+ if (name == null) {
+ throw NexusError(HttpStatusCode.InternalServerError, "no parameter for
bank connection")
}
- when (res.connectionType) {
- "ebics" -> {
- // FIXME(dold): Support fetching not only the latest transactions.
- // It's not clear what's the nicest way to support this.
- fetchEbicsBySpec(
- fetchSpec,
- client,
- res.connectionName
- )
- ingestBankMessagesIntoAccount(res.connectionName, accountid)
- }
- else -> throw NexusError(
- HttpStatusCode.BadRequest,
- "Connection type '${res.connectionType}' not implemented"
- )
+ val conn = transaction { NexusBankConnectionEntity.findById(name) }
+ if (conn == null) {
+ throw NexusError(HttpStatusCode.NotFound, "bank connection '$name' not
found")
}
+ return conn
}
-fun ensureNonNull(param: String?): String {
- return param ?: throw NexusError(
- HttpStatusCode.BadRequest, "Bad ID given: ${param}"
- )
-}
-
-fun ensureLong(param: String?): Long {
- val asString = ensureNonNull(param)
- return asString.toLongOrNull() ?: throw NexusError(
- HttpStatusCode.BadRequest, "Parameter is not a number: ${param}"
- )
-}
-
-/**
- * This helper function parses a Authorization:-header line, decode the
credentials
- * and returns a pair made of username and hashed (sha256) password. The
hashed value
- * will then be compared with the one kept into the database.
- */
-fun extractUserAndPassword(authorizationHeader: String): Pair<String, String> {
- logger.debug("Authenticating: $authorizationHeader")
- val (username, password) = try {
- val split = authorizationHeader.split(" ")
- val plainUserAndPass = String(base64ToBytes(split[1]), Charsets.UTF_8)
- plainUserAndPass.split(":")
- } catch (e: java.lang.Exception) {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "invalid Authorization:-header received"
- )
- }
- return Pair(username, password)
-}
fun serverMain(dbName: String) {
dbCreateTables(dbName)
@@ -434,6 +230,10 @@ fun serverMain(dbName: String) {
}
}
+ /**
+ * Allow request body compression. Needed by Taler.
+ */
+
/**
* Allow request body compression. Needed by Taler.
*/
@@ -453,6 +253,9 @@ fun serverMain(dbName: String) {
moreFrequentBackgroundTasks(client)
routing {
+ /**
+ * Shows information about the requesting user.
+ */
/**
* Shows information about the requesting user.
*/
@@ -481,6 +284,10 @@ fun serverMain(dbName: String) {
return@get
}
+ /**
+ * Add a new ordinary user in the system (requires superuser
privileges)
+ */
+
/**
* Add a new ordinary user in the system (requires superuser
privileges)
*/
@@ -492,7 +299,7 @@ fun serverMain(dbName: String) {
throw NexusError(HttpStatusCode.Forbidden, "only
superuser can do that")
}
NexusUserEntity.new(body.username) {
- passwordHash = hashpw(body.password)
+ passwordHash = CryptoUtil.hashpw(body.password)
superuser = false
}
}
@@ -505,7 +312,10 @@ fun serverMain(dbName: String) {
}
get("/bank-connection-protocols") {
- call.respond(HttpStatusCode.OK,
BankProtocolsResponse(listOf("ebics", "loopback")))
+ call.respond(
+ HttpStatusCode.OK,
+ BankProtocolsResponse(listOf("ebics", "loopback"))
+ )
return@get
}
@@ -513,6 +323,10 @@ fun serverMain(dbName: String) {
ebicsBankProtocolRoutes(client)
}
+ /**
+ * Shows the bank accounts belonging to the requesting user.
+ */
+
/**
* Shows the bank accounts belonging to the requesting user.
*/
@@ -522,7 +336,14 @@ fun serverMain(dbName: String) {
authenticateRequest(call.request)
// FIXME(dold): Only return accounts the user has at least
read access to?
NexusBankAccountEntity.all().forEach {
-
bankAccounts.accounts.add(BankAccount(it.accountHolder, it.iban, it.bankCode,
it.id.value))
+ bankAccounts.accounts.add(
+ BankAccount(
+ it.accountHolder,
+ it.iban,
+ it.bankCode,
+ it.id.value
+ )
+ )
}
}
call.respond(bankAccounts)
@@ -545,6 +366,9 @@ fun serverMain(dbName: String) {
}
call.respond(res)
}
+ /**
+ * Submit one particular payment to the bank.
+ */
/**
* Submit one particular payment to the bank.
*/
@@ -559,6 +383,10 @@ fun serverMain(dbName: String) {
return@post
}
+ /**
+ * Shows information about one particular payment initiation.
+ */
+
/**
* Shows information about one particular payment initiation.
*/
@@ -589,6 +417,10 @@ fun serverMain(dbName: String) {
return@get
}
+ /**
+ * Adds a new payment initiation.
+ */
+
/**
* Adds a new payment initiation.
*/
@@ -624,6 +456,10 @@ fun serverMain(dbName: String) {
return@post
}
+ /**
+ * Downloads new transactions from the bank.
+ */
+
/**
* Downloads new transactions from the bank.
*/
@@ -639,7 +475,10 @@ fun serverMain(dbName: String) {
val fetchSpec = if (call.request.hasBody()) {
call.receive<FetchSpecJson>()
} else {
- FetchSpecLatestJson(FetchLevel.ALL, null)
+ FetchSpecLatestJson(
+ FetchLevel.ALL,
+ null
+ )
}
fetchTransactionsInternal(
client,
@@ -651,6 +490,10 @@ fun serverMain(dbName: String) {
return@post
}
+ /**
+ * Asks list of transactions ALREADY downloaded from the bank.
+ */
+
/**
* Asks list of transactions ALREADY downloaded from the bank.
*/
@@ -670,6 +513,10 @@ fun serverMain(dbName: String) {
return@get
}
+ /**
+ * Adds a new bank transport.
+ */
+
/**
* Adds a new bank transport.
*/
@@ -719,7 +566,12 @@ fun serverMain(dbName: String) {
val connList = mutableListOf<BankConnectionInfo>()
transaction {
NexusBankConnectionEntity.all().forEach {
- connList.add(BankConnectionInfo(it.id.value, it.type))
+ connList.add(
+ BankConnectionInfo(
+ it.id.value,
+ it.type
+ )
+ )
}
}
call.respond(BankConnectionsList(connList))
@@ -800,7 +652,13 @@ fun serverMain(dbName: String) {
val list = BankMessageList()
val conn = requireBankConnection(call, "connid")
NexusBankMessageEntity.find {
NexusBankMessagesTable.bankConnection eq conn.id }.map {
- list.bankMessages.add(BankMessageInfo(it.messageId,
it.code, it.message.bytes.size.toLong()))
+ list.bankMessages.add(
+ BankMessageInfo(
+ it.messageId,
+ it.code,
+ it.message.bytes.size.toLong()
+ )
+ )
}
list
}
@@ -853,6 +711,9 @@ fun serverMain(dbName: String) {
route("/facades/{fcid}/taler") {
talerFacadeRoutes(this, client)
}
+ /**
+ * Hello endpoint.
+ */
/**
* Hello endpoint.
*/
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
index 3b79bac..97e48af 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
@@ -39,7 +39,14 @@ import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import tech.libeufin.nexus.bankaccount.addPaymentInitiation
-import tech.libeufin.util.*
+import tech.libeufin.nexus.server.Pain001Data
+import tech.libeufin.nexus.server.authenticateRequest
+import tech.libeufin.nexus.server.expectNonNull
+import tech.libeufin.nexus.server.expectUrlParameter
+import tech.libeufin.util.CryptoUtil
+import tech.libeufin.util.EbicsProtocolError
+import tech.libeufin.util.parseAmount
+import tech.libeufin.util.parsePayto
import kotlin.math.abs
import kotlin.math.min
@@ -188,15 +195,6 @@ fun extractReservePubFromSubject(rawSubject: String):
String? {
return result.value.toUpperCase()
}
-/**
- * Tries to extract a valid wire transfer id from the subject.
- */
-fun extractWtidFromSubject(rawSubject: String): String? {
- val re = "\\b[a-z0-9A-Z]{52}\\b".toRegex()
- val result = re.find(rawSubject) ?: return null
- return result.value.toUpperCase()
-}
-
private fun getTalerFacadeState(fcid: String): TalerFacadeStateEntity {
val facade = FacadeEntity.find { FacadesTable.id eq fcid }.firstOrNull()
?: throw NexusError(
HttpStatusCode.NotFound,
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated: refactor, towards common interface for bank protocols,
gnunet <=