gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: Use Ktor 2.2.1 and general polishing.


From: gnunet
Subject: [libeufin] branch master updated: Use Ktor 2.2.1 and general polishing.
Date: Fri, 30 Dec 2022 19:41:05 +0100

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new ce74df3f Use Ktor 2.2.1 and general polishing.
ce74df3f is described below

commit ce74df3f515cc5caec39381c00645714f3d7f5ba
Author: MS <ms@taler.net>
AuthorDate: Fri Dec 30 19:37:36 2022 +0100

    Use Ktor 2.2.1 and general polishing.
---
 access-api-stash/AccessApiNexus.kt                 | 210 ------------------
 build.gradle                                       |   7 +-
 nexus/build.gradle                                 |  20 +-
 .../main/kotlin/tech/libeufin/nexus/Anastasis.kt   |   8 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt  |   3 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt  |   2 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt |  53 +++--
 .../tech/libeufin/nexus/bankaccount/BankAccount.kt |   9 +-
 .../tech/libeufin/nexus/ebics/EbicsClient.kt       |  33 +--
 .../kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt |  23 +-
 .../tech/libeufin/nexus/server/NexusServer.kt      |  79 ++++---
 .../nexus/server/RequestBodyDecompression.kt       |  47 ++---
 nexus/src/test/kotlin/DownloadAndSubmit.kt         |  33 +--
 nexus/src/test/kotlin/JsonTest.kt                  |  17 +-
 nexus/src/test/kotlin/MakeEnv.kt                   |   9 +-
 nexus/src/test/kotlin/SandboxAccessApiTest.kt      | 132 +++---------
 nexus/src/test/kotlin/SandboxBankAccountTest.kt    |   5 -
 nexus/src/test/kotlin/SandboxCircuitApiTest.kt     |  32 ++-
 nexus/src/test/kotlin/SandboxLegacyApiTest.kt      | 220 +++++--------------
 sandbox/build.gradle                               |  45 ++--
 .../kotlin/tech/libeufin/sandbox/CircuitApi.kt     |  85 +++++++-
 .../src/main/kotlin/tech/libeufin/sandbox/DB.kt    |   8 +-
 .../tech/libeufin/sandbox/EbicsProtocolBackend.kt  |   8 +-
 .../main/kotlin/tech/libeufin/sandbox/Helpers.kt   |  92 +++++++-
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  | 235 ++++++++++-----------
 .../tech/libeufin/sandbox/XMLEbicsConverter.kt     |  46 ++--
 util/build.gradle                                  |   6 +-
 util/src/main/kotlin/Config.kt                     |   8 +-
 util/src/main/kotlin/HTTP.kt                       |  18 +-
 util/src/main/kotlin/UnixDomainSocket.kt           |  28 ++-
 util/src/test/kotlin/DomainSocketTest.kt           |  12 +-
 31 files changed, 660 insertions(+), 873 deletions(-)

