gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]