diff --git a/access-api-stash/AccessApiNexus.kt 
b/access-api-stash/AccessApiNexus.kt
deleted file mode 100644
index 18347083..00000000
--- a/access-api-stash/AccessApiNexus.kt
+++ /dev/null
@@ -1,210 +0,0 @@
-package tech.libeufin.nexus.`access-api`
-
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.client.*
-import io.ktor.client.features.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.ktor.util.*
-import org.jetbrains.exposed.sql.not
-import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.server.AccessApiNewTransport
-import tech.libeufin.nexus.server.EbicsNewTransport
-import tech.libeufin.nexus.server.FetchSpecJson
-import tech.libeufin.nexus.server.client
-import tech.libeufin.util.*
-import java.net.URL
-import java.nio.charset.Charset
-
-private fun getAccessApiClient(connId: String): AccessApiClientEntity {
-    val conn = NexusBankConnectionEntity.find {
-        NexusBankConnectionsTable.connectionId eq connId
-    }.firstOrNull() ?: throw notFound("Connection '$connId' not found.")
-    val client = AccessApiClientEntity.find {
-        AccessApiClientsTable.nexusBankConnection eq conn.id.value
-    }.firstOrNull() ?: throw notFound("Connection '$connId' has no client 
data.")
-    return client
-}
-
-suspend fun HttpClient.accessApiReq(
-    method: HttpMethod,
-    url: String,
-    body: Any? = null,
-    // username, password
-    credentials: Pair<String, String>): String? {
-
-    val reqBuilder: HttpRequestBuilder.() -> Unit = {
-        contentType(ContentType.Application.Json)
-        if (body != null)
-            this.body = body
-
-        headers.apply {
-            this.set(
-                "Authorization",
-                "Basic " + 
bytesToBase64("${credentials.first}:${credentials.second}".toByteArray(Charsets.UTF_8))
-            )
-        }
-    }
-    return try {
-        when(method) {
-            HttpMethod.Get -> {
-                this.get(url, reqBuilder)
-            }
-            HttpMethod.Post -> {
-                this.post(url, reqBuilder)
-            }
-            else -> throw internalServerError("Method $method not supported.")
-        }
-    } catch (e: ClientRequestException) {
-        logger.error(e.message)
-        throw NexusError(
-            HttpStatusCode.BadGateway,
-            e.message
-        )
-    }
-}
-
-/**
- * Talk to the Sandbox via native Access API.  The main reason
- * for this class was to still allow x-taler-bank as a wire method
- * (to accommodate wallet harness tests), and therefore skip the
- * Camt+Pain schemas.
- */
-class JsonBankConnectionProtocol: BankConnectionProtocol {
-
-    override fun createConnection(connId: String, user: NexusUserEntity, data: 
JsonNode) {
-        val bankConn = NexusBankConnectionEntity.new {
-            this.connectionId = connId
-            owner = user
-            type = "access-api"
-        }
-        val newTransportData = jacksonObjectMapper(
-        ).treeToValue(data, AccessApiNewTransport::class.java) ?: throw 
NexusError(
-            HttpStatusCode.BadRequest, "Access Api details not found in 
request"
-        )
-        AccessApiClientEntity.new {
-            username = newTransportData.username
-            bankURL = newTransportData.bankURL
-            remoteBankAccountLabel = newTransportData.remoteBankAccountLabel
-            nexusBankConnection = bankConn
-            password = newTransportData.password
-        }
-    }
-
-    override fun getConnectionDetails(conn: NexusBankConnectionEntity): 
JsonNode {
-        val details = transaction { getAccessApiClient(conn.connectionId) }
-        val ret = ObjectMapper().createObjectNode()
-        ret.put("username", details.username)
-        ret.put("bankURL", details.bankURL)
-        ret.put("passwordHash", CryptoUtil.hashpw(details.password))
-        ret.put("remoteBankAccountLabel", details.remoteBankAccountLabel)
-        return ret
-    }
-
-    override suspend fun submitPaymentInitiation(
-        httpClient: HttpClient,
-        paymentInitiationId: Long // must refer to an x-taler-bank 
payto://-instruction.
-    ) {
-        val payInit = 
XTalerBankPaymentInitiationEntity.findById(paymentInitiationId) ?: throw 
notFound(
-            "Payment initiation '$paymentInitiationId' not found."
-        )
-        val conn = payInit.defaultBankConnection ?: throw notFound(
-            "No default bank connection for payment initiation 
'${paymentInitiationId}' was found."
-        )
-        val details = getAccessApiClient(conn.connectionId)
-
-        client.accessApiReq(
-            method = HttpMethod.Post,
-            url = urlJoinNoDrop(
-                details.bankURL,
-                "accounts/${details.remoteBankAccountLabel}/transactions"
-            ),
-            body = object {
-                val paytoUri = payInit.paytoUri
-                val amount = payInit.amount
-                val subject = payInit.subject
-            },
-            credentials = Pair(details.username, details.password)
-        )
-    }
-    /**
-     * This function gets always the fresh transactions from
-     * the bank.  Any other Wire Gateway API policies will be
-     * implemented by the respective facade (XTalerBank.kt) */
-    override suspend fun fetchTransactions(
-        fetchSpec: FetchSpecJson,
-        client: HttpClient,
-        bankConnectionId: String,
-        /**
-         * Label of the local bank account that mirrors
-         * the remote bank account pointed to by 'bankConnectionId' */
-        accountId: String
-    ) {
-        val details = getAccessApiClient(bankConnectionId)
-        val txsRaw = client.accessApiReq(
-            method = HttpMethod.Get,
-            url = urlJoinNoDrop(
-                details.bankURL,
-                "accounts/${details.remoteBankAccountLabel}/transactions"
-            ),
-            credentials = Pair(details.username, details.password)
-        )
-        // What format does Access API communicates the records in?
-        /**
-         * NexusXTalerBankTransactions.new {
-         *
-         *     .. details ..
-         * }
-         */
-    }
-
-    override fun exportBackup(bankConnectionId: String, passphrase: String): 
JsonNode {
-        throw NexusError(
-            HttpStatusCode.NotImplemented,
-            "Operation not needed."
-        )
-    }
-
-    override fun exportAnalogDetails(conn: NexusBankConnectionEntity): 
ByteArray {
-        throw NexusError(
-            HttpStatusCode.NotImplemented,
-            "Operation not needed."
-        )
-    }
-
-    override suspend fun fetchAccounts(client: HttpClient, connId: String) {
-        throw NexusError(
-            HttpStatusCode.NotImplemented,
-            "access-api connections assume that remote and local bank" +
-                    " accounts are called the same.  No need to 'fetch'"
-        )
-    }
-
-    override fun createConnectionFromBackup(
-        connId: String,
-        user: NexusUserEntity,
-        passphrase: String?,
-        backup: JsonNode
-    ) {
-        throw NexusError(
-            HttpStatusCode.NotImplemented,
-            "Operation not needed."
-        )
-    }
-
-    override suspend fun connect(client: HttpClient, connId: String) {
-        /**
-         * Future versions might create a bank account at this step.
-         * Right now, all the tests do create those accounts beforehand.
-         */
-        throw NexusError(
-            HttpStatusCode.NotImplemented,
-            "Operation not needed."
-        )
-    }
-}
-
diff --git a/build.gradle b/build.gradle
index 6d93ad61..8c81586d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,8 @@
 import org.apache.tools.ant.filters.ReplaceTokens
 
 plugins {
-    id 'org.jetbrains.kotlin.jvm' version '1.5.30'
+    // id 'org.jetbrains.kotlin.jvm' version '1.5.30'
+    id 'org.jetbrains.kotlin.jvm' version '1.7.22'
     id 'idea'
 }
 
@@ -12,6 +13,10 @@ if (!JavaVersion.current().isJava11Compatible()){
 }
 
 allprojects {
+    ext.set("ktor_version", "2.2.1")
+    ext.set("ktor_auth_version", "1.6.8")
+    ext.set("exposed_version", "0.32.1")
+
     repositories {
         mavenCentral()
         jcenter()
diff --git a/nexus/build.gradle b/nexus/build.gradle
index c5009588..f1ccfb22 100644
--- a/nexus/build.gradle
+++ b/nexus/build.gradle
@@ -3,14 +3,6 @@ plugins {
     id 'java'
     id 'application'
     id 'org.jetbrains.kotlin.jvm'
-    /**
-     * Design choice: native installation logic doesn't provide one
-     * single command to generate a unique jar, and even by generating
-     * a unique jar manually, then the usual gradle wrappers wouldn't be
-     * able to run those.  Therefore, the dependency below ('shadow')
-     * was added as it provides _both_: unique jar packaging _and_ a
-     * suitable launch script.
-     */
     id "com.github.johnrengelman.shadow" version "5.2.0"
 }
 
@@ -51,12 +43,8 @@ compileTestKotlin {
     }
 }
 
-def ktor_version = '1.6.1'
-def exposed_version = '0.32.1'
-
 dependencies {
     // Core language libraries
-    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21'
     implementation 
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt'
 
     // LibEuFin util library
@@ -78,7 +66,6 @@ dependencies {
     implementation('com.github.ajalt:clikt:2.8.0')
 
     // Exposed, an SQL library
-    implementation "org.jetbrains.exposed:exposed-core:$exposed_version"
     implementation "org.jetbrains.exposed:exposed-dao:$exposed_version"
     implementation "org.jetbrains.exposed:exposed-jdbc:$exposed_version"
 
@@ -88,12 +75,15 @@ dependencies {
 
     // Ktor, an HTTP client and server library
     implementation "io.ktor:ktor-server-core:$ktor_version"
+    implementation "io.ktor:ktor-server-content-negotiation:$ktor_version"
+    implementation "io.ktor:ktor-server-status-pages:$ktor_version"
     implementation "io.ktor:ktor-client-apache:$ktor_version"
     implementation "io.ktor:ktor-client-auth:$ktor_version"
     implementation "io.ktor:ktor-server-netty:$ktor_version"
+    // Brings the call-logging library too.
     implementation "io.ktor:ktor-server-test-host:$ktor_version"
-    implementation "io.ktor:ktor-auth:$ktor_version"
-    implementation "io.ktor:ktor-jackson:$ktor_version"
+    implementation "io.ktor:ktor-auth:$ktor_auth_version"
+    implementation "io.ktor:ktor-serialization-jackson:$ktor_version"
 
     // PDF generation
     implementation 'com.itextpdf:itext7-core:7.1.16'
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt
index a7ec84ef..4a6f75c1 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt
@@ -1,9 +1,7 @@
 package tech.libeufin.nexus
 
-import io.ktor.application.*
 import io.ktor.client.*
 import io.ktor.http.*
-import io.ktor.response.*
 import org.jetbrains.exposed.sql.transactions.transaction
 import tech.libeufin.nexus.iso20022.TransactionDetails
 import tech.libeufin.nexus.server.PermissionQuery
@@ -13,7 +11,9 @@ import tech.libeufin.util.EbicsProtocolError
 import kotlin.math.abs
 import kotlin.math.min
 import io.ktor.content.TextContent
-import io.ktor.routing.*
+import io.ktor.server.application.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
 import tech.libeufin.util.buildIbanPaytoUri
 
 data class AnastasisIncomingBankTransaction(
@@ -107,7 +107,7 @@ private suspend fun historyIncoming(call: ApplicationCall) {
     return call.respond(TextContent(customConverter(history), 
ContentType.Application.Json))
 }
 
-fun anastasisFacadeRoutes(route: Route, httpClient: HttpClient) {
+fun anastasisFacadeRoutes(route: Route) {
     route.get("/history/incoming") {
         historyIncoming(call)
         return@get
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
index 447754d9..3decc126 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
@@ -1,9 +1,8 @@
 package tech.libeufin.nexus
 
 import UtilError
-import io.ktor.application.*
 import io.ktor.http.*
-import io.ktor.request.*
+import io.ktor.server.request.*
 import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.transactions.transaction
 import tech.libeufin.nexus.server.Permission
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index a5d46530..e27d7df1 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -40,7 +40,7 @@ import java.io.File
 import kotlin.system.exitProcess
 
 val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
-val NEXUS_DB_ENV_VAR_NAME = "LIBEUFIN_NEXUS_DB_CONNECTION"
+const val NEXUS_DB_ENV_VAR_NAME = "LIBEUFIN_NEXUS_DB_CONNECTION"
 
 class NexusCommand : CliktCommand() {
     init {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
index 97434bb8..264d092e 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
@@ -20,20 +20,20 @@
 package tech.libeufin.nexus
 
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.application.ApplicationCall
-import io.ktor.application.call
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.application.call
 import io.ktor.client.*
-import io.ktor.client.features.*
+import io.ktor.client.plugins.*
 import io.ktor.client.request.*
 import io.ktor.content.TextContent
 import io.ktor.http.*
-import io.ktor.request.receive
-import io.ktor.response.respond
-import io.ktor.response.respondText
-import io.ktor.routing.Route
-import io.ktor.routing.get
-import io.ktor.routing.post
-import io.ktor.util.*
+import io.ktor.server.request.receive
+import io.ktor.server.response.respond
+import io.ktor.server.response.respondText
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.get
+import io.ktor.server.routing.post
+import io.ktor.server.util.*
 import org.jetbrains.exposed.dao.Entity
 import org.jetbrains.exposed.dao.id.IdTable
 import org.jetbrains.exposed.sql.*
@@ -486,10 +486,10 @@ private suspend fun addIncoming(call: ApplicationCall) {
     val currentBody = call.receive<String>()
     val fromDb = transaction {
         val f = FacadeEntity.findByName(facadeId) ?: throw notFound("facade 
$facadeId not found")
-        val state = FacadeStateEntity.find {
+        val facadeState = FacadeStateEntity.find {
             FacadeStateTable.facade eq f.id
         }.firstOrNull() ?: throw internalServerError("facade $facadeId has no 
state!")
-        val conn = NexusBankConnectionEntity.findByName(state.bankConnection) 
?: throw internalServerError(
+        val conn = 
NexusBankConnectionEntity.findByName(facadeState.bankConnection) ?: throw 
internalServerError(
             "state of facade $facadeId has no bank connection!"
         )
         val ebicsData = NexusEbicsSubscribersTable.select {
@@ -500,10 +500,12 @@ private suspend fun addIncoming(call: ApplicationCall) {
         // Resort Sandbox URL from EBICS endpoint.
         val sandboxUrl = URL(ebicsData[NexusEbicsSubscribersTable.ebicsURL])
         // NOTE: the exchange username must be 'exchange', at the Sandbox.
-        return@transaction Pair(url {
-            protocol = URLProtocol(sandboxUrl.protocol, 80)
-            host = sandboxUrl.host
-            if (sandboxUrl.port != 80) port = sandboxUrl.port
+        return@transaction Pair(
+            url {
+                protocol = URLProtocol(sandboxUrl.protocol, 80)
+                host = sandboxUrl.host
+            if (sandboxUrl.port != 80)
+                port = sandboxUrl.port
             path(
                 "demobanks",
                 "default",
@@ -512,22 +514,17 @@ private suspend fun addIncoming(call: ApplicationCall) {
                 "admin",
                 "add-incoming"
             )
-        }, state.bankAccount
+        },
+            facadeState.bankAccount
         )
     }
     val client = HttpClient { followRedirects = true }
     try {
-        client.post<String>(
-            urlString = fromDb.first,
-            block = {
-                this.body = currentBody
-                this.header(
-                    "Authorization",
-                    buildBasicAuthLine("exchange", "x")
-                )
-                this.header("Content-Type", "application/json")
-            }
-        )
+        client.post(fromDb.first) {
+            setBody(currentBody)
+            basicAuth("exchange", "x")
+            contentType(ContentType.Application.Json)
+        }
     } catch (e: ClientRequestException) {
         logger.error("Proxying /admin/add/incoming to the Sandbox failed: $e")
     } catch (e: Exception) {
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 1b62719d..fa8064d5 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -20,7 +20,7 @@
 package tech.libeufin.nexus.bankaccount
 
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.application.ApplicationCall
+import io.ktor.server.application.ApplicationCall
 import io.ktor.client.HttpClient
 import io.ktor.http.HttpStatusCode
 import org.jetbrains.exposed.sql.*
@@ -183,13 +183,14 @@ fun processCamtMessage(
                 "Nexus hit a report or statement of a wrong IBAN!"
             )
             it.balances.forEach { b ->
-                var clbdCount = 0
                 if (b.type == "CLBD") {
-                    clbdCount++
                     val lastBalance = NexusBankBalanceEntity.all().lastOrNull()
                     /**
                      * Store balances different from the one that came from 
the bank,
-                     * or the very first balance.
+                     * or the very first balance.  This approach has the 
following inconvenience:
+                     * the 'balance' held at Nexus does not differentiate 
between one
+                     * coming from a statement and one coming from a report.  
As a consequence,
+                     * the two types of balances may override each other 
without notice.
                      */
                     if ((lastBalance == null) ||
                         (b.amount.toPlainString() != lastBalance.balance)) {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
index bc2233b1..6d2ce4de 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
@@ -23,8 +23,9 @@
 package tech.libeufin.nexus.ebics
 
 import io.ktor.client.HttpClient
-import io.ktor.client.features.*
+import io.ktor.client.plugins.*
 import io.ktor.client.request.*
+import io.ktor.client.statement.*
 import io.ktor.http.*
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
@@ -39,13 +40,10 @@ private suspend inline fun HttpClient.postToBank(url: 
String, body: String): Str
         HttpStatusCode.InternalServerError,
         "EBICS (outgoing) document is invalid"
     )
-    val response: String = try {
-        this.post(
-            urlString = url,
-            block = {
-                this.body = body
-            }
-        )
+    val response: HttpResponse = try {
+        this.post(urlString = url) {
+                setBody(body)
+        }
     } catch (e: ClientRequestException) {
         logger.error(e.message)
         throw NexusError(
@@ -60,8 +58,17 @@ private suspend inline fun HttpClient.postToBank(url: 
String, body: String): Str
             e.message ?: "Could not reach the bank"
         )
     }
-    // logger.debug("Receiving: $response")
-    return response
+    /**
+     * EBICS should be expected only after a 200 OK response
+     * (including problematic ones); throw exception in all the other cases,
+     * by echoing what the bank said.
+     */
+    if (response.status.value != HttpStatusCode.OK.value)
+        throw NexusError(
+            HttpStatusCode.BadGateway,
+            "bank says: ${response.bodyAsText()}"
+        )
+    return response.bodyAsText()
 }
 
 sealed class EbicsDownloadResult
@@ -227,7 +234,7 @@ suspend fun doEbicsUploadTransaction(
     logger.debug("Bank acknowledges EBICS upload initialization.  Transaction 
ID: $transactionID.")
     /* now send actual payload */
 
-    val payload = createEbicsRequestForUploadTransferPhase(
+    val ebicsPayload = createEbicsRequestForUploadTransferPhase(
         subscriberDetails,
         transactionID,
         preparedUploadData,
@@ -235,7 +242,7 @@ suspend fun doEbicsUploadTransaction(
     )
     val txRespStr = client.postToBank(
         subscriberDetails.ebicsUrl,
-        payload
+        ebicsPayload
     )
     val txResp = parseAndValidateEbicsResponse(subscriberDetails, txRespStr)
     when (txResp.technicalReturnCode) {
@@ -303,4 +310,4 @@ suspend fun doEbicsHpbRequest(
         "Cannot find data in a HPB response"
     )
     return parseEbicsHpbOrder(orderData)
-}
+}
\ No newline at end of file
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 a7935f92..67fc483d 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -31,15 +31,15 @@ import com.itextpdf.kernel.pdf.PdfWriter
 import com.itextpdf.layout.Document
 import com.itextpdf.layout.element.AreaBreak
 import com.itextpdf.layout.element.Paragraph
-import io.ktor.application.call
+import io.ktor.server.application.call
 import io.ktor.client.HttpClient
 import io.ktor.http.ContentType
 import io.ktor.http.HttpStatusCode
-import io.ktor.request.receiveOrNull
-import io.ktor.response.respond
-import io.ktor.response.respondText
-import io.ktor.routing.Route
-import io.ktor.routing.post
+import io.ktor.server.request.*
+import io.ktor.server.response.respond
+import io.ktor.server.response.respondText
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.post
 import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.insert
 import org.jetbrains.exposed.sql.select
@@ -196,14 +196,13 @@ fun getEbicsSubscriberDetails(bankConnectionId: String): 
EbicsClientSubscriberDe
 
 fun Route.ebicsBankProtocolRoutes(client: HttpClient) {
     post("test-host") {
-        val r = call.receiveJson<EbicsHostTestRequest>()
+        val r = call.receive<EbicsHostTestRequest>()
         val qr = doEbicsHostVersionQuery(client, r.ebicsBaseUrl, r.ebicsHostId)
         call.respond(qr)
         return@post
     }
 }
 
-
 fun Route.ebicsBankConnectionRoutes(client: HttpClient) {
     post("/send-ini") {
         val subscriber = transaction {
@@ -320,12 +319,8 @@ fun Route.ebicsBankConnectionRoutes(client: HttpClient) {
         if (orderType.length != 3) {
             throw NexusError(HttpStatusCode.BadRequest, "ebics order type must 
be three characters")
         }
-        val paramsJson = call.receiveOrNull<EbicsStandardOrderParamsDateJson>()
-        val orderParams = if (paramsJson == null) {
-            EbicsStandardOrderParams()
-        } else {
-            paramsJson.toOrderParams()
-        }
+        val paramsJson = 
call.receiveNullable<EbicsStandardOrderParamsDateJson>()
+        val orderParams = paramsJson?.toOrderParams() ?: 
EbicsStandardOrderParams()
         val subscriberDetails = transaction {
             val conn = requireBankConnection(call, "connid")
             if (conn.type != "ebics") {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index 8c4e6dbe..b39c72ec 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -20,6 +20,8 @@
 package tech.libeufin.nexus.server
 
 import UtilError
+import io.ktor.serialization.jackson.*
+import io.ktor.server.plugins.contentnegotiation.*
 import com.fasterxml.jackson.core.util.DefaultIndenter
 import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
 import com.fasterxml.jackson.databind.JsonNode
@@ -28,20 +30,19 @@ import com.fasterxml.jackson.databind.DeserializationFeature
 import com.fasterxml.jackson.databind.JsonMappingException
 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 io.ktor.application.*
+import com.fasterxml.jackson.module.kotlin.*
 import io.ktor.client.*
-import io.ktor.features.*
 import io.ktor.http.*
-import io.ktor.jackson.*
 import io.ktor.network.sockets.*
-import io.ktor.request.*
-import io.ktor.response.*
-import io.ktor.routing.*
+import io.ktor.server.application.*
 import io.ktor.server.engine.*
 import io.ktor.server.netty.*
+import io.ktor.server.plugins.*
+import io.ktor.server.plugins.callloging.*
+import io.ktor.server.plugins.statuspages.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
 import io.ktor.util.*
 import org.jetbrains.exposed.exceptions.ExposedSQLException
 import org.jetbrains.exposed.sql.and
@@ -51,11 +52,12 @@ import tech.libeufin.nexus.*
 import tech.libeufin.nexus.bankaccount.*
 import tech.libeufin.nexus.ebics.*
 import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
+import tech.libeufin.sandbox.SandboxErrorDetailJson
+import tech.libeufin.sandbox.SandboxErrorJson
 import tech.libeufin.util.*
 import java.net.BindException
 import java.net.URLEncoder
 import kotlin.system.exitProcess
-import java.net.URL
 
 /**
  * Return facade state depending on the type.
@@ -121,18 +123,6 @@ fun ApplicationCall.expectUrlParameter(name: String): 
String {
         ?: throw NexusError(HttpStatusCode.BadRequest, "Parameter '$name' not 
provided in URI")
 }
 
-suspend inline fun <reified T : Any> ApplicationCall.receiveJson(): T {
-    try {
-        return this.receive()
-    } catch (e: MissingKotlinParameterException) {
-        throw NexusError(HttpStatusCode.BadRequest, "Missing value for 
${e.pathReference}")
-    } catch (e: MismatchedInputException) {
-        throw NexusError(HttpStatusCode.BadRequest, "Invalid value for 
'${e.pathReference}'")
-    } catch (e: JsonParseException) {
-        throw NexusError(HttpStatusCode.BadRequest, "Invalid JSON")
-    }
-}
-
 fun requireBankConnectionInternal(connId: String): NexusBankConnectionEntity {
     return transaction {
         NexusBankConnectionEntity.find { 
NexusBankConnectionsTable.connectionId eq connId }.firstOrNull()
@@ -157,6 +147,7 @@ val nexusApp: Application.() -> Unit = {
         this.level = Level.DEBUG
         this.logger = tech.libeufin.nexus.logger
     }
+    install(LibeufinDecompressionPlugin)
     install(ContentNegotiation) {
         jackson {
             enable(SerializationFeature.INDENT_OUTPUT)
@@ -164,12 +155,21 @@ val nexusApp: Application.() -> Unit = {
                 
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
                 indentObjectsWith(DefaultIndenter("  ", "\n"))
             })
-            registerModule(KotlinModule(nullisSameAsDefault = true))
+            registerModule(
+                KotlinModule.Builder()
+                    .withReflectionCacheSize(512)
+                    .configure(KotlinFeature.NullToEmptyCollection, false)
+                    .configure(KotlinFeature.NullToEmptyMap, false)
+                    .configure(KotlinFeature.NullIsSameAsDefault, enabled = 
true)
+                    .configure(KotlinFeature.SingletonSupport, enabled = false)
+                    .configure(KotlinFeature.StrictNullChecks, false)
+                    .build()
+            )
             configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
         }
     }
     install(StatusPages) {
-        exception<NexusError> { cause ->
+        exception<NexusError> { call, cause ->
             logger.error("Caught exception while handling '${call.request.uri} 
(${cause.reason})")
             call.respond(
                 status = cause.statusCode,
@@ -180,7 +180,7 @@ val nexusApp: Application.() -> Unit = {
                 )
             )
         }
-        exception<JsonMappingException> { cause ->
+        exception<JsonMappingException> { call, cause ->
             logger.error("Exception while handling '${call.request.uri}'", 
cause)
             call.respond(
                 HttpStatusCode.BadRequest,
@@ -191,7 +191,7 @@ val nexusApp: Application.() -> Unit = {
                 )
             )
         }
-        exception<UtilError> { cause ->
+        exception<UtilError> { call, cause ->
             logger.error("Exception while handling '${call.request.uri}'", 
cause)
             call.respond(
                 cause.statusCode,
@@ -202,7 +202,7 @@ val nexusApp: Application.() -> Unit = {
                 )
             )
         }
-        exception<EbicsProtocolError> { cause ->
+        exception<EbicsProtocolError> { call, cause ->
             logger.error("Caught exception while handling 
'${call.request.uri}' (${cause.reason})")
             call.respond(
                 cause.httpStatusCode,
@@ -213,7 +213,19 @@ val nexusApp: Application.() -> Unit = {
                 )
             )
         }
-        exception<Exception> { cause ->
+        exception<BadRequestException> { call, cause ->
+            tech.libeufin.sandbox.logger.error("Exception while handling 
'${call.request.uri}', ${cause.message}")
+            call.respond(
+                HttpStatusCode.BadRequest,
+                SandboxErrorJson(
+                    error = SandboxErrorDetailJson(
+                        type = "util-error",
+                        description = cause.message ?: "Bad request but did 
not find exact cause."
+                    )
+                )
+            )
+        }
+        exception<Exception> { call, cause ->
             logger.error("Uncaught exception while handling 
'${call.request.uri}'")
             cause.printStackTrace()
             call.respond(
@@ -226,7 +238,6 @@ val nexusApp: Application.() -> Unit = {
             )
         }
     }
-    install(RequestBodyDecompression)
     intercept(ApplicationCallPipeline.Fallback) {
         if (this.call.response.status() == null) {
             call.respondText("Not found (no route matched).\n", 
ContentType.Text.Plain, HttpStatusCode.NotFound)
@@ -332,7 +343,7 @@ val nexusApp: Application.() -> Unit = {
 
         // change a user's password
         post("/users/{username}/password") {
-            val body = call.receiveJson<ChangeUserPassword>()
+            val body = call.receive<ChangeUserPassword>()
             val targetUsername = ensureNonNull(call.parameters["username"])
             transaction {
                 requireSuperuser(call.request)
@@ -351,7 +362,7 @@ val nexusApp: Application.() -> Unit = {
 
         // Add a new ordinary user in the system (requires superuser 
privileges)
         post("/users") {
-            val body = call.receiveJson<CreateUserRequest>()
+            val body = call.receive<CreateUserRequest>()
             val requestedUsername = requireValidResourceName(body.username)
             transaction {
                 requireSuperuser(call.request)
@@ -896,7 +907,7 @@ val nexusApp: Application.() -> Unit = {
                     name = f.facadeName,
                     type = f.type,
                     baseUrl = URLBuilder(call.request.getBaseUrl()).apply {
-                        pathComponents("facades", f.facadeName, f.type)
+                        this.appendPathSegments(listOf("facades", 
f.facadeName, f.type))
                         encodedPath += "/"
                     }.buildString(),
                     config = getFacadeState(f.type, f)
@@ -921,7 +932,7 @@ val nexusApp: Application.() -> Unit = {
                             name = it.facadeName,
                             type = it.type,
                             baseUrl = 
URLBuilder(call.request.getBaseUrl()).apply {
-                                pathComponents("facades", it.facadeName, 
it.type)
+                                this.appendPathSegments(listOf("facades", 
it.facadeName, it.type))
                                 encodedPath += "/"
                             }.buildString(),
                             config = getFacadeState(it.type, it)
@@ -1041,7 +1052,7 @@ val nexusApp: Application.() -> Unit = {
             talerFacadeRoutes(this)
         }
         route("/facades/{fcid}/anastasis") {
-            anastasisFacadeRoutes(this, client)
+            anastasisFacadeRoutes(this)
         }
 
         // Hello endpoint.
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
index 76a71414..0eb5f57d 100644
--- 
a/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
+++ 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
@@ -19,9 +19,9 @@
 
 package tech.libeufin.nexus.server
 
-import io.ktor.application.*
-import io.ktor.features.*
-import io.ktor.request.*
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.request.*
 import io.ktor.util.*
 import io.ktor.util.pipeline.*
 import io.ktor.utils.io.*
@@ -30,37 +30,18 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import java.util.zip.InflaterInputStream
 
-/**
- * Decompress request bodies.
- */
-class RequestBodyDecompression private constructor() {
-    companion object Feature :
-        ApplicationFeature<Application, 
RequestBodyDecompression.Configuration, RequestBodyDecompression> {
-        override val key: AttributeKey<RequestBodyDecompression> = 
AttributeKey("Request Body Decompression")
-        override fun install(
-            pipeline: Application,
-            configure: RequestBodyDecompression.Configuration.() -> Unit
-        ): RequestBodyDecompression {
-            
pipeline.receivePipeline.intercept(ApplicationReceivePipeline.Before) {
-                if (this.context.request.headers["Content-Encoding"] == 
"deflate") {
-                    val deflated = this.subject.value as ByteReadChannel
-                    val brc = withContext(Dispatchers.IO) {
-                        val inflated = 
InflaterInputStream(deflated.toInputStream())
-                        // False positive in current Kotlin version, we're 
already in Dispatchers.IO!
-                        @Suppress("BlockingMethodInNonBlockingContext") val 
bytes = inflated.readAllBytes()
-                        ByteReadChannel(bytes)
-                    }
-                    
proceedWith(ApplicationReceiveRequest(this.subject.typeInfo, brc))
-                    return@intercept
+val LibeufinDecompressionPlugin = 
createApplicationPlugin("RequestingBodyDecompression") {
+    onCallReceive { call ->
+        transformBody { data ->
+            if (call.request.headers[HttpHeaders.ContentEncoding] == 
"deflate") {
+                val brc = withContext(Dispatchers.IO) {
+                    val inflated = InflaterInputStream(data.toInputStream())
+                    @Suppress("BlockingMethodInNonBlockingContext")
+                    val bytes = inflated.readAllBytes()
+                    ByteReadChannel(bytes)
                 }
-                proceed()
-                return@intercept
-            }
-            return RequestBodyDecompression()
+                brc
+            } else data
         }
     }
-
-    class Configuration {
-
-    }
 }
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/DownloadAndSubmit.kt 
b/nexus/src/test/kotlin/DownloadAndSubmit.kt
index fc8f7b4d..0da30b28 100644
--- a/nexus/src/test/kotlin/DownloadAndSubmit.kt
+++ b/nexus/src/test/kotlin/DownloadAndSubmit.kt
@@ -1,12 +1,12 @@
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.ktor.application.*
+import io.ktor.server.application.*
 import io.ktor.client.*
 import io.ktor.client.request.*
-import io.ktor.features.*
 import io.ktor.http.*
-import io.ktor.request.*
-import io.ktor.response.*
-import io.ktor.routing.*
+import io.ktor.server.plugins.contentnegotiation.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
 import io.ktor.server.testing.*
 import kotlinx.coroutines.runBlocking
 import org.jetbrains.exposed.sql.transactions.transaction
@@ -112,7 +112,8 @@ class DownloadAndSubmit {
                 "Exist in logging!",
                 "TESTKUDOS:5"
             )
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 runBlocking {
                     fetchBankAccountTransactions(
                         client,
@@ -139,7 +140,8 @@ class DownloadAndSubmit {
     @Test
     fun upload() {
         withNexusAndSandboxUser {
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 val conn = EbicsBankConnectionProtocol()
                 runBlocking {
                     // Create Pain.001 to be submitted.
@@ -181,7 +183,8 @@ class DownloadAndSubmit {
     @Test
     fun unallowedDebtorIban() {
         withNexusAndSandboxUser {
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 runBlocking {
                     val bar = transaction { 
NexusBankAccountEntity.findByName("bar") }
                     val painMessage = createPain001document(
@@ -233,7 +236,8 @@ class DownloadAndSubmit {
     @Test
     fun invalidPain001() {
         withNexusAndSandboxUser {
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 runBlocking {
                     // Create Pain.001 to be submitted.
                     addPaymentInitiation(
@@ -246,9 +250,7 @@ class DownloadAndSubmit {
                             currency = "TESTKUDOS"
                         ),
                         transaction {
-                            NexusBankAccountEntity.findByName(
-                                "foo"
-                            ) ?: throw Exception("Test failed")
+                            NexusBankAccountEntity.findByName("foo") ?: throw 
Exception("Test failed")
                         }
                     )
                     // Encounters errors.
@@ -270,7 +272,8 @@ class DownloadAndSubmit {
     @Test
     fun unsupportedCurrency() {
         withNexusAndSandboxUser {
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 runBlocking {
                     // Create Pain.001 to be submitted.
                     addPaymentInitiation(
@@ -283,9 +286,7 @@ class DownloadAndSubmit {
                             currency = "EUR"
                         ),
                         transaction {
-                            NexusBankAccountEntity.findByName(
-                                "foo"
-                            ) ?: throw Exception("Test failed")
+                            NexusBankAccountEntity.findByName("foo") ?: throw 
Exception("Test failed")
                         }
                     )
                     var thrown = false
diff --git a/nexus/src/test/kotlin/JsonTest.kt 
b/nexus/src/test/kotlin/JsonTest.kt
index 03c6e364..b67aad44 100644
--- a/nexus/src/test/kotlin/JsonTest.kt
+++ b/nexus/src/test/kotlin/JsonTest.kt
@@ -2,8 +2,13 @@ import com.fasterxml.jackson.databind.JsonNode
 import org.junit.Test
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import com.fasterxml.jackson.module.kotlin.readValue
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.http.*
+import io.ktor.server.testing.*
 import tech.libeufin.nexus.server.CreateBankConnectionFromBackupRequestJson
 import tech.libeufin.nexus.server.CreateBankConnectionFromNewRequestJson
+import tech.libeufin.sandbox.sandboxApp
 
 
 class JsonTest {
@@ -23,5 +28,15 @@ class JsonTest {
         assert(roundTripNew.data.toString() == "{}" && roundTripNew.type == 
"ebics" && roundTripNew.name == "new-connection")
     }
 
-
+    /*@Test
+    fun testSandboxJsonParsing() {
+        testApplication {
+            application(sandboxApp)
+            client.post("/admin/ebics/subscribers") {
+                basicAuth("admin", "foo")
+                contentType(ContentType.Application.Json)
+                setBody("{}")
+            }
+        }
+    }*/
 }
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt
index 47d1ff45..dff2c54d 100644
--- a/nexus/src/test/kotlin/MakeEnv.kt
+++ b/nexus/src/test/kotlin/MakeEnv.kt
@@ -70,7 +70,8 @@ fun prepNexusDb() {
             type = "ebics"
         }
         tech.libeufin.nexus.EbicsSubscriberEntity.new {
-            ebicsURL = "http://localhost:5000/ebicsweb";
+            // ebicsURL = "http://localhost:5000/ebicsweb";
+            ebicsURL = "http://localhost/ebicsweb";
             hostID = "eufinSandbox"
             partnerID = "foo"
             userID = "foo"
@@ -84,7 +85,7 @@ fun prepNexusDb() {
             bankEncryptionPublicKey = ExposedBlob(bankKeys.enc.public.encoded)
             bankAuthenticationPublicKey = 
ExposedBlob(bankKeys.auth.public.encoded)
         }
-        val a = NexusBankAccountEntity.new {
+        NexusBankAccountEntity.new {
             bankAccountName = "foo"
             iban = FOO_USER_IBAN
             bankCode = "SANDBOXX"
@@ -92,7 +93,7 @@ fun prepNexusDb() {
             highestSeenBankMessageSerialId = 0
             accountHolder = "foo"
         }
-        val b = NexusBankAccountEntity.new {
+        NexusBankAccountEntity.new {
             bankAccountName = "bar"
             iban = BAR_USER_IBAN
             bankCode = "SANDBOXX"
@@ -141,7 +142,7 @@ fun prepSandboxDb() {
             this.demoBank = demoBank
             isPublic = false
         }
-        val otherBankAccount = BankAccountEntity.new {
+        BankAccountEntity.new {
             iban = BAR_USER_IBAN
             /**
              * For now, keep same semantics of Pybank: a username
diff --git a/nexus/src/test/kotlin/SandboxAccessApiTest.kt 
b/nexus/src/test/kotlin/SandboxAccessApiTest.kt
index 8b6366c0..028c41d1 100644
--- a/nexus/src/test/kotlin/SandboxAccessApiTest.kt
+++ b/nexus/src/test/kotlin/SandboxAccessApiTest.kt
@@ -1,16 +1,12 @@
 import com.fasterxml.jackson.databind.ObjectMapper
-import io.ktor.client.features.*
+import io.ktor.client.plugins.*
 import io.ktor.client.request.*
 import io.ktor.client.statement.*
-import io.ktor.client.utils.*
 import io.ktor.http.*
 import io.ktor.server.testing.*
-import io.netty.handler.codec.http.HttpResponseStatus
 import kotlinx.coroutines.runBlocking
-import org.jetbrains.exposed.sql.transactions.transaction
 import org.junit.Test
 import tech.libeufin.sandbox.*
-import tech.libeufin.util.buildBasicAuthLine
 
 class SandboxAccessApiTest {
     val mapper = ObjectMapper()
@@ -19,39 +15,24 @@ class SandboxAccessApiTest {
     fun debitWithdraw() {
         withTestDatabase {
             prepSandboxDb()
-            withTestApplication(sandboxApp) {
+            testApplication {
+                this.application(sandboxApp)
                 runBlocking {
                     // Normal, successful withdrawal.
-                    
client.post<Any>("/demobanks/default/access-api/accounts/foo/withdrawals") {
+                    
client.post("/demobanks/default/access-api/accounts/foo/withdrawals") {
                         expectSuccess = true
-                        headers {
-                            append(
-                                HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("foo", "foo")
-                            )
-                        }
-                        this.body = "{\"amount\": \"TESTKUDOS:1\"}"
+                        setBody("{\"amount\": \"TESTKUDOS:1\"}")
+                        contentType(ContentType.Application.Json)
+                        basicAuth("foo", "foo")
                     }
                     // Withdrawal over the debit threshold.
-                    val r: HttpStatusCode = 
client.post("/demobanks/default/access-api/accounts/foo/withdrawals") {
+                    val r: HttpResponse = 
client.post("/demobanks/default/access-api/accounts/foo/withdrawals") {
                         expectSuccess = false
-                        headers {
-                            append(
-                                HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("foo", "foo")
-                            )
-                        }
-                        this.body = "{\"amount\": \"TESTKUDOS:99999999999\"}"
+                        contentType(ContentType.Application.Json)
+                        basicAuth("foo", "foo")
+                        setBody("{\"amount\": \"TESTKUDOS:99999999999\"}")
                     }
-                    assert(HttpStatusCode.Forbidden.value == r.value)
+                    assert(HttpStatusCode.Forbidden.value == r.status.value)
                 }
             }
         }
@@ -61,12 +42,13 @@ class SandboxAccessApiTest {
      * Tests that 'admin' and 'bank' are not possible to register
      * and that after 'admin' logs in it gets access to the bank's
      * main account.
-     */
+     */ // FIXME: avoid giving Content-Type at every request.
     @Test
     fun adminRegisterAndLoginTest() {
         withTestDatabase {
             prepSandboxDb()
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 runBlocking {
                     val registerAdmin = mapper.writeValueAsString(object {
                         val username = "admin"
@@ -77,17 +59,10 @@ class SandboxAccessApiTest {
                         val password = "y"
                     })
                     for (b in mutableListOf<String>(registerAdmin, 
registerBank)) {
-                        val r = client.post<HttpResponse>(
-                            urlString = 
"/demobanks/default/access-api/testing/register",
-                        ) {
-                            this.body = b
+                        val r = 
client.post("/demobanks/default/access-api/testing/register") {
+                            setBody(b)
+                            contentType(ContentType.Application.Json)
                             expectSuccess = false
-                            headers {
-                                append(
-                                    HttpHeaders.ContentType,
-                                    ContentType.Application.Json
-                                )
-                            }
                         }
                         assert(r.status.value == 
HttpStatusCode.Forbidden.value)
                     }
@@ -99,17 +74,11 @@ class SandboxAccessApiTest {
                         "setting the balance",
                         "TESTKUDOS:99"
                     )
-                    // Get admin's balance.
-                    val r = client.get<String>(
-                        urlString = 
"/demobanks/default/access-api/accounts/admin",
-                    ) {
+                    // Get admin's balance.  Not asserting; it
+                    // fails on != 200 responses.
+                    val r = 
client.get("/demobanks/default/access-api/accounts/admin") {
                         expectSuccess = true
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "foo")
-                            )
-                        }
+                        basicAuth("admin", "foo")
                     }
                     println(r)
                 }
@@ -121,7 +90,8 @@ class SandboxAccessApiTest {
     fun registerTest() {
         // Test IBAN conflict detection.
         withSandboxTestDatabase {
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 runBlocking {
                     val bodyFoo = mapper.writeValueAsString(object {
                         val username = "x"
@@ -138,58 +108,24 @@ class SandboxAccessApiTest {
                         val password = "y"
                         val iban = BAR_USER_IBAN
                     })
-                    // The following block would allow to save many LOC,
-                    // but gets somehow ignored.
-                    /*client.config {
-                        this.defaultRequest {
-                            headers {
-                                append(
-                                    HttpHeaders.ContentType,
-                                    ContentType.Application.Json
-                                )
-                            }
-                            expectSuccess = false
-                        }
-                    }*/
                     // Succeeds.
-                    client.post<HttpResponse>(
-                        urlString = 
"/demobanks/default/access-api/testing/register",
-                    ) {
-                        this.body = bodyFoo
+                    
client.post("/demobanks/default/access-api/testing/register") {
+                        setBody(bodyFoo)
+                        contentType(ContentType.Application.Json)
                         expectSuccess = true
-                        headers {
-                            append(
-                                HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                        }
                     }
                     // Hits conflict, because of the same IBAN.
-                    val r = client.post<HttpResponse>(
-                        "/demobanks/default/access-api/testing/register"
-                    ) {
-                        this.body = bodyBar
+                    val r = 
client.post("/demobanks/default/access-api/testing/register") {
+                        setBody(bodyBar)
                         expectSuccess = false
-                        headers {
-                            append(
-                                HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                        }
+                        contentType(ContentType.Application.Json)
                     }
-                    assert(r.status.value == 
HttpResponseStatus.CONFLICT.code())
+                    assert(r.status.value == HttpStatusCode.Conflict.value)
                     // Succeeds, because of a new IBAN.
-                    client.post<HttpResponse>(
-                        "/demobanks/default/access-api/testing/register"
-                    ) {
-                        this.body = bodyBaz
+                    
client.post("/demobanks/default/access-api/testing/register") {
+                        setBody(bodyBaz)
                         expectSuccess = true
-                        headers {
-                            append(
-                                HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                        }
+                        contentType(ContentType.Application.Json)
                     }
                 }
             }
diff --git a/nexus/src/test/kotlin/SandboxBankAccountTest.kt 
b/nexus/src/test/kotlin/SandboxBankAccountTest.kt
index 9dc0982a..7d6aec9e 100644
--- a/nexus/src/test/kotlin/SandboxBankAccountTest.kt
+++ b/nexus/src/test/kotlin/SandboxBankAccountTest.kt
@@ -1,9 +1,4 @@
-import io.ktor.client.features.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
 import io.ktor.http.*
-import io.ktor.server.testing.*
-import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import tech.libeufin.sandbox.SandboxError
 import tech.libeufin.sandbox.getBalance
diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt 
b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
index d2d0e4c6..edc7ba74 100644
--- a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
+++ b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
@@ -1,22 +1,42 @@
-import io.ktor.client.features.*
-import io.ktor.client.features.get
+import io.ktor.client.plugins.auth.*
+import io.ktor.client.plugins.auth.providers.*
 import io.ktor.client.request.*
+import io.ktor.client.statement.*
 import io.ktor.server.testing.*
 import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import tech.libeufin.sandbox.sandboxApp
 
 class SandboxCircuitApiTest {
-    // Get /config
+    // Get /config, fails if != 200.
     @Test
     fun config() {
         withSandboxTestDatabase {
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 runBlocking {
-                    val r: String = 
client.get("/demobanks/default/circuit-api/config")
-                    println(r)
+                    val r= client.get("/demobanks/default/circuit-api/config")
+                    println(r.bodyAsText())
                 }
             }
         }
     }
+
+    // Tests the registration logic.  Triggers
+    // any error code, following at least one execution
+    // path.
+    @Test
+    fun registration() {
+        withSandboxTestDatabase {
+            testApplication {
+                application(sandboxApp)
+                runBlocking {
+                    client.post("/demobanks/default/circuit-api/accounts") {
+                        basicAuth("admin", "foo")
+                    }
+                }
+            }
+        }
+
+    }
 }
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/SandboxLegacyApiTest.kt 
b/nexus/src/test/kotlin/SandboxLegacyApiTest.kt
index d7dfaf44..5b0ed407 100644
--- a/nexus/src/test/kotlin/SandboxLegacyApiTest.kt
+++ b/nexus/src/test/kotlin/SandboxLegacyApiTest.kt
@@ -1,9 +1,7 @@
-import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream
-import io.ktor.client.features.*
+import io.ktor.client.plugins.*
 import io.ktor.client.request.*
-import io.ktor.client.statement.HttpResponse
+import io.ktor.client.statement.*
 import io.ktor.http.*
 import io.ktor.server.testing.*
 import io.ktor.util.*
@@ -16,12 +14,7 @@ import tech.libeufin.sandbox.sandboxApp
 import tech.libeufin.util.buildBasicAuthLine
 import tech.libeufin.util.getIban
 import java.io.ByteArrayOutputStream
-import java.io.InputStream
-import java.nio.ByteBuffer
 
-/**
- * Mostly checking legacy API's access control.
- */
 class SandboxLegacyApiTest {
     fun dbHelper (f: () -> Unit) {
         withTestDatabase {
@@ -31,12 +24,12 @@ class SandboxLegacyApiTest {
     }
     val mapper = ObjectMapper()
 
-
     // EBICS Subscribers API.
     @Test
-    fun adminEbiscSubscribers() {
+    fun adminEbicsSubscribers() {
         dbHelper {
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 runBlocking {
                     /**
                      * Create a EBICS subscriber.  That conflicts because
@@ -51,79 +44,42 @@ class SandboxLegacyApiTest {
                     })
                     var r: HttpResponse = 
client.post("/admin/ebics/subscribers") {
                         expectSuccess = false
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "foo")
-                            )
-                            append(
-                                HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                        }
-                        this.body = body
+                        contentType(ContentType.Application.Json)
+                        basicAuth("admin", "foo")
+                        setBody(body)
                     }
                     assert(r.status.value == HttpStatusCode.Conflict.value)
-                    /**
-                     * Check that EBICS subscriber indeed exists.
-                     */
+
+                    // Check that EBICS subscriber indeed exists.
                     r = client.get("/admin/ebics/subscribers") {
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "foo")
-                            )
-                        }
+                        basicAuth("admin", "foo")
                     }
                     assert(r.status.value == HttpStatusCode.OK.value)
-                    val buf = ByteArrayOutputStream()
-                    r.content.read { buf.write(it.array()) }
-                    val respObj = mapper.readTree(buf.toString())
+                    val respObj = mapper.readTree(r.bodyAsText())
                     assert("foo" == 
respObj.get("subscribers").get(0).get("userID").asText())
-                    /**
-                     * Try same operations as above, with wrong admin 
credentials
-                     */
+
+                    // Try same operations as above, with wrong admin 
credentials
                     r = client.get("/admin/ebics/subscribers") {
                         expectSuccess = false
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "wrong")
-                            )
-                        }
+                        basicAuth("admin", "wrong")
                     }
                     assert(r.status.value == HttpStatusCode.Unauthorized.value)
                     r = client.post("/admin/ebics/subscribers") {
                         expectSuccess = false
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "wrong")
-                            )
-                        }
+                        basicAuth("admin", "wrong")
                     }
                     assert(r.status.value == HttpStatusCode.Unauthorized.value)
-                    // Good credentials, but unauthorized user.
+                    // Good credentials, but insufficient rights.
                     r = client.get("/admin/ebics/subscribers") {
                         expectSuccess = false
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("foo", "foo")
-                            )
-                        }
+                        basicAuth("foo", "foo")
                     }
-                    assert(r.status.value == HttpStatusCode.Unauthorized.value)
+                    assert(r.status.value == HttpStatusCode.Forbidden.value)
                     r = client.post("/admin/ebics/subscribers") {
                         expectSuccess = false
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("foo", "foo")
-                            )
-                        }
+                        basicAuth("foo", "foo")
                     }
-                    assert(r.status.value == HttpStatusCode.Unauthorized.value)
+                    assert(r.status.value == HttpStatusCode.Forbidden.value)
                     /**
                      * Give a bank account to the existing subscriber.  Bank 
account
                      * is (implicitly / hard-coded) hosted at default demobank.
@@ -135,19 +91,11 @@ class SandboxLegacyApiTest {
                         val partnerID = "baz"
                         val systemID = "foo"
                     })
-                    client.post<HttpResponse>("/admin/ebics/subscribers") {
+                    client.post("/admin/ebics/subscribers") {
                         expectSuccess = true
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "foo")
-                            )
-                            append(
-                                HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                        }
-                        this.body = body
+                        contentType(ContentType.Application.Json)
+                        basicAuth("admin", "foo")
+                        setBody(body)
                     }
                     // Associate new bank account to it.
                     body = mapper.writeValueAsString(object {
@@ -162,65 +110,42 @@ class SandboxLegacyApiTest {
                         val name = "Now Have Account"
                         val label = "baz"
                     })
-                    client.post<HttpResponse>("/admin/ebics/bank-accounts") {
+                    client.post("/admin/ebics/bank-accounts") {
                         expectSuccess = true
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "foo")
-                            )
-                            append(
-                                HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                        }
-                        this.body = body
+                        expectSuccess = true
+                        contentType(ContentType.Application.Json)
+                        basicAuth("admin", "foo")
+                        setBody(body)
                     }
                     r = client.get("/admin/ebics/subscribers") {
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "foo")
-                            )
-                        }
+                        basicAuth("admin", "foo")
                     }
                     assert(r.status.value == HttpStatusCode.OK.value)
-                    val buf_ = ByteArrayOutputStream()
-                    r.content.read { buf_.write(it.array()) }
-                    val respObj_ = mapper.readTree(buf_.toString())
+                    val respObj_ = mapper.readTree(r.bodyAsText())
                     val bankAccountLabel = 
respObj_.get("subscribers").get(1).get("demobankAccountLabel").asText()
                     assert("baz" == bankAccountLabel)
                     // Same operation, wrong/unauth credentials.
                     r = client.post("/admin/ebics/bank-accounts") {
                         expectSuccess = false
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "wrong")
-                            )
-                        }
+                        basicAuth("admin", "wrong")
                     }
                     assert(r.status.value == HttpStatusCode.Unauthorized.value)
                     r = client.post("/admin/ebics/bank-accounts") {
                         expectSuccess = false
-                        headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("foo", "foo")
-                            )
-                        }
+                        basicAuth("foo", "foo")
                     }
-                    assert(r.status.value == HttpStatusCode.Unauthorized.value)
+                    assert(r.status.value == HttpStatusCode.Forbidden.value)
                 }
             }
         }
     }
 
     // EBICS Hosts API.
-    @Ignore
+    @Test
     fun adminEbicsCreateHost() {
         dbHelper {
-            withTestApplication(sandboxApp) {
+            testApplication {
+                application(sandboxApp)
                 runBlocking {
                     val body = mapper.writeValueAsString(
                         object {
@@ -229,73 +154,38 @@ class SandboxLegacyApiTest {
                         }
                     )
                     // Valid request, good credentials.
-                    var r = client.post<HttpResponse>("/admin/ebics/hosts") {
-                        this.body = body
-                        this.headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "foo")
-                            )
-                            append(
-                                HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                        }
-                    }
-                    assert(r.status.value == HttpResponseStatus.OK.code())
-                    r = client.get("/admin/ebics/hosts") {
-                        expectSuccess = false
-
+                    client.post("/admin/ebics/hosts") {
+                        expectSuccess = true
+                        setBody(body)
+                        contentType(ContentType.Application.Json)
+                        basicAuth("admin", "foo")
                     }
+                    var r = client.get("/admin/ebics/hosts") { expectSuccess = 
false }
                     assert(r.status.value == 
HttpResponseStatus.UNAUTHORIZED.code())
-                    r = client.get("/admin/ebics/hosts") {
-                        this.headers {
-                            append(
-                                HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "foo")
-                            )
-                        }
+                    client.get("/admin/ebics/hosts") {
+                        basicAuth("admin", "foo")
+                        expectSuccess = true
                     }
-                    assert(r.status.value == HttpResponseStatus.OK.code())
                     // Invalid, with good credentials.
                     r = client.post("/admin/ebics/hosts") {
                         expectSuccess = false
-                        this.body = "invalid"
-                        this.headers {
-                            append(
-                                io.ktor.http.HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "foo")
-                            )
-                            append(
-                                io.ktor.http.HttpHeaders.ContentType,
-                                ContentType.Application.Json
-                            )
-                        }
+                        setBody("invalid")
+                        contentType(ContentType.Application.Json)
+                        basicAuth("admin", "foo")
                     }
-                    assert(r.status.value == 
HttpResponseStatus.BAD_REQUEST.code())
+                    assert(r.status.value == HttpStatusCode.BadRequest.value)
                     // Unauth: admin with wrong password.
                     r = client.post("/admin/ebics/hosts") {
                         expectSuccess = false
-                        this.headers {
-                            append(
-                                io.ktor.http.HttpHeaders.Authorization,
-                                buildBasicAuthLine("admin", "bar")
-                            )
-                        }
+                        basicAuth("admin", "bar")
                     }
-                    assert(r.status.value == 
HttpResponseStatus.UNAUTHORIZED.code())
+                    assert(r.status.value == HttpStatusCode.Unauthorized.value)
                     // Auth & forbidden resource.
                     r = client.post("/admin/ebics/hosts") {
                         expectSuccess = false
-                        this.headers {
-                            append(
-                                io.ktor.http.HttpHeaders.Authorization,
-                                // Exist, but no rights over the EBICS host.
-                                buildBasicAuthLine("foo", "foo")
-                            )
-                        }
+                        basicAuth("foo", "foo")
                     }
-                    assert(r.status.value == 
HttpResponseStatus.UNAUTHORIZED.code())
+                    assert(r.status.value == HttpStatusCode.Forbidden.value)
                 }
             }
         }
diff --git a/sandbox/build.gradle b/sandbox/build.gradle
index 6b0a907e..c87bdf53 100644
--- a/sandbox/build.gradle
+++ b/sandbox/build.gradle
@@ -1,7 +1,8 @@
 plugins {
+    id 'kotlin'
     id 'java'
-    id 'org.jetbrains.kotlin.jvm'
     id 'application'
+    id 'org.jetbrains.kotlin.jvm'
     id "com.github.johnrengelman.shadow" version "5.2.0"
 }
 
@@ -15,6 +16,12 @@ compileKotlin {
     }
 }
 
+compileTestKotlin {
+    kotlinOptions {
+        jvmTarget = "11"
+    }
+}
+
 task installToPrefix(type: Copy) {
     dependsOn(installShadowDist)
     from("build/install/sandbox-shadow") {
@@ -29,29 +36,20 @@ task installToPrefix(type: Copy) {
      */
     into "${project.findProperty('prefix') ?: '/tmp'}"
 }
-
-compileTestKotlin {
-    kotlinOptions {
-        jvmTarget = "11"
-    }
-}
+apply plugin: 'kotlin-kapt'
 
 sourceSets {
-    main.java.srcDirs = ['src/main/java', 'src/main/kotlin']
+    main.java.srcDirs = [
+            'src/main/java',
+            'src/main/kotlin'
+    ]
 }
 
-def ktor_version = '1.6.1'
-/**
- * Exposed 0.38.2 caused a SQLITE_BUSY error at test-auditor.sh.
- * The error was caused by a concurrent handling of a CCT EBICS
- * message (see handleCct()).
- */
-def exposed_version = '0.32.1'
-
 dependencies {
+    implementation 
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt'
     implementation "com.hubspot.jinjava:jinjava:2.5.9"
-    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21'
     implementation 'ch.qos.logback:logback-classic:1.2.5'
+    implementation project(":util")
 
     // XML:
     implementation "javax.xml.bind:jaxb-api:2.3.0"
@@ -68,19 +66,22 @@ dependencies {
     implementation "org.jetbrains.exposed:exposed-jdbc:$exposed_version"
 
     implementation "io.ktor:ktor-server-core:$ktor_version"
+    implementation "io.ktor:ktor-server-call-logging:$ktor_version"
+    implementation "io.ktor:ktor-server-cors:$ktor_version"
+    implementation "io.ktor:ktor-server-content-negotiation:$ktor_version"
+    implementation "io.ktor:ktor-server-status-pages:$ktor_version"
     implementation "io.ktor:ktor-client-apache:$ktor_version"
+    implementation "io.ktor:ktor-client-auth:$ktor_version"
     implementation "io.ktor:ktor-server-netty:$ktor_version"
-    implementation "io.ktor:ktor-jackson:$ktor_version"
-    implementation "io.ktor:ktor-auth:$ktor_version"
+    implementation "io.ktor:ktor-server-test-host:$ktor_version"
+    implementation "io.ktor:ktor-auth:$ktor_auth_version"
+    implementation "io.ktor:ktor-serialization-jackson:$ktor_version"
 
     testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21'
     testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21'
     testImplementation group: "junit", name: "junit", version: '4.13.2'
-
-    implementation project(":util")
 }
 
-
 application {
     mainClassName = "tech.libeufin.sandbox.MainKt"
     applicationName = "libeufin-sandbox"
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
index 0becab7b..a923c052 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
@@ -1,8 +1,14 @@
 package tech.libeufin.sandbox
 
-import io.ktor.application.*
-import io.ktor.response.*
-import io.ktor.routing.*
+import io.ktor.server.application.*
+import io.ktor.http.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.util.InvalidPaytoError
+import tech.libeufin.util.conflict
+import tech.libeufin.util.parsePayto
 
 // CIRCUIT API TYPES
 
@@ -22,11 +28,78 @@ class RatioAndFees(
     val sell_out_fee: Float = 0F
 )
 
+// User registration request
+class CircuitAccountRequest(
+    val username: String,
+    val password: String,
+    val contact_data: CircuitAccountData,
+    val name: String,
+    val cashout_address: String, // payto
+    val internal_iban: String? // Shall be "= null" ?
+)
+// User contact data to send the TAN.
+class CircuitAccountData(
+    val email: String?,
+    val phone: String?
+)
+
+/**
+ * Allows only the administrator to add new accounts.
+ */
 fun circuitApi(circuitRoute: Route) {
+    circuitRoute.post("/accounts") {
+        call.request.basicAuth(onlyAdmin = true)
+        val req = call.receive<CircuitAccountRequest>()
+        // Validity and availability check on the input data.
+        if (req.contact_data.email != null) {
+            val maybeEmailConflict = DemobankCustomerEntity.find {
+                DemobankCustomersTable.email eq req.contact_data.email
+            }.firstOrNull()
+            if (maybeEmailConflict != null) {
+                // Warning since two individuals claimed one same e-mail 
address.
+                logger.warn("Won't register user ${req.username}: e-mail 
conflict on ${req.contact_data.email}")
+                throw conflict("E-mail address already in use!")
+            }
+            // Syntactic validation.  Warn on error, since UI could avoid this.
+            // FIXME
+            // From Taler TypeScript:
+            // 
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+        }
+        if (req.contact_data.phone != null) {
+            val maybePhoneConflict = DemobankCustomerEntity.find {
+                DemobankCustomersTable.phone eq req.contact_data.phone
+            }.firstOrNull()
+            if (maybePhoneConflict != null) {
+                // Warning since two individuals claimed one same phone number.
+                logger.warn("Won't register user ${req.username}: phone 
conflict on ${req.contact_data.email}")
+                throw conflict("Phone number already in use!")
+            }
+            // Syntactic validation.  Warn on error, since UI could avoid this.
+            // FIXME
+            // From Taler TypeScript
+            // /^\+[0-9 ]*$/;
+        }
+        // Check that cash-out address parses.
+        try {
+            parsePayto(req.cashout_address)
+        } catch (e: InvalidPaytoError) {
+            // Warning because the UI could avoid this.
+            logger.warn("Won't register account ${req.username}: invalid 
cash-out address: ${req.cashout_address}")
+        }
+        transaction {
+            val newAccount = insertNewAccount(
+                username = req.username,
+                password = req.password,
+                name = req.name
+            )
+            newAccount.customer.phone = req.contact_data.phone
+            newAccount.customer.email = req.contact_data.email
+        }
+        call.respond(HttpStatusCode.NoContent)
+        return@post
+    }
     circuitRoute.get("/config") {
-        call.respond(ConfigResp(
-            ratios_and_fees = RatioAndFees()
-        ))
+        call.respond(ConfigResp(ratios_and_fees = RatioAndFees()))
         return@get
     }
 }
\ No newline at end of file
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
index b0271e85..13bc8165 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
@@ -120,6 +120,8 @@ object DemobankCustomersTable : LongIdTable() {
     val username = text("username")
     val passwordHash = text("passwordHash")
     val name = text("name").nullable()
+    val email = text("email").nullable()
+    val phone = text("phone").nullable()
 }
 
 class DemobankCustomerEntity(id: EntityID<Long>) : LongEntity(id) {
@@ -127,6 +129,8 @@ class DemobankCustomerEntity(id: EntityID<Long>) : 
LongEntity(id) {
     var username by DemobankCustomersTable.username
     var passwordHash by DemobankCustomersTable.passwordHash
     var name by DemobankCustomersTable.name
+    var email by DemobankCustomersTable.email
+    var phone by DemobankCustomersTable.phone
 }
 
 /**
@@ -370,7 +374,9 @@ class BankAccountTransactionEntity(id: EntityID<Long>) : 
LongEntity(id) {
 
 /**
  * Table that keeps information about which bank accounts (iban+bic+name)
- * are active in the system.
+ * are active in the system.  In the current version, 'label' and 'owner'
+ * are always equal; future versions may change this, when one customer can
+ * own multiple bank accounts.
  */
 object BankAccountsTable : IntIdTable() {
     val iban = text("iban")
diff --git 
a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
index e09955af..b40234a1 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
@@ -20,12 +20,12 @@
 
 package tech.libeufin.sandbox
 
-import io.ktor.application.*
+import io.ktor.server.application.*
 import io.ktor.http.ContentType
 import io.ktor.http.HttpStatusCode
-import io.ktor.request.*
-import io.ktor.response.respond
-import io.ktor.response.respondText
+import io.ktor.server.request.*
+import io.ktor.server.response.respond
+import io.ktor.server.response.respondText
 import io.ktor.util.AttributeKey
 import org.apache.xml.security.binding.xmldsig.RSAKeyValueType
 import org.jetbrains.exposed.sql.*
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
index ffaab312..078e0546 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
@@ -19,9 +19,9 @@
 
 package tech.libeufin.sandbox
 
-import io.ktor.application.*
+import io.ktor.server.application.*
 import io.ktor.http.HttpStatusCode
-import io.ktor.request.*
+import io.ktor.server.request.*
 import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
 import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.transactions.transaction
@@ -44,6 +44,89 @@ data class SandboxCamt(
     val creationTime: Long
 )
 
+/**
+ * DB helper inserting a new "account" into the database.
+ * The account is made of a 'customer' and 'bank account'
+ * object.  The helper checks first that the username is
+ * acceptable (chars, no institutional names, available
+ * names); then checks that IBAN is available and then adds
+ * the two database objects under the given demobank.  This
+ * function contains the common logic shared by the Access
+ * and Circuit API.  Additional data that is peculiar to one
+ * API should be added separately.
+ *
+ * It returns a AccountPair type.  That contains the customer
+ * object and the bank account; the caller may this way add custom
+ * values to them.  */
+data class AccountPair(
+    val customer: DemobankCustomerEntity,
+    val bankAccount: BankAccountEntity
+)
+fun insertNewAccount(username: String,
+                     password: String,
+                     name: String? = null, // tests do not usually give one.
+                     iban: String? = null,
+                     isPublic: Boolean = false,
+                     demobank: String = "default"): AccountPair {
+    requireValidResourceName(username)
+    // Forbid institutional usernames.
+    if (username == "bank" || username == "admin") {
+        logger.info("Username: $username not allowed.")
+        throw forbidden("Username: $username is not allowed.")
+    }
+
+    val demobankFromDb = getDemobank(demobank)
+    // Bank's fault, because when this function gets
+    // called, the demobank must exist.
+    if (demobankFromDb == null) {
+        logger.error("Demobank '$demobank' not found.  Won't add account 
$username")
+        throw internalServerError("Demobank $demobank not found.  Won't add 
account $username")
+    }
+    // Generate a IBAN if the caller didn't provide one.
+    val newIban = iban ?: getIban()
+    // Check IBAN collisions.
+    val checkIbanExist = BankAccountEntity.find(BankAccountsTable.iban eq 
newIban).firstOrNull()
+    if (checkIbanExist != null) {
+        logger.info("IBAN $newIban not available.  Won't register username 
$username")
+        throw conflict("IBAN $iban not available.")
+    }
+    // Check username availability.
+    val checkCustomerExist = transaction {
+        DemobankCustomerEntity.find {
+            DemobankCustomersTable.username eq username
+        }.firstOrNull()
+    }
+    if (checkCustomerExist != null) {
+        throw SandboxError(
+            HttpStatusCode.Conflict,
+            "Username $username not available."
+        )
+    }
+    val newCustomer = DemobankCustomerEntity.new {
+        this.username = username
+        passwordHash = CryptoUtil.hashpw(password)
+        this.name = name // nullable
+    }
+    // Actual account creation.
+    val newBankAccount = BankAccountEntity.new {
+        this.iban = newIban
+        /**
+         * For now, keep same semantics of Pybank: a username
+         * is AS WELL a bank account label.  In other words, it
+         * identifies a customer AND a bank account.  The reason
+         * to have the two values (label and owner) is to allow
+         * multiple bank accounts being owned by one customer.
+         */
+        label = username
+        owner = username
+        this.demoBank = demobankFromDb
+        this.isPublic = isPublic
+    }
+    if (demobankFromDb.withSignupBonus)
+        newBankAccount.bonus("${demobankFromDb.currency}:100")
+    return AccountPair(customer = newCustomer, bankAccount = newBankAccount)
+}
+
 /**
  *
  * Return true if access to the bank account can be granted,
@@ -90,10 +173,7 @@ fun ApplicationRequest.basicAuth(onlyAdmin: Boolean = 
false): String? {
         )
         return credentials.first
     }
-    /**
-     * If only admin auth was allowed, here it failed already,
-     * hence throw 401.  */
-    if (onlyAdmin) throw unauthorized("Only admin allowed.")
+    if (onlyAdmin) throw forbidden("Only admin allowed.")
     val passwordHash = transaction {
         val customer = getCustomer(credentials.first)
         customer.passwordHash
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index 04d812d0..b03ac61c 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -21,11 +21,15 @@ package tech.libeufin.sandbox
 
 import UtilError
 import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.core.JsonProcessingException
 import com.fasterxml.jackson.core.util.DefaultIndenter
 import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.JsonMappingException
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.databind.SerializationFeature
 import com.fasterxml.jackson.databind.exc.MismatchedInputException
+import com.fasterxml.jackson.module.kotlin.KotlinFeature
 import com.fasterxml.jackson.module.kotlin.KotlinModule
 import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
 import com.github.ajalt.clikt.core.CliktCommand
@@ -36,15 +40,21 @@ import com.github.ajalt.clikt.parameters.arguments.argument
 import com.github.ajalt.clikt.parameters.options.*
 import com.github.ajalt.clikt.parameters.types.int
 import execThrowableOrTerminate
-import io.ktor.application.*
-import io.ktor.features.*
+import io.ktor.server.application.*
 import io.ktor.http.*
-import io.ktor.jackson.*
-import io.ktor.request.*
-import io.ktor.response.*
-import io.ktor.routing.*
+import io.ktor.serialization.*
+import io.ktor.serialization.jackson.*
 import io.ktor.server.engine.*
 import io.ktor.server.netty.*
+import io.ktor.server.plugins.*
+import io.ktor.server.plugins.contentnegotiation.*
+import io.ktor.server.plugins.statuspages.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import io.ktor.server.util.*
+import io.ktor.server.plugins.callloging.*
+import io.ktor.server.plugins.cors.routing.*
 import io.ktor.util.*
 import io.ktor.util.date.*
 import kotlinx.coroutines.*
@@ -446,26 +456,22 @@ fun main(args: Array<String>) {
     ).main(args)
 }
 
-suspend inline fun <reified T : Any> ApplicationCall.receiveJson(): T {
-    try {
-        return this.receive()
-    } catch (e: MissingKotlinParameterException) {
-        throw SandboxError(HttpStatusCode.BadRequest, "Missing value for 
${e.pathReference}")
-    } catch (e: MismatchedInputException) {
-        // Note: POSTing "[]" gets here but e.pathReference is blank.
-        throw SandboxError(HttpStatusCode.BadRequest, "Invalid value for 
'${e.pathReference}'")
-    } catch (e: JsonParseException) {
-        throw SandboxError(HttpStatusCode.BadRequest, "Invalid JSON")
-    }
-}
-
 fun setJsonHandler(ctx: ObjectMapper) {
     ctx.enable(SerializationFeature.INDENT_OUTPUT)
     ctx.setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
         indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
         indentObjectsWith(DefaultIndenter("  ", "\n"))
     })
-    ctx.registerModule(KotlinModule(nullisSameAsDefault = true))
+    ctx.registerModule(
+        KotlinModule.Builder()
+            .withReflectionCacheSize(512)
+            .configure(KotlinFeature.NullToEmptyCollection, false)
+            .configure(KotlinFeature.NullToEmptyMap, false)
+            .configure(KotlinFeature.NullIsSameAsDefault, enabled = true)
+            .configure(KotlinFeature.SingletonSupport, enabled = false)
+            .configure(KotlinFeature.StrictNullChecks, false)
+            .build()
+    )
 }
 
 val sandboxApp: Application.() -> Unit = {
@@ -478,14 +484,14 @@ val sandboxApp: Application.() -> Unit = {
     }
     install(CORS) {
         anyHost()
-        header(HttpHeaders.Authorization)
-        header(HttpHeaders.ContentType)
-        method(HttpMethod.Options)
-        // logger.info("Enabling CORS (assuming no endpoint uses cookies).")
+        allowHeader(HttpHeaders.Authorization)
+        allowHeader(HttpHeaders.ContentType)
+        allowMethod(HttpMethod.Options)
         allowCredentials = true
     }
     install(IgnoreTrailingSlash)
     install(ContentNegotiation) {
+
         register(ContentType.Text.Xml, XMLEbicsConverter())
         /**
          * Content type "text" must go to the XML parser
@@ -502,7 +508,7 @@ val sandboxApp: Application.() -> Unit = {
     }
     install(StatusPages) {
         // Bank's fault: it should check the operands.  Respond 500
-        exception<ArithmeticException> { cause ->
+        exception<ArithmeticException> { call, cause ->
             logger.error("Exception while handling '${call.request.uri}', 
${cause.stackTraceToString()}")
             call.respond(
                 HttpStatusCode.InternalServerError,
@@ -514,8 +520,9 @@ val sandboxApp: Application.() -> Unit = {
                 )
             )
         }
-        exception<SandboxError> { cause ->
-            logger.error("Exception while handling '${call.request.uri}', 
${cause.reason}")
+        // Not necessarily the bank's fault.
+        exception<SandboxError> { call, cause ->
+            logger.debug("Exception while handling '${call.request.uri}', 
${cause.reason}")
             call.respond(
                 cause.statusCode,
                 SandboxErrorJson(
@@ -526,8 +533,9 @@ val sandboxApp: Application.() -> Unit = {
                 )
             )
         }
-        exception<UtilError> { cause ->
-            logger.error("Exception while handling '${call.request.uri}', 
${cause.reason}")
+        // Not necessarily the bank's fault.
+        exception<UtilError> { call, cause ->
+            logger.debug("Exception while handling '${call.request.uri}', 
${cause.reason}")
             call.respond(
                 cause.statusCode,
                 SandboxErrorJson(
@@ -538,9 +546,30 @@ val sandboxApp: Application.() -> Unit = {
                 )
             )
         }
+        // Happens when a request fails to parse.
+        exception<BadRequestException> { call, wrapper ->
+            var errorMessage: String? = wrapper.message // default, if no 
further details can be found.
+            if (errorMessage == null) {
+                logger.error("The bank didn't detect the cause of a bad 
request, fail.")
+                throw SandboxError(
+                    HttpStatusCode.InternalServerError,
+                    "Did not find bad request details."
+                )
+            }
+            logger.debug(errorMessage)
+            call.respond(
+                HttpStatusCode.BadRequest,
+                SandboxErrorJson(
+                    error = SandboxErrorDetailJson(
+                        type = "util-error",
+                        description = errorMessage
+                    )
+                )
+            )
+        }
         // Catch-all error, respond 500 because the bank didn't handle it.
-        exception<Throwable> { cause ->
-            logger.error("Exception while handling '${call.request.uri}'", 
cause.stackTrace)
+        exception<Throwable> { call, cause ->
+            logger.error("Unhandled exception while handling 
'${call.request.uri}'\n${cause.stackTraceToString()}")
             call.respond(
                 HttpStatusCode.InternalServerError,
                 SandboxErrorJson(
@@ -551,9 +580,9 @@ val sandboxApp: Application.() -> Unit = {
                 )
             )
         }
-        exception<EbicsRequestError> { e ->
-            logger.info("Handling EbicsRequestError: ${e.message}")
-            respondEbicsTransfer(call, e.errorText, e.errorCode)
+        exception<EbicsRequestError> { call, cause ->
+            logger.info("Handling EbicsRequestError: ${cause.message}")
+            respondEbicsTransfer(call, cause.errorText, cause.errorCode)
         }
     }
     intercept(ApplicationCallPipeline.Setup) {
@@ -568,10 +597,7 @@ val sandboxApp: Application.() -> Unit = {
 
                 )
             }
-            ac.attributes.put(
-                ADMIN_PASSWORD_ATTRIBUTE_KEY,
-                adminPassword
-            )
+            ac.attributes.put(ADMIN_PASSWORD_ATTRIBUTE_KEY, adminPassword)
         }
         return@intercept
     }
@@ -586,7 +612,6 @@ val sandboxApp: Application.() -> Unit = {
         }
     }
     routing {
-
         get("/") {
             call.respondText(
                 "Hello, this is the Sandbox\n",
@@ -597,7 +622,7 @@ val sandboxApp: Application.() -> Unit = {
         // Query details in the body.
         post("/admin/payments/camt") {
             val username = call.request.basicAuth()
-            val body = call.receiveJson<CamtParams>()
+            val body = call.receive<CamtParams>()
             if (body.type != 53) throw SandboxError(
                 HttpStatusCode.NotFound,
                 "Only Camt.053 documents can be generated."
@@ -630,7 +655,7 @@ val sandboxApp: Application.() -> Unit = {
          */
         post("/admin/bank-accounts/{label}") {
             val username = call.request.basicAuth()
-            val body = call.receiveJson<BankAccountInfo>()
+            val body = call.receive<BankAccountInfo>()
             if (!allowOwnerOrAdmin(username, body.label))
                 throw unauthorized("User '$username' has no rights over" +
                         " bank account '${body.label}'"
@@ -688,7 +713,7 @@ val sandboxApp: Application.() -> Unit = {
         // The debtor is not required to have a customer account at this 
Sandbox.
         post("/admin/bank-accounts/{label}/simulate-incoming-transaction") {
             call.request.basicAuth(onlyAdmin = true)
-            val body = call.receiveJson<IncomingPaymentInfo>()
+            val body = call.receive<IncomingPaymentInfo>()
             val accountLabel = ensureNonNull(call.parameters["label"])
             val reqDebtorBic = body.debtorBic
             if (reqDebtorBic != null && !validateBic(reqDebtorBic)) {
@@ -734,7 +759,7 @@ val sandboxApp: Application.() -> Unit = {
         // Associates a new bank account with an existing Ebics subscriber.
         post("/admin/ebics/bank-accounts") {
             call.request.basicAuth(onlyAdmin = true)
-            val body = call.receiveJson<EbicsBankAccountRequest>()
+            val body = call.receive<EbicsBankAccountRequest>()
             if (!validateBic(body.bic)) {
                 throw SandboxError(HttpStatusCode.BadRequest, "invalid BIC 
(${body.bic})")
             }
@@ -907,7 +932,7 @@ val sandboxApp: Application.() -> Unit = {
          */
         post("/admin/ebics/subscribers") {
             call.request.basicAuth(onlyAdmin = true)
-            val body = call.receiveJson<EbicsSubscriberObsoleteApi>()
+            val body = call.receive<EbicsSubscriberObsoleteApi>()
             transaction {
                 // Check it exists first.
                 val maybeSubscriber = EbicsSubscriberEntity.find {
@@ -984,11 +1009,18 @@ val sandboxApp: Application.() -> Unit = {
         // Create a new EBICS host
         post("/admin/ebics/hosts") {
             call.request.basicAuth(onlyAdmin = true)
-            val req = call.receiveJson<EbicsHostCreateRequest>()
+            val req = call.receive<EbicsHostCreateRequest>()
             val pairA = CryptoUtil.generateRsaKeyPair(2048)
             val pairB = CryptoUtil.generateRsaKeyPair(2048)
             val pairC = CryptoUtil.generateRsaKeyPair(2048)
             transaction {
+                val maybeHost = EbicsHostEntity.find {
+                    EbicsHostsTable.hostID eq req.hostID
+                }.firstOrNull()
+                if (maybeHost != null) {
+                    logger.info("EBICS host '${req.hostID}' exists already, 
this request conflicts.")
+                    throw conflict("EBICS host '${req.hostID}' exists already")
+                }
                 EbicsHostEntity.new {
                     this.ebicsVersion = req.ebicsVersion
                     this.hostId = req.hostID
@@ -1091,7 +1123,7 @@ val sandboxApp: Application.() -> Unit = {
                     }
                     logger.debug("TWG add-incoming passed authentication")
                     val body = try {
-                        call.receiveJson<TWGAdminAddIncoming>()
+                        call.receive<TWGAdminAddIncoming>()
                     } catch (e: Exception) {
                         logger.error("/admin/add-incoming failed at parsing 
the request body")
                         throw SandboxError(
@@ -1133,7 +1165,7 @@ val sandboxApp: Application.() -> Unit = {
                 }
                 post("/withdrawal-operation/{wopid}") {
                     val wopid: String = ensureNonNull(call.parameters["wopid"])
-                    val body = call.receiveJson<TalerWithdrawalSelection>()
+                    val body = call.receive<TalerWithdrawalSelection>()
                     val transferDone = transaction {
                         val wo = TalerWithdrawalEntity.find {
                             TalerWithdrawalsTable.wopid eq 
java.util.UUID.fromString(wopid)
@@ -1201,7 +1233,7 @@ val sandboxApp: Application.() -> Unit = {
             route("/access-api") {
                 post("/accounts/{account_name}/transactions") {
                     val bankAccount = getBankAccountWithAuth(call)
-                    val req = call.receiveJson<NewTransactionReq>()
+                    val req = call.receive<NewTransactionReq>()
                     val payto = parsePayto(req.paytoUri)
                     val amount: String? = payto.amount ?: req.amount
                     if (amount == null) throw badRequest("Amount is missing")
@@ -1259,7 +1291,7 @@ val sandboxApp: Application.() -> Unit = {
                     if (maybeOwnedAccount.owner != username && WITH_AUTH) 
throw unauthorized(
                         "Customer '$username' has no rights over bank account 
'${maybeOwnedAccount.label}'"
                     )
-                    val req = call.receiveJson<WithdrawalRequest>()
+                    val req = call.receive<WithdrawalRequest>()
                     // Check for currency consistency
                     val amount = parseAmount(req.amount)
                     if (amount.currency != demobank.currency)
@@ -1294,21 +1326,23 @@ val sandboxApp: Application.() -> Unit = {
                             -1
                         )
                         host = "withdraw"
-                        pathComponents(
-                            /**
-                             * encodes the hostname(+port) of the actual
-                             * bank that will serve the withdrawal request.
-                             */
-                            baseUrl.host.plus(
-                                if (baseUrl.port != -1)
-                                    ":${baseUrl.port}"
-                                else ""
-                            ),
-                            baseUrl.path, // has x-forwarded-prefix, or single 
slash.
-                            "demobanks",
-                            demobank.name,
-                            "integration-api",
-                            wo.wopid.toString()
+                        this.appendPathSegments(
+                            listOf(
+                                /**
+                                 * encodes the hostname(+port) of the actual
+                                 * bank that will serve the withdrawal request.
+                                 */
+                                baseUrl.host.plus(
+                                    if (baseUrl.port != -1)
+                                        ":${baseUrl.port}"
+                                    else ""
+                                ),
+                                baseUrl.path, // has x-forwarded-prefix, or 
single slash.
+                                "demobanks",
+                                demobank.name,
+                                "integration-api",
+                                wo.wopid.toString()
+                            )
                         )
                     }
                     call.respond(object {
@@ -1516,69 +1550,28 @@ val sandboxApp: Application.() -> Unit = {
                             "The bank doesn't allow new registrations at the 
moment."
                         )
                     }
-                    val req = call.receiveJson<CustomerRegistration>()
-                    // Forbid 'admin' or 'bank' usernames.
-                    if (req.username == "bank" || req.username == "admin")
-                        throw forbidden("Unallowed username: ${req.username}")
-                    val checkCustomerExist = transaction {
-                        DemobankCustomerEntity.find {
-                            DemobankCustomersTable.username eq req.username
-                        }.firstOrNull()
-                    }
-                    /**
-                     * Not allowing 'bank' username, as it's been assigned
-                     * to the default bank's bank account.
-                     */
-                    if (checkCustomerExist != null) {
-                        throw SandboxError(
-                            HttpStatusCode.Conflict,
-                            "Username ${req.username} not available."
-                        )
-                    }
-                    val newIban = req.iban ?: getIban()
-                    // Double-check if IBAN was taken already.
-                    val checkIbanExist = transaction {
-                        BankAccountEntity.find(BankAccountsTable.iban eq 
newIban).firstOrNull()
-                    }
-                    if (checkIbanExist != null)
-                        throw conflict("Proposed IBAN not available.")
-
-                    // Create new customer.
-                    requireValidResourceName(req.username)
-                    val bankAccount = transaction {
-                        val bankAccount = BankAccountEntity.new {
-                            iban = newIban
-                            /**
-                             * For now, keep same semantics of Pybank: a 
username
-                             * is AS WELL a bank account label.  In other 
words, it
-                             * identifies a customer AND a bank account.
-                             */
-                            label = req.username
-                            owner = req.username
-                            this.demoBank = demobank
+                    val req = call.receive<CustomerRegistration>()
+                    val newAccount = transaction {
+                        insertNewAccount(
+                            req.username,
+                            req.password,
+                            name = req.name,
+                            iban = req.iban,
                             isPublic = req.isPublic
-                        }
-                        DemobankCustomerEntity.new {
-                            username = req.username
-                            passwordHash = CryptoUtil.hashpw(req.password)
-                            name = req.name // nullable
-                        }
-                        if (demobank.withSignupBonus)
-                            bankAccount.bonus("${demobank.currency}:100")
-                        bankAccount
+                        )
                     }
-                    val balance = getBalance(bankAccount, withPending = true)
+                    val balance = getBalance(newAccount.bankAccount, 
withPending = true)
                     call.respond(object {
                         val balance = object {
-                            val amount = "${demobank.currency}:$balance"
-                            val credit_debit_indicator = "CRDT"
+                            val amount = 
"${demobank.currency}:${balance.abs()}"
+                            val credit_debit_indicator = if (balance < 
BigDecimal.ZERO) "DBIT" else "CRDT"
                         }
                         val paytoUri = buildIbanPaytoUri(
-                            iban = bankAccount.iban,
-                            bic = bankAccount.bic,
+                            iban = newAccount.bankAccount.iban,
+                            bic = newAccount.bankAccount.bic,
                             receiverName = 
getPersonNameFromCustomer(req.username)
                         )
-                        val iban = bankAccount.iban
+                        val iban = newAccount.bankAccount.iban
                     })
                     return@post
                 }
@@ -1592,7 +1585,7 @@ val sandboxApp: Application.() -> Unit = {
                     // Only the admin can create Ebics subscribers.
                     val user = call.request.basicAuth()
                     if (user != "admin") throw forbidden("Only the Admin can 
create Ebics subscribers.")
-                    val body = call.receiveJson<EbicsSubscriberInfo>()
+                    val body = call.receive<EbicsSubscriberInfo>()
                     // Create or get the Ebics subscriber that is found.
                     transaction {
                         val subscriber: EbicsSubscriberEntity = 
EbicsSubscriberEntity.find {
@@ -1633,7 +1626,7 @@ fun serverMain(port: Int, localhostOnly: Boolean, 
ipv4Only: Boolean) {
                 this.port = port
                 this.host = if (localhostOnly) "[::1]" else "[::]"
             }
-            parentCoroutineContext = Dispatchers.Main
+            // parentCoroutineContext = Dispatchers.Main
             module(sandboxApp)
         },
         configure = {
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLEbicsConverter.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLEbicsConverter.kt
index 4f80f96c..bfd95bc1 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLEbicsConverter.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLEbicsConverter.kt
@@ -1,27 +1,25 @@
 package tech.libeufin.sandbox
 
-import io.ktor.application.*
-import io.ktor.features.*
 import io.ktor.http.*
 import io.ktor.http.content.*
-import io.ktor.request.*
-import io.ktor.response.*
-import io.ktor.util.pipeline.*
+import io.ktor.serialization.*
+import io.ktor.util.reflect.*
 import io.ktor.utils.io.*
+import io.ktor.utils.io.charsets.*
 import io.ktor.utils.io.jvm.javaio.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import tech.libeufin.util.XMLUtil
-import java.io.OutputStream
-import java.nio.channels.ByteChannel
 
 class XMLEbicsConverter : ContentConverter {
-    override suspend fun convertForReceive(
-        context: PipelineContext<ApplicationReceiveRequest, ApplicationCall>): 
Any? {
-        val value = context.subject.value as? ByteReadChannel ?: return null
+    override suspend fun deserialize(
+        charset: io.ktor.utils.io.charsets.Charset,
+        typeInfo: io.ktor.util.reflect.TypeInfo,
+        content: ByteReadChannel
+    ): Any? {
         return withContext(Dispatchers.IO) {
             try {
-                
receiveEbicsXmlInternal(value.toInputStream().reader().readText())
+                
receiveEbicsXmlInternal(content.toInputStream().reader().readText())
             } catch (e: Exception) {
                 throw SandboxError(
                     HttpStatusCode.BadRequest,
@@ -30,11 +28,28 @@ class XMLEbicsConverter : ContentConverter {
             }
         }
     }
-    override suspend fun convertForSend(
-        context: PipelineContext<Any, ApplicationCall>,
+
+    // The following annotation was suggested by Intellij.
+    @Deprecated(
+        "Please override and use serializeNullable instead",
+        replaceWith = ReplaceWith("serializeNullable(charset, typeInfo, 
contentType, value)"),
+        level = DeprecationLevel.WARNING
+    )
+    override suspend fun serialize(
         contentType: ContentType,
+        charset: Charset,
+        typeInfo: TypeInfo,
         value: Any
-    ): Any? {
+    ): OutgoingContent? {
+        return super.serializeNullable(contentType, charset, typeInfo, value)
+    }
+
+    override suspend fun serializeNullable(
+        contentType: ContentType,
+        charset: Charset,
+        typeInfo: TypeInfo,
+        value: Any?
+    ): OutgoingContent? {
         val conv = try {
             XMLUtil.convertJaxbToString(value)
         } catch (e: Exception) {
@@ -42,7 +57,6 @@ class XMLEbicsConverter : ContentConverter {
              * Not always a error: the content negotiation might have
              * only checked if this handler could convert the response.
              */
-            // logger.info("Could not use XML custom converter for this 
response.")
             return null
         }
         return OutputStreamContent({
@@ -50,7 +64,7 @@ class XMLEbicsConverter : ContentConverter {
             withContext(Dispatchers.IO) {
                 out.write(conv.toByteArray())
             }},
-            contentType.withCharset(context.call.suitableCharset())
+            contentType.withCharset(charset)
         )
     }
 }
\ No newline at end of file
diff --git a/util/build.gradle b/util/build.gradle
index 06d1d18c..aa2334fb 100644
--- a/util/build.gradle
+++ b/util/build.gradle
@@ -26,12 +26,9 @@ sourceSets {
     main.java.srcDirs = ['src/main/java', 'src/main/kotlin']
 }
 
-def exposed_version = '0.32.1'
 def netty_version = '4.1.68.Final'
-def ktor_version = '1.6.1'
 
 dependencies {
-    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21'
     implementation 'io.ktor:ktor-server-netty:1.6.1'
     implementation 'ch.qos.logback:logback-classic:1.2.5'
 
@@ -53,8 +50,9 @@ dependencies {
     implementation "io.netty:netty-all:$netty_version"
     implementation "io.netty:netty-transport-native-epoll:$netty_version"
     implementation "io.ktor:ktor-server-test-host:$ktor_version"
-    implementation "io.ktor:ktor-jackson:$ktor_version"
+    implementation "io.ktor:ktor-serialization-jackson:$ktor_version"
 
+    testImplementation "io.ktor:ktor-server-content-negotiation:$ktor_version"
     testImplementation group: 'junit', name: 'junit', version: '4.13.2'
     testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21'
     testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21'
diff --git a/util/src/main/kotlin/Config.kt b/util/src/main/kotlin/Config.kt
index 11d71564..2b1da1ac 100644
--- a/util/src/main/kotlin/Config.kt
+++ b/util/src/main/kotlin/Config.kt
@@ -4,7 +4,7 @@ import ch.qos.logback.classic.Level
 import ch.qos.logback.classic.LoggerContext
 import ch.qos.logback.classic.util.ContextInitializer
 import ch.qos.logback.core.util.Loader
-import io.ktor.application.*
+import io.ktor.server.application.*
 import io.ktor.util.*
 import org.slf4j.LoggerFactory
 import printLnErr
@@ -63,12 +63,6 @@ fun setLogLevel(logLevel: String?) {
     }
 }
 
-internal fun <T : Any>ApplicationCall.maybeAttribute(name: String): T? {
-    val key = AttributeKey<T>("name")
-    if (!this.attributes.contains(key)) return null
-    return this.attributes[key]
-}
-
 /**
  * Retun the attribute, or throw 500 Internal server error.
  */
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
index 647a331f..6ba839fe 100644
--- a/util/src/main/kotlin/HTTP.kt
+++ b/util/src/main/kotlin/HTTP.kt
@@ -1,12 +1,12 @@
 package tech.libeufin.util
 
 import UtilError
-import io.ktor.application.*
 import io.ktor.http.*
-import io.ktor.request.*
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.util.*
 import io.ktor.util.*
 import logger
-import org.jetbrains.exposed.sql.transactions.transaction
 import java.net.URLDecoder
 
 fun unauthorized(msg: String): UtilError {
@@ -85,9 +85,8 @@ fun conflict(msg: String): UtilError {
 fun ApplicationRequest.getBaseUrl(): String {
     return if (this.headers.contains("X-Forwarded-Host")) {
         logger.info("Building X-Forwarded- base URL")
-        /**
-         * FIXME: should tolerate a missing X-Forwarded-Prefix.
-         */
+
+        // FIXME: should tolerate a missing X-Forwarded-Prefix.
         var prefix: String = this.headers.get("X-Forwarded-Prefix")
             ?: throw internalServerError("Reverse proxy did not define 
X-Forwarded-Prefix")
         if (!prefix.endsWith("/"))
@@ -100,8 +99,8 @@ fun ApplicationRequest.getBaseUrl(): String {
             host = this.headers.get("X-Forwarded-Host") ?: throw 
internalServerError(
                 "Reverse proxy did not define X-Forwarded-Host"
             ),
-            encodedPath = prefix
         ).apply {
+            encodedPath = prefix
             // Gets dropped otherwise.
             if (!encodedPath.endsWith("/"))
                 encodedPath += "/"
@@ -138,7 +137,7 @@ fun expectAdmin(username: String?) {
     if (username != "admin") throw unauthorized("Only admin allowed: $username 
is not.")
 }
 
-fun getHTTPBasicAuthCredentials(request: ApplicationRequest): Pair<String, 
String> {
+fun getHTTPBasicAuthCredentials(request: 
io.ktor.server.request.ApplicationRequest): Pair<String, String> {
     val authHeader = getAuthorizationHeader(request)
     return extractUserAndPassword(authHeader)
 }
@@ -168,7 +167,6 @@ fun buildBasicAuthLine(username: String, password: String): 
String {
  * 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 {
         // FIXME/note: line below doesn't check for "Basic" presence.
         val split = authorizationHeader.split(" ")
@@ -181,7 +179,7 @@ fun extractUserAndPassword(authorizationHeader: String): 
Pair<String, String> {
     } catch (e: Exception) {
         throw UtilError(
             HttpStatusCode.BadRequest,
-            "invalid Authorization:-header received: ${e.message}",
+            "invalid Authorization header received: ${e.message}",
             LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
         )
     }
diff --git a/util/src/main/kotlin/UnixDomainSocket.kt 
b/util/src/main/kotlin/UnixDomainSocket.kt
index 0bcedb8b..eaf187eb 100644
--- a/util/src/main/kotlin/UnixDomainSocket.kt
+++ b/util/src/main/kotlin/UnixDomainSocket.kt
@@ -1,4 +1,6 @@
-import io.ktor.application.*
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.server.application.*
 import io.ktor.client.statement.*
 import io.ktor.http.*
 import io.ktor.http.HttpHeaders
@@ -67,30 +69,24 @@ class LibeufinHttpInit(
 class LibeufinHttpHandler(
     private val app: Application.() -> Unit
 ) : SimpleChannelInboundHandler<FullHttpRequest>() {
-    @OptIn(EngineAPI::class)
+    // @OptIn(EngineAPI::class)
     override fun channelRead0(ctx: ChannelHandlerContext, msg: 
FullHttpRequest) {
-        withTestApplication(app) {
+        testApplication {
+            application(app)
             val httpVersion = msg.protocolVersion()
             // Proxying the request to Ktor API.
-            val call = handleRequest {
-                msg.headers().forEach { addHeader(it.key, it.value) }
+            val r = client.request(msg.uri()) {
+                expectSuccess = false
                 method = HttpMethod(msg.method().name())
-                uri = msg.uri()
-                version = httpVersion.text()
                 setBody(ByteBufInputStream(msg.content()).readAllBytes())
             }
-            val statusCode: Int = call.response.status()?.value ?: throw 
UtilError(
-                HttpStatusCode.InternalServerError,
-                "app proxied via Unix domain socket did not include a response 
status code",
-                ec = null // FIXME: to be defined.
-            )
             // Responding to Netty API.
             val response = DefaultHttpResponse(
                 httpVersion,
-                HttpResponseStatus.valueOf(statusCode)
+                HttpResponseStatus.valueOf(r.status.value)
             )
             var chunked = false
-            call.response.headers.allValues().forEach { s, list ->
+            r.headers.forEach { s, list ->
                 if (s == HttpHeaders.TransferEncoding && 
list.contains("chunked"))
                     chunked = true
                 response.headers().set(s, list.joinToString())
@@ -100,12 +96,12 @@ class LibeufinHttpHandler(
                 ctx.writeAndFlush(
                     HttpChunkedInput(
                         ChunkedStream(
-                            ByteArrayInputStream(call.response.byteContent)
+                            ByteArrayInputStream(r.readBytes())
                         )
                     )
                 )
             } else {
-                
ctx.writeAndFlush(Unpooled.wrappedBuffer(call.response.byteContent))
+                ctx.writeAndFlush(Unpooled.wrappedBuffer(r.readBytes()))
             }
         }
     }
diff --git a/util/src/test/kotlin/DomainSocketTest.kt 
b/util/src/test/kotlin/DomainSocketTest.kt
index 73e2cddb..8a3b99b0 100644
--- a/util/src/test/kotlin/DomainSocketTest.kt
+++ b/util/src/test/kotlin/DomainSocketTest.kt
@@ -3,16 +3,16 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
 import com.fasterxml.jackson.databind.DeserializationFeature
 import com.fasterxml.jackson.databind.SerializationFeature
 import com.fasterxml.jackson.module.kotlin.KotlinModule
-import io.ktor.application.*
-import io.ktor.features.*
+import io.ktor.server.application.*
 import io.ktor.http.*
-import io.ktor.response.*
-import io.ktor.routing.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
 import org.junit.Test
-import io.ktor.jackson.jackson
-import io.ktor.request.*
+import io.ktor.serialization.jackson.*
+import io.ktor.server.request.*
 import org.junit.Assert
 import org.junit.Ignore
+import io.ktor.server.plugins.contentnegotiation.*
 
 class DomainSocketTest {
     @Test @Ignore

-- 
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]